构建完善微服务——服务自动注册服务发现

 从公众号转载,关注微信公众号掌握更多技术动态

---------------------------------------------------------------

服务在哪

    微服务种类和数量很多,如果这些信息全部通过手工配置的方式写入各个微服务节点,首先配置工作量很大,配置文件可能要配几百上千行,几十个节点加起来后配置项就是几万几十万行了,人工维护这么大数量的配置项是一项灾难;其次是微服务节点经常变化,可能是由于扩容导致节点增加,也可能是故障处理时隔离掉一部分节点,还可能是采用灰度升级,先将一部分节点升级到新版本,然后让新老版本同时运行。不管哪种情况,我们都希望节点的变化能够及时同步到所有其他依赖的微服务。如果采用手工配置,是不可能做到实时更改生效的。因此,需要一套服务发现的系统来支撑微服务的自动注册和发现。

  • 提供了服务地址的存储;

  • 当存储内容发生变化时,可以将变更的内容推送给客户端。

1.注册中心原理

在微服务架构下,主要有三种角色:服务提供者(RPC Server)、服务消费者(RPCClient)和服务注册中心(Registry)

图片

RPC Server 提供服务,在启动时,根据服务发布文件 server.xml 中的配置的信息,向Registry 注册自身服务,并向 Registry 定期发送心跳汇报存活状态。

RPC Client 调用服务,在启动时,根据服务引用文件 client.xml 中配置的信息,向Registry 订阅服务,把 Registry 返回的服务节点列表缓存在本地内存中,并与 RPC Sever 建立连接。当 RPC Server 节点发生变更时,Registry 会同步变更,RPC Client 感知后会刷新本地内存中缓存的服务节点列表。

RPC Client 从本地缓存的服务节点列表中,基于负载均衡算法选择一台 RPC Sever 发起调用。

2.注册中心包括内容

(1)注册中心 API

  • 服务注册接口:服务提供者通过调用服务注册接口来完成服务注册。

  • 服务反注册接口:服务提供者通过调用服务反注册接口来完成服务注销。

  • 心跳汇报接口:服务提供者通过调用心跳汇报接口完成节点存活状态上报。

  • 服务订阅接口:服务消费者通过调用服务订阅接口完成服务订阅,获取可用的服务提供者节点列表。

  • 服务变更查询接口:服务消费者通过调用服务变更查询接口,获取最新的可用服务节点列表。

  • 服务查询接口:查询注册中心当前注册了哪些服务信息。

  • 服务修改接口:修改注册中心中某一服务的信息。

(2)集群部署

注册中心一般都是采用集群部署来保证高可用性,并通过分布式一致性协议来确保集群中不同节点之间的数据保持一致。

以开源注册中心 ZooKeeper 为例,ZooKeeper 集群中包含多个节点,服务提供者和服务消费者可以同任意一个节点通信,因为它们的数据一定是相同的。

  • 每个 Server 在内存中存储了一份数据,Client 的读请求可以请求任意一个 Server。

  • ZooKeeper 启动时,将从实例中选举一个 leader(Paxos 协议)。

  • Leader 负责处理数据更新等操作(ZAB 协议)。

  • 一个更新操作成功,当且仅当大多数 Server 在内存中成功修改 。

图片

(3)目录存储

ZooKeeper 为例注册中心存储服务信息一般采用层次化的目录结构:

  • ZooKeeper 启动时,将从实例中选举一个 leader(Paxos 协议)。

  • Leader 负责处理数据更新等操作(ZAB 协议)。

  • 一个更新操作成功,当且仅当大多数 Server 在内存中成功修改

图片

(4)服务健康状态检测

注册中心除了要支持最基本的服务注册和服务订阅功能以外,还必须具备对服务提供者节点的健康状态检测功能,这样才能保证注册中心里保存的服务节点都是可用的。

