第一章 绪论

开篇先引用一段《庖丁解牛》里的典故。梁惠王被庖丁解牛的技术所惊叹,于是就问庖丁。文惠君曰:“嘻,善哉!技盖至此乎?”意思是你的技术怎么会高明到这种程度呢?

庖丁曰:“始臣之解牛之时,所见无非牛者。三年之后,未尝见全牛也”。庖丁的回答意思是,我刚开始解牛的时候,对牛的结构还不了解,看见的无非就是整头的牛。但三年之后,我看见的再也不是整头的牛了,而是牛的内部的内部筋骨肌理,所以技术越来越精进!

开发技术和解牛技术道理是相通的。在你对底层工作原理不清楚的时候,你能看到的底层只是个整体。等到精进之后,你将能看到内核的内部筋骨肌理,各个模块是如何有机协作的。当你达到这个境界以后,你的技术能力也就变得更强!

1.1 我工作中遇到的困惑

有人说,学习网络就是在学习各种协议,这种说法其实误导了很多的人。

提到计算机网络的知识点,你肯定也首先想到的是 OSI 七层模型、IP、TCP、UDP、HTTP 等等。关于 TCP 再多一点你也会想到三次握手、四次挥手、滑动窗口、流量控制。关于 HTTP 协议就是报文格式、GET/POST、状态码、Cookie/Session 等等。现在市面上网络相关的书,课程也基本都是以协议为主。协议相关的内容确实很重要,但是我的这些知识却仍然不能帮我清除我在工作实践中遇到的一些疑问。

1.1.1 过多的 TIME_WAIT

有一次我们运维找过来,说某某几台线上机器上出现了 3 万多个 TIME_WAIT,说是不行了得赶紧处理。后来他帮我们打开了 tcp_tw_reusetcp_tw_recycle,先把问题处理掉了。

虽然问题算是临时处理了,但是我的思考却并没有停止,一个 TIME_WAIT 状态的连接到底会有哪些开销?是端口占用导致新连接无法建立?还是会过多消耗机器上的内存?3 万条 TIME_WAIT 究竟该算是 warning 还是 error?解决 TIME_WAIT 的更好的办法是啥?这些困惑激发了我强烈的好奇心。

1.1.2 长连接开销

另外一次是我们业务要进行性能优化,为了节约频繁的握手挥手开销,我们将访问 Mysql 和 Redis 等数据服务器时的短连接都改成了长连接。

那时我们公司还没有建立统一 redis 平台,是我们业务自己维护了一组 redis server。当开启长连接以后,一个 redis 实例上最终就出现了 6000 条的 TCP 连接。当时我的内心是有点小小的惶恐的,因为之前从来没试过这么高的并发数。虽然知道连接上大部分时间都是空闲的,但仍然担心这 6000 条即使是空闲的连接会不会把服务器搞坏?等上线以后观察一段时间发现没有太大问题才算是稍稍安心一些。

但到了 Mysql 上,就没那么顺利了。公司很早就提供了统一的 Mysql 平台。平台申请权限的时候需要为每一个 ip 填一个并发数,平台的负责同学来进行审批。因为我们当时使用的是 php-fpm,没有连接池。所以我们有多少个 fpm 进程,就得申请多大的并发数,我们当时申请了 200 个。然后工程部的同学就过来 PK 了:“你们这单机 200 个并发不行,太高了!”。

我告诉他虽然我们申请了这么高的并发,但其实绝大部分时候连接上都是空闲的。又给他看了我们长连接下的 redis 的服务器状态,最终勉强同意我们这么干。

在这个过程中,我发现了一个关键的问题,我当时其实是吃不准一条空闲的 tcp 连接到底有多大的开销的。我如果当时能把空闲 TCP 连接的 cpu、内存开销都理解到很透彻的话,就没有上面这么多的瞎担心了。

