

微服务注册中心注册表与hashcode实现golang版
source link: https://www.tuicool.com/articles/ZNvIJz6
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

背景
基于负载均衡的服务调用
基于负载均衡的服务相互调用指的是通过基于Lvs、Haproxy、Nginx等负载均衡软件来构建一个负载均衡服务,所有的服务调用都通过负载均衡器
从负载均衡的这种模式下其实有两个主要的问题:
一是中心化,整个系统都基于负载均衡器,负载均衡就相当于整个业务的中心,虽然我们可以通过一些高可用手段来保证,但其实内部流量通常是巨大的,很容易出现性能瓶颈
二是增加了一次TCP交互
当然也有很多好处,比如可以做一些负载均衡、长链接维护、分布式跟踪等,这不是本文重点
基于注册中心的服务调用
所有的服务都启动后都通过注册中心来注册自己,同时把注册中心里面的服务信息拉回本地,后续调用,就直接检查本地的服务和节点信息来进行服务节点的调用
注册中心中的注册表
每个服务节点都会来注册中心进行服务注册,那数据如何在服务端进行保存呢,其实就是注册表,其实等同于windows 里面的注册表,每个服务都来注册,把自己的信息上报上来,然后注册中心吧注册表,返回给client端,那服务之间就知道要调用服务的节点啦
注册中心事件队列
微服务注册注册中心通常会大量的服务注册, 那不能每次客户端来请求的时候,服务端都返回全量的数据,在数据传输的设计中,通常会有一种增量同步,其实在注册中心中也类似
注册中心通过将最近的服务变更事件保存在一个 事件队列
中,后续每次客户端拉取只返回增量数据,这样服务端的忘了压力就会小很多
注册中心hashcode
增量数据有一个问题就是,如果客户端错过啦某些事件,比如事件队列满了,则客户端与注册中心的注册表就会不一致, 所以eureka里面引入了一个hashcode的概念,通过比对hashcode是否相同, 如果不同则客户端需要重新全量拉取
代码实现
系统架构
系统整体上分为两个端:客户端(Client)和注册中心(Server)
Server: 提供服务注册和获取注册表的接口, 同时本地把保存服务和节点的对应信息,变更事件写入eventQueue
Client: 调用server接口进行服务注册, 同时调用注册表拉取接口进行注册表拉取,保存懂啊LocalRegistry
应用与节点信息
Server端的服务注册表里面的服务和节点的信息,我通过Application和lease来维护
Application: 代表一个应用,里面会包含服务对应的节点信息
Lease: 维护一个节点的信息,比如心跳信息
服务端注册表
注册表结构体
服务端注册表结构体Registry主要包含三部分信息: lock(读写锁)、apps(应用对应信息)、eventQueue(事件队列)
Lock: 注册中心是典型的读多写少的应用,server端注册表可能同时提供给N个服务进行读取,所以这里采用读写锁
apps: 保存应用对应的信息, 其实后面写完发现,没必要使用,只使用基础的map就可以搞定
eventQueue: 每次注册表变更都写入事件到里面
// Registry 注册表 type Registry struct { lock sync.RWMutex apps sync.Map duration time.Duration eventQueue *EventQueue }
注册表服务注册
注册流程主要分为下面几部分:
- 从注册表获取对应的应用Application
- 调用Application的add接口添加节点
- 为节点创建一个Lease
- 保存节点信息到Application.Node里
- 将事件写入到eventQueue
// Registr 注册服务 func (r *Registry) Registr(name, node string) bool { r.lock.Lock() defer r.lock.Unlock() app := r.getApp(name) if app == nil { app = NewApplication(name) r.apps.Store(name, app) } if lease, ok := app.add(node, r.duration); ok { r.eventQueue.Push(&Event{lease: lease, action: ADD}) return true } return false }
注册表拉取
全量拉取通过all接口拉取全量的返回的是服务对应的节点切片
增量拉取通过details接口返回增量的变更事件和服务端注册表的hashcode
// all 全量拉取 func (r *Registry) all() map[string][]string { r.lock.RLock() defer r.lock.RUnlock() apps := make(map[string][]string) r.apps.Range(func(k, v interface{}) bool { name, app := k.(string), v.(*Application) nodes := []string{} for key := range app.Node { nodes = append(nodes, key) } apps[name] = nodes return true }) return apps } // details 增量拉取 func (r *Registry) details() []*Event { r.lock.RLock() defer r.lock.RUnlock() events := []*Event{} for { event := r.eventQueue.Pop() if event == nil { break } events = append(events, event) } return events }
hashcode
hashcode是一个一致性的保证,eureka里面主要是通过拼接所有的服务名称和节点的个数来生成的一个字符串,这里我们也采用这种方式,
func (r *Registry) hashCode() string { r.lock.RLock() defer r.lock.RUnlock() hashCodes := []string{} r.apps.Range(func(_, value interface{}) bool { app := value.(*Application) hashCodes = append(hashCodes, app.HashCode()) return true }) sort.Sort(sort.StringSlice(hashCodes)) return strings.Join(hashCodes, "|") }
客户端注册表
数据结构
客户端本地注册表其实就比较简单了,只需要存储服务和节点的对应信息即可
// LocalRegistry 本地注册表 type LocalRegistry struct { lock sync.RWMutex apps map[string][]string }
客户端逻辑架构
- 启动流程: 启动时客户端首先调用注册接口进行自我注册,然后调用poll拉取全量注册表
func (c *Client) start() { c.wg.Add(1) c.registr() c.poll() go c.loop() }
- 主循环
func (c *Client) loop() { timer := time.NewTimer(time.Second) for { // 从服务的拉取增量事件,details内部会直接应用,然后返回服务端返回的注册表的hashcode respHashCode := c.details() localHashCode := c.registry.hashCode() // 如果发现本地和服务的的注册表的hashcode不同,则全量拉取 if respHashCode != localHashCode { fmt.Printf("client app %s node %s poll hashcode: %s\n", c.App, c.Name, respHashCode) c.poll() } select { case <-timer.C: timer.Reset(time.Second) case <-c.done: c.wg.Done() return } } }
验证逻辑
func main() { // 生成服务端 server := NewServer("aliyun", time.Second) // 注册两个test服务的节点 clientOne := NewClient("test", "1.1.1.1:9090", server) clientOne.start() clientTwo := NewClient("test", "1.1.1.2:9090", server) clientTwo.start() // 注册两个hello服务的节点 clientThree := NewClient("hello", "1.1.1.3:9090", server) clientThree.start() clientFour := NewClient("hello", "1.1.1.4:9090", server) clientFour.start() time.Sleep(time.Second * 3) // 验证每个服务节点的注册表的hashcode是否一致 println(clientOne.details()) println(clientTwo.details()) println(clientThree.details()) println(clientFour.details()) println(clientTwo.details() == clientOne.details()) println(clientThree.details() == clientFour.details()) println(clientOne.details() == clientFour.details()) clientOne.stop() clientTwo.stop() clientThree.stop() clientFour.stop() }
通过结果我们可以看出,节点增量拉取了注册表,同时如果发现与本地的hashcode不同就进行全量拉取,并最终达成一致
lr event add 1.1.1.3:9090 hello lr event add 1.1.1.4:9090 hello lr event add client app hello node 1.1.1.4:9090 poll hashcode: hello_2|test_2 1.1.1.1:9090 test lr event add 1.1.1.2:9090 test client app test node 1.1.1.1:9090 poll hashcode: hello_2|test_2 client app test node 1.1.1.2:9090 poll hashcode: hello_2|test_2 client app hello node 1.1.1.3:9090 poll hashcode: hello_2|test_2 hello_2|test_2 hello_2|test_2 hello_2|test_2 hello_2|test_2 true true true
总结
微服务注册中心注册表的这种实现机制,到这基本上就明白了,注册中心 通过增量、全量、hashcode三种机制来保证客户端与注册中心的注册表的同步
其实一个工业级的注册中心还是很麻烦的,比如注册表中那个事件队列,我现在的实现只有一个节点能获取增量,其他的都会通过hashcode来触发全量拉取,后续文章里面会相信介绍下,这块缓存和定时器来实现增量数据的打包
其实在go里面大家注册中心都是基于etcd、consul直接watch去做的,基本上可以完成eureka服务的8/9十的功能,但是当需要与公司现有的java做集成,可能就需要eureaka这种注册中心了
未完待续
关注公共号: 布衣码农
更多精彩内容可以查看 www.sreguide.com
Recommend
-
75
这篇文章还是基于SpringCloud开源框架体系来谈下对Eureka服务注册中心和Zuul服务网关在使用上的一些理解和说明。在使用微服务架构进行开发的时候,最基本的就是SpringBoot,但是对于一个大的项目会根据实际的业务和需求场景,使用到类似服...
-
29
戳蓝字“架构之美”关注我们哦! ...
-
35
前言 在微服务体系中,服务注册中心是最基础的组件,它的稳定性会直接影响整个服务体系的稳定性。本文主要介绍了爱奇艺微服务平台基于Consul的服务注册中心建设方式,与内部容器平台、API网关的集成情况,...
-
22
【编者的话】服务注册中心本质上是为了解耦服务提供者和服务消费者。对于任何一个微服务,原则上都应存在或者支持多个提供者,这是由微服务的分布式属性决定的。更进一步,为了支持弹性扩缩容特性,一个微服务的提供者的数量和分布往往是...
-
8
高可用的服务注册中心:Eureka 集群搭建 , 并使用Spring Security 为 Eureka 增加 Basic 安全认证机制上一篇 Spring Cloud 项目的搭建文章 : 链接再说下我的版本号 , 用...
-
5
PrivescCheck是是著名PowerUp的一种更新和扩展版本,可以针对Windows系统的提权枚举脚本,该脚本能够枚举出目标Windows系统中常见的Windows错误安全配置。如果你曾经在Windows 7或Windows Server 2008 R2上运行过此脚本,则可能会注意到重...
-
6
上一次我们介绍了 Ocelot 网关的基本用法。这次我们开始介绍服务注册发现组件 Consul 的简单使用方法。 服务注册发现 首先先让我们回顾下服务注册发现的...
-
9
0、前言 最近在进行重构一个新项目,为了后续更好的落地,适应于日新月异的技术更新,进行了各方的技术选型及技术预研,最终选型基于微服务架构体系进行开发重构。项目构建前最重要的一步就是要想清楚,整体的部署架构、高可用性(HA)等等,...
-
5
分区和复制//backupnew MapConfig().setBackupCount(0).setAsyncBackupCount(1).setReadBackupData(true)默认情况是同步复制一份,以...
-
8
当我们编写了自己的C#程序,有程序自定义的文件类型时,通常希望它满足以下需求: 双击自定义文件打开自定义程序 自定义文件有着自己的图标 此时,在网上检索可以发现,大多数回答是使用Microsoft.Win32下的...
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK