c100m 问题

互联网诞生以来,如何让一台服务器服务更多的用户就成为了软件工程师一直试图解决的难题。c10k1 指的是如何让一台服务器同时服务 10k 个用户的连接。工程师们发明了一种又一种技术方案来挑战性能的极限,Event driven IO,Async IO,Erlang,等等。Whatsapp 用 Erlang 在 24 核心的机器上支持了 2百万 个连接,MigratoryData 用 12 核心的机器,Java 语言,支持了 1千万的连接。虽然技术进步,硬件也在进步,这项性能挑战来到了 c100m,一亿个连接……

一亿个连接是什么概念呢?就算微信这种超大体量的用户,只需要几台机器就可以提供接入服务了。

本文小试牛刀,用 Linux 网络栈来尝试建立和保持尽可能多的连接。但是止步于 TCP 连接的建立,不做数据传输,所以除了好玩,没有实际意义。

实验针对的是主动发起连接的一侧,这样比较好控制速率。

实验的环境是,client 端用一个程序发起 TCP 连接建立,server 端用上一篇博文介绍的 XDP bounce 程序来回复 SYN-ACK 包2,假装建立好连接。然后我们观察 client 端最多可以建立多少连接。仅仅是建立连接而已,不会做数据发送。

Client 端的代码如下,代码来自 c1000k3,稍微做了修改,支持了并发,这样建立连接速度快一些。

代码的逻辑是:fork 1000 个进程,每一个进程对一个 dest port 建立 10000 个连接,总共是1千万个连接数。

实验需要两台机器对着发,因为 server 端使用的是 XDP,性能很高,所以 1核心的机器就足够了。

Client 端我用的 8 核心的机器。

需要进行配置的内容

  1. 修改 ulimit,这样程序才能用更多的 fd:ulimit -n 1048576;
  2. 修改 TCP keepalive,keepalive 会消耗额外的资源:sudo sysctl -w net.ipv4.tcp_keepalive_time=720000
  3. 如果内存不够,通过 swap 来扩展;
  4. 使用 cgroups 限制程序能用的 CPU,防止 ssh 都连不上:

几分钟就可以跑到 1千万连接,轻轻松松。

下面可以修改参数来尝试 1亿连接数:

还是用上面的环境,跑到3千万连接的时候虚拟机崩溃了,只提示 fatal error。

我又去 DigitalOcean 开了台虚拟机,看能不能跑到更高。

这次跑到 5千万连接机器又崩溃了。

约5千万连接数

到之类就没办法继续研究是什么原因了,可能是虚拟化的问题,如果折腾一下物理机环境,说不定可以跑到1亿。

另外我发现一个有趣的地方,如果 abort client 程序,大量连接的 fd 需要 kernel 去回收,会造成所有的 CPU 100% kernel state,机器几乎卡死了,直到连接回收完毕。这部分好像是用 cgroups 限制不住的。所以,如果在不可信的共享的执行环境,通过建立大量的连接再退出进程,说不定有可能恶意挤兑其他租户的资源?

  1. https://en.wikipedia.org/wiki/C10k_problem ↩︎
  2. XDP 实现所有的 TCP 端口都接受 TCP 建立连接 ↩︎
  3. https://github.com/ideawu/c1000k ↩︎
 

XDP 实现所有的 TCP 端口都接受 TCP 建立连接

一个 XDP 练习程序:作为 TCP 的 server 端,用 XDP 实现所有的 TCP 端口都接受 TCP 建立连接。(只是能够建立连接而已,无法支持后续的 TCP 数据传输,所以不具有实际意义,纯粹好玩。)

建立 TCP 连接需要实现 TCP 的三次握手,对于 server 端来说,要实现:

  • 收到 SYN 包,回复 SYN-ACK 包;
  • 收到 ACK 包,因为这里不再需要对客户端回复什么,所以这个包收到之后直接 DROP 即可。

