微服务注册中心注册表与hashcode实现golang版

简介: 微服务中通常使用注册表中心进行服务之间的发现,注册中心里面的核心机制就是注册表,本文基于golang实现微服务注册中心的注册表

背景

基于负载均衡的服务调用


基于负载均衡的服务相互调用指的是通过基于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
}

注册表服务注册


注册流程主要分为下面几部分:

  1. 从注册表获取对应的应用Application
  2. 调用Application的add接口添加节点
  3. 为节点创建一个Lease
  4. 保存节点信息到Application.Node里
  5. 将事件写入到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这种注册中心了

未完待续

相关实践学习
每个IT人都想学的“Web应用上云经典架构”实战
本实验从Web应用上云这个最基本的、最普遍的需求出发,帮助IT从业者们通过“阿里云Web应用上云解决方案”,了解一个企业级Web应用上云的常见架构,了解如何构建一个高可用、可扩展的企业级应用架构。
目录
相关文章
|
Java 网络安全 Nacos
Nacos作为流行的微服务注册与配置中心,其稳定性与易用性广受好评
Nacos作为流行的微服务注册与配置中心,其稳定性与易用性广受好评。然而,“客户端不发送心跳检测”是使用中常见的问题之一。本文详细探讨了该问题的原因及解决方法,包括检查客户端配置、网络连接、日志、版本兼容性、心跳检测策略、服务实例注册状态、重启应用及环境变量等步骤,旨在帮助开发者快速定位并解决问题,确保服务正常运行。
241 5
|
存储 缓存 负载均衡
微服务架构中的服务发现与注册中心实践
【7月更文挑战第26天】在微服务的海洋里,每个服务都是一座孤岛。要让这些孤岛彼此发现、相互通讯,就需要一个高效的信使系统——服务发现与注册中心。本文将深入探讨如何搭建和维护这一核心组件,确保微服务间的顺畅交流。
|
网络安全 Nacos 开发者
Nacos作为流行的微服务注册与配置中心,“节点提示暂时不可用”是常见的问题之一
Nacos作为流行的微服务注册与配置中心,其稳定性和易用性备受青睐。然而,“节点提示暂时不可用”是常见的问题之一。本文将探讨该问题的原因及解决方案,帮助开发者快速定位并解决问题,确保服务的正常运行。通过检查服务实例状态、网络连接、Nacos配置、调整健康检查策略等步骤,可以有效解决这一问题。
316 4
|
Java 网络安全 Nacos
Nacos作为流行的微服务注册与配置中心,其稳定性和易用性备受青睐。
Nacos作为流行的微服务注册与配置中心,其稳定性和易用性备受青睐。然而,实际使用中常遇到“客户端不发送心跳检测”的问题。本文深入探讨该问题的原因及解决方案,帮助开发者快速定位并解决问题,确保服务正常运行。通过检查客户端配置、网络连接、日志、版本兼容性、心跳策略、注册状态、重启应用和环境变量等步骤,系统地排查和解决这一问题。
257 3
|
安全 Nacos 数据库
Nacos是一款流行的微服务注册与配置中心,但直接暴露在公网中可能导致非法访问和数据库篡改
Nacos是一款流行的微服务注册与配置中心,但直接暴露在公网中可能导致非法访问和数据库篡改。本文详细探讨了这一问题的原因及解决方案,包括限制公网访问、使用HTTPS、强化数据库安全、启用访问控制、监控和审计等步骤,帮助开发者确保服务的安全运行。
742 3
|
Cloud Native Java Nacos
微服务注册中心-Nacos概述
该博客文章提供了对Nacos的全面概述,包括其基本介绍、与Spring Cloud集成的优势、主要功能以及如何在Spring Cloud Alibaba项目中作为服务注册中心使用Nacos。文章解释了Nacos是一个动态服务发现、配置管理和服务管理平台,支持服务发现、健康监测、动态配置、DNS服务和元数据管理。还介绍了如何下载和启动Nacos服务器,以及如何将微服务注册到Nacos中,包括修改pom.xml文件引入依赖、配置application.properties文件和使用@EnableDiscoveryClient注解开启服务注册发现功能。
微服务注册中心-Nacos概述
|
设计模式 存储 运维
微服务架构中的服务发现与注册中心设计模式
在现代软件工程实践中,微服务架构已成为构建灵活、可扩展系统的首选方案。本文将深入探讨微服务架构中至关重要的服务发现与注册中心设计模式。我们将从服务发现的基本原理出发,逐步解析注册中心的工作机制,并以Eureka和Consul为例,对比分析不同实现的优劣。文章旨在为开发者提供一套清晰的指导原则,帮助他们在构建和维护微服务系统时做出更明智的技术选择。
|
存储 设计模式 前端开发
|
负载均衡 Java Nacos
EureKa详解:微服务发现与注册的利器
EureKa详解:微服务发现与注册的利器
|
敏捷开发 设计模式 负载均衡
深入理解微服务架构中的服务发现与注册机制
【7月更文挑战第24天】在微服务架构的海洋中,服务发现与注册机制如同灯塔指引着航行的船只。本文将探索这一机制的重要性、实现原理以及面临的挑战,带领读者领略微服务架构中的关键导航系统。

推荐镜像

更多