性能文章>【全网首发】一次NSF FeignClient支持Apache HttpClient的优化>

【全网首发】一次NSF FeignClient支持Apache HttpClient的优化原创

350055

背景介绍

NSF(Netease Service Framework)是网易数帆下的一款微服务框架,项目在压测过程中,发现NSF FeignClient有性能瓶颈,下面对遇到的问题及优化方案进行分析,以备忘。

问题分析

在压测过程中发现系统耗时高,通过arthas thread -b发现大量线程处于阻塞状态,进一步分析发现NSF FeignClient使用的java.net.HttpURLConnection,而NSF FeignClient当前版本没有方便的方式(比如改个配置)来支持其他HttpClient,以下通过对NSF FeignClient实现分析来尝试解决这个问题。

NSF FeignClient实现方式

NSF是通过nsf-agent将自定义的FeignClient注入到Spring容器中的,下面是NSF FeignClient的一些主要实现逻辑。

SpringConfigurationTransformer

该类向Spring注入了一些Configuration组件。


public class SpringConfigurationTransformer implements NoneNamedTransformer {
  private static Logger logger = AgentLogFactory.getLogger(SpringConfigurationTransformer.class);
  
  public TransformerResult transform(ClassLoader paramClassLoader, String paramString, Class<?> paramClass, ProtectionDomain paramProtectionDomain, byte[] paramArrayOfByte, Object paramObject) throws Exception {
    ClassPool classPool = ClassPool.getDefault();
    InputStream ins = new ByteArrayInputStream(paramArrayOfByte);
    CtClass ctclass = classPool.makeClass(ins);
    TransformerResult result = new TransformerResult(ctclass);
    result.setClassModified(Boolean.valueOf(true));
    logger.debug("nsf-agent: SpringConfigurationTransformer process transform ...");
    List<String> importConfigurationList = new ArrayList<>();
    importConfigurationList.add("com.netease.cloud.nsf.agent.http.configuration.SpringContextConfiguration");
    importConfigurationList.add("com.netease.cloud.nsf.agent.http.configuration.NsfRegistryConfiguration");
    importConfigurationList.add("com.netease.cloud.nsf.agent.http.configuration.SpringFeignConfiguration");
    importConfigurationList.add("com.netease.cloud.nsf.agent.http.configuration.SpringRibbonConfiguration");
    importConfigurationList.add("com.netease.cloud.nsf.agent.http.configuration.ServletFilterConfiguration");
    importConfigurationList.add("com.netease.cloud.nsf.agent.http.configuration.ToolsConfiguration");
    importConfigurationList.add("com.netease.cloud.nsf.agent.http.configuration.NacosToolsConfiguration");
    importConfigurationList.add("com.netease.cloud.nsf.agent.core.autoconfig.ConfigDebugConfiguration");
    importConfigurationList.add("com.netease.cloud.nsf.agent.http.configuration.NsfSpringDiscoveryClientConfiguration");
    JavassistUtil.addImportAnnotations(ctclass, importConfigurationList);
    result.setClassCodeBytes(ctclass.toBytecode());
    ctclass.defrost();
    return result;
  }
  
  public Object classNameMatch(String paramString) {
    if (paramString.equalsIgnoreCase(GlobalConfigManager.getSpringBootClassName()))
      return Boolean.valueOf(true); 
    return null;
  }
}

与该问题直接相关的是SpringFeignConfiguration。

SpringFeignConfiguration

将自定义的feignClient相关配置注入Spring。

@Configuration
@ConditionalOnClass(name = {"feign.Client", "org.springframework.cloud.openfeign.FeignClient"})
public class SpringFeignConfiguration {
  private Logger logger = AgentLogFactory.getLogger(SpringFeignConfiguration.class);
  @PostConstruct
  public void initFeign() {
    this.logger.info("find feign, init NsfFeignClient ...");
  }
  @Bean(name = {"feignClient"})
  @ConditionalOnClass(name = {"org.springframework.cloud.openfeign.ribbon.LoadBalancerFeignClient"})
  public Client loadBalancerFeignClient(RestTemplate nsfMethodRestTemplate) {
    return (Client)new NsfLoadBalancerFeignClient(nsfMethodRestTemplate);
  }
  
  @Bean(name = {"feignClient"})
  @ConditionalOnMissingClass({"org.springframework.cloud.openfeign.ribbon.LoadBalancerFeignClient"})
  public NsfFeignClient feignClient(RestTemplate nsfMethodRestTemplate) {
    return new NsfFeignClient(nsfMethodRestTemplate);
  }
}

在运行环境中,加载的是NsfLoadBalancerFeignClient。

NsfLoadBalancerFeignClient

