这里立即申请

本文介绍了tcp长连接在实际工程中的实践过程,并总结了tcp连接保活遇到的挑战以及对应的解决方案。

作者:字节跳动终端技术 ——— 陈圣坤

概述

众所周知,作为传输层通信协议,TCP是面向连接设计的,所有请求之前需要先通过三次握手建立一个连接,请求结束后通过四次挥手关闭连接。通常我们使用TCP连接或者基于TCP连接之上的应用层协议例如HTTP 1.0等,都会为每次请求建立一次连接,请求结束即关闭连接。这样的好处是实现简单,不用维护连接状态。但对于大量请求的场景下,频繁创建、关闭连接可能会带来大量的开销。因此这种场景通常的做法是保持长连接,一次请求后连接不关闭,下次再对该端点发起的请求直接复用该连接,例如HTTP 1.1及HTTP 2.0都是这么做的。然而在工程实践中会发现,实现TCP长连接并不像想象的那么简单,本文总结了实现TCP长连接时遇到的挑战和解决方案。

事实上TCP协议本身并没有规定请求完成时要关闭连接,也就是说TCP本身就是长连接的,直到有一方主动关闭连接为止。实现TCP连接遇到的挑战主要有两个:连接池和连接保活。

连接池

长连接意味着连接是复用的,每次请求完连接不关闭,下次请求继续使用该连接。如果请求是串行的,那完全没有问题。但在并发场景下,所有请求都需要使用该连接,为了保证连接的状态正确,加锁不可避免,如果连接只有一个,就意味着所有请求都需要排队等待。因此长连接通常意味着连接池的存在:连接池中将保留一定数量的连接不关闭,有请求时从池中取出可用的连接,请求结束将连接返回池中。

用go实现一个简单的连接池(参考《Go语言实战》):

import (
"errors"
"io"
"sync"
) type Pool struct {
m sync.Mutex
resources chan io.Closer
closed bool
} func (p *Pool) Acquire() (io.Closer, error) {
r, ok := <-p.resources
if !ok {
return nil, errors.New("pool has been closed")
}
return r, nil
} func (p *Pool) Release(r io.Closer) {
p.m.Lock()
defer p.m.Unlock() if p.closed {
r.Close()
return
} select {
case p.resources <- r:
default:
// pool is full , just close
r.Close()
}
} func (p *Pool) Close() error {
p.m.Lock()
defer p.m.Unlock()
if p.closed {
return nil
} p.closed = true
close(p.resources)
for r := range p.resources {
if err := r.Close(); err != nil {
return err
}
} return nil
} func New(fn func() (io.Closer, error), size uint) (*Pool, error) {
if size <= 0 {
return nil, errors.New("size too small")
} res := make(chan io.Closer, size)
for i := 0; i < int(size); i++ {
c, err := fn()
if err != nil {
return nil, err
} res <- c
} return &Pool{
resources: res,
}, nil
}

池的对象只需实现io.Closer接口即可,利用go缓冲通道的特性可以轻松地实现连接池:获取连接时从通道中接收一个对象,释放连接时将该对象发送到连接池中。由于go的通道本身就是goroutine安全的,因此不需要额外加锁。Pool使用的锁是为了保证Release操作和Close操作的并发安全,防止连接池在关闭的同时再释放连接,造成预期外的错误。

连接池经常遇到的一个问题就是池大小的控制:过大的连接池会带来资源的浪费,同时对服务端也会带来连接压力;过小的连接池在高并发场景下会限制并发性能。通常的解决办法是延迟创建和设置空闲时间,延迟创建是指连接只在请求到来时才创建,空闲时间是指连接在一定时间内未被使用则将被主动关闭。这样日常情况下连接池控制在较小的尺度,当并发请求量较大时会为新的请求创建新的连接,这些连接在请求完毕后返还连接池,其中的大部分会在闲置一定时间后被主动关闭,这样就做到了并发性能和IO资源之间较好的平衡。

连接保活

长连接的第二个问题就是连接保活的问题。虽然TCP协议并没有限制一个连接可以保持多久,理论上只要不关闭连接,连接就一直存在。但事实上由于NAT等网络设备的存在,一个连接即使没有主动关闭,它也不会一直存活。

NAT

NAT(Network Address Translation)是一种被广泛应用的网络设备,直观地解释就是进行网络地址转换,通过一定策略对tcp包的源ip、源端口、目的ip和目的端口进行替换。可以说,NAT有效缓解了ipv4地址紧缺的问题,虽然理论上ipv4早已耗尽,但正由于NAT设备的存在,ipv4的寿命超出了所预计的时间。公司内部的网络也是通过NAT构建起来的。

虽然NAT有如此的优点,但它也带来了一些新的问题,对TCP长连接的影响就是其中之一。我们将一个通过NAT连接的网络简化成下面的模型: 

A如果想保持对B的长连接,它实际并不与B直接建立连接,而是与NAT A建立长连接,而NAT A又与NAT B、NAT B与B建立长连接。如果NAT设备任由下面的机器保持连接不关闭,那它很容易就耗尽所能支持的连接数,因此NAT设备会定时关闭一定时间内没有数据包的连接,并且它不会通知网络的双方。这就是为什么我们有时候会遇到这种错误:

