聊聊微服务治理到底在治什么
拆完微服务,服务从一个变几十个,调用关系乱成一团,一个挂了全链路雪崩。这篇把微服务治理拆开讲:注册发现、负载均衡、熔断限流、链路追踪、配置中心这几块到底各管什么、为什么缺一不可,用大白话把治理的来龙去脉讲清楚。
单体应用拆成微服务,这事现在几乎是默认操作。
但拆完之后很多人会发现一个问题:以前一个服务好好的,现在拆成几十个,天天出奇怪的毛病。
A 调 B 超时,B 又在等 C,C 其实早挂了,结果整条链路卡死。
这就是“治理”要解决的事。
我自己踩过不少这种坑,这篇就把微服务治理拆开,一块一块讲讲它到底在治什么。
为什么拆完反而更麻烦
先说个扎心的事实:微服务不是银弹,它是用一堆新问题换掉旧问题。
单体的时候,所有模块在一个进程里,函数调函数,内存里走一圈就完事。
拆成微服务之后,模块之间变成了网络调用。
网络这东西,天生不可靠。
一次本地函数调用,耗时是纳秒级,几乎不会失败。
一次跨服务调用,要经过序列化、网络传输、对端处理、再反序列化回来,耗时是毫秒级,而且随时可能超时、丢包、对端宕机。
我们把一个确定性很高的系统,拆成了一堆充满不确定性的小系统。
所以治理的本质,就是把这些不确定性重新管起来,让一堆小服务协同起来还能像个整体。
下面这几块,就是治理的核心拼图。
服务怎么找到彼此
第一个问题最朴素:A 要调 B,A 怎么知道 B 在哪?
单体时代不存在这个问题,大家在一个进程里。
拆开之后,B 可能部署了 3 个实例,分布在 3 台机器上,IP 还可能随时变(容器一重启 IP 就换了)。
你总不能把 IP 硬编码在代码里吧。
这就需要服务注册与发现。
思路很简单,搞一个“通讯录”,这个通讯录就是注册中心(比如 Nacos、Consul、Eureka)。
# 服务注册与发现的基本流程
1. B 服务启动时,主动把自己的地址报给注册中心
“我是 order-service,我在 192.168.1.10:8080,我还活着”
2. 注册中心记下来,并定期跟 B 做心跳检测
心跳断了就把 B 从通讯录里摘掉,不再给别人
3. A 要调 B 时,先问注册中心
“order-service 现在有哪些可用地址?”
4. 注册中心返回当前健康的实例列表
A 从里面挑一个去调
是不是一下就清爽了?
服务上线下线、扩容缩容,对调用方都是透明的,大家只认服务名,不认具体 IP。
这里有个细节值得一提:注册中心自己也得高可用。
它要是挂了,所有服务都找不到彼此,那就是全局灾难。
所以注册中心一般都是集群部署,这块后面讲配置的时候再提。
请求该发给哪个实例
接着上一个问题。
注册中心告诉 A,B 有 3 个实例可用。
那 A 这次请求到底发给哪一个?
这就是负载均衡要管的事。
最简单的策略是轮询,一人一次轮着来。
但轮询有个毛病:它假设所有实例的处理能力一样强。
现实里往往不是。有的机器配置高,有的低;有的实例刚启动还在预热,有的已经扛了一堆请求。
所以就有了更聪明的策略:
- 加权轮询:给配置高的机器多分点流量,按权重来
- 最少连接:谁手头活最少就给谁,避免忙的更忙
- 一致性哈希:同一个用户的请求尽量落到同一个实例,对有本地缓存的场景友好
- 响应时间优先:谁响应快就多给谁,自动避开慢节点
选哪个看场景。
我个人的习惯是,没有特殊需求就先用加权轮询,简单稳定,等真遇到性能瓶颈了再针对性换策略。
过早上一堆花哨策略,往往是给自己埋坑。
还有一个点要分清:负载均衡有客户端和服务端两种。
服务端负载均衡就是请求先打到一个中心节点(比如 Nginx),由它再转发。
客户端负载均衡是调用方自己拿到实例列表,自己挑一个直连,省掉中间那一跳。
微服务里客户端负载均衡用得更多,因为它少一次网络转发,延迟更低。
一个服务挂了别拖垮全家
这块是治理里最要命的一环,叫熔断、降级、限流。
先讲个场景你就懂了。
假设订单服务要调库存服务。
某天库存服务数据库卡住了,每个请求都要等 10 秒才超时返回。
订单服务这边呢,请求一个接一个打过去,每个都傻等着。
等着等着,订单服务的线程全被占满了,自己也卡死了。
然后调订单服务的网关也跟着卡死……
这就是雪崩。一个点的故障,顺着调用链一路蔓延,最后整个系统全趴下。
怎么防?三板斧。
熔断
熔断就像家里的保险丝。
电流过载了,保险丝先熔断,保护后面的电器,而不是让整条线路烧起来。
放到服务上就是:A 发现调 B 老是失败(比如 10 秒内失败率超过 50%),就暂时“断开”,不再调 B 了。
接下来一段时间,A 调 B 的请求直接走失败逻辑,根本不发出去。
# 熔断器的三个状态(经典的三态机)
关闭(Closed):正常放行请求,同时统计失败率
↓ 失败率超过阈值
打开(Open):直接拒绝请求,不再真正调用下游,快速失败
↓ 过了冷却时间(比如 10 秒)
半开(Half-Open):试探性放几个请求过去
- 成功了 -> 回到关闭,恢复正常
- 又失败 -> 回到打开,继续熔断
这个半开状态的设计很妙。
它不是死等人工干预,而是自己定期试探下游有没有恢复,恢复了就自动放行。
降级
熔断之后,请求不发了,那总得给个交代吧?
直接报错给用户体验太差。
降级就是:主逻辑走不通,退一步给个兜底结果。
比如商品详情页,推荐模块挂了,那就不展示推荐,或者展示一批默认的热门商品,别让整个页面打不开。
核心服务保住,非核心的坏了就先舍掉。
这是一种取舍,保命要紧。
限流
限流是防患于未然。
你的服务能扛 1000 QPS,突然来了 5000,与其全部放进来一起崩,不如只放 1000,剩下的直接拒绝。
保住已经进来的请求能正常处理,总比大家一起死强。
常见的限流算法:
- 计数器:固定时间窗口内计数,超了就拒。简单,但有临界问题
- 滑动窗口:把时间窗口切细,缓解临界突刺
- 漏桶:请求匀速流出,不管进来多猛,出去的速率恒定
- 令牌桶:按固定速率发令牌,有令牌才放行,能应对一定的突发流量
Sentinel、Hystrix 这类框架,基本把熔断、降级、限流打包一起给你了,不用自己从零造。
我的建议是别自己手搓这套东西,水很深,用成熟框架。
出了问题怎么定位
服务多了之后,还有个头疼的日常问题:排障。
单体的时候,一个请求的处理过程全在一个进程、一份日志里,grep 一下就看明白了。
微服务里,一个用户点击,背后可能串了七八个服务。
出了问题你都不知道卡在哪一环,日志散在七八台机器上,根本对不上。
这就需要链路追踪。
做法是给每个进来的请求打一个全局唯一的 ID(一般叫 TraceID)。
这个 ID 跟着请求在整条调用链里传递,每经过一个服务就记一笔。
# 一次请求的链路追踪示意
用户下单 TraceID=abc123
-> 网关 [abc123] 耗时 2ms
-> 订单服务 [abc123] 耗时 15ms
-> 库存服务 [abc123] 耗时 8ms
-> 优惠券服务 [abc123] 耗时 320ms <- 慢在这!
-> 支付服务 [abc123] 耗时 20ms
拿着 TraceID 一查,整条链路的耗时分布一目了然。
哪一环慢、哪一环报错,清清楚楚。
上面那个优惠券服务 320ms,不用猜就知道该去查它了。
Jaeger、Zipkin、SkyWalking 都是干这个的。
接入之后排障效率不是提升一点半点,强烈建议早接。
配置别散落在各处
最后说配置中心。
服务一多,配置管理也成了麻烦事。
数据库连接串、第三方密钥、各种开关参数……以前写在每个服务自己的配置文件里。
现在几十个服务,想改一个公共配置,难道要一个个改、一个个重启?
而且不同环境(开发、测试、生产)的配置还各不一样,手工管理迟早出错。
配置中心的思路是:把配置集中管起来,服务启动时去拉,运行时还能动态推送更新。
好处很直接:
- 配置改一处,推送到所有相关服务,不用逐个重启
- 不同环境的配置隔离管理,发布时不容易拿错
- 敏感配置集中加密存储,比散在一堆文件里安全
- 配置变更有记录,改坏了能回滚
Nacos 这类工具就是注册中心和配置中心二合一的,一套搞定两件事,挺省心。
小结
把这几块串起来看,微服务治理其实就一句话:让一堆独立的小服务,重新协同成一个可靠的整体。
- 注册发现解决“找得到”
- 负载均衡解决“分得匀”
- 熔断限流解决“死不了全家”
- 链路追踪解决“查得到”
- 配置中心解决“管得住”
缺哪一块,系统就在哪一块上漏风。
说到底,微服务拆分很容易,治理才是真功夫。
拆分是把蛋糕切开,治理是让切开的蛋糕还能整整齐齐摆回盘子里。
别光顾着拆,治理这块的基建一定要早点铺,不然服务越多窟窿越大。
那就从搭一个注册中心开始吧,一步步来。
