服务注册与发现:服务怎么找到彼此

8 阅读 2359 字 · 约 8 分钟

订单服务要调库存服务,库存服务的地址是什么?

单体架构里这不是问题,函数调用不需要地址。微服务拆开之后,订单服务和库存服务跑在不同机器上,订单服务得知道库存服务在哪。最简单的做法:把库存服务的 IP 写在配置文件里。

这能跑,直到有一天库存服务换了机器。改配置、发版、重启所有调用方。三台机器还好,三十台就有的等了。如果机器是动态扩缩容的,高峰期加机器、低峰期减机器,配置文件根本追不上。

服务注册与发现解决的就是这个问题:让服务自动知道彼此在哪,不用人工配地址。

注册:我在这里

每个服务启动时,把自己的地址告诉一个中心组件,注册中心。注册中心维护一张表:服务名 → 可用实例列表。服务下线时,从表里删掉。

DNS 解析的是域名到 IP,更新周期以分钟甚至小时计。注册中心维护的是服务实例列表,更新周期是秒级。DNS 适合不常变的地址,注册中心适合频繁上下线的服务实例。

健康检查是注册中心的核心职责。一个服务注册了,但可能下一秒就挂了。注册中心不能只管登记不管存活,得定期检查每个实例是否还活着。心跳机制:服务定期发心跳,注册中心超过一定时间没收到,就认为这个实例死了,从列表里摘掉。

心跳间隔是个参数。太短,网络抖一下就误判死了。太长,真挂了要等很久才发现,中间的请求还往死节点上发。通常几秒到十几秒,看业务对发现速度的要求。

发现:谁在

调用方需要调某个服务时,不直接连目标服务的 IP,而是先问注册中心:这个服务有哪些可用实例?拿到列表后,选一个发请求。

注册中心给的是候选列表,负载均衡器从候选里选一个。发现负责"有哪些",负载均衡负责"选哪个",两者配套。

两种发现模式。

客户端发现。 调用方直接问注册中心,自己拿实例列表,自己做负载均衡。Netflix Eureka 的客户端模式。少一跳,调用方直接连目标服务。每个语言的客户端都要实现一遍发现和负载均衡逻辑,语言多了维护成本高。

服务端发现。 中间放一个代理,调用方把请求发给代理,代理查注册中心选一个实例转发。Envoy、Nginx Plus、Kubernetes Service 都是这个模式。调用方不需要知道注册中心的存在,只管发请求给代理。多一跳网络,但客户端逻辑简单,任何语言只需要发 HTTP 请求。

注册中心挂了怎么办

注册中心是中心组件。它挂了,新服务注册不了,调用方也发现不了新实例。但已经拿到的实例列表还在调用方本地缓存着,不会立刻全断。

注册中心挂了十分钟,这十分钟内已经跑着的调用不受影响,但新上线的服务没人知道,下线的服务还被人调。

为了不让注册中心自己成单点,它通常也是集群部署。但和普通服务不同,注册中心集群的一致性要求更高,两个注册中心节点给的实例列表不一致,调用方拿到不同的列表,流量分配就乱了。

Zookeeper 和 etcd 用共识协议(ZAB 和 Raft)保证强一致。所有写操作经过 leader,多数节点确认后才返回。读也能保证线性一致。代价是延迟,每次注册和注销都要走一轮共识。对于服务上下线频率不高的场景,这个延迟可以接受。

Eureka 走了另一条路,AP 模式,放弃强一致。Eureka 节点之间异步复制,不保证同一时刻所有节点数据一致。任何一个节点都能独立工作,注册中心不会因为网络分区而拒绝服务。代价是可能短暂给出过时的实例列表。某个实例已经下线了,某个 Eureka 节点还不知道,还在返回它的地址。

注册中心选 CP 还是 AP,看业务能接受什么。能接受短暂返回死实例(反正调用方有重试和熔断兜底),选 AP。不能接受任何过时数据(比如配置管理中心),选 CP。

真实系统里的配合

Kubernetes 的服务发现是一套组合拳。Pod 创建时自动注册,Service 提供虚拟 IP,kube-proxy 负责把虚拟 IP 的流量转发到真实 Pod。Pod 挂了自动从 Endpoints 里摘掉。整个过程不需要应用感知,应用只管访问 Service 的虚拟 IP,Kubernetes 在背后完成发现和负载均衡。

Spring Cloud + Eureka 是 Java 生态的经典组合。服务启动时向 Eureka 注册,调用方通过 Eureka Client 拿实例列表,Ribbon 做客户端负载均衡。这套东西在 Java 里无缝集成,但换一门语言就得重新搭一套客户端。

Service Mesh(Istio、Linkerd)把服务发现从应用层挪到了基础设施层。每个 Pod 旁边跑一个 Envoy 代理,所有流量走代理。代理负责服务发现、负载均衡、重试、熔断,应用什么都不用管,只管发请求。代价是多了一层代理,延迟增加,运维复杂度上升。

选错了的代价

一个团队用了 Zookeeper 做注册中心。服务实例不多,几十个,但每次发布都要批量重启。重启时大量服务同时注销再注册,Zookeeper 的写压力陡增,每个注销和注册都是一次共识协议的写操作。Zookeeper 集群扛不住,响应变慢,调用方拿不到新的实例列表,发布窗口期内大量请求失败。

换成了 Eureka 之后没这个问题。Eureka 的异步复制不阻塞写操作,大量实例同时上下线也不会压垮注册中心。代价是短暂的数据不一致,某个 Eureka 节点可能晚几秒才看到新实例。但对于服务发现这个场景,晚几秒不是问题,发布期间的可用性更重要。

反过来,如果一个配置管理中心用 Eureka,就选错了。配置变更要求强一致,改了一个配置,有的节点看到了有的没看到,行为不一致。这种场景就该用 Zookeeper 或 etcd。

小结

服务注册与发现让服务自动感知彼此的地址变化,不用人工配。注册中心维护服务名到实例列表的映射,靠心跳检测存活。发现分为客户端发现和服务端发现,前者少一跳后者更通用。注册中心自身的高可用靠集群,CP 还是 AP 取决于业务。能接受过时数据选 AP,不能接受选 CP。和负载均衡是配套关系,发现负责"有哪些",负载均衡负责"选哪个"。