Showing Posts From

Linux

Missing X server or $DISPLAY:无头环境跑浏览器的正确姿势

在服务器 / 容器里想跑 Chromium、Electron、Playwright,报错: [...] ozone_platform_x11.cc:259] Missing X server or $DISPLAY意思很直白——程序想弹 GUI,但当前环境没 X11 显示服务。三个典型场景。 场景一:SSH 到 Linux 服务器 SSH 登进去的 shell 默认没有桌面: ssh root@server google-chrome # 直接报 Missing X server两种解法: Headless 模式(首选) google-chrome --headless=new --disable-gpu --no-sandbox https://example.comPlaywright: browser = playwright.chromium.launch(headless=True)Puppeteer: const browser = await puppeteer.launch({ headless: true });非 headless 场景用 X11 转发 ssh -X root@server或用 -Y(信任模式,速度更快)。前提是本机有 X server(Mac 上是 XQuartz,Windows 上是 VcXsrv/X410)。 场景二:Docker 容器里 容器默认没 X server: $ echo $DISPLAY (空)首选还是 headless。要真的跑 GUI 就靠 Xvfb(虚拟帧缓冲): apt update && apt install -y xvfb xvfb-run -a chromium https://example.comxvfb-run 会在后台起一个虚拟 X server 让程序连。Playwright 官方镜像就自带这套。 如果显示环境变量已经写了 DISPLAY=:1,但仍然报错,那是因为 :1 对应的 X server 根本没起: ls /tmp/.X11-unix/看不到 X0 / X1 就是没启动。容器里手动 export DISPLAY=:1 是没用的——DISPLAY 只是"连哪个 X server",不会创建 X server。 场景三:WSL WSL2 现在原生支持 WSLg,一般不会碰到这错。老 WSL1 或者关了 WSLg 的话: export DISPLAY=:0 sudo apt install x11-apps xclock # 测试有没有 GUI再不行就装个 X server(Windows 上 VcXsrv / X410)。 快速判断清单 先看几个变量: echo $DISPLAY # 应该是 :0 或 :1 ls /tmp/.X11-unix/ # 应该有 X0 之类的 socket ps aux | grep -E 'Xorg|Xvfb' # 有 X server 进程在跑 hostname # 类似 6e6340e2e0d2 基本是容器 cat /proc/1/cgroup # 看到 docker 关键字 = 容器对号入座:现象 判断DISPLAY 空 没有桌面环境,用 headless 或 XvfbDISPLAY 有但 X11 socket 目录空 声明了但 X server 没起hostname 是短哈希 Docker 容器WSL 里 xclock 不动 WSL 侧 X server 没通一句话总结 服务器 / 容器跑浏览器,能 headless 就 headless,非要 GUI 上 Xvfb。 手动改 $DISPLAY 只是告诉客户端连哪儿,从来不会凭空生出一个 X server。

cp 报错 cannot overwrite non-directory:目录被同名文件占位

某次要把 pojie/ 目录复制进 wpa-dictionary/ 里,cp 一直报错: $ cp -r pojie/ wpa-dictionary/pojie cp: cannot overwrite non-directory 'wpa-dictionary/pojie' with directory 'pojie/'$ cp -r pojie wpa-dictionary/ cp: cannot overwrite non-directory 'wpa-dictionary/pojie' with directory 'pojie'$ cp -f pojie wpa-dictionary/ cp: -r not specified; omitting directory 'pojie'问题原因 这个报错不是 cp 参数写错了,而是 目标路径 wpa-dictionary/pojie 已经存在一个普通文件,cp 不允许用目录去覆盖一个非目录。 先用 ls -l 确认: $ ls -l wpa-dictionary/pojie -rw-r--r-- 1 root root 1234 Jun 15 10:00 wpa-dictionary/pojie看到开头是 - 说明它是普通文件。而你想执行的 cp -r pojie wpa-dictionary/ 实际上等于 cp -r pojie wpa-dictionary/pojie——目标名字已经被文件占了。 三种处理方式 方案一:删除同名文件 如果目标位置那个文件本来就没用,直接删掉再复制: rm -f wpa-dictionary/pojie cp -r pojie wpa-dictionary/方案二:换个目标名 不想删旧文件的话,改个新名字: cp -r pojie wpa-dictionary/pojie_new方案三:先看清目录结构 用 tree 看一眼当前目录状态,避免以后再踩: tree -L 2 .或者两条 ls -ld 直接对比属性: ls -ld pojie ls -ld wpa-dictionary/pojie看到一个是 d 开头(目录)、一个是 - 开头(文件),就知道冲突在哪里了。 一句话总结 cp: cannot overwrite non-directory = 目标位置已被同名普通文件占位。要么删掉旧文件,要么换个目标名。

