Golang实现四层负载均衡的示例代码

 更新时间:2023年07月02日 14:29:25   作者:蓝胖子的编程梦  
做开发的同学应该经常听到过负载均衡的概念,今天我们就来实现一个乞丐版的四层负载均衡,并用它对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客户端去连接均衡服务器

## client1
mysql -h 127.0.0.1 -u root  -P 5555  -D test  -p1234567

## client2
mysql -h 127.0.0.1 -u root  -P 5555  -D test2  -p1234567

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

到此这篇关于Golang实现四层负载均衡的示例代码的文章就介绍到这了,更多相关Golang四层负载均衡内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • GoLang之标准库encoding/json包

    GoLang之标准库encoding/json包

    本文主要介绍了GoLang之标准库encoding/json包,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-08-08
  • Golang 断言与闭包使用解析

    Golang 断言与闭包使用解析

    这篇文章主要介绍了Golang 断言与闭包使用解析,Go中的断言用于判断变量的类型,更多相关内容需要的朋友可以参考一下
    2022-07-07
  • 一文带你搞懂Golang如何正确退出Goroutine

    一文带你搞懂Golang如何正确退出Goroutine

    在Go语言中,Goroutine是一种轻量级线程,它的退出机制对于并发编程至关重要,下午就来介绍几种Goroutine的退出机制,希望对大家有所帮助
    2023-06-06
  • Go语言中使用flag包对命令行进行参数解析的方法

    Go语言中使用flag包对命令行进行参数解析的方法

    这篇文章主要介绍了Go语言中使用flag包对命令行进行参数解析的方法,文中举了一个实现flag.Value接口来自定义flag的例子,需要的朋友可以参考下
    2016-04-04
  • Golang中的变量学习小结

    Golang中的变量学习小结

    本文主要带大家学习了Golang里面的四大类型的变量,十分的详细,有需要的小伙伴可以参考下
    2018-10-10
  • Go语言基础Json序列化反序列化及文件读写示例详解

    Go语言基础Json序列化反序列化及文件读写示例详解

    这篇文章主要为大家介绍了Go语言基础Json序列化反序列化以及文件读写的示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助
    2021-11-11
  • 详解Golang实现请求限流的几种办法

    详解Golang实现请求限流的几种办法

    这篇文章主要介绍了详解Golang实现请求限流的几种办法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-04-04
  • go generate代码自动生成指南

    go generate代码自动生成指南

    这篇文章主要介绍了go generate代码自动生成指南,本文将探讨 go generate 命令的使用方法、原理以及一些实际应用场景,希望读者能够更好地理解和运用这个强大的工具
    2024-01-01
  • 详解简单高效的Go struct优化

    详解简单高效的Go struct优化

    这篇文章主要为大家介绍了简单高效的Go struct优化示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-03-03
  • 详解Go语言中接口应用模式或惯例介绍

    详解Go语言中接口应用模式或惯例介绍

    这篇文章主要为大家详细介绍了Go语言中接口应用模式或惯例介绍的相关知识,文中的示例代码讲解详细,有需要的小伙伴可以跟随小编一起学习一下
    2023-11-11

最新评论