回复 SYN-ACK 包就有些麻烦。XDP 不能主动发出包,它能做的就是在收到包的时候,决定对这个包执行何种 action,支持的 action 如下:

  • XDP_DROP
  • XDP_PASS
  • XDP_TX – 将数据包直接从接收的网卡原路回送出去,等同于 MAC 层 loopback,适用于构造 L2 层反射或快速回应场景。注意并不支持构造完全新包,只能修改现有包;
  • XDP_REDIRECT – 将数据包重定向到其他网卡或用户空间(如使用 AF_XDP),常用于 zero-copy 的高速转发;
  • XDP_ABORTED – 用于调试,表示程序异常终止,包被丢弃;

为了实现 TCP 的 SYN-ACK 回复,这里我们可以选择 XDP_TX ——在收到包之后,对包的内容进行一些修改,比如把 SYN flag 改成 SYN+ACK flag,然后把包重新回送出去,对方收到这个包,其实也不知道是 XDP 返回的还是 Linux kernel 返回的。在 XDP_TX 程序的机器上,Kernel 网络栈根本不知道这个包的存在。

XDP 程序直接从网卡的驱动返回包

现在的重点在于如何修改这个 TCP SYN 包,并将其回送,使对方认为它是一个合法的 SYN-ACK 包。

我们可以从下往上一层一层看:

  • Ether 层:只需要交换 Src MAC 地址和 Dst MAC 地址就可以了。这样的话,直接从 LAN 主机发过来的包会发回去 LAN IP,从 LAN 网关发来的包也会发回网关;
    • CRC 校验码一般是网卡硬件负责计算的,所以 Linux 代码不需要处理;
  • IP 层:交换 Src IP 和 Dst IP 即可。
    • IP checksum 这里也不需要我们手动添加,现在的路由器大部分都是不计算 checksum 的1
  • TCP 层:
    • 交换 Src Port 和 Dst Port;
    • Flags 把 SYN 和 ACK 都设置为 1;
    • 把 ACK 字段,设置为 ack = SYN 包的 seq + 1,以确认对端的 SYN。;
    • 填写 seq 字段,因为不涉及后续的数据传输了,这里使用一个固定值即可;
    • 重新计算 TCP checksum。重新计算 TCP checksum 是最麻烦的一步,因为在 eBPF/XDP 程序中不能依赖内核自动计算,需要手动构造伪头部(pseudo-header)并累加 TCP 包体数据。所以我们要用 XDP 的代码重新实现 TCP 的 checksum。还要让 XDP 的 verifier2 认为我们写的代码是安全的,所以复杂一些。

因为这个程序直接把收到的 TCP SYN 包远路反弹,就叫它 tcp_bounce.c 吧。(这周末刚去了一个叫 Bounce 的地方团建……)

XDP 程序的源代码如下:

安装编译 XDP 程序需要的依赖:

安装 libc 开发包依赖,如果是 x86 操作系统:apt-get install -y libc6-dev-i386;如果是 ARM 操作系统:apt-get install -y libc6-dev-arm64-cross.

编译程序:

把 xdp 程序加载到网卡上:

然后从另一台机器对这个加载了 XDP 程序 tcp_bounce.o 发起 TCP 连接,对于任意端口,可以观察到连接建立成功了:

用 nc 随机对两个端口建立连接

也可以用 for 循环批量对端口建立连接,都可以连通。

for i in {5000..5010}; do nc -vz 172.16.199.22 ${i};done

XDP 的性能很高,客户端用 10000 个线程同时建立 TCP 连接,服务端的 XDP 程序使用了连 10% 都不到的 CPU。(Again,但是没有什么实际意义)

  1. 网络中的环路和防环技术 有提到过,IPv6 是直接取消了 checksum 字段。 ↩︎
  2. eBPF verifier ↩︎
 

Django 全局禁用外键

Django ORM 是我最喜欢的 ORM,它自带了全套数据库管理的解决方案,开箱即用。但是到了某一家公司里就有些水土不服。比如分享了如何 在你家公司使用 Django Migrate。这次我们来说说外键。

什么是外键

关系型数据库之所以叫「关系型」,因为维护数据之间的「关系」是它们的一大 Feature。

外键就是维护关系的基石。

比如我们创建两个表,一个是 students 学生表,一个是 enrollments 选课表。

选课表的 student_idstudent.id 关联。那么外键在这里为我们做了什么呢?

enrollments 创建的 SQL 如下:

其中 CONSTRAINT enrollments_ibfk_1 FOREIGN KEY (student_id) REFERENCES 就是外键的意思。这样确保 enrollments 表中的 student_id 必须来自 students 表中的 idenrollments.student_id 里的值,必须是 students.id 表中已经存在的值。
否则数据库会报错,防止插入无效的数据。

如果我们试图插入一条不存在的 student_id,数据库会拒绝插入:

使用外键的好处有:

  • 数据库帮我们维护数据的完整性,不会存在孤儿数据,不会因为编程错误插入错误数据;
  • 可以实现级联删除,比如 ON DELETE CASCADE,上面的例子中当我们从 students 表删除 id=2 的学生,在 enrollments 表相关的数据也同事会被删除;
  • 清晰的业务逻辑表达,在数据库表定义就有二者的关联关系,在语义上就比较好维护。还有一些数据库工具可以直接根据我们表定义中的 FOREIGN KEY 关系来画出来表之间的关系,在入手一个新的项目的时候,非常有用。
使用 ChartDB 可视化例子中的表关系1

为什么 DBA 不喜欢外键?

很多大公司的数据库都是禁用外键的,FOREIGN KEY (student_id) REFERENCES 这种 DDL 语句执行会直接失败。这样,数据库的表从结构上看不再有关系,每一个表都是独立的表而已,enrollments 表的 student_id Column 只是一个 INT 值,不再和其他的表关联。

为什么要把这个好东西禁用呢?

主要原因是不好维护。修改表结构和运维的时候,因为外键的存在,都会有很多限制。分库分表也不好实现。如果每一个表都是一个单独的表,没有关系,那 DBA 运维起来就方便很多了。

外键也会稍微降低性能。因为每次更新数据的时候,数据库都要去检查外键约束。

退一步讲,其实数据的完整性可以通过业务来保证,级联删除这些东西也做到业务的逻辑代码中。这样看来,使用外键就像是把一部分业务逻辑交给数据库去做了,本质上和存储过程差不多。

所以,互联网公司的数据库一般都是没有 REFERENCES 权限的。

Revoke REFERENCE 权限如下这样操作:

这样之后,如果在执行 Django migration 的时候,会遇到权限错误:

Django migration 如何不使用外键

在声明 Model 的时候,使用 ForeignKey 要设置 db_constraint=False2。这样在生成的 migration 就不会带外键约束了。

Django migration 如何全局禁用外键

每一个 ForeignKey 都要写这个参数,太繁琐了。况且,Django 会内置一些 table 存储用户和 migration 等信息,对这些内置 table 修改 DDL 比较困难。

Django 的内置 tables:

在 Github 看到一个项目3,发现 Django 的 ORM 里面是有 feature set 声明的,其实,我们只要修改 ORM 的 MySQL 引擎,声明数据库不支持外键,ORM 在生成 DDL 的时候,就不会带有 FOREIGN KEY REFERENCE 了。

核心的原理是继承 Django 的 MySQL 引擎,写自己的引擎,改动内容其实就是一行 supports_foreign_keys = False

具体的方法如下。

新建一个 mysql_engine,位置在 Django 项目的目录下,和其他的 app 平级。这样 mysql_engine 就可以在 Django 项目中 import 了。

我们要写自己的 mysql engine。为什么不直接使用 django_psdb_engine 项目呢?因为 django_psdb_engine 是继承自 Django 原生的 engine,就无法使用 django_prometheus4 的功能了。ORM 扩展的方式是继承,这就导致如果两个功能都是继承自同一个基类,那么只能在两个功能之间二选一了,或者自己直接基于其中一个功能去实现另一个功能。所以不如链式调用好,如 CoreDNS5 的 plugin,可以包装无限层,接口统一,任意插件可以在之间插拔。Django 自己的 middleware 机制也是这样。