把这个问题再拓展拓展,就整理出另外几个问题。

  1. 一台服务器最多可以支撑多少条 TCP 连接? 我们假设所有的 TCP 连接都是空连接,那么一台服务器上最多可以支撑多少条 TCP 连接?你是否能有一个量化的估计?这个最大数字是受 CPU 配置的影响,还是受内存大小的限制?一台机器有可能支撑起 100 W 的并发长连接吗? 当理解了机器在极限情况下的表现,回头再看我们项目中的并发数你就不会再感到无畏的恐慌了。

  2. 一台客户端最多可以支撑多少条 TCP 连接? 因为客户端和服务器不一样的地方在于,每次建立 TCP 连接请求的时候都会消耗一个端口。而这个端口在 TCP 协议中又是一个 16 bit 的整数(0 - 65535)。那么是否意味着客户端单机最多只能建立起 65535 条连接?

  3. 一条 TCP 连接需要消耗多大的内存? 相对前面两个问题,这个问题更本质一些。对前面两个问题把握不深很大程度上是因为不理解 TCP 连接的网络开销。我们可以还假设这条 TCP 连接是空连接。只是进行了三次握手,并没有真正的数据产生。好,请问一条 TCP 连接需要吃掉多少内存,是几 KB,还是几十 KB,还是几 MB?

图1.1 CPU与负载监控

1.1.3 CPU 被消耗光了

还有另外一次是我的一个线上 CPU 消耗过高的问题。事发是我们的一组云控接口,是 nginx + lua 写的。正常情况下,单虚机 8 核 8 G 可以抗每秒 2000 左右的 QPS,负载一直都比较健康。

但是该服务近期开始偶发出现一些 500 状态的请求了,监控时不时会出现报警。通过 sar -u 查看峰值时 cpu 余量只剩下了 20-30 %。但是奇怪的是,负载竟然是比较正常的,当时监控系统展示如图 1.1。

后来经过两天的排查以后发现根本原因是在端口不充足的情况下,connect 系统调用的 CPU 消耗会大幅度增加。负载指的是就绪状态等待 CPU 调用的进程数量情况统计,而服务器上进程又不多,所以自然负载并不高。定位到问题,处理起来办法就多了。最后通过干掉了一段不重要的业务逻辑后解决了问题。

那为什么在端口不充足的情况下,connect 系统调用的 CPU 消耗会大幅度增加,其根本原因是啥?我又陷入了深深的思考。

1.1.5 为什么不同的语言网络性能差别巨大

上一小节我们提到我们的一个 nginx + lua 写的服务。单虚机 8 核 8 G 可以抗每秒 2000 左右的 QPS,负载还一直都比较健康。但是我们手底下其它的 php-fpm 的服务却远远到不了这个数,500 QPS 都算是比较好的情况了。

那问题来了,为什么使用不同的语言网络性能差别有这么大,这底层的根本原因是啥。所以我接下来深入去挖掘了同步阻塞网络 IO、去分析阻塞在内核中到底是一个什么样的操作。也深入去分析了 epoll 的工作原理,终于彻底搞懂了多路复用之所以高性能的根本原因,也终于理解了为啥 Redis 可以做到每秒处理几万的请求。

有了这些深度的理解,再看其它的语言里的网络模型例如 Java 的 NIO,Golang 的 net 包将更轻松。因为不同的语言,只是对内核提供的网络 IO 进行不同方式的封装而已,本质上都相差无几。

1.1.6 127.0.0.1 过网卡吗?

现在的互联网业务中,尤其是近期随着 sidecar 等模式的兴起,本机网络 IO 的应用也越来越广泛。那么问题来了,本机网络 IO 和跨机比起来,执行过程是怎么样的?数据需要经过网卡吗?性能有没有那么一点点的优势?有的话,那是节约了哪一部分的开销了呢?

还有网上有的文章说建议把本机的网络通信中指定的本机 IP 都换成 127.0.0.1,这样就能节约一些开销,从而提升性能。我就开始好奇,这个说法靠谱吗?如果说它靠谱,那到底是节约了哪些开销。

1.1.7 软中断和硬中断

在内核的网络模块中,有两个很重要的组件,是硬中断和软中断,软中断还分成了 NET_RX(R 指的是 Receive)和 NET_TX(T 是 Transmit 的缩写)等几大类。从字面意思上来看 RX 是接收,TX 是发送。但是即使是收发差不多的服务器上 NET_RX 也是比 NET_TX 要大的多的多,对应此我也是非常的好奇。

