在Golang中保持TCP链接

原文地址:这里,省略了一些非重点片段。

尽管目前大多数系统都是通过gRPC或HTTP进行通信的,但仍有相当多的应用程序使用自定义协议。而且这些自定义协议中的许多都没有类似net/http这样方便的包来管理TCP连接。

这篇文章是为了那些直接和TCP连接打交道的人准备的。本文将讨论如何长期维护健康的TCP会话,以及如何对操持长链接的系统进行调优。

健康的链接

对于使用了长链接的应用来说最常见的问题就是如何保证这些链接的健康,很多情况都不利于长链接的保持。比如,防火墙管理员经常会设置最大空闲时间,当TCP链接中长时间没有数据传输时这些链接将被杀死。

修改了客户端或者服务端也可能导致链接断开,虽然其中一方可能知道链接被断开,而另一方则不一定知道,直到他试图发送一条消息。

一种有用的办法解决上面问题就是启用TCP存活消息机制。

TCP Keepalive

keepalive是TCP一种特性,当长时间不活跃时将发送一个特殊的数据包。这个数据包不包含任何数据而是要求一个ACK响应,当对端收到这个keepalive数据包时,将发送一个ACK响应回去。

这种设计的优点就是只需要一端开启TCP keepalive即可,因为keepalive是一个设置了ACK标志的数据包,所以TCP协议要求对端发送ACK,而不管对端的keepalive配置。

启用Keepalive

启用Keepalive十分简单,在Golang中设置net.TCPConn.SetKeepAlive()true即可。下面的代码展示了如何在服务端启用:


	// Resolve TCP Address
	addr, err := net.ResolveTCPAddr("tcp", "127.0.0.1:9000")
	if err != nil {
		fmt.Printf("Unable to resolve IP")
	}

	// Start TCP Listener
	l, err := net.ListenTCP("tcp", addr)
	if err != nil {
		fmt.Printf("Unable to start listener - %s", err)
	}

	// Wait for new connections and send them to reader()
	for {
		c, err := l.AcceptTCP()
		if err != nil {
			fmt.Printf("Listener returned - %s", err)
			break
		}

		// Enable Keepalives
		err = c.SetKeepAlive(true)
		if err != nil {
			fmt.Printf("Unable to set keepalive - %s", err)	
		}
		
		go reader(c)
	}

在客户端启用Keepalive示例代码如下:

	// Open TCP Connection
	c, err := net.DialTCP("tcp", nil, addr)
	if err != nil {
		fmt.Printf("Unable to dial to server - %s", err)
	}

 	// Enable Keepalives
	err = c.SetKeepAlive(true)
	if err != nil {
		fmt.Printf("Unable to set keepalive - %s", err)	
	}

调整系统参数

Keepalive Idle Time

tcp_keepalive_time这个系统参数决定了链接空闲多久之后才会发送keepalive数据包。

Keepalives通过周期性的发送数据包来保持TCP链接的存活和健康,但这仅仅是TCP连接不会频繁发送数据的情况才需要开始保活。如果在使用频繁的TCP连接上发送了保活使用的数据包,反而会引起问题(因为缺少ACK响应甚至收到了对端的RST响应)。

通常来说tcp_keepalive_time默认为7200秒(2小时),这意味着默认情况下只有连接空闲了2个小时后才会发送keepalive数据包。

当然了,这个值也可以通过sysctl命令来进行修改:

sysctl -w net.ipv4.tcp_keepalive_time=300

如果想在机器重启后也保持这个修改后的值,需要去修改/etc/sysctl.conf文件。

Keepalive Interval

tcp_keepalive_intvl这个参数决定了发送keepalive数据包之间的时间间隔。

默认值是75秒,结合上面的参数,也就是说连接空闲了2个小时后,每隔75秒钟发送一个keepalive数据包。

同样可以使用sysctl命令或/etc/sysctl.conf修改这个值:

sysctl -w net.ipv4.tcp_keepalive_intvl=30

Keepalive failures

tcp_keepalive_probes这个参数决定了发送多少keepalive数据包没有收到ACK响应后,通知应用层这个连接完蛋了。

默认值是9次,结合上面2个参数,也就是说连接空闲了2个小时后,每隔75秒钟发送一个keepalive数据包,重试9次如果都得不到ACK响应(11分钟15秒)后内核将通知应用层这个连接不健康。

同样可以使用sysctl命令或/etc/sysctl.conf修改这个值:

sysctl -w net.ipv4.tcp_keepalive_probes=5

修改系统默认值

tcp_keepalive_probes, tcp_keepalive_intvl, tcp_keepalive_time这三个参数都是启用Keepalive机制时系统的默认值,在golang中可以使用net.TCPConn.SetKeepAlivePeriod()方法来修改:

	// Set Keepalive time interval
	err = c.SetKeepAlivePeriod(30 * time.Second)
	if err != nil {
		fmt.Printf("Unable to set keepalive interval - %s", err)	
	}

上面的代码通过修改了tcp_keepalive_intvl的值为30秒,同时也修改了tcp_keepalive_time为30秒。原理可以看这里

总结

正如前文所述,TCP KEEPALIVE是一种保持长连接健康和存活的好办法,对于大多数情况默认的2小时11分钟去识别一个”死“连接都可以接受,某些场景则需要一些修改。对于这些场景通过net.TCPConn.SetKeepAlivePeriod()来进行修改是一个不错的选择。