I am LAZY bones?
AN ancient AND boring SITE

分类: '故障分析' 的归档

记一例 nginx 故障分析

早上起来习惯性瞄一眼 home lab 的日志,看到我那个 WebSocket 服务凌晨被重启过:

重启就重启吧,本来没当回事。结果顺手打开本博客——打不开。又试了下这台机器上跑的其他站,也全打不开。好家伙,整台机器的 nginx 全躺了,而且看时间已经躺了三个多小时。

先看现场

第一件事是确认机器有没有重启过。uptime 一看,没有,机器好好的。那就是 nginx 单独出事了。systemctl restart nginx 下去,唰一下全恢复了。

服务是回来了,但这更让人不安:能 restart 成功,说明磁盘上的配置本来就是好的,那它三个小时前到底为什么起不来?翻 unit 日志:

host not found in upstream。我博客有几个路径(比如这个)是反代到 GitHub Pages 的,nginx 起来的时候要解析 bones7456.github.io,那一刻没解析出来,直接 emerg 拒绝启动。

再一看,06:48 这个时间点太像 Debian 系的 cron.daily 窗口了(/etc/crontab 里是 6:25,加上 anacron 的随机延迟,另外 cron.weekly 是 6:47,更像),我当时第一反应是 certbot 续期把 nginx 重启崩了。结果把那五分钟的全量日志拉出来一看,跟 certbot 半毛钱关系没有。所以还是那句话:别猜,看日志。

1 秒的竞速

journalctl --since "06:45" --until "06:50" 全量捞出来,真凶一目了然:

看明白了:systemd 被 apt 自动升级,触发 needrestart 把机器上几乎所有服务全重启一遍。我这台机器上跑着 BIND 做内网 DNS,于是 named 和 nginx 一起被拖下水。

nginx 在 06:48:02 查 DNS,named 在 06:48:03 才恢复监听。就差 1 秒,nginx 输了这场竞速,代价是三个多小时的全站宕机。

最扎心的是同一批重启里的对比:php-fpm、redis、mariadb,还有我自己那几个 Flask 服务,全都 Started 成功了。唯独 nginx 死了。为什么?因为只有 nginx 在启动阶段需要解析外部域名,别的服务都不需要。

为什么 After=nss-lookup.target 没救下它

这才是这次最值得说的地方。翻开 Ubuntu 自带的 nginx unit:

nss-lookup.target 是 systemd 专门用来表示”域名解析已就绪”的标准锚点,DNS 服务一般会用 Before=nss-lookup.target 把自己挂上去。也就是说,nginx 早就声明过”我要等 DNS 就绪再启动”——然而它还是死在了 DNS 上。

原因在于After= 的语义被普遍误解了:

它只在同一个 systemd job transaction 内部编排启动先后,并不检查目标服务的实时健康状态。

needrestart 是逐个执行 systemctl restart <unit> 的,nginx 和 named 属于两个互相独立的 transaction,彼此之间没有任何排序约束。更要命的是,nss-lookup.target 是个 passive target,named 停止时它并不会跟着 deactivate,全程保持 active——于是 nginx 一查”依赖满足了吗”,满足,启动,然后一头撞死。

所以结论挺反直觉的:在 restart 场景下,After= 基本等于安慰剂。指望靠加一行 After=named.service 来防这类问题,是防不住的。

两道防线

既然顺序编排靠不住,那就换思路:一道事后自愈,一道事前免疫。

防线一:让它自己重试

Ubuntu 的 nginx.service 默认没有 Restart=,意味着启动失败一次就永久 failed,没有任何重试。而这次 named 只用了 1 秒就恢复——只要能重试一次,整件事根本不会发生。

StartLimit 那两行不是可选的:systemd 全局默认是 10 秒内最多 5 次,配上 RestartSec=10 会立刻撞上限流然后彻底放弃。改成 10 分钟内 20 次,才扛得住像样的 DNS 故障。

这里补一个 drop-in 的合并规则,很多人会搞混:

