一个Eureka服务可以注册多少个服务实例?


Spring Cloud Eureka是Spring Cloud Netflix微服务套件中的一部分,基于Netflix Eureka做了二次封装,主要负责完成微服务架构中的服务治理功能。出于好奇想知道Eureka可以注册的最大服务实例数是多少,于是有了下面的测试。

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

eureka整体架构图(图片来源网络)

可以看到eueka按逻辑上可以划分为3个模块:
eureka-server、eureka-client-service-provider、eureka-client-service-consumer。

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

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

client端:

主要做了如下几件事:

  • 服务注册(Registry)——初始化时执行一次,向服务端注册自己服务实例节点信息包括ip、端口、实例名等,基于POST请求。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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一次,保证当前服务节点状态信息实时更新,不被服务端失效剔除。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
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 ? : response.getStatus());

}

if (response !=null ) {

response.close();
}
}
}
  • 更新已经注册服务列表(fetchRegistry)——每隔30s从服务端GET一次增量版本信息,然后和本地比较并合并,保证本地能获取到其他节点最新注册信息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

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的时候,需要及时通知服务端把自己剔除,以避免客户端调用已经下线的服务。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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配置改成如下配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@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已经接近满负载,操作系统本身调度已经压到极限,于是结束了本次测试。

注意:有一种误解,就是我们常说一台机器有65536个端口,那么承载的连接数就是65536个,这个说法是极其错误的,这就混淆了源端口和访问目标端口。系统是通过一个四元组来标识一个TCP连接,(src_ip,src_port,dst_ip,dst_port)即源IP、源端口、目标IP、目标端口。比如我们有一台服务192.168.0.1,开启端口80.那么所有的客户端都会连接到这台服务的80端口上面,我们做压测的时候,利用压测客户端,这个客户端的连接数是受到端口数的限制,但是服务器上面的连接数可以达到成千上万个,一般可以达到百万(4C8G配置),至于上限是多少,需要看其自身优化的程度(可通过操作系统的文件句柄数量限制、TCP参数进行调优),但是参数值并不是设置的越大越好,有的需要考虑服务器的硬件配置,参数对服务器上其它服务的影响等。

结论: 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/