想象你住进一家庞大的五星级酒店。半夜饿了想叫送餐,你完全不需要知道送餐员的姓名、住在哪个房间,只需拿起电话拨“0“转前台,说一句”我要餐饮服务”。前台的登记表上记着每一位在岗员工的信息——送餐员现在在哪层、维修工是否空闲、保洁阿姨是否在岗。员工上班先到前台登记,下班来销号;前台还会隔一段时间用对讲机确认每位员工还清醒地守在岗位上。在微服务架构里,每个服务实例就像酒店里来去匆匆的员工——随时可能因为扩缩容、故障、发布而换房间、换工号甚至离开。如果客人死记硬背某个员工原来的房间号,很快就会扑空。这,就是服务发现要解决的问题。 微服务把单体应用拆成几十上百个独立进程,每个实例的 IP 和端口都在动态变化。服务发现注册,就是给这座“软件之城”装一个永不掉线的总前台:让服务地址自动登记、自动更新、自动剔除,消费者只需要说出服务名字,就能找到当前真正可用的实例。本篇文章将用“酒店前台”这个比喻贯穿全程,带你理解服务发现注册的核心原理、两种发现模式、主流组件选型与工程实践要点。
掌握服务发现注册,是构建可靠微服务基础设施的基石技能。它能让你独立设计服务寻址方案,为团队节省大量排障成本,是通往高级架构师的必修课。
可视化演示
动态演示服务发现注册的核心机制,通过全局/局部/动态/静态四种模式展示关键概念与交互流程。
本节配有交互动效(进入视口后自动加载,约 3KB)。
服务发现注册:服务发现 服务发现的核心问题 服务注册中心架构 核心机制可视化 服务发现注册整体架构与关键组件
服务发现问题
假设酒店没有前台,客人靠一张昨天打印的员工房间表去寻找服务。今早送餐员调去了三楼,保洁阿姨请假了,维修工的号码也换了——客人拿着旧表挨个房间扑空,打电话也没人接。这就是“静态配置”带来的麻烦:地址写死的那一刻,它就已经开始过期。
在微服务出现之前,单体应用部署在固定 IP 的服务器上,地址几乎不变,把 IP 写进配置文件并无大碍。但微服务将应用拆成几十上百个独立进程,每个实例的生命周期短暂而动态:弹性扩缩容、滚动发布、故障重调度,都会让实例的 IP 和端口频繁变化。消费者如果继续把地址“写死”在代码或配置里,就等于攥着一张过期电话簿。
这个问题具体有四个表现。第一,实例漂移:新实例创建后地址对消费者不可见,旧实例销毁后地址却残留在消费端。第二,数量未知:消费者无法预知当前到底有多少可用副本。第三,人工维护成本爆炸:几十个服务、上百个实例,靠人肉改配置既不现实又容易出错。第四,故障感知滞后:一个实例挂了,消费者往往要等到连接超时才发现。解决这一切的关键,是把“地址”变成一种动态查询的资源,而不是静态写死的配置。
为了量化寻址的质量,我们引入服务可用率的概念:
酒店前台存在的意义,就是让那张“员工房间表”永远保持最新。没有前台,客人的每一次呼叫都是一次盲猜;有了前台,可用率才从“碰运气”变成“可预期”。
- $N_{\text{total}}$ 总实例数当前服务下所有已注册实例的数量,例如订单服务一共有 10 个副本,则 $N_{\text{total}}=10$。
- $N_{\text{healthy}}$ 健康实例数通过心跳或健康检查确认仍然存活的实例数量,它排除了已经失联或崩溃的副本。
- $A_{\text{svc}}$ 服务可用率健康实例占全部实例的百分比,是衡量寻址可靠性的核心指标。
注册中心原理
酒店前台的工作流程很清晰:员工入职来登记,离职来销号,所有信息汇总在一本登记表上,客人来问就翻开表。注册中心做的正是这件事——提供一本“活”的登记表,以及一套登记的规矩。
注册中心本质上是一个高可用的分布式键值存储,保存着“服务名 → 实例列表”的映射。它对外提供三个核心操作:服务注册(register) 让服务提供者启动时把自身信息写入注册表;服务注销(deregister) 让实例在优雅停机时主动摘除自己;服务发现(discover) 让消费者按服务名获取当前可用实例列表。服务提供者写入的不仅是 IP 和端口,还包括权重、版本、所属环境等元数据。
注册表中的每个条目可以抽象成一条记录:
前台登记表上的每一行,都写着员工姓名、房间号、在岗时段和联系方式;注册中心的条目也是同一结构,元数据字段则补充了“这个员工能不能做 VIP 服务”之类的附加信息。
- service 与 instanceId(服务名与实例 ID)service 是查找的钥匙,如
order-service;instanceId 是同一服务下每个副本的唯一编号,防止注册条目互相覆盖。 - address 与 metadata(地址与元数据)address 是形如
192.168.1.10:8080的访问入口;metadata 携带权重、版本、可用区等辅助调度信息。 - ttl 与 $t_{\text{last}}$(存活时间与最近心跳)ttl 规定实例允许多久不心跳就被判定失效;$t_{\text{last}}$ 记录最后一次成功心跳的时刻,供过期判断使用。
健康检查机制
前台最怕什么?怕登记表上写着“在岗”,其实人已经溜了。所以靠谱的前台会定时通过对讲机喊一句“XX 在吗?”——应答上了,就继续登记在岗;没有应答,就在表上画个叉,并提醒客人别找他了。这就是健康检查。
服务实例注册之后,并不能保证一直健康。进程假死、网络分区、磁盘故障都可能让实例“失联”。健康检查就是注册中心确认实例存活的机制,常见两种形态:被动心跳上报,由实例周期性向注册中心发送 heartbeat;主动健康探测,由注册中心定期请求实例的 /health 端点,检查返回码是否正常。
心跳机制通常配合 TTL(Time To Live,生存时间)使用。注册中心记录每个实例最近一次心跳时间,一旦发现当前时间与最近心跳时间的间隔超过 TTL,就判定实例过期并从注册表中剔除。判定公式:
前台给员工定的“对讲机报岗”间隔是 5 分钟,超过就当人不在。报岗太勤,员工嫌烦;报岗太懒,客人被骗——这个分寸,就是 TTL 的艺术。
- $t_{\text{now}}$ 当前时间注册中心维护者本地时钟,判断“现在”距离上次心跳过去了多久。
- $t_{\text{last}}$ 最近心跳时间实例最后一次成功上报心跳的时刻,每次心跳都会刷新这个值,相当于续约。
- expire 过期标记当时间差超过 TTL 时,该实例被标记为过期并移出注册表,不再出现在发现结果中。
客户端发现模式
有的客人喜欢自己研究:到前台要一份在岗员工登记表,自己挑一个觉得合适的员工来服务。这叫“客户端发现”——消费者亲自查表,亲自选人。
在客户端发现模式中,服务消费者直接与注册中心通信,拉取目标服务的全部实例列表,再由客户端内置的负载均衡策略(随机、轮询、加权轮询等)选出一个实例发起调用。Netflix OSS 中的 Eureka 与 Ribbon 组合就是典型代表:Eureka 充当注册中心,Ribbon 在客户端完成实例选择。
客户端负载均衡的核心思想,是让权重越大的实例获得越多流量。每个实例被选中的概率为它的权重在总权重中的占比:
除了权重,客户端发现还有两个隐藏要点:一是客户端会本地缓存实例列表,避免每次调用都打注册中心;二是必须处理缓存滞后导致“挑中死实例”的情况,用失败重试和剔除列表兜底。自己查表的好处是选人自由,坏处是要自己背下员工的动向——灵活,但也要承担更多一致性维护责任。
- $w_i$ 实例 i 的权重权重是实例处理能力的量化,比如更强的 CPU、更低的当前负载,就拥有更大的权重值。
- $\sum_{j=1}^{n} w_j$ 总权重和把参与分流的全部实例权重加起来,作为归一化的分母。
- $P_i$ 选中概率实例 i 被选中的概率,权重占比越高,被分配到的请求比例越大。
服务端发现模式
还有些客人懒得研究,直接拨前台电话说“帮我转送餐部门”,前台自己翻表、自己挑一个空闲的员工来接电话。客人从头到尾不知道员工是谁、在哪。这就是“服务端发现”。
在服务端发现模式中,消费者不直接与注册中心交互,而是把请求发给一个中间组件——通常是 API 网关或独立的负载均衡器。由这个组件去查询注册中心、选择合适的实例并完成转发。典型例子是 AWS ELB 与 Consul 结合:客户端只认识 ELB 的固定地址,ELB 内部从 Consul 获取实例列表并做负载均衡。
服务端发现让消费者对实例地址完全透明,但引入了新问题:路由器要替消费者选择“最优”实例。常见策略是最小连接数调度——在所有实例中挑当前活跃连接数最少的那个:
前台转接的好处是客人省心,坏处是所有人都必须先打给前台——一旦前台忙不过来(网关性能瓶颈),整栋楼的电话都会受影响。
- $L_i(t)$ 实例 i 的当前负载可以是活跃连接数、CPU 占用率等实时指标,反映实例此刻的繁忙程度。
- $\arg \min_{i}$ 使目标最小的下标在全部实例中逐一比较负载值,找出负载最低的那个实例的编号。
- $R_{\text{target}}$ 最终转发目标网关计算出的本次请求应该转发到的实例。
组件选型对比
酒店的规模不同,前台的规格也不一样:小旅馆一个人兼收银和门房就够了,五星酒店则要电话总机、礼宾、门房分工协作。微服务注册中心也有各自的“性格”和取舍。
选型时主要围绕三个维度:一致性模型(CP 还是 AP)、功能完备度、生态集成度。CP 模型牺牲部分可用性换取数据强一致;AP 模型优先保证可用性,允许读到短暂的过期数据。两者的本质区别是:网络分区发生时,你是“宁可暂时不给答案”,还是“宁可给一个可能过期的答案”。
主流的四个代表组件各有定位:
- Consul:基于 Raft 的 CP 模型,提供服务发现、健康检查、KV 存储的一站式方案,Web UI 和生态完善。
- Eureka:AP 模型,强调可用性优先,允许读到过期数据,适合“服务不能中断”优先的业务。
- ZooKeeper:CP 模型,强一致但客户端连接成本高,常被当作分布式协调器而非专职注册中心。
- etcd:CP 模型,是 Kubernetes 的内置选型,watch 机制非常适合动态配置与服务发现。
选型时还要考虑注册中心本身的高可用:无论选择哪个组件,生产环境至少要部署 3 个节点形成集群,避免单点故障。CP 中心像严谨的律师——宁可先挂掉也不给错误信息;AP 中心像热心的中介——信息偶尔滞后但绝不耽误客户办事。业务在乎“绝不能错”还是“绝不能停”,决定了选谁。
工程实践要点
酒店运营久了会发现,即使有前台,也逃不过两类尴尬:员工辞职时电话一直响不停(存量连接),大促期间新人涌入导致前台应接不暇(容量压力)。微服务工程实践,就是把运营中这些边角磨平。
生产环境有三个高频实践问题。优雅上下线:实例下线时直接 kill 进程,会让正在处理中的请求断裂,消费者缓存的旧地址也会踩雷,因此要“先摘流量、再排空、后停服务”。多环境隔离:开发、测试、生产环境如果共用注册中心,会互相污染地址,应该用 namespace 或租户隔离。集群高可用:注册中心的节点数、CPU、网络白名单都要纳入 SLA 管理,定期演练故障切换。
优雅下线可以描述为一段受控过渡期:实例先向注册中心主动注销(停止被新流量发现),同时保持进程存活,等待存量请求处理完毕后再退出。整个过程的时间窗口等于“注销后的宽限期 + 在途请求的排空时间”:
好的酒店员工离职时,不会撂挑子就走:先告诉前台不再接新客,把手头正在办理的客人送完,最后才收拾东西离开。微服务的优雅上下线,学的正是这一份“人情味”。
- $t_{\text{grace}}$ 宽限期从注销到开始强制断开之间的缓冲时间,用于容忍消费者本地缓存的延迟刷新。
- $t_{\text{in-flight}}$ 存量请求处理时间正在处理中的在途请求完成所需的最长时间,也就是排空窗口。
- $t_{\text{drain}}$ 总排空时间优雅下线全程需要预留的总时间,是发布系统评估停机窗口的重要依据。
AI 应用场景
场景一:AI 推理服务的弹性寻址
阶段:模型部署与服务化阶段
解决的问题:GPU 推理服务(如 LLM 推理服务)在扩容缩容时,实例地址频繁变化。传统做法是手工维护推理实例的 IP 列表,GPU 节点故障或扩容后地址漂移,导致推理网关把请求发给已销毁的实例,白白浪费算力并拖慢用户体验。引入注册中心后,推理实例启动时自动注册,网关通过服务发现动态获取健康实例,缩容时实例先注销再优雅退出。
具体案例:某 AI 公司部署了 8 个 LLM 推理副本,高峰时动态扩容到 20 个。推理网关(服务端发现模式)从注册中心实时获取实例列表,按负载把用户请求转发给最空闲的 GPU 节点;某节点因 GPU 显存异常心跳超时,注册中心 5 秒内将其剔除,网关自动绕开故障节点。
场景二:AI 训练平台的故障自愈
阶段:模型训练与任务调度阶段
解决的问题:分布式训练中,Parameter Server 和 Worker 节点经常因抢占、抢占式实例回收而动态变化。人工监控 worker 状态不仅滞后,还会让训练框架反复向已失效的节点同步梯度。通过客户端发现模式,训练框架内置注册中心客户端,各节点启动时注册自己的地址和角色,训练调度器通过健康检查持续追踪节点状态,失联节点被自动剔除并从检查点恢复。
具体案例:某大模型训练任务使用 32 个 GPU Worker,某 Worker 所在物理机因断电离线。注册中心检测到心跳超时后剔除该 Worker,训练框架感知节点变化,将其负责的数据分片重新分配给其他存活 Worker,并从最近的 checkpoint 恢复训练,整个自愈过程无需人工介入。
Python 演示
import time class SimpleRegistry: """一个极简的服务注册中心:模拟注册、心跳、TTL 过期剔除与发现。""" def __init__(self, ttl=5.0): # {service_name: {instance_id: {"address": str, "last_heartbeat": float}}} self.services = {} self.ttl = ttl def register(self, service_name, instance_id, address): self.services.setdefault(service_name, {})[instance_id] = { "address": address, "last_heartbeat": time.time() } def deregister(self, service_name, instance_id): if service_name in self.services: self.services[service_name].pop(instance_id, None) if not self.services[service_name]: del self.services[service_name] def heartbeat(self, service_name, instance_id): if service_name in self.services and instance_id in self.services[service_name]: self.services[service_name][instance_id]["last_heartbeat"] = time.time() return True return False def _evict_expired(self): """扫描并剔除所有超过 TTL 未心跳的实例。""" now = time.time() for service_name in list(self.services.keys()): for instance_id in list(self.services[service_name].keys()): if now - self.services[service_name][instance_id]["last_heartbeat"] > self.ttl: self.services[service_name].pop(instance_id) if not self.services[service_name]: del self.services[service_name] def discover(self, service_name): """客户端发现入口:先清理过期实例,再返回可用地址列表。""" self._evict_expired() return [info["address"] for info in self.services.get(service_name, {}).values()] def all_instances(self): self._evict_expired() return {s: list(v.keys()) for s, v in self.services.items()} def main(): registry = SimpleRegistry(ttl=3.0) registry.register("order-service", "order-1", "192.168.0.11:8080") registry.register("order-service", "order-2", "192.168.0.12:8080") print("注册后发现:", registry.discover("order-service")) time.sleep(2) print("2秒后(未超时):", registry.discover("order-service")) time.sleep(2) print("4秒后(超时剔除):", registry.discover("order-service")) registry.register("pay-service", "pay-1", "192.168.0.21:9090") registry.heartbeat("pay-service", "pay-1") print("pay-1 心跳续租后:", registry.all_instances()) if __name__ == "__main__": main()
self.services.setdefault(service_name, {})[instance_id] = {...}用嵌套字典保存“服务名 → 实例 → 地址与时间戳”,setdefault自动创建不存在的服务分组。self._evict_expired()惰性淘汰函数,在每次发现前扫描全表,是 TTL 机制的核心执行者。if now - ...["last_heartbeat"] > self.ttlTTL 过期判定,与正文章节中的公式完全对应。discover()` 先调用 `_evict_expired()所有读取路径先过滤失效实例,保证消费者拿到的永远是可用的地址列表。heartbeat()` 刷新 `last_heartbeat每次心跳相当于给实例“续命”,是健康检查被动模式的落地。
常见误区
误区总担心注册中心是唯一的命门——只要它一宕机,整片微服务集群就全部失联,全部接口返回错误。
正解注册中心通常部署 3 节点及以上集群,单点故障不会导致整体不可用。即使极端情况下注册中心完全失联,消费者本地缓存的实例列表仍可继续工作一段时间,已建立的连接也不受影响。这就像酒店前台虽然停电了,但已经拿到登记表的客人仍然知道该去哪个房间找人。落地时还会给消费者配置“缓存 + 重试 + 直连兜底”三层降级策略,确保注册中心故障只是“寻址能力降级”,而不是“全站不可用”。
为何常见:因为把注册中心理解成了“唯一请求入口”(类似网关),而忽略了发现结果可以被消费端缓存这一层设计。
误区心跳是保证服务健康的关键,所以恨不得每 100 毫秒就发一次心跳,认为盯得越紧,服务就越可靠。
正解心跳是有实际成本的——每次上报都是一次网络 I/O,高频心跳会占用注册中心的连接和 CPU 资源,在数千实例规模下会被放大成显著开销。实践上 TTL 通常设置在 5~15 秒,配合主动探测做辅助,足以在“故障发现速度”和“资源成本”之间取得平衡。就像酒店前台不会每分钟给每个员工打一次对讲机,那样员工没法干活,前台也会被占用到无法接听客人来电。
为何常见:把“心跳”误解为免费的保险行为,没有从系统整体资源消耗的角度评估频次。
误区反正都是服务发现,两种模式差不多,选个顺眼的就行,换着用也没关系。
正解两种模式的架构位置完全不同。客户端发现把查询和负载均衡下放到应用内部,实现灵活、无额外链路,但对开发语言有侵入性,且每个消费者都要处理缓存一致性;服务端发现把寻址收敛到网关,对客户端完全透明,统一治理方便,却引入了额外的网络跳数,网关成为性能和可用性瓶颈。二者各有所长,需要根据团队的技术栈和运维能力选择,不能“随便混用”。
为何常见:只看到两种模式“都能找到服务”的同一面,忽略了它们对架构各层职责分工的根本差异。
误区以为实例调用 `deregister()` 的瞬间,所有消费者就立刻不再向它发请求了,连接会被立刻断开。
正解注册中心的数据更新需要传播时间,而且消费者的本地缓存不会实时刷新,旧地址仍有短暂存活期。直接 kill 进程会让这些“残余流量”和新到的存量连接全部失败。正确的做法是优雅下线:先注销(停止被发现),保留进程继续处理在途请求,等待排空窗口结束,最后才退出进程。这也就是正文排空时间公式要解决的问题。
为何常见:把注册中心想象成了全局实时广播,忽略了分布式系统中“最终一致”的传播延迟。
面试题
Q1. 服务发现解决的核心问题是什么?
- A.让服务实例的地址变得稳定可预测
- B.让消费者自动找到动态变化的服务地址← 正确答案
- C.完全替代负载均衡器完成流量转发
- D.加密所有微服务之间的通信链路
答案:B。核心问题是实例地址动态漂移,消费者需要一种动态寻址机制。A 把目标搞反了——地址本来就动态;C 是负载均衡的职责;D 是服务网格 TLS 的职责。服务发现做的是“找得到健康实例”,正如客人靠前台找到当前在岗的员工,而不是让员工永远住在固定房间。
Q2. 注册中心里最核心的数据结构是什么?
- A.哈希表(字典)实现的服务注册表← 正确答案
- B.线性数组存储的请求队列
- C.单链表组织的日志记录
- D.平衡二叉树维护的索引
答案:A。注册中心存储“服务名 → 实例列表”的映射,本质是键值存储(KV),哈希表/字典是最直接的数据结构。数组、链表、二叉树都不是“按名字查实例”的自然模型。如同前台登记表按员工姓名翻页查找,而不是按入职顺序从头扫到尾。
Q3. 健康检查中心跳机制的主要作用是什么?
- A.显著提升服务实例的运行性能
- B.确认实例存活并及时剔除失联实例← 正确答案
- C.加速服务启动时的加载速度
- D.自动加密消费者与提供者的通信
答案:B。心跳是实例向注册中心周期性上报“我还活着”的信号,注册中心据此判断实例是否健康,超时未上报则剔除。它不提升性能、不影响启动速度、也不负责加密。就像前台通过对讲机确认员工还在岗,纯属“存活确认”,不是提升员工工作效率的手段。
Q4. 客户端发现与服务端发现模式的主要区别是什么?
- A.客户端发现模式完全不需要注册中心
- B.客户端自行查询注册中心并选择实例← 正确答案
- C.服务端发现模式对客户端完全不可用
- D.两种模式的实现方式完全一致
答案:B。客户端发现中,消费者直接查注册中心并在应用内做负载均衡;服务端发现中,消费者只把请求交给网关,由网关查注册中心并选实例。A 错在客户端发现也需要注册中心;C 错在客户端只需要调用固定地址,可用性反而更高;D 显然不对。类比:前者是客人自己翻登记表,后者是让前台转接。
Q5. TTL 过期判定的核心逻辑是什么?
- A.实例心跳次数超过预设的固定阈值
- B.当前时间与最近心跳时间的差超过 TTL← 正确答案
- C.服务接口响应时间超过预设超时值
- D.注册表中的条目数量超过容量上限
答案:B。TTL 判定是时间差比较:t_now - t_last > TTL 则过期。心跳次数、响应超时、条目总数都是干扰项——心跳次数可能是“持续在线但很久没报”的漏网之鱼,响应超时关心的是单次请求质量,条目数量则与容量规划相关。就像前台判断员工失联的标准是“多久没报岗”,而不是“报过多少次岗”。
Q6. 服务端发现模式中充当“代理路由器”角色的通常是哪个组件?
- A.应用容器运行时
- B.API 网关或独⽴负载均衡器← 正确答案
- C.消息队列中间件
- D.分布式关系型数据库
答案:B。服务端发现的核心是“消费者 → 中间代理 → 实例”,API 网关或负载均衡器正是这个中间代理,它查询注册中心并转发请求。容器运行时只是实例的运行环境,消息队列用于异步解耦,数据库用于持久化业务数据。如同客人拨打总机电话,总机(网关)负责查表并转接。
Q7. 以下哪些属于健康检查的常见实现方式?(多选)
- A.实例主动上报心跳← 正确答案
- B.注册中心主动探测健康端点← 正确答案
- C.分析应用日志中的错误关键字
- D.基于 TTL 判定心跳是否超时← 正确答案
答案:ABD。健康检查三件套是心跳上报(被动)、主动探测(主动)、TTL 过期判定(规则)。日志分析属于可观测性领域,它只能事后发现错误,不是注册中心判断实例存活的机制。前台确认员工在岗靠的是“员工自己报岗”和“前台主动呼叫”,而不是翻看员工昨天的工作日志。
Q8. 注册中心组件选型时需要考虑哪些要素?(多选)
- A.一致性模型 CP 还是 AP← 正确答案
- B.集群高可用部署形态← 正确答案
- C.办公协同软件的版本
- D.生态集成与运维成本← 正确答案
答案:ABD。一致性和可用性的取舍(CP/AP)、集群部署方式、生态与运维成本,是注册中心选型的三大维度。办公软件版本与基础设施选型毫无关系。就像选酒店前台管理系统时,你关心它会不会算错账(一致性)、能不能轮班不倒班(高可用)、培训成本高不高(运维),而不是关心它跟什么牌子的打印机搭配。
Q9. 客户端发现模式下,消费者本地缓存实例列表后需要处理哪些问题?(多选)
- A.缓存过期后的主动刷新← 正确答案
- B.剔除列表中已失效的实例← 正确答案
- C.在本地做客户端侧负载均衡← 正确答案
- D.对服务端数据进行全量加密
答案:ABC。使用本地缓存后,消费者要自己负责三件事:定时刷新缓存、剔除失效实例、在缓存列表上做负载均衡。数据加密是传输层或网格层的职责,与“客户端发现”这个模式无关。如同客人自己留了一份员工名单副本,就要自己更新名单、划掉离职的人、然后自己挑一个来打电话。
Q10. 当注册中心发生网络分区时,基于 CP 模型(如 ZooKeeper)可能出现什么情况?
- A.牺牲可用性,少数分区节点拒绝服务← 正确答案
- B.所有分区都继续正常接受注册请求
- C.注册中心立即清空全部已存数据
- D.分区恢复后数据自动合并且无感知
答案:A。CP 模型在分区时优先保证一致性,少数派节点会停止写服务,等待分区恢复或 Leader 选举完成,表现为“宁可不可用也不错”。B 是 AP 模型的行为;C 是数据丢失事故而非设计行为;D 描述得过于理想,分区恢复后通常需要重新同步。如同律师型前台在停电时会暂停办理业务,而不是随便记一笔可能错的账。