通过客户端定时向服务端发送心跳消息(ping 消息),服务器重置下次 SESSION_TIMEOUT 时间。如果超过SESSION_TIMEOUT 后服务端都没有收到客户端的心跳消息,则服务端认为这个Session就已经结束了,注册中心就会认为这个服务节点已经不可用,将会从注册中心中删除其信息。

(5)服务状态变更通知

一旦注册中心探测到有服务提供者节点新加入或者被剔除,就必须立刻通知所有订阅该服务的服务消费者,刷新本地缓存的服务节点信息,确保服务调用不会请求不可用的服务提供者节点。

(6)白名单机制

在实际的微服务测试和部署时,通常包含多套环境,比如生产环境一套、测试环境一套。开发在进行业务自测、测试在进行回归测试时,一般都是用测试环境,部署的 RPC Server 节点注册到测试的注册中心集群。但经常会出现开发或者测试在部署时,错误的把测试环境下的服务节点注册到了线上注册中心集群,这样的话线上流量就会调用到测试环境下的 RPCServer 节点,可能会造成意想不到的后果。

为了防止这种情况发生,注册中心需要提供一个保护机制,你可以把注册中心想象成一个带有门禁的房间,只有拥有门禁卡的 RPC Server 才能进入。在实际应用中,注册中心可以提供一个白名单机制,只有添加到注册中心白名单内的 RPC Server,才能够调用注册心的注册接口,这样的话可以避免测试环境中的节点意外跑到线上环境中去。

3.注册中心设计

(1)存储服务信息方式

  • 服务名

  • 分组,每个分组的目的不同,一般来说有下面几种分组方式。

    • 核心与非核心,从业务的核心程度来分。

    • 机房,从机房的维度来分。

    • 线上环境与测试环境,从业务场景维度来区分。

  • 节点信息:节点地址(IP 和端口号)和节点其他信息(请求失败时重试的次数、请求结果是否压缩等信息)

(2)注册中心工作流程

①注册节点

图片

  • 首先查看要注册的节点是否在白名单内?如果不在就抛出异常,在的话继续下一步。

  • 其次要查看注册的 Cluster(服务的接口名)是否存在?如果不存在就抛出异常,存在的话继续下一步。

  • 然后要检查 Service(服务的分组)是否存在?如果不存在则抛出异常,存在的话继续下一步。

  • 最后将节点信息添加到对应的 Service 和 Cluster 下面的存储中。

②反注册

图片

  • 查看 Service(服务的分组)是否存在,不存在就抛出异常,存在就继续下一步。

  • 查看 Cluster(服务的接口名)是否存在,不存在就抛出异常,存在就继续下一步。

  • 删除存储中 Service 和 Cluster 下对应的节点信息。

  • 更新 Cluster 的 sign 值。

③查询节点信息

图片

  • 首先从 localcache(本机内存)中查找,如果没有就继续下一步。这里为什么服务消费者要把服务信息存在本机内存呢?主要是因为服务节点信息并不总是时刻变化的,并不需要每一次服务调用都要调用注册中心获取最新的节点信息,只需要在本机内存中保留最新的服务提供者的节点列表就可以。

  • 接着从 snapshot(本地快照)中查找,如果没有就继续下一步。这里为什么服务消费者要在本地磁盘存储一份服务提供者的节点信息的快照呢?这是因为服务消费者同注册中心之间的网络不一定总是可靠的,服务消费者重启时,本机内存中还不存在服务提供者的节点信息,如果此时调用注册中心失败,那么服务消费者就拿不到服务节点信息了就没法调用了。本地快照就是为了防止这种情况的发生,即使服务消费者重启后请求注册中心失败,依然可以读取本地快照,获取到服务节点信息。

④订阅服务变更

图片

  • 服务消费者从注册中心获取了服务的信息后,就订阅了服务的变化,会在本地保留Cluster 的 sign 值。

  • 服务消费者每隔一段时间,调用 getSign() 函数,从注册中心获取服务端该 Cluster 的sign 值,并与本地保留的 sign 值做对比,如果不一致,就从服务端拉取新的节点信息,并更新 localcache 和 snapshot。