1. 标量指令(Restart=RestartSec=Type=PIDFile=…)是覆盖
2. 列表指令(After=Wants=Environment=ExecStartPre=…)是追加,不是覆盖。想清空得先写一行空赋值 After= 再写新值 Update:After=还不能被清空,详见下方评论。
3. ExecStart= 是重灾区:Type=forking 下只允许一条,直接在 drop-in 里写会报错,必须先 ExecStart= 清空再写。

想看合并后到底生效了什么,别看文件,看这个:

还有个细节值得确认:这次失败的其实是 ExecStartPre 里的 nginx -t -q(日志里那句 Control process exited, code=exited, status=1/FAILURE,result 是 exit-code)。Restart=on-failure 是覆盖这种情况的,不用担心它管不到。

防线二:别让 nginx 在启动期解析域名

自愈只是兜底,根上的毛病是:一个 location 的 upstream 解析不了,整个 nginx 拒绝启动,机器上所有站点陪葬。nginx 的配置校验是 all-or-nothing 的,一个小站点的临时故障被放大成了全局故障。

解法是给 proxy_pass 用变量——只要 proxy_pass 里含变量,nginx 就不再在启动期做一次性解析,改成运行时按需查 resolver。这样 DNS 挂了 nginx 照样能起来,最坏只是那一个 location 返回 502。

但这里有个大坑,下面单独说。

变量化 proxy_pass 的那个坑

我原来的配置是这样的,注意 proxy_pass 后面带了路径

如果你天真地把域名换成变量,写成 proxy_pass https://$gh_pages/china-dynasty-timeline/;,就掉坑里了。nginx 的规则是:

1. proxy_pass 不含变量且带 URI → nginx 做前缀替换,把 location 匹配掉的 /data/shi/ 换成 /china-dynasty-timeline/
2. proxy_pass 含变量 → 前缀替换机制彻底失效,URI 被固定成你写的那个。

后果是这样的:

首页看着还挺正常,所有子资源全部错位——CSS、JS、图片全挂。这种”打开一看好像没事”的故障最恶心。

正解是用 rewrite ... break 手动接管路径映射,配一个不带 URIproxy_pass。nginx 文档明确写了:proxy_pass 不带 URI 时,若 URI 已被 rewrite 改写,传递的就是改写后的 URI——正是我们要的。

先在 server 块里放解析器和变量:

然后 location 改成:

实际只动了三处:加 rewrite ... breakproxy_pass 去掉路径换成变量、显式加 proxy_ssl_name(它默认取 $proxy_host,变量化后写死更稳)。proxy_redirect 是字面量匹配,不受影响。查询串也会自动带过去,/data/shi/a?b=1 照常工作。

几个坑

1. resolver 里写多个地址是轮询,不是主备。我这里反代的是公网域名,两个 DNS 都能解析,正好互为冗余;但如果你要反代本机 named 里那些内网 zone,就绝不能这么写——轮询到 1.1.1.1 直接解析失败。那种 location 得单独配 resolver 127.0.0.1;
2. ipv6=off 是刻意加的。变量化之后每次请求都要重新解析,家宽 IPv6 到 GitHub 的连通性又不一定稳,少一个变数是一个。确认 IPv6 走得通再打开。
3. 别指望 After= 能防住 restart 场景,前面说透了。它在正常开机时有用,成本为零可以留着,但不能当防线。
4. nginx -t 通过不代表这次改对了。它只校验语法,rewrite 的路径映射对不对,它一个字都不会告诉你。

怎么验证真的修好了

改完一定要实测,光看配置”觉得对”没用。先验路径映射——必须测子路径,别只测首页

说点本质的

复盘下来,这次事故里有三个独立的问题,任意修掉一个都不会出事:nginx 没有真正可靠的 DNS 就绪依赖、配置在启动期硬依赖 DNS、失败之后没有任何重试。三个凑齐了,一次 1 秒的 DNS 抖动才放大成 3 小时的全站宕机。

而我最想留下的一条经验是:别把系统的健壮性寄托在”顺序”上After=Before=network-online.target 这些东西给人一种”我已经处理好依赖了”的错觉,但它们描述的是编排意图,不是运行时的真实状态。真正靠得住的只有两种东西——失败了能自己重试(自愈),和压根不依赖那个东西(免疫)。分布式系统里讲了很多年的道理,放在单机 systemd 上一样成立。

