好书推荐
image.png image.png深入理解Linux网络: 修炼底层内功,掌握高性能原理
https://github.com/yanfeizhang/coder-kung-fu
目录
第1章 绪论 / 1
1.1 我在工作中的困惑 / 2
1.1.1 过多的TIME_WAIT / 2
1.1.2 长连接开销 / 2
1.1.3 CPU被消耗光了 / 3
1.1.4 为什么不同的语言网络性能差别巨大 / 4
1.1.5 访问127.0.0.1过网卡吗 / 4
1.1.6 软中断和硬中断 / 5
1.1.7 零拷贝到底是怎么回事 / 5
1.1.8 DPDK / 5
1.2 本书内容结构 / 6
1.3 一些约定 / 7
1.4 一些术语 / 8
第2章 内核是如何接收网络包的 / 9
2.1 相关实际问题 / 10
2.2 数据是如何从网卡到协议栈的 / 11
2.2.1 Linux网络收包总览 / 12
2.2.2 Linux启动 / 13
2.2.3 迎接数据的到来 / 23
2.2.4 收包小结 / 33
2.3 本章总结 / 34
第3章 内核是如何与用户进程协作的 / 41
3.1 相关实际问题 / 42
3.2 socket的直接创建 / 43
3.3 内核和用户进程协作之阻塞方式 / 46
3.3.1 等待接收消息 / 47
3.3.2 软中断模块 / 52
3.3.3 同步阻塞总结 / 57
3.4 内核和用户进程协作之epoll / 59
3.4.1 epoll内核对象的创建 / 60
3.4.2 为epoll添加socket / 62
3.4.3 epoll_wait之等待接收 / 68
3.4.4 数据来了 / 71
3.4.5 小结 / 79
3.5 本章总结 / 80
第4章 内核是如何发送网络包的 / 84
4.1 相关实际问题 / 85
4.2 网络包发送过程总览 / 86
4.3 网卡启动准备 / 90
4.4 数据从用户进程到网卡的详细过程 / 92
4.4.1 send系统调用实现 / 92
4.4.2 传输层处理 / 94
4.4.3 网络层发送处理 / 99
4.4.4 邻居子系统 / 103
4.4.5 网络设备子系统 / 105
4.4.6 软中断调度 / 109
4.4.7 igb网卡驱动发送 / 111
4.5 RingBuffer内存回收 / 114
4.6 本章总结 / 115
第5章 深度理解本机网络IO / 119
5.1 相关实际问题 / 120
5.2 跨机网络通信过程 / 120
5.2.1 跨机数据发送 / 120
5.2.2 跨机数据接收 / 125
5.2.3 跨机网络通信汇总 / 127
5.3 本机发送过程 / 127
5.3.1 网络层路由 / 127
5.3.2 本机IP路由 / 130
5.3.3 网络设备子系统 / 131
5.3.4 “驱动”程序 / 133
5.4 本机接收过程 / 135
5.5 本章总结 / 137
第6章 深度理解TCP连接建立过程 / 139
6.1 相关实际问题 / 140
6.2 深入理解listen / 141
6.2.1 listen系统调用 / 141
6.2.2 协议栈listen / 142
6.2.3 接收队列定义 / 143
6.2.4 接收队列申请和初始化 / 145
6.2.5 半连接队列长度计算 / 146
6.2.6 listen过程小结 / 148
6.3 深入理解connect / 148
6.3.1 connect调用链展开 / 149
6.3.2 选择可用端口 / 151
6.3.3 端口被使用过怎么办 / 153
6.3.4 发起syn请求 / 155
6.3.5 connect小结 / 156
6.4 完整TCP连接建立过程 / 157
6.4.1 客户端connect / 159
6.4.2 服务端响应SYN / 160
6.4.3 客户端响应SYNACK / 162
6.4.4 服务端响应ACK / 164
6.4.5 服务端accept / 167
6.4.6 连接建立过程总结 / 167
6.5 异常TCP连接建立情况 / 169
6.5.1 connect系统调用耗时失控 / 169
6.5.2 第一次握手丢包 / 171
6.5.3 第三次握手丢包 / 176
6.5.4 握手异常总结 / 178
6.6 如何查看是否有连接队列溢出发生 / 179
6.6.1 全连接队列溢出判断 / 179
6.6.2 半连接队列溢出判断 / 181
6.6.3 小结 / 183
6.7 本章总结 / 183
第7章 一条TCP连接消耗多大内存 / 187
7.1 相关实际问题 / 188
7.2 Linux内核如何管理内存 / 188
7.2.1 node划分 / 189
7.2.2 zone划分 / 191
7.2.3 基于伙伴系统管理空闲页面 / 192
7.2.4 slab分配器 / 194
7.2.5 小结 / 197
7.3 TCP连接相关内核对象 / 198
7.3.1 socket函数直接创建 / 198
7.3.2 服务端socket创建 / 206
7.4 实测TCP内核对象开销 / 207
7.4.1 实验准备 / 207
7.4.2 实验开始 / 208
7.4.3 观察ESTABLISH状态开销 / 209
7.4.4 观察非ESTABLISH状态开销 / 211
7.4.5 收发缓存区简单测试 / 214
7.4.6 实验结果小结 / 215
7.5 本章总结 / 216
第8章 一台机器最多能支持多少条TCP连接 / 218
8.1 相关实际问题 / 219
8.2 理解Linux最大文件描述符限制 / 219
8.2.1 找到源码入口 / 220
8.2.2 寻找进程级限制nofile和fs.nr_open / 221
8.2.3 寻找系统级限制fs.file-max / 223
8.2.4 小结 / 224
8.3 一台服务端机器最多可以支撑多少条TCP连接 / 225
8.3.1 一次关于服务端并发的聊天 / 225
8.3.2 服务器百万连接达成记 / 228
8.3.3 小结 / 232
8.4 一台客户端机器最多只能发起65 535条连接吗 / 232
8.4.1 65 535的束缚 / 232
8.4.2 多IP增加连接数 / 234
8.4.3 端口复用增加连接数 / 236
8.4.4 小结 / 243
8.5 单机百万并发连接的动手实验 / 243
8.5.1 方案一,多IP客户端发起百万连接 / 244
8.5.2 方案二,单IP客户端机器发起百万连接 / 248
8.5.3 最后多谈一点 / 250
8.6 本章总结 / 251
第9章 网络性能优化建议 / 253
9.1 网络请求优化 / 254
9.2 接收过程优化 / 256
9.3 发送过程优化 / 262
9.4 内核与进程协作优化 / 268
9.5 握手挥手过程优化 / 269
第10章 容器网络虚拟化 / 272
10.1 相关实际问题 / 273
10.2 veth设备对 / 274
10.2.1 veth如何使用 / 274
10.2.2 veth底层创建过程 / 276
10.2.3 veth网络通信过程 / 278
10.2.4 小结 / 281
10.3 网络命名空间 / 281
10.3.1 如何使用网络命名空间 / 282
10.3.2 命名空间相关的定义 / 284
10.3.3 网络命名空间的创建 / 287
10.3.4 网络收发如何使用网络命名空间 / 295
10.3.5 结论 / 296
10.4 虚拟交换机Bridge / 297
10.4.1 如何使用Bridge / 298
10.4.2 Bridge是如何创建出来的 / 301
10.4.3 添加设备 / 303
10.4.4 数据包处理过程 / 305
10.4.5 小结 / 308
10.5 外部网络通信 / 310
10.5.1 路由和NAT / 311
10.5.2 实现外部网络通信 / 313
10.5.3 小结 / 318
10.6 本章总结 / 319
前言/序言
从大厂的面试说起
互联网大厂是当今很多开发人员,尤其是应届毕业生们所向往的公司。但大家应该都听过关于大厂面试候选人的一句调侃的话,“面试造火箭,工作拧螺丝”。这虽然有一点儿夸张的成分,不过也确实描述得比较形象。在面试中,尤其是顶级互联网大厂的面试,对技术的考查往往都很深。但是到了工作中,可能确实又需要花不少时间在写各种各样的重复 CRUD 上。
那为啥会出现这种情况,是大厂闲得没事非得为难候选人吗?其实不是,这是因为扎实的底层功力确实对大厂来说很重要。
互联网大厂区别于小公司的一个业务特点就是海量请求,随便一个业界第二梯队的App,每天的后端接口请求数过亿很常见,更不用提微信、淘宝等头部应用了。在这种量级的用户请求下,业务能7×24小时稳定地提供服务就非常重要了。哪怕服务故障出现十分钟,对业务造成的损失可能都是不容小觑的。
所以在大厂中,你写出来的程序不是能跑起来就行了,是必须能够稳定运行。程序在运行期间可能会无法避免地遭遇各种线上问题。应用都是跑在硬件、操作系统之上的,因此线上的很多问题都和底层相关。如果遇到线上问题,你是否有能力快速排查和处理?例如有的时候线上访问超时是因为TCP的全连接队列满导致的。如果你对这类底层的知识了解得不够,则根本无法应对。
另外,大厂招聘高水平程序员的目的可能不仅仅是能快速处理问题,甚至希望程序员能在写代码之前就做出预判,从而避免出故障。不知道你所在的团队是否进行过Code Review(代码评审,简称CR)。往往新手程序员自我感觉良好、觉得写得还不错的代码给资深程序员看一眼就能发现很多上线后可能会出现的问题。
大厂在招人上是不怕花钱的,最怕的是业务不稳定和不可靠。如果以很低的价钱招来水平一般的程序员,结果导致业务三天两头出问题,给业务收入造成损失,那可就得不偿失了。所以,要想进大厂,扎实的内功是不可或缺的。
谈谈工作以后的成长
那是不是说已经工作了,或者已经进入大厂了,扎实的内功、能力就可有可无了呢?答案当然是否定的,工作以后内功也同样的重要!
拿后端开发岗来举例。初接触后端开发的朋友会觉得,这个方向太容易了。我刚接触后端开发的时候也有这种错觉。我刚毕业做Windows下的C++开发的时候,项目里的代码编译完生成的工程都是几个GB的,但是转到后端后发现,一个服务端接口可能100行代码就搞定了。
由于看上去的这种“简单性”,许多工作三年左右的后端开发人员会陷入一个成长瓶颈,手头的东西感觉已经特别熟练了,编程语言、框架、MySQL、Nginx、Redis都用得很溜,总感觉自己没有啥新东西可以学习了。
他们真的已经掌握了所有了吗?其实不然,当他们遇到一些线上的问题时,排查和定位手段又极其有限,很难承担得起线上问题紧急救火的重要责任。当程序性能出现瓶颈的时候,只是在网上搜几篇帖子,盲人摸象式地试一试,各个一知半解的内核参数调一调,对关键技术缺乏足够深的认知。
反观另外一些工作经验丰富的高级技术人员,他们一般对底层有着深刻的理解。当线上服务出现问题的时候,都能快速发现关键问题所在。就算是真的遇到了棘手的问题,他们也有能力潜入底层,比如内核源码,去找答案,看看底层到底是怎么干的,为啥会出现这种问题。
所以大厂不仅仅是在招聘时考察应聘者,在内部的晋升选拔中也同样注重考察开发人员对于底层的理解以及性能把控的能力。一个人的内功深浅,决定了他是否具备基本的问题排查以及性能调优能力。内功指的就是当年你曾经学过的操作系统、网络、硬件等知识。互联网的服务都是跑在这些基础设施之上的,只有你对它们有深刻的理解,才能够源源不断想到新的性能分析和调优办法。
所以说,扎实的内功并不是通过大厂面试以后就没有用了,而是会贯穿你整个职业生涯。
再聊聊中年焦虑
之前网络曾爆炒一篇标题为“互联网不需要中年人”的文章,疯狂渲染35岁码农的前程问题,制造焦虑。本来我觉得这件事情应该只是媒体博眼球的一个炒作行为而已,不过恰恰两三年前我们团队扩充,需要招聘一些级别高一点儿的开发人员,之后使我对此话题有了些其他想法。那段时间我面试了七十多人,其中有很多工作七八年以上的。
我面试的这些人里,有这么一部分人虽然已经工作了七八年以上,但是所有的经验都集中在手头的那点儿项目的业务逻辑上。对他们稍微深入问一点儿性能相关的问题都没有好的思路,技术能力并没有随着工作年限的增⻓而增⻓。换句话说,他们并不是有七八年经验,而是把两三年的经验用了七八年而已。
和这些人交流后,我发现共同的原因就是他们绝大部分的时间都是在处理各种各样的业务逻辑和bug,没有时间和精力去提升自己的底层技术能力,真遇到线上问题也没有耐心钻研下去,随便在网上搜几篇文章都试一试,哪个碰对了就算完事,或者干脆把故障抛给运维人员去解决,导致技术水平一直原地踏步,并没有随着工作年限而同步增长。我从那以后也确实认识到,码农圈里可能真的有中年焦虑存在。
那是不是说这种焦虑就真的无解了呢?答案肯定是“不是”。至少我面试过的这些人里还有一部分很优秀,不但业务经验丰富,而且技术能力出众,目前都发挥着重要作用。你也可以看看你们公司的高级别技术人员,甚至业界的各位技术大牛,相信他们会长期是你们公司甚至业界的中流砥柱。
那么工作了多年的这两类人中,差异如此巨大的原因是什么呢?我思考了很多,也和很多人都讨论过这个问题。最后得出的结论就是大牛们的技术积累是随着工作年限的增长而逐渐增长的,尤其是内功,和普通的开发人员相差巨大。
大牛们对底层的理解都相当深刻。深厚的内功知识又使得他们学习起新技术来非常快。举个例子,在初级开发人员眼里,可能Java的NIO和Golang的net包是两个完全不同的东西,所以学习起来需要分别花费不少精力。但在底层知识深厚的人眼里,它们两个只不过是对epoll的不同封装方式,就像只换了一身衣服,理解起来自然就轻松得多。
如此良性迭代下去,技术好的和普通的开发人员相比,整体技术水平差距越拉越大。普通开发人员越来越焦虑,甚至开始担心技术水平被刚毕业的年轻人赶超。
修炼内功的好处
内功,它不帮你掌握最新的开发语言,不教会你时髦的框架,也不会带你走进火热的人工智能,但是我相信它是你成为“大牛”的必经之路。我简单列一下修炼内功的好处。
1)更顺利地通过大厂的面试。大厂的面试对技术的考查比较底层,而网上的很多答案层次都还比较浅。拿三次握手举例,一般网上的答案只说到了初步的状态流转。其实三次握手中包含了非常多的关键技术点,比如全连接队列、半连接队列、防syn flood攻击、队列溢出丢包、超时重发等深层的知识。再拿epoll举例,如果你熟悉它的内部实现方式,理解它的红黑树和就绪队列,就知道它高性能的根本原因是让进程大部分时间都在处理用户工作,而不是频繁地切换上下文。如果你的内功能深入触达这些底层原理,一定会为你的面试加分不少。
2)为性能优化提供充足的“弹药”。目前大公司内部对于高级和高级以上工程师晋升时考核的重要指标之一就是性能优化。在对内核缺乏认识的时候,大家的优化方式一般都是盲人摸象式的,手段非常有限,做法很片面。当你对网络整体收发包的过程理解了以后,对网络在CPU、内存等方面的开销的理解将会很深刻。这会对你分析项目中的性能瓶颈所在提供极大的帮助,从而为你的项目性能优化提供充足的“弹药”。
3)内功方面的技术生命周期长。Linux 操作系统 1991 年就发布了,现在还是发展得如火如荼。对于作者 Linus,我觉得他也有年龄焦虑,但他可能焦虑的是找不到接班人。反观应用层的一些技术,尤其是很多的框架,生命周期能超过十年我就已经觉得它很牛了。如果你的精力全部押宝在这些生命周期很短的技术上,你说能不焦虑吗!所以我觉得戒掉浮躁,踏踏实实练好内功是你对抗焦虑的解药之一。
4)内功深厚的人理解新技术非常快。不用说业界的各位“大牛”了,就拿我自己来举两个小例子。我其实没怎么翻过Kafka的源码,但是当我研究完了内核是如何读取文件的、内核处理网络包的整体过程后,就“秒懂”了Kafka在网络这块为啥性能表现很突出了。还有,当我理解了epoll的内部实现以后,回头再看Golang的net包,才切切实实看懂了绝顶精妙的对网络IO的封装。所以你真的弄懂了Linux内核的话,再看应用层的各种新技术就犹如戴了透视镜一般,直接看到骨骼。
5)内核提供了优秀系统设计的实例。Linux作为一个经过千锤百炼的系统,其中蕴含了大量的世界顶级的设计和实现方案。平时我们在自己的业务开发中,在编码之前也需要先进行设计。比如我在刚工作的时候负责数据采集任务调度,其中的实现就部分参考了操作系统进程调度方案。再比如,如何在管理海量连接的情况下仍然能高效发现某一条连接上的IO事件,epoll内部的“红黑树 + 队列”组合可以给你提供一个很好的参考。这种例子还有很多很多。总之,如果能将 Linux的某些优秀实现搬到你的系统中,会极大提升你的项目的实现水平。
时髦的东西终究会过时,但扎实的内功将会伴随你一生。只有具备了深厚的内功底蕴,你才能在发展的道路上走得更稳、走得更远。
为什么要写这本书
平时大家都是用各种语言进行业务逻辑的代码编写,无论你用的是PHP、Go,还是Java,都属于应用层的范畴。但是应用层是建立在物理层和内核层之上的。我把在应用层的技术能力称为外功,把 Linux 内核、设备物理结构方面的技术能力称为内功。前面已经说了,无论是在职业生涯的哪个阶段,扎实的内功都很重要。
那好,既然内功如此重要,那就找一些底层相关的资料加强学习就行了。但很遗憾,我觉得目前市面上的技术资料在内功方向上存在一些不足。
先说网上的技术文章。目前网上的技术文章、博客非常多。大家遇到问题往往先去搜一下。但是你有没有发现,网上入门级资料一搜一大把,而内功深厚、能深入底层原理的文章却十分匮乏。
比如,现在的互联网应用大部分都是通过TCP连接来工作的,那么一台机器最多能撑多少个TCP 连接?按道理说,整个业界都在讲高并发,这应该算是很入门的一个问题了。但当年我产生这个疑问的时候,在搜索引擎上搜了个遍也没找到令我满意的答案。后来我干脆自己动手,花了一个多月时间边做测试,边扒内核源码,才算是把问题彻底搞明白了。
再比如,大部分的开发人员都搞过网络相关的开发。那么一个网络包是如何从网卡到达你的进程的?这个问题表面上看起来简单,但实际上很多性能优化方案都和这个接收过程有关,能不能深度理解这个过程决定了你在网络性能上有多少优化措施可用。例如多队列网卡的优化方案是在硬中断这一步开始将工作分散在多个 CPU 核上,进而提升性能的。我几年前想把这个问题彻底搞清楚,几乎搜遍了互联网,翻遍了各种经典书都没能找到想要的答案。
还比如,网上搜到的三次握手的技术文章都是在说一些简单的内容,客户端如何发起 SYN 握手进入 SYN_SENT 状态,服务端响应SYN并回复 SYNACK,然后进入 SYN_RECV……诸如此类。但实际上,三次握手的过程执行了很多内核操作,比如客户端端口选择、重传定时器启动、半连接队列的添加和删除、全连接队列的添加和删除。线上的很多问题都是因为三次握手中的某一个环节出问题导致的,能否深度理解这个过程直接决定你是否有在线上快速消灭或者避免此类问题的能力。网上能深入介绍三次握手的文章太少了。
你可能会说,网上的文章不足够好,不是还有好多经典书吗?首先我得说,计算机类的一些经典的书确实很不错,值得你去看,但是这里面存在几个问题。
一是底层的书都写得比较深奥难懂,你看起来需要花费大量的时间。假如你已经工作了,很难有这么大块的时间去啃。比如我刚开始深入探寻网络实现的时候,买来了《深入理解Linux内核》《深入理解Linux网络技术内幕》等几本书,利用工作之余断断续续花了将近一年时间才算理解了一个大概。
另外一个问题就是当你真正在工作中遇到一些困惑的时候,会发现很难有一本经典书能直接给你答案。比如在《深入理解 Linux 网络技术内幕》这本书里介绍了内核中各个组件,如网卡设备、邻居子系统、路由等,把相关源码都讲了一遍。但是看完之后我还是不清楚一个包到底是如何从网卡到应用程序的,一台服务器到底能支持多少个TCP连接。
还有个问题就是计算机技术不同于其他学科,除理论外对实践也有比较高的要求。如果只是停留在经典书里的理论阶段,实际上很多问题根本就不能理解到位。这些书往往又缺乏和实际工作相关的动手实验。比如对于一台服务器到底能支持多少个TCP连接这个问题,我自己就是在做了很多次的实验以后才算比较清晰地理解了。还有就是如果没有真正动过手,那你将来对线上的性能优化也就无从谈起了。
总的来说,看这些经典书不失为一个办法,但考量时间的花费和对工作问题的精准处理,我感觉效率比较低。所以鉴于此,我决定输出一些内容,也就有了这本书的问世。
创作思路
虽然底层的知识如此重要,但这类知识有个共同的特点就是很枯燥。那如何才能把枯燥的底层讲好呢?这个问题我思考过很多很多次。
2012年我在腾讯工作期间,在内部KM技术论坛上发表过一篇文章,叫作《Linux文件系统十问》(这篇文章现在在外网还能搜到,因为被搬运了很多次)。当时写作的背景是“老大”分配给我一个任务,把所有合作方提供的数据里的图片文件都下载并保存起来。我把在工作中产生的几个疑问进行了追根溯源,找到答案以后写成文章发表了出来。比如文件名到底存在了什么地方,一个空文件到底占不占用磁盘空间,Linux目录下子目录太多会有什么问题,等等。这篇文章发表出来以后,竟然在全腾讯公司内部传播开了,反响很大,最后成为了腾讯KM当年的年度热文。
为什么我的一篇简单的Linux文件系统的文章能得到这么强烈的回响?后来我在罗辑思维的一期节目里找到了答案。节目中说最好的学习方式就是你自己要产生一些问题,带着这些问题去知识的海洋里寻找答案,当答案找到的时候,也就是你真正掌握了这些知识的时候。经过这个过程掌握的知识是最深刻的,和你自身的融合程度也是最高的,能完全内化到你的能力体系中。
换到读者的角度来考虑也是一样的。其实读者并不是对底层知识感兴趣,而是对解决工作中的实际问题兴趣很大。这篇文章其实并不是在讲文件系统,而是在讲开发过程中可能会遇到的问题。我只是把文件系统知识当成工具,用它来解决掉这些实际问题而已。
所以我在本书的创作过程中,一直贯穿的是这个思路:以和工作相关的实际的问题为核心。
在每一章中,我并不会一开始就给你灌输软中断、epoll、socket 内核对象等内核网络模块的知识,我也觉得这些很乏味,而是每章先抛出几个和开发工作相关的实际问题,然后围绕这几个问题展开探寻。是的,我用的词不是“学习”,而是“探寻”。和学习相比,探寻更强调对要解惑的问题的好奇心,更有意思。
虽然本书中会涉及很多的源码,但这里先强调一下,这并不是一本源码解析的书。大家学习的真正目的是理解和解决项目实践相关的问题,进而提高驾驭手头工作的能力,而源码只是我们达成目的的工具和途径而已。
致谢
本书能够得以问世,要感谢许多许多人。
首先要感谢的是我的微信公众号和知乎专栏里的粉丝们。我提笔写下第一篇文章的时候,是根本没敢想能够成体系出一本书的,是你们的认可和鼓励支持着我输出一篇又一篇的硬核技术文。现在回头一看,竟然攒了好几十篇。基于这些文章,将来再整理出一本书都是有可能的。而且很多读者技术也非常优秀,指出了我的文章中不少的瑕疵。飞哥在此对大家表示感谢!
接下来要感谢的是我的爱人,在我写作的过程中给了我很大的支持和鼓励,还帮我分担了很多遛娃、看娃的任务,让我能专心地投入到写作中来。写作要投入的精力是巨大的,如果缺少家人的支持,想完成一本书基本是不可能的。
最后要感谢的是道然科技姚老师以及电子工业出版社的老师们,是你们帮我完成出书过程最后的“临门一脚”。
网友评论