Burn out 逃生指南

在同一家公司工作两年以上,有很大概率会 burn out(意思就是精疲力尽,俺不中了)。如果岗位又是 SRE,那么 burn out 时刻几乎是必然。

为什么会这样?一个是因为工作的时间越长,做的东西就越多,维护的东西也越多,维护的工作就越多,然而新的项目还是要做,就会忙不过来了。加上在大公司分工明确,没有人关心甲方的死活,所以你依赖的库时常会有不兼容更新,依赖的组件经常因为组织结构调整而下线,依赖的 IDC 也会下线,安全团队时不时也会找过来让做一些安全方面的加固。总有一天,会发现自己的 todo list 里面放满了待迁移的事项,自己的用户天天来问一些相同的问题,老板有新的想法需要马上实现。每天下了班都在想着工作,每天都不想上班。这个时候,你就知道这是 burn out 了。

作为一个资深的 SRE,我这里有两条靠谱的路可以逃生。

第一条:每两年换一次工作。

很显然,这样的话,上面这些工作就不会积累下来压死骆驼了。但是如果不想工作三十年打 15 份工的话,就需要一些技巧了。

建议一:提高工作效率,而不是工作时间

之所以放在第一条,是因为这是最重要的一条建议,也是常常被我们忽略的一条。

工作总量 = 工作效率 x 时间

在工作量大的时候,自然而然想到的是延长工作时间,这是非常不可取的,工作时间应该固定在每天 8小时,一周5天,不能再增加了。尤其是 SRE,工作时间越长,出错的概率越大,出了事故就得去救火,review incidents,提出改进措施,实施改进措施,带来更多的工作。此外,如果延长工作时间,那后面要讨论的心理管理等话题就都没有意义了。

所以工作量增加的时候,重点要放在提高工作效率,而不是增加工作时间,end of story.

建议二:安排工作的优先级和时间

在焦头烂额的时候,如果有人天天来跟你说「这个需求很重要,什么时候能完成?」可能就会先做这个需求。有段时间每天至少 5 个人来问我 xx 什么时候可以做完,我的回答每天都一样,「和最初承诺的时间一样,如果最近有空了可以加快一些」。因为最初的时间就是按照优先级排列的,不会因为有人天天来问就变得快一些。

优先级如何排列,也不是只看需求方说的。如果对方提了一个不合理的时间,要了解下为什么这样着急。很多 deadline 都是随意拍脑袋定的,可能是为了某人在某个时间点可以向大老板汇报,可能是依赖你的工作的人先承诺了一个 deadline,也可能就是随意定的一个日期。在焦头烂额的一堆工作中,有几个有着让人焦虑的 deadline,让人很难忽略这些工作。但是优先级不应该按照 deadline 来排列,而应该按照真正的重要程度来排列。

  • 项目的发布日期已经对外宣布,用户期待在这一天使用新功能;
  • 线上的系统摇摇欲坠,必须更新一个 fix;
  • 新的集群需要部署,但是如果晚几天部署,也不会 block 任何人的工作;
  • ……

遇到不合理的预期的时候,可以问这几个问题:

  • 这个需求是服务谁的?为什么要做?如果不按照这个时间上线会有什么后果?
  • 其他人的工作是否可以并行做,如果我的这个工作不做,会 block 谁?

有时候把项目在 deadline 之前做完了,却发现后续的一段时间并没有用起来,或者项目继续被其他人 block 着,原来给出的时间线本身就是不切实际的。在最开始就讨论好项目整体的计划,了解真正的紧急程度,避免这些问题。

按照优先级给出需求方截止时间,然后按时间交付工作。但是这之间难免会遇到其他事情,比如临时插入了更紧急的需求,线上发生了事故需要立即处理等等。这一般也不是问题,在时间线有变化,无法按时交付的时候,应该立即通知需求方遇到的困难,新的预估时间。忌讳的是没有和需求方同步,直到交付日期的时候才说,因为某某原因项目无法按时交付了。

建议三:大项目如何推进?

对于大型的项目,尤其是需要多方参与的那种,如果你不幸当了项目的 owner,那么这个建议很实用:用笔记软件记录每天的进展,记录每天遇到的问题,以及这些问题的进展。

以前有一次我们要新建一个数据中心,infra 把机器准备好,然后中间件团队部署好各种服务,缓存,队列,网关等等,然后业务团队部署好业务程序,最后上线。但是我们已经好几年没有完整地上线过一个数据中心了,很多代码中都已经编码了 IDC 的名字,所以这项工作异常困难,要么这个组件启动不了,要么那个组件存在硬编码问题。

