关于mysql:为MySQL-MGR实现简单的负载均衡代理

4次阅读

共计 6334 个字符,预计需要花费 16 分钟才能阅读完成。

  • GreatSQL 社区原创内容未经受权不得随便应用,转载请分割小编并注明起源。

原创:万里数据库,花家舍

导读

在多写(多节点写入)数据库(例如 MySQL MGR 的 multi-primary mode)与利用之间,往往会加一层代理组件,通过算法调节不同节点负载,散发高并发读写申请。

要求代理工具须要具备申请转发、负载平衡、故障转移的性能。

在后端节点故障产生或者连贯因为客户端异样、网络问题断开时,须要及时将故障节点及时踢出负载平衡队列或者敞开异样连贯,做到故障转移。

这就是接下来介绍的次要内容,应用 golang 简略编写一个这样的工具,来深刻学习一下负载平衡代理的实现。

1、性能一览

负载平衡

将利用端的连贯申请(负载)依照既定的平衡算法转发到不同的后端节点,服务程序建设利用(客户端)与数据库节点之间的通信并放弃至客户端断开连接。

故障转移

在后端节点呈现故障时,能及时的检测到故障,并将故障节点踢出负载平衡队列,不再将利用申请路由到故障节点,做到利用无感知。在故障复原后,可能检测到节点状态复原,将其再次退出到负载平衡队列。

2、实现细节

外围性能

申请转发

代理须要做到将申请散发到不同的后端节点下来,并放弃利用与对应节点的通信,直至其中一端退出(故障或者被动)。

负载平衡

对利用的负载,平衡的散发的不同的节点,须要对应的算法反对。目前通用的负载平衡算法有 随机 轮询 加权轮询,代码实现了这三种算法。

此外还有动静判断后端节点负载状况,依据负载状况动静调整负载散发,这须要额定的负载监控工作,这里没有实现。

故障检测

负载平衡代理须要防止向生效的节点散发申请。故障类型无疑是很多的,如果八面玲珑的对每个故障类型都关照到,无疑减少了实现难度。

例如在分布式中,不牢靠的网络减少了检测故障难度,对于数据库实例,在分布式中很难判断节点到底是 crash 了还是网络中断导致的。

并且节点因为负载较高无奈及时响应申请,这时也是很难判断节点状态,此时进行重试可能会加剧节点的负载。

在这里并不是要含糊这种判断,而是理论状况切实是太简单了,我并不是相干领域专家,所以在实现故障检测时,只思考了几种确定性较高或者容易判断的状况。

过程实现

其中,转发 实现过程是在接管到申请后,定义一个后端节点的地址,并建设一个和这个地址的连贯。

在开启两个协程,一个负责将利用(客户端)发送的数据包传递给后端的连贯,另一个是将后端的返回的数据传递给利用,这样就在利用与后端节点之间搭建起了通信,使之像间接通信一样替换数据,外围的步骤能够参考上面代码的实现。

sConn, err := l.Accept()
dTcpAddr, _ := net.ResolveTCPAddr("tcp4", addr)
dConn, err := net.DialTCP("tcp", nil, dTcpAddr)
go io.Copy(sConn, dConn)
go io.Copy(dConn, sConn)

在呈现连贯实现既定通信后断开连接,或者连贯因为故障退出,须要代理将客户端的申请连贯与转发向后端的连贯一起敞开。

这里应用的形式是获取连贯传输数据时的状态来判断,即 io.Copy(sConn, dConn)在呈现谬误时,连贯就能够敞开了。这里借助 channel 的阻塞个性来向主线程告诉退出。所以对上述的。

go io.Copy(sConn, dConn)
go io.Copy(dConn, sConn)

代码进行批改后如下:

        // channel 长度为 1,任意时刻只写入一个 bool 值,在其中的值未被读取之前,处于阻塞状态
    exitCH := make(chan bool, 1)
        // 把客户端的的申请转发给后端
    go func(s net.Conn, d *net.TCPConn, ex chan bool) {_, err := io.Copy(sConn, dConn)
        if err != nil {Error.Println("Send data failure:", err)
        }
        exitCH <- true
    }(sConn, dConn, exitCH)
        // 把响应的数据返回给客户端
    go func(s net.Conn, d *net.TCPConn, ex chan bool) {_, err := io.Copy(dConn, sConn)
        if err != nil {Error.Println("Receive data failure:", err)
        }
        exitCH <- true
    }(sConn, dConn, exitCH)
        // channel 阻塞,读取连贯敞开状态
    <-exitCH
    // channel 收到信息(连贯终止)后,敞开连贯
    _ = dConn.Close()

