欢迎访问Spring Cloud中国社区

《重新定义Spring Cloud实战》由Spring Cloud中国社区倾力打造,基于Spring Cloud的Finchley.RELEASE版本,本书内容宽度足够广、深度足够深,而且立足于生产实践,直接从生产实践出发,包含大量生产实践的配置。欢迎加微信Software_King进群答疑,国内谁在使用Spring Cloud?欢迎登记

Eureka性能测试

xujin · 5年前 · 12931 ·

Eureka性能测试

Spring Cloud Eureka是Spring Cloud Netflix微服务套件之一,基于Netflix Eureka做了二次封装,主要负责完成微服务架构中的服务治理功能。我们试图在公司推动统一的的服务注册框架,必须非常情况Eureka性能的优劣。

一、Eureka服务注册与发现相关源码介绍

Eureka按逻辑上可以划分为3个模块:

  • eureka-server:服务端,提供服务注册和发现。
  • eureka-client-service-provider:客户端,服务提供者,通过http rest告知服务端注册,更新,取消服务。
  • eureka-client-service-consumer:客户端,服务消费者,通过http rest从服务端获取需要服务的地址列表,然后配合一些负载均衡策略(ribbon)来调用服务端服务。

对于服务注册中心、服务提供者、服务消费者这三个主要元素来说,后者(Eureka客户端)在整个运行机制中是大部分通信行为的主动发起者,而注册中心主要是处理请求的接收者。所以以下会我们会分别基于Eureka客户端、服务端入手看看他们是如何完成这些行为的。

二、Client端:

主要步骤如下:

  • 服务注册(Registry)——初始化时执行一次,向服务端注册自己服务实例节点信息包括ip、端口、实例名等,基于POST请求。
  public EurekaHttpResponse<Void> register(InstanceInfo info) {
        String urlPath = "apps/" + info.getAppName();
        ClientResponse response = null;
        try {
            Builder resourceBuilder = jerseyClient.resource(serviceUrl).path(urlPath).getRequestBuilder();
            addExtraHeaders(resourceBuilder);
            response = resourceBuilder
                    .header("Accept-Encoding", "gzip")
                    .type(MediaType.APPLICATION_JSON_TYPE)
                    .accept(MediaType.APPLICATION_JSON)
                    .post(ClientResponse.class, info);
            return anEurekaHttpResponse(response.getStatus()).headers(headersOf(response)).build();
        } finally {
            if (logger.isDebugEnabled()) {
                logger.debug("Jersey HTTP POST {}/{} with instance {}; statusCode={}", serviceUrl, urlPath, info.getId(),
                        response == null ? "N/A" : response.getStatus());
            }
            if (response != null) {
                response.close();
            }
        }
    }
  • 服务续约(renew)——默认每隔30s向服务端PUT一次,保证当前服务节点状态信息实时更新,不被服务端失效剔除。
public EurekaHttpResponse<InstanceInfo> sendHeartBeat(String appName, String id, InstanceInfo info, InstanceStatus overriddenStatus) {
        String urlPath = "apps/" + appName + '/' + id;
        ClientResponse response = null;
        try {
            WebResource webResource = jerseyClient.resource(serviceUrl)
                    .path(urlPath)
                    .queryParam("status", info.getStatus().toString())
                    .queryParam("lastDirtyTimestamp", info.getLastDirtyTimestamp().toString());
            if (overriddenStatus != null) {
                webResource = webResource.queryParam("overriddenstatus", overriddenStatus.name());
            }
            Builder requestBuilder = webResource.getRequestBuilder();
            addExtraHeaders(requestBuilder);
            response = requestBuilder.put(ClientResponse.class);
            EurekaHttpResponseBuilder<InstanceInfo> eurekaResponseBuilder = anEurekaHttpResponse(response.getStatus(), InstanceInfo.class).headers(headersOf(response));
            if (response.hasEntity()) {
                eurekaResponseBuilder.entity(response.getEntity(InstanceInfo.class));
            }
            return eurekaResponseBuilder.build();
        } finally {
            if (logger.isDebugEnabled()) {
                logger.debug("Jersey HTTP PUT {}/{}; statusCode={}", serviceUrl, urlPath, response == null ? "N/A" : response.getStatus());
            }
            if (response != null) {
                response.close();
            }
        }
    }
  • 更新已经注册服务列表(fetchRegistry)——默认每隔30s从服务端GET一次增量版本信息,然后和本地比较并合并,保证本地能获取到其他节点最新注册信息。
 private EurekaHttpResponse<InstanceInfo> getInstanceInternal(String urlPath) {
        ClientResponse response = null;
        try {
            Builder requestBuilder = jerseyClient.resource(serviceUrl).path(urlPath).getRequestBuilder();
            addExtraHeaders(requestBuilder);
            response = requestBuilder.accept(MediaType.APPLICATION_JSON_TYPE).get(ClientResponse.class);

            InstanceInfo infoFromPeer = null;
            if (response.getStatus() == Status.OK.getStatusCode() && response.hasEntity()) {
                infoFromPeer = response.getEntity(InstanceInfo.class);
            }
            return anEurekaHttpResponse(response.getStatus(), InstanceInfo.class)
                    .headers(headersOf(response))
                    .entity(infoFromPeer)
                    .build();
        } finally {
            if (logger.isDebugEnabled()) {
                logger.debug("Jersey HTTP GET {}/{}; statusCode={}", serviceUrl, urlPath, response == null ? "N/A" : response.getStatus());
            }
            if (response != null) {
                response.close();
            }
        }
    }
  • 服务下线(cancel)——在服务shutdown的时候,需要及时通知服务端把自己剔除,以避免客户端调用已经下线的服务。
    public EurekaHttpResponse<Void> cancel(String appName, String id) {
        String urlPath = "apps/" + appName + '/' + id;
        ClientResponse response = null;
        try {
            Builder resourceBuilder = jerseyClient.resource(serviceUrl).path(urlPath).getRequestBuilder();
            addExtraHeaders(resourceBuilder);
            response = resourceBuilder.delete(ClientResponse.class);
            return anEurekaHttpResponse(response.getStatus()).headers(headersOf(response)).build();
        } finally {
            if (logger.isDebugEnabled()) {
                logger.debug("Jersey HTTP DELETE {}/{}; statusCode={}", serviceUrl, urlPath, response == null ? "N/A" : response.getStatus());
            }
            if (response != null) {
                response.close();
            }
        }
    }

