一、从一个线上事故说起
上周五晚上十一点,我正在追剧,突然手机连环报警。订单服务的错误率飙到60%,紧接着库存服务也挂了,最后连用户服务都半死不活。查到最后,罪魁祸首竟然是一个被设成“永不超时”的配置。
这个场景大家可能都熟悉:上游服务迟迟不给回应,中间的线程等啊等,等到新请求也挤不进来,于是整个系统像堵车一样,全线瘫痪。这其实就是超时配置不当引发的雪崩事故。
在以前的单体应用里,超时设置可能还算简单。但到了微服务,特别是接入服务网格之后,超时可调的地方更多了,调不好就极易出事。
二、先搞懂服务网格里的超时参数
要调优,先得知道服务网格把哪些东西管起来了。网格里的超时,主要分两种。
2.1 连接超时 vs 请求超时
连接超时是“建立连接”的等待时间。比如你的服务要调用另一个服务,TCP握手半天连不上,那连接超时就生效。
请求超时是“这个调用从发出到收到响应”的总时间。它包含连接时间、等待处理时间、传输时间等。
在服务网格里,很多时候我们改的是请求超时。如果你只设置连接超时,不设置请求超时,那连接是成功了,但对方一直不返回数据,你还是在傻等,线程照样被耗死。
2.2 重试次数和超时时间的关系
重试是雪崩的好朋友。如果不控制总超时,一个请求失败后立刻再重试,重试又超时,又重试……流量会成倍放大。假设你每个请求重试3次,原本只有100个请求,现在就变成400个,下游本来就吃力,再被这么一冲,直接崩溃。
所以,服务网格里有“单次尝试超时”和“最大重试次数”两个参数要一起看。我们要确保“总耗时”可控。比如单次超时500毫秒,最多重试2次,那最坏情况就是1.5秒。当然,还要考虑重试退避。
2.3 超时和熔断的区别
很多人把超时和熔断搞混。超时是“我等多久就不等了”,熔断是“我知道你可能不行了,直接不让你进来”。熔断能让失败的服务喘口气,超时则能让调用方节省等待时间。两者经常配合使用。
在服务网格里,超时由VirtualService控制,熔断由DestinationRule控制。调优的时候,这两个配置要一起考虑。
三、超时配错为什么会雪崩
这一部分我们用生活里的场景来解释,大家就很容易理解。
3.1 线程池被占满,就像餐厅的桌子
想象一家餐厅,只有10张桌子。每桌客人点完菜后,如果后厨一直不上菜,客人也不走,占着桌子等。后面排队的客人进不来,整个餐厅就乱套了。
服务端的线程池也是一样。每个请求占用一个线程,如果这个线程一直等着下游响应,它就一直不释放。线程数量有限,一旦全部被占满,新请求直接排队或拒绝。你超时时间设得越长,线程被占住的时间就越久,雪崩就越严重。
3.2 重试放大了流量,就像堵车时猛按喇叭
堵车的时候,如果每个人都在后面疯狂按喇叭,只会让大家都更烦躁,交通更乱。重试也是这样。当下游服务已经处理不过来时,重试非但不能解决问题,反而会把更多的请求塞进去。更可怕的是,重试往往发生在“刚刚超时”的那一刻,下一次请求又撞在枪口上,造成连锁反应。
3.3 连接池耗尽,最后一根稻草
除了线程,连接池也会被耗尽。服务网格里,调用下游时需要从连接池里拿连接。如果连接一直不释放,连接池就被掏空了。这时候新的调用会一直等待连接,整个系统像被点了穴,动也动不了。
所以,超时配置不当,等于是同时埋了三个雷:线程池、重试流量、连接池。它们彼此影响,最终引发雪崩。
四、实战调优:一步步调整超时参数
下面我们进入正题,以一套实际的Kubernetes + Istio环境为例,演示怎么把超时和重试调好。
技术栈:Kubernetes + Istio
4.1 把终极超时设成“最多等2秒”
首先,我们给订单服务的路由设置一个请求超时。通过VirtualService,把“订单服务”访问“库存服务”的调用超时设置为2秒。
# virtual-service-order.yaml
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: svc-order-route
namespace: demo
spec:
hosts:
- svc-inventory # 目标服务名
http:
- route:
- destination:
host: svc-inventory # 真正要调用的服务
port:
number: 8080
timeout: 2s # 请求最长等2秒,超过就返回504
注意,这个 timeout 是“端到端”的。也就是说,从订单服务发起到库存服务返回完整响应,整个链路最多等2秒。如果库存服务需要调数据库,数据库慢,那2秒也包含了那段耗时。
4.2 让重试见好就收
光有超时还不够,我们还要设重试。但必须严格控制重试次数和每次尝试的超时时间。
# virtual-service-order-with-retry.yaml
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: svc-order-route
namespace: demo
spec:
hosts:
- svc-inventory
http:
- route:
- destination:
host: svc-inventory
port:
number: 8080
timeout: 2s # 整个请求的总超时
retries:
attempts: 2 # 最多重试2次,总请求量最多是原来的3倍
perTryTimeout: 500ms # 每次尝试的单独超时控制在500毫秒
retryOn: connect-failure,refused-stream,503 # 只在哪些错误上重试
这里有个关键点:timeout 是整个调用的总超时,perTryTimeout 是每次尝试(第一次+重试)的超时。如果第一次尝试耗时800毫秒,还没到总超时,但超过了单次超时,重试会被触发。设计时,我们要保证“总超时 >= 单次超时 × (重试次数 + 1)”,否则总超时就形同虚设。比如上面例子,单次500毫秒,重试2次,最坏情况是1.5秒,小于总的2秒,是合理的。
4.3 连接池和熔断配合
设置完路由,我们还需要在DestinationRule里把连接池和熔断配上,防止下游真的扛不住时,我们这里还能保护自己。
# destination-rule-inventory.yaml
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: svc-inventory-dr
namespace: demo
spec:
host: svc-inventory
trafficPolicy:
connectionPool:
tcp:
maxConnections: 100 # 每个上游实例最多保持100个TCP连接
http:
http1MaxPendingRequests: 10 # HTTP/1.1 排队请求数
http2MaxRequests: 50 # HTTP/2 最大并发请求数
outlierDetection:
consecutive5xxErrors: 5 # 连续5次5xx错误
interval: 30s # 每30秒扫描一次
baseEjectionTime: 60s # 被摘除的实例至少休息60秒
maxEjectionPercent: 50 # 最多摘除50%的实例
这个配置的意思是:如果库存服务连续返回5次5xx错误,就把对应实例从负载均衡池里摘掉60秒,让其他正常实例接管。同时限制连接数和排队数,防止流量一下子全压上去。
实际调优时,这些数字要根据你的机器配置、服务耗时、QPS来算。不能照搬。
五、调优之后的验证
配置改完后,怎么知道有没有效果?我们可以用命令行把配置打到集群里,然后用压测工具模拟高并发。
5.1 用kubectl查看配置
# 应用虚拟服务和目标规则
kubectl apply -f virtual-service-order-with-retry.yaml
kubectl apply -f destination-rule-inventory.yaml
# 查看当前的虚拟服务配置
kubectl get virtualservice -n demo svc-order-route -o yaml
# 查看目标规则
kubectl get destinationrule -n demo svc-inventory-dr -o yaml
5.2 压测结果对比
我们用一个简单的压测命令(这里用hey工具演示):
# 模拟2000个请求,并发200
hey -n 2000 -c 200 -host order.demo.com http://ingress-gateway/order
# 结果里重点关注:
# - 错误率是不是降下来了
# - P99延迟是不是小于2秒
# - 有没有大量的504超时错误
如果压测时,库存服务某个实例宕机了,应该看到错误率依然很低,因为熔断器已经把坏实例摘除了。同时,不会出现线程池爆满的情况,因为超时最多等2秒,线程能及时释放。
六、超时参数调优的注意事项
6.1 不要拍脑袋设超时
我看到很多团队,给所有服务都配了同一个超时时间,比如3秒。这是不对的。每个服务有各自的业务耗时特征。查数据库的服务可能需要1秒,调用外部支付的可能需要5秒。统一超时要么太紧导致误杀,要么太松导致资源浪费。
6.2 超时时间要跟业务耗时匹配
要基于压测数据和历史监控,看看服务的P99耗时是多少。超时可以设置为P99耗时的1.5到2倍。如果P99是300毫秒,设超时500毫秒比较合理。如果设成5秒,那大部分超时都不会触发,等于没保护。
6.3 全链路超时要逐层递减
在调用链里,每个环节的超时时间要小于上游给它的超时时间。比如网关给订单服务2秒,订单服务调用库存服务最多只能设1.5秒,这样订单服务还有500毫秒做自己的处理和兜底。如果订单服务自己设了1.8秒,整个链路可能超时。
6.4 监控和告警要跟上
调完超时后,要盯着监控看。如果突然出现大量超时错误,可能是某个下游变慢了。这时候不要马上把超时调大,而要去查下游为什么慢。调大超时只是掩盖问题,不是解决问题。
七、技术优缺点与应用场景
7.1 服务网格超时控制的优点
服务网格把超时、重试、熔断这些能力下沉到基础设施层,业务代码不用改。对于多语言场景特别友好。Java服务、Go服务、Node服务都能用同一套规则。而且配置可以动态下发,不需要重启服务。
7.2 局限性和适用场景
服务网格的超时控制主要用在服务间同步调用上。对于异步消息等场景,则不太适用。另外,超时控制依赖于Sidecar代理的转发能力,如果代理本身性能不佳,可能成为新的瓶颈。还有,配置复杂度较高,团队需要具备一定的底层知识和运维能力。对于很小的微服务系统,引入服务网格可能有点杀鸡用牛刀。
八、总结
超时配置看着不起眼,但一旦配错,可能会让整个系统崩溃。我们在调优时,要牢记三条:一是设置总超时,不能让线程无限等待;二是严格控制重试次数,防止流量放大;三是用熔断保护下游不健康的实例。
每一次调优,都要结合真实的监控数据和压测结果。不要拍脑袋,也不要照抄别人的配置。只有对自己的服务耗时心中有数,才能让超时参数成为稳固的防线,而不是雪崩的导火索。
现在,不妨去检查一下你的服务网格里,超时和重试是不是已经配得合理了。
评论
围绕“超时配置不当引发雪崩效应,服务网格超时参数调优实战”参与讨论