负责这个项目的同事是一个很靠谱的人,每遇到一个问题,他都在文档中记录下来。问题原因,负责人,解决方法,解决进度。项目结束之后,这个文档列出来长长的一串问题。看到这个文档我的感受有二:项目真难,这位负责的同事真靠谱。

同时我也学会了这项工作方法, 那时候起我就开始写工作笔记(用的软件是 Roam Research,笔记经过整理记录在公司的文档系统中),每一个项目都有详细的记录,记录的问题也成千上万了。

工作笔记的好处多多,显而易见的是,没有人能记住如此多的问题和细节,所以必须追踪记录。另外也让工作进度和内容透明,如果项目不能如期完成,也能知道问题在哪里。如果没有项目文档,无法解释项目进展和问题,就只能项目负责人的问题了。

经过实践我发现一个额外的好处是,可以带来工作心态的变化。

如果没有记录——想起来这个项目满是头疼,已经经历过 x 问题,y 问题,天知道还要经历什么问题,感觉每走一步都困难重重,想起来就头疼。

有了记录——我们已经解决了 x 问题和 y 问题,我倒要看看还可能出现什么问题!

建议四:使用异步的沟通方式

前面提到过我们要提高工作效率。一个重要的方法就是不要破坏自己的整块时间,不要让自己总是处于被打断的情况。如果养成了过几分钟就要切换到聊天软件查看消息的状态,那工作效率就完蛋了。

要像使用邮件一样使用消息软件,异步轮询沟通。(证明:基于忙轮询的 DPDK 比基于中断的 Linux 网络栈,性能就高多了)。

怎样做呢?前面我们已经学会写工作笔记了,在被 block 需要与人沟通的地方,就在这里记录下需要沟通和确认的地方。然后在每天定时(比如早上刚来和午饭之后)遍历所有在 block 的点,对每一个点都问一遍相关的同事需要确认的问题。但是一定要把所有的细节说全,比如咨询一个网络问题,要提供自己的 IP,对方的 IP 端口,现象是什么,预期结果是什么,traceroute 是什么。防止对方缺少信息需要跟你再次确认。这就回到问问题的艺术的话题1了。这样就不需要等待回复,所有相关的消息发出去之后就可以继续做没有被 block 的工作,然后等下下次轮询的时间查看消息。

对于收到的信息也一样,几乎所有的消息都不必立即回复。也可以用轮询的方式处理。很多人问问题的时候都不懂如何一次性把信息都提供出来,比如,报告网络问题,连从哪里到哪里有问题都说不明白。不必在等待回复上浪费时间。

建议五:安排工作计划

这条建议可以让你带着一个好心情上班:每周安排好这周要做的事情,每天安排好明天要做的事情,可以已经确定的优先级来安排。

如果没有工作计划,那每天上班看到的就是一个长长的 todo list,怎么能让人不焦虑。

如果有工作计划的话,至少确定今天只要完成这些工作就好了。心理上的负担也会轻松很多。明天的工作就让明天的自己去担心好了。

建议六:每天至少完成一件事情

这条建议可以让你带着一个好心情下班:每天至少完成一件事情,比如解决集群搭建中一个 block 的点,比如完整实现一个需求。

如果一天的时间都在开会,和不同的人讨论细节,到下班的时候一事无成,是很挫败的。每天至少动手完成了点什么,这点满足感会带来很大的不同。

建议七:不要完全放弃有长期收益的事情

不要花所有的时间去做紧急的事情,要花时间去做不紧急但是重要的事情。

比如:

  • 提高监控的覆盖度;
  • 自动化一个操作;
  • 从根本解决一个性能问题;

每天忙于救火,就永远无法从这种工作状态中脱身。去从根本解决问题,工作也会越来越少,形成良性循环。

举个例子:在给产品值班的时候,会有很多用户来问问题。我一般会提供用户文档链接,文档中有答案。如果对于一个问题没有现有的文档可以回答,要么是产品设计出了问题——为什么用户会有此疑问?要么是文档不够全面,我会去写一个关于这个问题的文档,然后再给用户文档链接。虽然表面上可以直接回答的问题花了更长的时间去解决,但是长远来看,将来的用户可能因为这个文档就不来问这个问题了,即使有人问相同的问题,我也可以给文档链接。

