0%

背景

本文是 实验室服务器 ssh 无法连接 的后续。

今天同学又告诉我发生了类似的情况,师兄已经在找人维修。

我连接上去看了服务器重启时间约 10 点。又看了几个信息:

  1. sar 监控
  2. journalctl 日志
  3. 磁盘信息

sar 监控

之前设置的 sar 监控频率是默认的,即 10 分钟采样一次。sar 怎么安装和配置可以参考 安装 sysstat 获取 CPU/ 磁盘 / 网络统计日志

过程

首先,创建一个 docker 容器,其 entrypoint 不是可回收子进程(init-like)的命令,也不要加上 --init 选项。这样即便僵尸被 init 进程收养,也不会被马上回收,方便观察。

在容器内部进行以下操作:

// parent.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>

int main() {
    pid_t pid = fork();

    if (pid < 0) {
        perror("fork failed");
        exit(1);
    }

    if (pid == 0) {
        // 子进程
        printf("Child process (PID: %d) exiting immediately.\n", getpid());
        exit(0);  // 快速退出,变成僵尸
    } else {
        // 父进程
        printf("Parent process (PID: %d), child PID: %d\n", getpid(), pid);
        printf("Sleeping 60 seconds without wait() to create zombie...\n");
        sleep(60);  // 不调用 wait(),让子进程变成僵尸
        printf("Parent exiting.\n");
    }

    return 0;
}

编译运行,然后在另外一个窗口将父进程 kill 掉。

root@677e804cc08c:/workspace/zombie# gcc -o parent parent.c
root@677e804cc08c:/workspace/zombie# ./parent
Parent process (PID: 308506), child PID: 308507
Sleeping 60 seconds without wait() to create zombie...
Child process (PID: 308507) exiting immediately.
Terminated

占用过多内存

TL;DR:大多是具体脚本的问题,不好处理。有的是没有及时 gc,有的是把所有数据全部载入内存之后(假设了内存足够大)才开始处理。

我是遇到了这样一个问题: megatron-lm huggingface 教程运行时资源占用大、有僵尸2025/4/22 文章还在修改中,尚未公开)。

o4-mini 建议我调整启动方式:multiprocessing — Process-based parallelism — Python 3.13.3 documentation

方法描述特征
spawn创建新的解释器并运行,干净但慢,是 Windows、macOS 的默认方法。大量 python -c from multiprocessing.spawn import spawn_main; 进程
fork直接 fork。除了 macOS 之外的 POSIX 的默认方法,Python 3.14 之后默认方法改成 forkserver大量命令参数(cmd)和启动命令相同的进程,例子 python tools/preprocess_data.py --input ...
forkserver首次用 forkserver 方法创建进程会启动一个单线程(除非 import 的库导致了多线程)的 server,每次调用 os.fork() 都是干净的,不会有冗余线程。大量 python -c from multiprocessing.forkserver import ... 进程

事情由来

实验室在今年初安装了两台新的服务器,IP 最后一个段分别为 149 和 150,后文将用这两个数字来指代服务器。据说 149 服务器网络出现故障。

检查网络配置(非原因,可以跳过)

journalctl 看日志说 dhcp4 失败。包失败的,服务器都是静态 ip,没有配置 dhcp 服务器。为什么明明是静态 ip 却要尝试使用 dhcp 服务器呢?

禁用异常的网络接口

考虑到安装的是桌面版,起作用的是 NetworkManager。

再检查一下,systemd-networkd 果然不活跃,确实应该是去检查 NetworkManager。

说明

从 Windows 的资源管理器拖动文件到连接到远程服务器的 VS Code 窗口,或者从 VS Code 文件目录下载文件,速度都比 scp 慢得多(3M/s VS 20M/s)。此文章尝试寻求其他替代方案。

rsync

macOS/Linux

参考 Remote Development Tips and Tricks,在 Linux/macOS 上面比较方便。

rsync -rlptzv --progress --delete --exclude=.git "user@hostname:/remote/source/code/path" .

Windows 使用 WSL 的 rsync

在 Windows 上面比较麻烦,VS Code 给出的教程是:

wsl rsync -rlptzv --progress --delete --exclude=.git "user@hostname:/remote/source/code/path" "`$(wslpath -a '$PWD')"

起因

