聊一聊微服务架构中的服务发现系统

发布于:2020 年 5 月 30 日 10:06

聊一聊微服务架构中的服务发现系统

导语:本文围绕服服务调用模式、一致性取舍、服务提供者的健康检查模式等方面,讨论了服务发现的技术选型和设计的各种优缺点,希望能够帮助大家在选择或者使用服务发现系统的时候更加顺畅。

01 服务发现系统的背景

不知道大家刚接触微服务治理的时候是否有这样的疑惑:为什么一定需要一个服务发现系统呢?服务启动的时候直接读取一个本地配置,然后通过远程配置系统,动态推送下来不行吗?实际上,当服务节点规模较小时,该方案也行得通,但如果遇到以下的场景呢?

  1. 在微服务的世界中,服务节点的扩缩容、服务版本的迭代是常态,服务消费端需要能够快速及时的感知到节点信息的变更(网络地址、节点数量)。
  2. 当服务节点规模巨大时,节点的不可用也会变成常态,服务提供者要能够及时上报自己的健康状态,从而做到及时剔除不健康节点(或降低权重)。
  3. 当服务部署在多个可用区时,需要将多个可用区的服务节点信息互相同步,当某个可用区的服务不可用时,服务消费者能够及时切换到其他可用区(通过负载均衡算法自动切换或手动紧急切换),从而做到多活和高可用。
  4. 服务发现背后的存储应该是分布式的,这样当部分服务发现节点不可用的时候,也能提供基本的服务发现功能
  5. 除了 ip、port 我们需要更多的信息,比如节点权重、路由标签信息等等。

使用文件配置或 DNS 等传统方式无法同时满足上述几点要求,因此我们需要重新设计一个能够匹配上微服务架构的服务发现系统。

02 服务间调用模式

客户端发现模式

由客户端负责向服务发现系统(可以认为是一个数据库,存储了所有服务提供者的所有节点位置信息)询问某个服务提供者的所有实例的 ip、port 信息,并采用某种负载均衡策略,直接发起对服务实例的访问。

其中一个经典代表就是 Netflix 提供的解决方案:Netflix Eureka 提供服务发现功能, Netflix Ribbon 作为一个通讯 SDK 库与客户端集成在一起提供负载均衡与故障转移。

聊一聊微服务架构中的服务发现系统

这种模式去除了对中心化单点 (API Gateway or Load Balancer) 的依赖,可以避开单点造成的性能瓶颈与故障问题,同时由于负载均衡的逻辑在客户端,它可以根据自身的配置选择负载均衡算法,比如一致性 Hash 算法。不过这种模式也存在缺陷,由于客户端的负载均衡逻辑是分布式的,各自为政,没有全局统一视角,在某些情景下会因为客户端的高度竞争而导致后端服务提供者节点的负载不均衡。同时客户端的业务逻辑和服务发现的逻辑耦合在一起, 不同的服务使用了不同的编程语言,那么就需要有不同语言的 SDK,如果未来某天服务发现的逻辑变更了,也需要重新发布所有的客户端节点。

服务端发现模式

把原本客户端执行的服务列表拉取 & 负载均衡 & 熔断 & 故障转移这部分逻辑抽象变成一个专属的服务。不过跟传统的 load balancer 不大一样的地方是: 这个的 load balancer 会跟服务发现系统密切的配合,实时订阅服务发现系统中服务提供者节点列表信息,扮演反向代理的角色,将请求分发到合适的 Endpoint。

这块的一个代表是 kubernetes 的服务发现解决方案:运行在每个 Node 节点的 kube-proxy 会实时的 watch Services 和 Endpoints 对象。每个运行在 Node 节点的 kube-proxy 感知到 Services 和 Endpoints 的变化后,会在各自的 Node 节点设置相关的 iptables 或 IPVS 规则,方便后面用户通过 Service 的 ClusterIP 去访问该 Service 下的服务。当 kube-proxy 把需要的规则设置完成之后,用户便可以在集群内的 Node 或客户端 Pod 上通过 ClusterIP 经过 iptables 或 IPVS 设置的规则进行路由和转发,最终将客户端请求发送到真实的后端 Pod。

聊一聊微服务架构中的服务发现系统

这种模式对于客户端来说是透明的,所有细节都被隔离在 load balancer 跟服务发现系统之间, 因此也沒有前面跨语言等相关问题,更新相关逻辑也只要統一部署 load balancer & service registry 就足够了。很明显,这种模式下服务的架构等于多了一层转发,延迟事件会增加;整个系统也多了一个故障点,整体系統的运维难度会提高;另外这个 load balancer 也可能会成为性能瓶颈。

基本上服务端发现模式我们平常接触到的机会比较少,但是由于是无任何入侵的,比较适合旧系统上微服务架构的一个过渡方案。

03 服务发现的一致性取舍