还有一次一位粉丝和我反馈说,他执行了一次测试,send 发送一个“Hello World”出去之后,其实 NET_TX 并没有增加。对于这个我更是感觉诧异了。

类似的疑惑还有。我们线上有一组服务的网络 IO 比较高,在单任务队列的机器,过多的软中断 si(top 命令里展示的软件中断 CPU 消耗占比)开销都打在一个核上了。所以我们决定开启多队列网卡优化。我们调研后发现,想要把软中断 si 开销分散到多个 CPU 核上,操作的却是硬中断号和 CPU 之间的绑定关系,这又是为啥?

在 Linux 上使用 top 等命令查看 CPU 开销的时候,展示结果中把总开销分成了 us、sy、hi、si 等几项。其中 us 指的是花在用户空间的 CPU 占比,sy 指的是内核空间占比,hi 是硬件中断消耗占比,si 是软件中断 CPU 占比。

1.1.7 DPDK

老的还没学完,又有很多新技术出来了。比如 DPDK 究竟是什么,是否需要学习和使用它。其实当你理解不了这个新技术的根本原因可能是你对 Linux kernel 工作原理不清楚。其实当你掌握了 Linux 内核的网络处理过程以后,回头再看 DPDK 这类 Kernel ByPass 的技术的时候,直接就理解个大概了。

零拷贝到底咋回事

很多性能优化方案里都会提到零拷贝。但是零拷贝到底是咋回事,是真的没有数据的内存拷贝了?究竟是避免了哪步到哪步的拷贝操作。如果不了解数据在网络包收发时在各个不同内核组件中的拷贝过程,对零拷贝根本理解不到本质上。

$ cat /proc/softirqs
                CPU0       CPU1       CPU2       CPU3
      HI:          0          0          0          0
   TIMER: 1670794607  218940516 3765758957 3937988107
  NET_TX:     384508     285972     244566     258230
  NET_RX: 1591545176 1212716226 1017620906 1058380340

这些问题都是飞哥工作中陆陆续续产生的,都是和实践相关。如果对于网络你只是学过协议,而不了解 Linux 内核的实现的话,对于这些问题其实是无能为力的。而且当我产生这些疑惑的时候,在网上进行了很多的搜索,但一直没能搜到能深入到根本原因的结论。索性我就撸起袖子,通过挖掘内核源码,做测试,自己在实现层面把计算机网络扒了个一遍。把这些问题彻底搞了明白。也就形成了本书的内容。

1.2 本书内容结构

第一章 绪论 分享了飞哥在这工作的十多年中遇到的一些线上问题,以及由此带来的困惑和疑问。

第二章 内核是如何接收网络包的 在这一章中,我们深入分析了 Linux 网络接收包的过程。在这里,你将看到网卡、RingBuffer、硬中断、软中断等组件是如何紧密配合的。也将了解到发送过程是如何消耗 CPU 的,也会深刻理解为什么网卡开启多队列能提升网络性能。

第三章 内核是如何与用户进程协作的 在这一章中,我们将分析阻塞到底干了什么,为什么同步阻塞的网络 IO 模型性能比较差。还有为什么 epoll 高效最深层次的原理。通过这章你也将能理解为什么 Redis 可以做到 10 W QPS 的高性能。

第四章 内核是如何发送网络包的 在这一章,我们会看到为什么软中断中 NET_RX 要比 NET_TX 高的多的多。也能理解内核在发送网络包的时候都涉及到哪些内存拷贝操作。理解了这个再来看 Kafka 里用到的零拷贝,就能很容易地理解了。还能理解到在查看发送网络包的 CPU 消耗的时候,应该是 sy(CPU 在内核空间的消耗占比)和 si(CPU 在软中断上消耗占比)同时都得看。

第五章 深度理解本机网络IO 现在本机网络 IO 用的也很多。那么本机 IO 过网卡吗?和外网网络通信相比,在内核收发流程上有啥差别?访问本机 Server 时,使用 127.0.0.1 能比使用本机 ip(例如 192.168.x.x)更快吗?这些问题你将在这一章得到清晰的理解。

