好好爱自己!

[转]微服务注册中心分布式集群设计原理与 Golang 实现

 

原文:http://www.zztongyun.com/article/golang%20%E6%B3%A8%E5%86%8C%E4%B8%AD%E5%BF%83

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

 

服务注册发现作为微服务的基础组件,它的稳定性和可用性备受考验。在之前的文章中,我们介绍了服务注册中心的基本原理和实现,具体参阅:

服务注册中心设计原理与Golang实现

今天我们来讨论实现注册中心集群版,本文主要内容包括:分布式集群架构原理

分布式数据复制技术

状态一致性协同算法

Golang 代码实现集群服务

集群待解决问题

要实现注册中心从单机版到分布式集群,有几个关键问题要解决:

集群成员间的关系与成员发现问题集群成员间数据复制与一致性问题数据副本机制和数据分区策略

针对上述问题会有不同解决方案,而不同方案会对集群的可用性、容错能力和数据一致性造成不同结果,著名的 CAP 理论就是对分布式问题的最好诠释。架构就是在不同的方案和结果中进行的折中,没有最好的方案,只有适合场景的最佳实践,权衡取舍也是架构之魅力所在。

节点关系与成员发现

架构模型

集群中节点关系可以分为两种:平等公平关系和非公平关系。

P2P (pear to pear)点对点架构就是平等公平关系,这种关系中各节点没有领导分工,大家分摊工作,共同努力完成目标。

与之对立的非公平关系,我们熟知的 Master/Slave 主从架构(主备架构),由于主从这个名字带有歧视色彩,最新的叫法是 Leader/Follower 领导者跟随者架构,在这个架构中节点的地位是不一样的,会有不同的角色分工。

技术选型

针对注册中心场景选择哪种架构呢?可以从以下几点分析。

1.读写性能点对点架构每个节点都可以承担读和写,读写性能最佳;

主从架构一般是做读写分离,写主读从(当然也有同步写,后面会分析到),相对来说写性能有限,但可以通过多个从来提升读性能。

注册中心场景一般读多写少,这点上倒也没有绝对的优劣。

 

2.可用性

点对点架构中某节点挂了,读写不受影响,但可能会丢数据造成数据不一致,数据一致性会差一些;

主从架构中主挂了会影响写,比如 MySQL 的 MHA,Redis 的 Sentinel 都是用来监控并实现切主,来保障高可用。而像 Zookeeper 支持半数以内的节点挂掉,超过半数就要触发重新选主了,此时不能写入。相比于点对点架构,整体可用性会差一点。

 

CAP 理论告诉我们,分布式系统在一致性(Consistency)、可用性(Availability) 和分区容错性 (Partition tolerance)三者只能选其二。在集群正常情况下,一致性和可用性都没问题(也就是 CA,网上大多数文章说 CA 模型不存在,其实说法并不准确,在正常情况下,一致性和可用性还是可以同时保障的)。但当集群出现异常,分区容错性必须保障(想想为什么?),那么一致性和可用性就要二选一,选 AP 还是 CP?

(CAP 理论 图片来自网络)

注册中心场景中,服务寻址都要依赖注册中心,可用性显得更加重要,而短暂不一致可忽略,毕竟服务上下线变动并不频繁,就算偶尔没拿到最新服务实例也不影响其他服务。著名的注册中心 Euraka 就使用了 AP 模型,并阐明 Zookeeper 这种基于 CP 模型的注册中心不可取,可参阅文章:

Why You Shouldn’t Use ZooKeeper for Service Discovery

3.架构实现

点对点架构实现相对更简单,不用考虑选主或主从切换的问题,节点状态也只要考虑上线状态和下线状态即可;

而领导者协调者架构在实现实现选主时要应对复杂的一致性协同算法,维护更复杂的状态机。

 

综合分析,注册中心技术选型使用点对点架构更合适,我们会以此架构展开讨论。

集群架构设计