我们先回顾一下 CAP 定律:在一个分布式系统中,Consistency(一致性)、Availability(可用性)、Partition Tolerance(分区容错性),不能同时成立。

  • 一致性:它要求在同一时刻点,分布式系统中的所有数据备份都处于同一状态。
  • 可用性:在系统集群的一部分节点宕机后,系统依然能够响应用户的请求。
  • 分区容错性:在网络区间通信出现失败,系统能够容忍。

对基于 raft、paxos 算法的 CP 服务发现系统比如 Consul、Zookeeper、Etcd 等,为了保证数据的线性强一致性(Linearizable),必然会牺牲掉高可用性,比如在网络分区的情况下心跳、注册、反注册这些操作都会超时并失败。同时由于一致性算法的要求,所有的写请求都会重定向至 leader 节点,那么这样无法做到写的水平扩展。而 AP 服务发现比如 Eureka 则强调最终一致性(在有限的时间内(例如 3s 内)将数据收敛到一致状态),在牺牲数据一致性的情况下最大程度保障服务的可用性。

聊一聊微服务架构中的服务发现系统

zookeeper 服务发现

可以考虑上图的跨机房容灾的情景,此时满足强一致要求的 Zookeeper 作为服务发现。如果机房 1 和机房 2 由于某些不稳定的原因发生网络断开,provider B 去往 Zookeeper Follower 的注册是无法实现的。因为 Zookeeper Follower 所有的请求是强一致,都有同步到 ZK Leader,这时机房 2 就无法注册了,但此时其实 Consumer B 和 Provider B 之间的网络是正常的,互相调用没有问题,可 Provider B 不能注册导致 Consumer B 无法访问 Provider B。所以我们可以发现,服务发现系统首先应当保证的服务可用性,为了保证数据一致性却不能提供注册功能,在生产实践中是不能接受的。 当然我们也可以在两个机房独立的部署两套 Zookeeper,然后再写一个工具互相同步数据,使得两个机房的 Zookeeper 互为 Master Slave,但这样不仅引入了新的复杂度,同时还得花大力气保证数据同步的一致性。

那么引入一个最终一致的 Netflix Eruka 的最终一致性设计是否就满足所有的场景万事大吉了呢?让我们设想这种情景:

聊一聊微服务架构中的服务发现系统

Eruka serverA 和 Eruka serverB 之前互相同步数据,但此时 Eruka serverC 和 Eruka serverB、Eruka serverA 之间的网络发生了故障,无法顺利同步信息。

ProviderB 向 Eruka serverA 注册了服务信息,并维持上报心跳,这样服务节点 ProvderB 的信息 Eruka serverA 和 Eruka serverB 中都是存在的,但是由于信息复制的问题,没办法同步到 Eruka serverC 中。这样当 ConsumerA 先向 eruka serverA 发起请求的时候,会得到一个正确的节点信息,但是当下次访问到 Eruka serverC 的时候又会得到一个错误的节点信息,这样之前正确的信息就被覆盖了。

那么为了避免上述的情况,我们需要改造上面的逻辑,Client SDK 需要同时去访问三个 eruka server 节点,再拿到三个节点返回 providerB 的节点信息中的的 dirty time(dirty time 由 ProviderB 维护,心跳上报的时候夹带,这样可以保证单调自增)后,通过比较选取 dirty time 最新的那个信息,这样就可以保证访问到正确的信息。当然上述情景是在生成环境中很难遇到,因为大多数情况下 eruka server 和 Provider、Consumer 都部署在同一个机房,如果 eruka serverC 和其他 eruka server 节点网络通信有问题的话,ConsumerA 大概率也是访问不到 eruka serverC 的;又如果 eruka serverC 是跨机房部署的,那么正常情况下 ConsumerA 也是不会主动跨机房访问 eruka serverC 的。

04 服务提供者的健康检查模式

客户端心跳

  • 客户端每隔一定时间主动发送“心跳”的方式来向服务端表明自己的服务状态正常,心跳可以是 TCP 的形式,也可以是 HTTP 的形式。
  • 也可以通过维持客户端和服务端的一个 socket 长连接自己实现一个客户端心跳的方式。

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

服务端主动探测

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

服务端主动调用服务进行健康检查是一个较为准确的方式,返回结果成功表明服务状态确实正常。但是服务端主动探测也存在问题。服务注册中心主动调用 RPC 服务的某个接口无法做到通用性;在很多场景下服务注册中心到服务发布者的网络是不通的,服务端无法主动发起健康检查,那么往往需要在宿主机器上部署一个 agent 来代替服务端的接口探测,比如 Consul 的健康检查机制就是这么实现的。

聊一聊微服务架构中的服务发现系统