看 free -h 别只盯 free 那列:available 才是真

服务器一查内存: $ free -h total used free shared buff/cache available Mem: 14Gi 10Gi 146Mi 0.0Ki 4.2Gi 4.1Gi Swap: 0B 0B 0B看到 free: 146Mi 就觉得"内存要爆了"——不对,Linux 从来不闲着,它会把空闲内存拿去做各种缓存。 各列到底是什么列 含义total 总内存used 已被进程占用free 完全空闲,谁都没在用(少不代表不好)shared tmpfs / shared memory 占用buff/cache 内核用作 page cache / dentry 缓存available 实际还能分配给程序的内存(这个才是真)关键:buff/cache 是弹性的——一旦有程序需要内存,内核会立刻回收 cache 让出来。所以真正衡量"还剩多少"的指标是 available,不是 free。 上面例子里 available = 4.1Gi,说明还能舒服再开 4G 内存的程序,没问题。 buff/cache 具体缓存什么Page cache:读过的文件内容缓存在内存里,下次读同一文件秒回 Dentry cache:目录项缓存,让 ls / find 之类操作更快 Inode cache:文件元数据(大小、时间戳)缓存 Buffers:块设备 IO 缓冲这些都是性能加速——把内存"用起来"总比闲着好。 手动清 cache 极少用得上,但知道一下: sync # 先把脏页刷盘 echo 3 > /proc/sys/vm/drop_caches # 清所有 cache1 = 清 page cache 2 = 清 dentry + inode 3 = 全清清完 free -h 里 free 会瞬间变大——但系统 IO 性能会立刻下降,因为所有文件读要重新从磁盘拉。生产上除非做基准测试,不然不要清。 Swap = 0 的风险 上面的例子 Swap: 0B。开 swap 有一个好处:内存暂时不够时,把不活跃页面换到磁盘,避免 OOM Killer 杀进程。 没 swap 的情况,某个进程突然吃到 15G 内存,系统会直接触发 OOM Killer: Killed process 12345 (python) Killed process 23456 (mysqld)杀谁看 oom_score(内核给每个进程算一个"该杀分数"),基本是"占内存多、优先级低"的先中招。 加个 swap(4G) sudo fallocate -l 4G /swapfile sudo chmod 600 /swapfile sudo mkswap /swapfile sudo swapon /swapfile验证: free -h # Swap: 4.0Gi ...开机自动挂载: echo '/swapfile swap swap defaults 0 0' | sudo tee -a /etc/fstabSwap 大小经验:内存 < 2G:swap 是内存的 2 倍 内存 2-8G:swap 等于内存 内存 > 8G:swap 2-4G 或者干脆不开(数据库服务器)找谁在吃内存 RSS 排序(真实占用物理内存): ps aux --sort=-rss | head -20或者 top 里按 M 排。列 RES才是实际占用,VIRT(虚拟内存)意义不大。 看某个进程详情: cat /proc/<PID>/status | grep -E 'VmRSS|VmSize'看具体是哪部分: pmap -x <PID> | tail -1MySQL / Redis / Java 类"吃内存大户" 这些程序的内存占用大多是配置定的:MySQL:innodb_buffer_pool_size,默认可能占 70% 内存 Redis:maxmemory,不配就吃到爆 Java:-Xmx 设堆大小,不设 JVM 会按机器算上生产前把这些参数对着物理内存卡好,不然多进程叠加轻松超总内存。 Linux OOM 前的预警 看 dmesg 有没有: sudo dmesg -T | grep -i "oom\|killed"Out of memory: Killed process 12345 (python) total-vm:15234567kB看到这个说明已经杀过了。生产上应该主动监控——node_exporter + Prometheus 采 node_memory_MemAvailable_bytes,跌破阈值发告警。 一句话总结 free -h 看的是 available,不是 free;buff/cache 是好东西不是问题。没 swap 的服务器建议加 4G 兜底,主要吃内存的程序(MySQL / Redis / Java)参数要卡死。

CIDR 记法要写对:43.255.122.56/24 匹配的是一整段

