1.服务调用架构图
2.硬件规格
client:MacBook pro(13-inch, 2017,Intel Core i5,8 GB 2133 MHz LPDDR3)scala脚本,并发请求
search服务端:4Core 8GB 4台
ES服务端:16Core 32GB 8台
3.压测记录
3.1 50并发 5分钟
3.2 100并发 5分钟
可以看到并发增加的情况下QPS有所下降,且有9个请求出错了。search服务端和ES服务端的负载并没有明显的增加。
可以推测,此时的瓶颈不在于机器的性能,应该有类似连接池的东西限制了QPS。
由于search服务端使用了jestClient,而jestClient是基于http与ES服务端进行通信的,是否是jestClient中维护了一个连接池呢?
在debug时查看jestClient中的数据结构,发现其内部使用了httpclient
httpclient通过maxTotal和defaultMaxPerRoute来配置连接池
maxTotal:设置连接池的最大连接数,即整个池子的大小。初始值20
defaultMaxPerRoute:设置每一个路由的最大连接数,这里的路由是指IP+PORT,例如连接池大小(MaxTotal)设置为300,路由连接数设置为200(DefaultMaxPerRoute),对于www.a.com与www.b.com两个路由来说,发起服务的主机连接到每个路由的最大连接数(并发数)不能超过200,两个路由的总连接数不能超过300。初始值2.
在defaultMaxPerRoute小于maxTotal时,主要是defaultMaxPerRoute起作用,因为在我这个例子中只有一个 ip+port
在没有配置httpclient的连接池参数时,默认使用初始值。接下来调整参数继续进行测试。
3.3 maxTotal=100, defaultMaxPerRoute=5, 50并发 5分钟
defaultMaxPerRoute变为原来的2.5倍,QPS变为原来的3倍。此时QPS主要受defaultMaxPerRoute参数影响。
search后端和ES后端的负载均由所增加,search后端的负载比ES后端的压力高约10%
3.4 maxTotal=100, defaultMaxPerRoute=5, 100并发 5分钟
线程数增加一倍,QPS只是略有增加,且95线 99线均增加了不少,看起来是defaultMaxPerRoute限制了并且数量。
3.5 maxTotal=100, defaultMaxPerRoute=10, 50并发 5分钟
此时QPS已经到5278,比前次有不少提升。search后端和ES后端的负载均由明显增加。接下来增大并发数。
3.6 maxTotal=100, defaultMaxPerRoute=10, 100并发 5分钟
从测试结果看,并发数增加了一倍,但是QPS只是略有增加,且95线 99线均由增加,推测是defaultMaxPerRoute限制了并发数。接下来继续调整参数。
3.7 maxTotal=200, defaultMaxPerRoute=20, 100并发 5分钟
此时QPS已达到8118,且95线 99线均表现良好。
从机器负载监控图看,search后端服务器压力较大,CPU使用率超过60%。ES后端CPU使用率35%左右,压力一般。
3.8 maxTotal=200, defaultMaxPerRoute=20, 200并发 10分钟
并发数增加一倍,QPS值增加了800。此时search后端服务器CPU使用率在70%左右,ES后端服务器CPU使用率不到40%。加下来继续增加defaultMaxPerRoute
3.9 maxTotal=200, defaultMaxPerRoute=30, 100并发 5分钟
maxTotal相同、并发数相同,defaultMaxPerRoute由20增加到30,QPS只增加了300。
search后端机器压力仍然较大,CPU负载约66%,略有增加。ES后端机器CPU负载约38%,没有明显增加。
接下来提高并发数。
3.10 maxTotal=200, defaultMaxPerRoute=30, 200并发 5分钟
maxTotal相同、defaultMaxPerRoute相同,并发数由100增加到200时,QPS增加了1500,突破了10000.
此时search后端服务器CPU负载约75%,ES后端服务器负载约41%。
综合看来,此时search后端服务器已到极限。而ES后端服务器仍有压榨空间。
4.结论
search后端4台机器(4C8G)、ES后端8台机器(16C32G),jestClient配置maxTotal=200、defaultMaxPerRoute=30的情况下,订单读服务的极限QPS为10000。此时search后端服务器到达极限,ES后端服务器仍有压榨空间。
目前订单读服务日常QPS在200-300,完全能满足未来10倍的增长需求。
5.存在的问题
1.压测时调用服务端接口使用的参数均为热点数据,ES服务端是否存在缓存?
2.没有jestClient连接数的监控,参数设置是否达到最优?
附:设置maxTotal和defaultMaxPerRoute的方法
@Slf4j
@Component
public class HttpSettingsCustomizer implements HttpClientConfigBuilderCustomizer {
//Apollo配置
@Value("${es.max.total.connection:100}")
private Integer maxTotalConnection;
@Value("${es.default.max.total.connection.per.route:5}")
private Integer defaultMaxTotalConnectionPerRoute;
@Override
public void customize(HttpClientConfig.Builder builder) {
log.info("set maxTotalConnection={}, defaultMaxTotalConnectionPerRoute={}", maxTotalConnection, defaultMaxTotalConnectionPerRoute);
builder.maxTotalConnection(maxTotalConnection).defaultMaxTotalConnectionPerRoute(defaultMaxTotalConnectionPerRoute);
}
}
参数设置过程可参考JestAutoConfiguration源码
@Configuration
@ConditionalOnClass(JestClient.class)
@EnableConfigurationProperties(JestProperties.class)
@AutoConfigureAfter(GsonAutoConfiguration.class)
public class JestAutoConfiguration {
private final JestProperties properties;
private final ObjectProvider<Gson> gsonProvider;
private final List<HttpClientConfigBuilderCustomizer> builderCustomizers;
public JestAutoConfiguration(JestProperties properties, ObjectProvider<Gson> gson,
ObjectProvider<List<HttpClientConfigBuilderCustomizer>> builderCustomizers) {
this.properties = properties;
this.gsonProvider = gson;
this.builderCustomizers = builderCustomizers.getIfAvailable();
}
@Bean(destroyMethod = "shutdownClient")
@ConditionalOnMissingBean
public JestClient jestClient() {
JestClientFactory factory = new JestClientFactory();
factory.setHttpClientConfig(createHttpClientConfig());
return factory.getObject();
}
protected HttpClientConfig createHttpClientConfig() {
HttpClientConfig.Builder builder = new HttpClientConfig.Builder(
this.properties.getUris());
PropertyMapper map = PropertyMapper.get();
map.from(this.properties::getUsername).whenHasText().to((username) -> builder
.defaultCredentials(username, this.properties.getPassword()));
Proxy proxy = this.properties.getProxy();
map.from(proxy::getHost).whenHasText().to((host) -> {
Assert.notNull(proxy.getPort(), "Proxy port must not be null");
builder.proxy(new HttpHost(host, proxy.getPort()));
});
map.from(this.gsonProvider::getIfUnique).whenNonNull().to(builder::gson);
map.from(this.properties::isMultiThreaded).to(builder::multiThreaded);
map.from(this.properties::getConnectionTimeout).whenNonNull()
.asInt(Duration::toMillis).to(builder::connTimeout);
map.from(this.properties::getReadTimeout).whenNonNull().asInt(Duration::toMillis)
.to(builder::readTimeout);
customize(builder);
return builder.build();
}
private void customize(HttpClientConfig.Builder builder) {
if (this.builderCustomizers != null) {
for (HttpClientConfigBuilderCustomizer customizer : this.builderCustomizers) {
customizer.customize(builder);
}
}
}
}