最后再补一句题外话:这次是我早上”顺手看了眼日志”才发现的,不然还得挂更久。所以监控该上还是得上,healthchecks.io、UptimeRobot 之类的免费额度足够个人站用了。再优雅的配置优化,也不如有人(或者有个机器人)在你睡觉的时候盯着。

全文完。

用ktransformers跑671b的DeepSeek R1

最近这两天,在折腾拿一台4块V100的GPU卡,来跑个671b准满血的DeepSeek R1,遇到了很多问题,也解决了很多问题,当然估计还留下很多问题。这期间遇到一个个的挑战,搞定一个后又遇到下一个,突然有点十多年前折腾linux的时候的意思了,觉得挺好玩的,就也像以前一样,稍微记录一下吧。

先说下背景,我这台机器的配置是比较古老的数据中心版的GPU机器,显卡上面说了是4块V100,每块16G显存,cpu只有20核、内存256GB、数据盘1.5T,系统是ubuntu 20.04。这硬件拿来跑671b确实是有点捉襟见肘,最终的结果也只是1 token/s 左右,并不具备生产价值,只是研究而已。

我最先遇到的问题,就是由于这个机器很多人有权限,而是最恐怖的公用一个linux账号,环境被搞得想当乱,python都是各种版很多套。所以我最先做的就是梳理环境,用了Miniconda搞了一套干净的python-3.12的运行时,后续的各种包都独立维护。
另外,原先的Nvidia驱动也有两个版本,我也顺手给升级并统一了一下,查了各种资料后,发现这个架构比较旧的显卡最好还是不要安装最新的(12.8)驱动,所以我安装了12.6的版本。
然后,我的思路是,在本机直接编译一个ktransformers,这样才有最大的自由度,我感觉这是最终能跑起来的关键,因为到最后还需要现场修改一些代码才能跑起来。
从源码编译安装的过程主要参照官网文档,但在执行 bash install.sh 之前,还需要干两件事:

  • 一个是export TORCH_CUDA_ARCH_LIST="7.0",这个7.0的值,就是V100的硬件架构。
  • 另一个是 pip3 install flash-attn,这个模块虽然在V100下最终没有用到,但不装会报依赖错误

现在,按照官方文档的说法,应该是要可以跑起来了,然而你跑 python -m ktransformers.local_chat --model_path ./DeepSeek-R1 --gguf_path ./DeepSeek-R1-Q4_K_M的时候,大概率会在load很久之后,报一个错:

RuntimeError: CUDA error: invalid device function
CUDA kernel errors might be asynchronously reported at some other API call, so the stacktrace below might be incorrect.
For debugging consider passing CUDA_LAUNCH_BLOCKING=1
Compile with TORCH_USE_CUDA_DSA to enable device-side assertions

这个问题,我折腾了很久,简单来说大概就是因为过程中会调用flash-attn,而flash-attn其实并不支持V100,在官方的issue列表里搜了一圈后我发现还真有人遇到类似的问题了,而且也贴了一些解决方案,评估了一下,我最终是按这个办法来做的,这里我感觉大概是刻意避开了对flash-attn的调用,可能会影响效率但不会报错了。其中第五步,对ktransformers/ktransformers/operators/attention.py的修改,是用了这个据说更快的方案。。。
改完再运行,还是会报内存不足的错误(可能是显存不足),因为默认只会用单块V100,16G的内存显然是不够的,这时候需要加 optimize_rule_path 参数,给一些优化规则,实测可以这样:python -m ktransformers.local_chat --model_path ./DeepSeek-R1 --gguf_path ./DeepSeek-R1-Q4_K_M --cpu_infer 20 --optimize_rule_path ktransformers/ktransformers/optimize/optimize_rules/DeepSeek-V3-Chat-multi-gpu-4.yaml这个DeepSeek-V3的4卡规则,对于R1也同样有效。
加了这个规则以后,加载过程应该就可以看到会加载到不同的GPU去了,nvidia-smi也能看到4块卡的显存都用起来了。
最终应该就可以响应你的问题了!虽然大概只能到1 token/s 左右。。。