(3)注册与发现的几个问题

①多注册中心

理论上对于一个服务消费者来说,同一个注册中心交互是最简单的。但是不可避免的是,服务消费者可能订阅了多个服务,多个服务可能是由多个业务部门提供的,而且每个业务部门都有自己的注册中心,提供的服务只在自己的注册中心里有记录。这样的话,就要求服务消费者要具备在启动时,能够从多个注册中心订阅服务的能力。

还有一种情况是,一个服务提供者提供了某个服务,可能作为静态服务对外提供,有可能又作为动态服务对外提供,这两个服务部署在不同的注册中心,所以要求服务提供者在启动的时候,要能够同时向多个注册中心注册服务。也就是说,对于服务消费者来说,要能够同时从多个注册中心订阅服务;对于服务提供者来说,要能够同时向多个注册中心注册服务。

②并行订阅服务

通常一个服务消费者订阅了不止一个服务,在我经历的一个项目中,一个服务消费者订阅了几十个不同的服务,每个服务都有自己的方法列表以及节点列表。服务消费者在服务启动时,会加载订阅的服务配置,调用注册中心的订阅接口,获取每个服务的节点列表并初始化连接。

最开始采用了串行订阅的方式,每订阅一个服务,服务消费者调用一次注册中心的订阅接口,获取这个服务的节点列表并初始化连接,总共需要执行几十次这样的过程。在某些服服务消费者从注册中心获取了服务的信息后,就订阅了服务的变化,会在本地保留Cluster 的 sign 值。服务消费者每隔一段时间,调用 getSign() 函数,从注册中心获取服务端该 Cluster 的sign 值,并与本地保留的 sign 值做对比,如果不一致,就从服务端拉取新的节点信息,并更新 localcache 和 snapshot。务节点的初始化连接过程中,出现连接超时的情况,后续所有的服务节点的初始化连接都需要等待它完成,导致服务消费者启动变慢,最后耗费了将近五分钟时间来完成所有服务节点的初始化连接过程。

后来改成了并行订阅的方式,每订阅一个服务就单独用一个线程来处理,这样的话即使遇到个别服务节点连接超时,其他服务节点的初始化连接也不受影响,最慢也就是这个服务节点的初始化连接耗费的时间,最终所有服务节点的初始化连接耗时控制在了 30 秒以内。

③批量反注册服务

通常一个服务提供者节点提供不止一个服务,所以注册和反注册都需要多次调用注册中心。在与注册中心的多次交互中,可能由于网络抖动、注册中心集群异常等原因,导致个别调用失败。对于注册中心来说,偶发的注册调用失败对服务调用基本没有影响,其结果顶多就是某一个服务少了一个可用的节点。但偶发的反注册调用失败会导致不可用的节点残留在注册中心中,变成“僵尸节点”,但服务消费者端还会把它当成“活节点”,继续发起调用,最终导致调用失败。

以前我们的业务中经常遇到这个问题,需要定时去清理注册中心中的“僵尸节点”。后来我们通过优化反注册逻辑,对于下线机器、节点销毁的场景,通过调用注册中心提供的批量反注册接口,一次调用就可以把该节点上提供的所有服务同时反注册掉,从而避免了“僵尸节点”的出现。

④服务变更信息增量更新

服务消费者端启动时,除了会查询订阅服务的可用节点列表做初始化连接,还会订阅服务的变更,每隔一段时间从注册中心获取最新的服务节点信息标记 sign,并与本地保存的 sign值作比对,如果不一样,就会调用注册中心获取最新的服务节点信息。

一般情况下,按照这个过程是没问题的,但是在网络频繁抖动时,服务提供者上报给注册中心的心跳可能会一会儿失败一会儿成功,这时候注册中心就会频繁更新服务的可用节点信息,导致服务消费者频繁从注册中心拉取最新的服务可用节点信息,严重时可能产生网络风暴,导致注册中心带宽被打满。

