聊聊微服务的服务注册与发现

聊起微服务的服务注册与发现,很多人立马就会脱口而出 zk、etcd、consul、eureka 这些组件,进而聊到 CAP 如何取舍,性能如何,高可用和容灾是怎么实现的。

引言

聊起微服务的服务注册与发现,很多人立马就会脱口而出 zk、etcd、consul、eureka 这些组件,进而聊到 CAP 如何取舍,性能如何,高可用和容灾是怎么实现的。

在这之前,站在组件使用者的角度,我想先问这么几个问题:

注册的 IP 和端口怎么确定 ?

实现服务治理还需要注册哪些信息 ?

如何进行优雅的服务注册与服务下线 ?注册服务的健康检查是如何做的 ?当服务有节点退出或新的节点加入时,订阅者能不能及时收到通知 ?我能方便地查看某个应用发布和订阅了哪些服务,以及所订阅的服务有哪些节点吗 ?

看完这些问题后,您也许会发现,对于服务注册与发现,首先应该关注的是服务注册发现本身的功能,然后才是性能和高可用。

一个好的服务注册发现中间件,应该是能完整地满足服务开发和治理的基础功能,然后才是性能和高可用。如果没有想清楚前面的功能,再高的可用性和性能都是浮云。最后,安全也同样重要。

服务端的性能如何 ?

服务发现的容灾策略是怎样的 ?

当我的应用和服务发现中心的网络连接出现问题时,会对我的调用产生什么影响 ?服务注册中心某台机器宕机或者全部宕机时,会对我的调用产生什么影响 ?服务注册和发现的链路安全吗,有没有做好权限控制 ?

下面将从 服务注册、服务发现、容灾和高可用三个大方面来回答这些问题的主流做法。

最后会介绍一下 ANS(Alibaba Naming Service) , ANS 综合了这些解决方案中的优点,并在 EDAS(阿里巴巴企业级分布式应用服务) 中输出,目前完全免费!

服务注册

注册的 IP 和端口怎么确定 ?

IP 如何确定

主流的 IP 获取有这几种方法。

最简单粗暴的方式,手动配置需要注册的IP。当然这种方式基本无法在生产环境使用,因为微服务基本都是支持水平扩容多机部署的,在配置中写死 IP 地址的方式无法支持一份代码水平扩容,会给运维带来极大的成本。

通过遍历网卡的方式去获取,找到第一个不为本地环回地址的 IP 地址。绝大多数情况下,这个方式比较好用,dubbo 等框架采用的就是这种方法。

在一些网络规划比较好的标准化机房中,我们还可以通过手动指定网卡名,即 interfaceName 的方式来指定使用哪一块网卡所对应的 IP 地址进行注册。当上述三种方式都不能有效解决问题的时候,有一个方法就是直接与服务注册中心建立 socket 连接,然后通过socket.getLocalAddress() 这种方式来获取本机的 IP。

端口如何确定

端口的获取,没有标准化的方案。

如果是 RPC 应用,启动的时候都有一个配置来指定服务监听的端口, 注册的时候直接使用配置项的端口值。

传统的 WEB 容器所提供的 HTTP 的应用,同样也存在一个配置文件来配置容器的监听端口,注册时候直接使用配置项的端口值。

特别的,在 Java 应用的 Spring Boot 框架中,可以通过 EmbeddedServletContainerInitializedEvent. getEmbeddedServletContainer().getPort()来获取。(Spring Boot 版本为 1.x)。

实现服务治理还需要注册哪些信息 ?

简单地将 IP 和 port 信息注册上去,可以满足基本的服务调用的需求,但是在业务发展到一定程度的时候,我们还会有这些需求:

想知道某个 HTTP 服务是否开启了 TLS。

对相同服务下的不同节点设置不同的权重,进行流量调度。

将服务分成预发环境和生产环境,方便进行AB Test功能。不同机房的服务注册时加上机房的标签,以实现同机房优先的路由规则。

这些高级功能的实现,本质上是依赖于客户端调用时候的负载均衡策略和调用策略,但是如果服务元数据没有注册上来,也只能是巧妇难为无米之炊。一个良好的服务注册中心在设计最初就应该支持这些扩展字段。