至此大致结束,后面又折腾了一下web的配置,大概也是根据官方文档来的,这个没啥特殊就不展开了。
最后,感谢朋友发的这篇文章,就是这篇公众号文章让我开始有动力折腾这个事情的,哈哈!

IPv6路由错误引起的怪异问题

我那Ubuntu源服务器(u.srt.cn),最近出现了一些很诡异的错误。
比如:之前设置的crontab同步linux.deepin.org的iso镜像,已经有一段时间没有成功过了,手工执行rsync,却发现连的似乎是自己,因为banner都出来“Thanks for using SRT ubuntu mirror.”了,但是ping linux.deepin.org 却又能得到正确的结果。
再比如之前我设置了用公钥可以ssh登录另外一台机器,但是现在却提示我输入密码。
排查了许久之后,发现了一个问题,很多(但不是全部)公网的域名虽然ping的时候对应了正确的ip,但是正在使用(比如上面的rsync或ssh)的时候,好像都指到本地了。
再后来,无意之中,发现用ping6 去ping那些有问题的域名,返回的都是 localhost(::1),于是终于知道怎么回事了:系统用IPv6去访问那些域名了,而那些域名的IPv6解析不正确。
为了验证这点,只需要把系统的IPv6彻底禁用再试试就成了,但是服务器也是ubuntu,而ubuntu最新的版本都已经把IPv6编译进内核了,不能通过rmmod来禁用IPv6了,要完全禁用需要修改grub的配置,给内核传参数才行(方法见这里)。
这显然太麻烦了,其实暂时禁用一下还是有方便的办法的,就是这样:

执行完以后,可以执行

来确认已经禁用成功了,如果这命令没有输出就OK了。
然后现在再用ping6的话,会提示connect: Network is unreachable
再去试试之前的rsync和ssh,果然都正常了。

现在我担心的是:IPv4地址不都已经枯竭了吗?接下来该怎么办呢?

gnome-panel 消失解决办法

2010年的最后一天,打开自己的blog看了一眼,最后一个月居然什么都没留下了,觉得这样实在不太好,于是趁最后时刻,写点什么。
想了一下有什么值得一写的,发现还真不多,因为近来实在是少有时间去折腾,就拿这个来充数吧,如果能帮到有同样现象的朋友,也算不错。
我的gentoo在某次升级以后,gnome-panel就突然消失不见了,我的环境是蛮正常蛮标准的gnome+compiz,我的compiz开了窗口阴影,在屏幕最上方,本来是panel的地方,阴影还是有的。ps看了一下进程,gnome-panel也在。杀掉重启,或者执行 gnome-panel –replace 都无效。看起来就是面板的高度变成了0像素。
查了任何可查的日志,也没有发现什么异常。于是google了一把,发现有人说把 .gconf 删掉就可以恢复了,于是先把整个xdm停掉,把 .gconf 改名,再启动gnome,发现面板果然正常了。当然副作用就是我的大部分设置都丢了,这是我不能接受的。
当然到了这一步,就比较好办了,虽然我的办法很土,但是有效:继续用类似二分法的办法缩小.gconf里面的影响范围,当然,由于gconfd每次都会随gnome启动,所以每做一次范围确认都得停掉gnome,再打开。呵呵,虽然麻烦,但是还是很快早到了元凶,那就是: ~/.gconf/desktop/gnome/interface/%gconf.xml 一个主管界面和字体设置的配置文件,于是,干脆把它删掉,重新在系统-首选项-外观 那里设置一下字体,我的gnome-panel就这么回来了。
可能我这个问题是因为我的ubuntu和gentoo共用一个 /home 引起的,但是奇怪的是ubuntu下面板一切正常,自是gentoo有问题。
好了,问题解决了,最后一句俗却真诚的话:新年快乐!

gentoo下的pppoe拨号