有关操作的自动化,也不是所有的操作都应该自动化,也要看投入产出比。如果一个操作一个月才有机会操作一次,那么用文档记录下来如何手工操作,也可以。相较之下,手工操作反而可能成本更低。此外,如果使用频率不高,那么下次用到的时候,自动化的流程很可能是坏掉的,需要临时去 debug 哪里出了问题。

  1. 程序员如何高效和同行交流 ↩︎
 

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,稍微做了修改,支持了并发,这样建立连接速度快一些。

/*****************************************************
 * The TCP socket client to help you to test if your
 * OS supports c1000k(1 million connections).
 * @author: ideawu
 * @link: http://www.ideawu.com/
 *****************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <arpa/inet.h>
#include <netinet/tcp.h>
#include <sys/wait.h>

#define LOCAL_PORTS 10000
#define DST_PORTS 1000

int child_process(const char *ip, int dest_port){
    printf("create %d connections to port=%d\n", LOCAL_PORTS, dest_port);
	struct sockaddr_in addr;
	int opt = 1;
	int bufsize;
	socklen_t optlen;
	int connections = 0;

	memset(&addr, sizeof(addr), 0);
	addr.sin_family = AF_INET;
	inet_pton(AF_INET, ip, &addr.sin_addr);

	char tmp_data[10];
    for (int conns=0; conns<LOCAL_PORTS; conns++){
        int port = dest_port;
        //printf("connect to %s:%d\n", ip, port);

        addr.sin_port = htons((short)port);

        int sock;
        if((sock = socket(AF_INET, SOCK_STREAM, 0)) == -1){
            goto sock_err;
        }
        if(connect(sock, (struct sockaddr *)&addr, sizeof(addr)) == -1){
            goto sock_err;
        }

        connections ++;

        if(connections % 1000 == 999){
            //printf("press Enter to continue: ");
            //getchar();
            printf("connections to dest_port %d: %d, fd: %d\n", dest_port, connections, sock);
        }
        usleep(1 * 1000);

        bufsize = 0;
        setsockopt(sock, SOL_SOCKET, SO_SNDBUF, &bufsize, sizeof(bufsize));
        setsockopt(sock, SOL_SOCKET, SO_RCVBUF, &bufsize, sizeof(bufsize));
    }
    
    sleep(3 * 60 * 60);

	return 0;
sock_err:
	printf("connections: %d\n", connections);
	printf("error: %s\n", strerror(errno));
	return 0;
}

int main(int argc, char **argv){
    if(argc <= 2){
        printf("Usage: %s ip port\n", argv[0]);
        exit(0);
    }

    const char *ip = argv[1];
    int base_port = atoi(argv[2]);

    for (int i = 0; i < DST_PORTS; i++) {
        pid_t pid = fork();
        if (pid == 0) {
            child_process(ip, base_port+i);
            exit(0); 
        } else if (pid < 0) {
            perror("fork failed");
            exit(1);
        }
    }

    for (int i = 0; i < DST_PORTS; i++) {
        wait(NULL);
    }

    return 0;
}

代码的逻辑是: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 都连不上:
mkdir /sys/fs/cgroup/testlimit
# 8 cores max
echo "2000000 100000" > /sys/fs/cgroup/testlimit/cpu.max
echo $$ > /sys/fs/cgroup/testlimit/cgroup.procs

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

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

#define LOCAL_PORTS 50000
#define DST_PORTS 2000

还是用上面的环境,跑到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 网关发来的包也会发回网关;
  • IP 层:交换 Src IP 和 Dst IP 即可。
  • TCP 层:

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

XDP 程序的源代码如下:

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

apt-get install -y clang llvm libelf-dev libpcap-dev build-essential  m4 pkg-config \
  linux-headers-$(uname -r) \
  linux-tools-generic tcpdump linux-tools-common \
  xdp-tools

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

编译程序:

clang -O2 -target bpf -g -c tcp_bounce.c -o tcp_bounce.o  -I /usr/include/aarch64-linux-gnu/

把 xdp 程序加载到网卡上:

xdp-loader load eth1 tcp_bounce.o --mode skb

然后从另一台机器对这个加载了 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 选课表。

MySQL root@(none):foreignkey_example1> select * from students;
+----+------+
| id | name |
+----+------+
| 1  | 张三 |
| 2  | 李四 |
+----+------+
2 rows in set
Time: 0.002s

MySQL root@(none):foreignkey_example1> select * from enrollments;
+----+------------+--------+
| id | student_id | course |
+----+------------+--------+
| 1  | 1          | 数学   |
| 2  | 2          | 语文   |
| 4  | 1          | 英语   |
+----+------------+--------+
3 rows in set
Time: 0.002s

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

enrollments 创建的 SQL 如下:

CREATE TABLE `enrollments` (
  `id` int NOT NULL AUTO_INCREMENT,
  `student_id` int NOT NULL,
  `course` varchar(50) NOT NULL,
  PRIMARY KEY (`id`),
  KEY `student_id` (`student_id`),
  CONSTRAINT `enrollments_ibfk_1` FOREIGN KEY (`student_id`) REFERENCES `students` (`id`)
) 

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

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

MySQL root@(none):foreignkey_example1> INSERT INTO enrollments (student_id, course) VALUES (3, '英语');
                                    ->
(1452, 'Cannot add or update a child row: a foreign key constraint fails (`foreignkey_example1`.`enrollments`, CONSTRAINT `enrollments_ibfk_1` FOREIGN KEY (`student_id`) REFERENCES `students` (`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 权限如下这样操作:

REVOKE REFERENCES ON testdb1_nofk.* FROM 'testuser1'@'localhost';

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

django.db.utils.OperationalError: (1142, "REFERENCES command denied to user 'testuser1'@'localhost' for table 'testdb1_nofk.django_content_type'")

Django migration 如何不使用外键

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

Django migration 如何全局禁用外键

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

Django 的内置 tables:

+-------------------------------+
| Tables_in_test_nofk           |
+-------------------------------+
| auth_group                    |
| auth_group_permissions        |
| auth_permission               |
| auth_user                     |
| auth_user_groups              |
| auth_user_user_permissions    |
| django_admin_log              |
| django_content_type           |
| django_migrations             |
| django_session                |
+-------------------------------+

在 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 了。

├── project_dir
│   ├── __init__.py
│   ├── __pycache__
│   ├── asgi.py
│   ├── settings
│   ├── urls.py
│   └── wsgi.py
├── manage.py
├── app_dir
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── migrations
│   ├── models.py
│   ├── serializers.py
│   ├── tests.py
│   └── views.py
└── mysql_engine
    ├── base.py
    └── features.py

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

engine 里面主要写两个文件。

base.py

from django_prometheus.db.backends.mysql.base import DatabaseWrapper as MysqlDatabaseWrapper
from .features import DatabaseFeatures


class DatabaseWrapper(MysqlDatabaseWrapper):
    vendor = 'laixintao'
    features_class = DatabaseFeatures

features.py

from django_prometheus.db.backends.mysql.base import (
    DatabaseFeatures as MysqlBaseDatabaseFeatures,
)


class DatabaseFeatures(MysqlBaseDatabaseFeatures):
    supports_foreign_keys = False

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

DATABASES = {
    "default": {
        "ENGINE": "mysql_engine",
        "NAME": "testdb1_nofk",
        "USER": "testuser1",
        'HOST': 'localhost',
        'PORT': '',
        'OPTIONS': {
            'unix_socket': '/tmp/mysql.sock',
        },
    }
}

这样之后就完成了。

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

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

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

python3 corelink/manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, meta, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying admin.0003_logentry_add_action_flag_choices... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying auth.0010_alter_group_name_max_length... OK
  Applying auth.0011_update_proxy_permissions... OK
  Applying auth.0012_alter_user_first_name_max_length... OK

查看一个 table 的创建命令:

MySQL root@(none):testdb1_nofk> show create table auth_user_groups \G
***************************[ 1. row ]***************************
Table        | auth_user_groups
Create Table | CREATE TABLE `auth_user_groups` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `user_id` int NOT NULL,
  `group_id` int NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `auth_user_groups_user_id_group_id_94350c0c_uniq` (`user_id`,`group_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

可以确认是没有 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. 重新认识 TCP 的握手和挥手答案和解析
  9. 3.5 秒初始延迟问题答案和解析
  10. 网络断断续续……答案和解析
  11. 延迟增加了多少?答案和解析
  12. 压测的时候 QPS 为什么上不去?答案和解析
  13. TCP 下载速度为什么这么慢?答案和解析
  14. 请求为什么超时了?答案和解析
  15. 0.01% 的概率超时问题答案和解析
  16. 后记:学习网络的一点经验分享
与本博客的其他页面不同,本页面使用 署名-非商业性使用-禁止演绎 4.0 国际 协议。