05 消费端的订阅机制

  • Push 推送:Push 的经典实现有两种,基于 socket 长连接的推送,典型的实现如 zookeeper;另一种为 HTTP 连接所使用的 Long Polling,这两种形式都保证了消息变更能够第一时间送达。但是基于 socket 长连接的推送和基于 HTTP 协议的 Long Polling 都会存在 notify 消息丢失的问题和代码实现复杂度过高的问题。
  • 定时轮询:比如 eruka,客户端每隔一段时间(默认 30 秒)会去服务端拉取注册表信息,保证注册表是最新的,这样的基于 http 短链接的订阅模式实现起来是最简单、最通用的。但也很容易导致一个问题,就是服务节点信息会有 30s 的延迟,在这 30s 内有可能会有请求打到已下线的节点上去。
  • 推拉结合的方式:比如 Consul,客户端和 consul server 之间会建立起一个最长 30s 的 http 长链接,如果期间有任何变更,则会立即推送,如果没有变更等到 30s 过后,客户端又会立即建立起新的连接,继续开始新的一轮订阅。这种模式的既吸收了 http 短链接方便通用的好处,又享受到消息即时推送的优势。

06 服务的上线与下线

优雅上线

  1. 服务提供一个通用的 Health check 接口(比如 spring boot actuator 模块自带 /actuator/health 接口,grpc 也提供了 health checking 的标准模型),服务发现的 sdk 通过检查该接口来确定服务是否准备好接流, 只有准备好节流才可将该节点注册上去。
  2. SDK 也可以提供一个回调接口,服务一切都准备就绪后再调用这个接口通知 sdk 去注册。

优雅下线

服务发现 SDK 接收到系统发出的 SigTerm 或者 SigInt 信号后,需要先主动反注册本身的实例,此时如果服务框架提供了 graceful shutdown 能力,就可以直接调用该方法,此时会阻塞住直到当前的所有 inflight 请求都处理完成或者超时才真正退出(不通)(grpc server 提供了直接 graceful shutdown 方法,spring web 应用则可以通过 java 提供的 ThreadPoolExecutor.awitTermination 来实现此能力)。如果没有 graceful shutdown 的能力,则需要主动 sleep 一定时间以确保所有 http、rpc 请求都处理完成后再退出。

07 服务发现的容灾与高可用

服务端

  • 服务节点信息原本是分布式存储的,少数节点挂了,不会影响整体可用性。
  • 当大多数节点挂了的时候,如果是强一致的系统此时会进入只读不可写的模式(比如 Zookeeper 和开启了 stale read 的 consul。如果是最终一致的系统,此时客户端 sdk 会自动重试并切换到正常节点上去,读和写都不受影响。(缺少后括号,但不知道在哪加)。
  • 当服务端所有的节点都挂了时候,此时需要服务端能够持久化存储之前注册的 Provider 节点信息,并在重启之后进入保护模式一段时间,在此期间先不剔除不健康的 Provider 节点(因为宕机过程中心跳没办法成功上报),否则可能会导致在一个 ttl 内大量 Provider 节点失效。
  • 网络闪断保护,监测到大面积出现服务提供者节点心跳没有上报,则自动进入保护模式,该模式下不会剔除因为心跳上报失败的服务提供者节点

客户端

  • 客户端 SDK 需要有不可用节点剔除能力,当服务端某个节点不可用的时候,能够立即切换到下一个节点尝试 (切换的时候随机 sleep 0-3s 防止重试风暴打垮某个节点)。这里要注意客户端 SDK 每次请求的超时时间是否设置正确,我们发现部分服务发现官方 SDK 的默认超时时间过长,比如 java 的 consul sdk 中默认超时是 10 分钟,在生产实践中如果发生了网络闪断导致 response 包回不来就会导致 sdk 的心跳请求一致阻塞住,没办法进行下次的心跳上报,从而导致节点从注册中心中异常下线。
  • 当所有的服务端节点都不可用的时候,SDK 能够使用内存中的缓存继续提供服务
  • 如果客户端重启了,内存中的数据不存在了,则走本地配置降级。

服务注册的 Metadata

服务注册的时候除了携带 serviceName、ip、port 这些信息就足够了呢?在一个大型为微服务系统中,服务支持的协议、服务的标签(比如 Abtest、蓝绿发布的时候需要筛选这些 tag 作为服务路由信息)、服务的健康状态、服务的调度权重等信息可能都需要传递给消费者感知到。不过在生产实践中,一般不推荐将过多的信息放入注册中心,以免导致性能下降,比如 swagger 生成的 api 信息最好单独存储。

08 总结

以上一些浅见便是我们团队在腾讯云微服务框架 TSF 中的服务发现系统开发和维护时所踩过的坑以及留下的经验和总结,如果大家不想再淌这些坑,可以直接使用腾讯云微服务框架 TSF ,其中提供了服务发现等微服务治理功能。

本文转载自公众号腾讯云中间件(ID:gh_6ea1bc2dd5fd)。

原文链接

https://mp.weixin.qq.com/s/IhsLvbhr8-jwg4nW-P7CRQ

阅读数:1 发布于:2020 年 5 月 30 日 10:06

评论

发布
暂无评论