配置防火墙、白名单、路由规则的时候,经常见到有人这样写: 43.255.122.56/24本意是想匹配单个 IP,结果匹配了 256 个。CIDR 的斜杠语法是"前面多少位固定",不是"IP 加上标签"。 /24 到底表示什么 43.255.122.56/24 意思是:IP 的前 24 位固定。IPv4 一共 32 位,24 位固定就是前三段(43.255.122)不变,最后一段(.56)被忽略、整段 0-255 都算命中。 等价于: 43.255.122.0 – 43.255.122.255 (256 个地址)所以: 43.255.122.0 ✅ 命中 43.255.122.56 ✅ 命中 43.255.122.100 ✅ 命中 43.255.122.255 ✅ 命中 43.255.123.56 ❌ 未命中想匹配单个 IP:/32 要精确匹配一个 IP,写 /32——32 位全都固定,就是它自己: 43.255.122.56/32如果配置格式不要求必须有 CIDR 后缀,直接写 IP 也行: 43.255.122.56/32 是最严格的等价形式。 常见 CIDR 对照写法 匹配范围 地址数43.255.122.56/32 只 43.255.122.56 143.255.122.56/31 43.255.122.56 – 57 243.255.122.56/30 43.255.122.56 – 59 443.255.122.56/29 43.255.122.56 – 63 843.255.122.56/28 43.255.122.48 – 63 1643.255.122.56/27 43.255.122.32 – 63 3243.255.122.56/26 43.255.122.0 – 63 6443.255.122.56/25 43.255.122.0 – 127 12843.255.122.56/24 43.255.122.0 – 255 25643.255.122.56/16 43.255.0.0 – 43.255.255.255 65,53643.255.122.56/8 43.0.0.0 – 43.255.255.255 16M+43.255.122.56/0 整个 IPv4 空间 全部注意:CIDR 里"起点 IP"随便写,实际会向下对齐。43.255.122.56/24 和 43.255.122.99/24 等价,都指 43.255.122.0/24 这个网段。 计算方法 给一个 CIDR A.B.C.D/N,怎么算范围? 掩码位数 → 主机位数 = 32 - N 主机位数决定了段长:主机位 = 0:1 个 IP(/32) 主机位 = 1:2 个(/31) 主机位 = 8:256 个(/24) 主机位 = 16:65536 个(/16) 主机位 = N:2^N 个起点:把 A.B.C.D 的后 (32-N) 位置零。 例:10.0.5.37/28N=28,主机位=4 段长 = 2^4 = 16 后 4 位置零:37 二进制 00100101 → 后 4 位置零 00100000 = 32 范围:10.0.5.32 – 10.0.5.47Linux 命令算 ipcalc 是最快的: $ ipcalc 43.255.122.56/24 Address: 43.255.122.56 Netmask: 255.255.255.0 = 24 Network: 43.255.122.0/24 HostMin: 43.255.122.1 HostMax: 43.255.122.254 Broadcast: 43.255.122.255 Hosts/Net: 254或者 Python 一行: import ipaddress net = ipaddress.ip_network("43.255.122.56/24", strict=False) print(net.network_address, net.broadcast_address, net.num_addresses)常见误区 误区 1:以为 /24 加个数字更保险不是。任何斜杠后缀都是"网段掩码",你越加越大。 误区 2:/32 单独 IP 时可以省掉大多数系统能省,但**某些严格配置格式(RBAC 白名单、AWS SG)**要求必须写 /32,别偷懒。 误区 3:以为 /0 是"无匹配"反过来——0.0.0.0/0 匹配整个 IPv4 空间,路由表里就是默认路由。 IPv6 版 IPv6 也用 CIDR: 2001:db8::1/128 单个 IP 2001:db8::/32 一整段 ::/0 整个 IPv6 空间规则一样,只是位数从 32 变成 128。 一句话总结 IP 加 /N 是网段,前 N 位固定其余任意。单个 IP 要写 /32(IPv6 是 /128)。别偷懒不加 CIDR 后缀,也别把 /24 当成"某个 IP 的编号"。

写个 Bash 自动 git pull:三种姿势和 cron 里的坑

