写点什么

golang 实现四层负载均衡

  • 2023-06-30
    广东
  • 本文字数:3071 字

    阅读完需:约 10 分钟

golang 实现四层负载均衡

大家好,我是蓝胖子,做开发的同学应该经常听到过负载均衡的概念,今天我们就来实现一个乞丐版的四层负载均衡,并用它对 mysql 进行负载均衡测试,通过本篇你可以了解到零拷贝的应用,四层负载均衡的本质以及实践。


本文代码已经上传到 github


https://github.com/HobbyBear/codelearning/tree/master/layer4balance
复制代码


为了知识的完整性,我们也科普下七层负载均衡的概念,我们先简单了解下四层负载均衡和 7 层负载均衡的区别。

四层负载均衡和七层负载均衡

七层负载均衡

首先,我们来看下七层负载均衡,它一般是针对应用层请求协议做请求转发,拿 http 请求举例,有 A,B 两台服务器,如果采用轮询的负载均衡策略,负载均衡器将第一个请求转发给了 A 服务器,那么第二个请求到达时,负载均衡器就会把请求转发到 B 服务器。


在转发时,能够在应用协议层对请求做一些变动,拿 http 请求来说,可以对 http 的请求头,http 路径做相应的变动。

四层负载均衡

再来看看四层负载均衡,它一般是指针对连接做的负载均衡,举例说明下,有 A,B 两台服务器,同样采取轮询的策略,某个客户端发起一个新的连接,经过均衡器连接到了 A 服务器,现在又来一个客户端同样发起连接,经过均衡器后,此时就该和 B 服务器建立连接了。而在同一个连接里是能够发送多个请求的,这也是和七层负载均衡最本质的区别,它是针对连接做的负载均衡。

实现四层负载均衡器

实现四层负载均衡策略的方式有很多,比较著名的四层负载均衡软件就有 lvs,它是通过修改数据包的 ip 地址或者 mac 地址实现四层负载均衡,性能较好,工作模式有好几种,具体的就不在本文展开了。


本文实现的四层负载均衡的原理和 nginx 四层负载类似 ,通过均衡器在客户端和服务端之前都维护一个连接来达到让 客户端在同一个连接里发送的请求都会被服务端同一个连接所接收的目的。如下图所示:



以后 client1 通过连接 A 发的请求都会由连接 B 发往服务器,而 client2 通过连接 C 发送的请求,都将经过连接 D 发往另一台服务器。

实现逻辑

现在让我们来实现下这部分的逻辑,我将会以轮询的策略实现连接的负载均衡。


并且这里还要考虑下实现数据复制的逻辑,我们需要在均衡器分别建立对客户端和服务端的 socket 连接,并且将其中一个 socket 的数据转移到另一个 socket,如果每次都将某一个 socket 数据读到用户层,再写到另一个 socket 就会导致一些没有必要的拷贝。伪代码如下:


