基于Go实现TCP长连接上的请求数控制

 更新时间:2024年05月31日 09:04:31   作者:二郎腿  
在服务端开启长连接的情况下,四层负载均衡转发请求时,会出现服务端收到的请求qps不均匀的情况或是服务器无法接受到请求,因此需要服务端定期主动断开一些长连接,所以本文给大家介绍了基于Go实现TCP长连接上的请求数控制,需要的朋友可以参考下

一、背景

  • 在服务端开启长连接的情况下,四层负载均衡转发请求时,会出现服务端收到的请求qps不均匀的情况,或是服务重启后会长时间无法接受到请求,导致不同服务端机器的负载不一致,qps高的机器过载的问题;
  • 该问题的原因是只有在新建连接时才会触发负载四层负载均衡器的再均衡策略,客户端随机与不同的服务器新建 TCP 连接,否则现有的 TCP 连接够用时,会一致被复用,在现有的 TCP 连接上传输请求,出现 qps 不均匀的情况;
  • 因此需要服务端定期主动断开一些长连接,触发四层转发连接的再均衡策略,实现类似于七层负载均衡 Nginx 中的 keepalive_requests 字段的功能,即同一个 TCP 长连接上的请求数达到一定数量时,服务端主动断开 TCP 长连接。

二、基本介绍

1,TCP 的 Keepalive:

  • 即 TCP 保活机制,是由 TCP 层(内核态) 实现的,位于传输层,相关配置参数在 /proc/sys/net/ipv4目录下:
    • tcp_keepalive_intvl:保活探测报文发送时间间隔,75s;
    • tcp_keepalive_probes:保活探测报文发送次数,9次,9次之后直接关闭;
    • tcp_keepalive_time:保活超时时间,7200s,即该 TCP 连接空闲两小时后开始发送保活探测报文;
  • TCP 连接传输完数据后,不会立马主动关闭,会先存活一段时间,超过存活时间后,会触发保活机制发送探测报文,多次探测确认没有数据继续传输后,再进行 TCP 四次挥手,关闭 TCP 连接;
  • 在 go 语言中,建立 TCP 连接时,默认设置的 keep-alive 为 15s,详见 go1.21 src/net/tcp/tcpsocket.go

2,HTTP 的 Keep-Alive

  • 即 HTTP 长连接,是由应用层(用户态) 实现的,位于应用层;

  • 需要在 HTTP 报文的头部设置以下信息:

* Connection: keep-alive
* Keep-Alive: timeout=7200
  • 上面信息表示,http 采用长连接,且超时时间为7200s;

  • http 协议 1.0 默认采用短连接,即每次发送完数据后会设置 Connection: close 表示需要主动关闭当前 TCP 连接,进行四次挥手后关闭;下次再发送数据前,又需要先进行三次握手建立 TCP 连接,才能发送数据;循环往复,每次建立的 TCP 连接都只能发送一次数据,每次发送数据都需要进行三次握手与四次挥手,每次建立连接与断开连接会导致网络耗时变长,如下图;

  • http 协议 1.1 开始默认采用长连接;即每次发送完数据后会设置 Connection: keep-alive 表示需要复用当前 TCP 连接,建立一次 TCP 连接后,可以发送多次的 HTTP 报文,即多次发送数据也只需要一遍三次握手与四次挥手,省去了每次建立连接与断开连接的时间,如下图:

3,四层负载均衡

  • 四层负载均衡是一种在网络层(第四层)上进行负载均衡的技术,通过传输层协议 TCP 或 UDP,将传入的请求分发到多个服务器上,以实现请求的负载均衡和高可用性;
  • 四层负载均衡主要基于目标IP地址和端口号对请求进行分发,不深入分析请求的内容和应用层协议,通常使用负载均衡器作为中间设备,接收客户端请求,并将请求转发到后端服务器;
  • 负载均衡器可以根据预定义的算法(例如轮询、最小连接数、哈希、随机、加权随机等)选择后端服务器来处理请求;
  • 四层负载均衡只需要解析到传输层协议即可进行请求转发,且是直接和真实服务器建立 TCP 连接,所以整体耗时比较小;