为了减少服务消费者从注册中心中拉取的服务可用节点信息的数据量,这个时候可以通过增量更新的方式,注册中心只返回变化的那部分节点信息,尤其在只有少数节点信息变更时,此举可以大大减少服务消费者从注册中心拉取的数据量,从而最大程度避免产生网络风暴。

(4)单机如何实现管理百万主机的心跳服务?

①如何设计更快的宕机判断算法?

通过心跳包找到宕机的主机需要一套算法,比如用 for 循环做一次遍历,找到停止上报心跳的主机就可以实现,当管理的对象数量级很大时,算法复杂度会严重影响程序性能,遍历算法此时并不可取。我们先分析下这个算法的时间复杂度。如果用红黑树(这里用红黑树是因为它既支持遍历,也可以实现对数时间复杂度的查询操作)存放主机及其最近一次上报时间,那么,新主机上报心跳被发现的流程,时间复杂度仅为 O(logN),这是查询红黑树的成本。寻找宕机服务的流程,需要对红黑树做全量遍历,用当前时间去比较每个主机的上次心跳时间,时间复杂度就是 O(N)!

如果业务对时间灵敏度要求很高,就意味着需要频繁地执行 O(N) 级的遍历,当 N 也就是主机数量很大时,耗时就很可观了。而且寻找宕机服务和接收心跳包是两个流程,如果它们都在单线程中执行,那么寻找宕机服务的那段时间就不能接收心跳包,会导致丢包!如果使用多线程并发执行,因为两个流程都需要操作红黑树,所以要使用到互斥锁,而当这两个流程争抢锁的频率很高时,性能也会急剧下降。

其实这个算法的根本问题在于,判断宕机的流程做了大量的重复工作。比如,主机每隔 1 秒上报一次心跳,而考虑到网络可能丢包,故 5 秒内失去心跳就认为宕机,这种情况下,如果主机 A 在第 10 秒时失去心跳,那么第 11、12、13、14 这 4 秒对主机 A 的遍历,都是多余的,只有第 15 秒对主机 A 的遍历才有意义。于是,每次遍历平均浪费了 4/5 的计算量。如何设计快速的宕机判断算法呢?其实,这是一个从一堆主机中寻找宕机服务的信息题。根据香农的理论,引入更多的信息,才能减少不确定性降低信息熵,从而减少计算量。就像心跳包间是有时间顺序的,上面的宕机判断算法显然忽略了接收到它们的顺序。比如主机 A 的上次心跳包距现在 4 秒了,而主机 B 距现在只有 1 秒,显然不应同等对待。于是,我们引入存放心跳包的先入先出队列,这就保存了心跳包的时序关系。新的心跳包进入队列尾部,而老的心跳包则从队列首部退出,这样,寻找宕机服务时,只要看队列首部最老的心跳包,距现在是否超过 5 秒,如果超过 5 秒就认定宕机,同时把它取出队列,否则证明队列中不存在宕机服务,维持队列不变。

当然,这里并没有解决如何发现新主机的问题。我们还需要一个能够执行高效查询的容器,存放所有主机及其状态。红黑树虽然不慢,但我们不再需要遍历容器,所以可以选择更快的、查询时间复杂度为 O(1) 的哈希表存放主机信息

图片

当然,队列中的心跳包并不是只能从队首删除,否则判断宕机流程的时间复杂度仍然是 O(N)。实际上,每当收到心跳包时,如果对应主机的上一个心跳包还在队列中,那么可以直接把它从队列中删除。显然,计算在线主机何时宕机,只需要最新的心跳包,老的心跳包没有必要存在。因此,这个队列为每个主机仅保留最新的那个心跳包。如下图所示:

图片

图片

这样,判断宕机的速度会非常快,它的计算量等于实际发生宕机的主机数量。同时,接收心跳包并发现新主机的流程,因为只需要做一次哈希表查询,时间复杂度也只有 O(1)。

图片

