标题本来想叫《如何设计一个注册中心》的,但是网上已经有很多类似标题的文章了。所以我打算另辟蹊径,换个角度,怎么组装一个注册中心。组装意味着不需要从头造轮子,这更符合很多企业对自研基础件的态度。知道如何组装注册表有什么用?首先,可以对注册中心有更深入的了解。从我个人的体验来看,对注册中心的第一印象是Dubbo的Zookeeper(以下简称zk),然后逐渐深入,学会了如何查看dubbo在zk上注册的数据,可以排查一些问题。了解了Nacos后,才知道注册中心原来可以这么简单。后来一直从事服务发现相关的工作,对一些细节有了新的认识。其次,可以学习技术选择的方法。注册中心的各个模块在不同的需求下会有不同的选择。最后的选择还是要看对需求的把握和技术眼光,但这两个都是内功。如果你暂时不能练习,学习一种选择方法还是可以的。本文打算从需求分析入手,一步步拆解各个模块,本着非必要不加实体的原则组装整个注册中心,但不会是玩具,对齐到生产可用性。当然,在实际项目中,不建议重新发明轮子,尝试使用现成的方案,所以本文仅供学习参考。需求分析本文对注册中心的需求很简单,就三点:注册、发现、高可用。服务的注册和发现是注册中心的基本功能,高可用是生产环境的基本要求。如果不需要高可用,本文需要说明的内容非常少。上图中的高可用标记只是一个提示。有多种方式可用。至于其他花里胡哨的东西,我们暂时就不一一列举了。这里我们介绍三个角色,这三个角色将作为下面的基础:Provider:服务的提供者(被调用者)Consumer(消费者):服务的消费者(调用者)Registry:本文主角,服务提供者列表,消费者关系等数据存储接口定义注册中心与客户端(SDK)交互接口有3个:注册(register),将服务提供者注册到注册中心,注销(unregister),将已注册的服务删除从注册中心订阅,服务消费者订阅所需的服务。订阅后,如果有变化,提供者会通知相应的消费者。注册和注销可以由服务提供者的进程发起,也可以由其他方发起,比如发布一台机器后,发布系统可以调用注册接口将其注册到注册中心,注销流程类似,但是这种方式很少见,如果只考虑一个注册中心,必须能够独立运行,所以通常由provider进程负责注册和注销。有了这三个接口,我们怎么定义接口呢?注册服务需要注册哪些字段?订阅需要传递哪些字段?以什么序列化方式?使用什么协议进行传输?这些问题接踵而至。我认为我们不应该急于做出选择。首先,我们应该检查该领域是否有相关标准。如果有,可以参考,也可以直接按照标准执行。如果没有,我们将分析每个选择点。服务发现确实有一套标准,但并不完整。叫做OpenSergo,其实是一套服务治理的标准,包括服务发现:OpenSergo是一套开放的、通用的、面向分布式服务架构的服务治理标准,覆盖全链路异构生态,基于行业服务治理场景和实践形成共同的标准和规范。OpenSergo最大的特点是用一套统一的配置/DSL/协议定义服务治理规则,面向多语言异构架构,实现全链路生态覆盖。无论微服务的语言是Java、Go、Node.js还是其他语言,无论是标准微服务还是Mesh接入,从网关到微服务,从数据库到缓存,从服务注册发现到配置,开发者都可以使用同一套OpenSergoCRD标准配置对每一层进行统一治理和控制,无需关注框架和语言之间的差异,降低异构和全链路服务治理和控制的复杂性。官网:https://opensergo.io/我们需要的服务注册和发现也有:有,但不完全,因为这个标准还在建设中,写的时候还有服务发现相关的标准这篇文章还没有给出。既然没有标准,可以结合现有的制度和经验来定义。这里我使用json的序列化方式来给。以下是笔者的总结,不能涵盖所有情况。必要时根据业务做一些调整:1.服务注册入口{"application":"provider_test",//应用名称"protocol":"http",//protocol"addr":"127.0.0.1:8080",//提供者的地址"meta":{//携带的元数据,以下三个是例子"cluster":"small","idc":"shanghai","tag":"read"}}2.服务订阅入参{"subscribes":[{"provider":"test_provider1",//订阅的应用名称"protocol":"http",//订阅的协议"meta":{//携带的元数据,下面是示例"cluster":"small","idc":"shanghai","tag":"read"}},{"provider":"test_provider2","protocol":"http","meta":{"cluster":"small","tag":"read"}}]}3.服务发现参数{"version":"23des4f",//version"endpoints":[//example{"application":"provider_test","protocol":"http","addr":"127.0.0.1:8080","meta":{"cluster":"small","idc":"shanghai","tag":"阅读"}},{"application":"provider_test","protocol":"http","addr":"127.0.0.2:8080","meta":{"cluster":"small","idc":"上海","标签":"阅读&quo吨;}}]}更改推送&服务健康检查已经定义,我们如何选择序列化方式?选择序列化方式有两个重要的参考点:语言的适配程度,比如json,几乎可以适配所有的编程语言。除非你非常确定5-10年内不会有多语言需求,否则我还是强烈建议你选择一个跨语言的序列化协议性能,序列化性能包括两层含义,序列化速度(cpu消??耗)和序列化体积,想象一个场景,一个服务被很多应用订阅,如果这个时候服务被释放,就会触发一个非常大的推送事件。此时注册中心的cpu和网络可能已满,导致服务不可用。至于编程语言的选择,我觉得应该更偏向于团队对语言的掌握程度。能hold住才是最重要的。这一点无话可说。一般只能在Java/Go中选择。很少见到用其他语言实现的注册中心。对于注册订阅接口,不管是基于TCP的自定义私有协议,还是HTTP协议,甚至是基于HTTP2的gRPC,我觉得都可以。但是,变更点推送的技术实现方式有很多种:定时查询,定时向注册中心请求订阅服务商列表,长轮询,向注册中心查询订阅服务商列表,如果列表是higher如果第一次没有变化,服务器会hold住请求,等待变化或者超时(时间更长)再返回UDP推送。当服务列表有变化时,会通过UDP通知客户端,但是UDP推送不一定可靠,可能会丢失,乱序,所以需要配合定时轮询(时间较长interval)作为自下而上的TCP长连接推送,客户端与注册中心建立TCP长连接,有变化时推送给客户端。从性能和资源消耗三个方面比较这四种实现方式:实时资源消耗备注定时轮询简单Lowhigh实时性能资源消耗越高,资源消耗越多长轮询中、高、中服务器持有很多请求UDP推送mediumhighlow推送可能会丢失,需要配合定时轮询(间隔时间更长)TCP长连接推送mediumhigh介质服务器需要维护很多长连接看来我们不能决定push该用哪种方式,但是以我自己的经验,应该先排除定时轮询,因为即使是初具规模的公司,定时轮询的消耗也是巨大的,更何况这个消耗还会最终随着服务的实时性和规模而变得更大。无法维护。剩下的三个选项可以选择,我们可以继续结合服务节点的健康检查来综合判断。当服务启动时,它被注册到注册表中。当服务停止时,它会从注册表中删除。通常,删除是通过劫持终止信号来实现的。如果是Java,有封装好的ShutdownHook。当进程被kill时,触发劫持逻辑,将进程从注册表中删除Remove,实现优雅退出。但事情并不总是像预期的那样。如果有人执行kill-9强行杀掉进程,或者机器出现硬件故障,提供者还在注册中心,但是不能再提供服务了。这时候就需要一个健康检查机制来保证当服务宕机时,消费者能够正常感知,从而切断流量,保证在线服务的稳定性。关于健康检查机制,在之前的文章《服务探活的五种方式》中有专门的总结,在此罗列一下,以便做出正确的选择:优点和缺点消费者被动触发不依赖注册中心,需要实现服务调用的逻辑;真实流量检测,可能存在滞后消费者主动检测不依赖注册中心需要在服务调用点实现逻辑提供者向调用上报心跳无侵入消费者服务发现模块实现逻辑,服务端处理心跳,消耗大量资源注册中心的主动检测,对客户端没有要求。资源消耗大,实时性不高。提供者和注册中心之间的会话保持了良好的实时性和低资源消耗。注册中心需要维护一个TCP长连接。我们暂时无法控制调用动作,因此前两项依赖它。消费者的解决方案被排除在外。如果provider的heartbeatreport很小,那还好,规模的增加会让人不知所措。这在Nacos中体现的淋漓尽致。Nacos1.x版本使用提供者的心跳报告来保持服务健康。由于每次上报健康状态(上次健康检查的时间)都需要写入数据,资源消耗非常大。所以在Nacos2.0版本之后,已经改为长连接会话来保持健康状态。因此,我个人更喜欢后两种健康检查方案:注册中心主动检测和提供者与注册中心之间的会话维护。结合上面的变更推送,我们发现,如果实现长连接,会有很多好处。在许多情况下,服务既是消费者又是提供者。这时候一个TCP长连接就可以解决推送和健康检查,甚至注册和注销接口我们也可以复用这个连接,可以说是一箭三雕。长连接技术选择长连接技术选择在电子书《Nacos架构与原理》中有详细介绍。我觉得这部分可以称为技术选型的典范。让我们参考一下。这一段很多内容都参考了《Nacos架构与原理》,如果有雷同,那就真的雷同了。首先是长连接的核心需求:图片来自低成本快感:客户端需要在服务器不可用时尽快切换到新的服务节点,以减少不可用的时间。客户端正常重启:客户端主动关闭连接,服务端实时感知服务器正常重启:服务端主动关闭连接,客户端实时感知防抖:网络暂时不可用,client需要能够接受短时间的网络抖动,需要一定的重试机制来防止集群抖动。自动切换服务器,但防止请求风暴断网:在断网的情况下,以合理的频率重试,断网结束后快速重连恢复。低成本多语言实现:在客户端层面尽可能支持多语言,降低多语言实现成本开源社区:文档、开源社区活动、用户数量等,是否有足够支持未来据此,我们的可选轮子有:gRPCRsocketNettyMina客户端感知断线基于stream流错误完成事件可以实现支持支持支持服务端感知断线支持支持支持心跳保活应用层定制,ping-pongmessagecustomizationkeepaliveframeTCP+customizedcustomizationkeepalivefilter多语言支持强generalonlyJavaonlyJava我比较喜欢gRPC,gRPC的社区活跃度比Rsocket强。数据存储registry数据存储方案大致分为两类:使用第三方组件,如Mysql、Redis等,优点是有现成的水平扩展方案,稳定性强;缺点是结构变复杂,使用注册中心本身来存储数据,优点是不需要引入额外的组件;缺点是需要解决稳定性问题。关于第一种方案,我们不需要多说。方案二最关键的是解决注册中心节点间数据的同步问题,因为在注册中心本身的节点上是存储数据的。如果是单机,机器出现故障或者挂掉,有数据丢失的风险,所以一定要有副本。数据不能丢失,这点必须保证,否则稳定性无从谈起。如何保证数据不丢失?客户端向注册中心发起注册请求后,收到正常响应,即数据存储完毕。除非所有注册中心节点都出现故障,否则数据必须存在。如下图所示,例如provider向节点注册数据后,正常响应,但是数据同步是异步的。如果nodeA在同步完成前挂掉,注册数据就会丢失。因此,我们必须尽量避免这种情况。共识算法(如raft)解决了这个问题。共识算法可以保证大部分节点正常,可以对外提供一致的数据服务,但是是以牺牲性能和可用性为代价的。不能对外提供服务。有第二好的算法吗?确实,像Nacos和Eureka提供的AP模型,他们的核心点是客户端可以恢复数据,即注册中心追求最终一致性。如果某些数据丢失,服务提供商可以重新注册数据。例如,我们在提供者和注册中心之间设计一个长期连接。provider注册服务后,连接的节点挂掉后才能同步数据到其他节点。这时,提供者的连接也会断开。当连接重新建立时,服务商可以在注册中心重新注册并恢复数据。注册中心到底选择AP还是CP模式,业界一直存在争论,但基本的共识是AP好于CP,因为数据不一致总比不可用好,对吧?你不同意吗?高可用其实高可用的设计分散在各个细节中,比如上面提到的数据存储,它的基本要求就是高可用。此外,我们的设计也必须为失败而设计。假设我们所有的服务器都会挂掉,那如何才能让服务之间的调用不受影响呢?通常注册中心不会侵入服务调用,而是在内存(或磁盘)中缓存一个服务列表。应用程序启动将受到影响。总结一下这篇文章的内容有点多,用一张图来总结一下:组装一套网上可用的最小注册中心,从需求分析开始,每一步都有很多选择,本文通过一些核心画了一个大概的蓝图技术选择,剩下的工作就是用代码组装这些。