最近,无线路由坏了,所以只能先用自己的电脑拨adsl了。
其实这本也没什么,我的win7和ubuntu都只要稍微设置一下就OK了。
这里再稍微提一下ubuntu的pppoe设置:记得以前的版本(应该是6.xx的时候吧),NetworkManager是不直接支持pppoe的,还要自己手工设置,然后执行pon/poff来拨号,但是现在进步了,直接在NM里输一下用户名和密码就可以上了。
但是我的gentoo是用wicd来管理网络的,而wicd至今都还不支持pppoe,于是只能用原始的命令行来拨号了。
于是eix一搜,发现有个net-dialup/rp-pppoe,安上,看到有 pppoe-setup、pppoe-start、pppoe-stop。啥都不用说了,先pppoe-setup,再pppoe-start,本以为会很顺利,但是几次尝试都在最后一步出错了,而且提示的错误都没啥价值,不知道从何查起~
正当我无计可施,想妥协安个NetworkManager的时候,忽然灵感一现,发现了可能的错误原因,那就是──内核模块。原来,之前我的gentoo内核基本上也是按需配置的,以前我一直都有路由器拨号,所以没有在内核选项里打开ppp的支持,才导致了这一郁闷的结果,哈哈,既然发现了可能的原因,那就好办了,make menuconfig 里面选上 Device Drivers —>Network device support —>PPP (point-to-point protocol) support 下面的所有项,编译完再重启。再 pppoe-start ,果然看到了 Connected!

记一下我的ubuntu升级到10.04时遇到都问题

今天,为了测试一下阿里拼音,很难得地进了一次ubuntu,后来发现居然还是9.10的版本,看不下去了,就顺手升级了一下。
本以为这种升级历史上已经做过很多次,应该不会有什么问题的,但是今天还是遇到问题了,就在这里记一下吧。
我升级的思路比较老土,就是先

再apt-get update,再一直交替进行upgrade和dist-upgrade,直到完全没有错误,再重启。如果中间遇到某个包有问题,一般是先卸载这个包,升级完成以后再给安装上就好了。
但是今天遇到一个无法先卸载的包,到某步的时候,出来这样一个错误:

E: Could not perform immediate configuration on ‘util-linux’.Please see man 5 apt.conf under APT::Immediate-Configure for details. (2)

很明显,这个是 util-linux 包出问题了,但是这个包太底层了,如果卸了这个,整个ubuntu就差不多没了,我可不敢保证我还能给折腾回去。
解决问题的思路:
先试着手工dpkg安装这个包:

看来其实是libc6这个包版本有问题,于是查到这个包及其依赖包的deb,手动下载并安装:

这样成功以后,就比较好办了,虽然直接dist-upgrade仍然不行,但是执行

就可以成功解决此问题了。

现在分析看来应该是由于我的sources.list里面没有security部分造成的,如果在里加上

deb http://security.ubuntu.com/ubuntu lucid-security main restricted universe multiverse
deb-src http://security.ubuntu.com/ubuntu lucid-security main restricted universe multiverse

应该就不会错了吧~

都说ubuntu的大版本升级比较折腾,看来还真是,呵呵。幸好咱也算老手了,不然遇到这种问题,还不被整成重装啊?

vsftpd只能匿名登录,本地用户出现530错误的一个实例

网上很多教程,在介绍vsftpd的本地用户的配置的时候,大意都是这样的:

建立一个xxx用户,家目录为/yyy/zzz,并把这个用户的shell(/etc/passwd里对应行的最后一列)设置成/sbin/nologin或者是/bin/false,再设置一个密码。
然后修改vsftpd的配置文件,一般是/etc/vsftpd.conf,加上:

local_enable=YES
write_enable=YES
local_umask=022

然后重启vsftpd就可以了。