如何进行优雅的服务注册与服务下线 ?

优雅发布

虽然服务注册一般发生在服务的启动阶段,但是细分的话,服务注册应该在服务已经完全启动成功,并准备对外提供服务之后才能进行注册。

有些 RPC 框架自身提供了方法来判断服务是否已经启动完成,如 Thrift ,我们可以通过 Server.isServing() 来判断。

有一些 RPC 框架本身没有提供服务是否启动完成的方式,这时我们可以通过检测端口是否已经处于监听状态来判断。

而对于 HTTP 服务,服务是否启动完毕也可以通过端口是否处于监听状态来判断。特别的,在 Java 应用的 Spring Boot 框架中,可以通过事件通知的形式来通知容器已经启动完毕, EmbeddedServletContainerInitializedEvent 事件来通知容器已经启动完成 (Spring Boot 版本为 1.x)。

优雅下线

绝大多数的服务注册中心都提供了健康检查功能,在应用停止后会自动摘除服务所对应的节点。但是我们也不能完全依赖此功能,应用应该在停止时主动调用服务注册中心的服务下线接口。

在 Java 应用中,通用的服务下线接口调用一般使用 JVM Shutdown Hook 的方式来实现。

特别的,在 Java 应用中的 Spring 框架中,可以通过 Spring Bean LifeCycle 来实现应用停止时主动调用服务下线接口。

当然上述两种方式还不够优雅,因为不能确保不出现 kill -9 这种粗暴的停止方式,而且应用调用服务下线接口也是尝试去调用,对于网络不通等异常场景并没有做异常处理。因此,调用客户端仍应该做好负载均衡与 failover 的处理。更优雅的方式,先将即将停止的应用所对应的权重调成 0,此时上游将不再调用此应用。这时候的停止应用的操作对服务订阅者完全没有影响,当然这种场景需要订阅者实现按权重的负载均衡和运维部署工具深度结合。

服务的健康检查是如何做的 ?

健康检查分为客户端心跳和服务端主动探测两种方式。

客户端心跳

客户端每隔一定时间主动发送“心跳”的方式来向服务端表明自己的服务状态正常,心跳可以是 TCP 的形式,也可以是 HTTP 的形式。

也可以通过维持客户端和服务端的一个 socket 长连接自己实现一个客户端心跳的方式。

ZooKeeper 并没有主动的发送心跳,而是依赖了组件本身提供的临时节点的特性,通过 ZooKeeper 连接的 session 来维持临时节点。

但是客户端心跳中,长连接的维持和客户端的主动心跳都只是表明链路上的正常,不一定是服务状态正常。

服务端主动调用服务进行健康检查是一个较为准确的方式,返回结果成功表明服务状态确实正常。

服务端主动探测服务端调用服务发布者某个 HTTP 接口来完成健康检查。对于没有提供 HTTP 服务的 RPC 应用,服务端调用服务发布者的接口来完成健康检查。可以通过执行某个脚本的形式来进行综合检查。

服务端主动探测也存在问题。服务注册中心主动调用 RPC 服务的某个接口无法做到通用性;在很多场景下服务注册中心到服务发布者的网络是不通的,服务端无法主动发起健康检查。

所以如何取舍,还是需要根据实际情况来决定,根据不同的场景,选择不同的策略。

服务发现

怎么找到服务发现服务端的地址?

在应用的配置文件中指定服务注册中心的地址,类似于 zookeeper 和 eureka。指定一个地址服务器的地址,然后通过这个地址服务器来获取服务注册中心的地址,地址服务器返回的结果会随着服务注册中心的扩缩容及时更新。

当服务有节点退出或新的节点加入时,订阅者如何及时收到通知 ?

很经典的 Push 和 Pull 问题。

Push 的经典实现有两种,基于 socket 长连接的 notify,典型的实现如 zookeeper;另一种为 HTTP 连接所使用 Long Polling。