服务器上想自动拉最新代码,写个脚本挂 cron 跑。看着就两行,实际上有几个必吃的坑。 最基础写法 #!/bin/bash cd /www/wwwroot/yourproject || exit 1 git pull|| exit 1:cd 失败就退出,不然当前目录是家目录,git pull 会执行在错误的仓库 保存为 pull.sh chmod +x pull.sh 执行 ./pull.sh更稳的写法:不依赖 cd git -C 可以直接指定仓库路径,省掉 cd 的失败风险: #!/bin/bash git -C /www/wwwroot/yourproject pull origin main同一个脚本要处理多个仓库时特别顺手。 挂 cron 时会踩的坑 */5 * * * * /www/wwwroot/scripts/pull.sh >> /var/log/pull.log 2>&1看着没问题,实际经常挂。原因: 1. SSH 密钥没加载 用户手动跑时有 ssh-agent。cron 里没有,git@github.com 直接拒绝。解法:走 HTTPS + PAT,别用 SSH 或在脚本里显式指定密钥:export GIT_SSH_COMMAND="ssh -i /root/.ssh/deploy_key -o StrictHostKeyChecking=no" git -C /www/wwwroot/yourproject pull origin main2. PATH 太窄 cron 的 PATH 只有 /usr/bin:/bin,找不到自定义安装的 git。crontab 里手动补: PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin */5 * * * * /www/wwwroot/scripts/pull.sh3. 分支不明确 裸 git pull 会走当前分支和默认 remote,可能拉到不该拉的分支。永远显式: git -C "$REPO" pull origin main处理本地修改的完整版 生产脚本上还得防几件事: #!/bin/bash set -eREPO=/www/wwwroot/yourproject BRANCH=maincd "$REPO"# 1. 如果本地有修改,暂存(避免 pull 失败) if [[ -n $(git status --porcelain) ]]; then echo "[$(date)] 本地有修改,先 stash" git stash push -m "auto-stash $(date +%s)" fi# 2. 拉最新 git fetch origin "$BRANCH" git reset --hard "origin/$BRANCH" # 直接对齐远程,别 merge# 3. 清空 stash(不还原,防止和拉下来的冲突) git stash clear注意 git reset --hard 会覆盖本地任何修改。这是刻意的——线上机器上不该有手改,任何修改都视为"意外"直接抹掉。要保守就用 git pull --ff-only。 大小写敏感的坑 服务器 Linux 大小写敏感,Windows / macOS 默认不敏感。仓库里同时存在: RiderAuditController.php RiderauditController.phpLinux 上拉下来会真的多出两个文件,Windows 上只能存一个,pull 立刻挂。 解法:清理仓库中的重名文件,用 git rm 保留一个: git rm --cached RiderauditController.php git commit -m "fix: 大小写重复" git push或者临时关掉大小写敏感(治标): git config core.ignorecase true一句话总结 git -C REPO pull origin BRANCH 是最省事的一行。挂 cron 要额外交代 PATH、SSH 密钥、分支名,生产脚本再套一层 reset --hard 防脏树。

Linux 上 cron 定时任务不跑?先查 cron 服务本身

写好一条 crontab,等到点没触发,先怀疑不是 cron 表达式的问题,是 cron 服务本身。Debian 系和 RHEL 系的服务名和日志路径都不一样,一步步查。 第一步:确认服务在跑 Debian / Ubuntu — 服务名叫 cron: systemctl status cron sudo systemctl start cron # 没起就启动 sudo systemctl enable cron # 开机自启CentOS / Rocky / AlmaLinux — 服务名叫 crond: systemctl status crond sudo systemctl start crond sudo systemctl enable crond粗暴验证: ps -ef | grep -E 'cron|crond'看到常驻进程就没死。 第二步:确认任务被 cron 读到了 用户任务: crontab -l # 查看 crontab -e # 编辑系统级任务分散在这几处: cat /etc/crontab ls /etc/cron.d/ ls /etc/cron.{hourly,daily,weekly,monthly}/cron 表达式必须以换行结尾——最后一条任务后没换行,很多 cron 实现会直接忽略这一条。 第三步:看日志 Ubuntu / Debian: grep CRON /var/log/syslog或走 journalctl: journalctl -u cron -fCentOS / Rocky: grep CRON /var/log/cron tail -f /var/log/cron journalctl -u crond -f日志里应该能看到每次触发的 (user) CMD (...)。看不到说明 cron 根本没执行这条:检查表达式、用户、文件位置。 第四步:cron 环境是"最小 shell" crontab 里的命令继承的 PATH 极简,通常只有 /usr/bin:/bin。写 python 会找不到,得写绝对路径: * * * * * /usr/bin/python3 /home/user/task.py或者在 crontab 里显式设 PATH: PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin * * * * * python3 /home/user/task.py第五步:脚本挂了但看不到报错 cron 默认把 stdout/stderr 发邮件,服务器上一般没配。把输出重定向到日志: * * * * * /path/to/task.sh >> /var/log/task.log 2>&1>> /var/log/task.log 追加、2>&1 把 stderr 合流。看日志就能知道到底哪一步失败。 常见坑速查现象 原因完全没触发 cron 服务没起 / 表达式写错手动跑脚本 OK,cron 跑挂 PATH 缺、用户身份不同、cwd 不同报"command not found" 命令写了相对路径,改绝对路径任务跑了但看不到结果 没重定向输出,看邮件或加 >> 日志最后一条任务没跑 crontab 文件末尾没换行系统 crontab 没生效 系统级 /etc/crontab 需要用户字段系统 crontab 和用户 crontab 的区别 /etc/crontab 每行必须写明执行用户: 0 3 * * * root /root/backup.shcrontab -e 编辑的用户 crontab 不需要,因为它已经是"这个用户"的。搞混了就没反应。 一句话总结 cron 任务不跑,按 服务状态 → 任务在不在 → 日志 → PATH → 输出重定向 五步走完。90% 的问题在 PATH 缺失或输出被吞。

