ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

高可用微服务系统设计:从架构拆分到故障演练

高可用微服务系统设计:从架构拆分到故障演练 微服务在国内走过这么多年到了2026年这个时间点几乎每个像样一点的团队都在搞微服务但真正把“高可用”三个字落地的其实不多。我见过太多项目是这样的用KubeKey装了三台master的Kubernetes集群MySQL做了主从Nacos也搭了集群看起来一切都在往“高可用”靠拢。结果线上一次数据库抖动整个链路直接雪崩服务一个都没挂但用户请求全部失败。问题出在哪出在“高可用”被理解成了“服务不挂”但高可用微服务系统设计真正要解决的是“链路不塌”。单机挂了有副本顶上这只是最基础的一层真正的挑战在于几十个微服务互相调用的时候任何一个慢节点、一个满了的线程池、一次没完没了的重试都可能让整条链路在几分钟内彻底瘫痪。这篇文章我就结合自己做微服务改造和高可用治理的实际经验把“高可用微服务系统设计与实现”这件事从架构拆分、到基础设施、到代码细节、再到流量治理和落地部署一条线讲透。文章既适合正在做微服务改造的架构师也适合刚接手微服务项目、天天被线上问题追着跑的后端开发。1. 高可用不是“服务不挂”而是“链路不塌”1.1 一个最简单的数学题99.9%的单服务可用性救不了三十个节点的链路很多团队讲高可用张嘴就是“我们要做到99.9%”。99.9%什么概念一年365天允许不可用时间是8.76小时平均到每个月只有43分钟。看起来挺严格了对不对但如果你的业务链路只有10个微服务依赖每个服务都做到了99.9%可用性那么这10个服务同时可用的概率是0.999的10次方约99%。一旦链路拉到30个服务0.999的30次方只剩97%。也就是说单个服务看每个都“很可用”组合起来用户体验就是一个月挂好几次。这个数学题做完了你就明白高可用微服务系统设计的第一个核心命题你关注的不应该是“某个服务挂了没有”而应该是“用户的这一次请求走完整个链路成功没有”。链路越长可用性被乘数效应稀释得越厉害所以你要么拼命缩短链路要么在链路的每个环节都设计好冗余、超时、降级和快速失败机制。这也是为什么微服务拆分在一定规模之后高可用治理的优先级会远高于新功能开发。1.2 故障域与爆炸半径设计之前先划清边界再聊一个很多人忽略的概念——故障域。故障域就是“一个故障能影响到的范围”。一台机器挂了影响的是部署在这台机器上的所有实例一个机房断电影响的是整个机房一个Redis集群主节点挂了但没自动切换影响的是所有依赖这个Redis的服务。高可用设计本质上就是在不断缩小故障域、压缩爆炸半径。拆服务、做隔离、做限流、做降级这些东西的底层逻辑全部指向同一个目标把故障关在一个笼子里不让它蔓延到整条链路。举个例子你有一个订单服务它同时依赖库存服务和积分服务。如果积分服务是个老系统稳定性本来就差那么最常见的高可用做法不是把积分服务重构一遍而是在订单服务里给积分调用加一个独立的线程池和熔断器。积分服务抖了最多就是拿不到积分这一小块降级数据订单主流程完全不受影响。这就叫故障隔离。1.3 一条可落地的高可用设计路线基于上面两个认知我把高可用微服务系统的设计拆成五层接下来按这条线展开。架构层服务拆分和状态设计决定故障域的边界。基础设施层K8s集群、数据库、注册中心解决“底座不塌”。代码层超时、重试、幂等、熔断降级解决“单个服务的自保能力”。流量层限流、排队、容量保护解决“流量尖峰来了扛不扛得住”。观测层监控、链路追踪、告警和故障演练解决“出了事能不能快速发现、快速定位”。这五层每一层都有大量细节少做一层高可用就是个半吊子。2. 服务拆分与状态设计高可用的第一道分水岭2.1 拆分边界怎么定别按功能拆按“独立交付能力”拆高可用微服务设计里第一步不是画架构图而是定服务边界。边界定错了后面所有的高可用手段都是在给一个错误的结构打补丁。我见过最典型的反面案例是把“用户管理”“订单管理”“商品管理”这种按数据表拆出来的服务当成微服务。问题是这种服务往往只对应某一张表业务逻辑没有完整边界改一个需求可能要同时改三个服务而且这三个服务根本没法定独立版本发布。拆完之后发布的复杂度不降反升高可用更无从谈起。我自己在业务中判断一个服务拆得对不对就用三个标准这个服务能不能独立部署、独立升级而不强依赖同批次发布另一个服务。这个服务有没有自己独立的数据存储而不是和其他服务共享同一个数据库。这个服务对外提供的接口是否对应一个完整的业务能力而不是一张表的增删改查。满足这三条这个服务才算一个真正的微服务。边界清晰了故障域才能清晰。比如支付服务挂了影响的是支付能力订单服务还在用户还能下单这是一个可接受的局部故障但如果订单服务和支付服务共享一张订单表那支付挂了对数据库的依赖会导致订单服务也跟着雪崩这就是典型的边界没划好。2.2 无状态化是命门副本永远能互相接管服务拆分完之后高可用的另一个基础是“无状态化”。这句话听起来像老生常谈但你真去看很多团队的代码session还在服务内存里临时文件还在本地磁盘上定时任务也是直接跑在服务进程里。这样的服务看起来可以水平扩展实际上副本之间根本没法治愈——杀掉一个实例它的状态就丢了。无状态化的含义很简单任何服务实例都可以随时被杀掉、重新拉起而不丢失数据、不影响正在进行的业务。要做到这一点几个经典的状态必须外置用户会话状态放到Redis不能放在本地内存。否则用户第一次请求打到A实例第二次请求打到B实例登录态就没了。上传的临时文件、生成的报表放到对象存储或共享文件系统不能放本地磁盘。否则要扩容的时候新实例根本没有那些文件。定时任务状态要可重入或者用分布式锁保证同一时刻只有一个实例在处理。否则多个副本同时跑任务数据就乱了。为什么无状态化对高可用这么重要因为Kubernetes里Pod是随时可能被重新调度的。节点宕机、镜像更新、资源不足驱逐任何一个动作都会杀掉你的实例。只有无状态化的服务才能让这些动作变得“不疼不痒”。反过来有状态的服务做高可用就需要额外引入主从切换、数据同步、分布式一致性协议复杂度和出故障的概率会直线上升。2.3 状态外置之后数据一致性怎么办服务无状态化之后还有一个绕不开的问题数据库层的一致性。微服务的核心原则之一是数据按业务边界拆分一个服务一个库不允许跨服务直接查别人的表。但业务往往需要跨服务的数据流转比如下单之后要扣减库存、加积分、发物流单。这时候你不能用传统数据库事务去保证ACID只能用最终一致性。我最常用的方案是本地消息表加消息队列订单服务在自己的库里写入订单数据和一条“订单创建成功”的消息放在同一个本地事务里。事务提交后后台任务把消息发到MQ库存服务、积分服务消费消息去更新自己的数据。这个方案看起来老但胜在简单可靠本地事务保证了“订单和消息不会少一条”MQ中间件保证了消息最终会被消费。如果消费失败就重试重试到一定次数进死信队列人工处理。整个过程是最终一致的。需要强调的是高可用不等于强一致。为了达到高可用很多时候你必须在一致性上做一些妥协——接受短暂的数据不一致用对账任务和补偿机制兜底。设计阶段就要把这个原则定下来不然所有服务都想去抢一个分布式事务性能和可用性都会很难看。2.4 连接池和线程池也是状态别把资源池当成无限大的还有一个容易被忽视的“状态”——数据库连接池、HTTP客户端连接池、线程池。这些资源如果配置不合理高可用就变成了一纸空文。最常见的故障场景是这样的某天某个上游服务变慢了调用它的下游服务里有大量线程卡在等待响应的状态。由于每个请求占着一个连接池里的连接和一个工作线程而系统的线程池是有上限的新请求进来发现线程不够用就开始排队。队列越来越长响应时间越来越长调用方开始超时重试重试的请求又涌进来最终整个服务被自己拖死。这个现象就是“线程池耗尽”它比服务宕机更难排查。所以高可用的服务设计里一定要根据依赖方的情况给线程池设上限给连接池设上限。数据库连接池不是越大越好每一条连接在MySQL端都是一个线程、一份内存盲目把连接池从50调到500数据库大概率先扛不住。实践里DB连接池一般建议是“CPU核数乘以2到4”如果业务特别复杂再结合压测往上调整。线程池则要遵循“一个依赖一组线程池”的隔离原则后面第四章细讲。3. 集群与数据底座Kubernetes和MySQL的高可用实现3.1 K8s三master高可用关键不在master而在etcd很多人搭高可用Kubernetes集群第一反应就是“多搞几台master”用KubeKey装个三master的HA集群。方向没错但我得说一句大实话三台master的意义不在于让kube-apiserver高可用而在于让etcd高可用。apiserver本身是个无状态组件前面挂一个负载均衡就能解决高可用问题controller-manager和scheduler自带Leader Election多副本自己会选主。真正有状态、需要奇数节点、需要quorum机制的是存储集群所有元数据的etcd。etcd用的是Raft一致性协议三节点时允许挂一个五节点时允许挂两个。注意一个非常反直觉的坑etcd的写入需要quorum也就是“多数节点写入成功”才算成功。当三节点集群挂了一个节点时集群还能服务但如果挂掉两个剩下的一个节点永远凑不齐quorum集群会变成只读甚至拒绝写入。所以etcd集群最好保持固定规模的奇数节点不要随便扩缩更不要在它上面跑一些无关的高负载任务。实际操作中完整的K8s高可用安装要做的几件事三台master节点部署etcdetcd配置里指定集群成员列表形成静态集群。master节点前面挂一个VIP或负载均衡keepalived加HAProxy是最常见的组合kube-apiserver通过这个VIP对外提供服务。worker节点中kubelet、kube-proxy访问apiserver时都走VIP不直接写具体IP。组件都开启Leader Electioncontroller-manager、scheduler的启动参数里加上--leader-electtrue。我之前帮一个朋友团队排查过一个问题他们明明装了三台master结果一台master宕机之后整个集群的Pod调度全停了。后来发现原因是kubelet配置里写的apiserver地址是某一台master的IP而负载均衡只配给了外部访问集群内部各组件并没有走VIP。这个问题提醒我们HA集群不是装完就完事还要逐个组件检查它访问apiserver的入口是否经过了高可用层。3.2 MySQL高可用的几种常见方案选型决定了你的RPO和RTO微服务底座里数据库通常是整个系统里最“有状态”的组件也是故障影响最大的组件。做MySQL高可用常见的有主从复制加切换工具、半同步复制、MGRMySQL Group Replication还有云数据库自带的高可用。它们的核心差异在于两点RPO最多丢多少数据和RTO恢复需要多久。先看一张对比表。方案同步原理RPORTO适用场景异步主从 MHA切换主库写binlog从库异步拉取可能丢最近几秒事务30秒左右依赖脚本检测和切换对数据丢失不太敏感的业务半同步复制主库等至少一个从库收到并落盘binlog才提交基本不丢30秒左右大多数交易类业务MySQL 5.7开始支持MGR单主模式组复制多节点强一致自动选主不丢若多数派存活秒级需要数据库层自动切换、且团队有运维能力的场景云数据库RDS依赖云厂商内部高可用极低感知不到或分钟级不想自己运维K8s和数据库的团队我自己的建议是交易链路相关的数据库至少用半同步复制把RPO压到接近零的水平。因为对用户来说一笔订单刚下成功过两秒查不到了这种事故比“数据库暂时连不上”更难接受。纯异步的方案虽然性能好但主库宕机的瞬间丢数据几乎是必然的适合日志、报表这种丢了能重建的场景。还有一个老生常谈但总有人犯的错做了主从高可用但应用层代码里把数据库地址写死在配置里切换工具把主库切到了从库上应用却还在连已经宕机的老主库。解决思路有两条要么通过VIP访问数据库主从切换时把VIP漂移过去应用感知不到要么利用Nacos这类配置中心动态刷新数据源配置。无论是哪条路上线之前都要做一次真实的故障切换演练别只在PPT里演练。3.3 注册中心和配置中心自己不能挂本地缓存是最后一道防线微服务架构里注册中心Nacos、Eureka、Consul承担的是服务发现职责配置中心承担的是动态配置下发职责。这俩组件的高可用级别往往决定了整个系统的可用性上限。把它们做成集群是基本操作Nacos至少三节点配置存储用内嵌或外部数据库都可以节点之间通过Raft或内嵌协议同步。但比集群更重要的是一个设计原则注册中心挂了服务之间的调用不能跟着挂。国内很多基于Spring Cloud或者Dubbo的项目都依赖在本地维护一份服务列表缓存。理想情况下服务启动时从注册中心拉取全量服务列表之后在本地内存里维护这份列表注册中心的下线推送只是一种增量更新手段。如果注册中心整体不可用服务之间仍然可以基于本地内存里的旧列表继续调用——新上线的实例可能发现不了但存量调用不受影响。这个原则我在好几个项目里实测过。有一次运维大清早升级Nacos集群升级过程中注册中心短暂不可用因为有本地缓存业务流量完全没受任何影响。如果当时没有这一层设计所有服务到Nacos拉列表都会超时那场面就是连锁雪崩。4. 后端代码里的高可用细节超时、重试与幂等4.1 超时控制给每一次调用都定好“生命的边界”代码层面的高可用第一个要补的短板就是超时。我接手过的几乎所有线上故障里没有超时或者超时设置过长是压垮系统的第一块多米诺骨牌。超时一定要分三段独立设置连接超时、读取超时、写入超时。连接超时表示TCP连接建立的最长等待时间一般设置500毫秒到1秒读取超时表示请求发出去之后等待响应的最长时间这个要根据业务场景来定正常接口300到500毫秒批量接口可以放宽到2到3秒写入超时是为发出请求预留的时间通常设置得比读取超时短否则会出现“请求已经写出去了但一直等不到结果”的悬空状态。举个例子用Go写一个HTTP客户端超时应该这样配client : http.Client{ Transport: http.Transport{ DialContext: (net.Dialer{ Timeout: 500 * time.Millisecond, }).DialContext, TLSHandshakeTimeout: 1 * time.Second, ResponseHeaderTimeout: 500 * time.Millisecond, IdleConnTimeout: 90 * time.Second, }, }这里有个容易被忽略的细节HTTP客户端的总超时不要单独设成一个很大数字否则传输层超时和业务层超时叠加起来一个请求可能卡好几秒。更合理的做法是每一层都设一个小一点的超时让故障可以被快速发现。全链路超时还有一个原则逐层递减。比如从网关到订单服务是800毫秒订单服务调用库存服务就只给400毫秒库存服务查数据库只给150毫秒。这样做的目的是上游等待时间必须小于下游的超时时间否则下游已经开始重试了上游还在傻等链路会因为超时叠加变得特别脆弱。4.2 重试是双刃剑不加判断的重试就是雪崩加速器网络调用不可避免地会遇到瞬时抖动所以重试机制是必要的。但无脑重试比不重试更可怕因为它能把一个服务节点的故障放大到整个集群。最典型的场景某个服务超时了调用方自动重试3次2万个并发请求同时超时意味着打进故障服务的一共有8万个请求彻底把它打挂。然后其他依赖它的服务也开始超时重试故障像滚雪球一样扩散。重试的三条军规只在幂等操作上自动重试。查询、删除、以及带了唯一业务ID的更新操作可以重试纯扣减库存、转账这种操作要么保证接口幂等要么绝对不重试。总重试次数不要超过2次。第一次失败立即重试第二次失败说明服务大概率有问题放它一条生路。重试必须加指数退避和随机抖动。每次重试的间隔时间成倍增加比如第一次100毫秒第二次200毫秒再加一个20毫秒的随机值。防止所有请求在同一时间点集中重试。还有一种很优雅的做法用状态机驱动重试。比如订单状态是“创建中”到“创建成功”或者“创建失败”每次重试都带上订单号即使上一次请求实际成功但响应丢了这次重试顶多是把状态从“创建中”幂等地修正为“创建成功”不会重复下单。这种幂等设计靠的不是运气而是接口设计一开始就带上了全局唯一的业务键。4.3 熔断、降级与舱壁隔离像保护心脏一样保护核心链路如果超时和重试是代码层的“减速带”那熔断降级就是代码层的“保险丝”。熔断器的状态机非常直观正常情况下是Closed所有请求正常放行当错误率超过阈值比如滑动窗口内50%的请求失败状态变成Open后续所有请求直接快速失败不再发往下游过一段时间进入Half-Open放几个探测请求试试下游是否恢复恢复则回到Closed没恢复则继续Open。用Resilience4j配置一个熔断器大概是下面这种感觉CircuitBreakerConfig config CircuitBreakerConfig.custom() .failureRateThreshold(50) .waitDurationInOpenState(Duration.ofSeconds(10)) .slidingWindowSize(20) .minimumNumberOfCalls(10) .build();这个配置的意思是统计最近20个请求最少要10个请求参与统计如果失败率超过50%熔断器打开10秒之后进入半开状态。具体数值要结合业务调整但思路是确定的熔断的粒度要细到“一个下游服务或一个接口一组熔断器”而不是整个服务共用一个熔断器。否则一个不重要的下游服务抖了一下熔断器直接把包含核心业务在内的所有请求都拒了这是典型的因小失大。舱壁隔离的原理我经常用银行柜台来类比。你去银行办事如果只有一个排队队列、四个窗口前面一个人办一笔特别复杂的业务卡了半小时后面所有人都得跟着等。舱壁隔离的做法是给四个窗口各开一个队列办复杂业务的人会被引导到其中一个队列哪怕那个队列堵死了另外三个窗口的存钱取款还能正常办理。代码里对应的就是每个下游依赖分配一个独立的线程池或信号量调用积分服务走积分线程池调用库存服务走库存线程池它们之间互不干扰。慢依赖的线程池满了只会丢弃它自己的请求不会吃掉核心业务的工作线程。4.4 优雅上下线与健康检查让发布不再是“定时炸弹”代码跑得好好的一发布就出问题这种情况在高可用微服务里特别常见。原因多半是发布流程里没有做好优雅上下线。上线的时候新实例注册到Nacos之前要把流量拦在外面。K8s里的Readiness探针就是干这个的探针返回“不成功”的阶段Service不会把流量转发给这个Pod但是Pod里的进程已经在运行。只有当探针确认服务已经初始化完成、依赖连接建立好、能够处理请求了才把Pod标记为Ready。常见配置是启动延迟10秒后每5秒探测一次失败阈值3次。下线的时候要处理好“存量请求”。进程需要先摘除注册从Nacos反注册自己的服务地址再把应用的端口断开让LoadBalancer不再分配新连接然后进入一个缓冲期让已经在途的请求有机会完成最后才关闭工作线程池、释放数据库连接、退出进程。在Go里这个流程可以用一个优雅退出逻辑实现go func() { // 处理SIGTERM信号 -ctx.Done() // 1. 反注册服务 registry.Deregister() // 2. 停止接收新请求 server.Shutdown(context.WithTimeout(context.Background(), 10*time.Second)) // 3. 关闭连接池 db.Close() }()K8s里配合preStop钩子做类似的事情。很多人以为配置了K8s就自动优雅了其实不是。K8s只能保证Pod的编排动作应用进程内部怎么配合还是要靠代码自己打磨。发布期间多花这几十秒换来的却是线上稳定性的大幅提升这笔账很划算。5. 流量治理与容量保护限流、排队与全链路压测5.1 限流算法选型计数器、滑动窗口、漏桶还是令牌桶高可用系统里限流是最常见的自我保护手段。限流算法各有侧重选型之前先把差异搞清楚。算法核心思想优点缺点固定窗口计数器每秒重置一个计数超过阈值就拒绝实现简单内存占用小窗口临界瞬间可能打两倍流量限流不够平滑滑动窗口按时间切片窗口内统计请求数比固定窗口平滑边界问题改善内存占用略高实现稍复杂漏桶请求以固定速率流出桶满则丢弃输出速率绝对均匀无法应对突发流量可能导致大量请求被延后令牌桶桶里放令牌请求来了消耗令牌允许一定突发流量天然削峰填谷突发处理不当可能压垮下游具体到业务里我的习惯是接口级别的保护优先用令牌桶或滑动窗口因为它在限流的同时还能容忍一定的流量毛刺如果我们用的是Sentinel网关和核心接口默认走的是滑动窗口模式配置起来也简单。如果系统用的是RedisLua脚本做分布式限流那一定要测试并发情况下Lua脚本的执行性能别让Redis变成新瓶颈。5.2 单机限流和分布式限流一个保护自己一个保护全局限流要分两层来做。单机限流保护的是本服务实例的CPU、内存、线程池简单的做法是在内存里做令牌桶每个实例各限各的分布式限流保护的是共享资源比如数据库、第三方接口、核心下游需要把计数放到Redis里所有实例共享一套限流规则。一个常见误区是“只要做了分布式限流单机限流就可以不做”。实际上分布式限流有它自己的问题Redis本身的延迟和可用性会影响限流准确性网络抖动可能导致误判。更稳妥的方案是两层配合在网关层做分布式限流用于全局配额控制保护整个系统的总容量在服务内部做单机限流为每个实例设一个略高于单机容量的阈值保护本机不被极端流量打爆。比如网关层限流“用户名下订单接口每秒最多2000次”但某个实例的本地限流是“每秒最多300次”。当2000个请求分散到5个实例每个实例最多承受400个却被本地阈值限制在300个系统会自动让一部分流量失败。这是在发生故障时的人工选择宁可让30%的用户看到失败提示也不允许系统被全部拖垮。高可用从来不是让所有用户成功而是保证系统不崩溃、核心业务可用。5.3 排队与异步化把瞬时洪峰削成平缓水流限流是“拦”排队是“吞”。遇到秒杀这种瞬时流量是平时几十倍的场景光靠限流拒绝请求会导致大量用户根本抢不到机会体验很差。更好的方案是把同步请求改成异步请求进来先返回“排队中”把任务投递到消息队列后台服务按自己的消费速率处理处理完通过站内消息或轮询接口通知用户。同步调用改异步之后有一个新的坑消息队列自身的高可用变成关键。Broker集群要配置多副本生产者要开启确认机制消费者要保证处理消息的幂等性。另外队列里的消息不能无限积压一定要监控消费延迟积压超过阈值就告警。否则就是“请求没把服务打挂反而是队列把整个系统拖到天荒地老”。5.4 全链路压测高可用不是拍脑袋是压出来的很多团队的高可用设计只到“配置了熔断、限流”这一步上线之后你敢不敢说系统能扛住多少QPS八成不敢。因为从来没有做过真实的全链路压测。全链路压测要模拟的是完整用户行为一个请求从网关进来依次经过认证、风控、业务服务、数据库、缓存、第三方接口最终返回成功。压测的目标是找出每个环节的容量拐点数据库CPU用到多少开始慢查询增多缓存命中率掉到多少性能开始变差线程池在多大并发下开始排队然后在拐点以下设一个报警阈值比如拐点是单实例1000 QPS限流阈值就设为800留20%余量应对流量抖动。压测只测单个微服务是不够的因为服务之间的排队效应只有在链路压测里才能暴露出来。压完之后写一份容量报告标明每个服务建议的副本数、每个接口的建议限流值、每次大促前需要扩容多少节点。这份报告才是高可用系统真正的“容量底气”。6. 可观测性给高可用系统装上仪表盘6.1 Metrics、Logging、Tracing三件套一起上高可用系统和高可观测性是成对出现的。没有观测能力的高可用就是盲人骑瞎马你根本不知道服务是在正常服务还是在摇摇欲坠。Metrics负责回答“量”的问题每秒请求数、错误率、P99延迟、CPU使用率、内存占用、数据库连接数。用Prometheus采集指标Grafana出面板再配Alertmanager告警这是目前后端最标准的观测链路。Logging负责回答“发生了什么”的问题每一条日志要带上traceId和spanId这样你才能在成千上万条日志里把一次完整调用串起来。Tracing负责回答“时间花在哪了”的问题用Jaeger或者SkyWalking追踪每个请求经过的每个服务每个环节消耗了多少时间一眼就能看出瓶颈在哪个节点。三者缺一不可但最常被忽视的是Tracing。很多团队Metrics和Logging都做了但是服务之间没有串联的traceId排查问题的时候只能靠猜这条错误日志对应的到底是哪个上游请求哪个下游环节慢没有traceId跨服务的排查效率直接减半。6.2 告警不是越多越好SLO才是北极星告警是要命的设计。我见过一个团队一个服务挂了五个告警因为CPU、内存、错误率、延迟、重启次数都设了独立告警值班同学一晚上被吵醒七次后来干脆把告警全部屏蔽了。告警一旦被屏蔽那跟没有告警没有任何区别。更好的做法是围绕SLO服务水平目标来设计告警。先定业务的核心SLO比如“核心下单接口成功率不低于99.9%P99延迟低于300毫秒”然后围绕它建立几个有限的告警规则。告警信息要能回答三个问题什么在变差影响谁需要谁去看比如一个有效的告警文案是“订单服务错误率超过1%P99延迟800毫秒持续5分钟影响所有下单用户值班开发请介入。”而不是冷冰冰的一句“CPU超过80%”。还有一类告警容易被忽略饱和度。数据库连接数快满了、Redis内存快满了、磁盘快要满了这些不是突发的错误但往往是更大事故的前兆。饱和度告警应该提前设置宁可早叫早处理也不要等系统完全不可用了才被用户反馈唤醒。6.3 故障演练高可用不是靠“运气”是靠“预案”最后一个容易被忽略的环节是故障演练。混沌工程这几年在国内也越来越普及Chaos Mesh、Litmus这些工具可以在K8s环境里直接注入故障比如杀掉一个Pod、让某个服务延迟几秒钟、模拟网络分区。但演练的意义不在于“会杀Pod”而在于每次演练都能暴露一个预案的缺口。第一次杀Pod可能发现流量没有完全切走因为有连着的旧连接第二次模拟MySQL主从切换可能发现配置中心没有通知应用刷新连接应用还连在老主库上第三次模拟某个服务全部副本宕机可能发现熔断阈值设得太低连正常流量都开始误伤。每演练一次修复一个缺口高可用系统才是真的滚起来了。没有经过演练的高可用方案大概率只能在故障发生那分钟变成“不可用方案”。7. 本地联调与部署落地从IDE里跑通到集群里高可用7.1 本地启动多个微服务的痛点能跑起来比能写好代码更考验人前面聊了这么多设计理念落到日常开发里第一个现实问题是微服务项目代码拉下来之后本地怎么跑起来几十个服务不可能全部在本机启动机器资源也扛不住。我的经验是用“依赖容器化 服务按需启动”的方式来解决。具体来说基础设施依赖MySQL、Redis、Nacos、RabbitMQ用Docker Compose一键起本地开发机只启动当前正在开发的那一两个服务其他下游服务通过注册中心的namespace或环境隔离来区分。比如你在本地起了一个订单服务它需要调用库存服务那就通过网关或者内网环境把库存服务的调用转发到测试环境已有实例上而不是在本地也起一个库存服务。这样既保证了本地环境干净又避免了每个开发都要启动全套服务的尴尬。IDE方面基于IntelliJ IDEA的微服务启动要养成分组管理的习惯。把服务按启动顺序分组第一组是注册中心和配置中心第二组是基础中间件连接第三组是各个业务服务。每组用一个Run Configuration保存启动参数这样新增一个服务之后其他人拉代码也能复制你的启动模板少踩很多环境坑。7.2 配置与环境隔离本地、测试、生产别让配置成为隐患微服务高可用的一个前提是环境隔离做得好配置里不写死环境相关内容每个环境用自己的配置中心命名空间。以Nacos为例推荐的做法是服务里只写应用名和应用版本启动时指定命名空间namespace同一个服务在dev、test、prod各自一套配置配置项的key完全一致只是value不同。这样代码在本地跑的时候只需要把Nacos地址指向本地启动参数加上对应namespace就能拿到本地的配置Jenkins或GitLab CI部署到测试环境时改成测试环境的Nacos地址和namespace配置自动切换。特别要注意的是数据库地址、密码、第三方密钥这些敏感信息绝对不能写进代码仓库。现在的配置中心都支持加密配置Nacos、Apollo都有加密插件。安全上省事一时线上被脱库一次再回来改成本就高了。7.3 部署到K8s的高可用检查清单最终所有服务都是要部署到K8s集群里的。我整理了一个部署上线前的高可用检查清单每次发布之前过一遍能拦下大部分低级故障服务是否配置了资源请求和限制request/limit避免单个Pod吃光节点资源也避免HPA扩缩容时调度失衡。是否配置了Readiness和Liveness探针且探针的阈值与服务的启动时间匹配不要一启动就探、探失败就重启形成重启死循环。是否配置了PDBPodDisruptionBudget保证在节点维护或集群升级时不会一次性把某个服务的所有副本全部抢占式调度掉。是否配置了反亲和性把同一个服务的多个副本调度到不同节点。否则物理机宕机的时候所有副本一起玩完高可用形同虚设。是否配置了HPA基于CPU使用率或自定义指标自动扩缩容。高可用系统要能在流量上涨时自动扩容而不是等运维半夜起来手动加副本。是否检测过服务启动时对注册中心、配置中心、数据库的反向依赖顺序避免出现“服务起来了但依赖没就绪于是疯狂重试”的羊群效应。7.4 一个建议先画依赖图再决定在哪里设防最后分享一个我个人的实操习惯。每个微服务项目接手下来我不会急着看代码而是先把整个系统的服务依赖图画出来标出每一条调用链路的依赖方向、协议类型、平均耗时、是否核心链路。然后针对每个依赖问四个问题它挂了我能降级吗它慢了我有超时吗它抖了我的熔断和隔离在哪里它恢复了我的半开探测能做对判断吗这张图一旦画清楚限流阈值、熔断策略、线程池隔离、降级开关应该放在哪个服务上就会非常直观。比如发现支付回调是长链路、强依赖、不可降级那就必须在它前面做多重保险比如发现积分服务是弱依赖那就优先做降级而不是优先做重构。高可用微服务系统设计说到底是两件事把故障的爆炸半径控制住把系统的恢复能力练出来。技术方案复杂但每一步落到代码和配置里都不玄乎。按上面的链路一步步做你的系统就算做不到一年5个9至少也能做到“出了故障不崩盘出了事故敢处理”。
返回列表