var (src net.Conn  // 一个socket 连接dst net.Conn  // 一个socket连接)// ...buf = make([]byte, size)    nr, er := src.Read(buf)nw, ew := dst.Write(buf[0:nr])
复制代码


有没有什么技术让内核自动将某个 socket 的数据转移到另一个 socket,不用将数据拷贝到应用层来,这正是零拷贝相关的技术,关于零拷贝的技术原理我在之前这篇文章 有很详细的介绍,内核提供了一个 splice 的系统调用,专门用于 socket 连接间拷贝数据,只需要调用时传入对应 socket 连接的文件描述符即可让内核自动完成拷贝过程。


func Splice(rfd int, roff *int64, wfd int, woff *int64, len int, flags int) (n int64, err error) 
复制代码


这个系统调用已经被 golang 更深层次的封装到了一个比较常用的方法 io.Copy 里,这个方法会自动判断 reader 和 writer 底层的类型,如果都是 socket 连接则会调用 splice 系统调用实现零拷贝。


func Copy(dst Writer, src Reader) (written int64, err error) {     return copyBuffer(dst, src, nil)  }
复制代码


接着我们看下均衡的代码逻辑,运行逻辑如下:


1, 监听到新连接,启动一个协程去处理连接。


2 , 在新协程里与通过轮询的策略,选择一个后端服务器并与之建立连接。


3, 启动两个协程分别进行 io.Copy ,将客户端的 socket 写到服务端 socket,将服务端 socket 返回的信息写到客户端 socket。代码如下:


type Server struct {     Li      net.Listener     Balance balancepolicy.Policy  }    func (s *Server) Run() {     for {        c, err := s.Li.Accept()        if err != nil {           log.Fatal(err)        }        go func(c net.Conn) {           remoteAddr := c.RemoteAddr()           backendIp := s.Balance.PickNode(remoteAddr.String())           serverConn, err := net.Dial("tcp", backendIp)           if err != nil {              log.Fatal(err)              c.Close()              return           }           fmt.Println("获取到了新连接", remoteAddr, backendIp)           go func() {              _, err := io.Copy(serverConn, c)              if err != nil {                 fmt.Println(err, 1)              }              c.Close()              serverConn.Close()              fmt.Println("结束1", err)           }()           go func() {              _, err := io.Copy(c, serverConn)              if err != nil {                 fmt.Println(err, 2)              }              c.Close()              serverConn.Close()              fmt.Println("结束2", err)           }()        }(c)     }    }
复制代码


io.Copy 会不断的拷贝源 socket 的数据到目的 socket,直到连接关闭。

更好的方案

可以看到上述方案中维护一个客户端的连接将会启动 3 个协程,当连接量上去后,均衡器很可能成为瓶颈,有没有办法减少下协程的数量,可以直接采用 epoll 的方式监听连接的读写,以及关闭事件(这样能在一个协程里处理多个连接),当连接可读时,直接使用 splice 系统调用对数据进行拷贝直到返回 syscall.EAGAIN 就停止,因为返回 syscall.EAGAIN 说明连接缓冲区内的数据暂时被读取完了,继续下一次 epoll wait 的监听循环。这样能极大的减少协程数量。不过实现我就不准备再继续展开了,后续有空再补充下这部分。对 epoll 的使用有兴趣的同学也可以看看我之前一篇用epoll实现类似redis的网络模型框架这篇文章

测试负载均衡代码

现在让我们来测试下负载均衡的代码,我会用 docker-compose 去启动两个 mysql,然后本地启动我们负载均衡器的代码,之后用两个 mysql 客户端去连接负载均衡器,看下是不是 mysql 客户端连接到了不同的 mysql 服务器。


docker-compose 的配置文件如下:


version: '3'  services:    mysql1:      restart: always      image: amd64/mysql:latest      container_name: mysql1      environment:        - "MYSQL_ROOT_PASSWORD=1234567"        - "MYSQL_DATABASE=test"      ports:        - "3306:3306"      mysql2:      restart: always      image: amd64/mysql:latest      container_name: mysql2      environment:        - "MYSQL_ROOT_PASSWORD=1234567"        - "MYSQL_DATABASE=test2"      ports:        - "3307:3306"
复制代码


为了能验证不同客户端的确连上了不同的 mysql 服务器,我在 mysql1 上创建了 test 数据库,在 mysql2 上创建了 test2 数据库。到时候连上不同服务器数据库是不一样的。


均衡服务器监听 5555 端口启动


s := &proxy.Server{}  li, err := net.Listen("tcp", ":5555")  if err != nil {     log.Fatal(err)  }  s.Li = li  s.Balance = balancepolicy.NewRoundRobin()  s.Balance.AddNode("127.0.0.1:3306", "mysql1")  s.Balance.AddNode("127.0.0.1:3307", "mysql2")  s.Run()
复制代码


之后用 mysql 客户端去连接均衡服务器


## client1mysql -h 127.0.0.1 -u root  -P 5555  -D test  -p1234567
## client2mysql -h 127.0.0.1 -u root -P 5555 -D test2 -p1234567
复制代码



发现两个 mysql 客户端的确连接到了不同服务器,并且能正常执行命令,over。

发布于: 刚刚阅读数: 4
用户头像

还未添加个人签名 2020-09-17 加入

还未添加个人简介

评论

发布
暂无评论
golang 实现四层负载均衡_nginx_蓝胖子的编程梦_InfoQ写作社区