我们来看点对点集群架构图:

(注册中心集群点对点架构图)

每个节点 Node 互相独立,并通过数据复制同步数据,每个节点都可接受服务注册、续约和发现操作。针对注册中心各节点相互发现问题,既然注册中心本身就是解决服务注册发现的,那么使用自己来管理自己不就好了?所以可以将节点作为服务实例,实现自发现。

(注册中心集群节点自发)

代码实现

下面我们通过具体代码来展开讲解实现原理。首先我们定义节点的概念和结构体,一个节点就是一个独立的注册中心服务,集群由多个节点组成。结构体 Node 存储节点地址和节点状态,节点状态有两种:上线状态(可对外提供服务),下线状态(不对外服务)。

type Node struct {    config      *configs.Config    addr        string    status      int }func NewNode(config *configs.GlobalConfig, addr string) *Node {    return &Node{        addr:        addr,        status:      configs.NodeStatusDown, //初始化设为下线状态    }   }

(代码 model/node.go)

结构体 Nodes 用于存放所有节点列表和当前节点地址,方便节点初始化和节点感知。

type Nodes struct {    nodes    []*Node    selfAddr string}//初始化默认从配置文件中加载节点信息func NewNodes(c *configs.GlobalConfig) *Nodes {    nodes := make([]*Node, 0, len(c.Nodes))    for _, addr := range c.Nodes {        n := NewNode(c, addr)        nodes = append(nodes, n)    }    return &Nodes{        nodes:    nodes,        selfAddr: c.HttpServer,    }   }

(代码 model/nodes.go)

最后将 Nodes 维护到 Discovery 结构体中,当服务启动首次加载全局 Discovery 时,开始创建并维护 Nodes 列表。type Discovery struct {    config    *configs.GlobalConfig    protected bool    Registry  *Registry+   Nodes     atomic.Value}func NewDiscovery(config *configs.GlobalConfig) *Discovery {//...+ dis.Nodes.Store(NewNodes(config))}

(代码 model/discovery.go)

注册中心节点实现自发现,节点之间可以感知到状态变化,注册中心集群当做服务 Application,将AppId  统一命名为 Kavin.discovery (写到配置文件configs.DiscoveryAppId),每个节点对应服务实例 Instance。对这块概念还不清楚,可以先参阅上一篇文章:

服务注册中心设计原理与Golang实现

在启动服务初始化 Discovery 时,将自己注册到注册中心。func (dis *Discovery) regSelf() {    now := time.Now().UnixNano()    instance := &Instance{        Env:             dis.config.Env,        Hostname:        dis.config.Hostname,        AppId:           configs.DiscoveryAppId, //Kavin.discovery        Addrs:           []string{"http://" + dis.config.HttpServer},        Status:          configs.NodeStatusUp,        RegTimestamp:    now,        UpTimestamp:     now,        LatestTimestamp: now,        RenewTimestamp:  now,        DirtyTimestamp:  now,    }    dis.Registry.Register(instance, now)    //注册后同步到其他集群,下面部分会展开讲解    dis.Nodes.Load().(*Nodes).Replicate(configs.Register, instance)}

(代码 model/discovery.go)

注册成功后同步给集群其他节点,数据复制后面会具体讲解。注册成功后还要实现节点的定期续约,每 30s 发送一次续约请求,如果续约返回了 NotFound 未找到实例,做了一次重新注册的操作,保障了系统的健壮性。func (dis *Discovery) renewTask(instance *Instance) {    now := time.Now().UnixNano()    ticker := time.NewTicker(configs.RenewInterval) //30 second    defer ticker.Stop()    for {        select {        case 



【本文地址】http://www.zztongyun.com/article/golang%20%E6%B3%A8%E5%86%8C%E4%B8%AD%E5%BF%83

posted @ 2023-02-10 14:56  立志做一个好的程序员  阅读(160)  评论(0编辑  收藏  举报

不断学习创作,与自己快乐相处