但是基于 socket 长连接的 notify 和基于 HTTP 协议的 Long Polling 都会存在notify消息丢失的问题。

所以通过 Pull 的方式定时轮询也必不可少,时间间隔的选择也很关键,频率越高服务注册中心所承受的压力也越大。需要结合服务端的性能和业务的规模进行权衡。

还有一种方式,真实的 Push,客户端开启一个 UDP server,服务注册中心通过 UDP 的方式进行数据推送,当然这个也受限于网络的连通性。

我能方便地查看我发布和订阅了哪些服务,订阅的服务有哪些节点吗 ?

一个好的产品,用户使用体验和运维体验必须是优雅的,如果查看本机发布和订阅的服务,只能通过查看日志,甚至是 jmap 的方式来获取,显然体验非常糟糕。

服务注册中心应该提供了丰富的接口,支持根据应用名、IP、订阅服务名、发布服务名,来进行多层次的组合查询。

同时,客户端的内存里,同样也应该保留服务发布与订阅的各种信息,并提供方式供人方便地查询。比如在 Java 中的 Spring Boot 的应用,可以结合 actuator endpoint,通过 HTTP 的方式来提供本机服务查询功能,查询此应用发布的服务,以及订阅的服务及各服务的对应节点。

容灾和高可用

性能如何

当服务节点数越来越多时,服务注册中心的性能会成为瓶颈,这时候就需要通过水平扩容来提升服务注册中心集群的性能。

对于那些采用了类 Paxos 协议的强一致性的组件,如ZooKeeper,由于每次写操作需要过半的节点确认。水平扩容不能提升整个集群的写性能,只能提升整个集群的读性能。 而对于采用最终一致性的组件来说,水平扩容可以同时提升整个集群的写性能和读性能。

客户端容灾策略

首先,本地内存缓存,当运行时与服务注册中心的连接丢失或服务注册中心完全宕机,仍能正常地调用服务。

然后,本地缓存文件,当应用与服务注册中心发生网络分区或服务注册中心完全宕机后,应用进行了重启操作,内存里没有数据,此时应用可以通过读取本地缓存文件的数据来获取到最后一次订阅到的内容。

最后,本地容灾文件夹。正常的情况下,容灾文件夹内是没有内容的。当服务端完全宕机且长时间不能恢复,同时服务提供者又发生了很大的变更时,可以通过在容灾文件夹内添加文件的方式来开启本地容灾。此时客户端会忽略原有的本地缓存文件,只从本地容灾文件中读取配置。

服务端容灾与高可用

当有新节点加入集群时,节点启动后能自动添加到地址服务器中,并通过地址服务器找到其他节点,自动从其他节点同步数据,以达到数据的最终一致性。 当某个节点宕机时,此服务注册中心节点的信息会自动地址服务器中摘除,客户端能及时感知到此节点已下线。

服务端的无状态性保证了服务的容灾和高可用可以做的很薄。

服务端安全是如何做的 ?

链路安全,对于使用 HTTP 连接的服务注册中心,保护链路安全的最好方式是使用 HTTPS。而使用 TCP 连接的服务注册中心来说,由于应用层协议一般使用的是私有协议,不一定存在现成的 TLS 支持方案。

在业务安全方面,应该在每一次的发布、订阅、心跳,都带上鉴权的信息就行验签和鉴权,确保业务信息的安全性。

Alibaba Naming Service

ANS (Alibaba Naming Service) 是阿里巴巴中间件团队将多年业务实践沉淀打磨的开源产品。在服务注册与发现方面,ANS 综合了上述解决方案中的优点,是最适合云原生应用的服务注册与发现组件。
ANS 服务已经在 EDAS(阿里巴巴企业级分布式应用服务) 上线,目前已经提供 Spring Cloud Ans Starter 方便 Spring Cloud 用户直接使用一个安全的可靠的商业版服务注册与发现功能。ANS 能完美地支持 Eureka 的特性,而且目前完全免费!更多信息参见 EDAS 帮助文档。

文章来源:

Author:阿里中间件
link:http://jm.taobao.org/2018/06/26/聊聊微服务的服务注册与发现/