engine 里面主要写两个文件。

base.py

features.py

最后,在 settings.py 中,直接把 ENGINE 改成自己的这个包 "ENGINE": "mysql_engine"

这样之后就完成了。

python manage.py makemgirations 命令不受影响。

python manage.py migrate 命令现在不会对 ForeignKey 生成 REFERENCE 了。

Django 的 migrate 可以正常执行,即使 Django 内置的 table 也不会带有 REFERENCE。

查看一个 table 的创建命令:

可以确认是没有 REFERENCE 的。

  1. chartdb 工具:https://app.chartdb.io/,其他类似的工具还有很多,比如 https://dbdiagram.io/ ↩︎
  2. db_constraint=False 文档:https://docs.djangoproject.com/en/5.2/ref/models/fields/#django.db.models.ForeignKey.db_constraint ↩︎
  3. https://github.com/planetscale/django_psdb_engine ↩︎
  4. https://github.com/korfuri/django-prometheus ↩︎
  5. https://www.kawabangga.com/posts/4728 ↩︎

 

请求为什么超时了?答案和解析

这篇博客是请求为什么超时了?这篇的答案和解析。

首先,通过这个抓包文件前几个包可以发现,服务器的网络是没有问题的,因为访问 DNS 或者 ubuntu apt 源都是通的。

这个问题有两个要点。第一点是,抓包文件并不是仅仅包含出问题的请求本身,还包含很多与问题无关的流量。不过现实的情况也往往如此,我们要在很多抓包和分析的时候过滤掉和问题无关的流量。

有一个非常好用的过滤方法,就是直接用 TCP 的 payload 内容进行过滤。比如,我们已经知道请求的目标是 example.com 了,那么 Host: example.com 必然会存在于 TCP 的 body 中,所以可以用以下的过滤条件:

tcp.payload contains "example.com"

(在之前的写的 网络抓包的技巧 中也介绍过,我们可以发送带有标记的请求,tcp.payload contains "xxx" 也是过滤出来这种标记请求的好方法)

用这个过滤条件可以得到以下的几个包,这就是我们要分析的请求了。

发给 example.com 的包

可以看到,我们发送给 exmaple.com 80 端口的包从来就没有得到过确认,于是一直在增大请求间隔并不断重试。

另一个奇怪的地方是,这个 TCP 请求没有 SYN 包被过滤出来,直接就开始发送 payload 了。这说明这个连接是在我们抓包之前就已经建立好的,所以我们没有看到连接建立的过程。

我们这个抓包文件的第一个包的时间是 37分52秒,而 HTTP 请求的第一个包时间是 39分28秒,间隔了 96 秒。这意味着这个 TCP 连接是至少在 96 秒之前建立的,并且在建立之后,至少在 96 秒 的时间内,没有发送过任何内容。(因为抓包文件没有抓到)

那么这个连接很可能因为 inactive 太久而被中间的网络设备丢弃了。如何定义 inactive?简单来说就是这个 TCP 连接上没有在一段时间内没有传输任何内容。

为什么网络设备会丢弃不活跃的 TCP 连接呢?因为机房的程序访问到公网要经过 NAT,防火墙等网络设备(其实和家用宽带是一样的,只不过家用路由器本质上是一个路由器+NAT+防火墙),而防火墙或 NAT 设备的内存只能保存有限的连接数,因为连接的保持需要内存,内存是有限的。它们普遍采用的策略是保留最近用到的连接,丢弃最旧没有有消息的连接。即使内存没有用完,一般在配置上也会设置一个连接最长的 inactive 时间,尤其是防火墙设备。