4,七层负载均衡

  • Nginx 可以用于七层负载均衡器,客户端与 Nginx 所在的服务器建立起 TCP 连接,通过解析应用层中的内容,选择对应的后端服务器,Nginx 所在的机器再与后端服务器建立起 TCP 连接,将应用层数据转发后端服务器上,这就是所谓的七层负载均衡,即根据应用层的信息进行转发;
  • 在 Nginx 中,keepalive_requests 指令用于设置在长连接上可以处理的最大请求数量,一旦达到这个数量,Nginx 将关闭当前连接并等待客户端建立新的连接以继续处理请求;
  • 通过限制每个持久连接上处理的请求数量,keepalive_requests 可以帮助控制服务器资源的使用,并防止连接过度占用服务器资源,也可以帮助避免潜在的连接泄漏和提高服务器的性能;
  • 该值设置得过小,会导致经常需要 TCP 三次握手和四次挥手,无法有效发挥长连接的性能;该值设置得过大,会无法发挥该值的作用,导致长连接上的请求过多;具体的大小,要根据实际请求的 QPS 和响应耗时来设置;
  • 七层负载均衡能够根据应用层的请求内容实现更惊喜的请求分发和处理,但是需要建立两次 TCP 连接,以及每次将报文逐步解析到应用层再又逐步封装链路层,会导致耗时和失败率上涨;
  • 四层负载均衡与七层负载均衡的比较如下:

三、具体实现

1,代码示例

package main

import (
	"sync"
	"time"

	"github.com/labstack/echo"
)

type QpsBalance struct {
	mu   sync.Mutex
	data map[string]int // key: ip:port
	num  int            // 通过配置文件来配置
}

// Update 返回 true 表示当前的 tcp 连接上的请求数超过限制,需要断开连接
// 如果是某个 tcp 连接长时间没有后续请求了,默认 15s 之后会发送保活报文,
func (q *QpsBalance) Update(k string) bool {
	q.mu.Lock()
	defer q.mu.Unlock()

	num := q.data[k] + 1
	if num >= q.num {
		q.data[k] = 0
		return true
	}
	q.data[k] = num
	return false
}

// Reset 通过定时任务每天3点重置,避免上游多次不同的扩容ip形成脏数据
func (q *QpsBalance) Reset() {
	q.mu.Lock()
	defer q.mu.Unlock()

	q.data = make(map[string]int)
}

func (q *QpsBalance) Init(n int) {
	q.mu.Lock()
	defer q.mu.Unlock()

	q.data = make(map[string]int)
	q.num = n
}

func main() {
	balancer := &QpsBalance{}
	balancer.Init(200)
	e := echo.New()
	e.PUT("/handle", func(c echo.Context) error {
		if balancer.Update(c.Request().RemoteAddr) {
			c.Response().Header().Set("Connection", "close")
		}
		// do other
		return nil
	})

	go func(b *QpsBalance) {
		ticker := time.NewTicker(time.Hour)
		for {
			t := <-ticker.C
			if t.Hour() == 3 {
				b.Reset()
			}
		}
	}(balancer)
}

2,基本原理

  • 当同一个 TCP 连接上的请求数达到一定限制时,设置返回头部为 Connection: close主动关闭 TCP 连接,并重置计数器
  • 需要注意的是,客户端如果也是服务器,并且存在自动扩容,那么需要定期清理计数的 map,避免多次不同的扩容ip形成脏数据;
  • 以及某些 TCP 连接可能没有达到计数的阈值,便不再被复用了,经过一段时间后会主动断开,这些 TCP 的计数依然存在 map 中,形成了脏数据;
  • 在 TCP 连接中,使用四元组来标记的一个唯一的 TCP 连接,即源ip、源端口、目的ip、目的端口,现在是在服务端进行计数的,所以目的ip和目的端口都是一样的,仅仅通过源ip和源端口便可以分别出 TCP 连接;