对于出现530 Login incorrect. 的解释一般是两种:
1. xxx用户对 /yyy/zzz 没有权限。
2. xxx用户被加到 /etc/vsftpd.user_list 列表里了。
但是我今天的操作中,这个新建的用户并没有发现以上两种现象,仍然出现了可恶的530错误,但是匿名用户正常登录。
折腾半天以后,发现用一个shell是/bin/bash的用户却是可以登录ftp的。于是,试着把xxx用户的shell也改成/bin/bash,果然也可以登录了。但是这样显然还没有解决我的问题,因为这样一来,xxx这个用户都可以通过ssh登录服务器了,安全就没有保障了。
于是再找更详细的原因,终于发现了:
其实vsftpd对本地用户鉴权的过程中是可以检查用户shell的合法性的,而且默认就启用了。虽然你可以在配置文件中通过添加

check_shell=NO

来取消vsftpd对shell的检测,但是这个配置项要生效却有个前提:编译的时候不能包含PAM特性(一种*nix系统中的插件式身份鉴别模块),而ubuntu等发行版的二进制包并不能满足这点,所以除非你是自己编译的vsftpd,这个配置项是没有多少用的。
要解决这个问题,还得继续问:vsftpd是怎么检查一个shell是否合法呢?其实这个答案很简单,vsftpd读取 /etc/shells 这个文件,如果用户的shell在这个文件里存在,就认为合法,否则即使你输入了正确的密码,仍然会给你一个530,哈哈。
所以,解决办法就是:把 /sbin/nologin 或者是 /bin/false 加到 /etc/shells 中去!

grub故障一例

昨天,心血来潮进了一下许久没有使用过的ubuntu,然后顺手给它升级了一下,发现这个把月已经有200多M的更新了,其中也包括内核在内。
于是开开心心地dist-upgrade完了,也没啥异常。但是到了昨晚,再开机的时候,发现机器没有正常显示grub菜单,而是直接进入了GRUB>这样的命令行。幸好我还记得几个grub的命令,瞎蒙地还算是启动了我的gentoo,然后上网一google,发现这个问题和我之前把文件系统全面升级到ext4有关:在升级了文件系统以后,再升级内核的话,就会导致grub找不到某些文件而无法正常工作。
解决办法就是在gentoo里chroot到ubuntu的/分区(因为我的grub是在ubuntu下安装的),然后执行:

如果没报什么错误的话,那恭喜你,你的grub又回来了。
当然,有人会问:如果我硬盘上没有gentoo或者记不住grub命令无法启动的话,怎么办呢?其实很简单,你只要随便找个linux的LiveCD,或者U盘系统之类的,启动以后,就一样可以chroot了。
哈哈,linux很灵活,所以基本是不死的(当然你要对它有足够了解才行)~

lafilefixer

前几天,对gentoo进行常规升级的时候,就有个别包没有编译过去,这对gentoo来说本不算什么的(谁让咱用的是 ~x86 呢),也就没太在意,但是近来越来越多的不同的包都出现了同一个错误:
报缺少 /usr/lib/libGL.la 文件,revdep-rebuild 也不能解决问题,甚至 revdep-rebuild 的过程中也有这个错误。
于是到sir里搜了一下,发现已经有人问过了,也得到了解决。
解决办法就是装上 lafilefixer ,运行一下

其实,la文件本身就是一个记录同名动态库或者静态库文件信息的一个文本文件。而lafilefixer也仅仅是一个bash脚本,它把需要更新的la文件都重写了一遍,哈哈。

Gentoo换了profile以后,鼠标键盘不能动的解决办法

Gentoo 10.0 的profile出来已经蛮久了,但是我一直都没换,直到昨天才设置成了 default/linux/x86/10.0/desktop ,结果一大堆包需要重新编译了(呃。。好吧,其实我本来就有好几天没更新系统了。。)。
编译完重启就发现问题了:启动到gdm以后,鼠标键盘都动不了了,按什么键都无效,输不了用户名了,包括ctrl+alt+F1都不管用了,但是触摸板却是有效的。
第一个想到的 qlist -I -C x11-drivers/ 里面的几个包重新编译一下,发现无效;然后看elogv,发现hal说要把xorg.conf里的input相关的都删掉,试了也无效。
最后的解决办法是:在 /etc/make.conf 的 INPUT_DEVICES= 里加上 evdev ,然后安装 x11-drivers/xf86-input-evdev 这个包。