那么如何解决这个问题呢?首先如果不用长连接肯定就没问题了,每次需要发送 HTTP 请求的时候,都重新建立 TCP 连接。但这样成本就高了,TCP 连接不复用会浪费硬件资源,延迟也会升高。所以更好的方法是使用 Keepalive,即还是复用长连接,但是需要把长连接保持住。Keepalive 的原理,其实就是定时在 TCP 连接上发送 len=0 的包,即不包含 payload,类似于 duplicate ACK。发送空包不会对对端造成任何干扰,但是这些数据包会刷新中间的网络设备,避免连接失效。退一步讲,即使连接失效了,也可以通过 Keepalive 包来提前发现,避免用到的时候才通过超时发现问题。

==计算机网络实用技术 目录==

这篇文章是计算机网络实用技术系列文章中的一篇,这个系列正在连载中,我计划用这个系列的文章来分享一些网络抓包分析的实用技术。这些文章都是总结了我的工作经历中遇到的问题,经过精心构造和编写,每个文件附带抓包文件,通过实战来学习网路分析。

如果本文对您有帮助,欢迎扫博客右侧二维码打赏支持,正是订阅者的支持,让我公开写这个系列成为可能,感谢!

没有链接的目录还没有写完,敬请期待……

  1. 序章
  2. 抓包技术以及技巧
  3. 理解网络的分层模型
  4. 数据是如何路由的
  5. 网络问题排查的思路和技巧
  6. 不可以用路由器?答案和解析
  7. 网工闯了什么祸?答案和解析阅读加餐!
  8. 延迟增加了多少?答案和解析
  9. 压测的时候 QPS 为什么上不去?答案和解析
  10. 重新认识 TCP 的握手和挥手答案和解析
  11. TCP 下载速度为什么这么慢?答案和解析
  12. 请求为什么超时了?答案和解析
  13. 0.01% 的概率超时问题答案和解析
  14. 后记:学习网络的一点经验分享
与本博客的其他页面不同,本页面使用 署名-非商业性使用-禁止演绎 4.0 国际 协议。
 

请求为什么超时了?

小明是一名网络工程师,有一天,同事报告问题说:自己的程序发送 HTTP 请求在测试环境好好的,但是在线上环境就总是超时,而且很容易复现,需要网络工程师的帮助。

这里的场景是,在线上运行环境,去用 HTTP 请求一个第三方(在这个例子中,是 example.com 提供的服务)。

首先,小明和同事一起复现了问题,确定超时确实存在,然后他们在请求发送方进行抓包,在抓包的同时又复现了一次超时的情况。拿到抓包文件,小明一看,立即就发现问题了所在了……

请下载这个文件并分析超时问题的根因。(如果没有头绪,可以打开这个提示

==计算机网络实用技术 目录==

这篇文章是计算机网络实用技术系列文章中的一篇,这个系列正在连载中,我计划用这个系列的文章来分享一些网络抓包分析的实用技术。这些文章都是总结了我的工作经历中遇到的问题,经过精心构造和编写,每个文件附带抓包文件,通过实战来学习网路分析。

如果本文对您有帮助,欢迎扫博客右侧二维码打赏支持,正是订阅者的支持,让我公开写这个系列成为可能,感谢!

没有链接的目录还没有写完,敬请期待……

  1. 序章
  2. 抓包技术以及技巧
  3. 理解网络的分层模型
  4. 数据是如何路由的
  5. 网络问题排查的思路和技巧
  6. 不可以用路由器?答案和解析
  7. 网工闯了什么祸?答案和解析阅读加餐!
  8. 延迟增加了多少?答案和解析
  9. 压测的时候 QPS 为什么上不去?答案和解析
  10. 重新认识 TCP 的握手和挥手答案和解析
  11. TCP 下载速度为什么这么慢?答案和解析
  12. 请求为什么超时了?答案和解析
  13. 0.01% 的概率超时问题答案和解析
  14. 后记:学习网络的一点经验分享
与本博客的其他页面不同,本页面使用 署名-非商业性使用-禁止演绎 4.0 国际 协议。