这样,新算法通过以空间换时间的思想,虽然使用了更加占用空间的哈希表,并新增了有序队列容器,但将宕机和新主机发现这两个流程都优化到了常量级的时间复杂度。尤其是宕机流程的计算量非常小,它仅与实际宕机服务的数量有关,这就允许我们将宕机判断流程插入到心跳包的处理流程中,以微观上的分时任务实现宏观上的并发,同时也避免了对哈希表的加锁。

②如何设计高并发架构?

有了核心算法,还需要充分利用服务器资源的架构,才能实现高并发。一颗 1GHZ 主频的 CPU,意味着一秒钟只有 10 亿个时钟周期可以工作,如果心跳服务每秒接收到 100 万心跳包,就要求它必须在 1000 个时钟周期内处理完一个心跳包。这无法做到,因为每一个汇编指令的执行需要多个时钟周期(参见CPI),一条高级语言的语句又由多条汇编指令构成,而中间件提供的反序列化等函数又需要很多条语句才能完成。另外,内核从网卡上读取报文,执行协议分析需要的时钟周期也要算到这 1000 个时钟周期里。因此,选择只用一颗 CPU 为核心的单线程开发模式,一定会出现计算力不足,不能及时接收报文从而使得缓冲区溢出的问题,最终导致大量丢包。所以,我们必须选择多线程或者多进程开发模式。多进程之间干扰更小,但内存不是共享的,数据同步较为困难,因此案例中我们还是选择多线程开发模式。使用多线程后我们需要解决 3 个问题。

  • 第一是负载均衡,我们应当把心跳包尽量均匀分配到不同的工作线程上处理。比如,接收网络报文的线程基于主机名或者 IP 地址,用哈希算法将心跳包分发给工作线程处理,这样每个工作线程只处理特定主机的心跳,相互间不会互相干扰,从而可以无锁编程。

  • 第二是多线程同步。分发线程与工作线程间可以采用生产者 - 消费者模型传递心跳包,然而多线程间传递数据要加锁,为了减少争抢锁对系统资源的消耗,需要做到以下两点:

    • 由于工作线程多过分发线程(接收心跳包消耗的资源更少),所以每个工作线程都配发独立的缓冲队列及操作队列的互斥锁;

    • 为避免线程执行主动切换,必须使用自旋锁,关于锁的选择你可以看[第 6 讲]。如下图所示:

图片

  • 第三要解决 CPU 亲和性问题。从[第 1 讲] 我们可以看到,CPU 缓存对计算速度的影响很大,如果线程频繁地切换 CPU 会导致缓存命中率下降,降低性能,此时将线程绑定到特定的 CPU 就是一个解决方案

③如何选择心跳包网络协议?

最后我们再来看看心跳包的协议该选择 TCP 还是 UDP 实现。网络报文的长度是受限的,MTU(Maximum Transmission Unit)定义了最大值。比如以太网中 MTU 是 1500 字节,如果 TCP 或者 UDP 试图传送大于 1500 字节的报文,IP 协议就会把报文拆分后再发到网络中,并在接收方组装回原来的报文。然而,IP 协议并不擅长做这件事,拆包组包的效率很低,因此 TCP 协议宁愿自己拆包(详见[第 11 讲])。所以,如果心跳包长度小于 MTU,那么 UDP 协议是最佳选择。如果心跳包长度大于 MTU,那么最好选择 TCP 协议,面对复杂的 TCP 协议,还需要解决以下问题。

首先,一台服务器到底能同时建立多少 TCP 连接?要回答这个问题,得先从 TCP 四元组谈起,它唯一确定一个 TCP 连接。TCP 四元组分别是 < 源 IP、目的 IP、源端口、目的端口 >,其中前两者在 IP 头部中,后两者在 TCP 头部中。由于 IPv4 地址为 4 个字节、端口为 2 个字节,所以当服务器 IP 地址和监听端口固定时,并发连接数的上限则是 2(32+16)。当然,这么高的并发连接需要很多条件,其中之一就是增加单个进程允许打开的最大句柄数(包括操作系统允许的最大句柄数 /proc/sys/fs/file-nr),因为 Linux 下每个连接都要用掉一个文件句柄。当然,作为客户端的主机如果想用足 216  端口,还得修改 ip_local_port_range 配置,扩大客户端的端口范围:

