聊聊服务间的网络通信 - TCP 与 HTTP
前言
阅读前你可能需要了解这些:
- 了解 TCP/IP、OSI 模型
- 了解 HTTP 协议
- 了解 Node.js
从几个问题入手:
- 服务间调用的长连接如何设置
- 服务器上 TCP 连接数限制
- 服务器上 TCP 连接数对业务的影响
服务间的长连接
假设我们的目标服务,存在服务的消费者和提供者,服务之间存在上下游依赖关系:
我们期望服务间的连接是长连接,即 TCP 连接只建立一次,无需每次请求调用都发起 3 次握手、4 次挥手,以提升网络 IO 吞吐量。但是事实跟期望可能有所出入。
假设微服务间通信使用的应用层协议是 HTTP 1.1,单个 TCP 连接同时只能发出单个 HTTP 请求。即当同一时间请求并发数为 n ,会存在 n 个 TCP 连接,并且会存在 3 * n + 4 * n 次握手挥手动作,甚至可能会触发 sockets 连接数用满。
长连接示例
我们通过一个示例,感受并发调用场景下,TCP 建连的过程。
以下为代码启动一个 HTTP server 作为上图中的 Provider Service:
- 建立 TCP 连接时,打印 new connection 日志
- 收到 HTTP 请求时,返回 ok 作为 response body,并打印 request 日志
const http = require('http');
var server = http.createServer(function (req, res) {
res.end('ok');
console.log('request');
});
server.on('connection', function (socket) {
console.log('new connection');
});
server.listen(3000);
以下代码为客户端代码作为 Target Server(由于我们想要测试长连接,双方交互的服务一定是长期存活的,所以这里我们启动一个服务,而不是直接写个 client.js 做测试):
- 服务端口监听在 3001
- 当收到
/batch
请求时,并发调用 Provider Service 10 次
const http = require('http');
var server = http.createServer(async function (req, res) {
if (req.url === '/batch') {
await Promise.all(Array(10).fill(1).map(request));
}
res.end('ok');
});
server.listen(3001);
async function request() {
return new Promise((resolve) => {
http
.request('http://127.0.0.1:3000', (res) => {
res.on('data', resolve);
})
.end();
});
}
以 curl 作为客户端(或者作为 Consumer Service)
$ curl http://127.0.0.1:3001/batch
Provider Service 输出如下日志:
整理下完整的调用链为:curl -> Target Service -> Provider Service。
可见 10 次 HTTP 并发调用产生了 10 次 TCP 连接,符合预期,因为 HTTP 1.1 并发调用一定会产生相对应数量的 TCP 连接。
再次 curl ,Target Service 与 Provider Service 之间继续新建 10 条 TCP 连接,原因也很简单,之前的 TCP 连接都是用完即销毁的。
假设我们想要第二次并发的 10 次请求,继续复用之前的 10 个 TCP 连接就需要做如下处理,代码变更如下:
连续手动操作进行 3 次 curl 调用:
对输出做一下分析:
- 首次 curl 调用,建立 10 次 TCP 连接,符合预期
- 二次 curl 调用,复用原有的 TCP 连接,符合预期
- 三次 curl 调用,又建连了 10 次 TCP 连接,不符合预期
大家可能对第三次调用结果比较疑惑,这里直接放下结论:因为 TCP 连接只存活 5s ,超时后,自动断连了。
Wireshark 网络分析
为了对如上的调用做解释,我们需要一个工具去查看 TCP、HTTP 的完整过程,这里我们用到一个工具: Wireshark。
Wireshark 是一个强大的网络分析工具,它工作于 OSI 网络模型的 Data Link Layer
层,即数据链路层,所以可以分析 Data Link Layer
以上的所有层数据,包括本次分析的 TCP、HTTP 过程。
Wireshark 相对于一些其他常用的网络分析工具,例如 Fiddler、Charles、Whistle 等工具,其有如下优势:
- 实现机制更底层,所以能捕获
Data Link Layer
上层的数据,而其他代理工具只能看应用层数据,顶多再看个传输层数据 - 由于更底层,所以无需配置应用的代理配置(部分应用可能不走默认系统代理,需要手动配置,例如你启动的 Node.js 服务)
话不多数,关于 Wireshark 的使用,有兴趣直接看官网文档吧: https://www.wireshark.org/docs/wsug_html_chunked/
对单条请求做分析
为了减少干扰,我们仅发出一条请求 Target Server -> Provider Service。
关于图例说明下:
- 绿底的输入框,由于网卡比较活跃,减少干扰过滤出 Provider Service 3000 端口号的网络交互
- 图中我们可以很直观的看到熟悉的三次握手、HTTP 请求、四次挥手
- Keep Alive Check:我们还发现每隔 1s Target Service 于 Provider Service 都会进行一次双向交互,这是为了:
- 检查死连接,及时断连
- 防止长时间无网络交互,导致断连
- 具体参考:https://tldp.org/HOWTO/TCP-Keepalive-HOWTO/overview.html
对图例的细节进行分析:
- 存在四次挥手,而且是在大概 5s 后,这个我们从 HTTP response 中得到验证
对 curl 三次结果做分析
从上述单个请求分析中,我们基本可以论证 curl
手动触发第三次不符合预期的原因,重复说明一下原因:因为 TCP 连接只存在 5s ,超时后,自动断连了。
我们再次重复 3 次手动 curl
:
- 触发第一次
- 1s 后触发第二次
- 8s 后触发第三次(此时之前的 TCP 长连接已断连,需要重新连接)
具体操作如下,到这一步已经非常清晰了:
如何操作长连接
回到问题,这里列一些解法:
如何配置长连接,以及超时时长
- 对于客户端,上述 Demo 已经很明确了,Node.js 上直接设置 http agent 即可。
- 对于服务端,可以调整 keepalive timeout 增长 TCP 连接的时长,可以设置
Server.keepAliveTimeout
属性,但是也要注意其可能频繁TCP Keep-Alive Check
,需要做好取舍,多次测试找到合适的阈值
Demo 里 10 次批量的请求,在 TCP 连接还没销毁前,二次并发调用时会重用,那么这个最大重用限制多少?
与客户端配置 Agent.maxFreeSockets
相关:
- 默认 256,即连接池的最大默认空闲容量,当下次请求来时会优先复用
- 当超过时,客户端在 http 结束时会立即发起断连
并发数过大时,TCP 连接数会建很多么,是否有限制?
与客户端配置 Agent.maxSockets
和 Agent.maxTotalSockets
相关:
- 前者限制单 host、后者针对所有 host
Agent.maxSockets
Node 0.12 以上就是不限制了- 设置此值的效果为:超出的数量的 HTTP 请求不会发出,直到 TCP 空闲
- 例如设置为 1,则所有请求都会是串行的效果,TCP 连接也仅仅存在一个
- 具体示例如下图,No.23 为第二个请求,在 No.19 第一个请求完全结束后才发出
TCP 连接数限制
通过 {Source IP, Source Port, Destination IP, Destination Port} 四元组确定唯一的 TCP 连接。
对于服务提供方:只需要一个暴露一个端口给客户端,即可接收无限数量的 TCP 连接,在不考虑内存的前提下,客户端的 IP, Port 只要不同即可。
对于客户端:连接数量限制在 2^16 - 1 内,即 65535 个端口,去掉 0 这个特殊端口。
客户端存在限制的核心原因:TCP 规范的要求。
- 端口号只能是 16 bits 内,如果超出可能会导致对方服务无法解析或解析错误
- 以上为 Wireshark 的示例,整个 TCP header 都是固定顺序与固定格式的
作为客户端 65535 个数量是否够用
个人电脑当然够用的,假设每个程序 100 个 TCP 连接,同时运行 100 个程序,也才 10000 个罢了。
微服务中的一台服务:也是够用的
- 假设你的服务都是短连接,每次客户端请求过来都要转发给相对应数量的上游其他服务,并且假设每个请求你都需要处理 5s
- 那么 5s 你能接受的最大单机请求数是 6w+ 个,基本单个服务是达不到这个数量的。除非你接收一个请求,分散出 10+ 的请求。况且存在这么高的并发时,内存和 CPU 可能更先刚不住,而不需要先担心 TCP 的数量是否够用
HTTP 1.1 与 2
相比之下,HTTP 2 带来了如下特性:
- 二进制,而不是文本
- 完全多路复用,而不是有序和阻塞,故可以使用一个连接进行并行
- 使用 Header 压缩来减少开销
- 允许服务器主动将响应“推送”到客户端缓存中
具体参考:https://http2.github.io/faq/
那么,我们是不是可以把服务间的通信协议升级到 HTTP 2 来解决并发流量导致的重复 TCP 建连开销?
立即开干,以下是 Provider Server:
const http2 = require('http2');
const fs = require('fs');
const server = http2.createSecureServer(
{
key: fs.readFileSync('localhost-privkey.pem'),
cert: fs.readFileSync('localhost-cert.pem'),
},
function (req, res) {
res.end('ok');
console.log('request');
},
);
server.on('connection', function (socket) {
console.log('new connection');
});
server.listen(3000);
我们创建了一个基于 TLS 的 HTTP 2,说明下为啥不使用 HTTP2 over TCP(即不加密的 HTTP 2):
- 浏览器等客户端无法识别
- Wireshark 无法识别(重点,不方便看明细)
以下是 Target Server。
const http2 = require('http2');
const http = require('http');
var server = http.createServer(async function (req, res) {
if (req.url === '/batch') {
await Promise.all(Array(10).fill(1).map(request));
}
res.end('ok');
});
server.listen(3001);
const client = http2.connect('https://localhost:3000');
async function request() {
return new Promise((resolve) => {
const req = client.request({ ':path': '/', ':method': 'GET' });
req.on('data', () => {});
req.on('end', resolve);
req.end();
});
}
curl http://localhost:3001/batch -v
进行测试结果:
- TCP 连接只在 Target Server 启动时即建连,且不主动销毁
- 批量 10 次请求,复用现有单个 TCP 连接,结果符合预期
注意事项
如果你想要按照上面的示例进行测试,有一些 TLS 带来的调试问题注意事项:
- Provider Server:需要自行生成证书,参考:https://nodejs.org/api/http2.html#server-side-example
- Provider Server:增加 Node.js 启动参数
node --tls-keylog=/somewhere/ssllogfile.txt provider-server.js
,用于 Wireshark - Target Server:增加环境变量
NODE_TLS_REJECT_UNAUTHORIZED=0 node target-server.js
解决自建证书的安全问题 - Wireshark:配置日志文件,用于解析 TLS 层数据包
总结
文章主要探讨了 TCP 长连接的相关知识。首先通过示例解释了长连接的基本原理和流程,包括 TCP 连接、HTTP 请求、Keep Alive 检查等。然后分析了在手动 curl 触发第三次请求时的问题,说明了因为 TCP 连接只存在5秒,超时后会自动断连。
接着,给出了如何配置长连接的解决方案,包括客户端和服务端的设置,以及超时时长的调整。同时还讲解了一些与客户端配置相关的参数,如 Agent.maxFreeSockets、Agent.maxSockets 和 Agent.maxTotalSockets 等,并解释了 TCP 连接数的限制和客户端存在限制的原因。
最后,我们探讨了 HTTP 2 对于微服务架构的可用性,我认为是可以实践的,不过要去掉 TLS ,走 HTTP2 over TCP。
完,希望对大家有所帮助。
参考
- https://zh.wikipedia.org/wiki/OSI模型
- https://www.wireshark.org/docs/wsug_html_chunked/
- https://tldp.org/HOWTO/TCP-Keepalive-HOWTO/overview.html
- https://www.geeksforgeeks.org/layers-of-osi-model/
- https://www.youtube.com/watch?v=o-EkdZW4zbA
- https://en.wikipedia.org/wiki/Transmission_Control_Protocol
- https://wiki.wireshark.org/TLS
- https://gist.github.com/dfrankland/0fec2cd565f1f7b78fb0e3ededf36b89
- https://http2.github.io/faq/
- https://nodejs.org/api/http2.html#server-side-example