负载平衡 算法的实现则是在每次向后端建设连贯的时候,这个后端地址是依据算法的不同,返回一个负载平衡算法举荐的后端节点的地址,而后应用这个地址建设一个连贯,并与利用搭建起通信(正如上一步骤介绍的那样)。

其中随机算法较为简单,外围是随机数的获取,应用这个随机数作为下标在负载平衡队列中拿到具体的节点:

type Random struct {
    CurIndex int
    Nodes    []*node.Node}

func (r *Random) Next() *node.Node {if len(r.Nodes) == 0 {return nil}
    r.CurIndex = rand.Intn(len(r.Nodes))
    return r.Nodes[r.CurIndex]
}

轮询算法则是每次获取后端节点信息是采取的一一查问的形式获取须要散发申请的节点:

type RoundRobin struct {
    CurIndex int
    Nodes    []*node.Node}

func (r *RoundRobin) Next() *node.Node {if len(r.Nodes) == 0 {return nil}
    l := len(r.Nodes)
    if r.CurIndex >= l {r.CurIndex = 0}
    currAddr := r.Nodes[r.CurIndex]
    r.CurIndex = (r.CurIndex + 1) % l
    return currAddr
}

加权轮询算法实现上绝对简单一些,为每个后端节点减少权重属性,蕴含三个权重属性:权重(Weight)、长期权重(CurWeight)、无效权重(EffectWeight)。

其中 CurWeight、EffectWeight 初始值为 0,Weight 值则读取配置文件设定来初始化。CurWeight 每轮都会变动,EffectWeight 默认与 Weight 雷同。

实现逻辑

1、currentWeight = currentWeight + effecitveWeight

2、选中最大的 currentWeight 节点为选中节点

3、currentWeight = currentWeight – totalWeight

type WeightRoundRobin struct {Nodes []*node.Node
}

func (r *WeightRoundRobin) Next() *node.Node {
    var n *node.Node
    total := 0
    for i := 0; i < len(r.Nodes); i++ {w := r.Nodes[i]
        total += w.EffectWeight
        w.CurWeight += w.EffectWeight
        if w.EffectWeight < w.Weight {w.EffectWeight++}
        if n == nil || w.CurWeight > n.CurWeight {n = w}
    }
    if n == nil {return nil}
    n.CurWeight -= total
    return n
}

故障检测 是保障负载平衡队列中的节点是能够失常拜访并且提供牢靠服务的前提,在检测到后端节点存在故障后,须要及时的从队列中剔除,并敞开与之对应的连贯。

检测在理论实现上应用了两种根本办法。一种是根本的连通性检测,一种是利用 MGR 或者 GreatDB 提供的外部视图来判断节点是否可写。

这种在 MGR 中从以后节点查问本节点状态可能并不精确例如:产生网络分区,从以后节点查看状态为 ONLINE,但从其余节点查看,则以后可能为 ERROR 状态,代码并未思考这个状况。

后续可减少对一个节点可写状态判断须要与其余节点的状态查问综合思考。

连通性检测:

_, err := net.DialTimeout("tcp", addr, time.Duration(dialtimeout)*time.Millisecond)

这里是借助命令行工具实现可写检测,没有应用开源的连贯驱动,次要是思考代码的简洁。

可写检测:

var CMD = "mysql"
func State(detectSql string, user string, pass string, port string, host string, cluster string) (bool, error) {ok, _ := CommandOk(CMD)
    if ok {
        sqlComLine := CMD + "-u" + user + "-p" + pass + "-h" + host + "-P" + port + "-NBe'"
        if cluster == "greatdb" {sqlComLine += detectSql + "WHERE HOST=" + "\"" + host + "\"" + "'"} else if cluster =="mgr" {sqlComLine += detectSql + "WHERE MEMBER_HOST=" + "\"" + host + "\"" + "'"}
        cmd := exec.Command("bash", "-c", sqlComLine)
        out, err := cmd.CombinedOutput()
        rest := strings.Replace(string(out), "\n", "", -1)
        if err == nil {
            if rest == "ONLINE" {return true, nil} else {return false, errors.New("instance is exists but cannot write")
            }
        }
        return false, err
    } else {return false, errors.New("cannot detect instance state")
    }
}