net.ipv4.ip_local_port_range = 32768    60999

其次,基于 TCP 协议实现百万级别的高并发,必须使用基于事件驱动的全异步开发模式。而且,TCP 协议的默认配置并没有考虑高并发场景,所以我们还得在以下 4 个方面优化 TCP 协议:

  • 三次握手建立连接的过程需要优化

  • 四次挥手关闭连接的过程也需要优化

  • 依据网络带宽时延积重新设置 TCP 缓冲区

  • 优化拥塞控制算法

最后还有一个问题需要我们考虑。网络中断时并没有任何信息通知服务器,此时该如何发现并清理服务器上的这些僵死连接呢?KeepAlive 机制允许服务器定时向客户端探测连接是否存活。其中,每隔 tcp_keepalive_time 秒执行一次探测。

net.ipv4.tcp_keepalive_time = 7200

每次探测的最大等待时间是 tcp_keepalive_intvl 秒。

net.ipv4.tcp_keepalive_intvl = 75

超时后,内核最多尝试 tcp_keepalive_probes 次,仍然没有反应就会及时关闭连接。

net.ipv4.tcp_keepalive_probes = 9

当然,如果在应用层通过心跳能及时清理僵死 TCP 连接,效果会更好。从上述优化方案可见,TCP 协议的高并发优化方案还是比较复杂的,这也是享受 TCP 优势时我们必须要付出的代价。

4.开源注册中心选型

(1)服务发现两种实现方式

方式一:自理式

图片

自理式结构就是指每个微服务自己完成服务发现。例如,图中 SERVICE INSTANCE A 访问 SERVICE REGISTRY 获取服务注册信息,然后直接访问 SERVICE INSTANCE B。

自理式服务发现实现比较简单,因为这部分的功能一般通过统一的程序库或者程序包提供给各个微服务调用,而不会每个微服务都自己来重复实现一遍;并且由于每个微服务都承担了服务发现的功能,访问压力分散到了各个微服务节点,性能和可用性上不存在明显的压力和风险。

方式二:代理式

图片

代理式结构就是指微服务之间有一个负载均衡系统(图中的 LOAD BALANCER 节点),由负载均衡系统来完成微服务之间的服务发现。

代理式的方式看起来更加清晰,微服务本身的实现也简单了很多,但实际上这个方案风险较大。第一个风险是可用性风险,一旦 LOAD BALANCER 系统故障,就会影响所有微服务之间的调用;第二个风险是性能风险,所有的微服务之间的调用流量都要经过 LOAD BALANCER 系统,性能压力会随着微服务数量和流量增加而不断增加,最后成为性能瓶颈。因此 LOAD BALANCER 系统需要设计成集群的模式,但 LOAD BALANCER 集群的实现本身又增加了复杂性。

不管是自理式还是代理式,服务发现的核心功能就是服务注册表,注册表记录了所有的服务节点的配置和状态,每个微服务启动后都需要将自己的信息注册到服务注册表,然后由微服务或者 LOAD BALANCER 系统到服务注册表查询可用服务。

(2)服务注册实践

对于注册中心来说,最主要的功能是服务的注册和发现,在网络出现问题的时候,可用性的需求要远远高于数据一致性。即使因为数据不一致,注册中心内引入了不可用的服务节点,也可以通过其他措施来避免,比如客户端的快速失败机制等,只要实现最终一致性,对于注册中心来说就足够了。因此,选择 AP 型注册中心,一般更加合适。

图片

实践一:Eureka

AP型注册中心,牺牲一致型来保证可用性,集群状态下不选举leader,每个Eureka服务器单独保存服务注册地址,因此可能出现数据信息不一致的情况。但是当网络出现问题的时候,每台服务器都可以完成独立的服务