第六章 深度理解 TCP 连接建立过程 实际上内核实现的三次握手过程涉及到了很多关键操作,如半/全连接队列的创建与长度限制、客户端端口的选择、半连接队列的添加与删除、全连接队列的添加与删除以及重传定时器的启动。通过这章你将能深入看到内核的这些底层工作。再遇到线上因三次握手而导致的问题的时候,相信你就能从容应对了。

第七章 一条 TCP 连接消耗多大内存 内核和我们应用程序一样,也是需要不停地申请和释放内存的。但和应用程序不同的是,内核使用一种叫做 SLAB 的方式来管理内存。在本章中,你将理解这种内存分配方式。并通过源码解析以及 slabtop 等工具看到一条 TCP 状态的空连接是如何消耗内存的,消耗是多大。

第八章 一台机器最多能支持多少条 TCP 连接 在到处都在谈论高并发的今天,弄清楚一台机器最多能支持多少条 TCP 连接这个问题非常的基础和重要。不仅仅是服务器端,在客户端最大能达到多少,如何突破 65535 的端口号的束缚创建更多连接都将在这章中进行讨论。我们还分析了一个实际需求,做一个支持一亿用户的长连接推送需要多少台机器。

第九章 网络性能优化建议 在这一章,我们将讨论一些网络开发时可用的优化手段。例如 RingBuffer 的扩容、多队列网卡的使用、配置充足的端口范围、使用零拷贝等等。我们还讨论了为什么 DPDK 等 kernel by pass 等新技术性能会很不错。

第十章 容器网络虚拟化 现在越来越多的公司在线上生产环境中不再是将服务部署到实体物理机或者是 KVM 虚拟机上,而是部署到基于 Docker 的容器云上。这就对我们技术同学提出了新的挑战,你需要理解容器网络工作原理。如果理解不到位,很有可能你将没有能力定位线上问题,也没有能力进行性能等方面的优化。在这一章我们深入分析容器网络中的核心技术点 veth、namespace、bridge 等技术。

欢迎大家加入我的知识星球,也欢迎加入我的技术交流群

1.3 一些约定

本书所使用的源码版本是 3.10,之所以采用这个版本是因为写作时我们公司线上 Linux 主要是基于 3.10 的。大家如果有需要可以到这个地址上下载,https://mirrors.edge.kernel.org/pub/linux/kernel/v3.x/。另外如果涉及到驱动,默认采用的都是 Intel 的 igb 网卡驱动。另外就是测试环境数据结果,如无特殊说明,也是在 3.10 的内核版本的服务器上做的。

B 和 b:B 代表的是一个 Byte,而 b 代表的是一个 bit。在本书中我们在内存开销上主要使用 B。

K 和 k:代表 1024 / 1000,这两个差别并不大,所以本书中有些地方是混着用了。

1.4 一些术语

在本书的内容中,我们会提到不少专业术语。在这里把一些关键术语都列出来,文中再提到的时候可能就提一下,不详细介绍了。

  • hi:CPU 开销中硬中断消耗的部分
  • si:CPU 开销中软中断消耗的部分
  • skb:skb 是 struct sk_buff 对象的简称。其中 struct sk_buff 是 linux 网络模块中的核心结构体,各个层用到的数据包都是存在这个结构体里的。
  • NAPI:Linux 2.5 以后的内核引入的一种高效网卡数据处理的技术,先用中断唤醒内核接收数据,后续采用 poll 轮询向网卡设备获取数据。通过减少中断次数来提高内核处理网卡数据的效率。
  • MSI / MSIx:msi 是 message signal interrupt 的缩写,是一种触发 CPU 中断的方式。

在知识星球中我们会进行内核等底层技术的视频讲解,能让你的底层学起来更快,事半功倍。还会进行线上问题排查以及性能优化等方面案例分享和交流。对大家技术深度和广度的积累很有好处。有想继续加入知识星球的同学微信扫描下面的二维码即可加入。另外在公众号后台发送「星球优惠券」可获取开发内功修炼读者的专属优惠券。