本文针对公司微服务并发的实际场景以及网上调研的资料,记录影响微服务并发的各种优化配置。
先说明线上调用的实际例子:
通过zuul网关 调用服务A的接口,服务A的接口里面通过Feign调用服务B的接口。
问题:
通过JMeter并发测试发现,并发数竟然没有达到30次/s,即QPS不到30。这显然不合理。
备注:
TPS(吞吐量) 系统在单位时间内处理请求的数量。
QPS(每秒查询率) 每秒的响应请求数
第一步:熔断器并发调优
首先想到的是Feign调用并发过大,导致的熔断问题,优化服务A中的熔断配置
hystrix.command.default.fallback.isolation.semaphore.maxConcurrentRequests 如果并发数达到该设置值,请求会被拒绝和抛出异常并且fallback不会被调用。默认10
果然,hystrix在semaphore隔离方案下,最大的并发默认是10。
优化配置:
#线程策略
hystrix.command.default.execution.isolation.strategy=SEMAPHORE
hystrix.command.default.execution.isolation.semaphore.maxConcurrentRequests=500
第二步:Zuul并发调优
经历了将熔断器执行线程并发设置为500后,继续用JMeter进行并发测试,结果QPS到达100后,又出现大量请求失败。
查看日志,发现zuul很多请求连接关闭。
优化配置:
#zuul网关配置
zuul.semaphore.max-semaphores=500
再次使用JMeter测试,发现并发500没有在出现问题。
以上2个就是spring cloud并发调优最核心的2个参数。
下面系统说下spring cloud工程调优的问题
主要从以下几个方面入手:
1、hystrix熔断器并发调优
2、zuul网关的并发参数控制
3、Feign客户端和连接数参数调优
4、Tomcat并发连接数调优
5、timeout超时参数调优
6、JVM参数调优
7、ribbon和hystrix的请求超时,重试以及幂等性配置
下面说明下具体调优参数:
表示HystrixCommand.run()的执行时的隔离策略,有以下两种策略
1 THREAD: 在单独的线程上执行,并发请求受线程池中的线程数限制
2 SEMAPHORE: 在调用线程上执行,并发请求量受信号量计数限制
在默认情况下,推荐HystrixCommands 使用 thread 隔离策略,HystrixObservableCommand 使用 semaphore 隔离策略。
只有在高并发(单个实例每秒达到几百个调用)的调用时,才需要修改HystrixCommands 的隔离策略为semaphore 。semaphore 隔离策略通常只用于非网络调用。
说明:高并发时,优先使用semaphore 。
hystrix.threadpool.default.coreSize=10
hystrix.threadpool.default.maximumSize=10
hystrix.threadpool.default.maxQueueSize=-1
#如该值为-1,那么使用的是SynchronousQueue,否则使用的是LinkedBlockingQueue。注意,修改MQ的类型需要重启。例如从-1修改为100,需要重启,因为使用的Queue类型发生了变化
如果想对特定的 HystrixThreadPoolKey 进行配置,则将 default 改为 HystrixThreadPoolKey 即可。
如果隔离策略是SEMAPHORE:
hystrix.command.default.execution.isolation.strategy=SEMAPHORE
hystrix.command.default.execution.isolation.semaphore.maxConcurrentRequests=10# 默认值
如果想对指定的 HystrixCommandKey 进行配置,则将 default 改为 HystrixCommandKey 即可。
我们知道Hystrix有隔离策略:THREAD 以及SEMAPHORE ,默认是 SEMAPHORE 。
查询资料发现是因为zuul默认每个路由直接用信号量做隔离,并且默认值是100,也就是当一个路由请求的信号量高于100那么就拒绝服务了,返回500。
线程池提供了比信号量更好的隔离机制,并且从实际测试发现高吞吐场景下可以完成更多的请求。但是信号量隔离的开销更小,对于本身就是10ms以内的系统,显然信号量更合适。
当 zuul.ribbonIsolationStrategy=THREAD时,Hystrix的线程隔离策略将会作用于所有路由。
此时,HystrixThreadPoolKey 默认为“RibbonCommand”。这意味着,所有路由的HystrixCommand都会在相同的Hystrix线程池中执行。可使用以下配置,让每个路由使用独立的线程池:
zuul:
threadPool:
useSeparateThreadPools: true
只有在隔离策略是thread才有效
1. 隔离策略
zuul.ribbon-isolation-strategy=thread
2. 最大信号
当Zuul的隔离策略为SEMAPHORE时:
全局设置默认最大信号量:
zuul.ribbon-isolation-strategy=Semaphore
zuul:
semaphore:
max-semaphores: 100 # 默认值
对路由linkflow和oauth单独设置最大信号量
routes:
linkflow:
path: /api1/**
serviceId: lf
stripPrefix: false
semaphore:
maxSemaphores: 2000
oauth:
path: /api2/**
serviceId: lf
stripPrefix: false
semaphore:
maxSemaphores: 1000
3.zuul并发连接参数
针对url的路由配置
zuul:
host:
max-total-connections: 200 # 默认值
max-per-route-connections: 20 # 默认值
针对serviceId的路由配置
serviceId:
ribbon:
MaxTotalConnections: 0 # 默认值
MaxConnectionsPerHost: 0 # 默认值
在默认情况下 spring cloud feign在进行各个子服务之间的调用时,http组件使用的是jdk的HttpURLConnection,没有使用线程池。本文先从源码分析feign的http组件对象生成的过程,然后通过为feign配置http线程池优化调用效率。
有种可选的线程池:HttpClient和OKHttp
个人比较推荐OKHttp,请求封装的非常简单易用,性能也很ok。
当使用HttpClient时,可如下设置:
feign.httpclient.enabled=true
feign.httpclient.max-connections=200# 默认值
feign.httpclient.max-connections-per-route=50# 默认值
代码详见:
org.springframework.cloud.netflix.feign.FeignAutoConfiguration.
HttpClientFeignConfiguration#connectionManager
org.springframework.cloud.netflix.feign.ribbon.HttpClientFeignLoadBalancedConfiguration.
HttpClientFeignConfiguration#connectionManager
当使用OKHttp时,可如下设置:
feign.okhttp.enabled=true
feign.okhttp.max-connections=200# 默认值
feign.okhttp.max-connections-per-route=50# 默认值
代码详见:
org.springframework.cloud.netflix.feign.FeignAutoConfiguration.
OkHttpFeignConfiguration#httpClientConnectionPool 。
org.springframework.cloud.netflix.feign.ribbon.OkHttpFeignLoadBalancedConfiguration.
OkHttpFeignConfiguration#httpClientConnectionPool
如果使用的是内嵌的tomcat保持默认就好
server.tomcat.max-connections=0 # Maximum number of connections that the server accepts and processes at any given time.
server.tomcat.max-http-header-size=0 # Maximum size, in bytes, of the HTTP message header.
server.tomcat.max-http-post-size=0 # Maximum size, in bytes, of the HTTP post content.
server.tomcat.max-threads=0 # Maximum number of worker threads.
server.tomcat.min-spare-threads=0 # Minimum number of worker threads.
由于默认的最大连接数,最大线程数都是0,没有限制,所以在spring boot中启动内嵌的tomcat,一般保持默认的配置就可以了。
关于Jvm调优Oracle官网有一份指导说明:
Oracle官网对Jvm调优的说明
有兴趣大家可以去看看。
执行启动设置Jvm参数的操作。
java -Xms1024m -Xmx1024m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=128m -Xmn256m -Xss256k -XX:SurvivorRatio=8 -XX:+UseConcMarkSweepGC -jar user-1.0.0.jar
关于这些设置的JVM参数是什么意思,请参考第二步中的oracle官方给出的调优文档。
我在这边简单说一下:
-XX:MetaspaceSize=128m (元空间默认大小)
-XX:MaxMetaspaceSize=128m (元空间最大大小)
-Xms1024m (堆最大大小)
-Xmx1024m (堆默认大小)
-Xmn256m (新生代大小)
-Xss256k (棧最大深度大小)
-XX:SurvivorRatio=8 (新生代分区比例 8:2)
-XX:+UseConcMarkSweepGC (指定使用的垃圾收集器,这里使用CMS收集器)
-XX:+PrintGCDetails (打印详细的GC日志)
#配置首台服务器重试1次
ribbon.MaxAutoRetries=1
##配置其他服务器重试1次
ribbon.MaxAutoRetriesNextServer=1
##获取连接的超时时间
ribbon.ConnectTimeout=1000
###请求处理时间
ribbon.ReadTimeout=1000
##每个操作都开启重试机制
ribbon.OkToRetryOnAllOperations=true
#开启Feign请求压缩
feign.compression.request.enabled=true
feign.compression.request.mime-types=text/xml,application/xml,application/json
feign.compression.request.min-request-size=2048
feign.compression.response.enabled=true
#配置断路器超时时间,默认是1000(1秒)
feign.hystrix.enabled=true
#feign use okhttp
feign.httpclient.enabled=false
feign.okhttp.enabled=true
#是否开启超时熔断, 如果为false, 则熔断机制只在服务不可用时开启
hystrix.command.default.execution.timeout.enabled=true
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=10000
请求在1s内响应,超过1秒先同一个服务器上重试1次,如果还是超时或失败,向其他服务上请求重试1次。
那么整个ribbon请求过程的超时时间为:
ribbonTimeout = (ribbonReadTimeout + ribbonConnectTimeout) * (maxAutoRetries + 1) * (maxAutoRetriesNextServer + 1);
ribbonTimeout = (1000 + 1000) * (1 + 1) * (1 + 1) = 8000
由于Hystrix timeout一定要大于ribbonTimeout 超时,所以
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds>8000
超时设置是为了防止某些耗时操作积压在线程池中,导致后续请求无法进行,压爆服务器。
重试是为了防止网络抖动等原因出现偶然性异常的自动补偿机制,不过这时一定要保证所有接口的幂等性。
Feign请求压缩是为了减少网络IO传递的耗时
你的系统架构中,只要涉及到了重试,那么必须上接口的幂等性保障机制。
否则的话,试想一下,你要是对一个接口重试了好几次,结果人家重复插入了多条数据,该怎么办呢?
其实幂等性保证本身并不复杂,根据业务来,常见的方案:
可以在数据库里建一个唯一索引,插入数据的时候如果唯一索引冲突了就不会插入重复数据
或者是通过redis里放一个唯一id值,然后每次要插入数据,都通过redis判断一下,那个值如果已经存在了,那么就不要插入重复数据了。
类似这样的方案还有一些。总之,要保证一个接口被多次调用的时候,不能插入重复的数据。
Spring Cloud各组件调优参数