图片

  • Eureka Server:注册中心的服务端,实现了服务信息注册、存储以及查询等功能。

  • 服务端的 Eureka Client:集成在服务端的注册中心 SDK,服务提供者通过调用 SDK,实现服务注册、反注册等功能。

  • 客户端的 Eureka Client:集成在客户端的注册中心 SDK,服务消费者通过调用 SDK,实现服务订阅、服务更新等功能。

在集群环境中如果某台 Eureka Server 宕机,Eureka Client 的请求会自动切换到新的 Eureka Server 节点上,当宕机的服务器重新恢复后,Eureka 会再次将其纳入到服务器集群管理之中。当节点开始接受客户端请求时,所有的操作都会在节点间进行复制操作,将请求复制到该 Eureka Server 当前所知的其它所有节点中。

当一个新的 Eureka Server 节点启动后,会首先尝试从邻近节点获取所有注册列表信息,并完成初始化。Eureka Server 通过 getEurekaServiceUrls() 方法获取所有的节点,并且会通过心跳契约的方式定期更新。默认情况下,如果 Eureka Server 在一定时间内没有接收到某个服务实例的心跳(默认周期为30秒),Eureka Server 将会注销该实例(默认为90秒, eureka.instance.lease-expiration-duration-in-seconds 进行自定义配置)。

当 Eureka Server 节点在短时间内丢失过多的心跳时,那么这个节点就会进入自我保护模式,这个测试环境的时候需要注意一下。Eureka的集群中,只要有一台Eureka还在,就能保证注册服务可用,只不过查到的信息可能不是最新的(不保证强一致性)。除此之外,Eureka还有一种自我保护机制,如果在15分钟内超过**85%**的节点都没有正常的心跳,那么Eureka就认为客户端与注册中心出现了网络故障,此时会出现以下几种情况:

  • Eureka不再从注册表中移除因为长时间没有收到心跳而过期的服务;

  • Eureka仍然能够接受新服务注册和查询请求,但是不会被同步到其它节点上(即保证当前节点依然可用)

  • 当网络稳定时,当前实例新注册的信息会被同步到其它节点中。

实践二:Consul

CA型注册中心,牺牲可用性来保证数据的强一致性,集群内只有一个leader,出现网络问题会导致产生多个leader,以至于出现脑裂的情况,导致注册中心不可用

Consul是在服务外进行完成一系列动作的,也就是说并不需要服务节点去依赖它的SDK,没有侵入性,所以跨语言的解决能力更强一些。它一般是在服务节点外通过一些探针的方法去检查应用是否存活,是否需要注册或注销。

  • Consul:注册中心的服务端,实现服务注册信息的存储,并提供注册和发现服务。

  • Registrator:一个开源的第三方服务管理器项目,它通过监听服务部署的 Docker 实例是否存活,来负责服务提供者的注册和销毁。

  • Consul Template:定时从注册中心服务端获取最新的服务提供者节点列表并刷新 LB 配置(比如 Nginx 的 upstream),这样服务消费者就通过访问 Nginx 就可以获取最新的服务提供者信息。

实践三:Nacos

Nacos是阿里开源的,Nacos 支持基于 DNS 和基于 RPC 的服务发现。在Spring Cloud中使用Nacos,只需要先下载 Nacos 并启动 Nacos server,Nacos只需要简单的配置就可以完成服务的注册发现。Nacos = Spring Cloud注册中心 + Spring Cloud配置中心。

5.如何识别服务节点是否存活

(1)心跳开关保护机制

在网络频繁抖动的情况下,注册中心中可用的节点会不断变化,这时候服务消费者会频繁收到服务提供者节点变更的信息,于是就不断地请求注册中心来拉取最新的可用服务节点信息。当有成百上千个服务消费者,同时请求注册中心获取最新的服务提供者的节点信息时,可能会把注册中心的带宽给占满,尤其是注册中心是百兆网卡的情况下。