四、数据验证

  • 通过服务记录的监控,查看上游请求过来的平均耗时、P99 耗时、失败率是否有明显的变化,以及不同服务器收到的请求 QPS 是否均匀
  • 通过 tcpdump src port 28080 -A | grep Connection 命令查看服务端响应的HTTP报文头部是否有 Connection: close 字段,即是否会主动关闭 TCP 连接;
  • 通过 netstat -antp | grep main | grep :28080 命令查看不同服务器上的服务建立的长连接数是否均匀
  • 选取某个长连接,多次查看其存在的时间,是否符合预期;
    • 预期时间:假如 keepalive_requests 设置为 100,客户端记录的响应耗时 20ms(包括网络耗时和服务端耗时),那么平均一个长连接一秒能够发送 5 个请求,约 20s 后能够处理 100 个,那么该长连接能够存活 20s;
    • 注意不能查看某个长连接对应的 socket 创建的时间,因为同一个 socket 会被不同的长连接复用,一般不会被关闭;

以上就是基于Go实现TCP长连接上的请求数控制的详细内容,更多关于Go TCP请求数控制的资料请关注脚本之家其它相关文章!

相关文章

  • Go语言开发中有了net/http为什么还要有gin的原理及使用场景解析

    Go语言开发中有了net/http为什么还要有gin的原理及使用场景解析

    这篇文章主要为大家介绍了Go语言有了net/http标准库为什么还要有gin第三方库的原理及使用场景详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-08-08
  • golang的时区和神奇的time.Parse的使用方法

    golang的时区和神奇的time.Parse的使用方法

    这篇文章主要介绍了golang的时区和神奇的time.Parse的使用方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-04-04
  • Golang中Gin框架中如何定义路由详解

    Golang中Gin框架中如何定义路由详解

    Gin是一个用Go语言编写的Web框架,具有高性能和易于使用的特点,本文将结合实际案例,详细介绍Gin框架的路由用法,有需要的小伙伴可以参考下
    2024-10-10
  • Go 语言数组和切片的区别详解

    Go 语言数组和切片的区别详解

    本文主要介绍了Go 语言数组和切片的区别详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-04-04
  • Go 实现热重启的详细介绍

    Go 实现热重启的详细介绍

    这篇文章主要介绍了Go 实现热重启的详细介绍,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-09-09
  • Go语言实现并发控制的常见方式详解

    Go语言实现并发控制的常见方式详解

    这篇文章主要为大家详细介绍了Go语言实现并发控制的几种常见方式,文中的示例代码讲解详细,具有一定的借鉴价值,有需要的小伙伴可以参考一下
    2024-03-03
  • 基于Go语言实现高性能文件上传下载系统

    基于Go语言实现高性能文件上传下载系统

    在Web应用开发中,文件上传下载是一个非常常见的需求,本文将介绍如何使用Go语言实现一个安全、高效的本地文件存储系统,感兴趣的小伙伴可以了解下
    2025-03-03
  • 详解Go多协程并发环境下的错误处理

    详解Go多协程并发环境下的错误处理

    这篇文章主要介绍了详解Go多协程并发环境下的错误处理,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-08-08
  • Go语言中序列化与反序列化示例详解

    Go语言中序列化与反序列化示例详解

    我们的数据对象要在网络中传输或保存到文件,就需要对其编码和解码动作,Go语言当然也支持所有这些编码格式,下面这篇文章主要给大家介绍了关于Go语言中序列化与反序列化的相关资料,需要的朋友可以参考下
    2022-07-07
  • golang实现LRU缓存淘汰算法的示例代码

    golang实现LRU缓存淘汰算法的示例代码

    这篇文章主要介绍了golang实现LRU缓存淘汰算法的示例代码,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-12-12

最新评论