public class NsfLoadBalancerFeignClient extends LoadBalancerFeignClient implements Client {
  private NsfFeignClient feignClient;
  public NsfLoadBalancerFeignClient(RestTemplate restTemplate) {
    super((Client)new Client.Default(null, null), null, null);
    this.feignClient = new NsfFeignClient(restTemplate);
  }
  public Response execute(Request request, Request.Options options) throws IOException {
    return this.feignClient.execute(request, options);
  }
}

我们接着查看NsfFeignClient的实现。

NsfFeignClient

public class NsfFeignClient implements Client {
  private static final String ACCEPT_HEADER_NAME = "Accept";
  private ClientHttpRequestFactory requestFactory;
  public NsfFeignClient(RestTemplate restTemplate) {
    this.requestFactory = (ClientHttpRequestFactory)new InterceptingClientHttpRequestFactory((ClientHttpRequestFactory)new NsfClientHttpRequestFactory(), restTemplate.getInterceptors());
  }
  ... ...
}

其中requestFactory是通过NsfClientHttpRequestFactory进行初始化的,也是问题的关键所在,下面看下NsfClientHttpRequestFactory的实现。

NsfClientHttpRequestFactory

public class NsfClientHttpRequestFactory extends SimpleClientHttpRequestFactory {
  public static final ThreadLocal<TimeOut> timeoutCache = new ThreadLocal<TimeOut>() {
      protected NsfClientHttpRequestFactory.TimeOut initialValue() {
        return new NsfClientHttpRequestFactory.TimeOut();
      }
    };
  ... ...
}

NsfClientHttpRequestFactory继承了Spring中的SimpleClientHttpRequestFactory,下面看下SimpleClientHttpRequestFactory中创建连接的逻辑。

SimpleClientHttpRequestFactory
@Override
public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException {
	HttpURLConnection connection = openConnection(uri.toURL(), this.proxy);
	prepareConnection(connection, httpMethod.name());

	if (this.bufferRequestBody) {
		return new SimpleBufferingClientHttpRequest(connection, this.outputStreaming);
	}
	else {
		return new SimpleStreamingClientHttpRequest(connection, this.chunkSize, this.outputStreaming);
	}
}
protected HttpURLConnection openConnection(URL url, @Nullable Proxy proxy) throws IOException {
	URLConnection urlConnection = (proxy != null ? url.openConnection(proxy) : url.openConnection());
	if (!(urlConnection instanceof HttpURLConnection)) {
		throw new IllegalStateException(
			"HttpURLConnection required for [" + url + "] but got: " + urlConnection);
	}
	return (HttpURLConnection) urlConnection;
}

其中创建连接的逻辑在openConnection方法中,使用HttpURLConnection创建的连接。

优化方案

通过问题分析知道了问题所在,优化方案就比较简单了,只要想办法替换掉java.net.HttpURLConnection就可以了,为了尽可能小的影响NSF FeignClient已有逻辑,我们仿照NSF定义一套相应的类,只需要替换掉java.net.HttpURLConnection的逻辑就可以了。

自定义FeignClient

仿照现有NSF FeignClient单独写一套即可,关键是替换掉java.net.HttpURLConnection。

替换NSF feignClient

通过BeanDefinitionRegistry使用我们自定义的feignClient替换掉NSF feignClient。

public class DefaultBeanDefinitionRegistryPostProcessor implements BeanDefinitionRegistryPostProcessor {
    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry beanDefinitionRegistry) throws BeansException {
        String[] beanDefinitionNames = beanDefinitionRegistry.getBeanDefinitionNames();
        for(String beanName : beanDefinitionNames){
            if(beanName.equals("feignClient")){
                BeanDefinition wlFeignClient = beanDefinitionRegistry.getBeanDefinition("wlFeignClient");
                if(wlFeignClient != null) {
                    beanDefinitionRegistry.removeBeanDefinition("feignClient");
                    beanDefinitionRegistry.removeBeanDefinition("wlFeignClient");
                    beanDefinitionRegistry.registerBeanDefinition("feignClient", wlFeignClient);
                }
                break;
            }
        }
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {
    }
}

总结

通过arthas定位出问题原因,通过分析搞清楚NSF FeignClient的实现,然后再实现一套支持Apache HttpClient的方案来替换原来的HttpURLConnection,经过测试NSF FeignClient已经不再成为阻塞点了。

点赞收藏
大禹的足迹

在阿里搬了几年砖的大龄码农,头条号:大禹的足迹

请先登录,查看5条精彩评论吧
快去登录吧,你将获得
  • 浏览更多精彩评论
  • 和开发者讨论交流,共同进步

为你推荐

JDBC PreparedStatement 字段值为null导致TBase带宽飙升的案例分析

JDBC PreparedStatement 字段值为null导致TBase带宽飙升的案例分析

随机一门技术分享之Netty

随机一门技术分享之Netty

MappedByteBuffer VS FileChannel:从内核层面对比两者的性能差异

MappedByteBuffer VS FileChannel:从内核层面对比两者的性能差异

5
5