func CommandOk(c string) (bool, error) {
    command := "which" + c
    cmd := exec.Command("bash", "-c", command)
    out, err := cmd.CombinedOutput()
    if err == nil {context := strings.Fields(strings.Replace(string(out), "\n", "", -1))
        if len(context) > 2 {if context[1] == "no" {return false, nil}
        }
        return true, nil
    }
    return false, err
}

在检测到后端节点连通性有问题或者节点状态为不可写,须要将节点踢出负载平衡队列,这里通过加锁来避免并发操作队列引入新的代码谬误。

而后通过 channel 告诉主线程负载平衡队列产生了变动,须要更新。其次是告诉主线程须要将各个协程在解决的与故障节点无关的连贯,须要敞开。

func DelNode(n *node.Node) {for i := 0; i < len(nodeList); i++ {if nodeList[i].Ip == n.Ip && nodeList[i].Port == n.Port {mu.Lock()
            nodeList = append(nodeList[:i], nodeList[i+1:]...)
            listChange <- 1
            connClose <- n.Ip
            mu.Unlock()
            Error.Println("The destination address is removed from the load balance list :", net.JoinHostPort(n.Ip, strconv.Itoa(n.Port)))
        }
    }
}

在心跳检测到后端节点可写状态复原,则须要将其再次退出到负载平衡队列,新的连贯会依据负载平衡算法的均衡,路由到复原的节点上,也就是会再次散发申请到失常节点。

    // 在队列中不存在,则增加
    if exists == false {mu.Lock()
        defer mu.Unlock()
        nodeList = append(nodeList, n)
        ch <- 1
    Info.Println("The destination address is added to the load balance list :", addr)
    }

3、应用问题

程序启动

目前只在 CentOS 7.6 上进行了简略测试,测试了后端节点被 kill、机器 reboot、连贯异样断开等故障状况

cd easy-proxy<br>
go build main/easyproxy<br>

批改配置,减少后端节点、端口、权重等

如果须要疾速故障转移,能够配置 ticktime 和 dialtimeout 参数,单位是毫秒。

nohup ./easyproxy --cnf=conf/easy.conf &

可能问题

在应用过程可能会遇到

accept tcp [::]:3310: accept4: too many open files

或者

dial tcp 127.0.0.0:3310 socket: too many files

这是系统文件描述符的数量不够用了,解决办法是能够减少文件描述符的数量

ulimit -n 1024000

批改文件描述符后,重新启动过程,查看过程最大关上文件数:Max open files

cat /proc/18659/limits 
......
Max open files            1024000              1024000              files      
......

一点想法

后续可思考对程序减少守护过程,保障程序肯定水平的可用性,代理工具无状态,也能够进行扩大来实现 HA。

这里只是简略的实现了一下申请代理和负载平衡,通过编码加深对负载平衡的了解不失为一个无效办法,测试并不充沛。

代码约 600 行左右,没有通过 DB Driver 连贯数据库,而是借助命令行来操作,后续会持续欠缺。心愿能带来一些对负载平衡的思考。

源码地址:https://gitee.com/huajiashe_byte/easy-proxy

Enjoy GreatSQL :)

文章举荐:

面向金融级利用的 GreatSQL 正式开源
https://mp.weixin.qq.com/s/cI…

Changes in GreatSQL 8.0.25 (2021-8-18)
https://mp.weixin.qq.com/s/qc…

MGR 及 GreatSQL 资源汇总
https://mp.weixin.qq.com/s/qX…

GreatSQL MGR FAQ
https://mp.weixin.qq.com/s/J6…

在 Linux 下源码编译装置 GreatSQL/MySQL
https://mp.weixin.qq.com/s/WZ…

# 对于 GreatSQL

GreatSQL 是由万里数据库保护的 MySQL 分支,专一于晋升 MGR 可靠性及性能,反对 InnoDB 并行查问个性,是实用于金融级利用的 MySQL 分支版本。

Gitee:

https://gitee.com/GreatSQL/Gr…

GitHub:

https://github.com/GreatSQL/G…

Bilibili:

https://space.bilibili.com/13…

微信 &QQ 群:

可搜寻增加 GreatSQL 社区助手微信好友,发送验证信息“加群”退出 GreatSQL/MGR 交换微信群

QQ 群:533341697

微信小助手:wanlidbc

本文由博客一文多发平台 OpenWrite 公布!

正文完
 0