三、Server端:

我们知道eureka client是通过Jersey Client基于Http协议与eureka server交互来注册服务、续约服务、取消服务、服务查询等。同时,Server端还会维护一份服务实例清单,并每隔90s对未续约的实例进行失效剔除。所以,eureka server肯定要提供上述http的服务端的Jersey Server实现,由于此次测试针对客户端模拟,服务端对应接口就先不在这描述了。

四、测试过程

  • 测试工具:
工具选项 描述 配置/能力
测试机器 CentOS release 5.4 (Final) 3台 8c16g,open files:65535,max user processes:65535
Eureka服务端 单机部署,boot版本(Dalston.SR5) -XX:MaxHeapSize=4g(默认)
Wireshark Windows平台抓包工具 抓取HTTP、TCP等报文内容、协议相关信息
UAV监控 公司自研监控平台 实时监控采集应用性能指标
  • 测试方案:
    1、先启动多组Eureka客户端并用Wireshark抓取其真实请求,然后结合Eureka源码分析其调用逻辑关系(基于TCP短链接交互)。
    2、根据源码,将客户端http调用方式进行池化,即每笔实例注册流程:(注册、获取实例、续约)等调用请求统一从连接池获取连接,获取实例过程为每次获取delta(增量)。
    3、每笔流程完成后sleep0.5s,保证所有节点续约、获取实例(间隔30s)的频率对服务端负载均匀。
    4、串行模拟整个流程,每完成500笔,整体观察5分钟记录服务端cpu、内存、线程、连接数等信息。直到服务端或客户端出现大量异常(超时、失效剔除等可能异常)则认为到达eureka注册瓶颈。
    5、更改服务端servlet容器配置,尝试进行优化(最大连接数、线程数等)从新开始流程测试直到最优。

                                              模拟测试流程图
  • 数据记录


可以看出到实例注册数到==7000多==时候,连接数不稳定飙到10000左右,同时此时客户端开始大量报错超时,服务端开始拒绝连接:

此时连接数截图详细,可以清楚看到conns达到10000阀值后接着下降:

jstack-F 后显示大量tomcat workThreadPoolExecutor线程block在Socket上:

此时结果和预期猜测一样。MaxConnection=10000,且大于AcceptCount=100时,Tomcat会触发拒绝连接。

而我们使用的spring boot版本使用内嵌Tomcat版本8.5
接着改了服务端tomcat配置改成如下配置:

   @Override
    public void customize(Connector connector) {

        Http11NioProtocol protocol = (Http11NioProtocol) connector.getProtocolHandler();
        // 设置最大连接数
        protocol.setMaxConnections(20000);// 10000
        // 设置最大线程数
        protocol.setMaxThreads(5000);// 200
        // protocol.setConnectionTimeout(1); // 20s
        protocol.setAcceptCount(1000); // 100
        // protocol.setKeepAliveTimeout(1);
        System.out.println("protocol.get:" + protocol.getMaxConnections());
    }
  • 再次从新开始一轮测试,数据记录如下:
实例数 7100 8000
cpu 749% 790%
mem 17.7 18%
conn 12007 13690
Threads 1982 1997

可以看出,在修改了tomcat对应配置,将最大连接数调至20000,线程数调至5000后,Eureka可注册的实例数突破了7000,连接数也突破了10000,实例数注册到8000后才开始报错,看出此时cpu已经接近满负载,操作系统本身调度已经压到极限,于是结束了本次测试。

结论:
Eureka Server服务实例注册量的负载值和操作系统、应用容器本身对应的配置相关,调整操作系统可打开最大文件句柄、进程数,调整应用容器相关最大连接数、线程数、NIO服务器模型引入等等手段都可提高我们应用服务整体吞吐量。

参考资料:
https://www.jianshu.com/p/5ffb71b4c13d
https://tomcat.apache.org/tomcat-8.0-doc/config/http.html
http://yeming.me/2016/12/01/eureka1/

作者:宜信-技术研发中心-开发工程师-王猛