rclone 批量上传下载脚本:重试 + 日志 + 分享链接

rclone 一条命令就能传单个文件到 Google Drive / OneDrive / S3 / OSS。但生产环境要批量、重试、日志、结果链接——这就要写脚本。 前置:rclone 配好 remote rclone config跟着交互命令走,配好一个叫 gdrive 的 remote(可以是 Google Drive、S3、OSS、Backblaze 都行,脚本不区分)。 验证: rclone lsd gdrive:能列出根目录说明认证 OK。 批量上传脚本 upload.sh: #!/usr/bin/env bash set -uLOG_FILE="./upload.log" RETRY=3 REMOTE_DIR="gdrive:backup"for file in "$@"; do echo "上传: $file" | tee -a "$LOG_FILE" if [ ! -e "$file" ]; then echo "文件不存在: $file" | tee -a "$LOG_FILE" continue fi filename=$(basename "$file") remote_path="$REMOTE_DIR/$filename" count=0 success=0 while [ $count -lt $RETRY ]; do printf "第 %d 次尝试\n" $((count + 1)) | tee -a "$LOG_FILE" rclone copy "$file" "$REMOTE_DIR" \ --stats 5s --stats-one-line \ 2>&1 | tee -a "$LOG_FILE" if [ ${PIPESTATUS[0]} -eq 0 ]; then success=1 break fi count=$((count + 1)) sleep 2 done if [ $success -eq 1 ]; then echo "成功: $file" | tee -a "$LOG_FILE" # 生成分享链接(Google Drive / OneDrive 支持) link=$(rclone link "$remote_path" 2>/dev/null) if [ -n "$link" ]; then echo "分享链接: $link" | tee -a "$LOG_FILE" fi else echo "失败: $file" | tee -a "$LOG_FILE" fi done用法: chmod +x upload.sh ./upload.sh a.zip b.tar.gz c.iso关键点:$@ — 传所有命令行参数 ${PIPESTATUS[0]} — 拿管道第一个命令的退出码(tee 会覆盖 $?,这里必须显式取 rclone 的) --stats 5s --stats-one-line — 每 5 秒一行简洁进度,log 好看 rclone link — 生成分享链接,不支持的后端会返回空对称的下载脚本 download.sh: #!/usr/bin/env bash set -uLOG_FILE="./download.log" RETRY=3 REMOTE_DIR="gdrive:backup"for remote_file in "$@"; do echo "下载: $remote_file" | tee -a "$LOG_FILE" remote_path="$REMOTE_DIR/$remote_file" count=0 success=0 while [ $count -lt $RETRY ]; do printf "第 %d 次尝试\n" $((count + 1)) | tee -a "$LOG_FILE" rclone copy "$remote_path" ./ \ --stats 5s --stats-one-line \ 2>&1 | tee -a "$LOG_FILE" if [ ${PIPESTATUS[0]} -eq 0 ]; then success=1 break fi count=$((count + 1)) sleep 2 done if [ $success -eq 1 ]; then echo "成功: $remote_file" | tee -a "$LOG_FILE" else echo "失败: $remote_file" | tee -a "$LOG_FILE" fi done用法: ./download.sh a.zip b.tar.gz加分项 并发上传:rclone copy 支持 --transfers 参数,一次传多个文件: rclone copy "$file" "$REMOTE_DIR" --transfers 8 --checkers 16限速(别把家里带宽打满): rclone copy "$file" "$REMOTE_DIR" --bwlimit 10M校验完整性: rclone check "$file" "$REMOTE_DIR/$filename"断点续传:rclone copy 天生支持,直接重跑同一命令即可。已经完成的文件会跳过。 多线程分块(大文件): rclone copy "$file" "$REMOTE_DIR" \ --multi-thread-streams 4 \ --multi-thread-cutoff 100M挂 cron 定时备份 0 3 * * * /home/me/upload.sh /var/backups/db-*.sql.gz >> /var/log/backup.log 2>&1配合 find 只传新文件: find /var/backups -mmin -60 -name "*.sql.gz" -exec /home/me/upload.sh {} +一句话总结 rclone copy + Bash 循环 + ${PIPESTATUS[0]} 判成功 = 稳的批量传输脚本。加个 --stats 看进度、rclone link 抓分享链接就够生产用。