今天同门遇到了一些 draw.io 图像导出的问题,我和他一起分析定位了原因。

draw.io 论文画图三宗罪

截至 2025/4/13:

  1. 导出 pdf 兼容性不好,得用旧版或者 ghostscript 转换。
  2. 把 px 和 pt 混为一谈。
  3. px 的单位支持小数,导出时四舍五入,导致和预览时效果不一致。

我确实也承认 draw.io 方便,但有些地方还是很痛苦的。其他备选项:PowerPoint、Visio。

draw.io 导出 pdf 过大

使用脚本对图像压缩

实际上是一个 draw.io 文件中有大量的图片,每张图片体积过大,一个 draw.io 文件总体积达到 50M 以上。先导出之后再用 pdf 压缩工具图像会糊得厉害,但如果对每张图片分别压缩,再导出即可在保持图像高质量的同时显著降低 draw.io(以及之后导出的 pdf)的 文件体积。

绝对导入需要包在 sys.path 中,可以通过环境变量 PYTHONPATH 来增加一些搜索路径。

相对导入需要当前在一个子包内。Relative imports in Python 3 - Stack Overflow 这个回答就说明如果直接运行一个包含了 import .xx 或者 from .xx import xx 的脚本就会失败,我的直观感受是这样的文件只能出现在比 main 文件(__name____main__ 的那个文件,也就是入口文件)更深的文件夹下。相对导入有助于避免 sys.path 中出现更靠前的搜索路径,且该路径包含同名包,导致真正要导入的包被覆盖。

例子(什么时候相对导入会出错):

main.py
mypackage/
    __init__.py
    mymodule.py
    myothermodule.py  # from .mymodule import xx

直接运行 main.py 和 mymodule.py 都 OK,但是运行 myothermodule.py 则报错说没有 parent module。通过 python -m 来将文件视为一个模块运行,则可以将文件所在的文件夹作为 module 来运行,提供了 module 环境,但是该文件所在的文件夹并不会被加入 sys.path 中。

Elapsed and execution time for commands in ZSH

在 ~/.zshrc 文件中添加:

function preexec() {
  timer=$(($(date +%s%0N)/1000000))
}

function precmd() {
  if [ $timer ]; then
    now=$(($(date +%s%0N)/1000000))
    elapsed=$(($now-$timer))

    export RPROMPT="%F{cyan}${elapsed}ms %{$reset_color%}"
    unset timer
  fi
}

2025/4/20 缺点:计时显示在一行最后,每次复制终端文本会把这个时间一并复制了,而且这一行相当长导致时间显示在第二行(折行),让人摸不着头脑这个多余的文本是哪里来的。

https://godbolt.org/z/n8hns47M8

#include <iostream>
#include <string>

int main() {
  std::string str = "hello";
  std::cout << "String: \"" << str << "\"" << std::endl;
  std::cout << "Length: " << str.length() << std::endl;
  std::cout << "Capacity: " << str.capacity() << std::endl;
  std::cout << "Size: " << sizeof(str) << std::endl;

  str += " world!。。。。";
  std::cout << "\nString: \"" << str << "\"" << std::endl;
  std::cout << "Length: " << str.length() << std::endl;
  std::cout << "Capacity: " << str.capacity() << std::endl;
  std::cout << "Size: " << sizeof(str) << std::endl;

  str += str;
  std::cout << "\nString: \"" << str << "\"" << std::endl;
  std::cout << "Length: " << str.length() << std::endl;
  std::cout << "Capacity: " << str.capacity() << std::endl;
  std::cout << "Size: " << sizeof(str) << std::endl;
  return 0;
}

可以看到 libstdc++ 中的 capacity 的增长方式是 15 → 30 → 60。一开始 SSO 缓冲区只能容纳 16 个字符,最后一个字符是 '\0',因此初始状态下只能容纳 15 个非空字符,capacity 为 15。而后面倍增的时候会直接按照非 '\0' 字符的最大容量来做倍增,而非按照实际缓冲区容量做倍增(即 15 → 31 → 63 这样的路径)。

而 libc++ 中 capacity 的后续倍增是考虑了空字符的:22 → 47 → 95。一开始并不是按照缓冲区容量 23 去倍增,而是用的 24,可能是认为 23 这个数字不够规整。