[{"content":"本篇论文是 2016 年 CMU 发表的一篇介绍 NewSQL 发展的综述论文，主要对新兴 NewSQL 数据库的起源、分类以及主要技术原理做了介绍，其中对数据库发展历史、NewSQL 实现原理的介绍非常值得一读。\n数据库发展简史 论文首先简要介绍了数据库的发展历史，我觉得这可能是论文最重要的一部分。通过了解历史我们可以深刻明白任何技术都不是凭空出现，而是在最初起源基础之上，根据现实条件和需求不断迭代出来的。\n1. 1960 年代 ~ 1970 年代：数据库的诞生\n早在 1960 年代，IBM 为了支持阿波罗计划，开发了 IMS 来存储数据，引入了代码与数据分离的思想，让开发者可以专注于操作数据，无需关心这些操作的底层实现细节。\n在之后的 1970 年代，作为关系型数据库的先驱，IBM 的 System R 和加州大学的 INGRES 数据库诞生，后者被其他大学广泛使用，并在之后被商业化。与此同时，Oracle 发布了其第一版的 DBMS。\n2. 1980-1990 年代：创新与开源\n1980 年代，IBM 发布了 DB2 数据库，与此同时还有 Sybase、Informix 等商业产品进入市场，推动着关系型数据库的普及。\n在 1980 年代末、1990 年代初，有一股面向对象数据库设计的浪潮，旨在克服关系型数据库和面向对象编程语言之间的不匹配。虽然此类数据库没有成为主流，但在此过程中的许多技术创新为后来 XML 数据存储、对象存储以及 NoSQL 文档数据库的设计奠定了基础。\n到了 1990 年代，开源数据库项目兴起，当前流行的 MySQL 和 PostgreSQL 数据库均在此期间诞生。\n3. 2000 年代：互联网推动数据库革新\n互联网迅猛发展，互联网应用对高并发和高可用的需求，让传统数据库成为瓶颈。虽然可以通过垂直扩展来解决，但这种方法存在局限性，并且从低配机器向高配机器迁移数据的成本也非常高。\n为了克服这些限制，Google、eBay 等公司开始采用 middleware 中间件的方式，将多台机器上的单点数据库组合起来，通过 middleware 做代理，实现跨机器的操作，但这种方式对复杂的查询和事务支持有限，有时候开发者需要自己来维护数据处理逻辑。像 eBay 的 middleware 组件就要求开发者自己实现事务和复杂查询。\n总的来看，现阶段的关系型数据库面临三个问题：\n关系型数据库的核心是 ACID，其关注重点在事务一致性和数据的正确性，而这是以可用性和性能为代价的，与互联网应用需要面对的高并发、高可用、高性能的需求不匹配。 与互联网应用一起而来的还有海量的数据，使用像 MySQL 这样的数据库存储海量数据是非常不明智的选择。 关系型数据库的数据建模和互联网应用所需的数据模型往往并不匹配，有时候需要更灵活、适配的建模方案。 以上问题最终催生了 NoSQL 数据库的诞生。\n4. NoSQL 的崛起\nNoSQL 数据库最大的特点就是放弃了对 ACID 强一致性的支持，转而追求最终一致性。数据模型也更加的灵活，比如可以是键值对、文档模型或者图数据库等。NoSQL 指的是 No only SQL，是对非关系型数据的数据存储服务的统称，包括键值存储、文档存储、列存储、图数据库等。\nGoogle的 BigTable、Amazon 的 Dynamo，以及后来的 Cassandra、MongoDB、ElasticSearch 、Redis 等开源产品都可以归类为 NoSQL 数据库。这类数据库通常都是以分布式集群的形式存在，天然支持数据的分片、多副本存储。\n5. NoSQL 的局限与 NewSQL 的诞生\nNoSQL 数据库虽然解决了传统关系型数据库的很多问题，但同时也带来了新的问题。许多企业应用（如金融系统）要求必须做到强一致性，同时又需要 NoSQL 所带来的高性能、高可用、可扩展性。为此，结合了传统数据库的强一致性和 NoSQL 的高性能、高可用、可扩展特性的数据库应运而生，此类数据库被称为 NewSQL 数据库。\nNewSQL 的定义与分类 NewSQL 的定义 论文中对 NewSQL 的定义如下：\nThey are a class of modern relational DBMSs that seek to provide the same scalable performance of NoSQL for OLTP read-write workloads while still maintaining ACID guarantees for transactions.\n它们是一类现代关系型 DBMS，旨在为 OLTP 读写工作负载提供与NoSQL相同的可扩展性能，同时仍然为事务保持ACID保证。\n简单来书就是，NewSQL 既有传统关系型数据库的 ACID 保证，又兼具了 NoSQL 的高可用、高性能、可扩展等特性。\nNewSQL 针对的是 OLTP 类型的数据处理，强调的是对读写事务操作的支持。\n还有一种更狭义的定义：\na lock-free concurrency control scheme and a shared-nothing distributed architecture [57] NewSQL 的分类 论文将 NewSQL 分为三类：\n从 0 开发的新数据库 透明分片中间件 DBaaS New Architecture 采用新的架构设计，从零开发的新数据库，该类数据库基本都采用了分布式架构，支持跨节点的并发控制和基于副本的容错处理。\n大多数情况下，这类数据库不会依赖现成的存储系统或存储引擎，比如 HDFS、Apache Ignite，而是自己实现数据的分布式存储。这样做的好处在于可以让数据库 send the query to the data rather than bring the data to the query，在处理数据量巨大的场景时，可以极大的减少网络流量，提高吞吐性能。\n通过自己管理数据存储，数据库还可以实现更加复杂、灵活的副本机制，比如 Aurora 实现的 3 可用区 6 副本的存储形式，这是那些采用现成存储方案的数据库做不到的（比如基于 HBase 的 Splice Machine，基于 Hadoop 的 Trafodion）。\n这类采用新架构的 NewSQL 数据库的问题在于，因为是新的产品，使用的人数较少，相关的支持更具也比较缺乏，如果遇到问题可能不容易解决；同时新的数据库如果没有长久的支持，产品可能会中途夭折，导致在推广时阻力也会较大，因此很多数据库均采用了兼容已有数据库的方式，比如 MemSQL 等数据库均兼容 MySQL。\n像 Google 的 Spanner 还有 VoltDB、MemSQL、H-Store 等数据库均属于此类 NewSQL。\nTransparent Sharding Middleware 另一种形式的 NewSQL 与之前的 middleware 处理类似，各个 DBMS 各自独立的部署在多个节点，然后通过实现一个中心化的 middleware 组件来管理数据的存储、查询、副本、事务处理等操作。通常在每个数据节点，还会有一个 shim 程序作为代理与 DBMS 交互，执行具体的查询返回等操作。整个架构和现在的 ServiceMesh 有些类似，通过 middleware、ship 组件的配合，整个集群作为一个逻辑上的数据库对外提供服务。\n该种类型的 NewSQL 数据库的最大优势在于可以直接替代当前的数据库且不需要应用做任何的改动，应用层是感知不到数据库的变更的。比如 Oracle 实现的 MySQL proxy 以及 Fabric 工具集，可以帮助我们无缝迁移到 NewSQL。\n此类 NewSQL 的问题在于其各个节点运行的依然是传统的 DBMS，比如 MySQL、Postgress 等，这些数据库都是面向磁盘的数据库，在现代高 CPU、高内存的机器下，其实现机制可能导致垂直扩展的能力不足，无法利用好机器的资源。\nAgilData Scalable Cluster2, MariaDB MaxScale, ScaleArc, ScaleBase3 等都属于此类 NewSQL。\nDatabase-as-a-Service 最后就是云厂商作为云服务提供的 NewSQL，最典型的代表就是 AWS 对外提供的兼容 MySQL 的 Aurora 。\n对于此类数据库，用户不需要自己维护，同时可以按需伸缩和付费，主打一个有钱可以为所欲为。需要明确的是，只有采用了新架构，同时支持 ACID 以及高可用、高性能、可扩展特性的数据库才会被视为 NewSQL，将 Azure SQL、Google Cloud SQL 这种仅仅提供传统关系型数据库节点管理的并不需要此列。\nNewSQL 的基本原理 本部分重点介绍了 NewSQL 的关键技术点，包括主存存储、分片处理、并发控制、二级索引、副本以及崩溃恢复处理，我们来分别看一下。\n主内存存储 传统的 DBMS 都是将数据库存储在 HDD/SSD 这种按块（block）存储的介质上，为了提高性能，这些数据库会使用内存来缓存读到的 block 以及事务的更新信息。因此内存有限，数据读写时经常需要去硬盘执行，这会导致事务阻塞，影响性能。\n随着科技的发展，当前的内存架构已经大大降低。在某些情况下，将整个数据集在内存中存储从而实现更加高效的数据读写性能已经是一个可行的策略。像 MemSQL、H-Store 等 NewSQL 均采用在主存存储数据的方式。\n主内存存储数据的方案并不是凭空而来的，早在 1980 年代威斯康星大学麦迪逊分校就针对用主内存来存储数据库开始了相关研究，到了 1990 年代，众多基于内存存储的数据库就已经崭露头角，像 Oracle 的 TimesTen 就是此类代表。\nNewSQL 使用内存存储数据的一个新特点是，它可以将某一部分数据子集驱逐到硬盘，从而使 DBMS 所能存储的数据量远大于内存容量。通常是在内部实现一个追踪机制，比如 H-Store 的 anti-caching 组件，可以将某些长时间没有被读取多的冷数据移动到磁盘，并在原来的数据中添加一个墓碑（tombstone）标记。当事务需要这部分数据时，事务会被阻塞，数据库启动一个新的线程异步将数据取回内存。\n对于支持 larger-than-memory 的 NewSQL 数据库，即使数据被驱逐回硬盘，数据库通常也会将 Key 留在内存中，这会导致不必要的内存浪费。一种参考的解决方案是 Microsoft’s Project Siberia 使用布隆过滤器来减少内存中对索引元素的跟踪数量。\nMemSQL 采用了双存储模型的形式，默认是行存储，数据全部存在内存中。如果数据量超过了内存大小，则可以转为列存储模式，将某些数据写入到硬盘中。\n二级索引 除了主键索引外，DBMS 通常需要针对某些字段创建二级索引以提升性能。通常需要解决两个问题：\n如何存储二级索引 如何在事务中维护二级索引 对于使用分片中间件实现的数据库集群，通常采用中心化的方式，每个节点都有一份完整的二级索引副本。而对于采用新架构的 NewSQL，基本都采用非中心化的方式，将二级索引也进行分片，每个节点只存储节点的一部分。二者各有利弊：\n对于中心化的方式，节点包含了所有索引，因此在一个节点上执行就可以查询到所有的数据；但在数据更新时，需要更新所有节点的二级索引。 对于非中心化的方式，可能需要遍历所有节点才能完成整个查询；而对于数据更新，则只需要更新索引所在的节点即可，不需要更新所有节点。 有一个特例是 Clustrix，其混合了上述两种方式，首先每个节点都有一份粗粒度（range-based）索引，每次数据查询时，通过该索引将查询路由到数据所在节点，然后每个节点维护各自的二级分片索引，帮助完成数据查询。\n对于不支持二级索引的 NewSQL，通常需要引入外部存储来实现，比如 Memcached，但需要在应用层实现缓存更新逻辑，避免数据过时。\n","permalink":"https://zouyingjie.github.io/posts/newsql%E8%AE%BA%E6%96%87/","summary":"\u003cp\u003e本篇论文是 2016 年 CMU 发表的一篇介绍 NewSQL 发展的综述论文，主要对新兴 NewSQL 数据库的起源、分类以及主要技术原理做了介绍，其中对数据库发展历史、NewSQL 实现原理的介绍非常值得一读。\u003c/p\u003e","title":"【读点论文】What’s Really New with NewSQL?"},{"content":"The most fundamental problem in computer science is problem decomposition: how to take a complex problem and divide it up into pieces that can be solved independently.\nIt\u0026rsquo;s all about complexity.\nComplexity Complexity is anything related to the structure of a software system that makes it hard to understand and modify the system.\nComplexity manifests itself in three general ways:\nChange amplification: a seemingly simple change requires code modifications in many different places.\nCognitive load: A higher cognitive load means that developers have to spend more time learning the required information.\nUnknown unknowns: It is not obvious which pieces of code must be modified to complete a task, or what information a developer must have to carry out the task successfully. This is the worst kind of complexity.\nComplexity is caused by two things:\ndependencies: obscurity So there are two general approaches to fighting complexity:\nclean code: Eliminating complexity by making code simpler and more obvious.\nmodular design: A software system is divided up into modules, such as classes in an object-oriented language. The modules are designed to be relatively independent of each other, so that a programmer can work on one module without having to understand the details of other modules\nStrategic vs. Tactical Programming Tactical programming means that your main focus is to get something working as soon as possible, such as a new feature or a bug fix, but it is short-sighted. It will make the system more complex in the future.\nThe first step towards becoming a good software designer is to realize that working code isn’t enough. Your primary goal must be to produce a great design, which also happens to work.\nModular Design Different Layer, Different abstraction Software systems are composed in layers, where higher layers use the facilities provided by lower layers. In a well-designed system, each layer provides a different abstraction from the layers above and below it; if you follow a single operation as it moves up and down through layers by invoking methods, the abstractions change with each method call.\nPass-through methods If different layers have the same abstraction, such as pass-through methods or decorators, then there’s a good chance that they haven’t provided enough benefit to compensate for the additional infrastructure they represent.\nThere are several solutions for remove pass-through methods.\nExpose the lower level class directly to the callers of the high level class. Redistribute the functionality between the classes. Merge theme Pass-through variable Another form of API duplication across layers is a pass-through variable, which is a variable that is passed down through a long chain of methods.\nPass-through variables add complexity, if a new variable comes into existence, you may have to modify a large number of interfaces and methods to pass the variable through all of the relevant paths.\nYou can use these methods for eliminating pass-though variables:\nShared object Global variables Context object, this is better Decorators\nThe decorator design pattern (also known as a “wrapper”) is one that encourages API duplication across layers. However, decorator classes tend to be shallow: they introduce a large amount of boilerplate for a small amount of new functionality.\nYou can consider alternatives before creating a decorator class:\nAdd new functionality directly to the underlying class. If the new functionality is specialized for a particular use case, would it make sense to merge it with the use case, rather than creating a separate class? Merge the new functionality with an existing decorator. Could you implement it as a stand-alone class that is independent of the base class? Design general-purpose modules Most modules have more users than developers, so it is better for the developers to suffer than the users. So it is more important for a module to have a simple interface than a simple implementation.\nPulling complexity down when:\nthe complexity being pulled down is closely related to the class’s existing functionality. pulling the complexity down will result in simplifications elsewhere in the application pulling the complexity down simplifies the class’s interface. ","permalink":"https://zouyingjie.github.io/posts/readnote-%E8%BD%AF%E4%BB%B6%E8%AE%BE%E8%AE%A1%E7%9A%84%E5%93%B2%E5%AD%A6/","summary":"\u003cp\u003eThe most fundamental problem in computer science is \u003cstrong\u003eproblem decomposition: how to take a complex problem and divide it up into pieces that can be solved independently.\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eIt\u0026rsquo;s all about \u003cstrong\u003ecomplexity\u003c/strong\u003e.\u003c/p\u003e\n\u003ch2 id=\"complexity\"\u003eComplexity\u003c/h2\u003e\n\u003cp\u003eComplexity is anything related to the structure of a software system that makes it hard to understand and modify the system.\u003c/p\u003e\n\u003cp\u003eComplexity manifests itself in three general ways:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eChange amplification\u003c/strong\u003e: a seemingly simple change requires code modifications in many different places.\u003c/p\u003e","title":"【读书笔记】《A Philosophy of Software Design》"},{"content":"最近项目遇到个需求，需要将后端的服务器出口统一成一个 IP，服务器在 AWS 上，这个可以用 AWS 的 NAT Gateway 实现，调研实施的过程中发现如果对 AWS 相关概念不熟悉的话会绕点路的，这里简单整理下，希望对需要的小伙伴有帮助。\n一. 相关概念简介 1. NAT Gateway NAT Gateway（网络地址转换网关） 主要用来对一组私有子网内的服务器进行代理，被代理的所有服务器的对外请求都将通过 NAT 网关发出，这样目标服务所看到的请求 IP 也都是 NAT 网关的 IP。这么做有两个好处：\n应用服务器只能在内网访问，提高了安全性 如果要访问的服务存在 IP 白名单的话，只需要将 NAT 网关的 IP 加进去即可，不需要挨个添加服务器地址。（自己之前和一家公司对接数据接口，对方就有 IP 白名单，当时没有统一网关导致添加了 20 几个 IP，一旦服务器 IP 变了还得重加，都是泪。。。） 下图是 AWS 官网中给出一个包含 NAT 网关的 VPC 架构图：\n简单讲解一下，在 10.0.0.0 VPC 下有两个子网：公共子网 10.0.0.0/24 和 私有子网 10.0.1.0/24。NAT 网关位于公共子网中，因此可以访问公网。私有子网的三台机器连接到了 NAT 网关，对公网的访问全部通过 NAT 网关实现。\n公共子网与私有子网\nAWS 的 VPC 网络配置关系是：实例与子网关联，子网关联路由表，路由表设置网关。所谓公共子网就是其对应的路由表中配置了规则将请求路由到了 Internet Gateway，这样公共子网内的服务器实例、NAT 网关就可以对外访问。而私有子网就是其关联的路由表中没有配置到 Internet Gatway 的路由规则，因此无法对外访问。\n清楚了公共子网、私有子网的概念，就可以进行 NAT 网关的相关设置了。私有子网中的服务器通过 NAT 网关对外访问需要做下面几步操作：\nVPC 下创建公共子网和私有子网 在私有子网中开通服务器实例 在公共子网中开通 NAT 网关和登陆到私有服务器的跳板机 修改私有子网的路由表规则，配置路由地址到 NAT 网关 执行测试。 下面是具体的操作，\n二. NAT 网关设置 1. 设置私有子网 上面提到，AWS 中网络配置的关系：实例与子网关联，子网关联路由表，路由表设置网关。我在 AWS 香港地区有三个子网如下\n所有子网的默认路由表配置一般都是指向了 Internet Gateway，如图所示：\n图中 0.0.0.0/0 的规则就表示将所有的请求路由到默认的 Internet Gateway，从而可以与公网通信。为了将使得子网变为私有，我们需要自己新建路由表，并将指向 Internet Gateway 的路由规则给去掉，这样子网就无法直接访问公网了。如图所示：\n创建路由表 修改子网关联 可以看到新建好的路由表并没有关联子网，点击 编辑子网关联 将 subnet-1f2a2767 子网关联上，这样这个子网下的服务器就无法和公网通信了。\n2. 创建私有服务器与跳板机 我选择第上面关联到自定义路由表的自我一个子网 subnet-1f2a2767 作为私有子网，第二个 f500f 作为公有子网。在私有子网下开了两台服务器，在公有子网下设置了一台服务器作为跳板机，步骤如下，注意在 「配置示例」模块选择对应的子网，并将在私有子网中的服务器禁用公网 IP。\n选择服务器 选择子网，启动实例。 此时子网中的两台服务器是无法与公网通信的。跳板机的创建也是上面的步骤，只是要选择公共子网并设置公网 IP，这里不再赘述。\n3. 创建 NAT 网关 在 AWS 的 VPC 控制面板，选择 「NAT 网关」，点击创建，需要选择子网，这里一定要选择公共子网，保证 NAT 网关是可以与公网通信。\n创建 NAT 网关，选择公共子网并分配 IP 可以看到创建的 NAT 网关 IP 为 18.162.217.123，待 NAT 网关的状态变为可用之后就可以修改路由表，将子网中的请求路由到 NAT 网关了。\n4. 修改私有子网的路由表 创建完成 NAT 网关之后，修改我们自己创建的私有子网的路由表，将子网中所有的请求路由到 NAT 网关了。如图：\n选择之前新建的路由表，选择「编辑路由」 第一栏目标设置为 0.0.0.0/0 表示除第一条外所有的请求都路由向设置的网关，第二栏目标选择 NAT 网关，就会出现可选的 NAT 网关列表 选择上面新建的 NAT 网关\n5. 测试网络 默认情况下，私有网络内的服务器是无法访问公网的，可以先用 ping 或者 curl 命令试下。配置完成之后就可以测试我们的私网服务器是否可以与外界通信了，我在腾讯云开了一台新的服务器并运行了 Nginx，访问情况如下：\n可以看到私网内的服务器可以访问其他网络内的服务器了，然后看下腾讯云上的 Nginx 日志，如下：\n日志中请求的源 IP 为 18.162.217.123，是我们设置的 NAT 网关的 IP 地址，由此通过 NAT 网关实现私有子网内服务器对外统一访问的设置就完成了。\n","permalink":"https://zouyingjie.github.io/posts/awsnatgateway%E4%BD%BF%E7%94%A8%E7%AE%80%E8%AE%B0/","summary":"\u003cp\u003e最近项目遇到个需求，需要将后端的服务器出口统一成一个 IP，服务器在 AWS 上，这个可以用 AWS 的 NAT Gateway 实现，调研实施的过程中发现如果对 AWS 相关概念不熟悉的话会绕点路的，这里简单整理下，希望对需要的小伙伴有帮助。\u003c/p\u003e","title":"AWS NAT Gateway 使用简记"},{"content":"实验准备 服务端环境准备 服务器信息 阿里云 99 大洋白嫖机\n1 2 [root@t01 ~]# cat /proc/version Linux version 5.10.134-18.al8.x86_64 (mockbuild@h87c01383.na61) (gcc (GCC) 10.2.1 20200825 (Alibaba 10.2.1-3.8 2.32), GNU ld version 2.35-12.3.al8) #1 SMP Fri Dec 13 16:56:53 CST 2024 安装 JDK， docker， tshark 1 ​yum install -y java-1.8.0-openjdk.x86_64 java-1.8.0-openjdk-devel.x86_64 podman-docker.noarch wireshark 启动 MySQL 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 [root@t01 ~]# docker run -it -d --net=host -e MYSQL_ROOT_PASSWORD=123 --name=mysql mysql:8.0.28 Emulate Docker CLI using podman. Create /etc/containers/nodocker to quiet msg. 233c8ca518d0e8feef367995aee656fffb65b6a2f16a589d2e765f06dad96828 [root@t01 ~]# docker ps Emulate Docker CLI using podman. Create /etc/containers/nodocker to quiet msg. CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 233c8ca518d0 docker.io/library/mysql:8.0.28 mysqld 11 seconds ago Up 12 seconds mysql [root@t01 ~]# docker exec -it mysql sh Emulate Docker CLI using podman. Create /etc/containers/nodocker to quiet msg. # mysql -uroot -p Enter password: Welcome to the MySQL monitor. Commands end with ; or \\g. Your MySQL connection id is 8 Server version: 8.0.28 MySQL Community Server - GPL Copyright (c) 2000, 2022, Oracle and/or its affiliates. Oracle is a registered trademark of Oracle Corporation and/or its affiliates. Other names may be trademarks of their respective owners. Type \u0026#39;help;\u0026#39; or \u0026#39;\\h\u0026#39; for help. Type \u0026#39;\\c\u0026#39; to clear the current input statement. mysql\u0026gt; \\s -------------- mysql Ver 8.0.28 for Linux on x86_64 (MySQL Community Server - GPL) Connection id:\t8 Current database: Current user:\troot@localhost SSL:\tNot in use Current pager:\tstdout Using outfile:\t\u0026#39;\u0026#39; Using delimiter:\t; Server version:\t8.0.28 MySQL Community Server - GPL Protocol version:\t10 Connection:\tLocalhost via UNIX socket Server characterset:\tutf8mb4 Db characterset:\tutf8mb4 Client characterset:\tlatin1 Conn. characterset:\tlatin1 UNIX socket:\t/var/run/mysqld/mysqld.sock Binary data as:\tHexadecimal Uptime:\t23 sec Threads: 2 Questions: 5 Slow queries: 0 Opens: 117 Flush tables: 3 Open tables: 36 Queries per second avg: 0.217 -------------- 初始化 MySQL 密码、数据库 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 CREATE DATABASE test CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ALTER USER \u0026#39;root\u0026#39;@\u0026#39;%\u0026#39; IDENTIFIED WITH mysql_native_password BY \u0026#39;123\u0026#39;; CREATE USER \u0026#39;test\u0026#39;@\u0026#39;%\u0026#39; IDENTIFIED BY \u0026#39;123\u0026#39;; GRANT ALL PRIVILEGES ON test.* TO test@\u0026#39;%\u0026#39;; FLUSH PRIVILEGES; # 创建表 create table t_user ( id bigint(20) unsigned not null auto_increment primary key comment \u0026#39;primary key\u0026#39;, name varchar(64) not null default \u0026#39;\u0026#39; comment \u0026#39;user name\u0026#39;, age tinyint unsigned not null default 0 comment \u0026#39;年龄\u0026#39;, gender tinyint unsigned not null default 0 comment \u0026#39;性别, 0 男，1 女\u0026#39;, create_at datetime(3) not null default current_timestamp(3) comment \u0026#39;record create date\u0026#39;, update_at datetime(3) not null default current_timestamp(3) on update current_timestamp(3) comment \u0026#39;record update date\u0026#39; ) engine = innodb default charset = utf8mb4 collate = utf8mb4_unicode_ci comment \u0026#39;用户表\u0026#39;; # 插入数据 insert into t_user (id, name, age) values (1, \u0026#34;tom\u0026#34;, 18); 执行查询 1 2 3 4 5 6 7 8 9 10 mysql\u0026gt; select sleep(10), id, name from t_user where id = 100; Empty set (0.00 sec) mysql\u0026gt; select sleep(10), id, name from t_user where id = 1; +-----------+----+------+ | sleep(10) | id | name | +-----------+----+------+ | 0 | 1 | tom | +-----------+----+------+ 1 row in set (10.00 sec) 能查到数据时，sleep 生效 数据不存在时，sleep 不生效 服务端抓包命令\n这里用 tshark 进行抓包，命令如下：\n1 sudo tshark -i lo -f \u0026#34;port 3306\u0026#34; -T fields -e frame.number -e frame.time_delta -e tcp.srcport -e tcp.dstport -e _ws.col.Info -e mysql.query -w /tmp/test01.pcapng 该命令的含义是：\n-i lo 指定抓取的网卡为 lo，即本地回环网卡 -f \u0026ldquo;port 3306\u0026rdquo; 指定抓取的端口为 3306，即 MySQL 端口 -T fields 指定输出格式为字段 -e frame.number -e frame.time_delta -e tcp.srcport -e tcp.dstport -e _ws.col.Info -e mysql.query 指定输出字段 -w /tmp/test01.pcapng 指定输出文件为 /tmp/test01.pcapng 客户端环境 下载依赖 mysql-connector-java-5.1.45.jar 客户端代码：Test.java 准备好环境后，执行客户端代码访问 MySQL，抓包分析网络通信过程。\n实验一：正常查询数据 客户端执行如下命令\n1 java -cp .:./mysql-connector-java-5.1.45.jar Test \u0026#34;jdbc:mysql://172.17.150.182:3306/test?useSSL=false\u0026amp;useServerPrepStmts=true\u0026amp;cachePrepStmts=true\u0026amp;connectTimeout=500\u0026amp;socketTimeout=1700\u0026#34; test 123 \u0026#34;select id from t_user where id= ?\u0026#34; 1 上述 JDBC 连接中各个参数的含义：\nuseSSL=false：禁用 SSL 加密 useServerPrepStmts=true：启用服务器端预处理语句 cachePrepStmts=true：启用预处理语句缓存 connectTimeout=500：连接超时时间，单位为毫秒 socketTimeout=1700：socket 超时时间，单位为毫秒，也就是客户端等待 MySQL 返回结果的最大时间，这里我们设置为 1.7s 下面分析抓包结果。首先是 TCP 的三次握手，三次握手成功后，MySQL Server 向客户端发送 Greeting 信息，除了基本的版本、状态信息，还有 salt 字段，用于后续的密码校验。\n客户端响应 ACK 后会发送 Login 请求，可以看到数据库、用户名、密码信息。密码基于 md5、sha1 算法以及 greeting 请求的盐进行了加密。\n如果校验失败会报 1045 错误，提示 Access denied for user xxx。下图是一个登录失败的抓包示例：\n登录成功后，会开始执行查询，在 Prepare Statement 前会执行一系列的准备语句。比如图中的一个请求执行的是 set autocommit = 1语句。\n上述一系列语句执行完成后，就会执行 Prepare Statement，会向 MySQL 发 prepareStatement 请求，然后在发送参数执行 Execute Statement 请求。\n在代码中我们执行了两次查询，分别调用 conn.prepareStatement(sql); 和 stmt2.executeQuery(); ，但抓包显示通信过程只有一次 prepareStatement 请求和两次 Execute Statement 请求。这说明预处理语句缓存已生效，查看 JDBC 代码应该是下面这段代码的作用：\n1 2 3 4 5 6 7 8 9 10 11 if (this.cachePrepStmts.getValue()) { ParseInfo pStmtInfo = this.cachedPreparedStatementParams.get(nativeSql); if (pStmtInfo == null) { pStmt = ClientPreparedStatement.getInstance(getMultiHostSafeProxy(), nativeSql, this.database); this.cachedPreparedStatementParams.put(nativeSql, pStmt.getParseInfo()); } else { pStmt = ClientPreparedStatement.getInstance(getMultiHostSafeProxy(), nativeSql, this.database, pStmtInfo); } } 第二次查询结束后 TCP 连接三次挥手断开。\n实验二：查询时 sleep(10) 这次我们将查询命令改为 select sleep(10), id from t_user where id = ? 且 ID 传 1，按照开始的测试 sleep 会生效，因此查询应该会超时。因此执行是报如下错误：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 java -cp .:./mysql-connector-java-5.1.45.jar Test \u0026#34;jdbc:mysql://172.17.150.182:3306/test?useSSL=false\u0026amp;useServerPrepStmts=true\u0026amp;cachePrepStmts=true\u0026amp;connectTimeout=500\u0026amp;socketTimeout=1700\u0026#34; test 123 \u0026#34;select sleep(10) id from t_user where id= ?\u0026#34; 1 com.mysql.jdbc.exceptions.jdbc4.CommunicationsException: Communications link failure The last packet successfully received from the server was 1,704 milliseconds ago. The last packet sent successfully to the server was 1,704 milliseconds ago. at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62) at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) at java.lang.reflect.Constructor.newInstance(Constructor.java:423) at com.mysql.jdbc.Util.handleNewInstance(Util.java:425) at com.mysql.jdbc.SQLError.createCommunicationsException(SQLError.java:990) at com.mysql.jdbc.MysqlIO.reuseAndReadPacket(MysqlIO.java:3559) at com.mysql.jdbc.MysqlIO.reuseAndReadPacket(MysqlIO.java:3459) at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3900) at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:2527) at com.mysql.jdbc.ServerPreparedStatement.serverExecute(ServerPreparedStatement.java:1283) at com.mysql.jdbc.ServerPreparedStatement.executeInternal(ServerPreparedStatement.java:783) at com.mysql.jdbc.PreparedStatement.executeQuery(PreparedStatement.java:1966) at Test.main(Test.java:21) Caused by: java.net.SocketTimeoutException: Read timed out at java.net.SocketInputStream.socketRead0(Native Method) at java.net.SocketInputStream.socketRead(SocketInputStream.java:116) at java.net.SocketInputStream.read(SocketInputStream.java:171) at java.net.SocketInputStream.read(SocketInputStream.java:141) at com.mysql.jdbc.util.ReadAheadInputStream.fill(ReadAheadInputStream.java:101) at com.mysql.jdbc.util.ReadAheadInputStream.readFromUnderlyingStreamIfNecessary(ReadAheadInputStream.java:144) at com.mysql.jdbc.util.ReadAheadInputStream.read(ReadAheadInputStream.java:174) at com.mysql.jdbc.MysqlIO.readFully(MysqlIO.java:3008) at com.mysql.jdbc.MysqlIO.reuseAndReadPacket(MysqlIO.java:3469) ... 7 more 分析抓包信息，前期的握手、登录 prepareStatement 和 executeStatement 都和正常查询时一致。但是因为设置了 sleep(10)，MySQL 会等 10s 才会返回结果。而我们客户端设置的超时时间是 1.7s，从 Wireshark 中可以看到，客户端在 0.117s 发送了 executeStatement 请求，数据没有返回后，在 1.84s 客户端向服务端发送了 FIN 请求，并在 MySQL 返回 ACK 发送了 RST 包断开连接。\n实验三：sleep(10) 但查询不到数据 这里将传递的 ID 改为 10，数据库中是没有这条数据的，因此 sleep(10) 不会生效，查询会正常执行，\n1 java -cp .:./mysql-connector-java-5.1.45.jar Test \u0026#34;jdbc:mysql://172.17.150.182:3306/test?useSSL=false\u0026amp;useServerPrepStmts=true\u0026amp;cachePrepStmts=true\u0026amp;connectTimeout=500\u0026amp;socketTimeout=1700\u0026#34; test 123 \u0026#34;select sleep(10), id from t_user where id= ?\u0026#34; 10 抓包结果和实验一基本一致，唯一区别就是执行 executeStatement 请求后，因为查不到数据，MySQL 返回的数据大小不一致。 简要总结 中间件连接问题，本质上都是 TCP/IP 的通信问题，抓包之下一切无所遁形。\n做技术当然要对很多具体的知识点做掌握，但更重要的是底层内功的修炼，可以提高自己在不熟悉的问题场景下蹚出一条路的能力。\n","permalink":"https://zouyingjie.github.io/posts/mysql%E5%AE%A2%E6%88%B7%E7%AB%AFtimeout%E5%88%86%E6%9E%90/","summary":"\u003ch2 id=\"实验准备\"\u003e实验准备\u003c/h2\u003e\n\u003ch3 id=\"服务端环境准备\"\u003e服务端环境准备\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e服务器信息\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e阿里云 99 大洋白嫖机\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e[\u003c/span\u003eroot@t01 ~\u003cspan class=\"o\"\u003e]\u003c/span\u003e\u003cspan class=\"c1\"\u003e# cat /proc/version\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eLinux version 5.10.134-18.al8.x86_64 \u003cspan class=\"o\"\u003e(\u003c/span\u003emockbuild@h87c01383.na61\u003cspan class=\"o\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e(\u003c/span\u003egcc \u003cspan class=\"o\"\u003e(\u003c/span\u003eGCC\u003cspan class=\"o\"\u003e)\u003c/span\u003e 10.2.1 \u003cspan class=\"m\"\u003e20200825\u003c/span\u003e \u003cspan class=\"o\"\u003e(\u003c/span\u003eAlibaba 10.2.1-3.8 2.32\u003cspan class=\"o\"\u003e)\u003c/span\u003e, GNU ld version 2.35-12.3.al8\u003cspan class=\"o\"\u003e)\u003c/span\u003e \u003cspan class=\"c1\"\u003e#1 SMP Fri Dec 13 16:56:53 CST 2024\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cul\u003e\n\u003cli\u003e安装 JDK， docker， tshark\u003c/li\u003e\n\u003c/ul\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-shell\" data-lang=\"shell\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e​yum install -y java-1.8.0-openjdk.x86_64 java-1.8.0-openjdk-devel.x86_64  podman-docker.noarch wireshark\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cul\u003e\n\u003cli\u003e启动 MySQL\u003c/li\u003e\n\u003c/ul\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e 1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 8\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 9\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e10\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e11\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e12\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e13\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e14\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e15\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e16\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e17\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e18\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e19\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e20\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e21\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e22\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e23\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e24\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e25\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e26\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e27\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e28\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e29\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e30\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e31\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e32\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e33\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e34\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e35\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e36\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e37\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e38\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e39\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e40\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e41\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e42\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e43\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e44\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e45\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e46\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e47\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e48\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e49\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-shell\" data-lang=\"shell\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e[\u003c/span\u003eroot@t01 ~\u003cspan class=\"o\"\u003e]\u003c/span\u003e\u003cspan class=\"c1\"\u003e#  docker run -it -d --net=host -e MYSQL_ROOT_PASSWORD=123 --name=mysql mysql:8.0.28\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eEmulate Docker CLI using podman. Create /etc/containers/nodocker to quiet msg.\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e233c8ca518d0e8feef367995aee656fffb65b6a2f16a589d2e765f06dad96828\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e[\u003c/span\u003eroot@t01 ~\u003cspan class=\"o\"\u003e]\u003c/span\u003e\u003cspan class=\"c1\"\u003e# docker  ps\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eEmulate Docker CLI using podman. Create /etc/containers/nodocker to quiet msg.\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eCONTAINER ID  IMAGE                           COMMAND     CREATED         STATUS         PORTS       NAMES\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e233c8ca518d0  docker.io/library/mysql:8.0.28  mysqld      \u003cspan class=\"m\"\u003e11\u003c/span\u003e seconds ago  Up \u003cspan class=\"m\"\u003e12\u003c/span\u003e seconds              mysql\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e[\u003c/span\u003eroot@t01 ~\u003cspan class=\"o\"\u003e]\u003c/span\u003e\u003cspan class=\"c1\"\u003e# docker exec -it mysql sh\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eEmulate Docker CLI using podman. Create /etc/containers/nodocker to quiet msg.\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# mysql -uroot -p\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eEnter password:\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eWelcome to the MySQL monitor.  Commands end with \u003cspan class=\"p\"\u003e;\u003c/span\u003e or \u003cspan class=\"se\"\u003e\\g\u003c/span\u003e.\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eYour MySQL connection id is \u003cspan class=\"m\"\u003e8\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eServer version: 8.0.28 MySQL Community Server - GPL\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eCopyright \u003cspan class=\"o\"\u003e(\u003c/span\u003ec\u003cspan class=\"o\"\u003e)\u003c/span\u003e 2000, 2022, Oracle and/or its affiliates.\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eOracle is a registered trademark of Oracle Corporation and/or its\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eaffiliates. Other names may be trademarks of their respective\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eowners.\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eType \u003cspan class=\"s1\"\u003e\u0026#39;help;\u0026#39;\u003c/span\u003e or \u003cspan class=\"s1\"\u003e\u0026#39;\\h\u0026#39;\u003c/span\u003e \u003cspan class=\"k\"\u003efor\u003c/span\u003e help. Type \u003cspan class=\"s1\"\u003e\u0026#39;\\c\u0026#39;\u003c/span\u003e to clear the current input statement.\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003emysql\u0026gt; \u003cspan class=\"se\"\u003e\\s\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e--------------\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003emysql  Ver 8.0.28 \u003cspan class=\"k\"\u003efor\u003c/span\u003e Linux on x86_64 \u003cspan class=\"o\"\u003e(\u003c/span\u003eMySQL Community Server - GPL\u003cspan class=\"o\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eConnection id:\t\t\u003cspan class=\"m\"\u003e8\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eCurrent database:\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eCurrent user:\t\troot@localhost\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eSSL:\t\t\tNot in use\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eCurrent pager:\t\tstdout\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eUsing outfile:\t\t\u003cspan class=\"s1\"\u003e\u0026#39;\u0026#39;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eUsing delimiter:\t\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eServer version:\t\t8.0.28 MySQL Community Server - GPL\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eProtocol version:\t\u003cspan class=\"m\"\u003e10\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eConnection:\t\tLocalhost via UNIX socket\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eServer characterset:\tutf8mb4\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eDb     characterset:\tutf8mb4\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eClient characterset:\tlatin1\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eConn.  characterset:\tlatin1\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eUNIX socket:\t\t/var/run/mysqld/mysqld.sock\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eBinary data as:\t\tHexadecimal\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eUptime:\t\t\t\u003cspan class=\"m\"\u003e23\u003c/span\u003e sec\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eThreads: \u003cspan class=\"m\"\u003e2\u003c/span\u003e  Questions: \u003cspan class=\"m\"\u003e5\u003c/span\u003e  Slow queries: \u003cspan class=\"m\"\u003e0\u003c/span\u003e  Opens: \u003cspan class=\"m\"\u003e117\u003c/span\u003e  Flush tables: \u003cspan class=\"m\"\u003e3\u003c/span\u003e  Open tables: \u003cspan class=\"m\"\u003e36\u003c/span\u003e  Queries per second avg: 0.217\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e--------------\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cul\u003e\n\u003cli\u003e初始化 MySQL 密码、数据库\u003c/li\u003e\n\u003c/ul\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e 1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 8\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 9\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e10\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e11\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e12\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e13\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e14\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e15\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e16\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e17\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e18\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e19\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e20\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e21\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e22\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e23\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-sql\" data-lang=\"sql\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eCREATE\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003eDATABASE\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003etest\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nb\"\u003eCHARACTER\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003eSET\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eutf8mb4\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003eCOLLATE\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eutf8mb4_unicode_ci\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\u003c/span\u003e\u003cspan class=\"k\"\u003eALTER\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003eUSER\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;root\u0026#39;\u003c/span\u003e\u003cspan class=\"o\"\u003e@\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;%\u0026#39;\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eIDENTIFIED\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003eWITH\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003emysql_native_password\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003eBY\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;123\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\u003c/span\u003e\u003cspan class=\"k\"\u003eCREATE\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003eUSER\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;test\u0026#39;\u003c/span\u003e\u003cspan class=\"o\"\u003e@\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;%\u0026#39;\u003c/span\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"n\"\u003eIDENTIFIED\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003eBY\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;123\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\u003cspan class=\"w\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\u003c/span\u003e\u003cspan class=\"k\"\u003eGRANT\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003eALL\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003ePRIVILEGES\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003eON\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003etest\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003eTO\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003etest\u003c/span\u003e\u003cspan class=\"o\"\u003e@\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;%\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\u003cspan class=\"w\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\u003c/span\u003e\u003cspan class=\"n\"\u003eFLUSH\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003ePRIVILEGES\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\u003c/span\u003e\u003cspan class=\"o\"\u003e#\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"err\"\u003e创建表\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\u003c/span\u003e\u003cspan class=\"k\"\u003ecreate\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003etable\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003et_user\u003c/span\u003e\u003cspan class=\"w\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"w\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"n\"\u003eid\u003c/span\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"nb\"\u003ebigint\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mi\"\u003e20\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eunsigned\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003enot\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003enull\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eauto_increment\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003eprimary\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003ekey\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003ecomment\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;primary key\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"n\"\u003ename\u003c/span\u003e\u003cspan class=\"w\"\u003e      \u003c/span\u003e\u003cspan class=\"nb\"\u003evarchar\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mi\"\u003e64\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e         \u003c/span\u003e\u003cspan class=\"k\"\u003enot\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003enull\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003edefault\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;\u0026#39;\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003ecomment\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;user name\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"n\"\u003eage\u003c/span\u003e\u003cspan class=\"w\"\u003e       \u003c/span\u003e\u003cspan class=\"n\"\u003etinyint\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eunsigned\u003c/span\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"k\"\u003enot\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003enull\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003edefault\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003ecomment\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;年龄\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"n\"\u003egender\u003c/span\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"n\"\u003etinyint\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eunsigned\u003c/span\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"k\"\u003enot\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003enull\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003edefault\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003ecomment\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;性别, 0 男，1 女\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"n\"\u003ecreate_at\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003edatetime\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mi\"\u003e3\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e         \u003c/span\u003e\u003cspan class=\"k\"\u003enot\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003enull\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003edefault\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003ecurrent_timestamp\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mi\"\u003e3\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003ecomment\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;record create date\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"n\"\u003eupdate_at\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003edatetime\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mi\"\u003e3\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e         \u003c/span\u003e\u003cspan class=\"k\"\u003enot\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003enull\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003edefault\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003ecurrent_timestamp\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mi\"\u003e3\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003eon\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003eupdate\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003ecurrent_timestamp\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mi\"\u003e3\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003ecomment\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;record update date\u0026#39;\u003c/span\u003e\u003cspan class=\"w\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eengine\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003einnodb\u003c/span\u003e\u003cspan class=\"w\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"k\"\u003edefault\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003echarset\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eutf8mb4\u003c/span\u003e\u003cspan class=\"w\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"k\"\u003ecollate\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eutf8mb4_unicode_ci\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003ecomment\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;用户表\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\u003c/span\u003e\u003cspan class=\"o\"\u003e#\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"err\"\u003e插入数据\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\u003c/span\u003e\u003cspan class=\"k\"\u003einsert\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003einto\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003et_user\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eid\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eage\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003evalues\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;tom\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"mi\"\u003e18\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cul\u003e\n\u003cli\u003e执行查询\u003c/li\u003e\n\u003c/ul\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e 1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 8\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 9\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e10\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-sql\" data-lang=\"sql\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003emysql\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"k\"\u003eselect\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003esleep\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mi\"\u003e10\u003c/span\u003e\u003cspan class=\"p\"\u003e),\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eid\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003ename\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003efrom\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003et_user\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003ewhere\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eid\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"mi\"\u003e100\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\u003c/span\u003e\u003cspan class=\"n\"\u003eEmpty\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003eset\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"mi\"\u003e00\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003esec\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\u003c/span\u003e\u003cspan class=\"n\"\u003emysql\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"k\"\u003eselect\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003esleep\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mi\"\u003e10\u003c/span\u003e\u003cspan class=\"p\"\u003e),\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eid\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003ename\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003efrom\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003et_user\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003ewhere\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eid\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\u003c/span\u003e\u003cspan class=\"o\"\u003e+\u003c/span\u003e\u003cspan class=\"c1\"\u003e-----------+----+------+\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e\u003c/span\u003e\u003cspan class=\"o\"\u003e|\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003esleep\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mi\"\u003e10\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e|\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eid\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e|\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003ename\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e|\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\u003c/span\u003e\u003cspan class=\"o\"\u003e+\u003c/span\u003e\u003cspan class=\"c1\"\u003e-----------+----+------+\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e\u003c/span\u003e\u003cspan class=\"o\"\u003e|\u003c/span\u003e\u003cspan class=\"w\"\u003e         \u003c/span\u003e\u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e|\u003c/span\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e|\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003etom\u003c/span\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"o\"\u003e|\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\u003c/span\u003e\u003cspan class=\"o\"\u003e+\u003c/span\u003e\u003cspan class=\"c1\"\u003e-----------+----+------+\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e\u003c/span\u003e\u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003erow\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003ein\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003eset\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mi\"\u003e10\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"mi\"\u003e00\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003esec\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cul\u003e\n\u003cli\u003e能查到数据时，sleep 生效\u003c/li\u003e\n\u003cli\u003e数据不存在时，sleep 不生效\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003e服务端抓包命令\u003c/strong\u003e\u003c/p\u003e","title":"【动手实验】MySQL 客户端 SocketTimeout 问题抓包分析"},{"content":"之前在实验中提到了 tcp_max_orphans 和 tcp_orphan_retries 两个参数，我们使用 ss -s 命令查看当前系统中的 socket 状态也有 orphan 状态的 socket，本篇文章我们就来分析下到底什么情况下的 socket 才会被视为 orphan socket。\n1 2 3 4 # ubuntu @ node1 in ~ [12:07:15] $ ss -s Total: 184 TCP: 14 (estab 5, closed 1, orphaned 0, timewait 1) 什么是孤儿(Orphan) socket？ 没有在 Wiki 中找到关于孤儿 socket 的定义，在 StackOverflow 中找到了一个比较通俗易懂的解释：\nDo you have \u0026ldquo;too many\u0026rdquo; orphan sockets?\nFirst of all: what\u0026rsquo;s an orphan socket? It\u0026rsquo;s simply a socket that isn\u0026rsquo;t associated to a file descriptor. For instance, after you close() a socket, you no longer hold a file descriptor to reference it, but it still exists because the kernel has to keep it around for a bit more until TCP is done with it. Because orphan sockets aren\u0026rsquo;t very useful to applications (since applications can\u0026rsquo;t interact with them), the kernel is trying to limit the amount of memory consumed by orphans, and it does so by limiting the number of orphans that stick around. If you\u0026rsquo;re running a frontend web server (or an HTTP load balancer), then you\u0026rsquo;ll most likely have a sizeable number of orphans, and that\u0026rsquo;s perfectly normal.\n下面一段是 DeepSeek 给出的解释：\nOrphan Sockets refer to TCP sockets that no longer have an associated user process but remain in the operating system\u0026rsquo;s kernel network stack. These sockets persist because the owning process terminated (e.g., crashed or exited abruptly) without properly closing the socket, leaving the kernel to manage cleanup\n总的来说就是当程序调用 close() 函数关闭 socket 后，socket 就和应用程序无关了，但此时 TCP 终止流程还没有走完，所以内核里还有这些 socket 的数据，这些 socket 就是所谓的孤儿 socket。下面我们结合源码和实验分析下哪些情况下的 socket 会被视为孤儿 socket。\nOrphan socket 的产生 上面提到内核是在程序调用 close() 函数后的状态视为 orphan socket，因此内核中 orphan socket 的产生主要是在 tcp_close 函数中，其内部调用 __tcp_close 函数执行主要逻辑：我们来分析下。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 // https://elixir.bootlin.com/linux/v5.15.130/source/net/ipv4/tcp.c#L2910 void tcp_close(struct sock *sk, long timeout) { lock_sock(sk); __tcp_close(sk, timeout); release_sock(sk); sock_put(sk); } void __tcp_close(struct sock *sk, long timeout) { struct sk_buff *skb; int data_was_unread = 0; int state; WRITE_ONCE(sk-\u0026gt;sk_shutdown, SHUTDOWN_MASK); // 处于 listen 状态，直接进入 TCP_CLOSE 状态 if (sk-\u0026gt;sk_state == TCP_LISTEN) { tcp_set_state(sk, TCP_CLOSE); /* Special case. */ inet_csk_listen_stop(sk); goto adjudge_to_death; } // ... 代码省略 } 可以看到，在 __tcp_close 函数中，有一个 adjudge_to_death的 goto 标签，这部分代码执行了将 socket 变为 orphan 状态的逻辑，并增加 orphan socket 计数，然后执行一系列状态变迁逻辑后，如果 socket 处于 TCP_CLOSE 状态，则清理掉 socket 并减少 orphan socket 计数。\n执行清理，增加 orphan socket 计数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 // 源码地址：https://elixir.bootlin.com/linux/v5.15.130/source/net/ipv4/tcp.c#L2828 adjudge_to_death: state = sk-\u0026gt;sk_state; sock_hold(sk); // 执行清理，将 socket 变为 orphan 状态 sock_orphan(sk); local_bh_disable(); bh_lock_sock(sk); /* remove backlog if any, without releasing ownership. */ __release_sock(sk); // 增加 orphan socket 计数 this_cpu_inc(tcp_orphan_count); // ...代码省略：状态变迁逻辑 具体的清理任务 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 // 源码地址：https://elixir.bootlin.com/linux/v5.15.130/source/include/net/sock.h#L1992 /* Detach socket from process context. * Announce socket dead, detach it from wait queue and inode. * Note that parent inode held reference count on this struct sock, * we do not release it in this function, because protocol * probably wants some additional cleanups or even continuing * to work with this socket (TCP). */ static inline void sock_orphan(struct sock *sk) { write_lock_bh(\u0026amp;sk-\u0026gt;sk_callback_lock); // 设置 SOCK_DEAD 标志，标记 socket 已死亡，不在于任何进程有关系 sock_set_flag(sk, SOCK_DEAD); // 设置 socket 为 NULL，不在于文件描述符有关系 sk_set_socket(sk, NULL); // 清空等待队列 sk-\u0026gt;sk_wq = NULL; write_unlock_bh(\u0026amp;sk-\u0026gt;sk_callback_lock); } 减少 orphan socket 计数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 // 源码地址：https://elixir.bootlin.com/linux/v5.15.130/source/net/ipv4/tcp.c#L2890 void __tcp_close(struct sock *sk, long timeout) { // ... 代码省略 // 如果 socket 处于 TCP_CLOSE 状态，则执行清理 if (sk-\u0026gt;sk_state == TCP_CLOSE) { // ... 代码省略 // 销毁 socket，减少 orphan socket 计数 inet_csk_destroy_sock(sk); } } // 具体的销毁任务 // 源码地址：https://elixir.bootlin.com/linux/v5.15.130/source/net/ipv4/inet_connection_sock.c#L1005 void inet_csk_destroy_sock(struct sock *sk) { // ... 代码省略 // 减少 orphan socket 计数 this_cpu_dec(*sk-\u0026gt;sk_prot-\u0026gt;orphan_count); sock_put(sk); } 由此我们知道，内核在执行 close() 时会先将 socket 视为 orphan 状态，增加计数，执行完 TCP 状态转换后，如果 socket 进入 CLOSE 状态，那么 orphan socket 计数会减少，也就是说此时不会有 orphan socket 了。我们在来分析下具体的状态转换逻辑，看哪些情况下 socket 会进入 CLOSE 状态。\nTCP 主动关闭连接。如果 socket 处于 TCP_LISTEN 状态，则直接进入 TCP_CLOSE 状态，然后执行 adjudge_to_death 标签，增加 orphan socket 计数，但最终会走到 inet_csk_destroy_sock 函数，减少 orphan socket 计数，并销毁 socket。 1 2 3 4 5 6 7 8 9 10 // 源码地址：https://elixir.bootlin.com/linux/v5.15.130/source/net/ipv4/tcp.c#L2747 if (sk-\u0026gt;sk_state == TCP_LISTEN) { // 设置 socket 为 TCP_CLOSE 状态 tcp_set_state(sk, TCP_CLOSE); /* Special case. */ inet_csk_listen_stop(sk); goto adjudge_to_death; } 如果接收队列中存在未被应用读取的数据，则进入 TCP_CLOSE 状态，并向对端发送 RST 报文，然后销毁 socket。正常情况下，应用应该会在处理完数据后调用 close()，所以如果调用 close() 时还没有处理完数据，会被视为异常关闭，比如在应用异常崩溃、网络中断时会出现这种情况，因此会直接发送 RST 报文，并销毁 socket。 1 2 3 4 5 6 7 // 源码地址：https://elixir.bootlin.com/linux/v5.15.130/source/net/ipv4/tcp.c#L2784 else if (data_was_unread) { /* Unread data was tossed, zap the connection. */ NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPABORTONCLOSE); // 设置 socket 为 TCP_CLOSE 状态 tcp_set_state(sk, TCP_CLOSE); tcp_send_active_reset(sk, sk-\u0026gt;sk_allocation); 系统开启了 so_linger 并设置为0，表示 0 延迟关闭 ，socket 会丢弃数据并发送 RST 报文，然后进入 TCP_CLOSE 状态，并销毁 socket。 1 2 3 4 5 6 // 源码地址：https://elixir.bootlin.com/linux/v5.15.130/source/net/ipv4/tcp.c#L2789 else if (sock_flag(sk, SOCK_LINGER) \u0026amp;\u0026amp; !sk-\u0026gt;sk_lingertime) { /* Check zero linger _after_ checking for unread data. */ sk-\u0026gt;sk_prot-\u0026gt;disconnect(sk, 0); NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPABORTONDATA); } disconnect 调用会执行 tcp_disconnect 函数，该函数会发送 RST 报文，并设置 socket 为 TCP_CLOSE 状态，然后销毁 socket。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 // 源码地址：https://elixir.bootlin.com/linux/v5.15.130/source/net/ipv4/tcp.c#L2967 int tcp_disconnect(struct sock *sk, int flags) { struct inet_sock *inet = inet_sk(sk); struct inet_connection_sock *icsk = inet_csk(sk); struct tcp_sock *tp = tcp_sk(sk); int old_state = sk-\u0026gt;sk_state; u32 seq; /* Deny disconnect if other threads are blocked in sk_wait_event() * or inet_wait_for_connect(). */ if (sk-\u0026gt;sk_wait_pending) return -EBUSY; // 设置 socket 为 TCP_CLOSE 状态 if (old_state != TCP_CLOSE) tcp_set_state(sk, TCP_CLOSE); 除了以上 3 种情况，后面就是正常的 TCP 状态转换逻辑了，代码从状态机映射中，获取到下个状态值做状态转换，我们最常看到的关闭时的状态转换就是在这里执行的，可以结合图例加深理解。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 // 源码地址：https://elixir.bootlin.com/linux/v5.15.130/source/net/ipv4/tcp.c#L2793 else if (tcp_close_state(sk)) { // 完成状态转换后，发送 FIN 报文 tcp_send_fin(sk); } // 源码地址：https://elixir.bootlin.com/linux/v5.15.130/source/net/ipv4/tcp.c#L2664 static const unsigned char new_state[16] = { /* current state: new state: action:\t*/ [0 /* (Invalid) */]\t= TCP_CLOSE, [TCP_ESTABLISHED]\t= TCP_FIN_WAIT1 | TCP_ACTION_FIN, [TCP_SYN_SENT]\t= TCP_CLOSE, [TCP_SYN_RECV]\t= TCP_FIN_WAIT1 | TCP_ACTION_FIN, [TCP_FIN_WAIT1]\t= TCP_FIN_WAIT1, [TCP_FIN_WAIT2]\t= TCP_FIN_WAIT2, [TCP_TIME_WAIT]\t= TCP_CLOSE, [TCP_CLOSE]\t= TCP_CLOSE, [TCP_CLOSE_WAIT]\t= TCP_LAST_ACK | TCP_ACTION_FIN, [TCP_LAST_ACK]\t= TCP_LAST_ACK, [TCP_LISTEN]\t= TCP_CLOSE, [TCP_CLOSING]\t= TCP_CLOSING, [TCP_NEW_SYN_RECV]\t= TCP_CLOSE,\t/* should not happen ! */ }; static int tcp_close_state(struct sock *sk) { int next = (int)new_state[sk-\u0026gt;sk_state]; int ns = next \u0026amp; TCP_STATE_MASK; tcp_set_state(sk, ns); return next \u0026amp; TCP_ACTION_FIN; } 结合状态变迁和后续的代码执行，我们知道最终 socket 的状态只会是 CLOSE，FIN_WAIT1、FIN_WAIT_2、CLOSING、LAST_ACK中的一种。除了 CLOSE 状态，其他状态下都不会执行 inet_csk_destroy_sock 函数，也就不会减少 orphan socket 计数，因此这些状态的 socket 都会被视为 orphan socket。CLOSING 状态的构造相对麻烦，这里我们做实验看下 socket 在 FIN_WAIT_1，LAST_ACK 状态时 orphan socket 的计数变化。FIN_WAIT_2 状态的处理有一些特殊，我们在下一节做更详细的探讨。\n首先服务端和客户端查看 socket 统计信息，orphan 计数为 0。\n1 2 3 $ ss -s Total: 176 TCP: 15 (estab 5, closed 5, orphaned 0, timewait 5) 1. FIN_WAIT_1 状态 这里用 Go 程序做服务端和客户端，代码如下：\n服务端程序 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package main import ( \u0026#34;log\u0026#34; \u0026#34;net\u0026#34; \u0026#34;time\u0026#34; ) func main() { l, err := net.Listen(\u0026#34;tcp\u0026#34;, \u0026#34;:8888\u0026#34;) if err != nil { log.Printf(\u0026#34;failed to listen due to %v\u0026#34;, err) } defer l.Close() log.Println(\u0026#34;listen :8888 success\u0026#34;) for { time.Sleep(time.Second * 100) } } 客户端程序 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 package main import ( \u0026#34;context\u0026#34; \u0026#34;log\u0026#34; \u0026#34;net\u0026#34; \u0026#34;os\u0026#34; \u0026#34;os/signal\u0026#34; \u0026#34;sync\u0026#34; \u0026#34;syscall\u0026#34; \u0026#34;time\u0026#34; ) var wg sync.WaitGroup func establishConn(ctx context.Context, i int) { defer wg.Done() conn, err := net.DialTimeout(\u0026#34;tcp\u0026#34;, \u0026#34;172.19.0.12:8888\u0026#34;, time.Second*50) if err != nil { log.Printf(\u0026#34;%d, dial error: %v\u0026#34;, i, err) return } log.Printf(\u0026#34;%d, dial success\u0026#34;, i) _, err = conn.Write([]byte(\u0026#34;hello world\u0026#34;)) if err != nil { log.Printf(\u0026#34;%d, send error: %v\u0026#34;, i, err) return } select { case \u0026lt;-ctx.Done(): log.Printf(\u0026#34;%d, dail close\u0026#34;, i) } } func main() { ctx, cancel := context.WithCancel(context.Background()) // 并发请求 10 次服务端，连接建立成功后发送数据 for i := 0; i \u0026lt; 10; i++ { wg.Add(1) time.Sleep(1 * time.Millisecond) go establishConn(ctx, i) } go func() { sc := make(chan os.Signal, 1) signal.Notify(sc, syscall.SIGINT) select { case \u0026lt;-sc: cancel() } }() wg.Wait() log.Printf(\u0026#34;client exit\u0026#34;) } 我们使用 iptables DROP 掉服务端返回的 ACK 包，使客户端处于 FIN_WAIT_1 状态，然后查看 socket 统计信息，预期 orphan 计数为 10。\n服务端 DROP 客户端的 FIN 包\n1 $ sudo iptables -A INPUT -p tcp --dport 8888 --tcp-flags FIN FIN -j DROP 我们运行客户端命令后按下 ctrl+c 中断客户端程序，查看 socket 统计信息，orphan 计数为 10，符合预期。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 $ ss -ant | grep -E \u0026#34;Recv|8888\u0026#34; State Recv-Q Send-Q Local Address:Port Peer Address:Port Process FIN-WAIT-1 0 1 172.19.0.15:48344 172.19.0.12:8888 FIN-WAIT-1 0 1 172.19.0.15:48290 172.19.0.12:8888 FIN-WAIT-1 0 1 172.19.0.15:48300 172.19.0.12:8888 FIN-WAIT-1 0 1 172.19.0.15:48322 172.19.0.12:8888 FIN-WAIT-1 0 1 172.19.0.15:48276 172.19.0.12:8888 FIN-WAIT-1 0 1 172.19.0.15:48302 172.19.0.12:8888 FIN-WAIT-1 0 1 172.19.0.15:48330 172.19.0.12:8888 FIN-WAIT-1 0 1 172.19.0.15:48288 172.19.0.12:8888 FIN-WAIT-1 0 1 172.19.0.15:48310 172.19.0.12:8888 FIN-WAIT-1 0 1 172.19.0.15:48334 172.19.0.12:8888 # ubuntu @ node3 in ~ [18:03:09] $ ss -s Total: 209 TCP: 30 (estab 10, closed 4, orphaned 10, timewait 4) 2. LAST_ACK 状态 这里我用 nc 启动服务端，然后在客户端发起两次连接:\n1 2 3 4 5 # 服务端 $ nc -k -l 172.19.0.12 8888 # 客户端，开两个终端发起两次 $ nc 172.19.0.12 8888 因为 nc 没有数据需要处理，所以服务端收到客户端的 FIN 包后会将 ACK-FIN 包一起发，然后进入 LAST_ACK 状态，我们在客户端设置 iptables 规则将包丢弃，这样客户端就不会收到 ACK-FIN 包，服务端就会一直处于 LAST_ACK 状态。\n1 2 # DROP 源端口为 8888 的 ACK-FIN 包 sudo iptables -A INPUT -p tcp --sport 8888 --tcp-flags ACK,FIN ACK,FIN -j DROP 设置完成后客户端按下 ctrl+c 中断连接，查看服务端的 socket 统计信息，可以看到有 2 个 LAST_ACK 状态的 socket，orphan 计数为 2，符合预期。\n1 2 3 4 5 6 7 $ ss -ant | grep -E \u0026#34;Recv|:8888\u0026#34; \u0026amp;\u0026amp; ss -s State Recv-Q Send-Q Local Address:Port Peer Address:Port Process LISTEN 0 1 172.19.0.12:8888 0.0.0.0:* LAST-ACK 0 1 172.19.0.12:8888 172.19.0.15:48566 LAST-ACK 0 1 172.19.0.12:8888 172.19.0.15:36574 Total: 212 TCP: 21 (estab 10, closed 2, orphaned 2, timewait 2) Orphan socket 的消亡 知道了哪些状态的 socket 会被视为 orphan socket，我们就可以分析下这些 socket 是如何消失的。\nFIN-WAIT-2 下的 socket 处理 我们上面分析了 FIN_WAIT_2 状态下的 socket 应该也算是 orphan，但如果执行正常连接关闭操作时，查看 ss -s 统计会发现 orphan 为 0。下面用实验验证下：\n服务端使用 nc 监听 8888，客户端发起三个连接 1 2 3 4 5 # 服务端 $ nc -k -l 172.19.0.12 8888 # 客户端，开三个窗口连接服务端 $ nc 172.19.0.12 8888 连接建立成功后，使用 iptables 将服务端的 FIN 包 drop 点然后 ctrl + c 终止客户端连接，查看统计。\n1 $ sudo iptables -A INPUT -p tcp --sport 8888 --tcp-flags FIN FIN -j DROP 可以看到客户端有 3 条 FIN-WAIT-2 的 socket，但 orphaned 计数为 0。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 $ ss -ant | grep -E \u0026#34;Recv|:8888\u0026#34; \u0026amp;\u0026amp; ss -s State Recv-Q Send-Q Local Address:Port Peer Address:Port Process ESTAB 0 0 172.19.0.15:42190 172.19.0.12:8888 ESTAB 0 0 172.19.0.15:42174 172.19.0.12:8888 ESTAB 0 0 172.19.0.15:41654 172.19.0.12:8888 Total: 222 TCP: 27 (estab 15, closed 6, orphaned 0, timewait 6) Transport Total IP IPv6 RAW\t1 0 1 UDP\t9 7 2 TCP\t21 20 1 INET\t31 27 4 FRAG\t0 0 0 # ubuntu @ node3 in ~ [15:46:37] $ ss -ant | grep -E \u0026#34;Recv|:8888\u0026#34; \u0026amp;\u0026amp; ss -s State Recv-Q Send-Q Local Address:Port Peer Address:Port Process FIN-WAIT-2 0 0 172.19.0.15:42190 172.19.0.12:8888 FIN-WAIT-2 0 0 172.19.0.15:42174 172.19.0.12:8888 FIN-WAIT-2 0 0 172.19.0.15:41654 172.19.0.12:8888 Total: 219 TCP: 29 (estab 12, closed 10, orphaned 0, timewait 10) 我们结合源码来分析下内核对 FIN-WAIT-2 状态 socket 的处理。\n1. FIN-WAIT-2 状态下的 0 延迟关闭，不计入 orphan 当 socket 处于 FIN-WAIT-2 状态并且设置了零延迟关闭时，会直接将 socket 设置为 TCP_CLOSE 状态，并发送 RST 报文，然后销毁 socket，最终减少 orphan socket 计数。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 // 源码地址：https://elixir.bootlin.com/linux/v5.15.130/source/net/ipv4/tcp.c#L2858 static void tcp_close(struct sock *sk, long timeout) if (sk-\u0026gt;sk_state == TCP_FIN_WAIT2) { struct tcp_sock *tp = tcp_sk(sk); // 如果 linger2 小于 0，则设置 socket 为 TCP_CLOSE 状态，并发送 RST 报文 if (tp-\u0026gt;linger2 \u0026lt; 0) { tcp_set_state(sk, TCP_CLOSE); tcp_send_active_reset(sk, GFP_ATOMIC); __NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPABORTONLINGER); } else { // 计算 超时时间 const int tmo = tcp_fin_time(sk); if (tmo \u0026gt; TCP_TIMEWAIT_LEN) { inet_csk_reset_keepalive_timer(sk, tmo - TCP_TIMEWAIT_LEN); } else { tcp_time_wait(sk, TCP_FIN_WAIT2, tmo); goto out; } } } 关于零延迟关闭这里多提一点，这个设置指的是当应用调用 close 函数时，如果应用还有数据没有发送完成，则需要等待一定的延迟时间，如果设置了为 0 ，socket 的数据就会被丢弃 掉并向对方发送一个 RST 包来中断连接，因此对于服务端来说最好永远不要设置为 0；但对于探活类的程序则是一种非常好的配置。具体可以参考皓哥的文章从一次经历谈 TIME_WAIT 的那些事。\n2. FIN-WAIT-2 下等待超时，启用 keepalive，计入 orphan 如果没有满足 0 延迟关闭的条件，内核转而会计算 socket 的超时时间，如果大于 60s 则启用 keepalive 定时器，注意这里是没有将 socket 状态设置为 CLOSE 的，因此可以认为超时时间大于 60s 的 FIN-WAIT-2 状态的 socket 被计入 orphan。 否则会执行 tcp_time_wait 代码将 socket 视为 timewait 状态。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 // 源码地址：https://elixir.bootlin.com/linux/v5.15.130/source/net/ipv4/tcp.c#L2866 const int tmo = tcp_fin_time(sk); if (tmo \u0026gt; TCP_TIMEWAIT_LEN) { inet_csk_reset_keepalive_timer(sk, tmo - TCP_TIMEWAIT_LEN); } else { tcp_time_wait(sk, TCP_FIN_WAIT2, tmo); goto out; } // 源码地址：https://elixir.bootlin.com/linux/v5.15.130/source/include/net/tcp.h#L123 #define TCP_TIMEWAIT_LEN (60*HZ) /* how long to wait to destroy TIME-WAIT * state, about 60 seconds\t*/ 这里的超时计算是根据 tcp_fin_timeout 来的，默认是 60s，我们把这个值改大然后在客户端屏蔽掉服务端的 FIN 包后，在查看客户端 FIN-WAIT-2 状态的 socket 就已经被计入 orphan socket了。如果没有设置，则会被计入 timewait 状态。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 # 没有修改时会被认为是 timewait 计数 $ ss -ant | grep -E \u0026#34;Recv|:8888\u0026#34; \u0026amp;\u0026amp; ss -s State Recv-Q Send-Q Local Address:Port Peer Address:Port Process FIN-WAIT-2 0 0 172.19.0.15:48172 172.19.0.12:8888 FIN-WAIT-2 0 0 172.19.0.15:48116 172.19.0.12:8888 FIN-WAIT-2 0 0 172.19.0.15:48130 172.19.0.12:8888 FIN-WAIT-2 0 0 172.19.0.15:48220 172.19.0.12:8888 FIN-WAIT-2 0 0 172.19.0.15:48206 172.19.0.12:8888 FIN-WAIT-2 0 0 172.19.0.15:48194 172.19.0.12:8888 FIN-WAIT-2 0 0 172.19.0.15:48162 172.19.0.12:8888 FIN-WAIT-2 0 0 172.19.0.15:48182 172.19.0.12:8888 FIN-WAIT-2 0 0 172.19.0.15:48126 172.19.0.12:8888 FIN-WAIT-2 0 0 172.19.0.15:48146 172.19.0.12:8888 Total: 221 TCP: 41 (estab 14, closed 16, orphaned 0, timewait 16) # 客户端修改 tcp_fin_timeout sudo sysctl -w net.ipv4.tcp_fin_timeout=120 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 # 查看客户端统计信息 $ ss -ant | grep -E \u0026#34;Recv|:8888\u0026#34; \u0026amp;\u0026amp; ss -s State Recv-Q Send-Q Local Address:Port Peer Address:Port Process FIN-WAIT-2 0 0 172.19.0.15:59016 172.19.0.12:8888 FIN-WAIT-2 0 0 172.19.0.15:58996 172.19.0.12:8888 FIN-WAIT-2 0 0 172.19.0.15:58966 172.19.0.12:8888 FIN-WAIT-2 0 0 172.19.0.15:58936 172.19.0.12:8888 FIN-WAIT-2 0 0 172.19.0.15:58956 172.19.0.12:8888 FIN-WAIT-2 0 0 172.19.0.15:59030 172.19.0.12:8888 FIN-WAIT-2 0 0 172.19.0.15:58940 172.19.0.12:8888 FIN-WAIT-2 0 0 172.19.0.15:58984 172.19.0.12:8888 FIN-WAIT-2 0 0 172.19.0.15:59012 172.19.0.12:8888 FIN-WAIT-2 0 0 172.19.0.15:58982 172.19.0.12:8888 Total: 243 TCP: 37 (estab 14, closed 5, orphaned 11, timewait 5) Transport Total IP IPv6 RAW\t1 0 1 UDP\t9 7 2 TCP\t32 31 1 INET\t42 38 4 FRAG\t0 0 0 # ubuntu @ node3 in ~ [17:33:06] $ sudo netstat -anpo | grep -E \u0026#34;Recv-Q|8888\u0026#34; Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name Timer tcp 0 0 172.19.0.15:59016 172.19.0.12:8888 FIN_WAIT2 - keepalive (13.92/0/0) tcp 0 0 172.19.0.15:58996 172.19.0.12:8888 FIN_WAIT2 - keepalive (13.92/0/0) tcp 0 0 172.19.0.15:58966 172.19.0.12:8888 FIN_WAIT2 - keepalive (13.92/0/0) tcp 0 0 172.19.0.15:58936 172.19.0.12:8888 FIN_WAIT2 - keepalive (13.92/0/0) tcp 0 0 172.19.0.15:58956 172.19.0.12:8888 FIN_WAIT2 - keepalive (13.92/0/0) tcp 0 0 172.19.0.15:59030 172.19.0.12:8888 FIN_WAIT2 - keepalive (13.92/0/0) tcp 0 0 172.19.0.15:58940 172.19.0.12:8888 FIN_WAIT2 - keepalive (13.92/0/0) tcp 0 0 172.19.0.15:58984 172.19.0.12:8888 FIN_WAIT2 - keepalive (13.92/0/0) tcp 0 0 172.19.0.15:59012 172.19.0.12:8888 FIN_WAIT2 - keepalive (13.92/0/0) tcp 0 0 172.19.0.15:58982 172.19.0.12:8888 FIN_WAIT2 - keepalive (13.92/0/0) 可以看到每个 FIN_WAIT_2 状态的 socket 都有一个 keeplive 定时器，并且 orphan 的统计数发生了变化。\n2. orphan socket 过多清理资源 完成了 FIN-FIN_WAIT_2 状态的判断后，如果 Socket 状态还不是 CLOSE，内核会判断当前 orphan 的 socket 数量是否过多，如果过多的话会执行资源清理。内核会根据 net.ipv4.tcp_max_orphans 参数判断 orphan socket 的数量是否过多。计算的时候内核会传一个 shift 参数，取值范围是 0~2，因此内核实际可能会拿当前 orphan 数量的 2 倍甚至 4 倍去做比较。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 // 源码地址：https://elixir.bootlin.com/linux/v5.15.130/source/net/ipv4/tcp.c#L2877 if (sk-\u0026gt;sk_state != TCP_CLOSE) { sk_mem_reclaim(sk); if (tcp_check_oom(sk, 0)) { tcp_set_state(sk, TCP_CLOSE); tcp_send_active_reset(sk, GFP_ATOMIC); __NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPABORTONMEMORY); } else if (!check_net(sock_net(sk))) { /* Not possible to send reset; just close */ tcp_set_state(sk, TCP_CLOSE); } } // 源码地址：https://elixir.bootlin.com/linux/v5.15.130/source/net/ipv4/tcp.c#L2719 static bool tcp_too_many_orphans(int shift) { return READ_ONCE(tcp_orphan_cache) \u0026lt;\u0026lt; shift \u0026gt; READ_ONCE(sysctl_tcp_max_orphans); } 3. 定时器执行资源清理 系统有两个参数和 orphan 有关：\nnet.ipv4.tcp_max_orphans：上面提到的 orphan socket 的最大数量。 net.ipv4.tcp_orphan_retries：该字段控制 FIN 包的最大重传字数，设置默认为 0。如果为 0，内核会重置为 8。 当内核处于 FIN_WAIT 状态时，会有定时器执行包的重传或者探活，此时也会执行上面提到的资源清理操作。此时如果重传次数已到，或者 orphan socket 数量过多，也会执行清理操作。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 // 源码地址：https://elixir.bootlin.com/linux/v5.15.130/source/net/ipv4/tcp_timer.c#L231 static int tcp_write_timeout(struct sock *sk) { // 代码省略 if (sock_flag(sk, SOCK_DEAD)) { const bool alive = icsk-\u0026gt;icsk_rto \u0026lt; TCP_RTO_MAX; // 基于重试次数判断是否 reset retry_until = tcp_orphan_retries(sk, alive); do_reset = alive || !retransmits_timed_out(sk, retry_until, 0); // 判断是否需要释放资源 if (tcp_out_of_resources(sk, do_reset)) return 1; } } static int tcp_out_of_resources(struct sock *sk, bool do_reset) { struct tcp_sock *tp = tcp_sk(sk); int shift = 0; /* If peer does not open window for long time, or did not transmit * anything for long time, penalize it. */ if ((s32)(tcp_jiffies32 - tp-\u0026gt;lsndtime) \u0026gt; 2*TCP_RTO_MAX || !do_reset) shift++; /* If some dubious ICMP arrived, penalize even more. */ if (sk-\u0026gt;sk_err_soft) shift++; // 计算移位（0 - 2），判断 orphan socket 是否过多 if (tcp_check_oom(sk, shift)) { /* Catch exceptional cases, when connection requires reset. * 1. Last segment was sent recently. */ if ((s32)(tcp_jiffies32 - tp-\u0026gt;lsndtime) \u0026lt;= TCP_TIMEWAIT_LEN || /* 2. Window is closed. */ (!tp-\u0026gt;snd_wnd \u0026amp;\u0026amp; !tp-\u0026gt;packets_out)) do_reset = true; if (do_reset) tcp_send_active_reset(sk, GFP_ATOMIC); tcp_done(sk); __NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPABORTONMEMORY); return 1; } } 简要总结 可以用 ss -s 或者 cat /proc/net/sockstat 查看孤儿套接字是否过多。\n根据文档 介绍，每个 orphan socket 大约占用 64KB 内存。\nFIN_WAIT_1、LAST_ACK、CLOSING 状态下的 socket 都可以归类为 orphan socket。\n超时时间小于 60s 的 FIN_WAIT_2 计入 timewait，而大约 60s 的则记为 orphan socket。\n相关参数：net.ipv4.tcp_orphan_retries、net.ipv4.tcp_max_orphans。\nLAST_ACK、FIN_WAIT_1 状态下的连接都在等待回复，因此可以像发起 SYN Flood 那样的攻击，客户端故意断开但不在返回 ACK 信息，这样会导致服务器资源被占用。因此可以适当调整 上面两个参数，减少重传次数以及 orphan 最大数量，从而可以更快的清理掉无用的 socket。\n参考资料 结合案例深入解析orphan socket产生与消亡（一） 结合案例深入解析orphan socket产生与消亡（二） nvcr.io/nvidia/cloud-native/gpu-operator-validator:v24.3.0 10.10.206.201:20443/megaease/nvidia/cloud-native/gpu-operator-validator:v24.3.0\nnvcr.io/nvidia/k8s-device-plugin:v0.15.0-ubi8 10.10.206.201:20443/megaease/nvidia/k8s-device-plugin:v0.15.0-ubi8\nregistry.k8s.io/nfd/node-feature-discovery:v0.15.4 10.10.206.201:20443/megaease/nfd/node-feature-discovery:v0.15.4\nnvcr.io/nvidia/k8s/container-toolkit:v1.15.0-ubuntu20.04 10.10.206.201:20443/megaease/nvidia/k8s/container-toolkit:v1.15.0-ubuntu20.04\nnvcr.io/nvidia/k8s/dcgm-exporter:3.3.5-3.4.1-ubuntu22.04 10.10.206.201:20443/megaease/nvidia/k8s/dcgm-exporter:3.3.5-3.4.1-ubuntu22.04\nnvcr.io/nvidia/cloud-native/k8s-driver-manager:v0.6.8 10.10.206.201:20443/megaease/nvidia/cloud-native/k8s-driver-manager:v0.6.8\nregistry.k8s.io/sig-storage/csi-node-driver-registrar:v2.10.1 10.10.206.201:20443/megaease/sig-storage/csi-node-driver-registrar:v2.10.1\nquay.io/cephcsi/cephcsi:v3.11.0 10.10.206.201:20443/megaease/cephcsi/cephcsi:v3.11.0\nregistry.k8s.io/sig-storage/csi-node-driver-registrar:v2.10.1 10.10.206.201:20443/megaease/sig-storage/csi-node-driver-registrar:v2.10.1\nrook/ceph:v1.14.9 10.10.206.201:20443/megaease/rook/ceph:v1.14.9\n","permalink":"https://zouyingjie.github.io/posts/tcp%E5%AD%A4%E5%84%BFsocket%E7%9A%84%E4%BA%A7%E7%94%9F%E4%B8%8E%E6%B6%88%E4%BA%A1/","summary":"\u003cp\u003e之前在实验中提到了 tcp_max_orphans 和 tcp_orphan_retries 两个参数，我们使用 \u003ccode\u003ess -s\u003c/code\u003e 命令查看当前系统中的 socket 状态也有 orphan 状态的 socket，本篇文章我们就来分析下到底什么情况下的 socket 才会被视为 orphan socket。\u003c/p\u003e","title":"【动手实验】TCP orphan socket 的产生与消亡"},{"content":"本文是基于知识星球程序员踩坑案例分享中的作业进行的复现和总结，借此加深对 TCP 协议的理解， 原文参见TCP 连接的建立和关闭 —— 强烈建议新手看看。\n实验环境 这里使用两台位于同一子网的腾讯云服务器，IP 分别是 node2（172.19.0.12）和 node3（172.19.0.15），内核版本均为 5.15.0-130-generic。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 # node02 $ uname -a Linux node2 5.15.0-130-generic #140-Ubuntu SMP Wed Dec 18 17:59:53 UTC 2024 x86_64 x86_64 x86_64 GNU/Linux $ ip -4 addr 1: lo: \u0026lt;LOOPBACK,UP,LOWER_UP\u0026gt; mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000 inet 127.0.0.1/8 scope host lo valid_lft forever preferred_lft forever 2: eth0: \u0026lt;BROADCAST,MULTICAST,UP,LOWER_UP\u0026gt; mtu 8500 qdisc mq state UP group default qlen 1000 altname enp0s5 altname ens5 inet 172.19.0.12/20 metric 100 brd 172.19.15.255 scope global eth0 valid_lft forever preferred_lft forever # node03 $ uname -a Linux node3 5.15.0-130-generic #140-Ubuntu SMP Wed Dec 18 17:59:53 UTC 2024 x86_64 x86_64 x86_64 GNU/Linux $ ip -4 addr 1: lo: \u0026lt;LOOPBACK,UP,LOWER_UP\u0026gt; mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000 inet 127.0.0.1/8 scope host lo valid_lft forever preferred_lft forever 2: eth0: \u0026lt;BROADCAST,MULTICAST,UP,LOWER_UP\u0026gt; mtu 8500 qdisc mq state UP group default qlen 1000 altname enp0s5 altname ens5 inet 172.19.0.15/20 metric 100 brd 172.19.15.255 scope global eth0 valid_lft forever preferred_lft forever 启动服务 首先我们使用 nc(netcat) 作为服务端，在 node2 监听 9527 端口：\n1 2 # ubuntu @ node2 in ~ [10:40:58] $ nc -k -l 172.19.0.12 9527 该命令表示在 IP 地址 172.19.0.12 的 9527 端口上持续监听（等待连接并接收数据）。参数含义如下：\n-k\t保持连接（Keep Listening），在客户端断开后继续监听端口。 -l\t监听模式（Listen Mode），启动服务器等待连接。 启动成功后用 netstat 命令查看 socket 的连接状态：\n1 2 3 $ sudo netstat -anpo | grep Recv-Q; sudo netstat -anpo | grep 9527 Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name Timer tcp 0 0 172.19.0.12:9527 0.0.0.0:* LISTEN 13504/nc off (0.00/0/0) netstat 命令的各个参数含义如下：\n-a\t显示所有连接和监听的套接字。 -n\t显示 IP 地址和端口号，不解析主机名。 -o\t显示进程 ID（PID）和计时器信息。 -p\t显示进程名称。 可以看到 9527 端口处于 LISTEN 状态，表示正在监听端口，等待连接请求。\n连接建立 在客户端请求 node2 之前，我们先在 node2 开启抓包：\n1 2 3 # ubuntu @ node2 in ~ [10:38:33] $ sudo tcpdump -s0 -X -nn \u0026#34;tcp port 9527\u0026#34; -w tcp.pcap --print tcpdump: listening on eth0, link-type EN10MB (Ethernet), snapshot length 262144 bytes 命令各个参数含义为：\n-s0\t捕获完整数据包（默认 -s 只抓取前 68/96 字节），0 代表不截断。 -X\t以十六进制（hex）+ ASCII 格式打印数据包内容。 -nn\t不解析主机名和端口（-n 不解析 IP，-nn 也不解析端口）。 \u0026quot;tcp port 9527\u0026quot;\t仅捕获 TCP 端口 9527 的流量。 -w tcp.pcap\t将捕获的数据包写入 tcp.pcap 文件（可用 wireshark 或 tcpdump -r tcp.pcap 查看）。 --print\t同时在终端打印数据包内容（类似 -X，但 \u0026ndash;print 仅在 -w 选项启用时生效）。 接下来我们在 node3 上使用 nc 连接 node2 的 9527 端口：\n1 2 # ubuntu @ node3 in ~ [10:41:48] $ nc 172.19.0.12 9527 然后分别在 node2 和 node3 上使用 netstat 命令查看 socket 的连接状态：\n1 2 3 4 5 6 7 8 9 10 11 # node2 $ sudo netstat -anpo | grep -E \u0026#34;Recv-Q|9527\u0026#34; Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name Timer tcp 0 0 172.19.0.12:9527 0.0.0.0:* LISTEN 13504/nc off (0.00/0/0) tcp 0 0 172.19.0.12:9527 172.19.0.15:48868 ESTABLISHED 13504/nc off (0.00/0/0) # node3 $ sudo netstat -anpo | grep -E \u0026#34;Recv-Q|9527\u0026#34; Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name Timer tcp 0 0 172.19.0.15:48868 172.19.0.12:9527 ESTABLISHED 17255/nc off (0.00/0/0) 可以看到 node2 和 node3 中都有一条端口为 9527 处于 ESTABLISHED 状态的连接，表示连接已建立。 tcpdump 命令也会输出三次握手的数据包详情。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 $ sudo tcpdump -s0 -X -nn \u0026#34;tcp port 9527\u0026#34; -w tcp.pcap --print tcpdump: listening on eth0, link-type EN10MB (Ethernet), snapshot length 262144 bytes 10:54:13.960797 IP 172.19.0.15.48868 \u0026gt; 172.19.0.12.9527: Flags [S], seq 2713301685, win 59220, options [mss 8460,sackOK,TS val 2002430584 ecr 0,nop,wscale 7], length 0 0x0000: 4500 003c 3a31 4000 4006 a849 ac13 000f E..\u0026lt;:1@.@..I.... 0x0010: ac13 000c bee4 2537 a1b9 b2b5 0000 0000 ......%7........ 0x0020: a002 e754 92b3 0000 0204 210c 0402 080a ...T......!..... 0x0030: 775a aa78 0000 0000 0103 0307 wZ.x........ 10:54:13.960874 IP 172.19.0.12.9527 \u0026gt; 172.19.0.15.48868: Flags [S.], seq 3309498602, ack 2713301686, win 59136, options [mss 8460,sackOK,TS val 556655863 ecr 2002430584,nop,wscale 7], length 0 0x0000: 4500 003c 0000 4000 4006 e27a ac13 000c E..\u0026lt;..@.@..z.... 0x0010: ac13 000f 2537 bee4 c542 f0ea a1b9 b2b6 ....%7...B...... 0x0020: a012 e700 5870 0000 0204 210c 0402 080a ....Xp....!..... 0x0030: 212d e4f7 775a aa78 0103 0307 !-..wZ.x.... 10:54:13.961020 IP 172.19.0.15.48868 \u0026gt; 172.19.0.12.9527: Flags [.], ack 1, win 463, options [nop,nop,TS val 2002430584 ecr 556655863], length 0 0x0000: 4500 0034 3a32 4000 4006 a850 ac13 000f E..4:2@.@..P.... 0x0010: ac13 000c bee4 2537 a1b9 b2b6 c542 f0eb ......%7.....B.. 0x0020: 8010 01cf 05fa 0000 0101 080a 775a aa78 ............wZ.x 0x0030: 212d e4f7 !-.. 三次握手抓包 \u0026amp; TCP 协议头解析 我们将抓包文件拖入 Wireshark 中来分析三次握手的过程。\n首先回顾下 TCP 协议头格式：\n图片来自 TCP/IP Reference\n像序列号、端口信息、FLAG 等字段都比较熟悉了，我们这里重点看下 Options 的各个字段，完整的 Option 字段可以参考 Transmission Control Protocol (TCP) Parameters，这里我们只关注包中出现的最常见的几个字段：\nMSS（Maximum Segment Size） 该字段只能在 SYN 包中，用来告知对方自己可以接收的最大数据包，这里指的是 TCP 包中 data 的大小，不包含 TCP 头数据。RFC 6691 中规定了 MSS 的值为 MTU 减去 IP 固定头大小（20 字节）和 TCP 固定头大小（20字节），不包含任何 Option 字段。从 ip -4 addr 命令中可以看到网卡的 MTU 大小为 8500，因此 MSS 大小为 8500 - 20 - 20 = 8460，和抓包中显示的 MSS 大小一致。 1 2 3 4 5 # ubuntu @ node3 in ~ [10:41:48] $ ip -4 addr ... 2: eth0: \u0026lt;BROADCAST,MULTICAST,UP,LOWER_UP\u0026gt; mtu 8500 qdisc mq state UP group default qlen 1000 ... SACK（Selective Acknowledgment） 选择性确认。在 RFC 2018 确定的机制，必须在握手时确认是否支持。TCP 最开始是按顺序响应的，比如有 1、2、3、4 共 4 个包，如果 2 没有收到，那即使 3、4 收到了也不会响应 ACK，这可能导致客户端不断重传 3、4 号包，对网络造成不必要的负载。SACK 解决了这一问题，可以让服务端响应 3、4 包，客户端只需要重传 2 号包就可以了。 图片来自 TCP/IP Guide\n在 Linux 内核中，使用 net.ipv4.tcp_sack 参数来控制是否开启 SACK ，默认开启。\n1 2 $ sysctl net.ipv4.tcp_sack net.ipv4.tcp_sack = 1 TS（Timestamp） 时间戳标记。内核用来计算 RTT（Round-Trip Time），即数据包从发送端到接收端的时间。在内核中可以使用 net.ipv4.tcp_timestamps 参数来控制是否开启该选项。 1 2 $ sysctl net.ipv4.tcp_timestamps net.ipv4.tcp_timestamps = 1 NOP（No Operation） NOP 一般用来占位对齐，因为 TCP 头大小必须是 4 字节的倍数。因此当 TCP 固定头 + Option 字段长度不为 4 字节的倍数时，一般会填充 NOP 字段。\nWScale（Window Scale） 窗口缩放因子。TCP 的 window 窗口字段大小是 16bit，其最大值为 65536 ，也就是说 TCP 包能传输的最大数据为 65536 byte / 1024 = 64KB。在硬件设备和网络如此发达的今天，这个窗口大小显然有点太小了，为此 RFC 7323 中提出了 WScale 选项，用来扩展 window 字段的大小。\nWScale Option 中有 shift.count 值，顾名思义就是移位数，表示 2 的多少次方，虽然 shift.count 占了 1 个字节，但 RFC 规定只能使用后 4 位，其最大值为 1110，也就是 14。结合最大 window 值为 64KB，在 WScale 的帮助下，最大窗口大小可以达到 64KB * （2^14） = 1048576KB = 1GB。\n在我们的抓包中，可以看到 WScale 选项的值为 7，因此 window * (2^7) 才是真正的 window 大小。\n需要注意的是，WScale 只会在携带这个选项的包之后生效，因此发送第一个 SYN 包时是没有生效的，在第三次握手时该选项才生效，可以看到 window 值为 463，而计算后的 window 值为 463 * (2^7) = 463 * 128 = 59264，和 Wireshark 中显示的 window 值一致。\n在 Linux 内核中，可以通过 net.ipv4.tcp_window_scaling 参数来控制是否开启 WScale 选项。\n1 2 $ sysctl net.ipv4.tcp_window_scaling net.ipv4.tcp_window_scaling = 1 SYN-SENT 状态抓包 前文抓包我们看到的是 LISTEN 和 ESTABLISHED 状态的 socket，除了这两种状态，连接建立时客户端、服务端还会分别经历 SYN-SENT 和 SYN-RECV 状态。\n图片来自 TCP/IP Guide\n这里通过 iptables 拦截握手包来看下 SYN-SENT 和 SYN-RECV 状态的 socket，首先在 node2 上使用 iptables 规则，将访问 9527 的端口包丢弃掉，命令如下：\n1 sudo iptables -A INPUT -p tcp --dport 9527 -j DROP 然后在 node3 再次执行 nc 命令连接服务，这次带上参数 -w 3600，表示连接超时时间为 3600 秒，命令如下：\n1 nc -w 3600 172.19.0.12 9527 请求发出后，tcpdump 抓包会打印 SYN 包和后续的重传包，用 Wireshark 打开抓包文件：\n可以看到 SYN 包一共有 6 次重传，共传了 7 个包。Linux 的 SYN 最大重传次数是由内核参数 net.ipv4.tcp_syn_retries 控制的，默认值为 6。\n1 2 3 # ubuntu @ node3 in ~ [16:26:35] C:130 $ sysctl net.ipv4.tcp_syn_retries net.ipv4.tcp_syn_retries = 6 重传的超时 RTO 时间初始值通常在 1s 左右，按照指数级增长，因此重传时间间隔大约为 1s、2s、4s、8s、16s、32s。从抓包中也可以看到，在 1.02，3.03，7.726s，15.15，31.58，65.11s 发生了重传，因此默认情况下，一个 TCP 连接建立的超时时间会大于 64s。\n在重传期间，查看客户端的 netstat 信息可以看到 SYN-SENT 状态的 socket，表示连接正在等待 SYN 包的响应。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 # ubuntu @ node3 in ~ [16:26:35] C:130 $ while true; do sudo netstat -anpo | grep 9527; sleep 1; done tcp 0 1 172.19.0.15:44004 172.19.0.12:9527 SYN_SENT 3066236/nc on (0.77/0/0) tcp 0 1 172.19.0.15:44004 172.19.0.12:9527 SYN_SENT 3066236/nc on (1.78/1/0) tcp 0 1 172.19.0.15:44004 172.19.0.12:9527 SYN_SENT 3066236/nc on (0.76/1/0) tcp 0 1 172.19.0.15:44004 172.19.0.12:9527 SYN_SENT 3066236/nc on (3.76/2/0) tcp 0 1 172.19.0.15:44004 172.19.0.12:9527 SYN_SENT 3066236/nc on (2.74/2/0) tcp 0 1 172.19.0.15:44004 172.19.0.12:9527 SYN_SENT 3066236/nc on (1.72/2/0) tcp 0 1 172.19.0.15:44004 172.19.0.12:9527 SYN_SENT 3066236/nc on (0.70/2/0) tcp 0 1 172.19.0.15:44004 172.19.0.12:9527 SYN_SENT 3066236/nc on (7.88/3/0) tcp 0 1 172.19.0.15:44004 172.19.0.12:9527 SYN_SENT 3066236/nc on (6.86/3/0) tcp 0 1 172.19.0.15:44004 172.19.0.12:9527 SYN_SENT 3066236/nc on (5.84/3/0) tcp 0 1 172.19.0.15:44004 172.19.0.12:9527 SYN_SENT 3066236/nc on (4.82/3/0) tcp 0 1 172.19.0.15:44004 172.19.0.12:9527 SYN_SENT 3066236/nc on (3.80/3/0) tcp 0 1 172.19.0.15:44004 172.19.0.12:9527 SYN_SENT 3066236/nc on (2.78/3/0) tcp 0 1 172.19.0.15:44004 172.19.0.12:9527 SYN_SENT 3066236/nc on (1.76/3/0) tcp 0 1 172.19.0.15:44004 172.19.0.12:9527 SYN_SENT 3066236/nc on (0.75/3/0) tcp 0 1 172.19.0.15:44004 172.19.0.12:9527 SYN_SENT 3066236/nc on (15.92/4/0) tcp 0 1 172.19.0.15:44004 172.19.0.12:9527 SYN_SENT 3066236/nc on (14.90/4/0) tcp 0 1 172.19.0.15:44004 172.19.0.12:9527 SYN_SENT 3066236/nc on (13.88/4/0) tcp 0 1 172.19.0.15:44004 172.19.0.12:9527 SYN_SENT 3066236/nc on (12.86/4/0) tcp 0 1 172.19.0.15:44004 172.19.0.12:9527 SYN_SENT 3066236/nc on (11.84/4/0) tcp 0 1 172.19.0.15:44004 172.19.0.12:9527 SYN_SENT 3066236/nc on (10.83/4/0) tcp 0 1 172.19.0.15:44004 172.19.0.12:9527 SYN_SENT 3066236/nc on (9.81/4/0) tcp 0 1 172.19.0.15:44004 172.19.0.12:9527 SYN_SENT 3066236/nc on (8.79/4/0) tcp 0 1 172.19.0.15:44004 172.19.0.12:9527 SYN_SENT 3066236/nc on (7.77/4/0) tcp 0 1 172.19.0.15:44004 172.19.0.12:9527 SYN_SENT 3066236/nc on (6.75/4/0) tcp 0 1 172.19.0.15:44004 172.19.0.12:9527 SYN_SENT 3066236/nc on (5.73/4/0) tcp 0 1 172.19.0.15:44004 172.19.0.12:9527 SYN_SENT 3066236/nc on (4.71/4/0) tcp 0 1 172.19.0.15:44004 172.19.0.12:9527 SYN_SENT 3066236/nc on (3.70/4/0) tcp 0 1 172.19.0.15:44004 172.19.0.12:9527 SYN_SENT 3066236/nc on (2.68/4/0) tcp 0 1 172.19.0.15:44004 172.19.0.12:9527 SYN_SENT 3066236/nc on (1.65/4/0) tcp 0 1 172.19.0.15:44004 172.19.0.12:9527 SYN_SENT 3066236/nc on (0.64/4/0) tcp 0 1 172.19.0.15:44004 172.19.0.12:9527 SYN_SENT 3066236/nc on (31.74/5/0) 最后一列是 Timer 计时器，格式为 timer(a/b/c)，timer取值有四种\non： 超时计时器 off： 没有计时器 keepalive： keepalive 计时器 timewait： TIME_WAIT 计时器 对于超时计时器，a 表示当前计时器剩余时间，b 表示当前计时器重传次数，c 表示已发送的保活探测次数，比如命令中一行时 (1.72/2/0)，1.72 表示在等 1.72 秒进行重传，2 表示已经重传了两次。\nnode2 使用 iptables 屏蔽了所有 9527 端口的包，因此不会有任何 socket 信息。\nSYN-RECV 状态抓包 我们在修改下 node3 的 iptables 规则，将源端口为 9527 的包丢弃掉，命令如下：\n1 2 # --sport 9527 表示源端口为 9527 的包被匹配，也就是 node2 发来的 ACK 包会被拦截 sudo iptables -A INPUT -p tcp --sport 9527 -j DROP 为了避免 SYN 重传，这里使用 namp 命令执行访问，命令如下：\n1 sudo nmap -sS 172.19.0.12 -p 9527 node02 抓包如下：\n可以看到 SYN-ACK 重传了 5 次，这是由内核参数 net.ipv4.tcp_synack_retries 控制的，默认值为 5，重传时间也是从 1s 开始逐渐翻倍，成指数级增长。\n1 2 $ sysctl net.ipv4.tcp_synack_retries net.ipv4.tcp_synack_retries = 5 重传过程中，查看 node2 的 netstat 信息可以看到 SYN-RECV 状态的 socket，表示连接正在等待 SYN-ACK 包的响应。\n1 2 3 4 5 6 7 8 9 $ $ while true; do sudo netstat -anpo | grep SYN_RECV; sleep 1; done tcp 0 0 172.19.0.12:9527 172.19.0.15:48803 SYN_RECV - on (1.24/1/0) tcp 0 0 172.19.0.12:9527 172.19.0.15:48803 SYN_RECV - on (0.22/1/0) tcp 0 0 172.19.0.12:9527 172.19.0.15:48803 SYN_RECV - on (3.22/2/0) tcp 0 0 172.19.0.12:9527 172.19.0.15:48803 SYN_RECV - on (2.20/2/0) tcp 0 0 172.19.0.12:9527 172.19.0.15:48803 SYN_RECV - on (1.18/2/0) tcp 0 0 172.19.0.12:9527 172.19.0.15:48803 SYN_RECV - on (0.16/2/0) tcp 0 0 172.19.0.12:9527 172.19.0.15:48803 SYN_RECV - on (7.34/3/0) tcp 0 0 172.19.0.12:9527 172.19.0.15:48803 SYN_RECV - on (6.32/3/0) SYN Flood 攻击 上面实验可以看到在 SYN-ACK 包重传期间，始终有 socket 在占用服务器的资源。如果有恶意攻击者不断发送 SYN 包，同时拒绝接收 SYN-ACK 或者故意不响应 ACK，服务器就会有大量处于 SYN-RECV 状态的连接消耗资源。这里的原理是在三次握手时，Linux 内核维护了半连接队列（SYN Queue）和全连接队列（Accept Queue），大量 SYN-RECV 状态的 socket 会占满 SYN Queue 队列，导致服务器无法正常处理新的 SYN 包，这就是 SYN Flood 攻击。\nLinux 内核提供了 net.ipv4.tcp_syncookies 参数来应对 SYN Flood 攻击，当该参数开启时，如果队列已满，内核会计算一个 Cookies 值作为 SYN-ACK 包的序列号返回，客户端收到后会在 ACK 中使用 Cookie+1 作为序列号进行响应，服务端只有在检查 ACK 包的序列号正确后才会建立连接。这样如果有 SYN Flood 攻击，服务端每次都只计算 cookie 进行响应，不会真的占用半连接队列，从而达到服务拒绝的目的。\n关于半连接队列全连接队列的更详细介绍可以以参考笔者的另一篇文章 【动手实验】TCP半连接队列、全连接队列实战分析，这里不在赘述。\nPS：原实验用了 nc 验证 SYN Queue 的队列长度，但笔者在做实验时发现 nc 的 SYN-Queue 默认长度为 1，无法复现实验中的效果。\n在 ChatGPT 帮助下了解到，对于网络 socket 来说，nc 在调用 listen 时，默认的 backlog 长度为 1，因此无法复现实验中的效果。查看 nc 的源码也可以验证这一点。因此如果要做类似的实验，最好用其他工具，比如 Python、Go 等语言做服务端。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 // 源码地址 // https://github.com/openbsd/src/blob/d800967ee04b1c92ceefa78494d0ff66606a806d/usr.bin/nc/netcat.c#L1072 /* * local_listen() * Returns a socket listening on a local port, binds to specified source * address. Returns -1 on failure. */ int local_listen(const char *host, const char *port, struct addrinfo hints) { // 代码省略 if (!uflag \u0026amp;\u0026amp; s != -1) { // 调用 listen 时，默认的 backlog 长度为 1 if (listen(s, 1) == -1) err(1, \u0026#34;listen\u0026#34;); } // 代码省略 return s; } 为什么需要三次握手？ 实验完成了这里多扯一句三次握手的目的，网络上的资料大部分都会提到三次握手的目的是客户端、服务端同步序列号、窗口大小、MSS、SACK 等信息，其实这部分在前两次握手就已经完成了。三次握手最重要的原因在 RFC 里写的很清楚，主要是为了是防止历史的重复连接初始化造成的混乱问题，防止使用 TCP 协议通信的双方建立了错误的连接。。\nThe principal reason for the three-way handshake is to prevent old duplicate connection initiations from causing confusion.\nRFC9293\nTCP 是半开、全双工通信的，通信双方要互相建立连接才行，所谓的三次握手本质上是完成四步操作：\n客户端请求建立连接；服务端响应确认。 服务端请求建立连接，客户端响应确认。 我们将主动请求建立连接的一方称为发起方，被动建立连接的一方称为接收方。连接建立时，发起方的 SYN 请求和接收方的 ACK 响应是必不可少的，但除此之外，发起方必须要确认接收方的的响应是否正确，因此 TCP 引入了三次握手和 RST 机制来完成这一确认操作：\n如果接收方响应正确。则接收方发送 ACK 消息，完成正常的第三次握手。 如果接收方响应错误，则接收方发送 RST 消息中断连接。 无论发送方返回 ACK 还是 RST 消息，都至少需要一次发起方到接收方的通信，这才是三次握手最重要的目的。\n下面是 RFC9293 中的例子：\n图片来自：为什么 TCP 建立连接需要三次握手\n客户端发送第一次 SYN 后响应超时，又发送了一次 SYN，然而服务端响应了首次的 SYN，客户端收到 ACK 后检查到序列号不对，此时返回 RST 包中断连接，然后重新执行三次握手过程。\n连接关闭 分析完了 TCP 连接建立的过程，我们再来分析下 TCP 连接关闭的过程。我们继续使用 nc 作为工具，首先启动服务端和客户端。\n1 2 3 4 5 # node2 使用 nc 启动服务端 $ nc -k -l 172.19.0.12 9527 # node3 使用 nc 启动客户端 $ nc 172.19.0.12 9527 完成后查看服务端和客户端的状态信息：\n1 2 3 4 5 6 7 8 9 10 # node2 服务端 $ ss -atnp | grep -E \u0026#34;Recv-Q|9527\u0026#34; State Recv-Q Send-Q Local Address:Port Peer Address:Port Process LISTEN 0 1 172.19.0.12:9527 0.0.0.0:* users:((\u0026#34;nc\u0026#34;,pid=147133,fd=3)) ESTAB 0 0 172.19.0.12:9527 172.19.0.15:42526 users:((\u0026#34;nc\u0026#34;,pid=147133,fd=4)) # node3 客户端 $ ss -atnp | grep -E \u0026#34;Recv-Q|9527\u0026#34; State Recv-Q Send-Q Local Address:Port Peer Address:Port Process ESTAB 0 0 172.19.0.15:42526 172.19.0.12:9527 users:((\u0026#34;nc\u0026#34;,pid=149072,fd=3)) 正常关闭 我们首先在 node2 执行抓包，然后在客户端按照 ctrl+c 关闭连接，然后执行 netstat 命令查看服务端的状态信息：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 # node2 抓包 $ sudo tcpdump -s0 -X -nn \u0026#34;tcp port 9527\u0026#34; -w tcp-handshake-03.pcap --print tcpdump: listening on eth0, link-type EN10MB (Ethernet), snapshot length 262144 bytes # node2 服务端 Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name Timer tcp 0 0 172.19.0.12:9527 0.0.0.0:* LISTEN 147133/nc off (0.00/0/0) tcp 0 0 172.19.0.12:9527 172.19.0.15:41492 ESTABLISHED 147133/nc off (0.00/0/0) # node3 客户端 $ sudo netstat -anpo | grep Recv-Q; sudo netstat -anpo | grep 9527 Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name Timer tcp 0 0 172.19.0.15:41492 172.19.0.12:9527 TIME_WAIT - timewait (58.92/0/0) $ sudo netstat -anpo | grep Recv-Q; sudo netstat -anpo | grep 9527 Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name Timer tcp 0 0 172.19.0.15:41492 172.19.0.12:9527 TIME_WAIT - timewait (47.42/0/0) $ sudo netstat -anpo | grep Recv-Q; sudo netstat -anpo | grep 9527 Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name Timer tcp 0 0 172.19.0.15:41492 172.19.0.12:9527 TIME_WAIT - timewait (36.56/0/0) $ sudo netstat -anpo | grep Recv-Q; sudo netstat -anpo | grep 9527 Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name Timer tcp 0 0 172.19.0.15:41492 172.19.0.12:9527 TIME_WAIT - timewait (24.22/0/0) 抓包结果如图：\n我们来简要分析下上述过程：\n连接断开的很快，从抓包结果可以看出耗时大约 0.0019s，因此服务端执行 netstat 已经查不到连接了。\n四次握手只有 3 个包，因为服务端没有数据需要处理，所以在对客户端的 FIN 进行 ACK 时，把 FIN 也捎带上了。\n客户端收到了服务端的 FIN 并发送了 ACK 后进入 TIME_WAIT 状态，从 netstat 输出结果看有一个 60s 的timewait 定时器正在执行。\n关于 TIME_WAIT 我们来做进一步的分析。\nTIME_WAIT 状态处理 TIME_WAIT 主要是为了解决两个问题：\n防止前一个连接的延迟发送的 Segment 被使用相同四元组的连接接收。 我们看下面图中的例子，第一个连接服务端发送的 SEQ=3 因为某些原因丢失，服务端执行了重传后客户端接收并断开了连接进入 TIME_WAIT 状态。此时如果 TIME_WAIT 时间过短，很快又和服务端建立了另一个使用相同四元组的连接，而此时之前丢失的 SEQ=3 包又发送来了，造成 TCP 状态的紊乱。\n图片来源：Coping with the TCP TIME-WAIT state on busy Linux servers\n确保远端已经关闭连接 当被动关闭的一方发送 FIN 后会进入 LAST_ACK 状态等待对端的 ACK。如果没有 TIME_WAIT 状态，服务端处于 LAST_ACK 状态时，客户端可能会使用相同的四元组来新建连接，因为新的连接会使用新的序列号，与之前的不匹配，服务端会认为新连接错误，从而返回 RST 包中止连接。\n图片来源：Coping with the TCP TIME-WAIT state on busy Linux servers\nTIME_WAIT 状态的 socket 本身也会带来问题，主要是端口占用，可能导致服务器无法建立新的连接。TIME_WAIT 状态只会在主动断开连接的一方出现，在收到对方的 FIN 包后进入该状态。Linux 默认的 MSL（Maximum Segment Lifetime, 最大报文生存时间） 为 30s，因此默认的 TIME_WAIT 时间为 2* MSL = 60s。在这期间端口是一直被占用的，服务器是根据四元组来识别 socket 的，因此在这 1 分钟内，服务器不能在建立相同的连接。\nLinux 开放给应用使用的端口大约在 3 万个左右，受 sysctl net.ipv4.ip_local_port_range 的影响。假设我们可以使用全部的可用接口，在大量连接执行正常断开的流程下，我们只能支持每秒 500 条连接建立。但实际情况是服务端程序往往只会监听若干固定端口，并且收到的流量可能是通过几台 LoadBalancer 转发过来的，因此实际能支持的四元组数量是有限的。\n1 2 $ sysctl net.ipv4.ip_local_port_range net.ipv4.ip_local_port_range = 32768\t60999 Linux 内核提供了以下参数来影响 TIME_WAIT 状态的处理：\nnet.ipv4.ip_local_port_range：可以通过该参数来扩大可用端口范围，让服务器可以创建更多的连接。\nnet.ipv4.tcp_timestamps：RFC 7323 引入的时间戳机制。其定义了一个 Timestamp 的 option，包含两个值：value，发送方当前的时间戳；echo，对端响应的最新时间戳。\nnet.ipv4.tcp_tw_recycle：开启后，如果某个远端发来的包的时间戳，小于上次发过来的时间戳，会将这些包丢掉。开启后理论上是可以解决上面提到的第一个问题的，旧的包发来时被发现其 timestamp 小于新连接发来的 timestamp，会被丢掉。但理想很丰满，现实很骨感，该参数要求 timestamp 必须是单调递增的。这在 LB/NAT 环境下是无法得到保证的，因为无法共享时间戳时钟。在 4.12 版本之后该配置已经被移除，因此在生产环境中，任何情况下都不在建议开启这个选项。\nnet.ipv4.tcp_tw_reuse: 将处于 TIME_WAIT 状态的 socket 用于主动建立新的 socket 连接，其允许内核复用超过 1s 的 TIME_WAT socket 被复用（仅适用于主动建立连接，被动建立连接的一方这个选项没啥用）。\nnet.ipv4.tcp_max_tw_buckets：内核允许的状态为 TIME_WAIT 的最大连接数。超过该数字后，新的 TIME_WAIT 会被立即销毁。\n1 2 3 4 5 $ sysctl net.ipv4.tcp_timestamps net.ipv4.tcp_tw_reuse net.ipv4.tcp_max_tw_buckets net.ipv4.tcp_tw_recycle net.ipv4.tcp_timestamps = 1 net.ipv4.tcp_tw_reuse = 2 # 0 - disable, 1 - global enable, 2 - enable for loopback traffic only net.ipv4.tcp_max_tw_buckets = 4096 sysctl: cannot stat /proc/sys/net/ipv4/tcp_tw_recycle: No such file or directory 下面做实验来看下上述参数的效果：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 # 服务端 启动服务 $ nc -k -l 172.19.0.12 9527 # 客户端程序 # 不断打开并关闭连接 import socket def connect_and_immediately_disconnect(host, port, count): try: for i in range(count): cli = socket.socket(socket.AF_INET, socket.SOCK_STREAM) cli.connect((host, port)) cli.close() except Exception as e: print(f\u0026#34;Failed to connect: {e}\u0026#34;) if __name__ == \u0026#39;__main__\u0026#39;: connect_and_immediately_disconnect(\u0026#39;172.19.0.12\u0026#39;, 9527, 70000) 先将 tcp_max_tw_buckets 调到 100 万，执行程序，结果如下，最终客户端报了 Cannot assign requested address 表示没有地址可用，客户端有 28232 个 TIME_WAIT 状态的 socket，与 ip_local_port_range 的计算范围一致。\n1 2 3 4 5 6 7 8 $ sysctl net.ipv4.ip_local_port_range net.ipv4.ip_local_port_range = 32768\t60999 $ sudo netstat -anpo | grep 9527 | grep timewait | wc -l 28232 $ python3 client.py Failed to connect: [Errno 99] Cannot assign requested address 现在我们将 net.ipv4.tcp_tw_reuse 设置为全局有效后再次执行程序，可以看到 TIME_WAIT 状态的数量会稳定在一万多条，客户端没有报错，执行完成后正常退出。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 # 修改客户端 tcp_tw_reuse sudo sysctl -w net.ipv4.tcp_tw_reuse=1 $ sudo netstat -anpo | grep 9527 | grep timewait | wc -l 10967 $ sudo netstat -anpo | grep 9527 | grep timewait | wc -l 13921 $ sudo netstat -anpo | grep 9527 | grep timewait | wc -l 14070 $ sudo netstat -anpo | grep 9527 | grep timewait | wc -l 14070 我们将 tcp_tw_reuse 关闭并调整 tcp_max_tw_buckets 为 5000 重复实验：\n1 $ sudo sysctl -w net.ipv4.tcp_tw_reuse=2 net.ipv4.tcp_max_tw_buckets=5000 再次执行客户端程序后会发现 TIME_WAIT 状态的 socket 不会超过 5000，超过阈值后的 socket 会被清理并统计，可以通过 netstat -s 命令查看：\n1 2 3 4 5 $ sudo netstat -anpo | grep 9527 | grep timewait | wc -l 4996 $ netstat -s | grep TCPTimeWaitOverflow TCPTimeWaitOverflow: 65008 这里我们总结下 TIME_WAIT 的处理：\nTIME_WAIT 是为了保证通信的可靠性而存在的，这也是为什么 Linux 内核不支持修改 60s 限制的原因。 The TIME_WAIT state is our friend and is there to help us (i.e., to let old duplicate segments expire in the network). Instead of trying to avoid the state, we should understand it. \u0026ndash; 《Unix programming》\n在服务端，永远不要开启 net.ipv4.tcp_tw_recycle，新的内核版本已废弃；旧的版本在 LB/NAT 环境下会将正常包丢弃，导致问题。\nnet.ipv4.tcp_tw_reuse 仅对主动断开和发起的一方有效，可以理解为只对客户端有效，服务端大部分都是被动建立连接，因此对其意义不大。\n客户端还可以设置 0 延迟关闭的方式，此时会发送 RST 直接终止连接，不走正常的断开流程，也就不会进入 TIME_WAIT 状态，对于探活类应用非常拥有。但服务端永远不要设置，否则客户端会收到 connnection reset by peer 的错误。\n服务端尽量不要主动断开连接，将 TIME_WAIT 留在客户端，不然会耗费更多的资源，并且调优方式有限。\n如果可以尽量使用长连接的方式。\n上面分析了正常关闭的流程，下面我们再来看下各个状态的情况。\nFIN_WAIT_1 状态 我们使用 iptalbes 拦截第一个 FIN 包，然后看下服务端和客户端的状态信息：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 # node2 $ nc -k -l 172.19.0.12 9527 # node3 $ nc 172.19.0.12 9527 # 在 node2设置规则，将目标端口为 9527 的 FIN 包丢弃 sudo iptables -A INPUT -p tcp --dport 9527 --tcp-flags FIN FIN -j DROP # 在 node2 开启抓包 $ sudo tcpdump -s0 -X -nn \u0026#34;tcp port 9527\u0026#34; -w tcp-handshake-FIN1-01.pcap --print # 在两台服务器执行命令，查看 socket 状态 $ while true; do sudo netstat -anpo | grep 9527; sleep 1; done 命令都执行后，我们在 node3 按下 ctrl+c 关闭连接，查看 node3 的链接可以看到进入了 FIN_WAIT1 状态。node2 因为 FIN 包被丢弃，所以还是 ESTABLISHED 状态。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 $ while true; do sudo netstat -anpo | grep 9527; sleep 1; done tcp 0 0 172.19.0.15:53072 172.19.0.12:9527 ESTABLISHED 546891/nc off (0.00/0/0) tcp 0 0 172.19.0.15:53072 172.19.0.12:9527 ESTABLISHED 546891/nc off (0.00/0/0) tcp 0 1 172.19.0.15:53072 172.19.0.12:9527 FIN_WAIT1 - on (0.36/1/0) tcp 0 1 172.19.0.15:53072 172.19.0.12:9527 FIN_WAIT1 - on (0.17/2/0) tcp 0 1 172.19.0.15:53072 172.19.0.12:9527 FIN_WAIT1 - on (0.80/3/0) tcp 0 1 172.19.0.15:53072 172.19.0.12:9527 FIN_WAIT1 - on (3.08/4/0) ... tcp 0 1 172.19.0.15:53072 172.19.0.12:9527 FIN_WAIT1 - on (0.35/7/0) tcp 0 1 172.19.0.15:53072 172.19.0.12:9527 FIN_WAIT1 - on (0.00/7/0) tcp 0 1 172.19.0.15:53072 172.19.0.12:9527 FIN_WAIT1 - on (51.56/8/0) tcp 0 1 172.19.0.15:53072 172.19.0.12:9527 FIN_WAIT1 - on (50.54/8/0) tcp 0 1 172.19.0.15:53072 172.19.0.12:9527 FIN_WAIT1 - on (49.52/8/0) ... tcp 0 1 172.19.0.15:53072 172.19.0.12:9527 FIN_WAIT1 - on (0.61/8/0) tcp 0 1 172.19.0.15:53072 172.19.0.12:9527 FIN_WAIT1 - on (0.00/8/0) node3 使用 ss -s 命令统计，可以看到有 1 个 orphaned 状态的 socket。\n1 2 3 4 5 6 7 8 9 10 $ ss -s Total: 192 TCP: 10 (estab 6, closed 0, orphaned 1, timewait 0) Transport Total IP IPv6 RAW\t1 0 1 UDP\t6 4 2 TCP\t10 9 1 INET\t17 13 4 FRAG\t0 0 0 下面是抓包结果\n可以看到 FIN 包重传了 8 次，一共发了 9 个包。这个的重传次数是由内核参数 net.ipv4.tcp_orphan_retries 控制的，该参数会控制连接关闭时所有的超时重传次数，默认为 0。其计算逻辑是：\n如果为 0，则重传 8 次 如果不为 0，则重传次数为该参数的值。可以将该值调小来减少重传次数，提高性能。 当超时重传次数达到上限后，内核将连接关闭并清除定时器。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 //源码地址：https://elixir.bootlin.com/linux/v5.15.130/source/net/ipv4/tcp_timer.c#L139 // 如果 net.ipv4.tcp_orphan_retries 是 0，则重传次数为 8。 /** * tcp_orphan_retries() - Returns maximal number of retries on an orphaned socket * @sk: Pointer to the current socket. * @alive: bool, socket alive state */ static int tcp_orphan_retries(struct sock *sk, bool alive) { int retries = READ_ONCE(sock_net(sk)-\u0026gt;ipv4.sysctl_tcp_orphan_retries); /* May be zero. */ /* We know from an ICMP that something is wrong. */ if (sk-\u0026gt;sk_err_soft \u0026amp;\u0026amp; !alive) retries = 0; /* However, if socket sent something recently, select some safe * number of retries. 8 corresponds to \u0026gt;100 seconds with minimal * RTO of 200msec. */ if (retries == 0 \u0026amp;\u0026amp; alive) retries = 8; return retries; } // 源码地址： https://elixir.bootlin.com/linux/v5.15.130/source/net/ipv4/tcp.c#L4450 // 将 state 设置为 TCP_CLOSE 状态，并清除发送定时器 void tcp_done(struct sock *sk) { struct request_sock *req; /* We might be called with a new socket, after * inet_csk_prepare_forced_close() has been called * so we can not use lockdep_sock_is_held(sk) */ req = rcu_dereference_protected(tcp_sk(sk)-\u0026gt;fastopen_rsk, 1); if (sk-\u0026gt;sk_state == TCP_SYN_SENT || sk-\u0026gt;sk_state == TCP_SYN_RECV) TCP_INC_STATS(sock_net(sk), TCP_MIB_ATTEMPTFAILS); tcp_set_state(sk, TCP_CLOSE); tcp_clear_xmit_timers(sk); if (req) reqsk_fastopen_remove(sk, req, false); WRITE_ONCE(sk-\u0026gt;sk_shutdown, SHUTDOWN_MASK); if (!sock_flag(sk, SOCK_DEAD)) sk-\u0026gt;sk_state_change(sk); else inet_csk_destroy_sock(sk); } 这里的控制参数是 tcp_orphan_retries，使用 orphan 代表而不是像连接建立时的参数tcp_syn_retries、tcp_synack_retries，明确按包类型区分。原因是 Linux 将执行关闭 的 socket 视为 orphan（孤儿）socket，处于 FIN-WAIT-1、FIN-WAIT-2、LAST-ACK、CLOSING 状态的 socket 都可能属于 orphan socket。\n内核还有一个参数 net.ipv4.tcp_max_orphans 用来控制 orphan socket 的最大数量。当该状态的 socket 数量超过阈值后，Linux 内核将不会走正常的四次挥手流程，而是直接发送 RST 信息终止连接。\n关于 orphan socket 的详细讨论可以参考笔者另一篇实验 TCP orphan socket 的产生与消亡，这里不再赘述。\nFIN_WAIT_2 状态 我们将 node2 的iptables 清理后，在重启服务端和客户端，然后在 node3 添加 iptables 拦截 node2 发来的 FIN 包。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 # node2 清理 iptables sudo iptables -F # node2 重启服务端和客户端 $ nc -k -l 172.19.0.12 9527 # node3 $ nc 172.19.0.12 9527 # 在 node3 设置规则，将源端口为 9527 的 FIN 包丢弃 sudo iptables -A INPUT -p tcp --sport 9527 --tcp-flags FIN FIN -j DROP # node2 开启抓包 $ sudo tcpdump -s0 -X -nn \u0026#34;tcp port 9527\u0026#34; -w tcp-handshake-FIN2-01.pcap --print # 在两台服务器执行命令，查看 socket 状态 $ while true; do sudo netstat -anpo | grep 9527; sleep 1; done 执行上述命令后，在 node3 按下 ctrl+c 关闭连接，可以看到 node2 服务端进入 LAST_ACK 状态，node3 客户端进入 FIN_WAIT2 状态。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 # node2 服务端 tcp 0 0 172.19.0.12:9527 172.19.0.15:55942 ESTABLISHED 579852/nc off (0.00/0/0) tcp 0 0 172.19.0.12:9527 0.0.0.0:* LISTEN 579852/nc off (0.00/0/0) tcp 0 0 172.19.0.12:9527 172.19.0.15:55942 ESTABLISHED 579852/nc off (0.00/0/0) tcp 0 0 172.19.0.12:9527 0.0.0.0:* LISTEN 579852/nc off (0.00/0/0) tcp 0 1 172.19.0.12:9527 172.19.0.15:55942 LAST_ACK - on (0.18/1/0) tcp 0 1 172.19.0.12:9527 172.19.0.15:55942 LAST_ACK - on (0.00/2/0) tcp 0 1 172.19.0.12:9527 172.19.0.15:55942 LAST_ACK - on (0.62/3/0) tcp 0 1 172.19.0.12:9527 172.19.0.15:55942 LAST_ACK - on (0.27/6/0) tcp 0 1 172.19.0.12:9527 172.19.0.15:55942 LAST_ACK - on (25.62/7/0) tcp 0 1 172.19.0.12:9527 172.19.0.15:55942 LAST_ACK - on (1.32/8/0) tcp 0 1 172.19.0.12:9527 172.19.0.15:55942 LAST_ACK - on (0.30/8/0) tcp 0 1 172.19.0.12:9527 172.19.0.15:55942 LAST_ACK - on (0.00/8/0) tcp 0 0 172.19.0.12:9527 0.0.0.0:* LISTEN 579852/nc off (0.00/0/0) tcp 0 0 172.19.0.12:9527 0.0.0.0:* LISTEN 579852/nc off (0.00/0/0) tcp 0 0 172.19.0.12:9527 0.0.0.0:* LISTEN 579852/nc off (0.00/0/0) tcp 0 0 172.19.0.12:9527 0.0.0.0:* LISTEN 579852/nc off (0.00/0/0) tcp 0 0 172.19.0.12:9527 0.0.0.0:* LISTEN 579852/nc off (0.00/0/0) # node3 客户端 tcp 0 0 172.19.0.15:55942 172.19.0.12:9527 ESTABLISHED 582121/nc off (0.00/0/0) tcp 0 0 172.19.0.15:55942 172.19.0.12:9527 ESTABLISHED 582121/nc off (0.00/0/0) tcp 0 0 172.19.0.15:55942 172.19.0.12:9527 ESTABLISHED 582121/nc off (0.00/0/0) tcp 0 1 172.19.0.15:55942 172.19.0.12:9527 FIN_WAIT1 - on (0.14/0/0) tcp 0 0 172.19.0.15:55942 172.19.0.12:9527 FIN_WAIT2 - timewait (59.12/0/0) tcp 0 0 172.19.0.15:55942 172.19.0.12:9527 FIN_WAIT2 - timewait (58.11/0/0) tcp 0 0 172.19.0.15:55942 172.19.0.12:9527 FIN_WAIT2 - timewait (57.09/0/0) tcp 0 0 172.19.0.15:55942 172.19.0.12:9527 FIN_WAIT2 - timewait (56.07/0/0) tcp 0 0 172.19.0.15:55942 172.19.0.12:9527 FIN_WAIT2 - timewait (55.05/0/0) tcp 0 0 172.19.0.15:55942 172.19.0.12:9527 FIN_WAIT2 - timewait (54.03/0/0) tcp 0 0 172.19.0.15:55942 172.19.0.12:9527 FIN_WAIT2 - timewait (53.01/0/0) tcp 0 0 172.19.0.15:55942 172.19.0.12:9527 FIN_WAIT2 - timewait (52.00/0/0) tcp 0 0 172.19.0.15:55942 172.19.0.12:9527 FIN_WAIT2 - timewait (50.98/0/0) tcp 0 0 172.19.0.15:55942 172.19.0.12:9527 FIN_WAIT2 - timewait (49.96/0/0) tcp 0 0 172.19.0.15:55942 172.19.0.12:9527 FIN_WAIT2 - timewait (48.94/0/0) tcp 0 0 172.19.0.15:55942 172.19.0.12:9527 FIN_WAIT2 - timewait (47.92/0/0) tcp 0 0 172.19.0.15:55942 172.19.0.12:9527 FIN_WAIT2 - timewait (46.90/0/0) tcp 0 0 172.19.0.15:55942 172.19.0.12:9527 FIN_WAIT2 - timewait (45.88/0/0) tcp 0 0 172.19.0.15:55942 172.19.0.12:9527 FIN_WAIT2 - timewait (44.86/0/0) tcp 0 0 172.19.0.15:55942 172.19.0.12:9527 FIN_WAIT2 - timewait (43.84/0/0) tcp 0 0 172.19.0.15:55942 172.19.0.12:9527 FIN_WAIT2 - timewait (42.82/0/0) tcp 0 0 172.19.0.15:55942 172.19.0.12:9527 FIN_WAIT2 - timewait (41.81/0/0) tcp 0 0 172.19.0.15:55942 172.19.0.12:9527 FIN_WAIT2 - timewait (40.79/0/0) tcp 0 0 172.19.0.15:55942 172.19.0.12:9527 FIN_WAIT2 - timewait (39.77/0/0) tcp 0 0 172.19.0.15:55942 172.19.0.12:9527 FIN_WAIT2 - timewait (38.75/0/0) tcp 0 0 172.19.0.15:55942 172.19.0.12:9527 FIN_WAIT2 - timewait (37.73/0/0) tcp 0 0 172.19.0.15:55942 172.19.0.12:9527 FIN_WAIT2 - timewait (36.71/0/0) tcp 0 0 172.19.0.15:55942 172.19.0.12:9527 FIN_WAIT2 - timewait (35.69/0/0) tcp 0 0 172.19.0.15:55942 172.19.0.12:9527 FIN_WAIT2 - timewait (34.67/0/0) tcp 0 0 172.19.0.15:55942 172.19.0.12:9527 FIN_WAIT2 - timewait (33.65/0/0) tcp 0 0 172.19.0.15:55942 172.19.0.12:9527 FIN_WAIT2 - timewait (32.63/0/0) tcp 0 0 172.19.0.15:55942 172.19.0.12:9527 FIN_WAIT2 - timewait (31.61/0/0) tcp 0 0 172.19.0.15:55942 172.19.0.12:9527 FIN_WAIT2 - timewait (30.59/0/0) tcp 0 0 172.19.0.15:55942 172.19.0.12:9527 FIN_WAIT2 - timewait (29.57/0/0) tcp 0 0 172.19.0.15:55942 172.19.0.12:9527 FIN_WAIT2 - timewait (28.56/0/0) tcp 0 0 172.19.0.15:55942 172.19.0.12:9527 FIN_WAIT2 - timewait (27.54/0/0) tcp 0 0 172.19.0.15:55942 172.19.0.12:9527 FIN_WAIT2 - timewait (26.52/0/0) tcp 0 0 172.19.0.15:55942 172.19.0.12:9527 FIN_WAIT2 - timewait (25.50/0/0) tcp 0 0 172.19.0.15:55942 172.19.0.12:9527 FIN_WAIT2 - timewait (24.48/0/0) tcp 0 0 172.19.0.15:55942 172.19.0.12:9527 FIN_WAIT2 - timewait (23.46/0/0) tcp 0 0 172.19.0.15:55942 172.19.0.12:9527 FIN_WAIT2 - timewait (22.44/0/0) tcp 0 0 172.19.0.15:55942 172.19.0.12:9527 FIN_WAIT2 - timewait (21.42/0/0) tcp 0 0 172.19.0.15:55942 172.19.0.12:9527 FIN_WAIT2 - timewait (20.40/0/0) tcp 0 0 172.19.0.15:55942 172.19.0.12:9527 FIN_WAIT2 - timewait (19.38/0/0) tcp 0 0 172.19.0.15:55942 172.19.0.12:9527 FIN_WAIT2 - timewait (18.36/0/0) tcp 0 0 172.19.0.15:55942 172.19.0.12:9527 FIN_WAIT2 - timewait (17.34/0/0) tcp 0 0 172.19.0.15:55942 172.19.0.12:9527 FIN_WAIT2 - timewait (16.33/0/0) tcp 0 0 172.19.0.15:55942 172.19.0.12:9527 FIN_WAIT2 - timewait (15.31/0/0) tcp 0 0 172.19.0.15:55942 172.19.0.12:9527 FIN_WAIT2 - timewait (14.29/0/0) tcp 0 0 172.19.0.15:55942 172.19.0.12:9527 FIN_WAIT2 - timewait (13.27/0/0) tcp 0 0 172.19.0.15:55942 172.19.0.12:9527 FIN_WAIT2 - timewait (12.25/0/0) tcp 0 0 172.19.0.15:55942 172.19.0.12:9527 FIN_WAIT2 - timewait (11.23/0/0) tcp 0 0 172.19.0.15:55942 172.19.0.12:9527 FIN_WAIT2 - timewait (10.21/0/0) tcp 0 0 172.19.0.15:55942 172.19.0.12:9527 FIN_WAIT2 - timewait (9.19/0/0) tcp 0 0 172.19.0.15:55942 172.19.0.12:9527 FIN_WAIT2 - timewait (8.18/0/0) tcp 0 0 172.19.0.15:55942 172.19.0.12:9527 FIN_WAIT2 - timewait (7.16/0/0) tcp 0 0 172.19.0.15:55942 172.19.0.12:9527 FIN_WAIT2 - timewait (6.14/0/0) tcp 0 0 172.19.0.15:55942 172.19.0.12:9527 FIN_WAIT2 - timewait (5.12/0/0) tcp 0 0 172.19.0.15:55942 172.19.0.12:9527 FIN_WAIT2 - timewait (4.10/0/0) tcp 0 0 172.19.0.15:55942 172.19.0.12:9527 FIN_WAIT2 - timewait (3.08/0/0) tcp 0 0 172.19.0.15:55942 172.19.0.12:9527 FIN_WAIT2 - timewait (2.06/0/0) tcp 0 0 172.19.0.15:55942 172.19.0.12:9527 FIN_WAIT2 - timewait (1.04/0/0) tcp 0 0 172.19.0.15:55942 172.19.0.12:9527 FIN_WAIT2 - timewait (0.02/0/0) tcp 0 0 172.19.0.15:55942 172.19.0.12:9527 FIN_WAIT2 - timewait (0.00/0/0) 下面是抓包结果：\n我们来简单分析下：\n服务端一直处于 LAST_ACK 状态，说明 FIN 包已发送，但一直没有收到客户端的 ACK 包。 客户端一直处于 FIN_WAIT2 状态，说明客户端已经收到了服务端的 ACK 包，但迟迟没收到服务端的 FIN 包。说明我们的 iptables 拦截生效了。 客户端进入 FIN_WAIT2 状态后，有一个 60s 的 timewait 计时器在运行。这是由内核参数 net.ipv4.tcp_fin_timeout 控制的，默认是 60s。超过后会自动关闭连接，不会进入 TIME_WAIT 状态。 连接保活 TCP 通信需要建立连接，这里的连接并不是真的在通信双方之间有一个通路，而是双方各自维护一个 TCB 来管理状态数据。在这种情况下，如果一方挂了并且没有数据传输，那另一方是感知不到的，其连接可能会一直存在，造成不必要的资源浪费。\n为了解决这个问题 TCP 也设计了保活机制，内核有三个参数与该机制有关：\n1 2 3 4 $ sysctl net.ipv4.tcp_keepalive_time net.ipv4.tcp_keepalive_probes net.ipv4.tcp_keepalive_intvl net.ipv4.tcp_keepalive_time = 7200 net.ipv4.tcp_keepalive_probes = 9 net.ipv4.tcp_keepalive_intvl = 75 net.ipv4.tcp_keepalive_time：最后一次数据发送到发送第一个探活包的间隔时长，默认为 7200s。也就是说如果超过了 7200s 没有发送数据，TCP 就会发送一个探活包。 net.ipv4.tcp_keepalive_probes：允许探活包没有回应的最大次数，默认为 9。也就是说如果发送了 9 次探活包后依然没有得到响应，那么 TCP 就会考虑连接已经失效，会通知应用层中断连接。 net.ipv4.tcp_keepalive_intvl：在第一个探测包发送后，在没有数据传输的情况下，每个探测包的发送频率，默认 75s。即每 75s 发送一个探测包。 我们来做实验验证一下，这里先将上述参数的值调小一些，方便我们观察实验结果。我们将首个探测包的发送时间改为最后一次发送数据 10s 后，并且探测包的时间间隔为 5s，超过 5 次后就断开连接。\n客户端先发送一次数据，然后休眠 30 s，在发送一次数据，然后休眠 200s。\n1 2 3 4 $ sudo sysctl -w net.ipv4.tcp_keepalive_time=10 net.ipv4.tcp_keepalive_intvl=5 net.ipv4.tcp_keepalive_probes=5 net.ipv4.tcp_keepalive_time = 10 net.ipv4.tcp_keepalive_intvl = 5 net.ipv4.tcp_keepalive_probes = 5 服务端代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import socket import time import os # 创建服务器端用于测试 def start_server(): server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server_sock.bind((\u0026#39;172.19.0.15\u0026#39;, 8888)) server_sock.listen(1) print(\u0026#34;Server listening on 172.19.0.15:8888...\u0026#34;) conn, addr = server_sock.accept() print(f\u0026#34;Connection from {addr}\u0026#34;) while True: data = conn.recv(1024) if not data: break print(f\u0026#34;Received: {data.decode(\u0026#39;utf-8\u0026#39;)}\u0026#34;) if __name__ == \u0026#34;__main__\u0026#34;: start_server() 客户端代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 import socket import time import os # 客户端代码，启用 TCP 保活 def start_client(): # 创建 socket client_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 启用 SO_KEEPALIVE client_sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) # 连接到服务器 client_sock.connect((\u0026#39;172.19.0.15\u0026#39;, 8888)) print(\u0026#34;Connected to server\u0026#34;) # 发送第一次 \u0026#34;hello world\u0026#34; client_sock.send(\u0026#34;hello world\u0026#34;.encode(\u0026#39;utf-8\u0026#39;)) print(\u0026#34;Sent first \u0026#39;hello world\u0026#39;\u0026#34;) # 休眠 28 秒 time.sleep(28) # 发送第二次 \u0026#34;hello world\u0026#34; client_sock.send(\u0026#34;hello world\u0026#34;.encode(\u0026#39;utf-8\u0026#39;)) print(\u0026#34;Sent second \u0026#39;hello world\u0026#39;\u0026#34;) # 休眠 200 秒 print(\u0026#34;Sleeping for 200 seconds...\u0026#34;) time.sleep(200) # 关闭连接 client_sock.close() print(\u0026#34;Connection closed\u0026#34;) if __name__ == \u0026#34;__main__\u0026#34;: # 启动客户端 start_client() 启动程序，完成第二次数据传输中用 iptables 将 ACK 包连接来伪造探活失败的场景，\n1 2 # 服务端执行 $ sudo iptables -A INPUT -p tcp --dport 8888 -j DROP socket 状态抓包结果如下：\n1 2 3 4 5 6 7 8 $ sudo netstat -anpo | grep -E \u0026#34;Recv-Q|8888\u0026#34; Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name Timer tcp 0 0 172.19.0.12:38224 172.19.0.15:8888 ESTABLISHED 3276657/python3 keepalive (0.12/0/4) # ubuntu @ node2 in ~/labs/syn-queue-lab [12:38:09] $ sudo netstat -anpo | grep -E \u0026#34;Recv-Q|8888\u0026#34; Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name Timer tcp 0 0 172.19.0.12:38224 172.19.0.15:8888 ESTABLISHED 3276657/python3 keepalive (4.13/0/5) 在第 10s 客户端发送了探活包，其数据长度为 0，这是第一次休眠时的探活检测。 在第 28s 客户端发送了第二次数据，之后从第 38s 开始，每 10s 发送一次探活包。由此可以知道探活包的 ACK 也被视作正常的数据收发，探活检测会根据 net.ipv4.tcp_keepalive_time的值来确定。 从第 68s 开始，我们在服务端设置了 iptables 规则拦截探活包，之后开始每隔 5s 发送一次探活包，这里开始受 net.ipv4.tcp_keepalive_intvl 参数的控制。 连续 5 个探活包没有收到响应后，客户端发送了 RST 包中断连接，说明 net.ipv4.tcp_keepalive_intvl = 5 的改动已经生效。 PS：最开始使用的是 Golang 程序，但发现修改系统设置并不生效，探活包的发送时间间隔一直是 15s。\n1 2 3 4 5 6 7 8 $ sudo netstat -anpo | grep -E \u0026#34;Recv-Q|8888\u0026#34; Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name Timer tcp 0 0 172.19.0.12:46706 172.19.0.15:8888 ESTABLISHED 3270763/client keepalive (0.12/0/0) # ubuntu @ node2 in ~/labs/syn-queue-lab [12:25:07] $ sudo netstat -anpo | grep -E \u0026#34;Recv-Q|8888\u0026#34; Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name Timer tcp 0 0 172.19.0.12:46706 172.19.0.15:8888 ESTABLISHED 3270763/client keepalive (14.77/0/0) 经过调查后发现原因是 Golang 的 net net.DialTimeout 创建连接时默认设置了 15s，如果想修改必须获取到 TCPConn 对象后自行修改。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 // 源码地址：https://github.com/golang/go/blob/bc5f4a555e933e6861d12edba4c2d87ef6caf8e6/src/net/dial.go#L19 const ( // defaultTCPKeepAliveIdle is a default constant value for TCP_KEEPIDLE. // See go.dev/issue/31510 for details. defaultTCPKeepAliveIdle = 15 * time.Second // defaultTCPKeepAliveInterval is a default constant value for TCP_KEEPINTVL. // It is the same as defaultTCPKeepAliveIdle, see go.dev/issue/31510 for details. defaultTCPKeepAliveInterval = 15 * time.Second // defaultTCPKeepAliveCount is a default constant value for TCP_KEEPCNT. defaultTCPKeepAliveCount = 9 // For the moment, MultiPath TCP is used by default with listeners, if // available, but not with dialers. // See go.dev/issue/56539 defaultMPTCPEnabledListen = true defaultMPTCPEnabledDial = false ) 总结 作为程序员，虽然接触到的网络知识基本逃不过 RFC1180： A TCP/IP Tutorial 的范畴，但这确实是最让人头大的知识点之一。作为将《TCP/IP Guide》、《TCP/IP 详解（英文版）》以及主要 RFC 都读过的踩坑者，只能无奈的感慨，光读这些是资料顶多可以让我们勉强了解，但要想在实际工作中面对遇到的问题手到擒来，还远远不够。\n网络知识的学习至少涉及到三方面内容：RFC 定义的协议原理、操作系统的具体实现、命令工具的使用。而每一部分学习起来都不容易，RFC 理论的枯燥，操作系统不同版本实现机制的繁杂，命令工具各种参数的琐碎，都让人望而却步。最好的方式就是做实验，将三者统一起来，通过动手实验，尤其是做生产级别的故障排查类实验，可以帮助我们熟悉工具的使用，验证系统的实现，并通过实验结果加深对理论的理解，做到全面而深刻的学习。\n","permalink":"https://zouyingjie.github.io/posts/%E5%AE%9E%E9%AA%8C-tcp%E8%BF%9E%E6%8E%A5%E5%BB%BA%E7%AB%8B%E4%B8%8E%E5%85%B3%E9%97%AD%E6%8A%93%E5%8C%85/","summary":"\u003cp\u003e本文是基于\u003ca href=\"https://wx.zsxq.com/group/15552551584552\"\u003e知识星球程序员踩坑案例分享\u003c/a\u003e中的作业进行的复现和总结，借此加深对 TCP 协议的理解， 原文参见\u003ca href=\"https://articles.zsxq.com/id_ppf2tv11zc64.html\"\u003eTCP 连接的建立和关闭 —— 强烈建议新手看看\u003c/a\u003e。\u003c/p\u003e\n\u003ch2 id=\"实验环境\"\u003e实验环境\u003c/h2\u003e\n\u003cp\u003e这里使用两台位于同一子网的腾讯云服务器，IP 分别是 node2（172.19.0.12）和 node3（172.19.0.15），内核版本均为 5.15.0-130-generic。\u003c/p\u003e","title":"【动手实验】TCP 连接的建立与关闭抓包分析"},{"content":"重传 实验环境\n这里使用两台腾讯云服务器：vm-1（172.19.0.3）和vm-2（172.19.0.6）。\n超时重传 首先 vm-1 作为服务端启动 nc，然后开启抓包，并使用 netstat 查看连接状态：\n1 2 3 4 5 6 7 $ nc -k -l 172.19.0.3 9527 # 新开一个终端开启抓包 $ sudo tcpdump -s0 -X -nn \u0026#34;tcp port 9527\u0026#34; -w tcp.pcap --print # 新开一个终端查看连接状态 $ while true; do sudo netstat -anpo | grep 9527 | grep -v LISTEN; sleep 1; done 然后我们在 vm-2 上使用 nc 连接 vm-1，三次握手成功后使用 iptables 拦截所有 vm-1 发来的包。\n1 2 3 4 $ nc 172.19.0.3 9527 # 新开一个终端使用 iptables 拦截所有 vm-1 发来的包 $ sudo iptables -A INPUT -p tcp --sport 9527 -j DROP 准备好后我们从 vm-1 输入 abc 按下回车， vm-2 的 iptables 会将包丢弃，因此会触发 vm-1 进行重传，我们来看下 vm-1 的网络连接状态以及抓包结果：\n网络连接状态 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 tcp 0 0 172.19.0.3:9527 172.19.0.6:41278 ESTABLISHED 490833/nc off (0.00/0/0) tcp 0 4 172.19.0.3:9527 172.19.0.6:41278 ESTABLISHED 490833/nc on (0.30/1/0) tcp 0 4 172.19.0.3:9527 172.19.0.6:41278 ESTABLISHED 490833/nc on (0.08/2/0) tcp 0 4 172.19.0.3:9527 172.19.0.6:41278 ESTABLISHED 490833/nc on (0.72/3/0) tcp 0 4 172.19.0.3:9527 172.19.0.6:41278 ESTABLISHED 490833/nc on (2.96/4/0) ... tcp 0 4 172.19.0.3:9527 172.19.0.6:41278 ESTABLISHED 490833/nc on (6.35/5/0) ... tcp 0 4 172.19.0.3:9527 172.19.0.6:41278 ESTABLISHED 490833/nc on (12.31/6/0) ... tcp 0 4 172.19.0.3:9527 172.19.0.6:41278 ESTABLISHED 490833/nc on (25.12/7/0) ... tcp 0 4 172.19.0.3:9527 172.19.0.6:41278 ESTABLISHED 490833/nc on (50.24/8/0) ... tcp 0 4 172.19.0.3:9527 172.19.0.6:41278 ESTABLISHED 490833/nc on (101.48/9/0) ... tcp 0 4 172.19.0.3:9527 172.19.0.6:41278 ESTABLISHED 490833/nc on (119.18/10/0) ... tcp 0 4 172.19.0.3:9527 172.19.0.6:41278 ESTABLISHED 490833/nc on (119.30/11/0) ... tcp 0 4 172.19.0.3:9527 172.19.0.6:41278 ESTABLISHED 490833/nc on (119.41/12/0) ... tcp 0 4 172.19.0.3:9527 172.19.0.6:41278 ESTABLISHED 490833/nc on (119.54/13/0) ... tcp 0 4 172.19.0.3:9527 172.19.0.6:41278 ESTABLISHED 490833/nc on (119.66/14/0) ... tcp 0 4 172.19.0.3:9527 172.19.0.6:41278 ESTABLISHED 490833/nc on (119.80/15/0) ... 抓包结果 我们来分析下抓包结果。\n1. RTO 计算算法 三次握手后第 4 个包发送数据，其 length 为 4，我们输入了 abc 并按下回车，刚好四个字节，因为客户端收不到包，因此后续触发了重传。\nTCP 重传是基于时间来判断的，这里有两个概念：\nRTO（Retransmission TimeOut）：重传超时时间 RTT（Round Trip Time）：往返时间 TCP 会根据 RTT 来动态的计算 RTO，如果超时 RTO 会采用指数退避原则进行指数级增长，但最大不超过 120s。我们先来回顾下 RTO 的计算算法：\n经典算法 RFC 793 中定义的 RTO 计算算法如下：\n记录初始的几次 RTT 值 计算平滑 RTT 值（SRTT，Smoothed RTT），计算公式为如下： 1 2 # alpha 为平滑因子，取值在 0.8 到 0.9 之间，Linux 内核中默认是 0.875 SRTT = ( ALPHA * SRTT ) + ((1-ALPHA) * RTT) 可以看到，如果 alpha 值越大，标识系统越信任之前的计算结果，否则就会更信任新的 RTT 值。\n计算 RTO 值，计算公式为如下： 1 RTO = min[Ubound,max[Lbound,(BETA*SRTT)]] Ubound 为 RTO 上限，Linux 内核中默认是 120s Lbound 为 RTO 下限，Linux 内核中默认是 200ms Beta 为延迟方差因子，取值在 1.3 到 2.0 之间。 Karn 算法 上述算法的问题在于将所有包的 RTT 一视同仁，是对于重传的包，如果取第一次发送+ACK 包的 RTT 值，会导致 RTT 明显偏大；如果取重传的包，此时如果之前的 ACK 响应回来了，又会导致取值偏小。\n为此 1987 年 Phil Karn/Craig Partridge 在论文 Improving Round-Trip Time Estimates in Reliable Transport Protocols 中提出了 Karn 算法，其最大的特点是将重传的包忽略掉，不用来做 RTT 的计算，同时一旦重传，RTO 会立即翻倍。\nrfc6298 中规定，RTT 的采用必须采用 Karn 算法。\nJacobson/Karels 算法 RFC2988 中改进了重传算法，并在 rfc6298 中进行了更新，其规定的 RTO 计算算法如下:\n1 2 3 4 5 6 7 8 9 10 11 12 对于初始 RTO，当第一个包的 RTT 获取到后： SRTT = RTT RTTVAR = RTT / 2 RTO = SRTT + max(K*RTTVAR, G) where K = 4 and G = 200ms 对于后续的 RTO 值计算，获取到新的 RTT 后： RTTVAR = (1-Beta)*RTTVAR + Beta*|SRTT - RTT| SRTT = (1-Alpha)*SRTT + Alpha*RTT 最后 RTO 的计算公式为： RTO = SRTT + max(K*RTTVAR, G) 在 Linux 中，Alpha 取值为 0.125，Beta 取值为 0.25，K 取值为 4，G 取值为 200ms，其次还做了一些工程上的优化，这里先不深究，具体源码参考tcp_rtt_estimator 和 tcp_set_rto。\nRTO 与 Delayed ACK 我们可以通过 ss -tip 命令查看某个连接的 rto，可以看到我们的连接初始 RTO 为 200ms，每次超时重传后都会翻倍，一直增长到 120s 后固定不变。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 # 初始 RTO 为 200ms ESTAB 0 0 172.19.0.3:9527 172.19.0.6:41278 users:((\u0026#34;nc\u0026#34;,pid=490833,fd=4)) cubic wscale:7,7 rto:200 rtt:0.153/0.076 mss:8448 pmtu:8500 rcvmss:536 advmss:8448 cwnd:10 segs_in:2 send 4.42Gbps lastsnd:11221 lastrcv:11221 lastack:11221 pacing_rate 8.83Gbps delivered:1 app_limited rcv_space:57076 rcv_ssthresh:57076 minrtt:0.153 snd_wnd:59264 ESTAB 0 4 172.19.0.3:9527 172.19.0.6:41278 users:((\u0026#34;nc\u0026#34;,pid=490833,fd=4)) cubic wscale:7,7 rto:12800 backoff:6 rtt:0.153/0.076 mss:8448 pmtu:8500 rcvmss:536 advmss:8448 cwnd:1 ssthresh:7 bytes_sent:32 bytes_retrans:28 segs_out:8 segs_in:2 data_segs_out:8 send 442Mbps lastsnd:1115 lastrcv:28668 lastack:28668 pacing_rate 8.83Gbps delivered:1 app_limited busy:14438ms unacked:1 retrans:1/7 lost:1 rcv_space:57076 rcv_ssthresh:57076 minrtt:0.153 snd_wnd:59264 ESTAB 0 4 172.19.0.3:9527 172.19.0.6:41278 users:((\u0026#34;nc\u0026#34;,pid=490833,fd=4)) cubic wscale:7,7 rto:51200 backoff:8 rtt:0.153/0.076 mss:8448 pmtu:8500 rcvmss:536 advmss:8448 cwnd:1 ssthresh:7 bytes_sent:40 bytes_retrans:36 segs_out:10 segs_in:2 data_segs_out:10 send 442Mbps lastsnd:45728 lastrcv:112705 lastack:112705 pacing_rate 8.83Gbps delivered:1 app_limited busy:98475ms unacked:1 retrans:1/9 lost:1 rcv_space:57076 rcv_ssthresh:57076 minrtt:0.153 snd_wnd:59264 ESTAB 0 4 172.19.0.3:9527 172.19.0.6:41278 users:((\u0026#34;nc\u0026#34;,pid=490833,fd=4)) cubic wscale:7,7 rto:102400 backoff:9 rtt:0.153/0.076 mss:8448 pmtu:8500 rcvmss:536 advmss:8448 cwnd:1 ssthresh:7 bytes_sent:44 bytes_retrans:40 segs_out:11 segs_in:2 data_segs_out:11 send 442Mbps lastsnd:2475 lastrcv:124748 lastack:124748 pacing_rate 8.83Gbps delivered:1 app_limited busy:110518ms unacked:1 retrans:1/10 lost:1 rcv_space:57076 rcv_ssthresh:57076 minrtt:0.153 snd_wnd:59264 $ sudo ss -tip | grep -A 1 9527 ESTAB 0 4 172.19.0.3:9527 172.19.0.6:41278 users:((\u0026#34;nc\u0026#34;,pid=490833,fd=4)) cubic wscale:7,7 rto:120000 backoff:10 rtt:0.153/0.076 mss:8448 pmtu:8500 rcvmss:536 advmss:8448 cwnd:1 ssthresh:7 bytes_sent:48 bytes_retrans:44 segs_out:12 segs_in:2 data_segs_out:12 send 442Mbps lastsnd:4544 lastrcv:233313 lastack:233313 pacing_rate 8.83Gbps delivered:1 app_limited busy:219083ms unacked:1 retrans:1/11 lost:1 rcv_space:57076 rcv_ssthresh:57076 minrtt:0.153 snd_wnd:59264 $ sudo ss -tip | grep -A 1 9527 ESTAB 0 4 172.19.0.3:9527 172.19.0.6:41278 users:((\u0026#34;nc\u0026#34;,pid=490833,fd=4)) cubic wscale:7,7 rto:120000 backoff:15 rtt:0.153/0.076 mss:8448 pmtu:8500 rcvmss:536 advmss:8448 cwnd:1 ssthresh:7 bytes_sent:68 bytes_retrans:64 segs_out:17 segs_in:2 data_segs_out:17 send 442Mbps lastsnd:2520 lastrcv:845689 lastack:845689 pacing_rate 8.83Gbps delivered:1 app_limited busy:831459ms unacked:1 retrans:1/16 lost:1 rcv_space:57076 rcv_ssthresh:57076 minrtt:0.153 snd_wnd:59264 从 ss 的信息中可以看到虽然 RTT 的大小始终是 rtt:0.153/0.076 ，代表 rtt 时间为 0.153ms，平均偏差为 0.076ms，但 RTO 时间最小也是 200ms，后续一直增加到120000 ms，看起来和 RTT 并没有关系。\n这样是因为 Linux 内核规定了 RTO 的最小值和最大值分别为 200ms 和 120s，具体源码如下：\n1 2 3 // 源码地址：https://elixir.bootlin.com/linux/v6.0/source/include/net/tcp.h#L141 #define TCP_RTO_MAX ((unsigned)(120*HZ)) #define TCP_RTO_MIN ((unsigned)(HZ/5)) HZ 表示 CPU 一秒种发出多少次时间中断–IRQ-0，通常使用 HZ 做时间片的单位，可以理解为 1HZ 就是 1s。\n1 2 3 4 5 6 7 $ cat /boot/config-`uname -r` | grep \u0026#39;^CONFIG_HZ=\u0026#39; CONFIG_HZ=1000 # ubuntu @ vm-1 in ~ [15:44:15] $ cat /proc/interrupts | grep timer \u0026amp;\u0026amp; sleep 1 \u0026amp;\u0026amp; cat /proc/interrupts | grep timer LOC: 134957597 148734818 Local timer interrupts LOC: 134957987 148735153 Local timer interrupts 这样做主要是为了给 Delayed ACK 留出时间。简单来说就是让 TCP 在收到数据包后稍微等一会，看有没有其他需要发送的数据，如果有就让 ACK 搭个便车一起发送回去，这样可以减少网络上小包的数量，提高网络传输效率。RTO 的计算逻辑几经改进，最终一顿操作猛如虎，不如 Delayed ACK 直接一把梭给你定死个下限。\n重传超时时长 netstat 查看状态也可以看到重传计时器在不断变化，从 200ms 开始不断翻倍，最终在传完 10 次后固定为 120s，最终显示已经重传了 15 次 on (119.80/15/0)。这里主要受 tcp_retries2 参数的控制，默认为 15。注意这里不是精确控制一定会重传 15 次，而是 tcp_retries2 结合 TCP_RTO_MIN（200ms）计算出一个超时时间来，tcp 连接不断重传，最终不能超过这个超时时间。源码如下，\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 // 源码地址：https://elixir.bootlin.com/linux/v6.0/source/net/ipv4/tcp_timer.c#L231 static int tcp_write_timeout(struct sock *sk) { // ... 代码省略 bool expired = false, do_reset; int retry_until = READ_ONCE(net-\u0026gt;ipv4.sysctl_tcp_retries2); if (!expired) expired = retransmits_timed_out(sk, retry_until, icsk-\u0026gt;icsk_user_timeout); if (expired) { /* Has it gone just too far? */ tcp_write_err(sk); return 1; } } // 源码地址：https://elixir.bootlin.com/linux/v6.0/source/net/ipv4/tcp_timer.c#L209 static bool retransmits_timed_out(struct sock *sk, unsigned int boundary, unsigned int timeout) { // ... 代码省略 unsigned int start_ts; unsigned int rto_base = TCP_RTO_MIN; timeout = tcp_model_timeout(sk, boundary, rto_base); return (s32)(tcp_time_stamp(tcp_sk(sk)) - start_ts - timeout) \u0026gt;= 0; } // 源码地址：https://elixir.bootlin.com/linux/v6.0/source/net/ipv4/tcp_timer.c#L182 static unsigned int tcp_model_timeout(struct sock *sk, unsigned int boundary, unsigned int rto_base) { unsigned int linear_backoff_thresh, timeout; linear_backoff_thresh = ilog2(TCP_RTO_MAX / rto_base); if (boundary \u0026lt;= linear_backoff_thresh) timeout = ((2 \u0026lt;\u0026lt; boundary) - 1) * rto_base; else timeout = ((2 \u0026lt;\u0026lt; linear_backoff_thresh) - 1) * rto_base + (boundary - linear_backoff_thresh) * TCP_RTO_MAX; return jiffies_to_msecs(timeout); } 可以看到内核取 tcp_retries2 参数值作为 boundary，核心计算逻辑位于 tcp_model_timeout 函数中，首先会计算出小于 120s 时的指数退避次数为 9。因此重传次数在小于等于 9 次时，下一次的重传时间都是指数增加的，如果超过 9 次比如已经发生了 10 次重传，那下一次的重传时间就是 120s 了。从 netstat 的输出中我们可以验证这一点：\n1 2 3 tcp 0 4 172.19.0.3:9527 172.19.0.6:41278 ESTABLISHED 490833/nc on (101.48/9/0) ... tcp 0 4 172.19.0.3:9527 172.19.0.6:41278 ESTABLISHED 490833/nc on (119.18/10/0) 总超时的计算逻辑为：\ntcp_retries2 \u0026lt;= 9 时， timeout = ((2 \u0026lt;\u0026lt; boundary) - 1) * rto_base tcp_retries2 \u0026gt; 9 时， timeout = ((2 \u0026lt;\u0026lt; linear_backoff_thresh) - 1) * rto_base + (boundary - linear_backoff_thresh) * TCP_RTO_MAX; 基于上述逻辑，在 rto 为 200ms时，我们可以计算出 tcp_retries2 设置和总重传超时时间的关系：\ntcp_retries2 重传超时时间 总超时时间 0 200ms 200ms 1 400ms 600ms 2 800ms 1.4s 3 1.6s 3s 4 3.2s 6.2s 5 6.4s 12.6s 6 12.8s 25.4s 7 25.6s 51s 8 51.2s 102.2s 9 102.4s 204.6s 10 120s 324.6s 11 120s 444.6s 12 120s 564.6s 13 120s 684.6s 14 120s 804.6s 15 120s 924.6s tcp_retries2 默认是 15，因此默认情况下，TCP 发送数据失败后大约会在 924.6s，大约 15 分钟左右才会放弃连接。如果实际 RTO 很大，也不会真的重传 15 次导致等待时间过长，而是在超过 924.6s 后放弃连接。下面我们使用 tc qdisc 将 vm-2 的延迟改为 2s 来模拟网络延迟在来看下重传的次数：\n1 2 # ubuntu @ vm-2 in ~ [10:05:28] $ sudo tc qdisc add dev eth0 root netem delay 2000ms 修改完成后重新建立连接并发送数据，通过 ss、netstat 查看，可以看到初始 RTO 已经成了 6s，抓包显示实际的重传次数为 11 次，超时时长为 973.2567 - 45.5127 = 927.744s，大约 15 分钟多一些，基本符合预期。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 # 初始 RTO 为 6s $ sudo ss -tip | grep -A 1 9527 ESTAB 0 0 172.19.0.3:9527 172.19.0.6:36856 users:((\u0026#34;nc\u0026#34;,pid=1880252,fd=4)) cubic wscale:7,7 rto:6000 rtt:2000/1000 mss:8448 pmtu:8500 rcvmss:536 advmss:8448 cwnd:10 segs_in:3 send 338kbps lastsnd:25355 lastrcv:25355 lastack:24330 pacing_rate 676kbps delivered:1 app_limited retrans:0/1 rcv_space:57076 rcv_ssthresh:57076 minrtt:2000 snd_wnd:59264 # 超时时间翻倍到 120s 后，RTO 也变为 120000ms $ sudo ss -tip | grep -A 1 9527 ESTAB 0 4 172.19.0.3:9527 172.19.0.6:39054 users:((\u0026#34;nc\u0026#34;,pid=1910324,fd=4)) cubic wscale:7,7 rto:120000 backoff:5 rtt:2000/1000 mss:8448 pmtu:8500 rcvmss:536 advmss:8448 cwnd:1 ssthresh:7 bytes_sent:28 bytes_retrans:24 segs_out:7 segs_in:3 data_segs_out:7 send 33.8kbps lastsnd:74641 lastrcv:308618 lastack:307585 pacing_rate 676kbps delivered:1 app_limited busy:269672ms unacked:1 retrans:1/7 lost:1 rcv_space:57076 rcv_ssthresh:57076 minrtt:2000 snd_wnd:59264 # 从 6 s 开始翻倍，6、12、24、48、96，在传完 5 次后超时时间固定为 120s。最终重传完 11 次后，总时间超过了 900 多s，系统终止连接 $ while true; do sudo netstat -anpo | grep 9527 | grep -v LISTEN; sleep 1; done tcp 0 0 172.19.0.3:9527 172.19.0.6:39054 ESTABLISHED 1910324/nc off (0.00/0/0) tcp 0 4 172.19.0.3:9527 172.19.0.6:39054 ESTABLISHED 1910324/nc on (3.98/0/0) tcp 0 4 172.19.0.3:9527 172.19.0.6:39054 ESTABLISHED 1910324/nc on (2.96/0/0) tcp 0 4 172.19.0.3:9527 172.19.0.6:39054 ESTABLISHED 1910324/nc on (1.94/0/0) tcp 0 4 172.19.0.3:9527 172.19.0.6:39054 ESTABLISHED 1910324/nc on (0.92/0/0) tcp 0 4 172.19.0.3:9527 172.19.0.6:39054 ESTABLISHED 1910324/nc on (0.00/0/0) .... tcp 0 4 172.19.0.3:9527 172.19.0.6:39054 ESTABLISHED 1910324/nc on (5.24/0/0) tcp 0 4 172.19.0.3:9527 172.19.0.6:39054 ESTABLISHED 1910324/nc on (4.22/0/0) tcp 0 4 172.19.0.3:9527 172.19.0.6:39054 ESTABLISHED 1910324/nc on (3.20/0/0) tcp 0 4 172.19.0.3:9527 172.19.0.6:39054 ESTABLISHED 1910324/nc on (2.17/0/0) tcp 0 4 172.19.0.3:9527 172.19.0.6:39054 ESTABLISHED 1910324/nc on (1.15/0/0) tcp 0 4 172.19.0.3:9527 172.19.0.6:39054 ESTABLISHED 1910324/nc on (0.13/0/0) tcp 0 4 172.19.0.3:9527 172.19.0.6:39054 ESTABLISHED 1910324/nc on (11.25/1/0) ... tcp 0 4 172.19.0.3:9527 172.19.0.6:39054 ESTABLISHED 1910324/nc on (23.27/2/0) ... tcp 0 4 172.19.0.3:9527 172.19.0.6:39054 ESTABLISHED 1910324/nc on (47.80/3/0) ... tcp 0 4 172.19.0.3:9527 172.19.0.6:39054 ESTABLISHED 1910324/nc on (95.36/4/0) ... tcp 0 4 172.19.0.3:9527 172.19.0.6:39054 ESTABLISHED 1910324/nc on (119.48/5/0) ... tcp 0 4 172.19.0.3:9527 172.19.0.6:39054 ESTABLISHED 1910324/nc on (119.48/11/0) ... tcp 0 4 172.19.0.3:9527 172.19.0.6:39054 ESTABLISHED 1910324/nc on (2.70/11/0) tcp 0 4 172.19.0.3:9527 172.19.0.6:39054 ESTABLISHED 1910324/nc on (1.68/11/0) ... tcp 0 4 172.19.0.3:9527 172.19.0.6:39054 ESTABLISHED 1910324/nc on (0.00/11/0) tcp 0 4 172.19.0.3:9527 172.19.0.6:39054 ESTABLISHED 1910324/nc on (0.00/11/0) 抓包结果如下：\n快速重传 可以看到依赖于 RTO 的重传会因为 TCP_RTO_MIN 的影响，导致重传超时时间很长，效率很低。为此 RFC 5681 中提出了快速重传（Fast Retransmit），该算法不以时间作为重传依据，而是按照收到的重复 ACK 来判断是否需要重传。\nRFC 规定，当接收方收到的包乱序时，要立即响应一个 duplicate ACK，比如有 1、2、3、4、5 共5个包，在收到 1 后接收方 ACK 为 2，表示希望接下来收到 2 号包，但此时如果收到了 3、4、5 号包，此时接收方需要立即响应 duplicate ACK 给发送方。\nRFC 规定发送方在收到 3 个 Duplicate ACK 后，会立即重传，这样判断的依据是，有两种情况会导致接收方收到的包乱序：乱序或丢包。如果是乱序，接收方通常会稍后收到预期的包，比如在收到 3 后才收到 2 号包，此时发送方一般只会收到 1 ~ 2 次 Duplicate ACK。但如果是丢包，就会导致接收方多次响应 Duplicate ACK，此时发送方就可以认为是丢包，从而引发进行快速重传。\n下面我们使用 scapy 来模拟快速重传的过程。代码如下：\n服务端程序 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 import socket import time def start_server(host, port, backlog): server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.bind((host, port)) server.listen(backlog) client, _ = server.accept() client.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) # 禁用 Nagle 算法 client.sendall(b\u0026#34;a\u0026#34; * 1460) time.sleep(0.01) # 避免协议栈合并包的方式，不严谨但是凑合能工作 client.sendall(b\u0026#34;b\u0026#34; * 1460) time.sleep(0.01) client.sendall(b\u0026#34;c\u0026#34; * 1460) time.sleep(0.01) client.sendall(b\u0026#34;d\u0026#34; * 1460) time.sleep(0.01) client.sendall(b\u0026#34;e\u0026#34; * 1460) time.sleep(0.01) client.sendall(b\u0026#34;f\u0026#34; * 1460) time.sleep(0.01) client.sendall(b\u0026#34;g\u0026#34; * 1460) time.sleep(10000) if __name__ == \u0026#39;__main__\u0026#39;: start_server(\u0026#39;172.19.0.3\u0026#39;, 9527, 8) 客户端程序 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 import threading import time from scapy.all import * from scapy.layers.inet import * class ACKDataThread(threading.Thread): def __init__(self): super().__init__() self.first_data_ack_seq = 0 def run(self): def packet_callback(packet): ip = IP(dst=\u0026#34;172.19.0.3\u0026#34;) resp_tcp = packet[TCP] # 收到第二次握手包 if \u0026#39;SA\u0026#39; in str(resp_tcp.flags): recv_seq = resp_tcp.seq recv_ack = resp_tcp.ack print(f\u0026#34;received SYN, seq={recv_seq}, ACK={recv_ack}\u0026#34;) send_ack = recv_seq + 1 tcp = TCP(sport=9528, dport=9527, flags=\u0026#39;A\u0026#39;, seq=2, ack=send_ack) print(f\u0026#34;send ACK={send_ack}\u0026#34;) # 第三次握手 send(ip/tcp) return # 收到数据包 elif resp_tcp.payload: print(\u0026#34;-\u0026#34; * 50) print(f\u0026#34;Received TCP packet\u0026#34;) print(f\u0026#34;Flags: {resp_tcp.flags}\u0026#34;) print(f\u0026#34;Sequence: {resp_tcp.seq}\u0026#34;) print(f\u0026#34;ACK: {resp_tcp.ack}\u0026#34;) print(f\u0026#34;Payload: {resp_tcp.load}\u0026#34;) # send_ack = resp_tcp.seq + len(resp_tcp.load) if self.first_data_ack_seq == 0: self.first_data_ack_seq = resp_tcp.seq + len(resp_tcp.load) send_ack = self.first_data_ack_seq tcp = TCP(sport=9528, dport=9527, flags=\u0026#39;A\u0026#39;, seq=2, ack=send_ack) print(f\u0026#34;send ACK={send_ack}\u0026#34;) # 发送 4 次重复的 ACK send(ip/tcp) send(ip/tcp) send(ip/tcp) send(ip/tcp) interface = \u0026#34;eth0\u0026#34; # 根据实际络接口名称更改 sniff(iface=interface, prn=packet_callback, filter=\u0026#34;tcp and port 9527\u0026#34;, store=0) def main(): thread = ACKDataThread() thread.start() time.sleep(1) ip = IP(dst=\u0026#34;172.19.0.3\u0026#34;) tcp = TCP(sport=9528, dport=9527, flags=\u0026#39;S\u0026#39;, seq=1, options=[(\u0026#39;MSS\u0026#39;, 1460)]) # 第一次握手 print(\u0026#34;send SYN, seq=0\u0026#34;) send(ip/tcp) thread.join() if __name__ == \u0026#34;__main__\u0026#34;: main() 启动程序\n1 2 3 4 5 6 7 8 9 10 11 12 13 # vm-1 # 启动服务端 $ python3 server.py # 开启抓包 $ sudo tcpdump -S -s0 -nn \u0026#34;tcp port 9527\u0026#34; -w tcp-fast-retra.pcap --print # vm-2 # 丢弃 RST 包 $ sudo iptables -A OUTPUT -p tcp --tcp-flags RST RST --dport 9527 -j DROP # 启动客户端 $ python3 client.py 我们将抓包结果放到 Wireshark 中做分析，其标识了 Duplicate ACK 的包和快速重传的包，可以看到在服务端 0.018s 发送了数据包，然后在 0.072s 进行了快速重传，中间只差了 54ms，比 RTO 要小很多。然后在 0.285s 又进行了一次快速重传，这个和之前的快速重传包差了大约 200ms，已经是超时重传在进行了，后续在 0.709s、1.589s 进行的重传，时间间隔基本符合指数退避的规律。\nWireshark -\u0026gt; 统计 -\u0026gt; TCP 流图形 -\u0026gt; 序列号（tcptrace）窗口中可以看到重传的标识，其中的蓝色竖线表示有包发生了重传。\n虽然 RFC 规定收到 3 个 Duplicate ACK 后才需要快速重传，但 Linux 提供了参数 net.ipv4.tcp_reordering来控制，默认为 3，如果我们修改为 1 可以看到在收到一个 Duplicate ACK 后就会立即重传。当然，生产环境中不建议修改这些参数。\n1 2 $ sudo sysctl -w net.ipv4.tcp_reordering=1 net.ipv4.tcp_reordering = 1 SACK（Selective ACK） SACK 是 TCP 提供的一种可选择重传机制，允许发送方在收到乱序包时，只重传丢失的包，而不是重传整个窗口的数据。\nSACK 的实现需要双方协商，在握手时需要发送方在选项中携带 SACK 选项，接收方在收到后会启用 SACK 机制。\n滑动窗口 拥塞控制 开启 BBR 算法 可以通过 net.ipv4.tcp_available_congestion_control 参数查看当前已经启用的拥塞控制算法：\n1 2 $ sysctl net.ipv4.tcp_available_congestion_control net.ipv4.tcp_available_congestion_control = reno cubic Linux 内核从 4.9 开始就支持 BBR 算法了，我们的内核版本是 5.15.0-130-generic，因此是支持的只需要启用下即可。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 # 检查内核配置文件是否支持BBR，如果是 y 说明已经内置，可以直接启用；如果是 m 说明是基于模块存在，需要加载模块；如果没有需要更新内核。 $ sudo cat /boot/config-$(uname -r) | grep CONFIG_TCP_CONG_BBR CONFIG_TCP_CONG_BBR=m # BBR 需要配合 fq 调度器使用，看是否已支持，输出是 m 说明支持。 # ubuntu @ vm-02 in ~ [10:02:06] $ sudo cat /boot/config-$(uname -r) | grep CONFIG_NET_SCH_FQ CONFIG_NET_SCH_FQ_CODEL=m CONFIG_NET_SCH_FQ=m CONFIG_NET_SCH_FQ_PIE=m # 加载 bbr 模块 $ sudo modprobe tcp_bbr # 查看可用算法 $ sysctl net.ipv4.tcp_available_congestion_control net.ipv4.tcp_available_congestion_control = reno cubic bbr bbr 算法可用后，修改 tcp_congestion_control 和 qdisc 配置即可启用 BBR：\n1 2 3 $ sysctl -w net.ipv4.tcp_congestion_control=bbr net.core.default_qdisc=fq net.core.default_qdisc=fq net.ipv4.tcp_congestion_control=bbr ","permalink":"https://zouyingjie.github.io/posts/tcp%E6%95%B0%E6%8D%AE%E7%9A%84%E5%8F%91%E9%80%81%E5%92%8C%E6%8E%A5%E6%94%B6/","summary":"\u003ch2 id=\"重传\"\u003e重传\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003e实验环境\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e这里使用两台腾讯云服务器：vm-1（172.19.0.3）和vm-2（172.19.0.6）。\u003c/p\u003e\n\u003ch3 id=\"超时重传\"\u003e超时重传\u003c/h3\u003e\n\u003cp\u003e首先 vm-1 作为服务端启动 nc，然后开启抓包，并使用 netstat 查看连接状态：\u003c/p\u003e","title":"【动手实验】TCP 数据的发送与接收抓包分析"},{"content":"本文是对 从一次线上问题说起，详解 TCP 半连接队列、全连接队列 这篇文章的实验复现和总结，借此加深对 TCP 半连接队列、全连接队列的理解。\n实验环境 两台腾讯云服务器 node2（172.19.0.12） 和 node3（172.19.0.15）配置为 2C4G，Ubuntu 系统，内核版本 5.15.0-130-generic 。\n全连接半连接队列简介 在 TCP 三次握手过程中，Linux 会维护两个队列分别是：\nSYN Queue 半连接队列 Accept Queue 全连接队列 创建连接时，两个队列作用如下：\n客户端向服务端发送 SYN 包，客户端进入 SYN_SENT 状态 服务端收到 SYN 包后，进入 SYN_RECV 状态，内核将连接信息放入 SYN Queue 队列，然后向客户端发送 SYN+ACK 包 客户端收到 SYN+ACK 包后，发送 ACK 包，客户端进入 ESTABLISHED 状态 服务端收到 ACK 包后，将连接从 SYN Queue 队列中取出移到 Accept Queue 队列，Server 端进入 ESTABLISHED。 服务端应用程序调用 accept 函数处理数据，连接从 Accept Queue 队列移除。 图片来自：从一次线上问题说起，详解 TCP 半连接队列、全连接队列 图片来自Cloudflare Blog: SYN Packet Handling in the Wild\n两个队列的长度都是有限的，当队列满了之后，新建连接时内核会将 SYN 包丢弃或者直接返回 RST 包。\n全连接队列实战 全连接队列长度控制 TCP 全连接队列的长度计算公式为：\nmin(somaxconn, backlog)\nsomaxconn Linux 内核参数 net.core.somaxconn 的值，默认为 4096。可以通过修改该参数来控制全连接队列的长度。 backlog 是系统调用 listen 函数 int listen(int sockfd, int backlog) 的 backlog 参数， Golang 中默认使用系统 somaxconn 的值。 下面是 Linux 5.15.130 内核源码中计算全连接队列长度的代码：\n源码地址：https://elixir.bootlin.com/linux/v5.15.130/source/net/socket.c#L1716 我们修改 somaxconn 的值，然后运行实验代码查看全连接队列的长度变化。\n服务端实验代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package main import ( \u0026#34;log\u0026#34; \u0026#34;net\u0026#34; \u0026#34;time\u0026#34; ) func main() { l, err := net.Listen(\u0026#34;tcp\u0026#34;, \u0026#34;:8888\u0026#34;) if err != nil { log.Printf(\u0026#34;failed to listen due to %v\u0026#34;, err) } defer l.Close() log.Println(\u0026#34;listen :8888 success\u0026#34;) for { time.Sleep(time.Second * 100) } } 首先我们修改 somaxconn 为 128：\n1 sudo sysctl -w net.core.somaxconn=128 启动服务后查看全连接队列的长度：\n1 2 3 4 5 6 7 8 $ go run server.go 2025/02/13 09:53:01 listen :8888 success $ ss -lnt State Recv-Q Send-Q Local Address:Port Peer Address:Port Process LISTEN 0 128 *:8888 *:* ... 这里简单解释下 ss 命令输出的含义：\n对于 Listen 状态的 socket，Recv-Q 表示当前全连接队列的长度，也就是已经完成三次握手，等待应用层调用 accept 的 TCP 连接数；Send-Q 表示全连接队列的最大长度。\n对于非 Listen 状态的 socket，Recv-Q 表示已经收到但尚未被应用读取的字节数；Send-Q 表示已发送但尚未收到确认的字节数。\n再次修改 somaxconn 为 1024 重启服务后，查看全连接队列的长度已经变成了 1024。\n1 2 3 4 5 6 7 8 9 $ sudo sysctl -w net.core.somaxconn=1024 $ go run server.go 2025/02/13 09:53:01 listen :8888 success $ ss -lnt State Recv-Q Send-Q Local Address:Port Peer Address:Port Process LISTEN 0 1024 *:8888 *:* ... 全连接队列溢出 下面我们让服务端只 Listen 端口但不执行 accept() 处理数据，模拟全连接队列溢出的情况。\n服务端代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 // server 端监听 8888 tcp 端口 package main import ( \u0026#34;log\u0026#34; \u0026#34;net\u0026#34; \u0026#34;time\u0026#34; ) func main() { l, err := net.Listen(\u0026#34;tcp\u0026#34;, \u0026#34;:8888\u0026#34;) if err != nil { log.Printf(\u0026#34;failed to listen due to %v\u0026#34;, err) } defer l.Close() log.Println(\u0026#34;listen :8888 success\u0026#34;) for { time.Sleep(time.Second * 100) } } 客户端代码 和原实验相比加了 time.Sleep(500 * time.Millisecond) 一行代码，让连接一个个建立，可以更精准的复现全连接队列已满的情况。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 package main import ( \u0026#34;context\u0026#34; \u0026#34;log\u0026#34; \u0026#34;net\u0026#34; \u0026#34;os\u0026#34; \u0026#34;os/signal\u0026#34; \u0026#34;sync\u0026#34; \u0026#34;syscall\u0026#34; \u0026#34;time\u0026#34; ) var wg sync.WaitGroup func establishConn(ctx context.Context, i int) { defer wg.Done() conn, err := net.DialTimeout(\u0026#34;tcp\u0026#34;, \u0026#34;:8888\u0026#34;, time.Second*5) if err != nil { log.Printf(\u0026#34;%d, dial error: %v\u0026#34;, i, err) return } log.Printf(\u0026#34;%d, dial success\u0026#34;, i) _, err = conn.Write([]byte(\u0026#34;hello world\u0026#34;)) if err != nil { log.Printf(\u0026#34;%d, send error: %v\u0026#34;, i, err) return } select { case \u0026lt;-ctx.Done(): log.Printf(\u0026#34;%d, dail close\u0026#34;, i) } } func main() { ctx, cancel := context.WithCancel(context.Background()) // 并发请求 10 次服务端，连接建立成功后发送数据 for i := 0; i \u0026lt; 10; i++ { wg.Add(1) time.Sleep(500 * time.Millisecond) go establishConn(ctx, i) } go func() { sc := make(chan os.Signal, 1) signal.Notify(sc, syscall.SIGINT) select { case \u0026lt;-sc: cancel() } }() wg.Wait() log.Printf(\u0026#34;client exit\u0026#34;) } 我们先将全连接队列的最大长度设置为 5：\n1 2 3 4 $ sudo sysctl -w net.core.somaxconn=5 $ cat /proc/sys/net/core/somaxconn 5 运行服务端和客户端后，查看全连接队列情况：\n服务端 socket 情况 1 2 3 4 5 6 7 8 9 $ ss -ant | grep -E \u0026#34;Recv|8888\u0026#34; State Recv-Q Send-Q Local Address:Port Peer Address:Port Process LISTEN 6 5 *:8888 *:* ESTAB 11 0 [::ffff:172.19.0.12]:8888 [::ffff:172.19.0.15]:40148 ESTAB 11 0 [::ffff:172.19.0.12]:8888 [::ffff:172.19.0.15]:40162 ESTAB 11 0 [::ffff:172.19.0.12]:8888 [::ffff:172.19.0.15]:40128 ESTAB 11 0 [::ffff:172.19.0.12]:8888 [::ffff:172.19.0.15]:40132 ESTAB 11 0 [::ffff:172.19.0.12]:8888 [::ffff:172.19.0.15]:40110 ESTAB 11 0 [::ffff:172.19.0.12]:8888 [::ffff:172.19.0.15]:40112 客户端 socket 情况 1 2 3 4 5 6 7 8 9 10 11 12 $ ss -ant | grep -E \u0026#34;Recv|8888\u0026#34; State Recv-Q Send-Q Local Address:Port Peer Address:Port Process ESTAB 0 0 172.19.0.15:40132 172.19.0.12:8888 ESTAB 0 0 172.19.0.15:40162 172.19.0.12:8888 ESTAB 0 0 172.19.0.15:40148 172.19.0.12:8888 SYN-SENT 0 1 172.19.0.15:51906 172.19.0.12:8888 ESTAB 0 0 172.19.0.15:40112 172.19.0.12:8888 ESTAB 0 0 172.19.0.15:40128 172.19.0.12:8888 SYN-SENT 0 1 172.19.0.15:51912 172.19.0.12:8888 SYN-SENT 0 1 172.19.0.15:40176 172.19.0.12:8888 ESTAB 0 0 172.19.0.15:40110 172.19.0.12:8888 SYN-SENT 0 1 172.19.0.15:51926 172.19.0.12:8888 客户端日志输出 1 2 3 4 5 6 7 8 9 10 11 $ go run client.go 2025/02/19 11:14:22 0, dial success 2025/02/19 11:14:22 1, dial success 2025/02/19 11:14:23 2, dial success 2025/02/19 11:14:23 3, dial success 2025/02/19 11:14:24 4, dial success 2025/02/19 11:14:24 5, dial success 2025/02/19 11:14:30 6, dial error: dial tcp 172.19.0.12:8888: i/o timeout 2025/02/19 11:14:30 7, dial error: dial tcp 172.19.0.12:8888: i/o timeout 2025/02/19 11:14:31 8, dial error: dial tcp 172.19.0.12:8888: i/o timeout 2025/02/19 11:14:31 9, dial error: dial tcp 172.19.0.12:8888: i/o timeout 我们来分析下上述结果：\n1. 全连接队列是否已满 服务端 Listen 状态的 socket 显示 Send-Q 为 5，表示该 socket 的全连接队列最大值为 5；Recv-Q 为 6，表示当前 Accept queue 中数量为 6，我们看有 6 条 ESTAB 状态的连接，符合观察结果。Linux 内核的判断依据是 \u0026gt; 而不是 \u0026gt;=，所以实际的连接数为比队列的最大值多 1 个。5.15.0-130-generic 内核代码如下：\n1 2 3 4 5 6 7 8 9 10 // 源码地址 // https://elixir.bootlin.com/linux/v5.15.130/source/include/net/sock.h#L980 /* Note: If you think the test should be: *\treturn READ_ONCE(sk-\u0026gt;sk_ack_backlog) \u0026gt;= READ_ONCE(sk-\u0026gt;sk_max_ack_backlog); * Then please take a look at commit 64a146513f8f (\u0026#34;[NET]: Revert incorrect accept queue backlog changes.\u0026#34;) */ static inline bool sk_acceptq_is_full(const struct sock *sk) { return READ_ONCE(sk-\u0026gt;sk_ack_backlog) \u0026gt; READ_ONCE(sk-\u0026gt;sk_max_ack_backlog); } 之所以这样做，是为了保证在 backlog 设置为 0 时，依然可以有一个连接进入全连接队列，具体可以查看以下 commit 信息：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 https://github.com/torvalds/linux/commit/64a146513f8f12ba204b7bf5cb7e9505594ead42 [NET]: Revert incorrect accept queue backlog changes. This reverts two changes: 8488df8 248f067 A backlog value of N really does mean allow \u0026#34;N + 1\u0026#34; connections to queue to a listening socket. This allows one to specify \u0026#34;0\u0026#34; as the backlog and still get 1 connection. Noticed by Gerrit Renker and Rick Jones. Signed-off-by: David S. Miller \u0026lt;davem@davemloft.net\u0026gt; 2. 内核 drop 包处理逻辑 客户端有 6 个 ESTAB 状态的 socket，另外还有 4 个 SYN-SENT 状态的 socket，对应着 4 条 timeout 报错信息。我们只改了全连接队列大小为 5，半连接队列大小依然为默认的 net.ipv4.tcp_max_syn_backlog=256，所以第 6 个请连接建立后 Accept Queue 满了但 SYN Queue 还没有满。按理说从第 7 个请求开始服务端可以接收 SYN 但不能在处理客户端的 ACK 进入 Accept Queue，服务端会有 4 条 SYN-RECV 状态的连接，而实际情况是服务端不存在 SYN_RECV 状态的连接，这是因为当 Accept Queue 被占满时，即使 SYN Queue 没有满，Linux 内核也会将新来的 SYN 请求丢弃掉。 5.15.0-130-generic 内核处理这部分逻辑的代码如下：：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 // 源码地址：https://elixir.bootlin.com/linux/v5.15.130/source/net/ipv4/tcp_input.c#L6848 int tcp_conn_request(struct request_sock_ops *rsk_ops, const struct tcp_request_sock_ops *af_ops, struct sock *sk, struct sk_buff *skb) { // ... 代码省略 syncookies = READ_ONCE(net-\u0026gt;ipv4.sysctl_tcp_syncookies); /* TW buckets are converted to open requests without * limitations, they conserve resources and peer is * evidently real one. */ // 强制启用 SYN cookie 或者半连接队列已满 // !isn 表示是一个新的请求连接建立的 SYN if ((syncookies == 2 || inet_csk_reqsk_queue_is_full(sk)) \u0026amp;\u0026amp; !isn) { // 这里表示是否启用 SYN cookie 机制；如果不开启，则直接 drop，如果开启，则继续执行。 want_cookie = tcp_syn_flood_action(sk, rsk_ops-\u0026gt;slab_name); if (!want_cookie) goto drop; } // 如果 accept queue 满了则 drop if (sk_acceptq_is_full(sk)) { NET_INC_STATS(sock_net(sk), LINUX_MIB_LISTENOVERFLOWS); goto drop; } static bool tcp_syn_flood_action(const struct sock *sk, const char *proto) { struct request_sock_queue *queue = \u0026amp;inet_csk(sk)-\u0026gt;icsk_accept_queue; const char *msg = \u0026#34;Dropping request\u0026#34;; struct net *net = sock_net(sk); bool want_cookie = false; u8 syncookies; syncookies = READ_ONCE(net-\u0026gt;ipv4.sysctl_tcp_syncookies); // 开启 SYN Cookie 机制 #ifdef CONFIG_SYN_COOKIES if (syncookies) { msg = \u0026#34;Sending cookies\u0026#34;; want_cookie = true; __NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPREQQFULLDOCOOKIES); } else #endif // 没有启用 syncookies，统计丢弃包的数量 __NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPREQQFULLDROP); // 如果启用了 SYN cookie 机制，发送警告 if (!queue-\u0026gt;synflood_warned \u0026amp;\u0026amp; syncookies != 2 \u0026amp;\u0026amp; xchg(\u0026amp;queue-\u0026gt;synflood_warned, 1) == 0) net_info_ratelimited(\u0026#34;%s: Possible SYN flooding on port %d. %s. Check SNMP counters.\\n\u0026#34;, proto, sk-\u0026gt;sk_num, msg); return want_cookie; } // 判断半连接队列是否满，用的是半连接队列的长度是否大于等于全连接队列的最大长度 static inline int inet_csk_reqsk_queue_is_full(const struct sock *sk) { return inet_csk_reqsk_queue_len(sk) \u0026gt;= sk-\u0026gt;sk_max_ack_backlog; } 从代码中可以推测出 net.ipv4.tcp_syncookies 参数值的含义和 Linux 的处理机制：\n2：强制开启 SYN Cookie 机制，发送警告 1：当半连接队列满时，开启 SYN Cookie 机制，发送警告 0：不开启 SYN Cookie 机制，并统计丢弃包的数量 这里判断半连接队列是否满的依据是 inet_csk_reqsk_queue_len(sk) \u0026gt;= sk-\u0026gt;sk_max_ack_backlog，也就是说当半连接队列长度不小于全连接队列的最大长度时，如果不开启 SYN Cookie 机制，就会将 SYN 包丢弃。\n回到我们的实验环境，net.ipv4.tcp_syncookies 设置为 1 并且半连接队列没满，因此不会开启 SYN Cookie 机制，继续往后执行时会因为 Accept Queue 满了将包丢弃。可以通过 netstat -s 命令查看丢弃包的数量。\n1 2 3 4 5 6 7 8 $ date;netstat -s | grep -i \u0026#34;SYNs to LISTEN\u0026#34; Wed Feb 19 12:05:51 PM CST 2025 1289 SYNs to LISTEN sockets dropped $ date;netstat -s | grep -i \u0026#34;SYNs to LISTEN\u0026#34; Wed Feb 19 12:06:05 PM CST 2025 1301 SYNs to LISTEN sockets dropped 可以看到有 12 个 SYN 包被 DROP 了，查看抓包情况可以看到，我们有 4 个请求连接超时，每个请求传了 3 次 SYN（一次发起 + 两次重传）。\n查看客户端 socket 状态能够看到重传计时器在工作，这里重传了两次和默认的 net.ipv4.tcp_syn_retries = 6 有出入，是因为代码 conn, err := net.DialTimeout(\u0026quot;tcp\u0026quot;, \u0026quot;172.19.0.12:8888\u0026quot;, time.Second*5)设置了 5s 超时，操作系统的默认重传间隔大约为 1s、2s、4s、8s、16s、32s，第 3 次重传会发生在 7s 以后，客户端已经主动断开连接了。\n1 2 3 4 5 6 7 8 9 10 11 12 $ sudo netstat -anpo | grep -E \u0026#34;Recv-Q|8888\u0026#34; Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name Timer tcp 0 0 172.19.0.15:57384 172.19.0.12:8888 ESTABLISHED 3123924/client keepalive (7.57/0/0) tcp 0 0 172.19.0.15:57388 172.19.0.12:8888 ESTABLISHED 3123924/client keepalive (8.07/0/0) tcp 0 0 172.19.0.15:60276 172.19.0.12:8888 ESTABLISHED 3123924/client keepalive (9.58/0/0) tcp 0 1 172.19.0.15:60304 172.19.0.12:8888 SYN_SENT 3123924/client on (0.08/1/0) tcp 0 1 172.19.0.15:60286 172.19.0.12:8888 SYN_SENT 3123924/client on (2.60/2/0) tcp 0 0 172.19.0.15:60270 172.19.0.12:8888 ESTABLISHED 3123924/client keepalive (9.08/0/0) tcp 0 0 172.19.0.15:60280 172.19.0.12:8888 ESTABLISHED 3123924/client keepalive (10.08/0/0) tcp 0 1 172.19.0.15:60292 172.19.0.12:8888 SYN_SENT 3123924/client on (3.11/2/0) tcp 0 0 172.19.0.15:57398 172.19.0.12:8888 ESTABLISHED 3123924/client keepalive (8.57/0/0) tcp 0 1 172.19.0.15:60294 172.19.0.12:8888 SYN_SENT 3123924/client on (3.62/2/0) 3. overflow 参数控制 当全连接队列满时，Linux 默认会 drop 掉包，这个受 net.ipv4.tcp_abort_on_overflow 参数控制，默认为 0 表示直接 drop，为 1 则表示中断连接，服务端会返回 RST 包。可以通过如下方式修改\n1 2 3 4 5 $ sudo sysctl -w net.ipv4.tcp_abort_on_overflow=1 或者 echo 1 \u0026gt; /proc/sys/net/ipv4/tcp_abort_on_overflow 我们修改参数后再次执行客户端请求，会出现 connection reset by peer 错误，抓包能看到 RST 包。(在实验时，如果客户端不加时间间隔，会出现返回 RST 包的情况，如果加了则不会出现这种情况，应该是和两者的生效机制有关，SYN Cookie 和全连接队列满 drop 发生在 tcp_conn_request 函数，而 abort_on_overflow 发生在 tcp_check_req 函数， 先挖个坑，等后续梳理整个网络传输流程时在做进一步分析)。\n1 2 3 4 5 6 7 8 9 $ go run client.go 2025/03/01 13:36:55 2, dial success 2025/03/01 13:36:55 5, dial success 2025/03/01 13:36:55 4, dial success 2025/03/01 13:36:55 1, dial success 2025/03/01 13:36:55 3, dial success 2025/03/01 13:36:55 0, dial success 2025/03/01 13:36:55 7, dial error: dial tcp 172.19.0.12:8888: connect: connection reset by peer 2025/03/01 13:36:55 6, dial error: dial tcp 172.19.0.12:8888: connect: connection reset by peer 4. ss 命令展示含义 服务端有 6 条 ESTAB 状态的 socket，RECV_Q 的值为 11，与客户端发送的数据 []byte(\u0026quot;hello world\u0026quot;) 数据长度一致，因为我们的没有执行 accept 接收数据，所以 RECV_Q 会展示这部分数据的大小；\n客户端 6 条 ESTAB 状态的 socket，其 RECV_Q 和 SEND_Q 均为 0；而 4 条 SYN-SENT 状态的 SEND-Q 为 1，这是因为 6 条已建立连接的 socket 包可以被正常 ACK，而 4 条建立连接失败的 socket，其 SYN 包没有收到 ACK 包，因为 SEND-Q 显示为 1。由此我们可以再次总结下 ss 的展示含义：\n对于 LISTEN 状态的 socket\nRecv-Q：表示当前全连接队列的大小，即已完成三次握手等待应用程序 accept() 的 TCP 连接数。 Send-Q：全连接队列的最大长度，即全连接队列所能容纳的 socket 数量。 对于非 LISTEN 状态的 socket\nRecv-Q：表示已被接收但尚未执行 accept 被应用程序读取的数据字节数，通常在服务端能观察到。 Send-Q：表示已经发送但尚未收到 ACK 确认的字节数。 内核代码如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 // https://elixir.bootlin.com/linux/v5.15.130/source/net/ipv4/tcp_diag.c#L18 static void tcp_diag_get_info(struct sock *sk, struct inet_diag_msg *r, void *_info) { struct tcp_info *info = _info; if (inet_sk_state_load(sk) == TCP_LISTEN) { // LISTEN 状态的连接 // 当前已完成三次握手但未被 accept 的连接数 r-\u0026gt;idiag_rqueue = READ_ONCE(sk-\u0026gt;sk_ack_backlog); // 最大队列长度 r-\u0026gt;idiag_wqueue = READ_ONCE(sk-\u0026gt;sk_max_ack_backlog); } else if (sk-\u0026gt;sk_type == SOCK_STREAM) { // 非 LISTEN 状态的普通连接 const struct tcp_sock *tp = tcp_sk(sk); // TCP 读队列，即接收缓冲区中未被应用层读取的数据量，单位是字节 r-\u0026gt;idiag_rqueue = max_t(int, READ_ONCE(tp-\u0026gt;rcv_nxt) - READ_ONCE(tp-\u0026gt;copied_seq), 0); // TCP 写队列，即已经发送但尚未被对方 ACK 确认的数据量，单位是字节 r-\u0026gt;idiag_wqueue = READ_ONCE(tp-\u0026gt;write_seq) - tp-\u0026gt;snd_una; } if (info) tcp_get_info(sk, info); } 5. SYN+ACK 重传 原实验有三种情况：\n三次握手成功，数据正常发送 客户端认为连接建立成功，但服务端一直处于 SYN-RECV 状态，不断重传 SYN + ACK 客户端发送 SYN 未得到响应一直在重传 我们复现了第 1 中和第 3 种，之所以没有第二种情况是因为每次请求加了 500ms 的间隔，这样下一个请求发起 SYN 时，上一个请求已经完成三次握手，服务端的 socket 已经进入全连接队列了。如果我们去掉时间间隔，请求可能会一下子发出去全部进入半连接队列，等到服务端在接收到客户端的 ACK 包时，全连接队列已经满了，从而导致服务端的 socket 无法进入全连接队列，从而 DROP 掉 ACK 包出现第二种情况。这里我们去掉时间间隔尝试复现，此时可以看到服务端有 SYN-RECV 状态的连接，\n1 2 3 4 5 6 7 8 9 10 11 $ ss -ant | grep -E \u0026#34;Recv|8888\u0026#34; State Recv-Q Send-Q Local Address:Port Peer Address:Port Process LISTEN 6 5 *:8888 *:* ESTAB 11 0 [::ffff:172.19.0.12]:8888 [::ffff:172.19.0.15]:33430 ESTAB 11 0 [::ffff:172.19.0.12]:8888 [::ffff:172.19.0.15]:33458 ESTAB 11 0 [::ffff:172.19.0.12]:8888 [::ffff:172.19.0.15]:33482 SYN-RECV 0 0 [::ffff:172.19.0.12]:8888 [::ffff:172.19.0.15]:33512 ESTAB 11 0 [::ffff:172.19.0.12]:8888 [::ffff:172.19.0.15]:33442 ESTAB 11 0 [::ffff:172.19.0.12]:8888 [::ffff:172.19.0.15]:33428 ESTAB 11 0 [::ffff:172.19.0.12]:8888 [::ffff:172.19.0.15]:33472 SYN-RECV 0 0 [::ffff:172.19.0.12]:8888 [::ffff:172.19.0.15]:33496 查看抓包结果可以看到 SYN-ACK 包重传。 全连接队列的实验就到这里，下面我们来看半连接队列的实验。\n半连接队列实战 半连接队列的最大长度计算有些麻烦，网络上资料也很繁杂，本着 talk is cheap, show me the code 的原则，这里还是直接看 Linux 的源码来分析，还是 tcp_conn_request 函数。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 // 源码地址：https://elixir.bootlin.com/linux/v5.15.130/source/net/ipv4/tcp_input.c#L6848 int tcp_conn_request(struct request_sock_ops *rsk_ops, const struct tcp_request_sock_ops *af_ops, struct sock *sk, struct sk_buff *skb) { // ... 代码省略 u8 syncookies; // 第一部分，基于 syncookies 和半连接队列是否超过全连接队列长度、半连接队列是否已满来判断是否 drop syncookies = READ_ONCE(net-\u0026gt;ipv4.sysctl_tcp_syncookies); if ((syncookies == 2 || inet_csk_reqsk_queue_is_full(sk)) \u0026amp;\u0026amp; !isn) { want_cookie = tcp_syn_flood_action(sk, rsk_ops-\u0026gt;slab_name); if (!want_cookie) goto drop; } // 第二部分，判断全连接队列是否已满 if (sk_acceptq_is_full(sk)) { NET_INC_STATS(sock_net(sk), LINUX_MIB_LISTENOVERFLOWS); goto drop; } req = inet_reqsk_alloc(rsk_ops, sk, !want_cookie); if (!req) goto drop; // ... 代码省略 if (!want_cookie \u0026amp;\u0026amp; !isn) { // 获取系统参数 ``net.ipv4.tcp_max_syn_backlog`` 的值 int max_syn_backlog = READ_ONCE(net-\u0026gt;ipv4.sysctl_max_syn_backlog); /* Kill the following clause, if you dislike this way. */ // 第三部分：判断半连接队列是否超过长度限制 if (!syncookies \u0026amp;\u0026amp; (max_syn_backlog - inet_csk_reqsk_queue_len(sk) \u0026lt; (max_syn_backlog \u0026gt;\u0026gt; 2)) \u0026amp;\u0026amp; !tcp_peer_is_proven(req, dst)) { /* Without syncookies last quarter of * backlog is filled with destinations, * proven to be alive. * It means that we continue to communicate * to destinations, already remembered * to the moment of synflood. */ pr_drop_req(req, ntohs(tcp_hdr(skb)-\u0026gt;source), rsk_ops-\u0026gt;family); goto drop_and_release; } isn = af_ops-\u0026gt;init_seq(skb); } tcp_ecn_create_request(req, skb, sk, dst); if (want_cookie) { isn = cookie_init_sequence(af_ops, sk, skb, \u0026amp;req-\u0026gt;mss); if (!tmp_opt.tstamp_ok) inet_rsk(req)-\u0026gt;ecn_ok = 0; } return 0; } 核心计算逻辑是 (max_syn_backlog - inet_csk_reqsk_queue_len(sk) \u0026lt; (max_syn_backlog \u0026gt;\u0026gt; 2))，即 max_syn_backlog 的值减去当前半连接队列的长度的值小于 max_syn_backlog 的 1/4 时，就会将 SYN 包丢弃。简单来说就是半连接队列长度不能超过 max_syn_backlog 的 3/4。因为比较条件是 \u0026gt; 而不是 \u0026gt;=，所以在不开启 syncookies 的情况下，实际的半连接队列长度应该是 max_syn_backlog 的 3/4 + 1。大致计算如下：\nmax_syn_backlog 为 128，则半连接队列长度最大为 97 max_syn_backlog 为 256，则半连接队列长度最大为 193 max_syn_backlog 为 512，则半连接队列长度最大为 385 max_syn_backlog 为 1024，则半连接队列长度最大为 769 结合上面全连接实验中的代码分析，我们可以总结下 Linux 5.15.30 内核下 SYN 包的 Drop 机制：\n我们修改参数验证下上述三种情况。\n实验一：关闭 syncookies，半连接长度超过全连接最大长度 客户端我们使用 iptables 将服务端的包拦截，模拟 SYN Flood 攻击，这样服务端不会收到 ACK 包，也就不会进入全连接队列。系统参数 syn_cookies=0，max_syn_backlog=128，somaxconn=64，理论上会有 64 个 SYN-RECV 状态连接，其余的包被丢弃。\n1 2 3 4 5 # 拦截服务端 8888 端口的包 $ sudo iptables -A INPUT -p tcp --sport 8888 -j DROP # 发送 SYN 包 $ sudo hping3 -S 172.19.0.12 -p 8888 --flood 查看服务端情况\n1 2 3 4 5 6 7 $ ss -ant | grep -E \u0026#34;Recv|8888\u0026#34; State Recv-Q Send-Q Local Address:Port Peer Address:Port Process LISTEN 0 64 *:8888 *:* # ubuntu @ node2 in ~ [11:58:11] $ sudo netstat -nat | grep :8888 | grep SYN_RECV | wc -l 64 结果符合预期。这里可以用 go 客户端做更精确的验证，我们使用 Go 程序发送 100 个请求，然后查看服务端连接数和 DROP 数\n1 2 3 4 5 6 7 8 9 10 $ date;netstat -s | grep -i \u0026#34;SYNs to LISTEN\u0026#34; Fri Feb 21 12:01:58 PM CST 2025 3030591019 SYNs to LISTEN sockets dropped $ sudo netstat -nat | grep :8888 | grep SYN_RECV | wc -l 64 $ date;netstat -s | grep -i \u0026#34;SYNs to LISTEN\u0026#34; Fri Feb 21 12:02:14 PM CST 2025 3030591127 SYNs to LISTEN sockets dropped 可以看到服务端只有 64 个 SYN-RECV 状态连接，程序执行有有 3030591127-3030591019=108 个 SYN 包被丢弃。上面我们分析过，因为客户端设置了超时时间为 5s，所以 SYN 只会重传 2 次，也就是每个被 DROP 的连接都会发送 3 次 SYN。100 - 64 = 36，36 * 3 = 108，符合我们预期。\n实验二：关闭 syncookies，全连接队列已满 修改服务端系统参数 syn_cookies=0，max_syn_backlog=128，somaxconn=64，这样全连接队列最大长度为 64，当有 65 个连接建立时，全连接队列就会满，此时再有 SYN 包建立连接时就会被丢弃。\n首先我们清理掉客户端机器的 iptables 规则，是的三次握手能够正常进程。\n1 $ sudo iptables -F 设置系统参数\n1 2 3 $ sudo sysctl -w net.ipv4.tcp_syncookies=0 $ sudo sysctl -w net.ipv4.tcp_max_syn_backlog=128 $ sudo sysctl -w net.core.somaxconn=64 我们再次用 Go 客户端发送 100 个请求，然后查看服务端状态，可以看到有 65 个 ESTAB 状态连接，没有 SYN-RECV 状态连接，因为全连接队列已满，所有 SYN 包都会被丢弃。\n1 2 3 4 5 6 7 8 9 10 11 12 $ ss -ant | grep -E \u0026#34;Recv|8888\u0026#34; State Recv-Q Send-Q Local Address:Port Peer Address:Port Process LISTEN 65 64 *:8888 $ sudo netstat -nat | grep :8888 | grep ESTAB | wc -l 65 # ubuntu @ node2 in ~ [12:18:27] C:130 $ sudo netstat -nat | grep :8888 | grep SYN_RECV | wc -l 0 按照以上逻辑，会有 35 个连接被拒绝，一共有 35 * 3 = 105 个 SYN 包被丢弃。我们查看统计信息可以验证，3030591766 - 3030591661 = 105，符合预期。\n1 2 3 4 5 6 7 8 $ date;netstat -s | grep -i \u0026#34;SYNs to LISTEN\u0026#34; Fri Feb 21 12:18:19 PM CST 2025 3030591661 SYNs to LISTEN sockets dropped # ubuntu @ node2 in ~ [12:18:19] $ date;netstat -s | grep -i \u0026#34;SYNs to LISTEN\u0026#34; Fri Feb 21 12:18:34 PM CST 2025 3030591766 SYNs to LISTEN sockets dropped 实验三：关闭 syncookies，半连接队列长度超过 max_syn_backlog 的 3/4 现在我们将全连接队列长度调大 net.core.somaxconn 设置为 4096，使用 iptables 拦截服务端 8888 端口的包，这样全连接队列始终不会填满，然后 max_syn_backlog 分别设置为：\n128，预期有 97 个 SYN-RECV 状态连接 256，预期有 193 个 SYN-RECV 状态连接 512，预期有 385 个 SYN-RECV 状态连接 1024，预期有 769 个 SYN-RECV 状态连接 分别设置并发送请求后，服务端显示结果如下，基本符合预期。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 # 客户端设置 iptables 拦截服务端 sudo iptables -A INPUT -p tcp --sport 8888 -j DROP # 服务端查看 SYN-RECV 状态连接数 $ sudo sysctl -w net.ipv4.tcp_max_syn_backlog=128 $ ss -ant | grep -E \u0026#34;Recv|:8888\u0026#34; | grep SYN-RECV | wc -l 97 $ sudo sysctl -w net.ipv4.tcp_max_syn_backlog=256 $ ss -ant | grep -E \u0026#34;Recv|:8888\u0026#34; | grep SYN-RECV | wc -l 193 $ sudo sysctl -w net.ipv4.tcp_max_syn_backlog=512 $ ss -ant | grep -E \u0026#34;Recv|:8888\u0026#34; | grep SYN-RECV | wc -l 385 $ sudo sysctl -w net.ipv4.tcp_max_syn_backlog=1024 $ ss -ant | grep -E \u0026#34;Recv|:8888\u0026#34; | grep SYN-RECV | wc -l 769 执行过程数值会有变化，但最大半连接队列长度符合预期。\n实验四：开启 syncookies，半连接队列长度取决于 max(somaxconn, backlog) 当开启 syncookies 时，半连接队列不在保留 1/4 的限制，而是取决于 max(somaxconn, backlog)。这里源码判断是 \u0026gt;=，因此最大长度应该会等于 max(somaxconn, backlog)\n1 2 3 4 5 // 源码地址：https://elixir.bootlin.com/linux/v5.15.130/source/include/net/inet_connection_sock.h#L280 static inline int inet_csk_reqsk_queue_is_full(const struct sock *sk) { return inet_csk_reqsk_queue_len(sk) \u0026gt;= sk-\u0026gt;sk_max_ack_backlog; } 我们分别设置 net.core.somaxconn 为 512，1024，4096并设置 net.ipv4.tcp_syncookies=1 开启 syncookies，每次设置完重启服务端，然后在发起请求，理论上会有 512，1024，4096 个 SYN-RECV 状态连接。\n修改服务端 somaxconn 并重启后，使用 watch 命令查看 SYN-RECV 状态连接数，结果如下，符合预期。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 $ watch -n 1 \u0026#34;netstat -nat | grep :8888 | grep SYN_RECV | wc -l\u0026#34; Every 1.0s: netstat -nat | grep :8888 | grep SYN_RECV | wc -l node2: Sat Mar 1 15:07:22 2025 512 $ watch -n 1 \u0026#34;netstat -nat | grep :8888 | grep SYN_RECV | wc -l\u0026#34; Every 1.0s: netstat -nat | grep :8888 | grep SYN_RECV | wc -l node2: Sat Mar 1 15:08:15 2025 1024 $ watch -n 1 \u0026#34;netstat -nat | grep :8888 | grep SYN_RECV | wc -l\u0026#34; Every 1.0s: netstat -nat | grep :8888 | grep SYN_RECV | wc -l node2: Sat Mar 1 15:09:11 2025 4096 简要总结 半连接队列受限于全连接队列长度，而全连接队列会受应用的影响，尽量不要将 somaxconn 设置的过小，否则会影响服务器的性能。 尽量开启 syncookies，可以有效防止 SYN Flood 攻击，同时可以避免半连接队列被大量占用。 ss、netstat 的熟练使用对探查网络状态非常重要，要熟练掌握。 代码之下无秘密，一定要结合源码去理解 Linux 的网络工作机制，不要只是死记硬背协议。 动手！动手！动手！实践出真知。 ","permalink":"https://zouyingjie.github.io/posts/tcp%E5%8D%8A%E8%BF%9E%E6%8E%A5%E9%98%9F%E5%88%97%E5%85%A8%E8%BF%9E%E6%8E%A5%E9%98%9F%E5%88%97%E5%AE%9E%E9%AA%8C/","summary":"\u003cp\u003e本文是对 \u003ca href=\"https://www.51cto.com/article/687595.html\"\u003e从一次线上问题说起，详解 TCP 半连接队列、全连接队列\u003c/a\u003e 这篇文章的实验复现和总结，借此加深对 TCP 半连接队列、全连接队列的理解。\u003c/p\u003e\n\u003ch2 id=\"实验环境\"\u003e实验环境\u003c/h2\u003e\n\u003cp\u003e两台腾讯云服务器 node2（172.19.0.12） 和 node3（172.19.0.15）配置为 2C4G，Ubuntu 系统，内核版本 5.15.0-130-generic 。\u003c/p\u003e","title":"【动手实验】TCP半连接队列、全连接队列实战分析"},{"content":"iptables 是 Linux 下的一个非常重要的网络包过滤工具，可以用来配置防火墙、NAT 等。学习 iptables 有助于理解网络包的传输过程、网络安全等。Kubernetes 在 1.31 版本中引入 nfttables 前，也使用 iptables 作为 kube-proxy 的实现。\niptables 的原理和使用方法网上有很多资料，这里推荐一些个人觉得不错的材料。\nIllustrated introduction to Linux iptables 一篇非常好的介绍 iptables 的入门文章，给出了大量的图例帮助理解。比如 iptables 是基于 netfilter 的 hook 点工作的。文章给出的图示非常直观的将 5 个 hook 的生效位置展现了出来。\nA Deep Dive into Iptables and Netfilter Architecture 这篇文章讲的更细致一些，对表的功能、优先级、规则做了比较全面的介绍，非常浅显易懂。读英文觉得困难的话也可以看这篇译文 (译)深入理解 iptables 和 netfilter 架构。\n读完上述两篇文章，对 iptables 就能有个基本的理解了。\n简单来说，iptables 就是使用 netfilter 提供的 hook 点来注册 IP 包的处理规则， iptables 基于功能（过滤、地址转换、修改包、追踪、安全）将这些规则组织成了不同的表。\n每个表有若干个规则链，iptables 默认有 5 种链，对应 netfilter 的 5 个 hook 点。在某个表的某个链上添加规则，本质上就是在链对应的 hook 点注册规则。如果多个表在同一个链上注册规则，则基于表的优先级生效。\n结合包的传输顺序、表的作用点和优先级，就形成了文章中的二维表格：\nTables 列有由上到下代表的 table 的执行顺序，由此我们可以得出处理网络包时，iptables 规则执行顺序：\n发送到本机的包：PREROUTING(raw, mangle, dnat) -\u0026gt; INPUT(mangle, filter, security, snat) 本机路由到其他机器的包：PREROUTING(raw, mangle, dnat) -\u0026gt; FORWARD(mangle, filter, security) -\u0026gt; POSTROUTING(mangle, snat) 本地发送到其他机器的包：OUTPUT(raw, mangle, dnat, filter, security) -\u0026gt; POSTROUTING（mangle, snat） 理解了上述内容，在看下面这张经典的配图就不至于头大了。\nNetwork Address Translation 这篇文章对 iptables 中的 NAT 做了更细致的讲解。也可以看这篇译文 (译)# NAT - 网络地址转换。\niptables 快速入门系列文章 国内一位技术人员写的一系列关于 iptables 的文章，有十几篇，从基础到进阶都有涉及，通俗易懂的介绍了 iptables 的概念、命令、用法，非常值得一读。\n","permalink":"https://zouyingjie.github.io/posts/iptables%E5%AD%A6%E4%B9%A0%E8%B5%84%E6%96%99%E6%8E%A8%E8%8D%90/","summary":"\u003cp\u003eiptables 是 Linux 下的一个非常重要的网络包过滤工具，可以用来配置防火墙、NAT 等。学习 iptables 有助于理解网络包的传输过程、网络安全等。Kubernetes 在 1.31 版本中引入 nfttables 前，也使用 iptables 作为 kube-proxy 的实现。\u003c/p\u003e","title":"【资料推荐】一些学习iptables的优质材料"},{"content":"我是谁 WHO AM I？ ","permalink":"https://zouyingjie.github.io/page/about/","summary":"\u003ch2 id=\"我是谁-who-am-i\"\u003e\u003cstrong\u003e我是谁 WHO AM I？\u003c/strong\u003e\u003c/h2\u003e","title":"关于我"},{"content":"寻雾 记录软件工程、分布式系统、云原生与 AI 时代的工程思考。\nSo you have to trust that the dots will somehow connect in your future.\n","permalink":"https://zouyingjie.github.io/about/","summary":"关于寻雾。","title":"关于"}]