所以针对这种情况,需要一种保护机制,即使在网络频繁抖动的时候,服务消费者也不至于同时去请求注册中心获取最新的服务节点信息。

一个可行的解决方案就是给注册中心设置一个开关,当开关打开时,即使网络频繁抖动,注册中心也不会通知所有的服务消费者有服务节点信息变更,比如只给 10% 的服务消费者返回变更,这样的话就能将注册中心的请求量减少到原来的1/10。

当然打开这个开关也是有一定代价的,它会导致服务消费者感知最新的服务节点信息延迟,原先可能在 10s 内就能感知到服务提供者节点信息的变更,现在可能会延迟到几分钟,所以在网络正常的情况下,开关并不适合打开;可以作为一个紧急措施,在网络频繁抖动的时候,才打开这个开关。

(2)服务节点摘除保护机制

服务提供者在进程启动时,会注册服务到注册中心,并每隔一段时间,汇报心跳给注册中心,以标识自己的存活状态。如果隔了一段固定时间后,服务提供者仍然没有汇报心跳给注册中心,注册中心就会认为该节点已经处于“dead”状态,于是从服务的可用节点信息中移除出去。

如果遇到网络问题,大批服务提供者节点汇报给注册中心的心跳信息都可能会传达失败,注册中心就会把它们都从可用节点列表中移除出去,造成剩下的可用节点难以承受所有的调用,引起“雪崩”。但是这种情况下,可能大部分服务提供者节点是可用的,仅仅因为网络原因无法汇报心跳给注册中心就被“无情”的摘除了。

这个时候就需要根据实际业务的情况,设定一个阈值比例,即使遇到刚才说的这种情况,注册中心也不能摘除超过这个阈值比例的节点。

这个阈值比例可以根据实际业务的冗余度来确定,我通常会把这个比例设定在20%,就是说注册中心不能摘除超过 20% 的节点。因为大部分情况下,节点的变化不会这么频繁,只有在网络抖动或者业务明确要下线大批量节点的情况下才有可能发生。而业务明确要下线大批量节点的情况是可以预知的,这种情况下可以关闭阈值保护;而正常情况下,应该打开阈值保护,以防止网络抖动时,大批量可用的服务节点被摘除。

(3)静态注册中心

服务提供者节点就不需要向注册中心汇报心跳信息,注册中心中的服务节点信息也不会动态变化,也可以称之为静态注册中心。

一开始采用了动态注册中心,后来考虑到网络的复杂性,心跳机制不一定是可靠的,而后开始改为采用服务消费者端的保活机制,事实证明这种机制足以应对网络频繁抖动等复杂的场景。

当然静态注册中心中的服务节点信息并不是一直不变,当在业务上线或者运维人工增加或者删除服务节点这种预先感知的情况下,还是有必要去修改注册中心中的服务节点信息。

比如在业务上线过程中,需要把正在部署的服务节点从注册中心中移除,等到服务部署完毕,完全可用的时候,再加入到注册中心。还有就是在业务新增或者下线服务节点的时候,需要调用注册中心提供的接口,添加节点信息或者删除节点。这个时候静态注册中心有点退化到配置中心的意思,只不过这个时候配置中心里存储的不是某一项配置,而是某个服务的可用节点信息。

6.节点信息的保障

当注册中心完全宕机后,微服务框架仍然需要有正常工作的能力。这得益于框架内处理节点状态的一些机制。

(1)本机内存

首先服务消费者会将节点状态保持在本机内存中。一方面由于节点状态不会变更得那么频繁,放在内存中可以减少网络开销。另一方面,当注册中心宕机后,服务消费者仍能从本机内存中找到服务节点列表从而发起调用。

(2)本地快照

注册中心宕机后,服务消费者仍能从本机内存中找到服务节点列表。那么如果服务消费者重启了呢?这时候我们就需要一份本地快照了,即我们保存一份节点状态到本地文件,每次重启之后会恢复到本机内存中。

  • 8
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值