导语:本文围绕服服务调用模式、一致性取舍、服务提供者的健康检查模式等方面,讨论了服务发现的技术选型和设计的各种优缺点,希望能够帮助大家在选择或者使用服务发现系统的时候更加顺畅。
不知道大家刚接触微服务治理的时候是否有这样的疑惑:为什么一定需要一个服务发现系统呢?服务启动的时候直接读取一个本地配置,然后通过远程配置系统,动态推送下来不行吗?实际上,当服务节点规模较小时,该方案也行得通,但如果遇到以下的场景呢?
使用文件配置或DNS等传统方式无法同时满足上述几点要求,因此我们需要重新设计一个能够匹配上微服务架构的服务发现系统。
客户端发现模式
由客户端负责向服务发现系统(可以认为是一个数据库,存储了所有服务提供者的所有节点位置信息)询问某个服务提供者的所有实例的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 也可能会成为性能瓶颈。
基本上服务端发现模式我们平常接触到的机会比较少,但是由于是无任何入侵的,比较适合旧系统上微服务架构的一个过渡方案。
我们先回顾一下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的。
客户端心跳
但是客户端心跳中,长连接的维持和客户端的主动心跳都只是表明链路上的正常,不一定是服务状态正常。
服务端主动探测
服务端主动调用服务进行健康检查是一个较为准确的方式,返回结果成功表明服务状态确实正常。但是服务端主动探测也存在问题。服务注册中心主动调用 RPC 服务的某个接口无法做到通用性;在很多场景下服务注册中心到服务发布者的网络是不通的,服务端无法主动发起健康检查,那么往往需要在宿主机器上部署一个agent来代替服务端的接口探测,比如Consul的健康检查机制就是这么实现的。
优雅上线
优雅下线
服务发现SDK接收到系统发出的SigTerm或者SigInt信号后,需要先主动反注册本身的实例,此时如果服务框架提供了graceful shutdown能力,就可以直接调用该方法,此时会阻塞住直到当前的所有inflight请求都处理完成或者超时才真正退出(不通)(grpc server提供了直接graceful shutdown方法,spring web应用则可以通过java提供的ThreadPoolExecutor.awitTermination来实现此能力)。如果没有graceful shutdown的能力,则需要主动sleep一定时间以确保所有http、rpc请求都处理完成后再退出。
服务端
客户端
服务注册的Metadata
服务注册的时候除了携带serviceName、ip、port这些信息就足够了呢?在一个大型为微服务系统中,服务支持的协议、服务的标签(比如Abtest、蓝绿发布的时候需要筛选这些tag作为服务路由信息)、服务的健康状态、服务的调度权重等信息可能都需要传递给消费者感知到。不过在生产实践中,一般不推荐将过多的信息放入注册中心,以免导致性能下降,比如swagger生成的api信息最好单独存储。
以上一些浅见便是我们团队在腾讯云微服务框架TSF中的服务发现系统开发和维护时所踩过的坑以及留下的经验和总结,如果大家不想再淌这些坑,可以直接使用腾讯云微服务框架TSF,其中提供了服务发现等微服务治理功能。
领取专属 10元无门槛券
私享最新 技术干货