Node 报 GLIBC_2.38 not found 的根因和四种解法

宝塔服务器启动 Node 项目,直接崩: Error: /lib64/libm.so.6: version `GLIBC_2.38' not found (required by /www/wwwroot/yourproject/server/node_modules/sqlite3/build/Release/node_sqlite3.node) code: 'ERR_DLOPEN_FAILED'这是典型的 glibc 版本不匹配。sqlite3.node 是个 native 二进制,编译时链接的 glibc 是 2.38,服务器 glibc 只有 2.17 或 2.28,加载不了。 先确认服务器 glibc 版本 ldd --versionCentOS 7 通常是 2.17,Rocky 8 / 老 Ubuntu 是 2.28。而 Ubuntu 24 / Debian 13 的 glibc 已经到 2.38+,本地编译的东西拿过去自然跑不动。 场景还原 99% 是这么造成的:本地新系统 npm install 打包整个项目(包括 node_modules)上传到服务器 服务器上直接 node index.js 崩node_modules/sqlite3/build/Release/node_sqlite3.node 是在本机 Ubuntu 24 编译的,扔到 CentOS 7 上当然报 GLIBC。 四种解法 方案一:服务器重新编译(推荐) cd /www/wwwroot/yourproject/server rm -rf node_modules package-lock.json npm install或者只重编译原生模块: npm rebuild sqlite3编译失败通常是缺工具链: # CentOS / RHEL yum groupinstall "Development Tools" -y yum install python3 gcc gcc-c++ make -y# Ubuntu / Debian apt install build-essential python3 make g++ -y方案二:本地别上传 node_modules 正确的部署流程:本地只上传 package.json + package-lock.json 服务器 npm install.gitignore 里加上 node_modules/,部署脚本里 rsync --exclude=node_modules/,这类问题就彻底没了。 方案三:换更省心的 SQLite 驱动 老牌 sqlite3 依赖 node-gyp 编译,每次升 Node 都可能炸。换成:better-sqlite3(同步 API、性能更好) libsql(Cloudflare 生态)npm install better-sqlite3大多数场景 API 迁移不复杂,还能避开 native 编译地狱。 方案四:升级系统 glibc(不推荐) CentOS 7 手动装高版本 glibc 有极高概率把系统弄崩,别碰。要新 glibc 就直接升系统或者上 Docker——用官方 Node 镜像编译,产物就是那个环境的 glibc。 一条经验 Node 项目部署,永远不要把 node_modules 传上服务器。 让服务器自己 npm install,很多 native 模块问题从此消失。

systemd 服务里设置工作目录:WorkingDirectory

写 Python / Node 服务的 systemd 单元时,程序里各种相对路径都得基于"工作目录"来解读。systemd 提供了 WorkingDirectory 字段,比在 ExecStart 里 cd 一下再启动规范得多。 最简单的例子 [Unit] Description=My App After=network-online.target[Service] Type=simple User=appuser WorkingDirectory=/opt/myapp ExecStart=/usr/bin/python3 main.py Restart=always RestartSec=3[Install] WantedBy=multi-user.targetWorkingDirectory=/opt/myapp 的效果:进程启动时 cwd 就是这个目录 ExecStart 里的相对路径按它算(比如 python3 main.py 就是 /opt/myapp/main.py) Python 里 os.getcwd() 返回这个目录 写日志到 ./logs/app.log 就是 /opt/myapp/logs/app.log为什么不用 cd 见过这种写法: ExecStart=/bin/bash -c 'cd /opt/myapp && python3 main.py'能跑,但坏处一堆:多一层 shell,进程树多了个 bash 语义没那么直观 systemd 的 Restart / 信号处理会作用在 bash 上,不是真进程 环境变量、User、日志跟你想的不一样能用字段就用字段,cd 是最后的兜底。 常配套的字段 User / Group 以指定用户启动,不用 root 跑业务: User=appuser Group=appuser注意 WorkingDirectory 里的目录必须让这个用户能读、能进: sudo chown -R appuser:appuser /opt/myappEnvironmentFile 把环境变量放外面,方便修改: EnvironmentFile=/etc/myapp/envenv 文件内容: DATABASE_URL=postgres://user:pass@localhost/mydb APP_PORT=8000代码里 os.getenv("DATABASE_URL") 就能拿到。 Restart 崩溃后的行为: Restart=always # 无条件重启 RestartSec=3 # 3 秒后重启 StartLimitInterval=60 # 60 秒内 StartLimitBurst=5 # 最多重启 5 次,再多就放弃生产上都需要——不然崩了以后就得手动来。 日志 默认 stdout / stderr 会走 journald: journalctl -u myapp -f journalctl -u myapp --since "10 minutes ago"想直接写文件: StandardOutput=append:/var/log/myapp/out.log StandardError=append:/var/log/myapp/err.logappend: 需要 systemd 240+,老版本用 file:。 一个"生产可用"的模板 [Unit] Description=My FastAPI App After=network-online.target Wants=network-online.target[Service] Type=simple User=appuser Group=appuser WorkingDirectory=/opt/myapp EnvironmentFile=/etc/myapp/env ExecStart=/opt/myapp/.venv/bin/uvicorn main:app --host 0.0.0.0 --port 8000 Restart=always RestartSec=3 StartLimitBurst=5 StandardOutput=journal StandardError=journal[Install] WantedBy=multi-user.target启用: sudo systemctl daemon-reload sudo systemctl enable --now myapp sudo systemctl status myapp一句话总结 WorkingDirectory=/path 就是 cwd,不要在 ExecStart 里套 bash -c 'cd ... && ...'。搭配 User、EnvironmentFile、Restart 一份 systemd 单元就是完整的服务定义。

Linux 上用 vsftpd 搭 FTP:本地用户 + 被动模式 + 防火墙

Linux 上搭 FTP 最常用 vsftpd(Very Secure FTP Daemon)——轻量、稳定、配置简单。但先说前提:FTP 是明文协议,2024 年再上生产环境只推荐 SFTP 或 FTPS。搭 FTP 通常是维护老系统 / 内网数据交换用。 安装 Ubuntu / Debian: sudo apt update sudo apt install vsftpdCentOS / Rocky: sudo yum install vsftpd启动: sudo systemctl enable --now vsftpd sudo systemctl status vsftpd关键配置 编辑 /etc/vsftpd.conf: # 禁用匿名(生产必须) anonymous_enable=NO# 允许本地用户登录 local_enable=YES# 允许写入 write_enable=YES# 限制用户只能在自己家目录 chroot_local_user=YES allow_writeable_chroot=YES# 被动模式端口范围(关键) pasv_enable=YES pasv_min_port=30000 pasv_max_port=31000# UTF-8 文件名 utf8_filesystem=YES# 日志 xferlog_enable=YES xferlog_file=/var/log/vsftpd.log# 默认监听 21,可改 listen_port=21改完重启: sudo systemctl restart vsftpd为什么需要被动模式端口范围 FTP 协议有两种模式:主动模式(Active):客户端告诉服务器"你连回我的 xxx 端口"——服务器主动发起数据连接。大多数客户端在 NAT 后面,这条路走不通 被动模式(Passive):服务器打开一个端口告诉客户端"你连过来"——现代默认被动模式服务器需要有一段可用端口范围(例子里是 30000-31000)。防火墙必须放行这段: sudo ufw allow 21/tcp sudo ufw allow 20/tcp # 主动模式(可选) sudo ufw allow 30000:31000/tcp sudo ufw reload云服务器(阿里云、腾讯云、AWS)安全组里也要放行这段。经常有人本地防火墙开了、云安全组没开,一样连不上。 创建 FTP 用户 方式 1:普通本地用户 sudo adduser ftpuser sudo passwd ftpuser# 家目录权限 sudo mkdir -p /home/ftpuser/ftp sudo chown -R ftpuser:ftpuser /home/ftpuser/ftp sudo chmod 755 /home/ftpuser方式 2:不允许 shell 登录(更安全) sudo adduser --shell /usr/sbin/nologin ftpuser然后 /etc/shells 里加: /usr/sbin/nologin不然 vsftpd 会拒绝这类用户。 SELinux(CentOS / RHEL) sudo setsebool -P ftp_home_dir on sudo setsebool -P ftpd_full_access on不开 SELinux 布尔值就是"连上能列表、能创建但一写文件就 550"的经典状态。 测试 本机测: ftp -p localhost # -p 强制被动模式或者用现代客户端 lftp: lftp -u ftpuser,PASSWORD ftp://localhost > ls > put test.txt > bye外部测: lftp -u ftpuser,PASSWORD ftp://<服务器 IP>加 TLS 变 FTPS(推荐) 明文 FTP 密码在网上裸奔。加 TLS: # /etc/vsftpd.conf ssl_enable=YES allow_anon_ssl=NO force_local_data_ssl=YES force_local_logins_ssl=YESssl_tlsv1=YES ssl_sslv2=NO ssl_sslv3=NO require_ssl_reuse=NO ssl_ciphers=HIGHrsa_cert_file=/etc/ssl/private/vsftpd.pem rsa_private_key_file=/etc/ssl/private/vsftpd.pem生成自签证书: sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ -keyout /etc/ssl/private/vsftpd.pem \ -out /etc/ssl/private/vsftpd.pem \ -subj "/C=CN/O=Local/CN=ftp.example.com" sudo chmod 600 /etc/ssl/private/vsftpd.pem客户端连 FTPS(FileZilla 里选"要求显式的 FTP over TLS")。 真正推荐:SFTP 其实根本不用装 vsftpd。Linux 只要装了 OpenSSH(99% 服务器都有),就自带 SFTP。 创建 chroot 用户: sudo useradd --shell /usr/sbin/nologin sftpuser sudo passwd sftpuser sudo mkdir -p /home/sftpuser/upload sudo chown root:root /home/sftpuser sudo chown sftpuser:sftpuser /home/sftpuser/upload/etc/ssh/sshd_config 末尾加: Match User sftpuser ChrootDirectory /home/sftpuser ForceCommand internal-sftp AllowTcpForwarding no重启 sshd: sudo systemctl restart sshd客户端用 FileZilla / WinSCP 选 SFTP 就能连——端口 22、加密传输、和 SSH 走一起、防火墙就一个端口。 没有特殊需求,SFTP 优先,FTP 一律不推荐。 一句话总结 vsftpd 搭 FTP 的关键:本地用户 + 被动模式端口范围 + 防火墙放行。生产环境别用明文 FTP,改 SFTP 或者 FTPS。

Centos7升级Gcc

前言 之前讲过一次关于Centos7的GCC版本的升级,这里,主要使用源码对GCC进行升级,即在安装完成后不用再切换GCC环境。 1. 切换到root属性 su yum -y install wget2. 下载GCC源码 以下命令会放在 usr/local/ 下面 wget http://ftp.gnu.org/gnu/gcc/gcc-4.9.2/gcc-4.9.2.tar.gz3. 解压压缩包 cd /usr/local/ tar -zxvf gcc-4.9.2.tar.gz4. 下载编译安装的依赖包 4.1 假设有网的时候 cd gcc-4.9.2 ./contrib/download_prerequisites4.2 假设没网的情况下 如果Linux没有网络连接,则用Windows上网下载这几个包:ftp://ftp.gnu.org/gnu/gmp/gmp-4.3.2.tar.bz2 http://www.mpfr.org/mpfr-2.4.2/mpfr-2.4.2.tar.bz2 http://www.multiprecision.org/mpc/download/mpc-0.8.1.tar.gz然后解压并移动到gcc-4.9.2下面: tar -xjf gmp-4.3.2.tar.bz2 tar -xjf mpfr-2.4.2.tar.bz2 tar -xzf mpc-0.8.1.tar.gz mv gmp-4.3.2 gcc-4.9.2/gmp mv mpfr-2.4.2 gcc-4.9.2/mpfr mv mpc-0.8.1 gcc-4.9.2/mpc5. 编译安装GCC yum install -y gcc-c++ glibc-static gcc ./configure --prefix=/usr/local/gcc --enable-bootstrap --enable-checking=release --enable-languages=c,c++ --disable-multilib make make install编译参数说明--prefix=/usr/local/ 指定安装路径 --enable-bootstrap 用第一次编译生成的程序进行第二次编译 --enable-checking=release 以软件发布版的标准来对编译时生成的代码进行一致性检查 --enable-languages=c,c++ 支持的高级语言类型和运行时库 --disable-multilib 如果你的操作系统是64位,要禁止生成32位代码配置环境变量 1. 查看当前gcc版本 gcc -v # gcc (GCC) 4.8.5 20120313 (Red Hat 4.8.5-16)2. 编写环境变量的路径 vim /etc/profile.d/gcc.sh # 输入:export PATH=/usr/local/gcc/bin:$PATH3. 激活环境变量 source /etc/profile.d/gcc.sh gcc -v # gcc (GCC) 4.9.2导出头文件 ln -sv /usr/local/gcc/include/ /usr/include/gcc导出库文件 vim /etc/ld.so.conf.d/gcc.conf # 添加:/usr/local/gcc/lib64 ldconfig -v ldconfig -p |grep gcc注意事项 如果您的程序之前跑的是一个gcc >= 4.9.2的环境,可以通过以下命令切换: source /opt/rh/devtoolset-7/enable # 或 source scl_source enable devtoolset-7