error: read tcp4 1.1.1.1:8888->2.2.2.2:9999: i/o timeout

按照TCP的设计,连接有一方要关闭连接时会有“四次挥手”的过程,通过一个关闭的连接发送数据时会抛出Broken pipe的错误。但NAT关闭连接时并不通知连接双方,发送方不知道连接已关闭,会继续通过该连接发送数据,并且不会抛出Broken pipe的错误,而接收方也不知道连接已关闭,还会持续监听该连接。这样发送方请求能成功发送,但接收方无法接收到该请求,因此发送方自然也等不到接收方的响应,就会阻塞至接口超时。经过实践发现公司的NAT超时是一个小时,也就是保持连接不关闭并闲置一个小时后,再通过该连接发送请求时,就会出现上述timeout的错误。

我们上面提到连接池大小的控制问题,其实看起来有点类似NAT的超时控制,那既然我们允许连接池关闭超时的闲置连接,为什么不能接受NAT设备关闭呢?答案就是上面提到的,NAT设备关闭连接时并未通知连接双方,因此客户端使用连接请求时并不知道该连接实际上是否可用,而如果是由连接池主动关闭连接,那它自然知道连接是否是可用的。

Keepalive

通过上面的描述我们就知道怎么解决了,既然NAT会关闭一定时间内没有数据包的连接,那我们只需要让这个连接定时自动发送一个小数据包,就能保证连接不会被NAT自动关闭。

实际上TCP协议中就包含了一个keepalive机制:如果keepalive开关被打开,在一段时间(保活时间:tcp_keepalive_time) 内此连接不活跃,开启保活功能的一端会向对端发送一个保活探测报文。只要我们保证这个tcp_keepalive_time小于NAT的超时时间,这个探测报文的存在就能保证NAT设备不会关闭我们的连接。

unix系统为TCP开发封装的socket接口通常都有keepalive的相关设置,以go语言为例:

conn, _ := net.DialTCP("tcp4", nil, tcpAddr)

_ = conn.SetKeepAlive(true)

_ = conn.SetKeepAlivePeriod(5 * time.Minute)

另一个常见的保活机制是HTTP协议的keep-alive,不同于TCP协议,HTTP 1.0设计上默认是不支持长连接的,服务器响应完立即断开连接,通过请求头中的设置“connection: keep-alive”保持TCP连接不断开(HTTP 1.1以后默认开启)。

流水线控制

尽管使用连接池一定程度上能平衡好并发性能和io资源,但在高并发下性能还是不够理想,这是因为可能有上百个请求都在等同一个连接,每个请求都需要等待上一个请求返回后才能发出:

: 

这样无疑是低效的,我们不妨参考HTTP协议的流水线设计,也就是请求不必等待上一个请求返回才能发出,一个TCP长连接会按顺序连续发出一系列请求,等到请求发送成功后再统一按顺序接收所有的返回结果:

这样无疑能大大减少网络的等待时间,提高并发性能。随之而来的一个显而易见的问题是如何保证响应和请求的正确对应关系?通常有两种策略:

  1. 如果服务端是单线程/进程地处理每个连接,那服务端天然就是以请求的顺序依次响应的,客户端接收到的响应顺序和请求顺序是一致的,不需要特殊处理;
  2. 如果服务端是并发地处理每个连接上的所有请求(例如将请求入队列,然后并发地消费队列,经典的如redis),那就无法保证响应的顺序与请求顺序一致,这时就需要修改客户端与服务端的通信协议,在请求与响应的数据结构中带上独一无二的序号,通过匹配这个序号来确定响应和请求之间的映射关系;

HTTP 2.0实现了一个多路复用的机制,其实可以看成是这种流水线的优化,它的响应与请求的映射关系就是通过流ID来保证的。

总结

以上就是对TCP长连接实践中遇到的挑战和解决思路的总结,结合笔者在公司内部的实践经验分别探讨了连接池、连接保活和流水线控制等问题,梳理了实现TCP长连接经常遇到的问题,并提出了解决思路,在降低频繁创建连接的开销的同时尽可能地保证高并发下的性能。

参考


火山引擎 APMPlus 应用性能监控是火山引擎应用开发套件 MARS 下的性能监控产品。我们通过先进的数据采集与监控技术,为企业提供全链路的应用性能监控服务,助力企业提升异常问题排查与解决的效率。目前我们面向中小企业特别推出「APMPlus 应用性能监控企业助力行动」,为中小企业提供应用性能监控免费资源包。现在申请,有机会获得60天免费性能监控服务,最高可享6000万*条事件量。

点击这里,立即申请

