2025-08-02 20:20:45
很久之前画重金买的阅读器1是 A4 纸大小,无论是阅读电子书还是 paper 都很好。但是后来莫名其妙地屏幕部分区域失灵了(情况和这里2介绍的差不多),维修找不到售后,京东推给 SONY,SONY 客服根本不知道有这么个产品,所以索性换了另一款阅读器:remarkable2.
这件事也加深了我 SNOY 品控差的印象,之前的买过的 SNOY 产品还包括 PSV,遥感漂移了(好像 switch 也有这类问题,所以可以饶恕吧);PS4 手柄莫名其妙也坏了,PS4 主机后来也坏了。以后不想再买 SNOY 的产品了……
回到 Remarkable2,这款屏幕是 10.3 英寸,没有比之前的尺寸小很多,有一些 PDF 阅读起来就不太方便。有一些阅读器支持重排版和裁剪,有这个功能就解决问题了。但是仔细一想——重排版和裁剪不应该是一个软件功能吗?那么直接使用软件对 PDF 进行处理,然后阅读处理之后的文档不是也可以解决问题吗?
然后就发现了这个软件 K2pdfopt3,可以重新版本 PDF 为阅读器的尺寸。并且可以自动删除 PDF 的白边。
比如下面这个文档,对于印刷比较友好,左侧页面有右侧的留白,右侧页面有左侧的留白,但是使用阅读器,就浪费空间了。
K2pdfopt 可以自动裁切这种空白,命令是:
k2pdfopt input.pdf -h 1872 -w 1404 -dpi 226 -p 1-50 -wrap+ -o output.pdf -ui-
效果如下。
双栏的论文 PDF 页可以改成单栏的:
k2pdfopt paper1.pdf -mode 2col -col 2 -n -fc- -x -y -t -ds 2 -h 1872 -w 1404 -dpi 226 -o output_paper.pdf
最后在阅读器上的效果如下:
2025-07-19 14:06:47
也叫做 CIDR (Classless Inter-Domain Routing),表示一个网络段,比如 192.168.0.0/24。
路由设备通过网络掩码去匹配地址,所以子网的划分一般用这种形式。/24
有的地方也用掩码 255.255.255.0
,表示的内容是一样的。
ipcalc
1 这个工具可以帮助计算 IP 网络。
表示一个 IP 范围,从起始 IP 到结束 IP。比如 192.168.1.1 到 192.168.1.100,一共 100 个 IP。
它可以表示如:192.168.1.100 – 192.168.2.10 这种连续的段,但是 Network 是无法表示出来的。
使用 *
通配符来匹配 IP 的某部分,语法类似于 shell 中对文件名的 glob 匹配。
比如 192.168.1.*
就等同与 192.168.1.0/24
。但是 192.168.1.2*
就没有与之等同的 Network 表示。
反过来,192.168.1.0/26
的范围是 192.168.1.0 – 192.168.1.63, 也不能用 IP Glob 表示。
SSH 的 ~/.ssh/config
就是用 IP Glob 来定义不同的 IP (Host)登陆的配置的。
IPSet 是一个 Set,一般来说是 IP 地址和 CIDR 的集合,所以可以表示任意 IP 的集合。ACL 一般用 IPSet 的方式来配置。
2025-07-19 14:05:55
在同一家公司工作两年以上,有很大概率会 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 来排列,而应该按照真正的重要程度来排列。
遇到不合理的预期的时候,可以问这几个问题:
有时候把项目在 deadline 之前做完了,却发现后续的一段时间并没有用起来,或者项目继续被其他人 block 着,原来给出的时间线本身就是不切实际的。在最开始就讨论好项目整体的计划,了解真正的紧急程度,避免这些问题。
按照优先级给出需求方截止时间,然后按时间交付工作。但是这之间难免会遇到其他事情,比如临时插入了更紧急的需求,线上发生了事故需要立即处理等等。这一般也不是问题,在时间线有变化,无法按时交付的时候,应该立即通知需求方遇到的困难,新的预估时间。忌讳的是没有和需求方同步,直到交付日期的时候才说,因为某某原因项目无法按时交付了。
对于大型的项目,尤其是需要多方参与的那种,如果你不幸当了项目的 owner,那么这个建议很实用:用笔记软件记录每天的进展,记录每天遇到的问题,以及这些问题的进展。
以前有一次我们要新建一个数据中心,infra 把机器准备好,然后中间件团队部署好各种服务,缓存,队列,网关等等,然后业务团队部署好业务程序,最后上线。但是我们已经好几年没有完整地上线过一个数据中心了,很多代码中都已经编码了 IDC 的名字,所以这项工作异常困难,要么这个组件启动不了,要么那个组件存在硬编码问题。
负责这个项目的同事是一个很靠谱的人,每遇到一个问题,他都在文档中记录下来。问题原因,负责人,解决方法,解决进度。项目结束之后,这个文档列出来长长的一串问题。看到这个文档我的感受有二:项目真难,这位负责的同事真靠谱。
同时我也学会了这项工作方法, 那时候起我就开始写工作笔记(用的软件是 Roam Research,笔记经过整理记录在公司的文档系统中),每一个项目都有详细的记录,记录的问题也成千上万了。
工作笔记的好处多多,显而易见的是,没有人能记住如此多的问题和细节,所以必须追踪记录。另外也让工作进度和内容透明,如果项目不能如期完成,也能知道问题在哪里。如果没有项目文档,无法解释项目进展和问题,就只能项目负责人的问题了。
经过实践我发现一个额外的好处是,可以带来工作心态的变化。
如果没有记录——想起来这个项目满是头疼,已经经历过 x 问题,y 问题,天知道还要经历什么问题,感觉每走一步都困难重重,想起来就头疼。
有了记录——我们已经解决了 x 问题和 y 问题,我倒要看看还可能出现什么问题!
前面提到过我们要提高工作效率。一个重要的方法就是不要破坏自己的整块时间,不要让自己总是处于被打断的情况。如果养成了过几分钟就要切换到聊天软件查看消息的状态,那工作效率就完蛋了。
要像使用邮件一样使用消息软件,异步轮询沟通。(证明:基于忙轮询的 DPDK 比基于中断的 Linux 网络栈,性能就高多了)。
怎样做呢?前面我们已经学会写工作笔记了,在被 block 需要与人沟通的地方,就在这里记录下需要沟通和确认的地方。然后在每天定时(比如早上刚来和午饭之后)遍历所有在 block 的点,对每一个点都问一遍相关的同事需要确认的问题。但是一定要把所有的细节说全,比如咨询一个网络问题,要提供自己的 IP,对方的 IP 端口,现象是什么,预期结果是什么,traceroute 是什么。防止对方缺少信息需要跟你再次确认。这就回到问问题的艺术的话题1了。这样就不需要等待回复,所有相关的消息发出去之后就可以继续做没有被 block 的工作,然后等下下次轮询的时间查看消息。
对于收到的信息也一样,几乎所有的消息都不必立即回复。也可以用轮询的方式处理。很多人问问题的时候都不懂如何一次性把信息都提供出来,比如,报告网络问题,连从哪里到哪里有问题都说不明白。不必在等待回复上浪费时间。
这条建议可以让你带着一个好心情上班:每周安排好这周要做的事情,每天安排好明天要做的事情,可以已经确定的优先级来安排。
如果没有工作计划,那每天上班看到的就是一个长长的 todo list,怎么能让人不焦虑。
如果有工作计划的话,至少确定今天只要完成这些工作就好了。心理上的负担也会轻松很多。明天的工作就让明天的自己去担心好了。
这条建议可以让你带着一个好心情下班:每天至少完成一件事情,比如解决集群搭建中一个 block 的点,比如完整实现一个需求。
如果一天的时间都在开会,和不同的人讨论细节,到下班的时候一事无成,是很挫败的。每天至少动手完成了点什么,这点满足感会带来很大的不同。
不要花所有的时间去做紧急的事情,要花时间去做不紧急但是重要的事情。
比如:
每天忙于救火,就永远无法从这种工作状态中脱身。去从根本解决问题,工作也会越来越少,形成良性循环。
举个例子:在给产品值班的时候,会有很多用户来问问题。我一般会提供用户文档链接,文档中有答案。如果对于一个问题没有现有的文档可以回答,要么是产品设计出了问题——为什么用户会有此疑问?要么是文档不够全面,我会去写一个关于这个问题的文档,然后再给用户文档链接。虽然表面上可以直接回答的问题花了更长的时间去解决,但是长远来看,将来的用户可能因为这个文档就不来问这个问题了,即使有人问相同的问题,我也可以给文档链接。
有关操作的自动化,也不是所有的操作都应该自动化,也要看投入产出比。如果一个操作一个月才有机会操作一次,那么用文档记录下来如何手工操作,也可以。相较之下,手工操作反而可能成本更低。此外,如果使用频率不高,那么下次用到的时候,自动化的流程很可能是坏掉的,需要临时去 debug 哪里出了问题。
2025-07-19 14:02:44
互联网诞生以来,如何让一台服务器服务更多的用户就成为了软件工程师一直试图解决的难题。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 核心的机器。
需要进行配置的内容:
ulimit -n 1048576
;sudo sysctl -w net.ipv4.tcp_keepalive_time=720000
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千万连接机器又崩溃了。
到之类就没办法继续研究是什么原因了,可能是虚拟化的问题,如果折腾一下物理机环境,说不定可以跑到1亿。
另外我发现一个有趣的地方,如果 abort client 程序,大量连接的 fd 需要 kernel 去回收,会造成所有的 CPU 100% kernel state,机器几乎卡死了,直到连接回收完毕。这部分好像是用 cgroups 限制不住的。所以,如果在不可信的共享的执行环境,通过建立大量的连接再退出进程,说不定有可能恶意挤兑其他租户的资源?
2025-06-08 23:57:48
一个 XDP 练习程序:作为 TCP 的 server 端,用 XDP 实现所有的 TCP 端口都接受 TCP 建立连接。(只是能够建立连接而已,无法支持后续的 TCP 数据传输,所以不具有实际意义,纯粹好玩。)
建立 TCP 连接需要实现 TCP 的三次握手,对于 server 端来说,要实现:
回复 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 网络栈根本不知道这个包的存在。
现在的重点在于如何修改这个 TCP SYN 包,并将其回送,使对方认为它是一个合法的 SYN-ACK 包。
我们可以从下往上一层一层看:
因为这个程序直接把收到的 TCP SYN 包远路反弹,就叫它 tcp_bounce.c
吧。(这周末刚去了一个叫 Bounce 的地方团建……)
XDP 程序的源代码如下:
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/icmp.h>
#include <linux/tcp.h>
#include <linux/in.h>
#define MAX_CHECKING 4
#define MAX_CSUM_WORDS 750
static __always_inline __u32 sum16(const void* data, __u32 size, const void* data_end) {
__u32 sum = 0;
const __u16 *ptr = (const __u16 *)data;
#pragma unroll
for (int i = 0; i < MAX_CSUM_WORDS; ++i) {
if ((const void *)(ptr + 1) > (data + size)) {
break;
}
if ((const void *)(ptr + 1) > data_end) {
return sum;
}
sum += *ptr;
ptr++;
}
// Handle the potential odd byte at the end if size is odd
if (size & 1) {
const __u8 *byte_ptr = (const __u8 *)ptr; // ptr is now after the last full word
// BPF Verifier check: Ensure the single byte read is within packet bounds
if ((const void *)(byte_ptr + 1) <= data_end && (const void *)byte_ptr < data_end) {
// In checksum calculation, the last odd byte is treated as the
// high byte of a 16-bit word, padded with a zero low byte.
// E.g., if the byte is 0xAB, it's treated as 0xAB00.
sum += (__u16)(*byte_ptr) << 8;
}
// If the bounds check fails, we just return the sum calculated so far.
}
return sum;
}
SEC("xdp")
int tcp_bounce(struct xdp_md *ctx) {
void *data = (void *)(long)ctx->data;
void *data_end = (void *)(long)ctx->data_end;
struct ethhdr *eth = data;
if ((void *)eth + sizeof(*eth) > data_end)
return XDP_PASS; // not enough data
if (eth->h_proto != bpf_htons(ETH_P_IP))
return XDP_PASS;
struct iphdr *iph = data + sizeof(*eth);
if ((void *)iph + sizeof(*iph) > data_end)
return XDP_PASS;
if (iph->protocol != IPPROTO_TCP)
return XDP_PASS;
//check ip len
int ip_hdr_len = iph->ihl*4;
if((void *)iph + ip_hdr_len > data_end)
return XDP_PASS;
// convert to TCP
struct tcphdr *tcph = (void *)iph + ip_hdr_len;
if ((void *)tcph + sizeof(*tcph) > data_end)
return XDP_PASS;
if (!(tcph->syn) || tcph->ack)
return XDP_DROP;
// swap MAC addresses
__u8 tmp_mac[ETH_ALEN];
__builtin_memcpy(tmp_mac, eth->h_source, ETH_ALEN);
__builtin_memcpy(eth->h_source, eth->h_dest, ETH_ALEN);
__builtin_memcpy(eth->h_dest, tmp_mac, ETH_ALEN);
// swap IP addresses
__be32 tmp_ip = iph->saddr;
iph->saddr = iph->daddr;
iph->daddr = tmp_ip;
// TCP
// swap port
__be16 tmpsrcport = tcph->source;
tcph->source = tcph->dest;
tcph->dest = tmpsrcport;
// syn+ack
tcph->ack = 1;
__u32 ack_seq = bpf_ntohl(tcph->seq) + 1;
tcph->ack_seq = bpf_htonl(ack_seq);
// checksum pseudo header
__u32 csum = 0;
tcph->check = (__be16)csum;
if ((void *)&iph->saddr + 8 > data_end)
return XDP_PASS;
csum = bpf_csum_diff(0, 0, (__be32 *)&iph->saddr, 8, csum);
__u16 tcp_len = bpf_ntohs(iph->tot_len) - ip_hdr_len;
csum += (__u32)(bpf_htons(IPPROTO_TCP) << 16) | bpf_htons(tcp_len);
csum += sum16(tcph, tcp_len, data_end);
while (csum >> 16)
csum = (csum & 0xFFFF) + (csum >> 16);
tcph->check = (__be16)~csum;
return XDP_TX;
}
char _license[] SEC("license") = "GPL";
安装编译 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 连接,对于任意端口,可以观察到连接建立成功了:
也可以用 for 循环批量对端口建立连接,都可以连通。
for i in {5000..5010}; do nc -vz 172.16.199.22 ${i};done
XDP 的性能很高,客户端用 10000 个线程同时建立 TCP 连接,服务端的 XDP 程序使用了连 10% 都不到的 CPU。(Again,但是没有什么实际意义)
2025-06-01 21:53:26
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_id
和 student.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
表中的 id
。enrollments.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 (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'")
在声明 Model 的时候,使用 ForeignKey
要设置 db_constraint=False
2。这样在生成的 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 的。