TCP长连接实践与挑战的更多相关文章

  1. 网络编程-TCP长连接和短连接

    TCP是一个面向连接的协议.无论哪一方向另一方发送数据之前,都必须先在双方之间建立一条连接.下面会介绍一个TCP连接是如何建立的以及通信结束后是如何终止的. 一.TCP连接的建立与终止 1.1 建立连 ...

  2. Http 和TCP的关系,TCP长连接和短连接有什么区别?

    HTTP 协议即超文本传送协议(Hypertext Transfer Protocol ),是Web联网的基础,也是手机联网常用的协议之一,HTTP协议是建立在TCP协议之上的一种应用.由于HTTP在 ...

  3. TCP 长连接与短连接的区别

    TCP连接 当网络通信时采用TCP协议时,在真正的读写操作之前,server与client之间必须建立一个连接,当读写操作完成后,双方不再需要这个连接时它们可以释放这个连接,连接的建立是需要三次握手的 ...

  4. TCP长连接与短连接

    1.概念区别 所谓TCP短连接,是指通信双方有数据交互时,就建立一个TCP连接,数据发送完成后,则断开此TCP连接.也就是说TCP连接维持的时间比较短.一般银行网页数据交互都使用短连接.再比如说htt ...

  5. TCP长连接与短连接的区别

    http://www.cnblogs.com/liuyong/archive/2011/07/01/2095487.html 1. TCP连接 当网络通信时采用TCP协议时,在真正的读写操作之前,se ...

  6. 多进程解决datasnap支持的tcp长连接数量少的问题

    对于实时采集数据的项目,应用场景比如是这样的:5000客户端,每个客户端每隔500MS要给服务器上传一次数据. 大家知道,像INDY这种阻塞型的通信控件,所能支持的TCP长连接的一般地不能超过1000 ...

  7. TCP长连接与短连接的原理及区别

    一.当网络通信时采用TCP协议时:  1.过程: 第一步:(在真正的读写操作之前)Server 和Client 之间必须建立一个连接,连接的建立需要三次握手 经典的三次握手示意图: 第二步:进行读写操 ...

  8. [转载] TCP长连接与短连接的区别

    转载自http://www.cnblogs.com/liuyong/archive/2011/07/01/2095487.html 1. TCP连接 当网络通信时采用TCP协议时,在真正的读写操作之前 ...

  9. UNIX网络编程——TCP长连接与短连接的区别

    一.TCP短连接 我们模拟一下TCP短连接的情况,client向server发起连接请求,server接到请求,然后双方建立连接.client向server发送消息,server回应client,然后 ...

随机推荐

  1. 【九度OJ】题目1109:连通图 解题报告

    [九度OJ]题目1109:连通图 解题报告 标签(空格分隔): 九度OJ 原题地址:http://ac.jobdu.com/problem.php?pid=1109 题目描述: 给定一个无向图和其中的 ...

  2. 关于wlw连接wordpress的问题

    前几天搭建好wordpress博客网站后,一直想和博客园一样,使用wlw发布文章.无奈遇到了难题,一直没有办法解决. 今天我看到一篇博客,遇到问题和我类似:尝试连接到您的日志时出错:基础连接已经关闭: ...

  3. 前端性能和加载体验优化实践(附:PWA、离线包、内存优化、预渲染)

    一.背景:页面为何会卡? 1.1 等待时间长(性能) 项目本身包/第三方脚本比较大. JavaScript 执行阻塞页面加载. 图片体积大且多. 特别是对于首屏资源加载中的白屏时间,用户等待的时间就越 ...

  4. MySQL 中 count(*) 和 count(1)

    一张有 100W 条数据的表 CREATE TABLE `user` (  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,  `username` var ...

  5. 源码分析 SpringCloud 2020.0.4 版本 EurekaClient 的注册过程

    1. 概述 老话说的好:要善于思考,有创新意识. 言归正传,之前聊了 Springboot 的启动过程,今天来聊聊 Eureka Client 的注册过程. 2. Eureka Client 的注册过 ...

  6. [CNKI]个人论文收录

    [1]在校期间参加大创项目研究 以论文形式结题 发表时间:2018-03-25 基于VR虚拟现实技术的CBD微圈电商平台的研究 林旭; 陈丽娟 内江科技 2018-03-25 期刊 链接: 基于VR虚 ...

  7. 使用 DDL 语句分别创建仓库表、供应商表、产品表和入库表,并对其进行操作

    查看本章节 查看作业目录 需求说明: 使用 DDL 语句分别创建仓库表.供应商表.产品表和入库表 使用 DML 语句分别新增一条仓库表.供应商表.产品表和入库表记录 使用 DML 语句更新满足指定条件 ...

  8. VoIP语音处理流程和知识点梳理

    做音频软件开发10+年,包括语音通信.语音识别.音乐播放等,大部分时间在做语音通信.做语音通信中又大部分时间在做VoIP语音处理.语音通信是全双工的,既要把自己的语音发送出去让对方听到,又要接收对方的 ...

  9. .NET 云原生架构师训练营(模板方法 && 建造者)--学习笔记

    目录 模板方法 源码 建造者 模板方法 定义一个操作中的算法的骨架,而将一些步骤延迟到子类中,使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤 源码 https://github.com ...

  10. js- float类型相减 出现无限小数的问题

    6.3 -1.1 是不是应该等于5.2? 但是js 会导致得出 5.19999999999的结果 怎么办?可以先先乘100 后相减,然是用方法 舍入为最接近的整数,然后再除于100, Math.rou ...