Linux 服务器运维教程

Linux Operations · Shell 与文件系统、文本处理、用户权限、进程管理、网络配置、防火墙、服务部署、安全加固与性能监控,41 章互链最密集
← 返回教程馆
5阶段
41章节
41源文档
12 周计划配套生态

📊 我的学习进度 本地自动保存

加载中…
🧭 导览0/0
docs 1-80/0
docs 9-160/0
docs 17-240/0
docs 25-320/0
docs 33-400/0
💡 每章卡片右上角「✓ 标记已学」;教程间互链已自动映射到本页章节;代码示例请配合 Gitee 仓库 使用。

📖 Linux 服务器部署教程(面向初学者) README.md

一套照着敲就能跑通、并且知道为什么这么敲的 Linux 服务器部署教程。

默认环境:Ubuntu 22.04 LTS(apt 系)。遇到 RHEL / Rocky / AlmaLinux 差异大的地方,用可折叠对照块给出等价命令。

[!NOTE] 本教程所有版本号截至 2026 年 9 月。软件版本会持续变化,教程里每一章都教了你如何自己核对当前版本(见 版本速查),这是教程过期后仍然可用的关键。


这套教程适合谁

  • 刚拿到一台云服务器(阿里云 / 腾讯云 / 华为云 / AWS 等),不知道从何下手的人
  • 会在本机写代码,但没在 Linux 服务器上部署过完整项目的开发者
  • 会一点 Linux 命令,但每次都要搜、不知道命令背后在做什么的人
  • 需要一份可检索、可长期维护的运维速查手册的团队成员

不适合谁

  • 想要一份「三分钟速成命令表」的人 —— 本教程是详细讲解型,会解释原理
  • 需要 Kubernetes / CI/CD / Prometheus 监控栈深度内容的人 —— 本教程明确不覆盖

内容结构

部分 章节 内容
PART 0 00 前言与阅读约定 安全基线、环境变量原理、危险命令红名单
PART 1 10–18 Shell 与文件系统、文件操作、文本处理、用户权限、进程、网络、防火墙、包管理、SSH 加固
PART 2 20–33 JDK、Python、Node.js、MySQL、PostgreSQL、MongoDB、Redis、RabbitMQ、Kafka、Nginx、Docker、Git 与构建工具、HTTPS 证书、国内镜像加速
PART 3 40–44 systemd、源码编译安装、进程守护选型、运维工具箱、备份与定时任务
PART 4 50–55 日志基础、journalctl、查询技巧、统计聚合、logrotate、磁盘占满排查
PART 5 60 端到端实战:从零部署一个完整项目
附录 90–92 命令速查、版本速查、排错手册

完整目录与三条推荐阅读路线见 → docs/README.md


PART 1 Linux 基础操作

文件 主题
10-shell-and-filesystem.md FHS 目录地图、绝对路径/相对路径、软链接与硬链接
11-file-operations.md 增删改查、打包压缩、scp / rsync 传输
12-text-processing.md 重定向、管道、grep / find / sed / awk / xargs / sort
13-users-and-permissions.md 用户与组、rwx 权限、为什么是 755 不是 777、umask、特殊位
14-process-management.md ps / top / kill、信号、前后台、负载怎么看
15-network-configuration.md ip / ss / netplan、DNS、时间同步
16-firewall.md 防火墙三层模型、ufw / firewalld / iptables
17-package-management.md apt / dpkg、如何查可用版本、signed-by、dnf 对照
18-ssh-hardening.md 密钥登录、禁 root 直登、改端口、fail2ban

PART 2 环境部署

文件 主题
20-jdk.md JDK 17/21/25、Temurin、多版本切换、JAVA_HOME
21-python.md 不动系统 python3、deadsnakes / pyenv / 源码、venv、pip
22-nodejs.md nvm / NodeSource、pnpm、PM2 守护
23-mysql.md 8.0 vs 8.4 LTS、安全初始化、认证插件、utf8mb4
24-postgresql.md PGDG 源、双配置文件、角色与库
25-mongodb.md 8.0 官方源、bindIp、先建管理员再开认证
26-redis.md 8.x 安装、License 提示与 Valkey 替代、内存策略
27-rabbitmq.md Erlang 版本对应表与安装顺序、用户 / vhost / 插件
28-kafka.md KRaft 模式(4.x 无 ZooKeeper)、格式化存储、topic 管理
29-nginx.md nginx.org 源、反向代理、nginx -t、reload vs restart
30-docker.md Engine 29.x、Compose v5 插件、daemon.json、镜像加速
31-git-and-build-tools.md git、Maven / Gradle、build-essential、make
32-https-certbot.md Let's Encrypt 证书申请与自动续期
33-mirrors-acceleration.md apt / pip / npm / Maven / Docker 国内镜像源

PART 3 服务管理与运维

文件 主题
40-systemd.md unit 结构、Restart= 为什么这么配、开机自启的本质
41-source-compile-install.md configure → make → install、ldconfig、--prefix 隔离
42-process-supervision.md systemd / supervisor / PM2 三种守护方式对比选型
43-ops-toolbox.md htop / btop / iotop / ncdu / lsof / strace / tcpdump / fail2ban / tmux
44-backup-and-cron.md crontab 五段与常见坑、systemd timer、3-2-1 备份

PART 4 日志查询与分析

文件 主题
50-log-fundamentals.md /var/log 地图、rsyslog 与 journald 的分工
51-journalctl.md 时间窗口、按 unit、按优先级、持久化与清理
52-log-query-recipes.md tail -f/-F、less +F、上下文过滤、查历史压缩包
53-log-analysis.md awk 聚合、错误 TOP N、按 IP/状态码、时间窗截取
54-logrotate.md 配置逐条讲解、copytruncate 的坑、干跑调试
55-disk-full-troubleshooting.md df -h vs df -i、定位大文件、已删文件仍占空间

PART 5 实战

文件 主题
60-hands-on-project.md 端到端:Java 后端 + MySQL + Redis + Nginx 反代 + HTTPS + systemd

附录

文件 主题
90-cheatsheet-commands.md 跨章高频命令索引
91-cheatsheet-versions.md 版本速查表(全篇唯一权威来源)
92-troubleshooting-playbook.md 排错方法论 + 症状 → 章节索引

三条推荐阅读路线

路线一:新手直通(按顺序读) 00 → 10 → 11 → 12 → 13 → 17 → 40 → 按需挑 PART 2 的组件章 → 60(精简版,完整路线见 docs/README.md)

路线二:有基础,直接装环境 00(只看约定部分)→ 挑 PART 2 对应组件章 → 40(systemd 托管)→ 29(Nginx 反代)

路线三:服务器出故障了 92(症状 → 章节索引)→ 55(磁盘满了)/ 53(日志分析)/ 16(连不上,防火墙)


使用约定

  • 命令块里的 $ 是提示符,不要复制它;$ sudo xxx 表示这条命令需要管理员权限
  • 占位符如 <服务器IP>、<你的域名>、<强密码> 需要替换成你自己的值
  • 提示块含义:> [!NOTE] 补充说明 / [!TIP] 技巧 / [!IMPORTANT] 必须做 / [!WARNING] 会出事

三条铁律

[!WARNING]

  1. 不要用 root 直接操作系统。用有 sudo 权限的普通用户,改配置前先备份。
  2. 不要动系统自带的 python3,它支撑着 apt 等系统工具,弄坏很难修。
  3. 任何 rm -rf、chmod -R 777、kill -9、启用防火墙前,先确认目标写对了 —— 这些操作不可撤销。

📄 前言与阅读约定 00-preface.md


title: 00 前言与阅读约定 updated: 2026-09 applies-to: Ubuntu 22.04 LTS

这是一套面向零基础读者的《Linux 服务器部署教程》。在翻开任何一章之前,请先读完本章——它只有十几分钟的阅读量,却能帮你建立正确的操作姿势,避开后面 80% 的坑。本章不讲部署,讲的是怎么读这本书、怎么安全地操作服务器、以及 shell 背后最基本的运行规则。

[!NOTE] 本教程基于 Ubuntu 22.04 LTS 编写,版本号截至 2026-09。用 RHEL 系的读者请留意各章的「RHEL 对照」折叠块。

这本教程怎么读

三条阅读路线

不同的人带着不同的目的来,我们准备了三条路线:

路线 适合谁 怎么走
新手直通 从零开始、想系统学习 按章节编号从 00 顺序读到 40,每章动手做完再往下
按组件查阅 有基础,只想装某个软件 直接跳到对应章节(如 MySQL、防火墙),章内自成闭环
救火 线上出事了,急需答案 先看 排错手册 定位章节,用「常见问题」表对症下药

完整章节目录见 README。

动手优先

每章的「验证方法」小节都给了命令 + 预期输出。请务必亲手敲一遍、亲眼核对输出——服务器运维是肌肉记忆,光看不练等于没学。准备一台练习环境即可开始:本地 VirtualBox/VMware 虚拟机,或任意云厂商按量付费的低配服务器(用完记得销毁,防止持续扣费)。

全书约定

提示符

全书代码块统一使用 $ 作为命令提示符:

# 以普通用户执行(绝大多数场景)
$ ls -la
# 需要 root 权限时,加 sudo 前缀
$ sudo apt update

三条规则:

  1. $ 是提示符,不是命令的一部分,不要把它复制进去。
  2. $ sudo 开头表示该命令需要管理员权限,执行后可能要求输入当前用户的密码(输入时屏幕不显示任何字符,这是正常设计,输完回车即可)。
  3. 全书不使用 root 直接登录,需要最高权限的地方一律 sudo。这是安全基线,原因见下文「安全基线」一节。

占位符

代码中出现的尖括号是占位符,需要替换成你自己的真实值,不要原样照抄:

占位符 替换成
<服务器IP> 你服务器的公网/内网 IP,如 203.0.113.10
<你的域名> 你在域名商处购买的域名,如 example.com
<强密码> 你自己生成的强密码(建议用密码管理器生成)
<新主机名> 你想要的主机名,如 web-01
<用户名> 你的登录用户名

提示块

全书只用四种提示块,含义如下:

标记 含义 你该怎么做
> [!NOTE] 补充说明、背景知识 可以跳过,回头再看也行
> [!TIP] 更好的做法、省事的技巧 建议采纳
> [!IMPORTANT] 关键前提,不做会失败或留隐患 必须照做
> [!WARNING] 有风险,可能丢数据或失联 停下来读清楚再动手

代码块语言标注

全书代码块开头都标注语言:bash(命令)、console(终端交互含输出)、text(纯文本输出)、ini / yaml(配置文件)、powershell(Windows PowerShell 命令)。标注语言除了高亮,更重要的目的是让你一眼分辨「这是要输入的命令」还是「参考输出」。

RHEL 对照折叠块

Ubuntu 之外,很多公司用 RHEL / Rocky / AlmaLinux。涉及发行版差异的地方,我们会放一个可点击展开的折叠块,不影响 Ubuntu 读者的阅读连贯性。长这样(点击即可展开):

RHEL / Rocky / AlmaLinux 9 对照
Ubuntu (apt/ufw) RHEL (dnf/firewalld) 说明
sudo apt install nginx sudo dnf install nginx 包管理器不同
sudo ufw allow 80/tcp sudo firewall-cmd --permanent --add-service=http 防火墙工具不同

版本声明

全书版本号截至 2026-09。 软件版本会持续更新,本书给出的具体小版本号(如 8.0.4x)可能已经过期。唯一权威的核对方式是查阅 版本速查表,以及动手前自己执行 apt policy <包名> 确认当前源里的候选版本。冲突时一律以官方文档为准,本书只负责告诉你「怎么自己核对」。

安全基线(新服务器第一小时要做的事)

[!IMPORTANT] 任何一台新拿到的服务器(云主机、虚拟机都算),在部署任何业务之前,先完成下面 6 件事。顺序执行,全程不超过一小时。跳过任何一步,都是在给未来的自己埋雷。

  1. 用 SSH 密钥登录,禁用密码登录。 密码可以被暴力破解,密钥几乎不能。生成密钥并配置的完整步骤见 18 SSH 加固。

  2. 建一个普通用户并加入 sudo 组,禁止 root 直接登录。 root 权限太大,一条手滑命令就能毁掉系统;日常用普通用户 + sudo,既留了审计痕迹,也让误操作多一道确认门槛。

  3. 开启防火墙,但第一步必须先放行 SSH(22 端口)。 顺序反了会把自己锁在门外。Ubuntu 用 ufw,详见 16 防火墙:

    # 先放行 SSH,再启用防火墙,顺序不能反!
    $ sudo ufw allow 22/tcp
    $ sudo ufw enable
    
  4. 更新系统。 新镜像自带的软件包往往落后于安全补丁:

    # 刷新软件源索引,再升级已安装的包
    $ sudo apt update && sudo apt upgrade
    
  5. 设置时区并开启时间同步。 时间不准会弄坏 HTTPS 证书校验、日志对齐和定时任务,原理与操作见 15 网络配置 的时间同步一节:

    # 设置时区为北京时间
    $ sudo timedatectl set-timezone Asia/Shanghai
    
  6. 改任何配置文件前,先备份。 养成条件反射:

    # -a 归档模式复制:保留权限、属主、时间戳,还原时不会因权限问题出错
    # $(date +%F) 是命令替换,展开成当天日期如 2026-09-29,备份文件带日期便于区分版本
    $ sudo cp -a /etc/ssh/sshd_config /etc/ssh/sshd_config.bak.$(date +%F)
    

    备份加日期后缀的好处:同一个文件改多次也不会覆盖旧备份,出问题时能精确回滚到「出事前一天」的版本。

环境变量与 PATH(先搞懂这个,后面少踩 80% 的坑)

什么是环境变量

环境变量是操作系统传给每个程序的「全局设置」,格式是 名字=值。你已经在不知不觉中用过它:HOME 是你的家目录,LANG 是语言设置,而其中最重要的是 PATH。

# 查看当前用户家目录
$ echo $HOME
# 查看 PATH:一串用冒号分隔的目录
$ echo $PATH
预期输出(具体内容因环境而异):
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin

命令是怎么被找到的

当你敲下 ls 回车,shell 不会搜索整个硬盘,而是按 PATH 里目录的先后顺序,逐个目录找名为 ls 的可执行文件,找到第一个就执行。这个机制解释了大量新手疑问:

  • 「为什么我 cd 到目录里,敲 ./xxx.sh 前面要加 ./?」——因为当前目录 . 通常不在 PATH 里,必须显式指明。
  • 「为什么 ./xxx 能跑而 xxx 报 command not found?」——同上,shell 在 PATH 里找不到它。
# which:显示命令的可执行文件在哪个目录(按 PATH 顺序找到的第一个)
$ which python3
# type:比 which 信息更全,能识别别名和内建命令
$ type ll
# whereis:把相关的二进制、源码、手册页位置全列出来
$ whereis nginx

临时设置与永久设置

# 临时添加环境变量:export 后当前终端立即生效,但关闭终端就消失
$ export APP_ENV=production
# 验证
$ echo $APP_ENV

想永久生效,必须写进配置文件。写哪里取决于你希望它对谁生效:

写在哪 对谁生效 适用场景
~/.bashrc 只对你自己的交互终端 个人别名、个人常用变量
/etc/profile.d/*.sh 所有用户(推荐做法) 服务器级配置,如 JAVA_HOME
systemd 服务的 Environment= 只对该服务进程 给 nginx/应用服务注入变量,见 40 systemd

给所有用户设置变量的推荐做法:

# 新建一个独立配置文件,命名建议带上用途前缀
$ sudo nano /etc/profile.d/myapp.sh
# /etc/profile.d/myapp.sh 的内容
export APP_ENV=production
export JAVA_HOME=/opt/jdk-21

为什么「装好了却 command not found」

最常见的根因:程序装了,但它所在目录不在 PATH 里。比如你把程序解压到 /opt/myapp/bin/,shell 自然找不到。排查三步:

# 第 1 步:确认程序文件真实存在
$ ls /opt/myapp/bin/
# 第 2 步:确认该目录在不在 PATH 里
$ echo $PATH | tr ':' '\n' | grep myapp
# 第 3 步:不在就把目录追加进 PATH(写进 ~/.bashrc 持久化)
$ echo 'export PATH=/opt/myapp/bin:$PATH' >> ~/.bashrc
$ source ~/.bashrc

登录 shell 与非登录 shell:为什么 cron 里读不到我的变量

shell 分三类,加载的配置文件不同,这是本书埋得最深的一个坑:

类型 什么时候出现 按序加载
登录 shell SSH 登录、控制台登录 /etc/profile → ~/.profile(其内部会 source ~/.bashrc;Ubuntu 22.04 默认没有 ~/.bash_profile)
非登录交互 shell 直接开一个终端窗口 只加载 ~/.bashrc
非交互 shell 执行脚本、cron 任务 不加载任何 bash 启动文件(除非设置了 BASH_ENV)

[!WARNING] cron 任务的环境变量和你 SSH 登录后看到的完全不同。 你在 ~/.bashrc 里 export 的变量,cron 的非交互 shell 不会加载——这就是「命令行里跑得好好的脚本,放进 cron 就失败」的头号原因。解决方法见定时任务章节:要么在 crontab 里写绝对路径,要么在任务行里显式 source 配置文件。

source 与点命令

source(简写就是一个点 .)的作用:在当前 shell 里执行指定脚本的命令,而不是新开一个子进程。

# 修改 ~/.bashrc 后让它立即生效,无需重新登录
$ source ~/.bashrc
# 点命令与 source 完全等价
$ . ~/.bashrc

为什么要区分?普通方式执行脚本(bash xx.sh 或 ./xx.sh)会开一个子进程,脚本里 export 的变量随子进程结束而消失;source 则把变量留在当前 shell 里。所以「让 .bashrc 的修改立即生效」只能用 source。

危险命令红名单

[!WARNING] 下面这些命令本身都是正常工具,但用错场景就是灾难:轻则删光数据,重则整台服务器报废。在生产服务器上敲任何本节命令前,请默念一遍「安全做法」。

危险命令 一句后果 安全做法
rm -rf <目录> 递归强制删除,无确认、不进回收站,删了就没了 先 ls 确认路径;变量路径先 echo "$DIR" 确认非空
rm -rf $DIR/(变量为空时) 变量为空时命令变成 rm -rf /,删除整个系统 用 "${DIR:?变量为空}" 语法强制检查:rm -rf "${DIR:?}/"
chmod -R 777 <目录> 所有文件对所有人可写可执行,是安全漏洞,还可能破坏 ssh 等对权限敏感的服务 按需最小授权,目录 755、文件 644,私钥 600
mkfs <设备> 格式化磁盘,数据全部清空且无法恢复 执行前三次确认设备名;用 lsblk 核对,防止格错盘
dd if=... of=<设备> 直接按字节写设备,写错目标 = 烧毁数据 先写清 of 目标再回车;可用 oflag=sync status=progress 观察进度及时 Ctrl+C
> 重要文件(如 > /etc/fstab) 重定向立即清空文件内容,没有确认提示 追加用 >>;覆盖前先备份(见安全基线第 6 条)
ufw enable(未放行 SSH 前) 防火墙一开,SSH 立即被拦,把自己锁在服务器外 永远先 sudo ufw allow 22/tcp 再 enable,见 16 防火墙
kill -9 <数据库进程> 数据库来不及刷盘和释放锁,可能数据损坏、下次起不来 先 systemctl stop mysql 走正常停机流程,实在不行用普通 kill(SIGTERM)留善后时间
crontab -r 一次性删除当前用户全部定时任务,且没有确认 用 crontab -e 删单条;删前先 crontab -l > crontab.bak 备份

一条通用原则:破坏性操作的命令,先在测试环境跑一遍,生产环境执行前先把命令完整写出来检查两遍,不要凭肌肉记忆直接敲。

Windows 读者的特别提醒

换行符问题(最常见的翻车点)

Windows 的文本文件用 CRLF(\r\n)换行,Linux 用 LF(\n)。在 Windows 上编辑过的 shell 脚本传到 Linux 后,每行结尾多一个看不见的 \r,运行时就会报:

/bin/bash^M: bad interpreter: No such file or directory

^M 就是那个多余的 \r。三种解决办法:

# 方法 1:在 Linux 服务器上用 dos2unix 转换(没有就先安装)
$ sudo apt install dos2unix
$ dos2unix script.sh

更推荐在源头解决:编辑器设置成 LF。VSCode 点右下角状态栏的 CRLF 切换为 LF,或 settings.json 里加 "files.eol": "\n";Notepad++ 在「编辑 → 文档格式转换」里切换。这样保存的文件天然兼容 Linux。

推荐终端

  • Windows Terminal(微软官方,微软商店免费装):多标签、字体渲染好,是 Windows 上操作 Linux 服务器的首选界面。
  • 连接服务器不需要装 Xshell/PuTTY:Windows 10/11 自带 OpenSSH 客户端,直接在 PowerShell 或 Windows Terminal 里用:
# 在 PowerShell 里直接 ssh,与 Linux 上的用法一致
ssh <用户名>@<服务器IP>
  • Windows 上的 WSL2(适用于 Linux 的 Windows 子系统)也值得一试:一个真正的 Linux 环境跑在 Windows 里,本地练手非常方便。

推荐阅读顺序

如果你是零基础新手,建议严格按这个顺序走(编号即难度递进):

  1. 本章 00 前言与阅读约定
  2. 15 网络配置 —— 排错思路是一切的基础
  3. 16 防火墙 —— 懂了端口,防火墙才不会把自己锁外面
  4. 18 SSH 加固 —— 新服务器第一小时的安全基线
  5. 40 systemd 服务管理 —— 理解服务怎么被管理
  6. 23 MySQL 安装与配置 —— 第一个完整的生产组件实战
  7. 随时查阅 91 版本速查表 与各章命令速查

完整目录见 README。祝部署顺利。

📄 10 Shell 与文件系统 10-shell-and-filesystem.md


title: 10 Shell 与文件系统 updated: 2026-09 applies-to: Ubuntu 22.04 LTS

本章解决三个问题:Linux 的文件都放在哪、怎么在目录间准确移动、为什么有些文件「看起来在但其实不在」(链接)。命令均在 Ubuntu 22.04 LTS 上验证,涉及版本号的信息一律截至 2026-09。

[!NOTE] 本章版本号截至 2026 年 9 月。版本信息的唯一权威来源是 版本速查表,冲突时以速查表为准,核对方法见 91 版本速查。

[!NOTE] 本章绝大多数命令不需要管理员权限,只有安装 tree 那一步需要 sudo。


Linux 目录结构(FHS)

是什么

Linux 没有 C 盘、D 盘,所有存储设备都挂到一棵以 / 为根的树上。FHS(Filesystem Hierarchy Standard)规定了哪个目录放什么:约定统一后,你在甲机器学会的路径到乙机器同样适用,包管理器也依赖它——apt 必须把程序放 /usr/bin,否则 Shell 找不到。

目录 放什么 / 提醒
/ 根目录。根下不要放业务数据,根分区满了系统就起不来
/home 普通用户家目录 /home/<用户名>,个人配置 .bashrc 在这里
/etc 系统和程序的配置文件,改配置主要就是改这里
/var 经常变化的数据:日志 /var/log、网站根 /var/www、缓存 /var/cache
/usr 系统级程序与只读资源:/usr/bin 命令、/usr/lib 库。名字来自 Unix System Resources,不是 user
/opt 第三方独立软件,如手动安装的 /opt/jdk-21
/tmp 临时文件,会被定期清理,不要放重要数据
/root root 的家目录,不在 /home 下,普通用户无权进入
/boot 内核与 grub,删错就开不了机
/dev 设备文件:磁盘 /dev/sda、黑洞 /dev/null
/proc 内核与进程的虚拟视图,不占磁盘,如 /proc/cpuinfo
/bin /sbin /lib 基本命令与库,Ubuntu 22.04 上是到 /usr 同名目录的软链
/run 运行时数据(pid、socket),重启即清空
/mnt /media 临时挂载点

怎么做

先亲眼看一遍,比背表格记得牢:

$ ls -1 /
$ ls -ld /bin /sbin /lib

第二行输出开头的 l 和 -> 说明它们是指向 /usr 的软链。养成习惯:任何写操作前先确认「我在哪、目标长什么样」,这是避免事故最便宜的习惯。

参数说明

参数 作用 何时用
-1 每行一个名字 输出要交给脚本或管道
-l 长格式:权限、属主、大小、时间 看细节
-d 只显示目录本身不展开 看目录自己的属性
-F 结尾加类型标记,目录加 / 快速分辨目录和文件
-a 显示点开头的隐藏文件 找 .env 这类文件

验证方法

$ ls -ld /etc

预期输出:

drwxr-xr-x 183 root root 12288 Sep 20 10:02 /etc

d 是目录;两个 root 分别是属主和属组——所以改 /etc 下的配置需要 sudo;183 是硬链接数(子目录数 + 2);12288 是目录元数据大小,不是内容大小。

常见问题

现象 根因 解决
源码放 /root 下,Nginx 报 403 /root 对其他用户完全不可读 放 /var/www 或家目录
/tmp 里的数据第二天没了 systemd-tmpfiles 定期清理 重要数据放 /var
装了程序却找不到在哪 包管理器按 FHS 放 /usr/bin command -v <命令> 定位
根分区 100% 却没存大文件 日志在 /var/log 持续增长 用 du 定位,见 磁盘排查

路径:绝对路径、相对路径与特殊符号

是什么

绝对路径从根写起(/var/log/nginx/access.log),无论当前在哪指向都一样;相对路径以「当前目录」为起点,起点变了指向就变了。特殊符号:. 当前目录、.. 上级、~ 家目录、- 上一次所在目录。

为什么重要:定时任务和 systemd 服务启动时,当前目录往往不是你以为的那个。所以写脚本和服务配置一律用绝对路径。

怎么做

$ pwd
$ cd /var/log
$ cd ../..
$ cd ~
$ cd -
$ ./deploy.sh

最后一行为什么必须带 ./:Shell 只在 PATH 列出的目录里找命令,故意不含当前目录(防止误执行被投放的同名程序)。给出路径才是安全的做法。

参数说明

命令 / 参数 作用 说明
cd 不带参数 回到家目录 等价 cd ~
cd - 切回上一个目录 依赖 OLDPWD,并打印目标目录
pwd / pwd -P 逻辑路径 / 解析软链后的物理路径 软链目录下两者不同
$PWD $OLDPWD 当前 / 上一个目录 脚本里用 $(pwd) 取位置

验证方法

$ cd /var/log/nginx && pwd && cd - && pwd

预期输出:

/var/log/nginx
/home/ubuntu
/home/ubuntu

第二行是 cd - 打印的「回到哪里」,第三行确认已回来。

常见问题

现象 根因 解决
No such file or directory 相对路径起点不对或拼错 先 pwd,或改用绝对路径
目录名带空格被拆成两个参数 Shell 按空格分词 加引号 cd "/data/my app"
cd "~" 说目录不存在 ~ 在引号里不展开 去掉引号或写 /home/ubuntu
脚本里 cat app.conf 找不到 脚本当前目录是执行位置,不是脚本所在位置 用绝对路径,或 cd "$(dirname "$0")"

查看目录内容:ls 输出解读与 tree

是什么

ls 列目录,tree 把目录画成树。既然天天用,就值得把 ls -l 的输出逐字段看懂——排查权限、判断文件是否被改动都靠这些字段。

怎么做

$ ls -lah

典型输出:

total 48K
drwxr-xr-x  2 ubuntu ubuntu 4096 Sep 20 10:02 .
-rw-r--r--  1 ubuntu ubuntu  1.2K Sep 20 09:58 app.conf
lrwxrwxrwx  1 ubuntu ubuntu    11 Sep 20 10:01 current -> /opt/app

逐字段:① 权限位 drwxr-xr-x,首字符是类型(d 目录、- 文件、l 软链),后 9 位分三组给属主、属组、其他人;② 硬链接数(文件通常 1,目录是子目录数 + 2);③ 属主;④ 属组;⑤ 大小(-h 变人类可读,目录的 4096 只是元数据,不代表内容大小);⑥ mtime;⑦ 名称,软链显示 -> 目标。

$ ls -lt
$ ls -lSh
$ sudo apt install -y tree
$ tree -L 2 -d /etc/nginx

ls -lt 按时间排(找最近被改的文件),ls -lSh 按大小排。tree 需安装,-L 限制层数、-d 只看目录——不限制层数在大目录上会刷屏。

RHEL / Rocky / AlmaLinux 9 对照
Ubuntu (apt) RHEL (dnf) 说明
sudo apt install -y tree sudo dnf install -y tree 包名相同
/etc/nginx/sites-available /etc/nginx/conf.d RHEL 系没有 sites-available/-enabled 软链机制

参数说明

参数 作用 何时用
-h 大小用 K/M/G 几乎总是加
-t / -S 按时间 / 按大小排序 找最近改动、找大文件
-r 逆序 常与 -t、-S 组合
-i 显示 inode 号 判断硬链接关系
tree -L n 限制层数 建议总是加
tree -d / -a / -h 只看目录 / 含隐藏 / 显示大小 看结构、找 .env
tree -I 'pattern' 排除匹配项 如跳过 node_modules

验证方法

$ ls -lht /var/log | head -n 2

预期输出:

-rw-r----- 1 syslog adm  12M Sep 20 10:05 syslog
-rw-r----- 1 syslog adm 4.2M Sep 20 09:40 auth.log

常见问题

现象 根因 解决
找不到 .env 隐藏文件默认不显示 加 -a
cannot open 'x': Permission denied 父目录缺执行权限 x 加 sudo,见 用户与权限
目录大小都显示 4.0K ls 显示的是元数据大小 用 du -sh <目录>
tree: command not found 未安装 sudo apt install -y tree
tree 一执行就刷屏 没限制层数 加 -L 2,刷屏中按 Ctrl+C

目录容量与磁盘:du 与 df

是什么

df(disk free)看分区还剩多少,du(disk usage)看这个目录占了多少(累加文件大小)。排障顺序固定:先 df 定位是哪个分区满了,再用 du 在那个分区里逐级缩小范围。顺序反了会浪费大量时间。

怎么做

$ df -h
$ du -sh /var/log
$ du -h --max-depth=1 /var | sort -hr

第三条最常用:把 /var 下每个子目录的大小列出来并从大到小排序,谁占用最大一目了然。

参数说明

参数 作用 说明
-h 人类可读单位 两个命令通用
du -s 只给汇总值 只要「这个目录多大」
du --max-depth=n 展开 n 层 配 sort -hr
du -c / --exclude=P 给合计 / 排除目录 跳过 node_modules
df -T 显示文件系统类型 区分 ext4 / xfs
df -i 显示 inode 使用率 有空间却报 No space 时必查

验证方法

$ df -h /

预期输出:

Filesystem      Size  Used Avail Use% Mounted on
/dev/sda1        40G   12G   26G  32% /

Use% 超 80% 就该排查,90% 以上要马上处理——很多程序在磁盘写满时直接崩溃,而不是报错。

常见问题

现象 根因 解决
df 100% 但 du 加起来差很多 文件被删后仍被进程占用 lsof | grep deleted 找到进程并重启
No space left 但还有空间 inode 耗尽(小文件太多) df -i 确认
du: ... Permission denied 部分目录无权进入 加 sudo,扫 /proc 的报错可忽略
du 跑得极慢 文件数量巨大 限定目录范围

软链接与硬链接:从 inode 说起

是什么

理解链接先要理解 inode:文件系统把每个文件登记成一条 inode 记录(存权限、属主、数据块位置,不存文件名);文件名只存在于目录里——目录本质是一张「名字 → inode 号」的表。名字和文件是分开的两件事,于是有了两种链接:

硬链接 软链接(符号链接)
本质 多个名字指向同一个 inode 特殊文件,内容是目标路径字符串
跨文件系统 不可以 可以
指向目录 不可以(会造成目录环) 可以
源文件删除后 数据仍在,另一个名字照常访问 失效,访问报 No such file

为什么需要软链:让 /opt/app/current 指向 /opt/app-1.2.3,程序永远读 current,升级换目录、回滚改指向,不用改任何配置。Nginx 的 sites-enabled、systemd 的开机自启都是这个机制。

怎么做

$ ln -s /opt/app-1.2.3 /opt/app/current
$ ln -sfn /opt/app-1.2.4 /opt/app/current
$ ln app.conf app.conf.hardlink
$ ls -li app.conf app.conf.hardlink
$ readlink -f /opt/app/current

第二行是更新软链的正确写法:-n 防止在旧链接指向的目录里再套一个链接(否则会出现 current/current)。

[!WARNING] 删除软链接时不要在结尾加斜杠。rm -rf /opt/app/current/ 会顺着链接删掉目标目录里的内容,而 rm /opt/app/current 只删链接本身。这是最经典的删库姿势之一。

参数说明

参数 作用 说明
-s 创建软链接 不加就是硬链接
-f 目标已存在时强制覆盖 更新软链常用
-n 把已存在的软链当文件而非目录 与 -f 一起用,防套娃
-i / -v 覆盖前询问 / 显示过程 不确定时加 -i
ls -i 显示 inode 号 验证硬链接
readlink -f 递归解析真实绝对路径 脚本里拿真实路径

验证方法

$ ln app.conf app.conf.hardlink
$ ls -li app.conf app.conf.hardlink

预期输出:

131084 -rw-r--r-- 2 ubuntu ubuntu 1024 Sep 20 10:00 app.conf
131084 -rw-r--r-- 2 ubuntu ubuntu 1024 Sep 20 10:00 app.conf.hardlink

inode 相同、链接数变 2,说明是同一份数据的两个名字。此时删掉 app.conf,另一个名字仍能读到内容——数据要等链接数归 0 才释放。

常见问题

现象 根因 解决
软链红色闪烁 / 报 No such file 目标被删或移动 ln -sfn <真实目标> <链接名> 重建
软链移动后失效 相对路径是相对链接所在目录解析的 建软链时源路径写绝对路径
Invalid cross-device link 跨分区建硬链接 改用 ln -s 或 cp -a
hard link not allowed for directory 对目录建硬链接 系统禁止,用软链
出现 current/current 套娃 已存在指向目录的链接又被 -sf 覆盖 用 ln -sfn 重建

文件类型与 file 命令

是什么

ls -l 第一个字符就是类型:- 普通文件、d 目录、l 软链、b 块设备(磁盘 /dev/sda)、c 字符设备(/dev/null)、s 套接字(/run/mysqld/mysqld.sock,进程间通信)、p 管道。Linux 不靠扩展名判断类型——app.log 也可能是二进制。要知道文件到底是什么,用 file。

怎么做

$ file /var/log/syslog
$ file /dev/null /dev/sda
$ file -L /opt/app/current

第三行加 -L 才能跟随软链看目标的类型,否则 file 只会告诉你「这是软链」。

参数说明

参数 作用 说明
file -b 只输出类型不带文件名 脚本解析
file -i 输出 MIME 类型 判断编码,如 charset=utf-8
file -L 跟随软链 默认只报「symbolic link to ...」
file -s 读取设备文件内容判断 查看磁盘分区文件系统
stat -c %F 只输出类型描述 如 regular file

验证方法

$ file /dev/null /dev/sda

预期输出:

/dev/null: character special (1/3)
/dev/sda:  block special (8/0)

常见问题

现象 根因 解决
file: cannot open 'x' (Permission denied) 无权读取 加 sudo file
file 只说「symbolic link to ...」 默认不跟随 加 -L
文本被识别成 data 混入不可见字节或编码异常 file -i 看编码,iconv 转换
脚本有 .sh 却不能执行 看的是执行权限位而非后缀 加执行位,见用户与权限

Shell 效率:补全、历史、快捷键与通配符

是什么

服务器没有图形界面,敲命令的速度就是排查速度。三件套:Tab 补全(不拼错)、历史命令(不重敲)、行编辑快捷键(不必重打整句)。另外通配符让你一次选中一批文件。

怎么做

$ history
$ !102
$ sudo !!
$ history | grep nginx

sudo !! 是「忘了加 sudo」时的救星——!! 代表上一条命令。交互式快捷键:Tab 补全(连按两次列出候选)、Ctrl+R 反向搜索历史(继续按找更早的,回车执行)、Ctrl+A/Ctrl+E 跳行首行尾、Ctrl+U/Ctrl+K 删光标前后全部、Ctrl+W 删前一个词、Ctrl+L 清屏、Ctrl+C 中断。

通配符由 Shell 展开后再交给命令,echo 可以预览结果:

$ ls *.log
$ ls app?.log
$ mkdir -p /data/{logs,backups,tmp}
$ cp app.conf{,.bak}
$ echo *.conf

[!WARNING] 通配符和 rm 组合是最危险的操作之一。执行 rm *.log 前请先用 ls *.log 看一遍匹配到了什么——展开结果若包含你没预料到的文件,删除不可逆。

参数说明

符号 / 变量 含义 说明
* / ? 任意长度字符 / 单个字符 不匹配点开头的隐藏文件
[abc] [a-z] [!a] 括号中任一 / 范围 / 取反 glob 语法
{a,b} 依次展开成多个参数 file.{txt,bak}
引号 阻止展开 "*.log" 是字面量
HISTSIZE / HISTFILESIZE 内存 / 文件保存的历史条数 Ubuntu 默认 1000 / 2000
history -a / -d N 立即写入文件 / 删除第 N 条 防断线丢失、清掉带密码的命令
shopt -s histappend 多终端追加而非覆盖 建议写进 ~/.bashrc

[!IMPORTANT] glob 的 * 和正则表达式的 * 不是一回事:glob 里是「任意字符」,正则里是「前一个字符重复任意次」。正则用法见文本处理。

验证方法

$ echo app.conf{,.bak}

预期输出:

app.conf app.conf.bak

用 echo 预览展开结果,是确认通配符行为最安全的办法。

常见问题

现象 根因 解决
cannot access '*.log': No such file 没有匹配项,原样交给命令 确认目录内容;或 shopt -s nullglob
文件名带空格被当成多个参数 Shell 按空格分词 加引号 rm "my file.log"
* 没匹配到隐藏文件 glob 默认跳过点开头文件 显式写 .*(会包含 . 和 ..)
刚执行的命令 history 里没有 历史默认退出时才写文件 history -a 立即写入
两个 SSH 窗口历史互相覆盖 后退出者覆盖了文件 ~/.bashrc 加 shopt -s histappend

本章命令速查

命令 作用
pwd / pwd -P 显示当前目录(逻辑 / 解析软链后的物理路径)
cd ~ / cd - / cd .. 回家目录 / 回上一次的目录 / 上一级
ls -lah 长格式列出所有文件,大小人类可读
ls -lt / ls -lSh 按时间排序 / 按大小排序
ls -ld <目录> 只看目录自身属性
ls -i 显示 inode 号,判断硬链接
tree -L 2 -d 树状显示两层目录结构
df -h / df -i 分区空间 / inode 使用率
du -sh <目录> 目录总大小
du -h --max-depth=1 | sort -hr 找目录下占用最大的子目录
ln -s <源> <链接名> 创建软链接
ln -sfn <源> <链接名> 强制更新软链接(防套娃)
ln <源> <链接名> 创建硬链接
readlink -f <路径> 解析软链得到真实绝对路径
file <文件> / file -L 判断类型 / 跟随软链判断
history / !n / !! 查历史 / 执行第 n 条 / 重复上一条
Ctrl+R / Ctrl+A / Ctrl+E 反向搜索 / 跳行首 / 跳行尾

相关章节

📄 文件操作 11-file-operations.md


title: 文件操作 updated: 2026-09 applies-to: Ubuntu 22.04 LTS

本章覆盖部署中最频繁的动作:建目录、复制、移动、删除、查看内容、打包压缩、跨机器传输。命令均在 Ubuntu 22.04 LTS 上验证,涉及版本号的信息一律截至 2026-09。

[!NOTE] 贯穿全章的原则:写操作之前先确认。复制前确认目标,删除前确认清单,解压前确认压缩包内容。这个习惯能挡掉大多数运维事故。


创建目录与文件:mkdir、touch

是什么

mkdir 建目录,touch 建空文件。touch 本职是"修改时间戳",文件不存在时顺手创建一个空的——所以它也常用于占位、让日志轮转程序识别到文件、触发依赖 mtime 的构建。

为什么要加 -p:不加时父目录不存在会报错;加了之后已存在也不报错。脚本和自动化部署一律用 mkdir -p,否则第二次运行就失败。

怎么做

$ sudo mkdir -p /data/app/{logs,conf,backup}
$ touch README.md
$ touch app.log error.log

第一条同时建出嵌套目录和三个子目录(花括号展开,见通配符)。

参数说明

参数 作用 说明
mkdir -p 递归创建,已存在不报错 脚本必用
mkdir -m 755 创建时直接指定权限 省一步 chmod
touch -c 文件不存在时不创建 只想更新时间戳
touch -d "2026-09-01" 指定具体时间戳 测试日志轮转

验证方法

$ sudo mkdir -p /data/app/logs && ls -ld /data/app/logs

预期输出:

drwxr-xr-x 2 root root 4096 Sep 20 10:10 /data/app/logs

用 sudo 首次创建的目录属主是 root。若要交给普通用户使用,记得 sudo chown <用户名>:<属组> /data/app -R 调整属主。

常见问题

现象 根因 解决
No such file or directory / File exists 父目录不存在 / 已存在 都加 -p(幂等)
Permission denied 在 /etc、/var 下建目录需管理员权限 加 sudo 或改到家目录
建出的目录权限不对 受 umask 影响 -m 755 显式指定,见用户与权限

复制:cp

是什么

三种场景对应三种写法:日常复制 cp a b;复制目录 cp -r(不加 -r 会直接跳过目录);备份用 cp -a——保留权限、属主、时间戳和软链接本身。

为什么备份必须用 -a:部署包的权限位、软链结构、时间戳都承载信息。用 -r 备份 Nginx 配置,sites-enabled 里的软链会变成实体副本,恢复后维护关系就断了。

怎么做

$ cp nginx.conf nginx.conf.bak
$ sudo cp -i nginx.conf /etc/nginx/
$ sudo cp -a /etc/nginx /etc/nginx.bak-$(date +%F)
$ cp -ru /data/src/ /data/dst/

[!TIP] 覆盖系统配置前先留一份带日期的副本,是成本最低的后悔药: sudo cp -a /etc/nginx/nginx.conf /etc/nginx/nginx.conf.bak-$(date +%F)

参数说明

参数 作用 说明
-r / -R 递归复制目录 复制目录必须加
-a 归档模式,保留权限/属主/时间/软链 备份首选
-i / -n 覆盖前询问 / 绝不覆盖 交互式操作建议 -i
-u / -p 只复制更新的 / 保留权限时间属主 -p 是 -a 的子集

验证方法

$ sudo cp -a /etc/hostname /tmp/hostname.copy
$ ls -l /etc/hostname /tmp/hostname.copy

预期输出:

-rw-r--r-- 1 root root 14 Sep 20 09:00 /etc/hostname
-rw-r--r-- 1 root root 14 Sep 20 09:00 /tmp/hostname.copy

权限、属主、时间戳三者一致说明 -a 生效。去掉 -a 再试,时间会变成当前时间。

常见问题

现象 根因 解决
omitting directory 'xxx' 复制目录忘了 -r 加 -r,备份用 -a
复制后权限或属主变了 默认不保留属性 用 -a 或 -p
软链变成了实体文件 -r 会跟随链接复制内容 用 -a 保留链接

移动与重命名:mv

是什么

同一分区内 mv 不搬运数据,只改目录项里的名字(或换个父目录),所以瞬间完成;跨分区则是"复制+删除",大文件会很慢——这不是 bug,是文件系统的工作方式。重命名和移动在 Linux 里是同一个操作,因为路径本来就包含文件名。

怎么做

$ mv app.conf app.conf.old
$ mv app.log /var/log/app/
$ mv -i app.conf /etc/app/
$ cp app.conf{,.bak}

最后一条是"移动/复制+改名"的快捷写法(花括号展开)。

参数说明

参数 作用 说明
-i / -n 覆盖前询问 / 不覆盖 目标已存在时保护数据
-f 强制覆盖不询问 脚本中避免卡住,风险自负
-v / -u / -t <目录> 显示过程 / 只移更新的 / 先写目标 -t 配合 xargs

验证方法

$ mv app.conf app.conf.old && ls -l app.conf.old

预期输出:

-rw-r--r-- 1 ubuntu ubuntu 1024 Sep 20 10:15 app.conf.old

注意时间戳没变——同分区移动不改文件本身,这正是它快的原因。

常见问题

现象 根因 解决
移动大文件特别慢 跨文件系统,实为复制+删除 正常;或用 rsync 看进度
移动后属主变了 跨分区复制按当前用户重建 用 rsync -a 或事后 chown
同名文件被覆盖 未加 -i 先 ls 目标;设 alias mv='mv -i'
Permission denied 对所在目录无写权限 检查父目录而非文件本身

删除:rm 与它的安全替代方案

是什么

Linux 的删除是"解除目录项与 inode 的关联",没有回收站,也没有确认弹窗。这就是它值得单独用一整节讲的原因。

[!WARNING] rm -rf 是最危险的命令组合:-r 递归、-f 强制不询问——不问你、不进回收站、不给第二次机会。下面这些写法都会造成不可逆的数据丢失,永远不要照抄:

rm -rf /
rm -rf $VAR/
rm -rf ./*
rm -rf /opt/app/current/

依次是:删掉整个系统(部分发行版有保护,但仍会毁掉数据盘)、变量为空时展开成 rm -rf /、通配符可能匹配到意外的目录、结尾斜杠会顺着软链删掉目标目录的内容。

怎么做

四个安全替代方案,按推荐度排序:

# 1. 先确认清单再删(立刻能用)
$ ls -l *.log && rm -i *.log

# 2. 先移到隔离目录,观察几天再清理(可反悔)
$ mkdir -p ~/.trash && mv old-release ~/.trash/old-release-$(date +%F)

# 3. 用 trash-cli 提供真正的回收站
$ sudo apt install -y trash-cli && trash-put old-release

# 4. find 精确定位后删(比通配符可控)
$ find /var/log/app -name "*.log" -mtime +30
$ find /var/log/app -name "*.log" -mtime +30 -delete

方案 4 的顺序很关键:先不带 -delete 看清单,确认后再加 -delete。

参数说明

参数 作用 说明
-i 删除前逐个询问 交互式默认保护
-r / -R 递归删除目录 删目录必须加
-f 强制,不询问、忽略不存在 慎用,会屏蔽"路径写错"的提示
-d / -v 删空目录(等价 rmdir) / 显示过程
trash-put 移入回收站 需装 trash-cli,trash-restore 恢复

[!TIP] 在 ~/.bashrc 加一行 alias rm='rm -i',让删除默认需要确认。代价是多敲一个 y。

验证方法

$ mkdir -p /tmp/rm-demo/sub && touch /tmp/rm-demo/sub/a.txt
$ rm -r /tmp/rm-demo
$ ls /tmp/rm-demo

预期输出:

ls: cannot access '/tmp/rm-demo': No such file or directory

常见问题

现象 根因 解决
cannot remove 'x': Is a directory 删目录没加 -r 加 -r,空目录可用 rmdir
Permission denied 对父目录无写权限 用 sudo,但先确认路径
Argument list too long 参数超过系统上限 用 find ... -delete 或 xargs,见文本处理
删了大文件但磁盘没释放 文件仍被进程打开 lsof | grep deleted 找到进程重启

查看文件内容:cat、less、head、tail

是什么

cat 一次输出全部(只适合小文件);less 分页浏览并支持搜索(看日志的默认选择);head/tail 看开头/结尾几行,tail -f 实时跟踪。为什么看日志不能用 cat:几百 MB 的文件会疯狂刷屏,甚至卡死终端;less 只加载当前屏需要的内容,秒开。

怎么做

$ cat /etc/hostname
$ less /var/log/syslog
$ head -n 20 /var/log/syslog
$ tail -f /var/log/nginx/access.log

less 里的按键(进入后按,不是命令行参数):空格/b 翻页、/关键词 搜索(n 下一个、N 上一个)、g/G 跳首尾、F 进入实时跟踪、q 退出。

参数说明

参数 作用 说明
cat -n / -A 显示行号 / 显示不可见字符 排查 Windows 换行 ^M
less -N / -i / +F 行号 / 搜索忽略大小写 / 打开即跟踪
head -n N / -c N 前 N 行 / 前 N 字节 可简写 -20
tail -n N / -n +N 后 N 行 / 从第 N 行到结尾
tail -f / -F 跟踪文件增长 / 并按文件名重试 日志轮转场景用 -F

验证方法

$ head -n 2 /etc/passwd

预期输出:

root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin

常见问题

现象 根因 解决
cat 大文件刷屏停不下 一次性输出全部 Ctrl+C 中断,改用 less
tail -f 过一会儿不动 日志被轮转,跟踪的是旧 inode 改用 tail -F,见日志基础
Permission denied 日志通常仅 root 与 adm 组可读 加 sudo 或加入 adm 组
看到 ^M 符号 文件在 Windows 编辑过(CRLF) sed -i 's/\r$//' 或 dos2unix

打包与压缩:tar、zip

是什么

tar 只做打包,压缩由 gzip/xz 完成,所以常见后缀是 .tar.gz(默认选择,快、兼容性好)和 .tar.xz(压缩率更高,适合分发包)。zip 的优势是 Windows/macOS 原生支持,跨平台交换文件时用它;服务器之间优先 tar.gz,因为后者能完整保留权限和软链。

[!IMPORTANT] 解压前务必先看一眼压缩包内容:

$ tar tf app-1.2.3.tar.gz | head -n 5

有些压缩包不是"一个目录装着所有文件",而是散装的一堆文件,直接解压会把当前目录铺满,清理起来非常麻烦。先看顶层结构,再 mkdir 一个子目录用 -C 解压进去。

怎么做

$ tar czf app-1.2.3.tar.gz app/
$ tar cJf app-1.2.3.tar.xz app/
$ tar xzf app-1.2.3.tar.gz -C /opt/app
$ zip -r app.zip app/ -x "app/node_modules/*"
$ unzip -l app.zip && unzip app.zip -d /opt/app

-C 指定的目录必须已存在,所以常先 mkdir -p。

RHEL / Rocky / AlmaLinux 9 对照
Ubuntu (apt) RHEL (dnf) 说明
sudo apt install -y zip unzip sudo dnf install -y zip unzip 包名相同
sudo apt install -y rsync sudo dnf install -y rsync 最小化安装通常不自带
sudo apt install -y trash-cli sudo dnf install -y trash-cli RHEL 需 EPEL 源

参数说明

参数 作用 说明
-c / -x / -t 创建 / 解包 / 列出内容 -t 不解压只看清单
-z / -J / -j gzip / xz / bzip2 对应 .tar.gz / .tar.xz / .tar.bz2
-f <文件名> 指定归档文件 必须紧挨文件名放最后
-v / -C <目录> 显示过程 / 指定解压位置
-p / --exclude=P 保留权限 / 排除文件 如排除 node_modules
zip -r / -x P / unzip -l 递归压缩 / 排除 / 列内容 解压用 unzip -d <目录>

验证方法

$ tar tf app-1.2.3.tar.gz | head -n 3

预期输出:

app/
app/conf/
app/conf/app.conf

第一项是 app/ 目录,说明可以安全解压;若直接就是文件名(无统一目录),就该先建子目录再用 -C。

常见问题

现象 根因 解决
解压后当前目录被铺满 包内没有统一顶层目录 先 tar tf,再 mkdir + -C
解压后属主变成陌生数字 包里存的是打包机器的 uid 加 --no-same-owner 或用 sudo
Removing leading '/' 打包时用了绝对路径 是安全保护,不是错误

远程传输:scp

是什么

scp 基于 SSH 传输,只要能 SSH 登录就能传,不需要额外开服务。语法始终是"源在前、目标在后",其中一端写成 用户@主机:路径 就是远程。

怎么做

$ scp ./app.tar.gz <用户名>@<服务器IP>:/opt/app/
$ scp <用户名>@<服务器IP>:/var/log/app/error.log .
$ scp -r ./dist <用户名>@<服务器IP>:/var/www/
$ scp -P 2222 ./app.tar.gz <用户名>@<服务器IP>:/opt/app/
$ scp -i ~/.ssh/id_ed25519 ./app.tar.gz <用户名>@<服务器IP>:/opt/app/

[!WARNING] 传输会直接覆盖远程同名文件,没有任何提示。上传前先确认远程目录里有什么: ssh <用户名>@<服务器IP> 'ls -l /opt/app/'

参数说明

参数 作用 说明
-r 递归传输目录 传目录必须加
-P <端口> 指定 SSH 端口 大写 P;小写 -p 是保留时间戳
-p / -i <私钥> 保留时间权限 / 指定身份文件
-C / -v / -l <速率> 压缩传输 / 显示详情 / 限速 限速单位 Kbit/s,如 -l 8000

验证方法

$ scp ./app.tar.gz <用户名>@<服务器IP>:/opt/app/

预期输出:

app.tar.gz                    100%   12MB   8.4MB/s   00:01

常见问题

现象 根因 解决
Permission denied (publickey,password) 密钥不对或该用户禁止登录 检查密钥与用户名,见SSH 加固
Connection refused SSH 端口未开或被防火墙拦 确认端口,见防火墙
No such file or directory 远程目标目录不存在 先 SSH 上去 mkdir -p
传大文件中断要重头再来 scp 不支持断点续传 改用 rsync --partial

远程同步:rsync 与结尾斜杠的语义

是什么

rsync 只传变化的部分,支持断点续传和保持两端一致,是部署代码和备份目录的首选。它最大的坑是源路径结尾的斜杠,语义完全不同:

写法 含义 结果
rsync -avz src/ dest/ 同步 src 目录里的内容 dest/a.txt、dest/sub/
rsync -avz src dest/ 同步 src 目录本身 dest/src/a.txt、dest/src/sub/

口诀:带斜杠是"里面的东西",不带斜杠是"连目录一起"。

[!WARNING] --delete 会删除目标端多出来的文件,让目标与源严格一致。若源路径写错(比如少了斜杠导致层级偏移),它会删掉目标目录下所有对不上的文件。 安全做法:先加 -n(预演)看清单,确认后再去掉 -n 真正执行。

怎么做

$ rsync -avz ./dist/ <用户名>@<服务器IP>:/var/www/html/
$ rsync -avzn --delete ./dist/ <用户名>@<服务器IP>:/var/www/html/
$ rsync -avz -e 'ssh -p 2222' --exclude 'node_modules' ./app/ <用户名>@<服务器IP>:/opt/app/
$ rsync -avzP ./bigdata.tar.gz <用户名>@<服务器IP>:/data/

第二条是预演(-n),只在屏幕上显示会做什么;确认无误后去掉 n 再执行。

参数说明

参数 作用 说明
-a 归档:保留权限、时间、软链并递归 几乎总是加
-v / -z 显示详情 / 传输时压缩 跨公网加 -z
-n 预演,不实际改动 配 --delete 时必用
--delete 删除目标端多余文件 保持严格一致,慎用
--exclude=P 排除文件 可写多次
-P / -e 'ssh -p N' 进度+断点续传 / 指定端口 非 22 端口时用 -e

验证方法

$ rsync -avzn ./dist/ /tmp/deploy-test/

预期输出:

sending incremental file list
./
index.html
assets/app.js

sent 220 bytes  received 28 bytes  496.00 bytes/sec

把源路径的斜杠去掉再跑一次,对比第一行是 ./ 还是 dist/,就能直观看到差异。

常见问题

现象 根因 解决
同步后多了一层 dest/dist/... 源路径没带结尾斜杠 源写成 ./dist/
目标端文件被删光 --delete + 源路径写错 立刻停止;先 -n 预演,从备份恢复
每次都全量重传 未用 -a,时间戳/权限未保留导致比对失败 加 -a

下载文件:wget 与 curl

是什么

两者都能下载。简单选择:wget 专做下载(默认存成文件、支持断点续传);curl 是通用网络工具(默认输出到屏幕,调试接口也靠它)。

怎么做

$ wget https://example.com/app.tar.gz
$ wget -O app-1.2.3.tar.gz https://example.com/download/latest
$ wget -c https://example.com/big.tar.gz
$ curl -O https://example.com/app.tar.gz
$ curl -Lo app.tar.gz https://example.com/download/latest

注意 curl 的大小写:大写 -O 按远程文件名保存,小写 -o 指定文件名。

[!IMPORTANT] 下载可执行文件后不要直接运行。先看内容确认来源可信:less install.sh;正规项目会提供校验值,下载后用 sha256sum 核对。

参数说明

参数 作用 说明
wget -O <名> / -P <目录> 指定保存名 / 保存目录
wget -c / -q 断点续传 / 安静模式 大文件必备 -c
curl -O / -o <名> 按远程名保存 / 指定输出名
curl -L / -s 跟随重定向 / 安静模式 多数下载链接需要 -L
curl -I / -f 只看响应头 / HTTP 错误返回非 0 脚本里用 -f 判断成败

验证方法

$ curl -sI https://example.com/app.tar.gz | head -n 2

预期输出:

HTTP/1.1 200 OK
Content-Length: 12582912

200 说明资源存在,Content-Length 告诉你文件大小——下载前先确认,能避免把 404 页面当安装包解压。

常见问题

现象 根因 解决
下载下来是 HTML 文本 链接是跳转页或需登录 加 -L;检查是否需认证
403 Forbidden 对方禁了默认 UA 或需鉴权 加 -H "User-Agent: ..." 或带 token
下载速度很慢 源站在境外 换国内镜像,见镜像加速
脚本里 curl 失败仍继续 curl 对 404 默认也返回 0 加 -f 并检查 $?

本章命令速查

命令 作用
mkdir -p /data/a/b 递归创建目录,已存在不报错
touch <文件> 创建空文件或更新时间戳
cp -a <源> <目标> 归档复制,保留权限/时间/软链(备份首选)
cp -r <目录> <目标> 递归复制目录(不加 -r 会跳过目录)
mv <源> <目标> 移动或重命名(同分区瞬时完成)
rm -i <文件> / rm -r <目录> 逐个确认删除 / 递归删目录
trash-put <文件> 移入回收站,可 trash-restore 恢复
cat <文件> 输出全部内容(只适合小文件)
less <文件> 分页浏览:/搜索、G 到底、F 跟踪、q 退出
head -n 20 / tail -n 20 看开头 / 结尾 20 行
tail -F <文件> 实时跟踪,支持日志轮转后重连
tar czf 包.tar.gz <目录> 打包并 gzip 压缩
tar cJf 包.tar.xz <目录> 打包并 xz 压缩(压缩率更高)
tar tf 包.tar.gz 只列内容不解压(解压前必做)
tar xzf 包.tar.gz -C <目录> 解压到指定目录
zip -r 包.zip <目录> / unzip -l zip 压缩 / 列出内容
scp -P 2222 -r <源> <用户>@<服务器IP>:<路径> 通过 SSH 传文件或目录(-P 指定端口)
rsync -avz src/ <服务器>:dest/ 增量同步目录内容(结尾斜杠决定层级)
rsync -avzn --delete src/ dest/ 预演同步(不实际改动)
wget -c <URL> / curl -LO <URL> 下载文件,wget 支持断点续传

相关章节

📄 文本处理 12-text-processing.md


title: 文本处理 updated: 2026-09 applies-to: Ubuntu 22.04 LTS

服务器上的信息绝大多数是纯文本:配置、日志、进程列表、CSV 数据。能不能在三分钟内从 20 万行日志里找出错误 TOP 10,靠的就是这一章。命令均在 Ubuntu 22.04 LTS 上验证,涉及版本号的信息一律截至 2026-09。

[!NOTE] 核心思想是"组合":每个命令只做一件事,用管道 | 串起来。先学会单个命令,再学组合。


输出重定向:>、>>、2>、2>&1

是什么

命令的输出分两条管道(文件描述符):1 号 stdout 走正常结果(>),2 号 stderr 走报错(2>)。默认都打到屏幕,重定向就是让它们改道。分开的意义:正常结果要存下来分析,报错要单独看,混在一起会干扰判断。

写法 含义
> file stdout 覆盖写(原有内容被清空)
>> file stdout 追加(安全)
2> / 2>> stderr 覆盖 / 追加
> file 2>&1 两者都进同一文件
&> / &>> 同上,bash 简写
> /dev/null 丢弃输出

怎么做

$ ls -lh /var/log > loglist.txt
$ echo "$(date): backup done" >> /var/log/mybackup.log
$ nginx -t 2> nginx-error.txt
$ ./deploy.sh > deploy.log 2>&1
$ ./deploy.sh > /dev/null 2>&1

[!IMPORTANT] 2>&1 必须写在 > 后面,顺序错了语义就变:

# 正确:stdout 先去文件,stderr 再跟过去
$ ./deploy.sh > out.log 2>&1
# 错误顺序:stderr 先接到"当前的"屏幕,stdout 才去文件
$ ./deploy.sh 2>&1 > out.log

原因:2>&1 的含义是"把 stderr 指向 stdout 当前所指向的地方",而不是永远跟着 stdout。重定向从左到右依次生效,所以顺序决定结果。

参数说明

写法 作用 说明
> / >> 覆盖 / 追加 stdout 文件不存在则创建
2> / 2>> 覆盖 / 追加 stderr 数字 2 紧贴 >,中间不能空格
2>&1 stderr 指向 stdout 当前目标 必须写在 > 之后
&> / &>> 两者一起覆盖 / 追加 bash 简写,脚本可读性更好
> /dev/null 丢弃 只关心退出码时用
set -o noclobber 禁止 > 覆盖已存在文件 需覆盖时用 >|

验证方法

$ echo "hello" > t.txt
$ echo "world" >> t.txt
$ cat t.txt

预期输出:

hello
world

把第二个 >> 换成 > 再跑,文件里只剩 world——这就是覆盖与追加的区别。

常见问题

现象 根因 解决
用 > 后原内容没了 > 是覆盖写 改用 >>;重要文件先备份
sudo echo x > /etc/a.conf 报 Permission denied 重定向由当前 Shell 执行,sudo 只作用于 echo echo x | sudo tee /etc/a.conf
加了 2> 屏幕还有输出 输出走的是 stdout 而非 stderr 改用 > file 2>&1
2>&1 似乎没生效 顺序写反了 放到 > 之后
定时任务里没输出 cron 输出默认丢弃或发邮件 显式写 >> /var/log/x.log 2>&1

管道 | 与 tee

是什么

管道把左边命令的 stdout 变成右边命令的 stdin。这是 Linux 设计的精髓:每个工具只做一件事,串联起来完成复杂任务。tee 是管道上的三通——一边往下传一边存文件,让你同时看到结果并保存。

怎么做

$ ls -1 | wc -l
$ du -h /var/log/* | sort -hr | head -n 5
$ sudo apt install -y nginx | tee install.log
$ ./deploy.sh | tee -a deploy.log
$ echo "server_tokens off;" | sudo tee -a /etc/nginx/nginx.conf

最后一条能绕过权限问题:写文件的是被 sudo 提权的 tee,而不是 Shell。

参数说明

写法 作用 说明
| 左边 stdout 接右边 stdin 只传 stdout,stderr 不传
|& 连 stderr 一起传 等价 2>&1 |
tee <文件> 输出屏幕并覆盖写入
tee -a <文件> 追加写入 记录多次运行
tee f1 f2 同时写多个文件
sudo tee 以管理员权限写文件 解决重定向提权问题

验证方法

$ echo "test-line" | tee out.txt

预期输出:

test-line

同时 out.txt 里也写入了 test-line——既看见又保存。

常见问题

现象 根因 解决
管道后加 sudo 没效果 sudo 只提升后一个命令的权限 | sudo tee
管道后错误信息消失 管道只传 stdout 用 2>&1 | 或 |&
tee 后原文件被清空 默认覆盖 用 tee -a
管道太长难调试 一次写太多段 从最短开始逐段加,用 head 查中间结果

xargs:把标准输入变成参数

是什么

很多命令(rm、cp、kill)只接受命令行参数,不读标准输入,而管道传过去的是文本流。xargs 把文本流转成参数列表,补上这个缺口:

find . -name "*.tmp" | rm        ← 不行,rm 读不到管道
find . -name "*.tmp" | xargs rm  ← 可以

怎么做

$ find . -name "*.tmp" | xargs echo rm
$ find . -name "*.tmp" | xargs rm
$ find . -name "*.conf" | xargs -I{} cp {} {}.bak
$ cat hosts.txt | xargs -n 1 ping -c 1
$ find . -name "*.log" -print0 | xargs -0 rm

[!WARNING] xargs rm 会删除管道送来的每一个路径。请先 xargs echo rm 把将要执行的完整命令打印出来看一遍,确认清单后再去掉 echo。

参数说明

参数 作用 说明
-I{} {} 作占位符,参数插到指定位置 参数不在命令末尾时用
-n N 每次取 N 个参数执行一次 逐个处理用 -n 1
-0 以 NUL 分隔输入 配 find -print0,正确处理含空格文件名
-p / -t 执行前询问 / 打印要执行的命令 危险操作加 -p
-r / -P N 输入为空时不执行 / 并发 N 个

验证方法

$ echo -e "a.txt\nb.txt" | xargs -I{} echo "cp {} {}.bak"

预期输出:

cp a.txt a.txt.bak
cp b.txt b.txt.bak

常见问题

现象 根因 解决
Argument list too long 参数数量超系统上限 用 xargs 分批,或 find -delete
文件名含空格被拆开 默认按空白分词 find -print0 | xargs -0
xargs: No such file or directory 参数为空仍执行了命令 加 -r
想在参数前后加内容 默认只能追加在末尾 用 -I{} 占位

grep:搜索文本

是什么

grep 按模式匹配行并输出,是"在几百个配置里找某一项""在日志里找报错"的首选。最常用的写法是 grep -rn "关键词" .:递归搜索当前目录并显示行号。找不到配置在哪时,第一反应就应该是它。

怎么做

$ grep -n "error" app.log
$ grep -rn "listen" /etc/nginx/
$ grep -v "^#" nginx.conf
$ grep -c "ERROR" app.log
$ grep -oE "[0-9]{1,3}(\.[0-9]{1,3}){3}" access.log
$ grep -n -C 3 "Exception" app.log
$ grep -rn "database" . --exclude-dir={.git,node_modules}

倒数第二条用 -C 3 显示匹配行的前后各 3 行,排错时看上下文必备。

参数说明

参数 作用 说明
-i / -n 忽略大小写 / 显示行号 -n 总是加
-r / -R 递归目录(-R 跟随软链) 找配置用 -r
-v / -c 反向匹配 / 只统计行数 过滤噪声、计数
-o / -w 只输出匹配部分 / 全词匹配 提取 IP;避免 err 命中 error
-E / -F 扩展正则 / 字面字符串 关键词含特殊字符用 -F(更快)
-l / -m N 只列文件名 / 每文件最多 N 条 只关心"哪些文件里有"
-A3 -B3 -C3 显示后 / 前 / 前后各 3 行 看上下文
--exclude-dir= --include= 排除目录 / 只搜匹配文件 支持花括号展开

验证方法

$ grep -n "^root" /etc/passwd

预期输出:

1:root:x:0:0:root:/root:/bin/bash

^ 是行首锚点,1: 是行号。

常见问题

现象 根因 解决
Binary file xxx matches 文件被识别为二进制 加 -a 当文本处理,或 -I 跳过
明明有关键词却搜不到 大小写不符或被拆行 加 -i;用 -C 3 看上下文
搜出几千行没法看 没限定范围 加 --include,或接 | head -n 20
正则的 |、+ 不生效 基本正则不支持 加 -E
关键词含 . * 匹配异常 被当正则元字符 加 -F 或转义 \.

find:按条件查找文件

是什么

grep 搜内容,find 找文件本身(按名字、类型、时间、大小)。两者常组合:先 find 圈定文件,再 grep 搜内容。

怎么做

$ find /etc -name "*.conf"
$ find /var -type d -name "log"
$ find /etc -type f -mtime -7
$ find / -type f -size +100M
$ find /var/log -maxdepth 2 -name "*.log"
$ find /etc -name "*.conf" -exec grep -Hn "listen" {} \;
$ find /var/log -name "*.log" -mtime +30 -delete

模式必须加引号,否则 Shell 会先展开通配符。倒数第二行是 find 与 grep 的组合,-H 让 grep 总是显示文件名。

[!WARNING] -delete 和 -exec rm {} \; 都会真正删除文件,且没有回收站。务必先不带 -delete 跑一遍看清单,确认后再删。另外 -delete 必须放在所有条件之后:find 从左到右求值表达式,-delete 一旦出现就对当前遍历到的每个文件直接执行删除,写在它后面的条件形同虚设——find . -delete -name "*.log" 会删光整棵目录树而不是只删 .log,而且不会报错提醒你。

参数说明

参数 作用 说明
-name "模式" / -iname 按名匹配(区分 / 不区分大小写) 模式要加引号
-type f d l 文件 / 目录 / 软链接 常加,缩小范围
-mtime -N / +N N 天内 / N 天前修改 -7 = 最近 7 天
-size +100M 大于指定大小 单位 k M G
-maxdepth N 最多深入 N 层 建议加,避免全盘扫描
-exec cmd {} \; / {} + 逐个执行 / 拼一批执行 + 快得多
-delete / -print0 删除 / 以 NUL 分隔输出 -delete 必须放在所有条件之后(放在前面会绕过条件全量删除且不报错);-print0 配 xargs -0

验证方法

$ find /etc -maxdepth 1 -name "*.conf"

预期输出(因机器而异):

/etc/host.conf
/etc/resolv.conf
/etc/sysctl.conf

常见问题

现象 根因 解决
结果奇怪或搜不到 模式没加引号被 Shell 展开 用引号 -name "*.conf"
Permission denied 刷屏 遍历到无权目录 加 2>/dev/null 或用 sudo
搜索特别慢 没限制深度,全盘遍历 加 -maxdepth 和起始路径
以为加了条件,结果删多了文件 -delete 写在了条件前面,条件被整体跳过 -delete 永远放最后;先用 -print 预览清单再删
找到了却删不掉 对父目录无写权限 加 sudo

sed:替换与行编辑

是什么

sed 逐行处理文本,最常用两个能力:替换(批量改配置)和取行区间(截取日志的一段)。

怎么做

$ sed 's/old/new/g' app.conf
$ sed -i 's/old/new/g' app.conf
$ sed -i.bak 's/old/new/g' app.conf
$ sed -n '10,20p' app.log
$ sed '/^#/d' nginx.conf
$ sed 's#/var/www/html#/data/www#g' nginx.conf

第一行不加 -i 只把结果打到屏幕(预览);第三行 -i.bak 会先备份再改。路径含斜杠时换 # 作分隔符,就不用写一堆转义。

[!TIP] 用 -i 之前先不加 -i 跑一遍,确认屏幕上输出的正是你要的结果,再加 -i。这一步能挡住绝大多数"改坏配置文件"的事故。改动重要配置时用 sed -i.bak,改完立刻做语法检查(如 nginx -t)。

参数说明

语法 / 参数 作用 说明
s/old/new/ / s/old/new/g 替换每行第一个 / 全部 g = global
-i / -i.bak 原地修改 / 修改并备份 先预览再改
-n '10,20p' 只打印 10–20 行 -n 抑制默认输出
/pat/d 删除匹配行 如删注释行
s#a#b#g 用 # 作分隔符 处理路径时避免转义
& / -e 'cmd' 代表匹配到的内容 / 执行多个规则 s/[0-9]/[&]/g

验证方法

$ echo "port=8080" | sed 's/8080/9090/'

预期输出:

port=9090

常见问题

现象 根因 解决
改完配置服务起不来 未预览、未备份 用 -i.bak,改完 nginx -t 校验
替换没生效 模式没匹配上(大小写、空格) 先 grep 确认原文
路径替换报 unknown option to 's' 替换串含 / 与分隔符冲突 换 s#a#b#g 或 `s
sed -i 报 extra characters macOS/BSD sed 的 -i 必须带后缀 Linux 无此限制;跨平台脚本用 -i.bak
只替换了每行第一个 忘了 g 加 g

awk:按列处理与统计

是什么

awk 把每行按分隔符切成列:$1 第一列、$2 第二列、$0 整行、$NF 最后一列。它是一门小型语言,能做条件判断和累加统计,是日志分析的主力。默认按任意空白分列(多个空格算一个),这一点比 cut 好用得多。

怎么做

$ awk -F: '{print $1}' /etc/passwd
$ awk -F: '{print $NF}' /etc/passwd
$ awk '{print NR": "$0}' app.log
$ awk '$3 > 100 {print $1, $3}' data.txt
$ awk 'NR>1 {print $1}' data.csv
$ awk '{sum+=$1} END {print sum}' numbers.txt
$ awk '{print $1}' access.log | sort | uniq -c | sort -rn | head -n 10

[!NOTE] awk 程序体必须用单引号 '{print $1}'。用双引号时 Shell 会把 $1 当自己的变量展开,结果就不是你要的列了。

参数说明

变量 / 参数 作用 说明
-F: 指定输入分隔符 如 -F, 处理 CSV
$0 / $1…$n / $NF 整行 / 第 n 列 / 最后一列 NF 是当前行列数
NR / FNR 当前行号 / 当前文件内行号 NR==1 是表头
BEGIN{} / END{} 处理前 / 处理后执行一次 打印表头、输出汇总
条件 {动作} 满足才执行 如 $3 > 100
-v OFS=',' 指定输出分隔符 控制输出格式

验证方法

$ echo -e "10\n20\n30" | awk '{sum+=$1} END {print sum}'

预期输出:

60

常见问题

现象 根因 解决
输出为空或整行 用了双引号,$1 被 Shell 展开 改用单引号
取列不对 分隔符不是空白 用 -F 指定
求和结果是科学计数法 数值过大 printf "%.0f\n", sum
表头也被统计 未跳过第一行 加 NR>1 或 tail -n +2
多空格导致列错位 用了 cut 而非 awk 优先用 awk

统计三件套:sort、uniq、wc、cut、tr

是什么

组合起来回答"哪个值出现得最多":

sort  → 把相同的值排到一起(uniq 只能去掉相邻的重复行)
uniq -c → 统计每种值出现几次
sort -rn → 按次数从大到小,最大的排最前

必须先 sort 再 uniq:uniq 只比较相邻行,不去重全局,所以"先排序再去重"是固定套路。cut 按分隔符或字符位置截取,tr 做字符级替换/删除;两者都比 awk/sed 简单,适合格式固定的简单场景——但 cut 遇到连续空格会错位,处理命令输出优先用 awk。

怎么做

$ awk '{print $1}' access.log | sort | uniq -c | sort -rn | head -n 10
$ sort -n numbers.txt
$ sort -t: -k3 -n /etc/passwd
$ wc -l access.log
$ cut -d: -f1,3 /etc/passwd
$ tr 'a-z' 'A-Z' < input.txt
$ tr -d '\r' < win.txt > unix.txt

参数说明

参数 作用 说明
sort -n / -r 数值排序 / 逆序 数字必须加 -n,否则 10 排在 9 前
sort -u / -h 排序并去重 / 按人类可读大小排 -h 配 du -h
sort -k N / -t: 按第 N 列排 / 指定列分隔符
uniq -c / -d / -u 计数 / 只显示重复 / 只显示唯一 -c 前必须先 sort
wc -l / -w / -c 行数 / 单词数 / 字节数
cut -d: -f1 指定分隔符 / 取第 1 列 -f1,3 多列,-f2- 到第 2 列起
cut -c1-10 按字符位置截取 定长格式日志
tr -d / -s 删除字符 / 压缩重复字符 如压缩多余空格

验证方法

$ printf "b\na\nb\nc\n" | sort | uniq -c

预期输出:

      1 a
      2 b
      1 c

去掉 sort 后 uniq -c 会把两个不相邻的 b 算成两次,得到错误结果——这就是必须先排序的原因。

常见问题

现象 根因 解决
uniq 没去掉重复 相同值不相邻 先 sort | uniq
排序结果 10 排在 9 前 按字符串排序 加 -n
统计结果包含表头 未过滤第一行 加 awk 'NR>1'
cut 取列错位 连续空格被当作多个分隔符 改用 awk '{print $1}'
tr 提示 extra operand 参数个数不对 tr 需两个集合,删除用 -d
tr 似乎没生效 它只处理 stdin,不接受文件名参数 用 tr ... < file

本章命令速查

命令 作用
cmd > f / cmd >> f 输出覆盖写 / 追加写到文件
cmd 2> f 只保存错误信息
cmd > f 2>&1 正常与错误都进同一文件(顺序不能反);换 /dev/null 即丢弃
cmd1 | cmd2 管道:前一个的输出交给后一个
cmd | tee -a f 同时显示并追加保存
echo x | sudo tee /etc/f 提权写系统文件(绕过重定向权限限制)
... | xargs -I{} cmd 把标准输入转成参数,{} 是占位符
find ... -print0 | xargs -0 安全处理含空格的文件名
grep -rn "关键词" . 递归搜索并显示行号(找配置首选)
grep -i -v -c -o 忽略大小写 / 反向 / 计数 / 只输出匹配部分
grep -E / -F / -C3 扩展正则 / 字面匹配 / 显示前后各 3 行
find -name "*.conf" / -mtime -7 按名查找(模式加引号)/ 找 7 天内改过的
find -size +100M / -delete 找大于 100MB 的文件 / 删除(先预览)
sed 's/old/new/g' f 替换并预览(不加 -i 不改文件)
sed -i.bak 's/old/new/g' f 原地修改并备份(-n '10,20p' 取行区间)
awk -F: '{print $1}' f 按冒号分隔取第 1 列(NR>1 跳过表头)
awk '{sum+=$1} END {print sum}' 对第一列求和
sort | uniq -c | sort -rn 统计出现次数并倒序(三件套)
sort -n / -h 数值排序 / 按人类可读大小排序(如配 du -h)
wc -l / cut -d: -f1 / tr -d '\r' 统计行数 / 按分隔符取列 / 删除字符

相关章节

📄 用户、组与权限管理 13-users-and-permissions.md


title: 用户、组与权限管理 updated: 2026-09 applies-to: Ubuntu 22.04 LTS

Linux 天生是多用户系统:一台服务器上可同时存在几十个账号,谁能读哪个文件、谁能重启哪个服务,全靠一套规则划分。这套规则只有三块积木:账号(你是谁)、归属(文件归谁)、权限位(属主 / 属组 / 其他人分别能干什么)。

先把最容易踩的两个坑写在最前面,因为它们都会造成不可逆的后果:图省事执行 chmod 777(等于把服务器的所有门钥匙发给每一个能登录的人);用普通编辑器直接改 /etc/sudoers(语法写错就会把自己永久锁在管理员权限之外)。

[!NOTE] 本章命令与默认行为均基于 Ubuntu 22.04 LTS,版本相关信息截至 2026-09。

用户、组与身份查询

是什么

Linux 里没有"用户数据库",用户信息就是三个纯文本配置文件:

文件 存什么 谁能读
/etc/passwd 用户名、UID、GID、家目录、登录 Shell 所有人
/etc/shadow 密码哈希与有效期 仅 root
/etc/group 组名、GID、附加成员名单 所有人

/etc/passwd 第二个字段恒为 x,意思是"密码不在这儿,去 /etc/shadow 找"——早期 Unix 把密码哈希放在人人可读的 passwd 里,容易被离线爆破,后来才拆出只给 root 读的 shadow。内核只认 UID(数字),用户名只是给人看的别名:0 是 root(绕过一切权限检查),1–999 是系统用户(服务账号,通常禁止登录,如 www-data=33),1000+ 是管理员创建的普通用户。

日常排错很少翻这三个文件,更多是用只读命令回答"我是谁、谁在机器上、谁登录过":whoami、id、groups、who(当前在线)、last(历史登录)。

怎么做

# 我是谁 / 我的全部身份 / 我的组
$ whoami
$ id
$ groups

# 当前在线会话(用户名、终端、来源 IP)
$ who

# 最近 10 条登录历史(含重启记录)
$ last -n 10

# 查账号(含 LDAP 等外部源)
$ getent passwd <用户名>

[!WARNING] 不要用 vim / nano 直接编辑这三个文件。少一个冒号就可能让所有用户无法登录。请始终用 useradd、usermod、passwd、groupadd,它们保证格式正确与并发安全。

参数说明

/etc/passwd 每行 7 个字段,冒号分隔:

字段 示例 说明
1 用户名 alice 登录名,最长 32 字符
2 密码占位 x 真身在 /etc/shadow
3 UID 1001 内核识别用户的数字 ID
4 GID 1001 主组,新建文件默认属该组
5 注释 Alice GECOS 字段,仅注释用途
6 家目录 /home/alice 登录后进入的目录
7 登录 Shell /bin/bash 填 /usr/sbin/nologin 表示禁止登录
id -u / -g / -G — 只打印 UID / 主组 / 全部组,id -n 显示名字而非数字

另外 last -n N 限制条数,who -a 信息更全。

验证方法

$ id

预期输出:

uid=1000(ubuntu) gid=1000(ubuntu) groups=1000(ubuntu),4(adm),27(sudo),100(users)

读作:我是 ubuntu,UID 1000,主组 ubuntu,另属 adm、sudo、users。出现在 sudo 组里,正是我能使用 sudo 的原因。

常见问题

现象 根因 解决
/etc/passwd 里 grep 不到某用户 账号由 LDAP / SSO 提供,不在本地文件 用 getent passwd <用户名>,它同时检索外部源
cat /etc/shadow 报 Permission denied 权限为 -rw-------,仅 root 可读 用 sudo cat;别为省事给它加读权限
进程属主显示为数字而非用户名 用户已删但进程还在,或在容器命名空间内 属正常,用 ps -o user= -p <PID> 复核
last 输出为空 /var/log/wtmp 被清空或被 logrotate 轮转 查 /var/log/auth.log 或 journalctl -u systemd-logind

创建、修改与删除用户

是什么

创建用户 = 在三个文件里追加记录 + 建家目录骨架 + 设初始密码。Ubuntu 给了两个命令:useradd(底层、不交互,不带 -m 时不建家目录,新手常被坑)和 adduser(交互式封装,自动建家目录、拷 /etc/skel 模板、问密码)。新手手动操作用 adduser,写自动化脚本用 useradd。

建好后的常见后续操作是改组和改状态:加进 sudo 组让他能提权、加进 docker 组让他能用 Docker、有人离职时锁定而不是立刻删除。配套命令是 groupadd 建组、gpasswd 加减成员。

怎么做

# -m 建家目录,-s 指定登录 shell
$ sudo useradd -m -s /bin/bash <用户名>

# 设密码(没密码的账号处于锁定态,无法登录)
$ sudo passwd <用户名>

# 更友好的交互式方式
$ sudo adduser <用户名>

# 加入 sudo 组(-a 是 append,必须加!)
$ sudo usermod -aG sudo <用户名>

# 锁定 / 解锁账号
$ sudo usermod -L <用户名>
$ sudo usermod -U <用户名>

# 建组、加人、减人
$ sudo groupadd devops
$ sudo gpasswd -a <用户名> devops
$ sudo gpasswd -d <用户名> devops

# 删账号;加 -r 连家目录一起删
$ sudo userdel -r <用户名>

[!WARNING] usermod -G 不带 -a 会覆盖用户的全部附加组,这是 Linux 运维的经典事故。 用户原属 sudo,docker,www-data,执行 usermod -G devops alice 之后他就只剩 devops ——刚丢掉的 sudo 意味着他再也提不了权。正确写法永远是 usermod -aG devops alice,改完务必用 id <用户名> 复核。此外 userdel -r 会递归删除家目录且不可恢复,生产环境建议先 usermod -L 锁定观察一段时间再删。

参数说明

参数 作用
-m 建家目录,并把 /etc/skel 下的 .bashrc 等模板拷进去
-s <shell> 指定登录 Shell;写 /usr/sbin/nologin 即建不可登录的服务账号
-aG <组列表> 追加附加组,逗号分隔;缺 -a 则变成覆盖(见上方警告)
-g <组> 改主组(小写 g),影响此后新建文件的默认属组
-L / -U Lock / Unlock:在 shadow 密码字段前加 ! 或去掉它
-r 建系统用户(UID < 1000);用在 userdel 上则是连同家目录一起删
gpasswd -a / -d 往组里 add / delete 成员

[!NOTE] -L 只锁密码登录,配了 SSH 密钥的账号照样能进。要彻底禁止请同时 usermod -s /usr/sbin/nologin,或移除 ~/.ssh/authorized_keys。RHEL 系的差异是 adduser 只是 useradd 的软链接、并不交互,所以那边必须显式加 -m。

验证方法

$ id <用户名>

预期输出:

uid=1001(alice) gid=1001(alice) groups=1001(alice),27(sudo),998(docker)

确认 27(sudo) 仍在列表中。若它消失了,说明误用了不带 -a 的 -G,立刻用 sudo usermod -aG sudo <用户名> 恢复。账号是否被锁定可用 sudo passwd -S <用户名> 确认,输出第二个字段 L=锁定、P=有可用密码、NP=空密码。

常见问题

现象 根因 解决
新用户提示符是光秃秃的 $,没颜色不补全 用了 useradd 却没加 -m,.bashrc 没被拷贝 补建 sudo mkhomedir_helper <用户名>
新用户无法 SSH 登录、或加了 sudo 组仍提示 "not in the sudoers file" 未设密码处于锁定态(或 Shell 是 nologin);组成员变更要重新登录才生效 sudo passwd <用户名> 并检查 /etc/passwd 第 7 字段;退出重连 SSH 或 newgrp sudo 刷新
userdel: user X is currently used by process N 该用户还有进程在跑 先 sudo pkill -u <用户名>,再删除
删了又重建同名用户,文件归属错乱 旧文件属主变成孤儿数字 UID sudo find / -uid <旧UID> -exec chown <用户名> {} +

sudo 提权与身份切换

是什么

root 的 UID 是 0,内核对它不做任何权限检查。直接用 root 登录有两个致命问题:没有刹车(一条手滑的 rm -rf / home/* 就是灾难),没有审计(三个人都用 root,日志里追不到人)。

sudo 的设计哲学是最小权限 + 可追溯:默认仍是你自己,只在需要时临时借用 root 身份执行一次命令,每次调用都写进 /var/log/auth.log,能查到谁、在什么时候、跑了什么。授权规则存在两处:/etc/sudoers(主配置,Ubuntu 上 sudo 组拥有全部权限)和 /etc/sudoers.d/*(推荐,一个应用一份,被主文件末尾的 @includedir 加载)。

要从"自己"变成别人,还有三个命令,关键差别在是否加载目标用户的环境:

命令 加载目标环境 需要谁的密码
su <用户> 否,$HOME、$PATH 还是原来的 对方密码
su - <用户> 是,等同于重新登录 对方密码
sudo -i / sudo -u <用户> -i 是 自己的密码

[!NOTE] su 不加 - 的典型症状:切过去了,但 echo $PATH 还是原来用户的,java、node 这些装在对方家目录里的命令全部找不到。只要是"要在这个账号下干活",就该用 su -。

怎么做

# 查看"我被授权了什么"——排查权限问题的第一步
$ sudo -l

# 以指定用户身份执行一条命令
$ sudo -u www-data whoami

# 打开完整的 root shell(环境也重置为 root 的)
$ sudo -i

# 完整切到其他用户;不切环境只跑一条命令则用 su -c
$ su - <用户名>
$ su <用户名> -c 'whoami'

# 只编辑独立文件,不碰主配置(visudo 保存时会做语法检查)
$ sudo visudo -f /etc/sudoers.d/deploy

写入一条"只允许免密重启某个服务"的规则;授予完整管理员权限(需输密码)则写 deploy ALL=(ALL:ALL) ALL。切换后离开用 exit:

deploy ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart myapp

[!IMPORTANT] 永远用 visudo,永远不要 sudo vim /etc/sudoers。 visudo 先把内容写临时文件做语法校验,只有正确才落盘;直接编辑一旦写错语法,系统里所有 sudo 调用都会失败——包括你想用来修复它的那次 sudo。这就是把自己锁在门外的经典方式。

[!WARNING] NOPASSWD:ALL 意味着该用户无需任何确认即可执行任意 root 命令,等同于送出一个不受限的 root 后门。它只允许出现在容器内一次性构建、CI runner 这类场景;任何人类账号、任何能从公网登录的账号都不要写。需要免密时把命令限定到具体路径:NOPASSWD: /usr/bin/systemctl restart myapp。

参数说明

sudo 规则格式为 用户 主机=(可切换的身份) 命令:

片段 / 参数 含义
deploy 被授权的用户;写 %devops 表示授权整个组
ALL= 在哪些主机生效,单台写 ALL 即可
(ALL:ALL) 可切换成的用户:组,省略则默认 root
末尾 ALL 允许执行的命令;写绝对路径则只允许那一条
NOPASSWD: 该命令免密;不加则要求输入自己的密码
sudo -l / -l -U <用户> 列出自己 / 指定用户的 sudo 规则
sudo -u <用户> 以指定用户身份执行
sudo -i / sudo -s 登录式 / 非登录式 root shell(后者保留部分原环境变量)
sudo -k 清空时间戳缓存,强制下次重新验密
su -c '<命令>' 只执行这一条命令后返回,适合脚本

规则里的命令路径请写绝对路径(which systemctl 查到的那个)——写成相对命令名的话,攻击者只要在 PATH 里放一个同名脚本就能提权。

RHEL / Rocky / AlmaLinux 9 对照
Ubuntu (sudo 组) RHEL (wheel 组) 说明
sudo usermod -aG sudo <用户名> sudo usermod -aG wheel <用户名> 组名不同,机制相同
安装时创建的首个用户默认在 sudo 组 需手动勾选 "Make this user administrator" RHEL 装完要先确认
/etc/sudoers.d/* /etc/sudoers.d/* 目录约定一致

验证方法

$ sudo -l

预期输出:

User ubuntu may run the following commands on <主机名>:
    (ALL : ALL) ALL

(ALL : ALL) ALL 表示可以以任意用户、任意组身份执行任意命令——这就是 sudo 组的默认授权。

su 加 - 与不加 - 的差别,用一条命令即可看清(先不加载环境、后加载环境):

$ sudo su alice -c 'echo $HOME' && sudo su - alice -c 'echo $HOME'

预期输出:

/root
/home/alice

常见问题

现象 根因 解决
xxx is not in the sudoers file 既不在 sudo 组,也没有自定义规则 管理员执行 sudo usermod -aG sudo <用户名>,然后重新登录
改了 sudoers 后所有 sudo 报语法错误 用普通编辑器写入了错误语法 见下方 IMPORTANT 的挽救办法
sudo: command not found PATH 问题(sudo 所在目录不在 PATH 里),或最小化镜像未安装 sudo 用绝对路径 /usr/bin/sudo;未安装则先想办法 apt install sudo
su - 输 root 密码总是错 Ubuntu 默认锁定 root 密码 日常提权改用 sudo -i
脚本里 sudo 卡住等密码 非交互环境无法输入密码 配精确到命令的 NOPASSWD:,不要 echo 密码 | sudo -S

[!IMPORTANT] 把自己锁在 sudo 门外后的挽救办法(代价从小到大):① 还有另一个 sudo 账号,用它登录修复;② 有桌面环境时用 pkexec visudo 走 polkit 提权,不依赖 sudo 本身可用;③ 云服务器用厂商提供的 VNC / 串行控制台以 root 身份直连;④ 物理机重启进单用户模式(GRUB 按 e,在 linux 行末尾加 init=/bin/bash),改完 exec /sbin/init 重启。这也是为什么服务器上永远要保留两个可用的 sudo 账号。

读懂文件权限位

是什么

每个文件都记着两套信息:归谁(属主 + 属组)和谁能干什么(三组 rwx)。ls -l 第一列就是权限位:

-rw-r--r-- 1 alice devops 2048 Sep 20 10:35 report.txt
drwxr-x--- 2 alice devops 4096 Sep 20 10:35 data/

第 1 字符是类型(-文件、d目录、l软链接),随后三组分别是属主(rwx)、属组(r--)、其他人(r--)的权限。匹配规则是一次命中即停止:你就是属主的话,只看前三位,后六位完全不参与判断。

rwx 对文件和目录含义差别很大,这是初学者最大的困惑点:

权限 对文件 对目录
r 能看内容(cat) 能列出目录里的文件名(ls)
w 能改内容 能新建 / 删除 / 重命名目录内的文件
x 能当程序运行 能进入该目录(cd)并访问其中文件的元数据

[!IMPORTANT] 目录的 x 位是"能否进入",没有 x 即使有 r 也列不出内容。 很多人以为"给了 r 就能看",于是 chmod 644 somedir,结果 ls somedir 报 Permission denied、cd 也进不去——因为少了 x,ls 连读取目录项这一步都走不到。目录可读 = r-x(5),可写 = rwx(7),几乎没有例外。同理,删除文件的权限取决于其所在目录的 w 而非文件本身:444 只读的文件,只要父目录可写照样能被删——因为"删除"改的是目录的内容清单。

怎么做

# 查看权限位与归属
$ ls -l report.txt

# 查看目录本身(而不是它的内容)的权限
$ ls -ld /var/www

# 同时显示符号与数字形式,最直观
$ stat -c '%A %a %U:%G %n' report.txt

# 逐层列出路径上每一级的权限,排查长路径的神器
$ namei -l /var/www/html/index.html

八进制换算只有一句:r=4、w=2、x=1,每组相加,三组依次是属主、属组、其他人:

数字 组合 常见场景
755 rwxr-xr-x 目录的标准权限;可执行脚本
644 rw-r--r-- 普通文件的标准权限(配置、网页、文档)
700 rwx------ 家目录、备份目录、私密脚本
600 rw------- 私钥(SSH key、TLS 私钥)、含密码的配置
777 rwxrwxrwx 所有人可读可写可执行——永远不要用

[!WARNING] 为什么是 755 而不是 777? 777 表示任何能登录这台机器的人都可以修改并执行该文件。放到 /var/www 上意味着普通用户能改写你的网页挂马;放到脚本上意味着别人能塞一行 rm -rf 等你或 cron 去执行。chmod 777 不是"宽松的权限设置",而是把服务器控制权交给所有登录者。 遇到 Permission denied 时正确做法是查出该文件应该属于谁、需要什么权限再精确授权,而不是无脑加权限。唯一勉强可接受的 777 场景是 /tmp,而它靠 sticky 位兜底,实际显示为 drwxrwxrwt。

参数说明

参数 作用
ls -l 长格式:权限、硬链接数、属主、属组、大小、时间、名称
ls -ld 对目录本身操作,而不是列出它的内容
ls -lh 大小显示成 4.0K 这种人类可读形式
stat -c '%a' 只输出八进制权限位(如 644)
stat -c '%A' 只输出符号形式(如 -rw-r--r--)
stat -c '%U:%G' 输出属主名:属组名
namei -l <路径> 逐层列出路径上每一级的权限与归属

验证方法

复现"目录没有 x 位"这个经典现象:

$ mkdir demo && echo hi > demo/a.txt
$ chmod 644 demo
$ ls demo

预期输出:

ls: cannot access 'demo/a.txt': Permission denied

明明有文件、r 也给了,却连文件名都看不到;执行 chmod 755 demo 之后 ls demo 才会正常显示 a.txt。

常见问题

现象 根因 解决
目录有 r 但 ls 仍 Permission denied 缺 x 位,进不去目录就读不到目录项 chmod 755 <目录>
文件是 444 只读却被人删了 删除权限看父目录的 w chmod o-w <父目录>
bash: ./deploy.sh: Permission denied 脚本没有 x 位 chmod +x deploy.sh,或 bash deploy.sh 显式用解释器
改了权限仍被拒 卡在父目录,任一层没 x 都进不去 namei -l <完整路径> 逐层查

遇到 Permission denied 时按这个顺序排查,比乱加权限快得多:① 权限位 → ② 属主属组 → ③ 父目录每一级 → ④ AppArmor / SELinux → ⑤ 文件系统是否只读或 immutable。

chmod 与 chown:修改权限和归属

是什么

chmod 改那三组 rwx,有两种写法:符号法(u/g/o/a 配合 +、-、=,只动你指定的那一位)和数字法(一次性给出三组最终值)。原则是微调用符号法,定值用数字法。

chown 改属主和属组,chgrp 只改属组。归属决定"这套权限对谁生效":最典型的部署场景是文件由 root 用 sudo 解压出来、服务却以 www-data 身份运行而读不到——这时该做的是 chown www-data:www-data,不是 chmod 777。

怎么做

# 符号法:属主加执行 / 去属组写 / 清空其他人 / 所有人加读
$ chmod u+x deploy.sh
$ chmod g-w report.txt
$ chmod o= secret.txt
$ chmod a+r README.md

# 数字法
$ chmod 755 /var/www/html/index.html
$ chmod 600 ~/.ssh/id_ed25519

# 改归属:同时改属主属组 / 只改组
$ sudo chown www-data:www-data /var/www/html/index.html
$ sudo chgrp www-data /var/www/html/index.html

# 递归把整个目录树交给应用账号
$ sudo chown -R www-data:www-data /var/www/html

[!WARNING] chmod -R 无差别作用于所有层级,而文件和目录需要的权限并不相同:chmod -R 755 会把普通文件也变成可执行,chmod -R 644 则会抹掉所有子目录的 x 位、让整棵目录树再也进不去。安全做法是分开处理——目录 find /var/www/html -type d -exec chmod 755 {} +、文件 find /var/www/html -type f -exec chmod 644 {} +;也可以直接用大写 X:chmod -R a+rX /var/www/html 只给目录和已可执行文件加 x。执行任何 -R 之前先确认路径写对了——多一个空格就可能改到完全不同的地方。

[!IMPORTANT] 递归 chown -R 前请确认路径写对。最经典的事故是 chown -R www-data:www-data /var/www/html / ——末尾手滑多敲了一个空格和斜杠,在 sudo 下它会把整个根目录的所有权改掉,系统立刻崩溃且极难恢复。危险命令执行前先 ls 一遍路径,或先不加 -R 试一次看结果。

参数说明

参数 作用
u / g / o / a 属主 / 属组 / 其他人 / 三者
+ / - / = 增加 / 移除 / 精确设定(= 会清空未列出的位)
X(大写) 只对目录和"已可执行的文件"加 x,配 -R 比小写安全
chmod -R / -v / -c 递归 / 显示每条改动 / 只显示真正变化的
chmod --reference=<文件> 从参照文件复制权限
chown user:group 同时指定属主和属组
chown user: / :group 只改属主(冒号不能省)/ 只改属组
chgrp -R <组> <目录> 递归只改组,文件仍归原属主

只想让组可写又不想改属主时,用 chgrp 而不是 chown:sudo chgrp -R www-data /var/www/html && sudo chmod -R g+w /var/www/html。文件仍归你,只是同组的服务账号也能写。

验证方法

$ chmod 644 demo.txt && stat -c '%a %A' demo.txt

预期输出:

644 -rw-r--r--

chmod o= demo.txt 会清空其他人的权限,得到 640 -rw-r-----。改归属后核对:

$ sudo chown www-data:www-data demo.txt && ls -l demo.txt

预期输出:

-rw-r--r-- 1 www-data www-data 12 Sep 20 11:05 demo.txt

常见问题

现象 根因 解决
chmod: Operation not permitted 你不是属主,也不是 root 用 sudo chmod,或先 chown 到自己名下
chmod -R 644 之后整个目录进不去 目录的 x 位被抹掉 find <目录> -type d -exec chmod 755 {} + 恢复
脚本加了 x 仍报 bad interpreter 内容有问题(CRLF 换行或 shebang 错),与权限无关 head -1 script.sh;file script.sh 看是否 CRLF
chown: invalid user: 'www-date' 用户名拼错或不存在 getent passwd www-data 核对
改了属主仍 Permission denied 路径中间某层没权限,或服务被 systemd ProtectSystem= 隔离 namei -l 排查;systemctl show nginx | grep Protect

umask、特殊权限位与 ACL

是什么

umask 回答"为什么我新建的文件自动就是 644 / 755":系统先给一个满权限初值,再减去 umask 标出的位——普通文件初值 666(本来就不给 x,避免新建文件都可执行的荒唐事),目录初值 777(必须有 x 才可用),所以 umask 022 下文件得 644、目录得 755。

特殊权限位是三组 rwx 之外的第 4 位数字,平时用得少但你要能认出来:

位 数值 作用
setuid 4 执行时以文件属主身份运行(/usr/bin/passwd 靠它让你改自己的密码)
setgid 2 对目录:其中新建的文件自动继承目录的属组,团队共享目录必配
sticky 1 对目录:只有文件属主或 root 能删自己的文件,即使目录是 777

/tmp 显示为 drwxrwxrwt —— 末尾的 t 就是 sticky 位,所有人都能建临时文件,却删不掉别人的。ACL 是 rwx 的补充:rwx 只能表达"属主 / 属组 / 其他"三类人,ACL 允许给某一个具体用户单独授权。

怎么做

# 查看当前掩码 / 符号形式 / 临时修改(只对当前 shell 有效)
$ umask
$ umask -S
$ umask 027

# 设 setgid,使目录内新建文件继承目录属组(等价数字法 chmod 2775)
$ sudo chmod g+s /srv/shared

# 加 sticky 位
$ sudo chmod +t /srv/tmpdir

# ACL:单独给 bob 读写执行;查看;删除该条
$ sudo setfacl -m u:bob:rwx /srv/shared/report.txt
$ getfacl /srv/shared/report.txt
$ sudo setfacl -x u:bob /srv/shared/report.txt

[!NOTE] 设了 ACL 后 ls -l 的权限位末尾会多一个 +(如 -rw-rwxr--+),提醒你"还有规则没显示"。备份用 getfacl -R /srv > acl.bak,恢复用 setfacl --restore=acl.bak。

参数说明

命令 / 参数 作用
umask 022 其他人不可写——Ubuntu 默认值
umask 027 其他人完全无权限,适合同组协作
umask 077 只有自己能访问,最严格,适合跳板机
chmod u+s / g+s / +t 设 setuid(4) / setgid(2) / sticky(1)
chmod 4755 4=setuid + 755
setfacl -m u:<用户>:rwx 给指定用户加 ACL(-m = modify)
setfacl -m g:<组>:rx 给指定组加 ACL
setfacl -x u:<用户> / -b 删除某条 ACL / 清除全部 ACL
getfacl <文件> 查看 ACL 规则

[!WARNING] 给脚本设 setuid 在 Linux 上无效(内核出于安全考虑忽略脚本的 setuid 位),别指望靠它提权。而给属主是 root 的二进制设 u+s,意味着任何人执行它都拥有 root 权限——若该程序有漏洞就是一条完整提权链。不要给来路不明的程序设 setuid;排查时可用 find / -perm -4000 -type f 2>/dev/null 列出系统上所有 setuid 文件。

验证方法

验证 umask 如何决定新建文件的权限:

$ umask 022
$ touch newfile && mkdir newdir
$ stat -c '%a %n' newfile newdir

预期输出:

644 newfile
755 newdir

验证 setgid 的组继承效果:我(ubuntu)建的文件,属组自动变成目录的 devops 而非我的主组——这就是 setgid。

$ sudo chmod g+s /srv/shared
$ touch /srv/shared/a.txt && ls -l /srv/shared/a.txt

预期输出:

-rw-r--r-- 1 ubuntu devops 0 Sep 20 11:20 /srv/shared/a.txt

常见问题

现象 根因 解决
改了 ~/.bashrc 里的 umask 不生效 Ubuntu 默认 ~/.profile 会 source ~/.bashrc,登录 shell 里其实同样生效;真正不生效的是非交互 shell(脚本/cron 不加载任何 bash 启动文件) 非交互场景在脚本里显式 source ~/.bashrc;或写 /etc/profile.d/umask.sh 统一所有登录环境
服务创建的文件权限与命令行不同 systemd unit 里单独设了 UMask= systemctl show <服务> | grep UMask,在 unit 里改
chmod +t 后显示大写 T 不是 t sticky 已设但该位没有 x 补 chmod o+x <目录>,或直接用 chmod 1777
setfacl: Operation not supported 或备份后 ACL 丢失 文件系统未开 acl;普通 cp 不保留 ACL mount 检查挂载选项;备份改用 cp -a / rsync -a,tar 加 --acls

本章命令速查

命令 作用
id / id <用户名> 查看 UID、GID 与所属的全部组
whoami / who / last / getent passwd 当前用户名 / 在线会话 / 历史登录 / 查账号(含 LDAP)
sudo adduser <用户名> 或 useradd -m -s /bin/bash 创建用户(前者交互式,推荐新手)
sudo passwd <用户名> 设密码;passwd -S 看锁定状态
sudo usermod -aG sudo <用户名> 加入 sudo 组(-a 不能忘)
sudo usermod -L / -U <用户名> 锁定 / 解锁账号
sudo userdel -r <用户名> 删用户并删家目录(危险)
sudo groupadd / gpasswd -a / -d 建组 / 加人 / 减人
sudo -l 查看自己被授予的 sudo 规则
sudo visudo -f /etc/sudoers.d/<名> 安全编辑自定义授权规则
su - <用户名> / sudo -i 完整切换身份 / 提权到 root
ls -l / stat -c '%a %A %U:%G' 查看权限位与归属;后者一次看全数字 + 符号 + 属主
chmod 755 / 644 / 700 / 600 目录 / 普通文件 / 私密目录 / 私钥
chmod u+x / g-w / o= / a+rX 符号法微调权限
sudo chown <用户>:<组> <文件> 改属主与属组;只改组用 chgrp;-R 递归
umask / chmod g+s / chmod +t 查看默认掩码 / 设 setgid 继承属组 / 设 sticky 防互删
setfacl -m u:<用户>:rwx <文件> / getfacl 给单个用户单独授权 / 查看 ACL

相关章节

📄 进程管理 14-process-management.md


title: 进程管理 updated: 2026-09 applies-to: Ubuntu 22.04 LTS

程序是躺在磁盘上的文件,进程是它被运行起来之后在内存里的实例。同一个 Nginx 程序可以同时跑出十几个进程,每个都有一份独立的运行时状态。服务器上"服务起不来""CPU 飙高""端口被占"本质上都是进程问题,所以这一章是排错的地基。

全章最重要的两条原则:先 SIGTERM、实在不行才 SIGKILL;杀进程前先确认你杀对了。

[!NOTE] 本章命令与默认行为均基于 Ubuntu 22.04 LTS,版本相关信息截至 2026-09。

进程、PID 与父子关系

是什么

内核给每个进程分配一个唯一编号 PID(Process ID),并记下是谁启动了它——PPID(Parent PID)。于是所有进程构成一棵树:开机后内核启动的第一个用户态进程是 systemd(PID 1),它拉起 sshd、cron、网络服务,sshd 再为你每个 SSH 连接开一个 bash,bash 再启动你敲的每一条命令。

这个"父子"关系在排错时非常有用:

  • 父进程退出,子进程会被"过继"给 PID 1(称为孤儿进程),不会消失
  • 子进程退出但父进程没来收尸,它就变成僵尸进程(<defunct>),占着一个 PID 条目
  • 杀父进程通常不会自动杀掉子进程,这就是为什么"重启了服务但旧进程还在"
概念 含义
PID 进程的唯一编号,kill 就是按它寻址
PPID 父进程编号,据此画出进程树
PID 1 systemd,所有进程的最终祖先
僵尸进程 已退出但父进程未回收,状态为 Z

怎么做

# 以树状结构查看进程关系(看清谁拉起了谁)
$ pstree -p

# 只看某个进程的树
$ pstree -p -s <PID>

# 查自己的 PID 与父进程
$ echo $$         # 当前 shell 的 PID
$ ps -o pid,ppid,cmd -p $$

# 查某个进程的父子信息
$ ps -o pid,ppid,cmd -p <PID>

参数说明

参数 作用
pstree -p 显示树状结构并标出每个进程的 PID
pstree -p -s <PID> 只显示指定进程及其祖先链(-s = show parents)
pstree -u 在进程名后标出属主,多用户机器上很直观
ps -p <PID> 只看指定 PID
$$ Shell 变量,展开为当前 shell 自己的 PID
$! Shell 变量,展开为最近一个后台任务的 PID

验证方法

$ ps -o pid,ppid,stat,cmd -p $$

预期输出:

  PID  PPID STAT CMD
 8123  8119 Ss   -bash

当前 bash 的 PID 是 8123,它的父进程是 8119——顺着 8119 往上查,最终会走到 SSH 会话和 PID 1。

常见问题

现象 根因 解决
pstree: command not found 精简镜像没装 psmisc sudo apt install psmisc
子进程还在,父进程没了 父进程退出后子进程被过继给 PID 1 属正常;要一起停得显式杀子进程或改用 systemd 托管
进程名后面带 <defunct> 僵尸进程,父进程未回收 杀不掉,需重启其父进程(真正要修的是父程序)
杀掉父进程后子进程仍在跑 Linux 不会级联杀子进程 用 pkill -P <PPID> 按父进程批量清理子进程

查看进程:ps 与 pgrep

是什么

ps 是"拍一张快照",它只显示执行瞬间的状态,不会刷新。它有两大风格,混用是新手最常见的困扰:

风格 写法 来源 特点
BSD 风格 ps aux BSD 传统 选项不带短横线,输出含 %CPU %MEM、命令行完整
System V 风格 ps -ef UNIX 传统 选项带短横线,额外显示 PPID

两者看到的进程完全一样,只是列不同。日常推荐 ps aux(列更实用),需要看 PPID 时用 ps -ef 或 ps -eo。

按名字找进程还有更省事的命令:pgrep 直接输出匹配到的 PID,pkill 直接对它们发信号。

怎么做

# BSD 风格:看全部进程,含资源占用与完整命令行
$ ps aux

# System V 风格:额外带 PPID
$ ps -ef

# 找 nginx 相关进程(注意 grep 自身也会匹配到)
$ ps aux | grep nginx

# 用 pgrep 直接拿到 PID,不会匹配到自己
$ pgrep -f nginx

# 自定义输出列:PID、父进程、启动时长、完整命令
$ ps -o pid,ppid,cmd,etime -p <PID>

# 只看某个用户的所有进程
$ ps -u www-data

[!TIP] ps aux | grep nginx 的结果里总有一条是 grep nginx 自己(因为它也是个进程,命令行里含 "nginx")。想排除它,用 ps aux | grep '[n]ginx',或者干脆用 pgrep -f nginx——后者不会把检索命令本身算进去。

参数说明

参数 作用
a 显示所有用户的进程(不只是自己的)
u 以用户友好的格式显示,含属主、%CPU、%MEM
x 包含没有控制终端的进程(守护进程关键在这)
-e 显示所有进程,等价于 BSD 的 ax
-f 全格式,含 PPID 与完整命令行
-o <列名> 自定义输出列,如 pid,ppid,cmd,etime,user,%cpu,%mem
etime 进程从启动到现在过了多久(比 TIME 的 CPU 时间更直观)
pgrep -f <模式> 按完整命令行匹配(不加 -f 只匹配进程名)
pgrep -u <用户> 只看某用户的进程
pgrep -a 同时打印 PID 和完整命令行

验证方法

$ ps -o pid,ppid,user,%cpu,%mem,etime,cmd -p $$

预期输出:

  PID  PPID USER     %CPU %MEM ELAPSED CMD
 8123  8119 ubuntu    0.0  0.1   02:31 -bash

各列含义:PID、父进程、属主、CPU 占用率、内存占用率、已运行 2 分 31 秒、完整命令。

确认 pgrep 不会匹配到自己:

$ pgrep -f nginx

预期输出:

1042
1043
1044

只列出真正的 nginx 进程 PID,没有多余条目。

常见问题

现象 根因 解决
ps aux 看不到别人的进程 容器/PID 命名空间隔离,只能看到本命名空间 属正常;宿主机上 ps aux 才看得到全部
命令行被截断看不全 默认按终端宽度截短 加 -ww(ps auxww),或用 ps -ef
grep 结果里总有一条多余的 grep 自身进程也被匹配 用 pgrep / grep '[n]ginx'
ps aux 里 STAT 列出现 Z 僵尸进程 见上一节,需处理其父进程
想看进程占用哪个端口 ps 不显示端口 用 ss -lntp 或 lsof -i:<端口>(见常见问题汇总)

实时监控:top 与 htop

是什么

ps 是快照,top 是实时刷新的动态视图(默认每 3 秒)。它的输出分两部分:上方 5 行是系统总览,下方是进程列表。

首部行 含义
第 1 行 当前时间、运行时长、登录用户数、load average(1/5/15 分钟)
第 2 行 Tasks 进程总数及 running / sleeping / stopped / zombie 数量
第 3 行 %Cpu(s) CPU 时间分配:us用户态、sy内核态、id空闲、wa等待 IO、st被虚拟机偷走
第 4 行 MiB Mem 物理内存:总量 / 空闲 / 已用 / 缓存与缓冲
第 5 行 MiB Swap 交换分区:总量 / 空闲 / 已用

[!TIP] 第 3 行的 wa(iowait)高是重要信号:它说明 CPU 在等磁盘或网络 IO 返回,而不是在计算。这时去优化 CPU 是南辕北辙,该查的是磁盘(iostat)和慢查询。

htop 是 top 的增强版:支持鼠标、彩色、横向滚动看完整命令行、直接按树状显示。Ubuntu 22.04 默认没装,需要 sudo apt install htop。

怎么做

# 启动实时监控(按 q 退出)
$ top

# 只监控某个用户的进程
$ top -u www-data

# 只盯着某个进程
$ top -p <PID>

# 安装并使用 htop
$ sudo apt install htop
$ htop

top 运行时的常用交互键:

按键 作用
q 退出
P 按 CPU 占用排序(默认)
M 按内存占用排序
1 展开显示每个 CPU 核心的单独占用率
k 杀进程(会提示输入 PID 和信号)
c 显示完整命令行(不再截断)
H 按线程显示而非进程

参数说明

参数 作用
-d <秒> 刷新间隔,如 top -d 1 每秒刷新
-p <PID> 只看指定进程,多个用逗号分隔
-u <用户> 只看指定用户的进程
-b 批处理模式,输出到标准输出,适合脚本采集(top -b -n 1)
-n <次数> 刷新 N 次后退出,配合 -b 用
-H 显示线程
htop -t 以树状结构显示父子关系
htop -u <用户> 只看某用户的进程
RHEL / Rocky / AlmaLinux 9 对照
Ubuntu (apt) RHEL (dnf) 说明
sudo apt install htop sudo dnf install htop 包名一致,包管理器不同
top(procps-ng) top(procps-ng) 命令与快捷键完全一致
sudo apt install psmisc(pstree) sudo dnf install psmisc 同上

验证方法

取一次性的系统总览(不进入交互界面,适合写进脚本或贴到工单里):

$ top -b -n 1 | head -n 5

预期输出:

top - 14:32:10 up 12 days,  3:41,  2 users,  load average: 0.35, 0.42, 0.38
Tasks: 187 total,   1 running, 186 sleeping,   0 stopped,   0 zombie
%Cpu(s):  3.1 us,  1.2 sy,  0.0 ni, 95.4 id,  0.3 wa,  0.0 hi,  0.0 si,  0.0 st
MiB Mem :   3916.9 total,    512.3 free,   1180.4 used,   2224.2 buff/cache
MiB Swap:   2048.0 total,   2048.0 free,      0.0 used

判读:负载 0.35 很低;zombie 为 0 说明没有僵尸进程;id 95.4% 说明 CPU 基本空闲;wa 0.3% 说明没有 IO 瓶颈;内存里真正被程序用的是 used,buff/cache 那部分内核会自动回收,不算占用。

常见问题

现象 根因 解决
top 里 %CPU 超过 100% 多核机器上按单核归一化,400% = 占满 4 核 属正常;按 1 看每核分别占用
内存看起来几乎用满 Linux 会用空闲内存做磁盘缓存(buff/cache) 看 used 而非 free;缓存可被内核自动回收
htop: command not found 未安装 sudo apt install htop
top 里看不到想找的进程 默认按 CPU 排序,进程在后面 按 c 看完整命令行,或用 top -p <PID>
zombie 数量不为 0 父进程没回收子进程 找到其父进程并重启父进程

理解 load average

是什么

load average 是 top 第一行末尾和 uptime 输出的三个数字,例如 0.35, 0.42, 0.38,分别是过去 1 分钟、5 分钟、15 分钟的系统平均负载。

它的含义常被误解:负载统计的是可运行(正在用 CPU 或排队等 CPU)的进程数 + 不可中断睡眠(等 IO)的进程数,不是 CPU 百分比。

判读的关键是核心数:

场景 判断
单核,负载 1.00 CPU 刚好打满,所有任务无需排队
单核,负载 2.00 有一半进程在排队,明显过载
4 核,负载 4.00 刚好打满(等价于单核的 1.00)
4 核,负载 1.00 只用了约 1/4 算力,很闲

[!IMPORTANT] 负载高 ≠ CPU 高。 如果 top 里 %Cpu 的 wa(iowait)很高而 us/sy 很低,说明进程都卡在等磁盘或网络 IO 上——它们不消耗 CPU,但仍然计入负载。同理,大量进程争抢一把锁(如数据库锁、NFS 挂死)也会让负载飙升而 CPU 空闲。所以看到负载高,第一步是看 wa,而不是急着找 CPU 大户。

三个值的趋势同样重要:1 分钟高而 15 分钟低 = 刚刚开始有压力;1 分钟低而 15 分钟高 = 压力正在缓解;三个都高且持续 = 真的过载了。

怎么做

# 看负载与运行时长
$ uptime

# 确认有几个核(判读负载的分母)
$ nproc

# 更详细的 CPU 信息
$ lscpu | grep -E '^CPU\(s\)|Model name'

# 看 CPU 各状态占比,重点看 wa
$ top -b -n 1 | head -n 3

参数说明

命令 / 参数 作用
uptime 一行显示运行时长、登录数、1/5/15 分钟负载
nproc 打印可用 CPU 核心数——判读负载的分母
lscpu 显示 CPU 架构、核心数、线程数、型号
cat /proc/loadavg 直接读内核暴露的负载值,前三个即 1/5/15 分钟
top 的 wa iowait 占比,判断负载是否来自 IO
top 的 st steal time,虚拟机上被宿主机"偷走"的 CPU,高说明宿主机超卖

验证方法

$ nproc && uptime

预期输出:

4
 14:32:10 up 12 days, 3:41, 2 users, load average: 1.20, 0.90, 0.60

判读:4 核机器,1 分钟负载 1.20 相当于占用了 30% 算力,不算过载;但 1 分钟 > 5 分钟 > 15 分钟,说明压力正在上升,值得继续观察。

$ cat /proc/loadavg

预期输出:

1.20 0.90 0.60 2/187 91234

前三个是 1/5/15 分钟负载,2/187 表示当前可运行进程数 2、总进程数 187,最后是最近创建的 PID。

常见问题

现象 根因 解决
负载很高但 CPU 空闲 进程卡在 IO(wa 高)或争锁 查磁盘 iostat -x 1、查数据库慢查询、查 NFS
负载很低但服务很慢 瓶颈不在 CPU(可能是网络延迟、DNS、连接池) 换个方向排查,别只盯负载
容器里 uptime 显示的负载是宿主机的 容器共享宿主机内核,/proc/loadavg 未被隔离 属正常,看容器的 CPU 配额与用量
负载突然飙升又回落 定时任务(cron)、日志轮转、备份 对比 1/5/15 分钟三个值判断是瞬时还是持续
虚拟机里明明很闲但负载高 st(steal)高,宿主机超卖 联系云厂商或迁移实例

前后台、作业控制与脱离终端

是什么

在终端里直接敲命令,进程在前台运行并独占终端——它不结束你就敲不了下一条命令。在命令末尾加 &,进程就进入后台运行,终端立刻还给你。

但后台进程仍然依附于这个终端(会话)。SSH 断开时,系统会向该会话的所有进程发 SIGHUP(hangup,挂断信号),默认行为就是终止进程。这就是为什么"关掉终端,程序就死了"。

手段 原理 适合场景
& 放到后台,但仍属于当前会话 临时让出终端
Ctrl+Z + bg 先挂起再转后台 命令已经在前台跑起来了才想转后台
nohup 忽略 SIGHUP,且自动把输出重定向到 nohup.out 简单的一次性长任务
setsid 让进程脱离当前会话,成为新会话组长 需要完全脱离终端
tmux / screen 虚拟终端,断开会话也不影响里面的进程 长期运行、还要回去看输出(最推荐)

怎么做

# 后台运行(终端立即可用)
$ long-task &

# 查看当前会话的后台作业
$ jobs

# 把 1 号作业拉回前台 / 让它在后台继续
$ fg %1
$ bg %1

# 挂起当前前台进程:这是键盘操作,直接按 Ctrl+Z 键(不是命令),之后可用 bg 转后台

# 脱离终端运行:忽略 SIGHUP,输出自动写入 nohup.out
$ nohup long-task > task.log 2>&1 &

# 完全脱离当前会话
$ setsid long-task > task.log 2>&1 &

# 最推荐:用 tmux 开一个可随时回来的会话
$ sudo apt install tmux
$ tmux new -s work

[!WARNING] nohup 只解决"不会被 SIGHUP 杀掉",不解决输出无处可去的问题。 如果你写了 nohup cmd & 却没重定向,输出会写进当前目录的 nohup.out——它可能在无人看管的情况下涨到几十 GB 撑爆磁盘。规范写法永远是显式重定向:nohup cmd > /var/log/myapp.log 2>&1 &,并且交给 logrotate 管理(见 54-logrotate.md)。线上长期服务请不要用 nohup,改用 systemd(见 40-systemd.md),它自带重启、日志和资源限制。

在 tmux 会话里,Ctrl+B 然后按 D 可以"脱离"(detach),进程继续跑;之后 tmux attach -t work 可以原样回来。

参数说明

命令 / 参数 作用
& 放到后台执行,$! 拿到它的 PID
Ctrl+Z 向前台进程发 SIGTSTP,挂起(暂停而非终止)
jobs 列出当前会话的后台作业,%1 是作业号
fg %N / bg %N 把 N 号作业拉回前台 / 在后台继续运行
disown %N / disown -h 把作业从 shell 的作业表移除,使其不受本次会话退出影响
nohup 忽略 SIGHUP;未重定向时输出写 nohup.out
setsid 在新会话中启动进程,彻底脱离当前终端
> file 2>&1 标准输出和错误输出都写进 file
tmux new -s <名> 新建一个具名会话
tmux attach -t <名> 重新连回会话

验证方法

后台运行后确认它还在:

$ sleep 300 &
$ jobs -l

预期输出:

[1]+  9128 Running                 sleep 300 &

[1] 是作业号,9128 是 PID,Running 是状态。

验证 nohup 已经忽略 SIGHUP(用 ps 看不出来,要发一个信号试):

$ nohup sleep 600 > /dev/null 2>&1 &
$ kill -HUP $! ; sleep 1 ; ps -p $! -o pid,cmd=

预期输出:

 9347 sleep 600

进程仍然存在,说明 HUP 被忽略了。普通后台任务在收到 HUP 后会立刻消失。

常见问题

现象 根因 解决
SSH 断开后进程就没了 会话退出时内核对该会话发 SIGHUP 用 nohup、setsid,或放进 tmux / systemd
关掉终端后程序"卡住"不退出 进程仍持有终端,写输出时阻塞 启动时就把 stdout/stderr 重定向到文件或 /dev/null
当前目录莫名出现巨大 nohup.out 用了 nohup 却没重定向 显式重定向,并 rm 掉旧文件(注意先确认无人写入)
jobs 看不到之前启动的后台任务 jobs 是 shell 内置,只认当前会话 换个终端就看不到了,用 ps aux | grep 找
disown 之后进程消失了 disown 只是移出作业表,并不阻止 SIGHUP 先 nohup 或 setsid 再考虑 disown

信号与终止进程

是什么

kill 这个名字有误导性——它真正的含义是向进程发一个信号,进程收到后怎么做由它自己决定。信号是进程间最基础的通信方式。

最常用的两个:

信号 编号 默认行为 什么时候用
SIGTERM 15 请求退出,进程可以捕获并做收尾 默认第一选择
SIGKILL 9 内核直接杀死,进程无法捕获、无法拒绝 只有 TERM 无效时才用

SIGTERM 相当于礼貌地请人离开:程序可以保存数据、关闭连接、清理临时文件、通知上游。"服务停不下来"时很多人直接 kill -9,代价是程序没机会做这些事。

其他常见信号:SIGHUP(1) 挂断,很多程序收到它会重新加载配置;SIGINT(2) 就是 Ctrl+C;SIGSTOP(19) 暂停进程(不可被捕获),SIGCONT(18) 继续。

怎么做

# 列出所有信号
$ kill -l

# 优雅停止(默认就是 SIGTERM)
$ kill <PID>

# 只有 TERM 无效时才用 -9
$ kill -9 <PID>

# 让程序重读配置(nginx / sshd 等都支持)
$ sudo kill -HUP <PID>

# 按进程名批量终止
$ killall nginx

# 按完整命令行匹配终止
$ pkill -f "python app.py"

# 先确认会杀到谁(把 pkill 换成 pgrep 预演一遍)
$ pgrep -af "python app.py"

[!WARNING] kill -9 是最后的手段,不是第一选择。 进程收到 SIGKILL 时会被内核立刻抹掉,没有任何机会执行清理代码。对数据库(MySQL、PostgreSQL、Redis)和数据写入类程序,kill -9 很可能造成数据文件损坏、事务丢失、启动时长时间崩溃恢复。正确顺序是:先 kill <PID>(TERM)→ 等 10~30 秒 → 仍无反应再 kill -9。如果是 systemd 托管的服务,一律用 systemctl stop,它会先 TERM 再超时 KILL,并且知道正确的停止流程。

[!WARNING] pkill -f 按完整命令行做正则匹配,非常容易误杀。 例如 pkill -f java 会杀掉机器上所有命令行里含 "java" 的进程——可能包括你不想碰的另一个服务,甚至包括你自己正在跑的部署脚本。执行前请先用 pgrep -af <模式> 把将要被杀的进程完整列出来核对一遍,确认无误后再把 pgrep 换成 pkill。

参数说明

命令 / 参数 作用
kill -l 列出全部信号名与编号
kill <PID> 发 SIGTERM(15),默认信号
kill -9 <PID> / kill -KILL 发 SIGKILL,强制、不可捕获
kill -HUP <PID> 发 SIGHUP(1),多数守护进程用来重读配置
kill -STOP / -CONT <PID> 暂停 / 继续进程(19 / 18)
kill -0 <PID> 不发信号,只探测进程是否存在(写脚本判断存活常用)
killall <名字> 按进程名终止所有匹配进程
pkill -f <模式> 按完整命令行正则匹配终止(危险,见上方警告)
pgrep -af <模式> 安全地预演:-a 打印 PID + 完整命令行
pkill -u <用户> 终止某用户的所有进程(踢人下机常用)

验证方法

先确认目标进程,再终止,最后确认已消失:

$ pgrep -af "sleep 900"

预期输出:

9150 sleep 900
$ kill 9150 ; sleep 1 ; ps -p 9150 -o pid,cmd=

预期输出:

(无输出,进程已退出)

ps -p 找不到该 PID 时输出为空,说明进程确实结束。用 kill -0 探测存活更明确:

$ kill -0 9150 2>/dev/null && echo "还在" || echo "已退出"

预期输出:

已退出

常见问题

现象 根因 解决
kill -9 也杀不掉 进程处于 D 状态(不可中断睡眠,通常卡在磁盘/NFS),或已是僵尸 D 状态只能等 IO 恢复或重启;僵尸进程需杀其父进程
kill: (PID) - Operation not permitted 该进程不属于你,也不是 root 用 sudo kill,或让属主操作
kill 后进程立刻又出现了 被 systemd / supervisor / PM2 自动拉起 要停服务就用 systemctl stop <服务>,别直接 kill
killall: command not found 未装 psmisc sudo apt install psmisc,或用 pkill
误杀了不该杀的进程 用了宽泛的 pkill -f 模式 以后先 pgrep -af 预演;被杀的服务用 systemctl start 恢复
端口被占用,要先找到是哪个进程 需要反查监听者 sudo ss -lntp | grep :<端口> 或 sudo lsof -i:<端口>

进程优先级与按资源定位

是什么

Linux 用 nice 值表示进程的"谦让程度",范围 -20(最优先,抢 CPU)到 +19(最谦让,让别人先跑),默认 0。nice 值越高,分到的 CPU 时间越少。它的意义在于:让备份、日志压缩这类批处理任务别拖垮线上服务。

按资源定位进程则是排错的核心动作——"CPU 飙高""内存爆了"都要能第一时间找出是哪个进程。

怎么做

# 以低优先级启动(+10 表示谦让,不抢线上服务的 CPU)
$ nice -n 10 tar czf backup.tgz /data

# 调整已在运行的进程的优先级
$ sudo renice -n 10 -p <PID>

# 按内存占用找前 5 名
$ ps aux --sort=-%mem | head -n 6

# 按 CPU 占用找前 5 名
$ ps aux --sort=-%cpu | head -n 6

# 查看进程的 nice 值(NI 列)
$ ps -o pid,ni,cmd -p <PID>

[!NOTE] 调低 nice 值(提高到 -20 这样的负优先级)需要 root 权限,普通用户只能把自己的进程越调越谦让(0 → 19)。这是防止普通用户抢占系统资源的设计。

参数说明

命令 / 参数 作用
nice -n <值> <命令> 以指定 nice 值启动进程,默认 10
renice -n <值> -p <PID> 改运行中进程的 nice 值
renice -n <值> -u <用户> 改某用户所有进程的 nice 值
ps --sort=-%mem 按内存降序(- 表示降序)
ps --sort=-%cpu 按 CPU 降序
ps -o ni / top 的 NI 列 查看 nice 值
head -n 6 取 5 条进程 ——多出的 1 行是 ps 的表头
top -b -n 1 -o %MEM 批处理模式下按内存排序,适合脚本
systemd 的 Nice= unit 里配置服务的固定优先级

验证方法

$ ps aux --sort=-%mem | head -n 3

预期输出:

USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
mysql     1042  2.1 18.4 1758236 721356 ?      Ssl  Sep12  45:12 /usr/sbin/mysqld
www-data  2311  0.3  4.2  238412 165204 ?      S    Sep12   3:04 nginx: worker process

第一列之后看 %MEM:mysqld 占了 18.4% 内存(RSS 721356 KB ≈ 704 MB),是本机内存大户。

验证 nice 值生效:

$ nice -n 10 sleep 600 &
$ ps -o pid,ni,cmd -p $!

预期输出:

  PID  NI CMD
 9180  10 sleep 600

NI 列为 10,说明该进程以谦让模式运行,会主动把 CPU 让给更重要的进程。

常见问题

现象 根因 解决
renice: failed to set priority 普通用户不能提高优先级(调低 nice 值) 加 sudo
明明有进程占内存,%MEM 却不高 机器内存很大,或看的是 VSZ(虚拟内存)而非 RSS 看 RSS 列(实际物理内存)
找不到 CPU 高的进程但负载很高 负载来自 IO(wa 高),不是 CPU 查 iostat -x 1 和 iotop
ps --sort 排序结果不对 列名要带 % 且区分大小写 用 --sort=-%cpu / --sort=-rss
想限制进程最多用多少 CPU / 内存 nice 只是相对权重,不是硬限制 用 systemd 的 CPUQuota=、MemoryMax=(见 40-systemd.md)
负载高,不知从何入手 可能来自 CPU、IO、内存三个方向 先看 top 的 %Cpu 与 wa,再决定查 ps --sort 还是 iostat

[!TIP] 负载高时的三步入手法:① 看 top 的 wa ——高就是 IO 问题,去查磁盘和慢查询;② wa 不高则按 P 按 CPU 排序找出 CPU 大户;③ 内存紧张时按 M 排序,并留意 swap 是否被用到(用到说明物理内存不足,性能会断崖式下降)。

本章命令速查

命令 作用
pstree -p 树状查看进程父子关系
ps aux / ps -ef 查看全部进程(BSD 风格 / System V 风格)
ps -o pid,ppid,cmd,etime -p <PID> 自定义列查看进程与运行时长
pgrep -af <模式> 按完整命令行查找并打印 PID + 命令(杀进程前预演)
top / htop 实时监控(后者需安装,交互更友好)
top -b -n 1 | head -n 5 取一次系统总览,适合写进工单
uptime / nproc 看负载 / 看核心数(判读负载的分母)
命令 & / jobs 后台运行 / 查看后台作业
fg %1 / bg %1 / disown 拉回前台 / 后台继续 / 移出作业表
nohup cmd > log 2>&1 & 忽略 SIGHUP 并重定向输出,断网也不停
setsid cmd > log 2>&1 & 彻底脱离当前会话
tmux new -s <名> / attach -t 建可重连的虚拟终端会话(长期任务推荐)
kill -l 列出所有信号
kill <PID> / kill -9 <PID> 优雅终止(TERM)/ 强制杀死(KILL,最后手段)
kill -HUP <PID> 让守护进程重读配置
kill -0 <PID> 只探测进程是否存活,不发信号
pkill -f <模式> / killall <名> 按命令行 / 进程名批量终止(先 pgrep -af 预演)
nice -n 10 <命令> / renice -n 10 -p <PID> 以低优先级启动 / 调整运行中进程优先级
ps aux --sort=-%cpu | head -n 6 找 CPU 占用最高的 5 个进程
ps aux --sort=-%mem | head -n 6 找内存占用最高的 5 个进程

相关章节

📄 网络配置 15-network-configuration.md


title: 15 网络配置 updated: 2026-09 applies-to: Ubuntu 22.04 LTS

服务器「连不上」是新手遇到频率最高、也最容易慌的问题。本章先给你一套从近到远的排错思路,再逐层讲清楚:怎么看 IP、怎么看监听端口、怎么测连通性、DNS 和主机名是怎么回事、怎么配静态 IP、为什么时间必须准。学完本章,面对「服务器不通」你不再靠猜,而是按层排查。

[!NOTE] 本章版本号截至 2026 年 9 月。版本信息的唯一权威来源是 版本速查表,冲突时以速查表为准。

排错思路:从近到远四层

是什么

网络排错的核心思想是:问题一定出在从本机到目标之间的某一段路上,逐段确认就能定位。就像家里停水时,你会先看自家水龙头,再看小区是否停水,而不是直接打电话问自来水厂。服务器排错同理,按「离自己由近到远」的顺序检查四层,每一层都有对应的命令。

怎么做

先记住这张排查路线表,后面小节会逐条展开:

层 确认什么 命令 通了说明
1. 本机 IP 网卡有没有拿到地址 ip a 本机网络配置存在
2. 网关 能不能出去(ping 网关 / 公网 IP) ping -c 4 192.168.1.1 本机到出口的链路正常
3. DNS 域名能不能解析成 IP dig example.com 名字解析正常
4. 目标端口 对方服务端口通不通 curl -I http://<服务器IP>:80 端到端全部正常

排错时从上往下走,在哪一层断掉,问题就在哪一层:

# 第 1 层:本机有 IP 吗?
$ ip -br a
# 第 2 层:网关通吗?(网关地址从 ip r 里看)
$ ip r
$ ping -c 4 192.168.1.1
# 第 3 层:DNS 正常吗?直接 ping 一个公网 IP 对比
$ ping -c 4 223.5.5.5
$ dig example.com
# 第 4 层:目标端口通吗?
$ curl -I http://example.com

参数说明

  • 为什么要分层次:新手最常见的错误是「curl 不通就重启服务器」。分层的意义在于把一个大问题(连不上)拆成四个小问题,每个小问题都有明确的是/否答案,定位效率完全不同。
  • 第 2 层 ping 公网 IP(如 223.5.5.5)是关键对照实验:如果 ping IP 通、ping 域名不通,问题几乎可以锁定在 DNS,而不是网络断了。
  • 第 4 层要分清「网络不通」和「服务没起」:能 ping 通但端口不通,多数情况不是网络问题,而是目标机器上服务没监听或防火墙拦截。

验证方法

按顺序执行四层检查,正常情况下每层都应有响应:

# 第 1 层:确认本机有 IP
$ ip -br a
预期输出:
lo               UNKNOWN        127.0.0.1/8 ::1/128
ens33            UP             192.168.1.10/24 fe80::20c:29ff:fe8a:1234/64

常见问题

现象 根因 解决
不知道从哪查起,乱试命令 没有排查框架 严格按本节路线表从第 1 层走到第 4 层,断在哪层查哪层
ping 网关都不通 网线/虚拟网络/云安全组问题 检查虚拟机网络模式(NAT/桥接)或云控制台安全组
四层都通但应用还是连不上 应用层问题(配置错端口、认证失败) 看应用日志,网络问题已排除

查看网卡地址与路由

是什么

ip 命令是 Linux 当代标准的网络查看/配置工具,属于 iproute2 包,Ubuntu 默认自带。老教程里的 ifconfig 属于 net-tools 包,已停止维护多年,Ubuntu 默认根本没装。看到教程让你 ifconfig,直接换成 ip a 即可,功能对应关系本章会给出。

怎么做

# 查看所有网卡的详细地址信息(最常用)
$ ip a
# 简洁视图:一行一块网卡,日常排错更快
$ ip -br a
# 查看路由表,default 那一行就是默认网关
$ ip r

ifconfig → ip 的常用对应关系:

老命令(net-tools,过时) 新命令(iproute2,推荐)
ifconfig ip a
route -n ip r

参数说明

  • ip -br a 里的 -br 是 --brief(简洁),只显示网卡名、状态、地址三列,适合快速扫一眼。
  • ip r 输出里 default via 192.168.1.1 表示:去往任何未知网络的数据包,都先交给 192.168.1.1(网关)转发。网关地址就是你的「出口大门」,排错第 2 层 ping 的就是它。
  • 网卡名在新系统上叫 ens33、enp0s3、eth0 等,不再统一叫 eth0,写配置时以 ip a 看到的为准。

验证方法

# 简洁视图确认网卡和 IP;再看默认网关
$ ip -br a
$ ip r
预期输出:
lo               UNKNOWN        127.0.0.1/8 ::1/128
ens33            UP             192.168.1.10/24
default via 192.168.1.1 dev ens33 proto dhcp src 192.168.1.10 metric 100
192.168.1.0/24 dev ens33 proto kernel scope link src 192.168.1.10

default via 192.168.1.1 就是默认网关,排错第 2 层要 ping 的就是它。

常见问题

现象 根因 解决
ifconfig: command not found net-tools 已被淘汰,默认不装 用 ip a,不要安装过时包
ip a 里网卡显示 DOWN 且无 IP 网卡没起来(驱动/虚拟网络问题) sudo ip link set 网卡名 up 后观察,仍不行查虚拟网络配置
只有 lo,看不到 ens33 虚拟机网络适配器未连接 / 云主机网卡异常 VMware/VirtualBox 检查「已连接」选项;云主机在控制台查看网卡状态
RHEL / Rocky / AlmaLinux 9 对照
Ubuntu (apt/ufw) RHEL (dnf/firewalld) 说明
ip a / ip r(自带) ip a / ip r(自带) iproute2 两边一致,命令通用
网卡名常见 ens33 常见 ens160、enp1s0 命名规则相同,以实际输出为准
网络由 netplan 管理 网络由 NetworkManager 管理 改 IP 的方式不同,见本章 netplan 一节的对照块

查看连接与监听端口

是什么

ss(socket statistics)是当代查看网络连接的标准工具,取代了老教程里的 netstat。它回答两个高频问题:「我的服务到底监听了哪个端口、监听在哪个地址上」和「现在有哪些连接连进来了」。判断「服务没起」还是「防火墙挡了」,全靠它。

怎么做

# 查看所有 TCP 监听端口,并显示是哪个进程在监听(最常用)
$ sudo ss -lntp
# 汇总统计:各类 socket 数量一览
$ ss -s
# 只看已建立的连接(排查谁在连我 / 我连着谁)
$ sudo ss -tnp | grep ESTAB

netstat → ss 一一对应:netstat -lntp 即 ss -lntp,netstat -an 即 ss -tan,netstat -s 即 ss -s。看到老教程里的 netstat 命令,直接替换成 ss 即可。

参数说明

ss -lntp 四个字母拆开记:-l 只显示 listening(正在监听)的 socket;-n 不解析域名和端口名,直接显示数字(快,且不受 DNS 故障影响);-t 只看 tcp;-p 显示占用端口的process(进程名和 PID),需要 sudo 才能看到全部。

输出里监听地址是关键:127.0.0.1:3306 表示只有本机能连;0.0.0.0:3306 表示所有网卡都能连。后面「判断本机端口为什么不通」一节全靠这个区别。

验证方法

# 查看 TCP 监听端口及所属进程
$ sudo ss -lntp
预期输出:
State   Recv-Q  Send-Q   Local Address:Port   Peer Address:Port  Process
LISTEN  0       4096     127.0.0.53:53        0.0.0.0:*          users:(("systemd-resolve",pid=512,fd=14))
LISTEN  0       4096     0.0.0.0:22           0.0.0.0:*          users:(("sshd",pid=701,fd=3))
LISTEN  0       151      127.0.0.1:3306       0.0.0.0:*          users:(("mysqld",pid=1204,fd=28))

上面输出能直接读出三条信息:本机 DNS 由 systemd-resolved 监听在 127.0.0.53;SSH 对全网开放(0.0.0.0:22);MySQL 只监听 127.0.0.1,外部连不上是设计如此,不是故障。

常见问题

现象 根因 解决
ss -lntp 不加 sudo 看不到进程名 -p 需要权限读取其他用户的进程信息 加 sudo
找不到 netstat 新系统默认不装 net-tools 一律改用 ss,参数几乎一一对应
服务明明起了,ss 里却搜不到 端口号记错了、服务其实没在监听,或监听在别的地址/端口上 先不筛选,直接 ss -lntp 全量看一遍再核对(ss -t 本身就同时列出 IPv4 与 IPv6 监听)

连通性测试:ping / traceroute / curl

是什么

确认了本机 IP 和监听状态后,就该测试「路通不通」。ping 测最基本的可达性,traceroute/mtr 看路径上断在哪一跳,curl 直接测 HTTP 服务(Web 排错的终极大招),wget 用来下载文件顺带验证网络。

怎么做

# ping 4 次就停(-c 指定次数,不加会一直 ping 下去)
$ ping -c 4 223.5.5.5
# 查看路由路径,每一跳到达哪个路由器
$ traceroute 223.5.5.5
# mtr = ping + traceroute 合体,实时刷新每跳丢包率,更好用
$ mtr 223.5.5.5
# 只看 HTTP 响应头:状态码 200 说明服务正常
$ curl -I http://example.com
# 显示详细过程:DNS 解析、TCP 连接、TLS 握手、请求响应全流程
$ curl -v https://example.com
# 下载文件,顺带验证网络与 DNS 都正常
$ wget https://example.com/file.tar.gz

traceroute 和 mtr 在 Ubuntu 最小安装里默认没有,需要安装:

# 安装路由追踪工具
$ sudo apt install traceroute mtr-tiny

参数说明

  • ping -c 4:务必加 -c。裸 ping 会永远执行下去,新手常以为终端卡死。Ctrl+C 可随时中断。
  • curl -I:-I(大写 i)表示只要响应头(HEAD 请求)。看状态码就够了:200 正常、404 路径错、502 后端挂了、connection refused 端口没开。
  • curl -v:verbose 模式。当 -I 显示结果异常时,用 -v 看卡在了哪一步:卡在 DNS 说明解析问题,卡在 TCP connect 说明端口不通,卡在 TLS handshake 多半是证书或时间问题。
  • mtr 比 traceroute 强在持续统计:单次 traceroute 某一跳超时可能只是路由器不响应 ping,不代表断;mtr 跑一分钟看丢包率才可信。

验证方法

# 测试到公网 DNS 的连通性
$ ping -c 4 223.5.5.5
# 只看响应头判断 HTTP 服务是否正常
$ curl -I http://example.com
预期输出:
PING 223.5.5.5 (223.5.5.5) 56(84) bytes of data.
64 bytes from 223.5.5.5: icmp_seq=1 ttl=118 time=8.32 ms
64 bytes from 223.5.5.5: icmp_seq=2 ttl=118 time=8.11 ms
64 bytes from 223.5.5.5: icmp_seq=3 ttl=118 time=8.25 ms
64 bytes from 223.5.5.5: icmp_seq=4 ttl=118 time=8.19 ms

--- 223.5.5.5 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3005ms
HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8

ping 的 0% packet loss 说明链路健康;curl 返回 HTTP/1.1 200 OK 说明服务正常。

常见问题

现象 根因 解决
ping 域名失败,ping IP 成功 DNS 问题 跳到本章「DNS 解析」一节
curl 报 Connection refused 目标端口没有服务在监听 去 4 层排查第 5 步:ss -lntp 看服务是否监听
curl 卡在 TLS 握手后无响应 服务器时间错误导致证书校验失败 timedatectl 检查时间,见本章时间同步一节

判断本机端口为什么不通(重点)

是什么

这是排错实战中出现率最高的场景:你从自己电脑 curl http://<服务器IP>:8080 连不上,但服务明明装了。原因只有三种可能,本节教你用两条命令把三种情况彻底区分开,不再靠猜。

怎么做

先在服务器本机上测试(这一步必须在服务器上做,不要在自己电脑上):

# 测试本机 8080 端口是否有服务在响应
$ curl localhost:8080
# 用 netcat 精确测试端口连通性(-z 只扫端口不发数据,-v 显示过程)
$ nc -zv 127.0.0.1 8080
# 关键一步:看服务到底监听在哪个地址上
$ sudo ss -lntp | grep 8080

nc(netcat)在最小安装里同样需要安装:

# 安装 netcat
$ sudo apt install netcat-openbsd

三种情况的对照表(先在服务器本机测,再在自己电脑测,对比结果):

服务器本机 curl localhost 自己电脑 curl <服务器IP> ss -lntp 监听地址 结论
不通 不通 搜不到 8080 服务没起:去查服务状态和日志
通 不通 127.0.0.1:8080 只监听了本机回环:改服务配置绑到 0.0.0.0
通 不通 0.0.0.0:8080 防火墙/安全组拦截:查 ufw 和云安全组

参数说明

  • nc -zv:-z 是 zero-I/O 模式,只探测端口开不开,不发送应用数据;-v 把结果打印出来。成功时输出 succeeded!,失败输出 refused 或超时。
  • 为什么只监听 127.0.0.1 会连不上:127.0.0.1 是「本机回环」地址,只有服务器自己能访问。很多服务(MySQL、Redis)出于安全默认只听 127.0.0.1,这是故意的保护机制,改成 0.0.0.0 之前必须想清楚是否要暴露给全网。
  • 为什么 0.0.0.0 还连不上:数据包到得了服务器,但被防火墙(ufw)或云平台安全组丢弃。云服务器还要特别注意控制台里的安全组,它独立于系统内的 ufw。

验证方法

# 在服务器本机验证端口是否开放,并确认监听地址
$ nc -zv 127.0.0.1 8080
$ sudo ss -lntp | grep 8080
预期输出:
Connection to 127.0.0.1 8080 port [tcp/http-alt] succeeded!
LISTEN  0  511  0.0.0.0:8080  0.0.0.0:*  users:(("myapp",pid=2301,fd=6))

nc 返回 succeeded、ss 显示监听在 0.0.0.0:8080,对应对照表第三行:如果外部仍连不上,就去查 ufw 和云安全组。

常见问题

现象 根因 解决
nc 报 Connection refused 端口上没有进程监听 ss -lntp 确认服务是否启动、端口是否写对
本机通、外部不通、ss 显示 0.0.0.0 ufw 或云安全组未放行 sudo ufw status 查看;云控制台检查安全组规则
外部偶尔能连上偶尔超时 安全组只放行了部分来源 IP 检查安全组授权对象是否包含你的出口 IP

DNS 解析

是什么

域名(如 example.com)和 IP 地址(如 93.184.216.34)之间的翻译工作由 DNS 完成。Ubuntu 22.04 用 systemd-resolved 服务统一管理 DNS:应用程序的解析请求先交给它,它再转发给真正的上游 DNS 服务器。理解这个架构,才能解释本节那个著名的坑。

怎么做

# 安装 DNS 诊断工具(dig 和 nslookup 都在这个包里)
$ sudo apt install dnsutils
# 用 dig 查询域名对应的 IP(推荐,输出信息全)
$ dig example.com
# nslookup 更简单直观,适合快速确认
$ nslookup example.com
# 查看 systemd-resolved 的整体状态:当前用的哪个上游 DNS
$ resolvectl status
# 查看真实的 DNS 配置文件(注意它是个软链接)
$ cat /etc/resolv.conf

[!WARNING] 不要直接编辑 /etc/resolv.conf 来「永久改 DNS」。 Ubuntu 22.04 上这个文件由 systemd-resolved 管理,是软链接,你的修改会在重启、systemd-resolved 重载或 DHCP 续租时被静默覆盖回去。要持久修改 DNS,正确做法是改 netplan 的 nameservers(见本章静态 IP 一节)或 resolvectl dns 网卡名 <DNS-IP>(临时生效)。

参数说明

  • dig 输出重点看两处:ANSWER SECTION 里的解析结果,和末尾 Query time(解析耗时)。status: NXDOMAIN 表示域名不存在,status: SERVFAIL 多为上游 DNS 故障。
  • /etc/resolv.conf 里的 nameserver 127.0.0.53 就是 systemd-resolved 的本机代理地址,它不是真正的 DNS 服务器,真正的上游用 resolvectl status 查看。
  • 常用公共 DNS:阿里 223.5.5.5、腾讯 119.29.29.29、谷歌 8.8.8.8(国内不稳)。

验证方法

# 验证 DNS 解析是否正常
$ dig example.com
预期输出:
; <<>> DiG 9.18.12 <<>> example.com
;; ANSWER SECTION:
example.com.    86400   IN  A   93.184.216.34

;; Query time: 25 msec
;; SERVER: 127.0.0.53#53(127.0.0.53)
;; status: NOERROR

SERVER: 127.0.0.53 证明请求走了 systemd-resolved,NOERROR + ANSWER 记录说明解析成功。

常见问题

现象 根因 解决
改了 resolv.conf 过一会又变回去 systemd-resolved 覆盖了手改内容 改 netplan 的 nameservers 后 sudo netplan apply
dig 报 SERVFAIL 或超时 上游 DNS 不可用 resolvectl status 看上游;临时换 resolvectl dns ens33 223.5.5.5
域名解析到内网假 IP 运营商 DNS 劫持 换可信公共 DNS(223.5.5.5 / 119.29.29.29)

主机名与 /etc/hosts

是什么

主机名是机器的「名字」(如 web-01),日志、提示符、集群里互相识别都用它。/etc/hosts 则是一张本地手工解析表:系统查域名时先查它,查不到才去问 DNS。这个优先级是理解很多诡异地名问题的关键。

怎么做

# 查看当前主机名与系统信息
$ hostnamectl
# 设置新主机名(把 <新主机名> 换成你想要的,如 web-01)
$ sudo hostnamectl set-hostname <新主机名>
# 编辑本地解析表,给新主机名配上 127.0.1.1,避免 sudo 报「无法解析主机」
$ sudo nano /etc/hosts

/etc/hosts 的典型内容:

127.0.0.1   localhost
127.0.1.1   web-01
192.168.1.20  db-01.internal

设置完主机名后,务必同步修改 /etc/hosts 里的 127.0.1.1 那行,否则每次执行 sudo 都会慢一下并报 sudo: unable to resolve host。

参数说明

  • 127.0.1.1 是 Debian/Ubuntu 系的习惯用法:给本机主机名一个回环地址,与 127.0.0.1(localhost)区分开。
  • 解析优先级:/etc/hosts > DNS。所以把 192.168.1.20 db-01.internal 写进 hosts 后,即使没有内网 DNS,ping db-01.internal 也能通。内网机器少时,用 hosts 管理互相访问是最简单的方案。
  • 主机名命名建议:小写字母、数字、连字符,带上用途和编号(web-01、db-01),多台服务器时日志里一眼可辨。

验证方法

# 确认主机名已生效,并验证 hosts 本地解析(不走 DNS,立即返回)
$ hostnamectl
$ ping -c 2 web-01
预期输出:
 Static hostname: web-01
       Icon name: computer-vm
 Operating System: Ubuntu 22.04.5 LTS
PING web-01 (127.0.1.1) 56(84) bytes of data.
64 bytes from web-01 (127.0.1.1): icmp_seq=1 ttl=64 time=0.021 ms

常见问题

现象 根因 解决
改完主机名,sudo 报 unable to resolve host /etc/hosts 里没有新主机名的记录 把 127.0.1.1 <新主机名> 加进 /etc/hosts
提示符还显示旧主机名 当前 shell 是改之前开的 重新登录或执行 bash 刷新
域名指向了意料之外的 IP /etc/hosts 里有历史残留记录 grep 域名 /etc/hosts 检查并删除过期条目

静态 IP 配置(netplan)

是什么

Ubuntu Server 从 17.10 起用 netplan 管理网络配置:你写 YAML 描述「想要什么网络」,netplan 把它翻译给底层的 systemd-networkd 执行。配置文件在 /etc/netplan/ 目录下,Server ISO 安装的机器默认是 00-installer-config.yaml;云服务器(cloud-init 镜像)上常见的则是 /etc/netplan/50-cloud-init.yaml,由 cloud-init 生成,改前先 ls /etc/netplan/ 看清自己机器上是哪个。DHCP 是「自动获取 IP」,静态 IP 是「手动指定固定地址」——服务器通常必须用静态 IP,否则重启后 IP 一变,所有人都连不上它了。

怎么做

先看清现状:

# 查看当前 netplan 配置(DHCP 模式长这样)
$ cat /etc/netplan/00-installer-config.yaml
network:
  version: 2
  renderer: networkd
  ethernets:
    ens33:
      dhcp4: true

改成静态 IP 的完整写法(按你的实际环境替换地址、网卡名和网关):

network:
  version: 2
  renderer: networkd
  ethernets:
    ens33:
      dhcp4: false
      addresses:
        - 192.168.1.10/24
      routes:
        - to: default
          via: 192.168.1.1
      nameservers:
        addresses: [223.5.5.5, 119.29.29.29]
# 应用配置(立即生效)
$ sudo netplan apply
# 更稳妥的方式:90 秒内不确认则自动回滚,适合改远程服务器
$ sudo netplan try

[!WARNING] YAML 对缩进极其敏感。 缩进必须用空格(不能有 Tab),同一层级的缩进格数必须一致。一个空格的差错都会导致 netplan apply 直接报错或配置不生效。改完可先用 sudo netplan --debug apply 看详细报错。

[!WARNING] 修改远程服务器的 IP 配置前,必须先想好回退方案,否则改错一步就永久失联,SSH 再也连不上,只能靠云控制台的 VNC/串口登录进去救。动手前确认三件事:① 新 IP、网关、掩码与所在网络完全匹配(最好先在本地虚拟机演练一遍);② 备份原文件 sudo cp /etc/netplan/00-installer-config.yaml /etc/netplan/00-installer-config.yaml.bak;③ 优先用 sudo netplan try——它会应用新配置并等待你确认,你不敲确认键就自动回滚旧配置,是远程改 IP 的救命机制。云服务器失联时的最后手段是控制台里的 VNC 登录。

参数说明

字段 含义 注意
addresses: 192.168.1.10/24 IP + CIDR 掩码,/24 等价于 255.255.255.0 掩码写错会导致连不通网段
routes: to: default / via: 默认网关 老写法 gateway4: 已废弃,新配置用 routes
nameservers: addresses: DNS 服务器列表 持久改 DNS 的正确位置,见 DNS 一节的 WARNING
dhcp4: false 关闭 IPv4 自动获取 静态 IP 的前提;静态地址必须避开 DHCP 分配范围,防止 IP 冲突

验证方法

# 确认新 IP、网关、DNS 都已生效
$ ip -br a && ip r
$ resolvectl status ens33
预期输出:
lo               UNKNOWN        127.0.0.1/8 ::1/128
ens33            UP             192.168.1.10/24
default via 192.168.1.1 dev ens33
Link 2 (ens33)
    DNS Servers: 223.5.5.5 119.29.29.29

常见问题

现象 根因 解决
netplan apply 报 YAML 语法错误 缩进用了 Tab 或层级错位 全部改空格,对照本节示例逐行核对缩进
改完 netplan 后 SSH 失联 IP/网关写错 云控制台 VNC 登录,还原 .bak 备份后 sudo netplan apply
静态 IP 生效但上不了外网 网关或 DNS 没配对 ip r 查 default 路由,resolvectl status 查 DNS
RHEL / Rocky / AlmaLinux 9 对照
Ubuntu (netplan) RHEL (NetworkManager/nmcli) 说明
配置文件 /etc/netplan/*.yaml nmcli con mod 命令或 /etc/NetworkManager/system-connections/*.nmconnection RHEL 没有 netplan,配置方式完全不同
sudo netplan apply nmcli con up ens160 或 nmcli dev reapply ens160 应用配置
sudo netplan try 无直接等价;可用 nmcli + 保留旧会话防失联 RHEL 远程改 IP 前同样要备好 VNC 退路
YAML 里写 nameservers nmcli con mod ens160 ipv4.dns "223.5.5.5" 持久改 DNS

时间同步

是什么

服务器时间不准不是小事,它会直接弄坏服务:

  • HTTPS 证书校验依赖时间:证书有生效/过期时间,系统时间偏差过大(比如差几年),浏览器和 curl 会直接报证书错误,服务之间互相调用全部失败。
  • 日志时间戳失去意义:排查问题时你要按时间对齐多台机器的日志,时间不准就无法定位故障顺序。
  • cron 定时任务会错乱:任务在错误的时间执行或不执行。
  • 分布式锁与令牌:很多鉴权机制(JWT 过期校验、分布式锁租期)都基于时间戳,多机时间不一致会造成灵异故障。

所以每台服务器装完系统的第一件事之一,就是配置自动时间同步。Ubuntu 22.04 的标准方案是 timedatectl(查看/设置时区)+ chrony(持续校准时间)。

怎么做

# 先看当前时间、时区、是否已开启 NTP 同步
$ timedatectl
# 设置时区为上海(北京时间,UTC+8)
$ sudo timedatectl set-timezone Asia/Shanghai
# 安装 chrony(Ubuntu 22.04 也可用系统自带的 systemd-timesyncd,chrony 精度与可控性更好)
$ sudo apt install chrony
# 启动并设置开机自启
$ sudo systemctl enable --now chrony

国内服务器建议把 chrony 的上游换成国内 NTP 源,编辑 /etc/chrony/chrony.conf,把默认 pool 行替换为:

pool ntp.aliyun.com iburst
pool ntp.tencent.com iburst
# 修改配置后重启服务
$ sudo systemctl restart chrony

参数说明

  • timedatectl 输出里最关键的两行:Time zone(应为 Asia/Shanghai)和 System clock synchronized: yes(表示已同步成功)。
  • chrony 配置里的 iburst 表示启动时先快速连续发几个探测包,加快首次同步速度。
  • 日志时区的两种口径:要么所有服务器统一用 UTC(日志记 UTC、展示时再换算),要么统一用本地时区(如本章演示的 Asia/Shanghai,日志时间戳即北京时间)。两种都可行,关键是团队统一一种口径即可,不要混用;否则对日志时会把 09:00 和 01:00 当成两件事。
  • chrony 与 systemd-timesyncd 二选一即可,同时开两个会互相抢时钟。

验证方法

# 确认时区与同步状态;再查看 chrony 的上游时间源
$ timedatectl
$ chronyc sources
预期输出:
               Local time: Tue 2026-09-29 10:30:12 CST
           Universal time: Tue 2026-09-29 02:30:12 UTC
                 Time zone: Asia/Shanghai (CST, +0800)
System clock synchronized: yes
       NTP service: active
MS Name/IP address         Stratum Poll Reach LastRx Last sample
===============================================================================
^* 203.107.6.88                  2   6   377    45    +12us[ +15us] +/- 18ms
^+ 42.192.0.10                   2   6   377    44    -28us[ -25us] +/- 24ms

System clock synchronized: yes 表示已同步成功;chronyc sources 里 ^* 标记的行是当前正在使用的时间源,Reach 达到 377 表示通信稳定,^? 则表示该源不可达。

常见问题

现象 根因 解决
curl 报证书错误但证书没过期 系统时间偏差过大 timedatectl 检查时间,配置 chrony 同步
chronyc sources 全是 ^? 防火墙拦了 NTP(UDP 123)或上游不可达 放行出站 UDP 123;换国内 NTP 源
双系统/克隆出来的虚拟机时间漂移大 虚拟机时钟与宿主机不同步 装 chrony 后执行 sudo chronyc makestep 强制立即校准

本章命令速查

命令 作用
ip a 查看所有网卡详细地址
ip -br a 简洁视图:网卡名、状态、IP 一行看清
ip r 查看路由表与默认网关
sudo ss -lntp 查看 TCP 监听端口及所属进程
ss -s socket 数量汇总统计
sudo ss -tnp | grep ESTAB 查看已建立的连接
ping -c 4 <IP> 测试连通性,4 次后自动停止
traceroute <IP> 查看到目标经过的每一跳
mtr <IP> 实时统计每跳丢包率(ping+traceroute)
curl -I <URL> 只看 HTTP 响应头,快速判断服务状态
curl -v <URL> 显示完整请求过程,定位卡在哪一步
nc -zv <IP> <端口> 精确测试某端口是否开放
dig <域名> 查询 DNS 解析结果与耗时
resolvectl status 查看 systemd-resolved 与上游 DNS
sudo hostnamectl set-hostname <名> 设置主机名
sudo netplan try 应用 netplan 配置,不确认自动回滚
sudo netplan apply 应用 netplan 配置(立即生效)
timedatectl 查看时间、时区、同步状态
sudo timedatectl set-timezone Asia/Shanghai 设置时区为北京时间
chronyc sources 查看 chrony 上游时间源状态

相关章节

📄 16 防火墙与网络安全 16-firewall.md


title: 16 防火墙与网络安全 updated: 2026-09 applies-to: Ubuntu 22.04 LTS

服务器一旦有公网 IP,从上线第一分钟起就会有扫描器试探 22、3306、6379 这些常见端口。防火墙就是你和它们之间的门。

[!NOTE] 本章版本号截至 2026 年 9 月。版本信息的唯一权威来源是 版本速查表,冲突时以速查表为准。

新手最常见的误解是:以为「配了 ufw 放行 8080」就等于「外网能访问 8080」。实际上请求要到达服务,必须连续通过三层关卡:

   客户端请求
        │
        ▼
  ┌───────────────────────────────┐
  │ 第 1 层 云安全组(云控制台)    │ 被挡:Connection timed out
  ├───────────────────────────────┤ 放行 ↓
  │ 第 2 层 主机防火墙 ufw         │ 被挡:timed out
  ├───────────────────────────────┤ 放行 ↓
  │ 第 3 层 进程监听地址           │ 被挡:Connection refused
  │ 0.0.0.0:8080 对外             │
  │ 127.0.0.1:8080 只本机         │
  └───────────────────────────────┘
        │ 放行
        ▼
     服务响应

[!IMPORTANT] 三层是「与」的关系,不是「或」。 三层全通才能连上,任一层挡住都不行。


三层模型与端口排查顺序

是什么

层 在哪配置 谁负责 典型拦截表现
云安全组 云厂商控制台 云平台,流量到网卡前就过滤 Connection timed out
主机防火墙 服务器内 ufw / firewalld 你自己,内核 netfilter 过滤 Connection timed out
进程监听地址 应用配置(bind / listen) 应用程序自己 Connection refused

理解这个模型的价值:「端口不通」不再是玄学,而是一个可穷举的清单。绝大多数「明明开了防火墙还是访问不了」属于三种之一:安全组没开(包根本没进机房)、ufw 已启用但没放行该端口、程序只监听了 127.0.0.1。

怎么做

[!IMPORTANT] 端口不通时严格按以下 4 步执行,每步排除一类原因。没走完 4 步之前不要改任何配置。

# 1. 进程是否真的在监听?注意地址是 127.0.0.1 还是 0.0.0.0
$ sudo ss -lntp | grep 8080

# 2. 本机自测,确认服务本身能响应
$ curl localhost:8080

# 3. 主机防火墙是否放行
$ sudo ufw status

# 4. 云安全组是否放行 —— 登录云控制台查看,服务器内查不到
步骤 1 步骤 2 步骤 3 结论
无输出 — — 进程没起,先查服务(40)
127.0.0.1 通 任意 监听地址不对,改绑 0.0.0.0
0.0.0.0 不通 任意 服务本身有问题,看应用日志
0.0.0.0 通 未放行 补 sudo ufw allow 8080
0.0.0.0 通 已放行 去查云安全组,九成问题在这

参数说明

参数 全称 作用
-l listening 只显示监听中的套接字
-n numeric 不解析域名和服务名(否则显示 http 而非 80)
-t tcp 只看 TCP(看 UDP 换成 -u)
-p process 显示进程名和 PID(需 sudo)

验证方法

$ sudo ss -lntp | grep 8080

预期输出(监听所有网卡,上两层放行后外网可达):

LISTEN 0  511  0.0.0.0:8080  0.0.0.0:*  users:(("java",pid=1234,fd=8))

预期输出(只监听回环,外网必然不通):

LISTEN 0  511  127.0.0.1:8080  0.0.0.0:*  users:(("java",pid=1234,fd=8))

常见问题

现象 根因 解决
防火墙全开,外网还是不通 进程只监听 127.0.0.1 改应用配置绑 0.0.0.0,或用 Nginx 反代
本机 curl 通、外网不通 安全组或 ufw 没放行 按 1 → 2 层顺序查
ping 得通但端口不通 安全组只放了 ICMP 控制台补业务端口规则

ufw 状态查看与启用

是什么

ufw(Uncomplicated Firewall)是 Ubuntu 自带的防火墙工具,本质是一个「写 iptables/nftables 规则的前端」,把复杂语法简化成 ufw allow 80 这样一句话。Ubuntu 22.04 上默认安装但未启用(inactive),即不拦截任何流量。

怎么做

# 查看状态(未启用时显示 inactive)
$ sudo ufw status

# 详细状态:额外显示默认策略和日志开关
$ sudo ufw status verbose

# 带编号显示,删除规则时要用编号
$ sudo ufw status numbered

[!WARNING] 启用 ufw 前必须先放行 SSH,否则你会把自己永久锁在服务器外面。 这是云服务器第一号事故。 补救思路:登录云控制台,用 VNC / WebShell / 串口终端(各家叫法不同)进入服务器 —— 这个通道不走 22 端口,进去后执行 sudo ufw allow 22 或 sudo ufw disable 即可恢复。 所以开新机器时,先确认控制台 VNC 能登录,再动防火墙。

# 第一步:放行 SSH(推荐后者,更语义化)
$ sudo ufw allow 22
$ sudo ufw allow OpenSSH

# 或只放行来自固定 IP 的 SSH(更安全)
$ sudo ufw allow from <你的IP> to any port 22

# 第二步:确认 22 已在规则里,再启用(重启后依然生效)
$ sudo ufw status
$ sudo ufw enable

参数说明

命令 作用
ufw status 查看规则与激活状态
ufw status verbose 增加显示默认策略、日志级别
ufw status numbered 给规则编号,供 ufw delete <编号> 使用
ufw enable / disable 启用 / 停用(规则保留,只是不生效)
ufw reload 重新加载规则,不断开现有连接

验证方法

$ sudo ufw status verbose

预期输出:

Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), disabled (routed)

To                         Action      From
--                         ------      ----
22                         ALLOW IN    Anywhere
22 (v6)                    ALLOW IN    Anywhere (v6)

注意 sudo ufw allow 22 的规则显示为 22(不带协议后缀)——它同时放行 TCP 和 UDP 两个协议的 22 端口。显示成 22/tcp 的是 sudo ufw allow OpenSSH 或 sudo ufw allow 22/tcp 这类写法的输出,只放行 TCP。

常见问题

现象 根因 解决
ufw enable 后 SSH 断线连不回 没先放行 22 云控制台 VNC 登录,sudo ufw allow 22
Status: inactive 但以为在保护 从未执行 enable 先放行 SSH 再 enable
重启后规则没了 只加规则没 enable sudo ufw enable 写入开机自启

ufw 规则:放行、拒绝与删除

是什么

规则描述「允许/拒绝什么样的流量进来」,通常包含目标端口 + 协议 + 来源地址三部分。

怎么做

# 放行 HTTP / HTTPS
$ sudo ufw allow 80
$ sudo ufw allow 443

# 只放行 TCP(推荐,Web 服务不需要 UDP)
$ sudo ufw allow 8080/tcp

# 放行端口范围
$ sudo ufw allow 6000:6010/tcp

# 只允许指定来源 IP 访问 22(SSH 强烈推荐)
$ sudo ufw allow from <你的IP> to any port 22

# 只允许内网网段访问数据库
$ sudo ufw allow from 10.0.0.0/8 to any port 3306

# 显式拒绝某端口 / 封某个 IP
$ sudo ufw deny 3306
$ sudo ufw deny from 203.0.113.66

删除规则:

# 方式一:按内容删除(写法必须与添加时完全一致)
$ sudo ufw delete allow 80

# 方式二:先编号,再按编号删(推荐,不会匹配错)
$ sudo ufw status numbered
$ sudo ufw delete 3

[!NOTE] 规则有先后顺序,先匹配的先生效。 前面已有 deny,后面再加 allow 也不会生效 —— 这就是「删除规则后仍不通」的真正原因:冲突的 deny 还在前面。

参数说明

写法 含义
allow 80 放行 80,TCP + UDP 都放行
allow 80/tcp 只放行 TCP,更精确
allow 6000:6010/tcp 放行 6000~6010 共 11 个 TCP 端口
allow from <IP> 只允许该来源,其余按默认策略处理
deny 显式拒绝并记日志,优先级高于默认策略

验证方法

$ sudo ufw status numbered

预期输出:

Status: active

     To                         Action      From
     --                         ------      ----
[ 1] 22/tcp                     ALLOW IN    Anywhere
[ 2] 80/tcp                     ALLOW IN    Anywhere
[ 3] 8080/tcp                   ALLOW IN    Anywhere
[ 4] 6000:6010/tcp              ALLOW IN    Anywhere
[ 5] 3306                       DENY IN     Anywhere

常见问题

现象 根因 解决
加了 allow 仍不通 前面有更优先的 deny status numbered 看顺序,删掉冲突的 deny
加了 allow 仍不通,规则没错 Docker 直接写 iptables,绕过 ufw 见下方 WARNING
delete allow 80 说规则不存在 添加时写的是 80/tcp,删除没带协议 用编号删
允许了来源 IP,同事还是连不上 同事出口 IP 与配置的不同(NAT/多线路) curl ifconfig.me 确认真实出口 IP

[!WARNING] Docker 会绕过 ufw。 容器启动时 Docker 直接往 iptables 插入自己的规则,优先级高于 ufw 生成的链,结果 deny 了某端口、容器的 -p 8080:8080 映射依然能被外网访问。 规避:容器只映射到 127.0.0.1(-p 127.0.0.1:8080:8080)再用 Nginx 反代,不要依赖 ufw 管容器端口,详见 30-docker.md。


ufw 应用配置、默认策略与日志

是什么

应用配置:部分软件(Nginx、OpenSSH)安装时会放一个描述文件声明自己用了哪些端口,于是可以写 ufw allow 'Nginx Full' 而不必记端口。 默认策略:对所有「未被任何规则匹配」的流量采取什么动作。

怎么做

# 列出可用的应用配置,并查看某个配置包含哪些端口
$ sudo ufw app list
$ sudo ufw app info 'Nginx Full'

# 按应用名放行(名字带空格必须加引号)
$ sudo ufw allow 'Nginx Full'

# 默认策略:拒绝所有入站,允许所有出站
$ sudo ufw default deny incoming
$ sudo ufw default allow outgoing

# 日志开关,日志写在 /var/log/ufw.log
$ sudo ufw logging on

# 重置:清空所有规则并停用
$ sudo ufw reset

[!IMPORTANT] 先 default deny incoming,再逐条 allow 需要的端口(白名单思路)。反过来「默认放行、只 deny 少数」迟早会漏。改默认策略前同样要确认 22 已放行。

[!WARNING] sudo ufw reset 会清空所有规则并把防火墙设为 inactive,包括放行 SSH 的那条。执行后不重新放行 22 就 enable,等于把服务器锁在门外。只在规则彻底搞乱时用。

参数说明

命令 作用
ufw app list 列出所有应用配置名
ufw app info <名字> 显示该配置包含的端口与协议
ufw default deny incoming 未匹配的入站流量全部丢弃
ufw default allow outgoing 服务器主动访问外网不受限
ufw logging on 开启日志,记录在 /var/log/ufw.log

deny incoming + allow outgoing 是推荐组合:别人进不来,我出去不受限。

验证方法

$ sudo tail -n 5 /var/log/ufw.log

预期输出(关键字段:[UFW BLOCK] 表示拦截,SRC 来源 IP,DPT 目标端口):

Sep 29 10:22:31 ubuntu kernel: [UFW BLOCK] IN=eth0 OUT= SRC=203.0.113.66 DST=10.0.0.5 PROTO=TCP SPT=42122 DPT=3306 SYN URGP=0

常见问题

现象 根因 解决
allow 'Nginx Full' 说找不到配置 没装 Nginx sudo apt install nginx 后再看
应用配置的端口与实际不符 改过 Nginx 监听端口 直接用 ufw allow <端口>/tcp
服务器访问外网失败(如 apt update) 出站被改成了 deny sudo ufw default allow outgoing

firewalld(RHEL 系)

是什么

RHEL / Rocky / AlmaLinux 9 默认用 firewalld,它比 ufw 多一层抽象:zone(区域)。zone 是一组规则的集合,网卡或来源 IP 被划到哪个 zone 就套用哪套规则(常用 public、internal、trusted、drop)。它还支持用 service(服务名) 代替端口号。

怎么做

# 查看运行状态与当前 zone 的完整规则
$ sudo firewall-cmd --state
$ sudo firewall-cmd --list-all

# 按服务名放行
$ sudo firewall-cmd --permanent --add-service=http
$ sudo firewall-cmd --reload

# 按端口放行
$ sudo firewall-cmd --permanent --add-port=8080/tcp
$ sudo firewall-cmd --reload

# 把当前运行时规则一次性保存为永久规则
$ sudo firewall-cmd --runtime-to-permanent

# 移除规则
$ sudo firewall-cmd --permanent --remove-service=http
$ sudo firewall-cmd --reload

[!WARNING] --permanent 不加 --reload 不生效 —— firewalld 第一号坑:加了 --permanent 只写进配置文件,内核里的规则没变,服务依然访问不了。 反过来不加 --permanent 的规则立即生效,但重启后消失。生产环境统一写 --permanent + --reload。

[!WARNING] --panic-on 会立刻切断所有网络连接(包括你当前的 SSH),只在被入侵需紧急隔离时使用。误执行后只能靠云控制台 VNC 执行 sudo firewall-cmd --panic-off 恢复。

参数说明

参数 作用
--list-all 显示默认 zone 的全部规则
--add-service=http 按服务名放行(服务定义已含端口)
--add-port=8080/tcp 按端口号放行
--permanent 写入配置文件永久生效,需配合 --reload
--reload 重载配置让永久规则生效,不中断现有连接
--runtime-to-permanent 反向操作:把运行时规则保存到配置文件

验证方法

$ sudo firewall-cmd --list-all

预期输出:

public (active)
  target: default
  interfaces: eth0
  services: ssh http https
  ports: 8080/tcp
  forward: yes

常见问题

现象 根因 解决
加了 --permanent 规则但端口不通 没执行 --reload sudo firewall-cmd --reload
重启后规则失效 加规则时漏了 --permanent 重新加,或用 --runtime-to-permanent
同时装 ufw 和 firewalld,规则打架 两个工具都写 iptables 只保留一个
RHEL / Rocky / AlmaLinux 9 对照
Ubuntu (apt/ufw) RHEL (dnf/firewalld) 说明
sudo apt install ufw sudo dnf install firewalld 安装防火墙工具
sudo ufw enable sudo systemctl enable --now firewalld 启用并开机自启
sudo ufw status sudo firewall-cmd --list-all 查看当前规则
sudo ufw allow 80 sudo firewall-cmd --permanent --add-port=80/tcp && sudo firewall-cmd --reload 放行端口
sudo ufw allow 'Nginx Full' sudo firewall-cmd --permanent --add-service=http 按应用/服务放行
sudo ufw allow from <IP> to any port 22 sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="<IP>" port port="22" protocol="tcp" accept' 限定来源 IP
sudo ufw delete allow 80 sudo firewall-cmd --permanent --remove-port=80/tcp 删除规则
sudo ufw reload sudo firewall-cmd --reload 重载生效
sudo tail /var/log/ufw.log sudo journalctl -u firewalld 查看防火墙日志

iptables 与 nftables 基础

是什么

iptables 是内核 netfilter 的用户态配置工具,规则按「表」和「链」组织:

  • 四表:filter(放行/丢弃,默认表)、nat(地址转换,Docker 端口映射用它)、mangle(改 TOS/TTL 等标记)、raw(连接跟踪前处理)
  • 五链:PREROUTING(刚进网卡还没定去向)、INPUT(目标是本机)、FORWARD(转发给别人)、OUTPUT(本机发出)、POSTROUTING(即将离开网卡)

[!NOTE] Ubuntu 22.04 底层已是 nftables,iptables 命令只是兼容层(iptables-nft),把 iptables 语法翻译成 nftables 规则。日常用 ufw 即可,不必深究。

怎么做

# 查看 filter 表规则(-n 不解析域名,-v 显示命中计数)
$ sudo iptables -L -n -v

# 查看 nat 表(Docker 的规则都在这)
$ sudo iptables -t nat -L -n -v

# 查看 nftables 原生规则集
$ sudo nft list ruleset

[!WARNING] 不要混用多种防火墙工具。 ufw、firewalld、裸 iptables 本质都在改同一套内核规则,同时用会互相覆盖,行为完全不可预测。一台机器只保留一种:Ubuntu 用 ufw,RHEL 用 firewalld。

[!WARNING] iptables -F(清空所有规则)是危险命令。 默认策略为 DROP 时执行它,你会立刻失去 SSH 连接;即使用 ufw,-F 也会清掉 ufw 生成的链,让防火墙形同虚设。排错不要图省事执行 -F。

参数说明

参数 作用
-L list,列出规则
-n numeric,不解析域名(不加会很慢)
-v verbose,显示命中数、接口
-t nat 指定表名,默认 filter
nft list ruleset 查看 nftables 原生规则集

验证方法

$ sudo iptables -L -n -v | head -n 5

预期输出(policy DROP + 一串 ufw- 链,正是 ufw 生成规则的证据,说明两者操作的是同一套机制):

Chain INPUT (policy DROP 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination
  120  7200 ufw-before-logging-input  0    --  *      *       0.0.0.0/0            0.0.0.0/0
  120  7200 ufw-before-input  0    --  *      *       0.0.0.0/0            0.0.0.0/0

常见问题

现象 根因 解决
输出里有大量 ufw- 链 ufw 已启用,规则由它生成 正常,统一用 ufw 管理
手动加的规则重启后没了 只存在于内存,未持久化 改用 ufw 或装 iptables-persistent
容器端口 ufw 拦不住 Docker 直接写 iptables,优先级更高 容器映射改用 127.0.0.1:

端口开放的安全建议

是什么

防火墙不是「开了就安全」,核心原则是最小暴露面:能不对外开放的端口一律不开。

怎么做

应该对外开放的:22(最好限来源 IP)、80(跳转 HTTPS)、443。 绝对不要对 0.0.0.0 开放的(弱认证或无认证,扫描器首要目标):3306 MySQL、5432 PostgreSQL、27017 MongoDB、6379 Redis、9200 Elasticsearch、11211 Memcached。

# 一(推荐):应用只监听 127.0.0.1,端口在公网上物理不可见
$ sudo ss -lntp | grep 3306

# 二:只对内网网段放行
$ sudo ufw allow from 10.0.0.0/8 to any port 3306

# 三:走 SSH 隧道,本地连 localhost:3306 即可
$ ssh -L 3306:127.0.0.1:3306 <用户名>@<服务器IP>

[!TIP] 能不放行就不放行,能限 IP 就限 IP。 每多开一个公网端口,就多一个需要长期打补丁的攻击面。临时远程访问数据库用 SSH 隧道,用完即关。

参数说明

写法 含义
0.0.0.0 所有网卡,等同于对公网开放
127.0.0.1 只有本机,外网无法连接
10.0.0.0/8 内网网段(CIDR 写法)
ssh -L 本地端口转发,见 18

验证方法

$ sudo ss -lntp | grep -E '3306|5432|27017|6379'

预期输出(安全,只监听本机):

LISTEN 0  151  127.0.0.1:3306  0.0.0.0:*  users:(("mysqld",pid=900,fd=21))

预期输出(危险,对公网开放,需立刻整改):

LISTEN 0  151  0.0.0.0:3306  0.0.0.0:*  users:(("mysqld",pid=900,fd=21))

常见问题

现象 根因 解决
数据库本机连得上,应用服务器连不上 监听在 127.0.0.1 改监听内网地址 + ufw 限内网 IP,或走 SSH 隧道
日志里大量 DPT=3306 的 BLOCK 扫描器在爆破数据库 正常,说明防火墙拦住了;确认端口没对公网开
改了 bind 配置但 ss 结果没变 没重启服务 sudo systemctl restart <服务>

本章命令速查

命令 作用
sudo ss -lntp | grep <端口> 查看端口是否被监听、监听在哪个地址
curl localhost:<端口> 本机自测,排除网络因素
sudo ufw status / verbose / numbered 查看状态 / 含默认策略 / 带编号
sudo ufw allow 22 放行 SSH(启用 ufw 前必做)
sudo ufw allow 80 / 443 / 8080/tcp 放行 HTTP / HTTPS / 指定 TCP 端口
sudo ufw allow 6000:6010/tcp 放行端口范围
sudo ufw allow from <IP> to any port 22 只允许指定 IP 访问 SSH
sudo ufw deny <端口> 显式拒绝某端口
sudo ufw delete <编号> 按编号删除规则(推荐)
sudo ufw enable / disable / reload 启用 / 停用 / 重载
sudo ufw default deny incoming 默认拒绝入站(白名单思路)
sudo ufw app list / allow 'Nginx Full' 列出应用配置 / 按应用放行
sudo ufw logging on + sudo tail -f /var/log/ufw.log 开启并查看拦截日志
sudo ufw reset 清空所有规则并停用(危险)
sudo iptables -L -n -v 查看底层规则(排错用)
sudo firewall-cmd --list-all RHEL:查看 firewalld 规则
sudo firewall-cmd --permanent --add-port=8080/tcp && sudo firewall-cmd --reload RHEL:永久放行端口并生效

相关章节

📄 17 软件包管理 17-package-management.md


title: 17 软件包管理 updated: 2026-09 applies-to: Ubuntu 22.04 LTS

在 Linux 上装软件,第一课不是记住 apt install,而是理解为什么不能像 Windows 那样去官网下载安装包双击打开 —— 因为依赖。一个 Web 服务可能依赖几十个库,每个库又有自己的依赖,手工下载意味着自己解决全部依赖。包管理器把这件事自动化了。

本章三件事:包是怎么来的(仓库与源)、怎么装查删(apt / dpkg)、怎么安全地加第三方源。


包管理与 apt 源结构

是什么

软件供应链只有三步:仓库(远端放 .deb 包的地方)→ 索引(本地缓存的包清单)→ 安装(按索引下载并补齐依赖)。apt update 拉索引,apt search 查索引,apt install 按索引下载安装。

概念 说明
仓库(源) 存放 .deb 包的远端服务器。Ubuntu 官方源、Nginx 源、Docker 源都是仓库
索引 仓库里所有包的清单,apt update 把它下载到 /var/lib/apt/lists/
依赖关系 A 包运行需要 B 包,apt 自动算出并装上 B —— 这是它最大的价值
候选版本 当前索引里 apt install 会装上的那个版本

源配置放在两处:/etc/apt/sources.list(主配置)与 /etc/apt/sources.list.d/*.list(推荐,每个第三方源一个文件)。分文件放便于管理:不想要某个源就删那一个文件,不会误伤系统源 —— 系统源一旦被破坏,apt 就再也装不了任何东西。

一行源配置的写法:deb [arch=amd64 signed-by=/etc/apt/keyrings/example.gpg] https://example.com/repo jammy main,依次是类型、可选项(架构 + 验签公钥)、仓库地址、发行版代号、组件名,各字段含义见下方参数说明。

[!NOTE] 为什么 apt install 前要先 apt update? 因为 apt 只认本地缓存那份索引。索引是上周拉的就只知道上周的包,你刚添加的源它完全不知道 —— 这是 Unable to locate package 最常见的根因。 Ubuntu 22.04 也支持 deb822 格式(.sources 后缀),把一行拆成多行键值对,两种格式可共存:

Types: deb
URIs: https://example.com/repo
Suites: jammy
Components: main
Signed-By: /etc/apt/keyrings/example.gpg

大多数第三方官方文档仍给 .list 格式,会用一种即可。

怎么做

# 查看主源配置与第三方源文件
$ cat /etc/apt/sources.list
$ ls -1 /etc/apt/sources.list.d/

# 停用某个源:改后缀名即可(apt 只认 .list / .sources)
$ sudo mv /etc/apt/sources.list.d/nginx.list /etc/apt/sources.list.d/nginx.list.disabled
$ sudo apt update

# 完整安装流程:拉索引 → 搜包名 → 看版本 → 安装
$ sudo apt update
$ apt search nginx
$ apt policy nginx
$ sudo apt install nginx

[!WARNING] 不要贸然替换默认 sources.list。 网上的「一键换源」教程让你整份覆盖,风险是第三方源(Docker、Nginx、PGDG)地址与镜像站目录结构不一致,混用后 apt update 大面积报 404,严重时连系统包都装不上。镜像源是可选加速手段,要换也只替换 Ubuntu 官方那几行,见 33 国内镜像加速。改之前先备份:

# 备份原始源配置,出问题可直接还原
$ sudo cp /etc/apt/sources.list /etc/apt/sources.list.bak

参数说明

字段 / 术语 作用
deb 二进制包源(日常用的就是这个)
deb-src 源码包源,一般注释掉以加快 apt update
arch=amd64 限定架构;可用 $(dpkg --print-architecture) 自动取值
signed-by=<路径> 指定该源用哪个 GPG 公钥验签(现代推荐写法)
jammy Ubuntu 22.04 代号,必须与实际系统一致
main 组件名;官方源还有 universe / restricted / multiverse

验证方法

# 查看系统版本代号,添加源时要用到
$ lsb_release -a

预期输出:

Distributor ID: Ubuntu
Description:    Ubuntu 22.04.5 LTS
Release:        22.04
Codename:       jammy

常见问题

现象 根因 解决
Unable to locate package xxx 没 apt update,或源里没有该包 先 sudo apt update,再 apt search 确认
apt update 报 404 发行版代号写错(如写成 focal) 改回 jammy 或用 $(lsb_release -cs)
装完发现版本很旧 用的是系统默认源 加官方源,见 91 版本速查

apt update 与系统升级

是什么

三个命令常被混为一谈,做的事完全不同:

命令 做的事 会删包吗
apt update 只刷新本地索引(拉最新包清单) 不会
apt upgrade 按新索引升级已装的包 不会
apt full-upgrade 升级,并允许为完成依赖而删包 可能会

[!TIP] apt update 像刷新应用商店的「可更新列表」,只告诉你有什么新版本;真正下载安装是 upgrade 的事。所以 apt update 是安全的,可以随便执行。

怎么做

# 刷新索引
$ sudo apt update

# 先看会动哪些包(干跑,不实际执行)
$ sudo apt upgrade --dry-run

# 升级所有可升级的包
$ sudo apt upgrade

# 只升级某一个包 / 查看可升级列表
$ sudo apt install --only-upgrade nginx
$ apt list --upgradable

# 彻底升级(可能删包,谨慎)
$ sudo apt full-upgrade

# 清理孤立依赖与过期缓存
$ sudo apt autoremove
$ sudo apt autoclean

[!WARNING] apt full-upgrade 可能删除软件包。 执行前务必看 --dry-run 输出里有没有 Remv(Remove)开头的行;若出现 Remv mysql-server 这类关键包,绝对不要执行。数据库大版本升级另见 91。

参数说明

参数 作用
--dry-run / -s 模拟执行,只显示会发生什么(升级前必做)
-y 自动回答 yes,适合脚本;手动执行时不加,留一次确认机会
--only-upgrade 配合 install,只升级不新装
apt clean 清空 /var/cache/apt/archives/ 下所有缓存
apt autoclean 只清理已过期的旧版本缓存(比 clean 温和,推荐)
apt autoremove 删除作为依赖装上、如今无人依赖的包

验证方法

$ apt list --upgradable

预期输出:

Listing... Done
nginx/jammy-updates 1.18.0-6ubuntu14.5 amd64 [upgradable from: 1.18.0-6ubuntu14.4]
openssl/jammy-updates 3.0.2-0ubuntu1.19 amd64 [upgradable from: 3.0.2-0ubuntu1.18]

没有可升级包时只输出 Listing... Done。

常见问题

现象 根因 解决
The following packages have been kept back 升级它需要装新包或删包,upgrade 不敢做 确认无风险后再用 apt full-upgrade 或 apt install <包>
apt update 报 NO_PUBKEY XXXX 源的公钥没导入或过期 重新导入公钥到 /etc/apt/keyrings/
升级中断网,包处于半装状态 dpkg 状态不一致 sudo dpkg --configure -a 再 sudo apt -f install

apt 安装与卸载

是什么

安装会连带依赖一起装;卸载分两种:remove 保留配置文件,purge 连配置一起删。区别很重要:remove 后重装,旧配置还在(可能带着错误配置重新出问题);purge 则彻底干净。

怎么做

# 安装单个 / 多个包
$ sudo apt install nginx
$ sudo apt install nginx curl vim git

# 装指定版本
$ sudo apt install nginx=1.18.0-6ubuntu14.4

# 不装推荐的附属包(镜像瘦身常用)
$ sudo apt install --no-install-recommends nginx

# 重装,修复被误删的文件
$ sudo apt install --reinstall nginx

# 卸载保留配置 / 连配置一起删 / 删孤立依赖
$ sudo apt remove nginx
$ sudo apt purge nginx
$ sudo apt autoremove

[!WARNING] apt purge 会删除配置文件、apt autoremove 会删除包,两者都不可逆。 数据库、Nginx 这类有重要配置的服务,purge 前先备份 /etc/<服务名>/ 和数据目录;autoremove 前务必看一遍它列出的清单 —— 偶尔会有你还在用的包被判定为「无人依赖」而误删。

参数说明

参数 作用
install 安装,已装则升级
remove 卸载程序文件,保留配置文件
purge 卸载程序文件 + 删除配置文件
autoremove 删除作为依赖装上、现已被孤立的包
--reinstall 重新安装同版本,修复被误删的文件
-f install 修复依赖关系,不是安装某个包

验证方法

# 查看安装状态(前两位是状态字符,含义见下)
$ dpkg -l | grep nginx

预期输出:

ii  nginx  1.18.0-6ubuntu14.4  amd64  small, powerful, scalable web/proxy server

ii 表示已安装且配置完成;rc = 已 remove 但配置还在;iU = 已解包但未完成配置(安装被中断);un = 从未安装。

常见问题

现象 根因 解决
卸载后重装,旧的错误配置还在 用的是 remove 不是 purge sudo apt purge <包> 后重装
dpkg -l 显示 iU,装什么都报错 上次安装被中断 sudo dpkg --configure -a
autoremove 想删掉我还在用的包 它被判定为无人依赖 先 apt-mark manual <包> 标记为手动安装
装完命令找不到 包名 ≠ 命令名,或不在 PATH dpkg -L <包> | grep bin 看装到哪

apt 查询:包信息与版本

是什么

查询类命令是排错和选版本的主力。其中 apt policy 最重要 —— 它回答三个问题:装了没?装了哪个版本?还能装哪个版本?

怎么做

# 按关键字搜索包(搜包名和描述)
$ apt search nginx

# 查看包的详细信息:版本、依赖、描述、体积
$ apt show nginx

# 查看已装版本、候选版本及来源(最重要)
$ apt policy nginx

# 列出源里所有可用版本
$ apt-cache madison nginx

# 列出已安装 / 可升级的包
$ apt list --installed
$ apt list --upgradable

[!TIP] 装任何软件之前,先执行 apt policy <包名>。 这是本章最实用的习惯:它让你在动手前就发现「源里只有 1.18 而我要 1.30」,避免装错版本再卸载重装。版本选错的代价很大 —— 装旧版、写到一半发现不支持某指令、卸载重装、配置还得改一遍。

参数说明

命令 输出内容
apt search <关键字> 包名 + 一句话描述,忘记完整包名时用
apt show <包> 版本、优先级、依赖、维护者、描述、体积
apt policy <包> Installed / Candidate / 版本表(含来源与优先级)
apt-cache madison <包> 所有可用版本的紧凑列表,装特定历史版本时用
apt list --installed 所有已安装包及版本,盘点机器用

apt policy 输出里的 500 是优先级;同一包在多个源都有时,apt 选优先级高的版本。

验证方法

$ apt policy nginx

预期输出(未安装时):

nginx:
  Installed: (none)
  Candidate: 1.18.0-6ubuntu14.4
  Version table:
     1.18.0-6ubuntu14.4 500
        500 http://archive.ubuntu.com/ubuntu jammy-updates/main amd64 Packages

已安装时 Installed 显示版本号,且版本表中当前版本前带 *** 标记。Candidate 就是 apt install 会装的版本。

常见问题

现象 根因 解决
Candidate 不是我想要的版本 有多个源,优先级高的那个较旧 加官方源,或 apt install <包>=<版本>
apt search 搜不到明明存在的包 没 apt update 或包名不完整 apt update 后换关键字
想装的版本源里没有 系统源版本太旧 加官方源 / PPA / 源码编译,见 41

锁定版本:apt-mark hold

是什么

apt-mark hold 给包打上「冻结」标记,之后 apt upgrade 会跳过它,即使有更新版本也不动。

为什么需要?有些服务不能随便升:MySQL 8.0 与 8.4 数据格式有差异,Redis 6 与 8 配置项不同。一次无心的 apt upgrade 把数据库升了大版本,可能直接导致服务起不来。

[!IMPORTANT] 数据库、消息队列等有状态服务,上线后一律 apt-mark hold。 升级必须经过计划窗口、备份和验证,绝不能作为日常 apt upgrade 的副产品发生。

怎么做

# 冻结单个 / 多个包
$ sudo apt-mark hold mysql-server
$ sudo apt-mark hold mysql-server redis-server nginx

# 查看所有被冻结的包
$ apt-mark showhold

# 解冻
$ sudo apt-mark unhold mysql-server

需要升级时的标准流程:备份数据 → 解冻 → --dry-run 确认 → 升级并验证 → 重新冻结。

$ sudo apt-mark unhold mysql-server
$ sudo apt upgrade --dry-run
$ sudo apt install --only-upgrade mysql-server
$ sudo apt-mark hold mysql-server

参数说明

命令 作用
apt-mark hold <包> 标记「保持」,阻止被 upgrade / autoremove 影响
apt-mark unhold <包> 取消标记,恢复正常升级
apt-mark showhold 列出所有被冻结的包
apt-mark manual <包> 标记为「手动安装」,防止被 autoremove 删掉

[!NOTE] hold 只挡 apt,不挡你手动 apt install <包>=<新版本>。它是「防误操作」而不是「防人为升级」。

验证方法

# 冻结后确认:upgrade 干跑时该包应被标记为「已保持」
$ sudo apt upgrade --dry-run

预期输出(节选):

The following packages have been kept back:
  mysql-server

常见问题

现象 根因 解决
冻结了但 apt upgrade 还是升了 apt 正常遵守 hold(dpkg selections),不会主动升级被冻结的包——被升多半是显式执行了 apt install <包>=<新版>、unattended-upgrades 的自动更新,或上次 unhold 升级后忘记重新 hold apt-mark showhold 确认 hold 状态;升级窗口结束后重新 apt-mark hold <包>
冻结后安全补丁也装不上 hold 会挡住所有升级 定期找窗口 unhold → 升级 → 重新 hold
想冻结整个系列的所有包 一个个 hold 太麻烦 dpkg -l | grep mysql | awk '{print $2}' 批量取包名

dpkg 底层操作

是什么

apt 是高层工具(处理依赖、联网下载),dpkg 是底层工具(只管解包、不管依赖)。apt install 内部就是先下载 .deb,再调用 dpkg -i 真正安装。

直接用 dpkg 的场景:装离线下载的 .deb,以及反查某个文件属于哪个包这类排错。

怎么做

# 安装本地 .deb(不解决依赖)
$ sudo dpkg -i nginx_1.18.0-6ubuntu14.4_amd64.deb

# dpkg -i 报依赖错误时的修复命令
$ sudo apt -f install

# 列出某个包安装的所有文件
$ dpkg -L nginx

# 反查某个文件 / 命令属于哪个包(排错神器)
$ dpkg -S /usr/sbin/nginx
$ dpkg -S $(which ss)

# 查看已安装状态 / 卸载
$ dpkg -l | grep nginx
$ sudo dpkg -r nginx

[!TIP] dpkg -S /path/to/file 是本章最值得记住的排错命令。 看到「某个配置文件莫名被改」「某个命令版本不对」时,用它一秒定位是哪个包提供的文件。若提示不属于任何包,说明该文件是手工放的或脚本生成的,排错方向立刻清楚。

参数说明

参数 作用
-i 安装本地 .deb(依赖问题需再执行 apt -f install)
-L <包> 列出该包安装到系统的所有文件路径
-S <路径> 反查该文件属于哪个包
-l 列出所有已安装包(配合 grep 最常用)
-r / -P 卸载保留配置 / 连配置一起删
--configure -a 配置所有未完成配置的包(安装中断后的修复)

验证方法

# 反查可执行文件属于哪个包
$ dpkg -S /usr/sbin/nginx

预期输出:

nginx-core: /usr/sbin/nginx

常见问题

现象 根因 解决
dpkg -i 报 dependency problems dpkg 不管依赖 sudo apt -f install 自动补齐
Could not get lock /var/lib/dpkg/lock 另一个 apt/dpkg 正在运行 等它结束,不要删锁文件,见下方 WARNING
dpkg -S 提示 no path found 该文件是手工创建或脚本生成 说明不属于任何包,去查部署脚本
dpkg -i 装完服务没起来 正常系统上 .deb 的 postinst 脚本会调用 systemctl 自启;容器/chroot 环境中 policy-rc.d 会阻止安装时自启 容器/chroot 中需手动 systemctl enable --now <服务>

[!WARNING] 报 Could not get lock /var/lib/dpkg/lock 时不要直接删锁文件。 它的含义是:有另一个 apt/dpkg 进程正在操作包数据库(很可能是后台的 unattended-upgrades)。 正确做法是等它跑完(几十秒到几分钟),或 ps aux \| grep -E 'apt\|dpkg' 看是谁在跑。直接 rm 锁文件会让两个进程同时改包数据库,极可能把 dpkg 状态搞坏到需要重装系统才能恢复。

添加第三方源、PPA 与镜像源

是什么

系统源里的版本往往偏旧(22.04 源里的 Nginx 是 1.18,官方已到 1.30),要装新版就得加官方源。加源要解决两个问题:从哪下载(URL)和怎么确认没被篡改(GPG 签名)—— 仓库用私钥对索引签名,你的机器用公钥验签,这就是 signed-by 的作用。

其他安装方式:PPA(Ubuntu 特有的第三方源托管,个人维护)、snap(Canonical 的容器化包格式,自带依赖自动更新,certbot 官方推荐)、flatpak(偏桌面,服务器上少见)、源码编译(见 41)。

怎么做

完整模板(照抄改名字即可):

# 下载 GPG 公钥到专用目录(不要用 apt-key)
$ sudo install -m 0755 -d /etc/apt/keyrings
$ curl -fsSL https://example.com/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/example.gpg
$ sudo chmod a+r /etc/apt/keyrings/example.gpg

# 添加源,用 signed-by 指定该源对应的公钥
$ echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/example.gpg] https://example.com/repo jammy main" | sudo tee /etc/apt/sources.list.d/example.list
$ sudo apt update

[!IMPORTANT] apt-key add 已废弃,不要再使用。 老教程里的 curl ... \| sudo apt-key add - 会让该公钥对系统上所有源都生效 —— 一个来路不明的第三方源就能伪造系统包,这是严重安全漏洞。现代写法是公钥放 /etc/apt/keyrings/ 并用 signed-by= 指定,做到一源一钥、互不影响。22.04 上用 apt-key add 会直接输出废弃警告。

# PPA(以 deadsnakes 为例)
$ sudo add-apt-repository ppa:deadsnakes/ppa
$ sudo apt update

# 移除 PPA
$ sudo add-apt-repository --remove ppa:deadsnakes/ppa

# snap 安装(certbot 官方推荐方式)
$ sudo snap install certbot --classic

[!WARNING] 第三方 PPA 有破坏系统的风险。 PPA 由个人维护,没有严格审核与兼容性保障,可能提供与系统包同名但不兼容的版本,一次 apt upgrade 就把关键库替换掉,导致系统工具甚至 apt 本身崩溃。原则:优先用软件官方 apt 源;没有官方源才考虑 PPA;生产环境尽量不用。

[!NOTE] 用 echo ... \| sudo tee 而不是 sudo echo ... > 的原因:重定向 > 由 shell 执行,sudo 只提升了 echo 的权限,写文件那一步仍是普通用户权限,会报 Permission denied。tee 是被提权的进程,由它写文件就没问题。

参数说明

片段 作用
install -m 0755 -d 创建目录并设权限,-d 表示目录
curl -fsSL -f 失败不输出错误页、-s 静默、-S 出错显示、-L 跟重定向
gpg --dearmor 把 ASCII 装甲格式(.asc)转成二进制 .gpg
$(dpkg --print-architecture) 自动填当前架构(amd64/arm64)
signed-by= 指定该源用哪个公钥验签,现代标准写法
snap install --classic 放宽沙箱限制,允许访问系统文件

验证方法

# 确认源已生效(公钥路径正确则会列出来)
$ apt-cache policy

预期输出(节选,出现在 Package files 列表里说明源生效):

 500 https://example.com/repo jammy/main amd64 Packages
     release o=Example,a=jammy,n=jammy,l=Example,c=main,b=amd64

公钥文件还需可读:ls -l /etc/apt/keyrings/example.gpg 应显示 -rw-r--r--。源出现在列表里且 apt update 没报 NO_PUBKEY,说明源和公钥都配好了。

常见问题

现象 根因 解决
NO_PUBKEY ABCDEF1234567890 公钥没导入或 signed-by 路径写错 重新下载公钥并修正 signed-by
apt-key is deprecated 警告 用了废弃命令 改用 signed-by 写法
gpg: no valid OpenPGP data found 下载到的不是公钥(可能是 404 页面) 浏览器打开该 URL 确认内容
add-apt-repository: command not found 未装相关包 sudo apt install software-properties-common
PPA 加完系统包被升级坏 PPA 提供了冲突版本 ppa-purge ppa:xxx 或移除该源并降级
RHEL / Rocky / AlmaLinux 9 对照
Ubuntu (apt/dpkg) RHEL (dnf/rpm) 说明
sudo apt update sudo dnf check-update 刷新索引
sudo apt install nginx sudo dnf install nginx 安装包
sudo apt remove nginx sudo dnf remove nginx 卸载(保留配置)
sudo apt purge nginx sudo dnf remove nginx && sudo rm -rf /etc/nginx 连配置一起删(dnf 无 purge)
sudo apt upgrade sudo dnf upgrade 升级所有包
sudo apt autoremove sudo dnf autoremove 清理孤立依赖
apt search <关键字> dnf search <关键字> 搜索包
apt show <包> dnf info <包> 查看包信息
apt policy <包> dnf list --showduplicates <包> 查看可用版本
apt list --installed dnf list installed / rpm -qa 列出已安装
dpkg -L <包> rpm -ql <包> 列出包装了哪些文件
dpkg -S /path/file rpm -qf /path/file 反查文件属于哪个包
dpkg -i xxx.deb dnf install ./xxx.rpm 装本地包(dnf 自动处理依赖)
apt-mark hold <包> dnf versionlock add <包> 冻结版本
/etc/apt/sources.list.d/*.list /etc/yum.repos.d/*.repo 第三方源位置

本章命令速查

命令 作用
sudo apt update 刷新软件索引(不装任何东西)
sudo apt upgrade 升级已装包(不删包)
sudo apt full-upgrade 升级并允许删包(谨慎)
sudo apt install <包> 安装包(自动处理依赖)
sudo apt install <包>=<版本> 安装指定版本
sudo apt remove <包> 卸载,保留配置文件
sudo apt purge <包> 卸载并删除配置文件
sudo apt autoremove / autoclean 删孤立依赖 / 清过期缓存
apt show <包> 查看包详细信息
apt policy <包> 查看已装/候选版本及来源(装前必查)
apt-cache madison <包> 列出源里所有可用版本
apt list --installed / --upgradable 列出已安装 / 可升级的包
sudo apt-mark hold <包> 冻结版本,禁止被 upgrade
apt-mark showhold 查看被冻结的包
sudo dpkg -i xxx.deb 装本地 .deb(依赖问题用 apt -f install 修)
dpkg -L <包> 列出该包安装的所有文件
dpkg -S /path/file 反查文件属于哪个包(排错神器)
dpkg -l | grep <关键字> 查看已安装包及状态
sudo apt -f install 修复依赖/中断的安装
sudo add-apt-repository ppa:xxx 添加 PPA(有风险)

相关章节

📄 18 SSH 远程登录与安全加固 18-ssh-hardening.md


title: 18 SSH 远程登录与安全加固 updated: 2026-09 applies-to: Ubuntu 22.04 LTS

云服务器没有显示器、没有键盘,你与它之间唯一的通道就是 SSH。这意味着:SSH 不通,你就彻底失去这台机器;同时 SSH 是攻击者最先盯的入口 —— 公网上每台机器的 22 端口每天都会被扫描爆破成千上万次。

本章做三件事:改用密钥登录、收紧服务端配置、装 fail2ban 自动封禁爆破者。

[!NOTE] 本章版本号截至 2026 年 9 月。版本信息的唯一权威来源是 版本速查表,冲突时以速查表为准。

[!WARNING] 本章所有操作都可能让你把自己锁在服务器外面。两条铁律:① 任何禁用密码登录、改端口的操作,必须先在另一个终端窗口验证新方式能登录成功;② 操作前确认你能在云控制台用 VNC / WebShell / 串口终端登录 —— 这是最后一条退路。


SSH 是什么与第一次连接

是什么

SSH(Secure Shell)是加密的远程登录协议,取代了明文传输的 telnet。它解决三个问题:密码不被窃听(全程加密)、确认连的是真服务器(首次连接校验指纹)、命令不被篡改(完整性校验)。

密钥登录是核心机制,比密码安全得多:私钥留在你电脑、从不在网络上传输,服务器只用公钥验证你的签名;而密码登录时密码本身在传输,且可以被无限次猜。

怎么做

# 基本登录(不指定用户名则用本地当前用户名)
$ ssh <用户名>@<服务器IP>

# 指定非 22 端口
$ ssh -p 2222 <用户名>@<服务器IP>

首次连接会提示确认服务器指纹 —— 这不是多此一举,它防止有人冒充你的服务器:

$ ssh <用户名>@<服务器IP>
The authenticity of host '<服务器IP>' can't be established.
ED25519 key fingerprint is SHA256:aBcDeFgHiJkLmNoPqRsTuVwXyZ0123456789abcdefg.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '<服务器IP>' (ED25519) to the list of known hosts.

输入 yes 后指纹记入本地 ~/.ssh/known_hosts,以后不再提示。

[!IMPORTANT] 第一次连接应当核对指纹:在云控制台 VNC 里执行 sudo ssh-keygen -l -f /etc/ssh/ssh_host_ed25519_key.pub 拿到真实指纹比对,对不上就立刻中止连接。 Windows 客户端:Windows 10/11 自带 OpenSSH,在 Windows Terminal 或 PowerShell 里直接用 ssh 命令;图形界面可选 PuTTY(它用 .ppk 私钥,需 PuTTYgen 转换)。

[!NOTE] Ubuntu 上 SSH 服务名是 ssh,不是 sshd。 systemctl restart sshd 会报 Unit sshd.service not found,RHEL 系才叫 sshd;但进程名两边都是 sshd,所以 ss -lntp 里看到的是 sshd。

参数说明

参数 作用
-p <端口> 指定端口,默认 22(小写 p)
-i <私钥路径> 指定用哪个私钥登录
-v / -vv / -vvv 输出详细握手过程,排错神器
~/.ssh/known_hosts 客户端记录已信任主机指纹的文件
~/.ssh/id_ed25519 私钥,权限必须 600,绝不外传
~/.ssh/id_ed25519.pub 公钥,要放到服务器上

验证方法

$ sudo systemctl status ssh

预期输出(节选,关键看 active (running) 和 enabled):

● ssh.service - OpenBSD Secure Shell server
     Loaded: loaded (/lib/systemd/system/ssh.service; enabled; vendor preset: enabled)
     Active: active (running) since Tue 2026-09-29 10:00:00 CST; 1h ago

常见问题

现象 根因 解决
Unit sshd.service not found Ubuntu 服务名是 ssh 用 systemctl status ssh
SSH 服务没装 最小化镜像可能没装 sudo apt install openssh-server
REMOTE HOST IDENTIFICATION HAS CHANGED 服务器重装,指纹变了 ssh-keygen -R <服务器IP>(确认确实重装过再删)
Connection timed out 安全组或防火墙没放行 见 16 防火墙

SSH 密钥登录

是什么

密钥对由一个私钥(留你电脑,绝不外传)和一个公钥(放服务器)组成。算法选择:ed25519(推荐,现代算法,256 位强度约等于 RSA 3072,密钥短速度快)、rsa(兼容老设备,需 -b 4096)、ecdsa(一般,不推荐)。

怎么做

在你的本地电脑上执行(不是服务器上):

# 生成 ed25519 密钥对
$ ssh-keygen -t ed25519 -C "your_email@example.com"

# 老设备兼容时用 RSA 4096
$ ssh-keygen -t rsa -b 4096 -C "your_email@example.com"

# 指定文件名(多密钥管理时区分公司/个人)
$ ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_work -C "work"

# 查看公钥内容(这个要放到服务器)
$ cat ~/.ssh/id_ed25519.pub

[!TIP] 建议给私钥设置 passphrase,这样私钥泄露攻击者没有口令也用不了。嫌每次输麻烦就用 ssh-agent 记住:eval "$(ssh-agent -s)" 然后 ssh-add ~/.ssh/id_ed25519。空口令私钥等于「谁拿到文件谁就能登录」。

推送公钥到服务器:

# 推荐:一条命令完成建目录 + 追加公钥 + 设权限
$ ssh-copy-id <用户名>@<服务器IP>

# 端口不是 22 时
$ ssh-copy-id -p 2222 -i ~/.ssh/id_ed25519.pub <用户名>@<服务器IP>

# 客户端没有 ssh-copy-id 时(Windows PowerShell 等)
$ cat ~/.ssh/id_ed25519.pub | ssh <用户名>@<服务器IP> "mkdir -p ~/.ssh && chmod 700 ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"

[!IMPORTANT] 权限必须正确,否则 SSH 会「静默」拒绝密钥登录 —— 客户端只看到 Permission denied (publickey),服务端日志里才有权限报错。这是密钥登录失败最常见的原因:

# 在服务器上修正:目录 700、文件 600、属主是该用户
$ chmod 700 ~/.ssh && chmod 600 ~/.ssh/authorized_keys
$ chown -R <用户名>:<用户名> ~/.ssh

另外追加公钥必须用 >>,用 > 会覆盖掉已有的其他公钥,可能把同事或你的另一台机器锁在外面。

验证:新开一个终端窗口测试,不要关掉当前的密码登录会话。

$ ssh <用户名>@<服务器IP>

参数说明

参数 / 路径 作用
-t ed25519 指定密钥类型
-b 4096 指定位数(RSA 用;ed25519 固定长度)
-C "注释" / -f <路径> 添加注释(邮箱或用途)/ 指定输出文件名
ssh-copy-id -i <公钥> 指定要推送的公钥(注意是 .pub)
~/.ssh/authorized_keys 服务器上的授权公钥文件,一行一个

验证方法

# 在服务器上检查权限
$ ls -ld ~/.ssh && ls -l ~/.ssh/authorized_keys

预期输出:

drwx------ 2 <用户名> <用户名> 4096 Sep 29 10:05 /home/<用户名>/.ssh
-rw------- 1 <用户名> <用户名>   96 Sep 29 10:05 /home/<用户名>/.ssh/authorized_keys

常见问题

现象 根因 解决
Permission denied (publickey) 权限不对 / 公钥没写进去 / 用户名不匹配 检查 700、600 权限与 authorized_keys 内容
密钥登不上但密码可以 公钥没正确写入或权限不对 用密码登录后按上面 IMPORTANT 修正
Permissions 0644 for 'id_ed25519' are too open 客户端私钥权限太宽 chmod 600 ~/.ssh/id_ed25519
换了用户名登录就不认密钥 每个用户有自己的 authorized_keys 把公钥加到对应用户家目录
老设备不认 ed25519 服务端 OpenSSH < 6.5 改用 rsa -b 4096

用 ~/.ssh/config 简化登录

是什么

本地的 ~/.ssh/config 给每台服务器起别名,把用户名、端口、私钥路径记下来,之后 ssh myserver 即可登录,不用每次敲一长串。

怎么做

Host myserver
    HostName <服务器IP>
    User <用户名>
    Port 2222
    IdentityFile ~/.ssh/id_ed25519
    ServerAliveInterval 60
# 等价于 ssh -p 2222 -i ~/.ssh/id_ed25519 <用户名>@<服务器IP>
$ ssh myserver

参数说明

配置项 作用
Host / HostName 别名(本地随便起)/ 真实 IP 或域名
User / Port 登录用户名 / SSH 端口
IdentityFile 私钥路径(多密钥时必配)
ServerAliveInterval 每 60 秒发保活包,防止连接被防火墙掐断
IdentitiesOnly yes 只用指定的私钥,避免多密钥导致认证失败
Host * 通配段,要放文件末尾(SSH 用先匹配到的配置)

验证方法

# 打印 SSH 解析配置后的实际参数
$ ssh -G myserver | head -n 5

预期输出:

user <用户名>
hostname <服务器IP>
port 2222
identityfile ~/.ssh/id_ed25519
serveraliveinterval 60

常见问题

现象 根因 解决
config 改了没生效 Host 段没匹配上(如用真实 IP 登录时匹配不到别名段)、写了语法错误,或被文件中先匹配到的配置覆盖(Host * 段放在了前面) ssh -G <别名> 看实际解析出的参数;chmod 600 ~/.ssh/config 作为卫生习惯也建议做,但它不是没生效的根因
别名不生效 用真实 IP 登录时匹配不到 Host 用 ssh <别名> 而非 ssh <IP>
Host * 覆盖了单个主机的配置 SSH 取最先匹配到的段 把 Host * 放到文件最后
多密钥时登不上 SSH 试了错误的密钥顺序 显式配置 IdentityFile

sshd_config 服务端加固

是什么

/etc/ssh/sshd_config 是 SSH 服务端配置。默认配置以「能用」为目标,安全上相当宽松,加固就是逐项收紧。

怎么做

# 先备份,改坏能秒还原
$ sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak
$ sudo nano /etc/ssh/sshd_config
# 禁止 root 直接登录(改用普通用户 + sudo)
PermitRootLogin no

# 启用密钥登录;禁用密码登录 —— 确认密钥能登录后再改!
PubkeyAuthentication yes
PasswordAuthentication no

# 禁止空密码,30 秒内没登录成功就断开,单连接最多试 3 次
PermitEmptyPasswords no
LoginGraceTime 30
MaxAuthTries 3

# 只允许指定用户登录(白名单,强烈推荐)
AllowUsers <用户名>

# 关闭 X11 转发;每 300 秒探活一次
X11Forwarding no
ClientAliveInterval 300

# 改端口(可选,只是减少扫描噪音)
Port 2222
配置项 推荐值 默认值 理由
PermitRootLogin no prohibit-password root 每台机器都存在,等于帮攻击者省一半力气
PasswordAuthentication no yes 最关键的加固项,彻底杜绝密码爆破
PubkeyAuthentication yes yes 密钥登录开关
PermitEmptyPasswords no no 防配置疏漏
MaxAuthTries 3 6 限制单连接内的认证尝试次数
LoginGraceTime 30 120 减少半开连接占用
AllowUsers 你的用户名 无限制 白名单;也可用 AllowGroups sshusers 按组授权
X11Forwarding no yes 无需图形转发,减少攻击面
ClientAliveInterval 300 0 定期探活,及时发现断连
Port 自定义 22 换端口,减少扫描噪音

[!WARNING] PasswordAuthentication no 是全章第二号「把自己锁在门外」事故(第一号是 ufw 没放行 SSH)。关掉密码登录后密钥又没配好,你就永远进不去了。 顺序必须是:① 密钥登录测试成功 → ② 关闭密码登录 → ③ 新开终端再次验证能登录 → ④ 才算完成。

[!IMPORTANT] 云服务器特别注意 drop-in 文件:22.04 云镜像的 /etc/ssh/sshd_config.d/50-cloud-init.conf 里常含 PasswordAuthentication yes,而 drop-in 文件优先生效——你在主配置里改成 no 也不会生效。改完务必用 sudo sshd -T | grep passwordauthentication 核对实际生效值,必要时同步修改该 drop-in 文件。

[!NOTE] 改端口不是真正的安全措施,只是减少噪音。 扫描器扫全端口只需几分钟,它唯一的作用是让日志少 99% 的自动扫描记录。真正的安全来自密钥登录 + 禁密码 + 禁 root。 改端口后防火墙和安全组要同步放行新端口(16);RHEL 系有 SELinux,还需 semanage port -a -t ssh_port_t -p tcp 2222。

参数说明

语法要点 说明
格式 选项 值,空格分隔,大小写不敏感
注释 # 开头的是注释,通常就是默认值
重复项 同一选项出现多次时,第一次出现的生效
AllowUsers 空格分隔多个用户;可写 <用户名>@<IP> 限定来源
Match 块 对特定用户单独配置,直到下一个 Match
自定义配置位置 推荐放 /etc/ssh/sshd_config.d/*.conf

验证方法

# 查看 sshd 实际生效的完整配置(含默认值)
$ sudo sshd -T | grep -E 'port|permitrootlogin|passwordauthentication|allowusers'

预期输出:

port 2222
permitrootlogin no
passwordauthentication no
allowusers <用户名>

常见问题

现象 根因 解决
改完重启后连不上 配置语法错或把自己排除了 VNC 登录还原备份文件
配置改了没生效 没重启服务,或写在 Match 块之后 sudo sshd -t 检查后 systemctl restart ssh
AllowUsers 加了但所有人都登不上 用户名写错 VNC 登录后修正
改端口后新连接失败 防火墙/安全组没放行新端口 sudo ufw allow 2222 + 控制台放行

修改 sshd 配置的安全流程

是什么

一套固定操作顺序,让「改坏 SSH 配置」永远不会导致你失去服务器。

怎么做

[!IMPORTANT] 改 sshd 配置时,务必保留一个已登录的会话不要退出。 sshd 重启不会影响已建立的连接,万一新配置有问题,你还能在这个窗口里改回来。在新窗口验证成功之前,绝对不要关掉旧会话。

# 1. 备份并编辑
$ sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak
$ sudo nano /etc/ssh/sshd_config

# 2. 语法检查(必做!有问题会直接指出行号)
$ sudo sshd -t

# 3. 重启(Ubuntu 服务名是 ssh)
$ sudo systemctl restart ssh

# 4. 【关键】新开终端窗口测试能否登录,成功后再关旧会话
$ ssh -p 2222 <用户名>@<服务器IP>

[!IMPORTANT] sudo sshd -t 是重启前的必做检查。 它会校验语法并直接报出文件和行号,例如 /etc/ssh/sshd_config: line 42: Bad configuration option: PermitRootLoginn,能拦住绝大多数「改完重启直接起不来」的事故。 没有 -t 就重启 = 拿服务器可用性赌博。 若配置有错,重启时 sshd 加载失败而退出,已建立的连接不受影响,但新连接进不去。

改坏了就还原:

$ sudo cp /etc/ssh/sshd_config.bak /etc/ssh/sshd_config
$ sudo sshd -t
$ sudo systemctl restart ssh

参数说明

命令 作用
sshd -t 测试配置文件语法,无输出表示通过
sshd -T 输出实际生效的完整配置(含默认值)
systemctl restart ssh 重启 SSH 服务,已有连接不受影响
systemctl reload ssh 重载配置,比 restart 温和
journalctl -u ssh -n 50 看 sshd 启动失败的详细原因

验证方法

# 语法检查:无输出即通过,退出码为 0
$ sudo sshd -t
$ echo $?

预期输出:

0

常见问题

现象 根因 解决
重启 sshd 后当前连接会不会断? 不会 —— 已有连接由独立子进程处理 放心重启,但要新开窗口验证新连接
sshd -t 报错看不懂 会给出文件名和行号 按行号定位,对照备份文件
Job for ssh.service failed 端口被占用或配置错 journalctl -u ssh -n 50 看详细错误
忘记备份就改了配置 无备份可还原 VNC 登录,注释掉可疑新增行

fail2ban 防爆破

是什么

fail2ban 扫描日志,发现某 IP 短时间内反复登录失败就自动把它加入防火墙黑名单。它不能替代密钥登录,但能大幅降低噪音和被爆破成功的概率。

概念 说明
filter(过滤器) 用正则定义「什么算一次失败」(匹配日志里的 Failed password)
jail(监狱) filter + 动作 + 阈值的组合:失败多少次 → 封多久 → 怎么封

怎么做

# 安装并开机自启
$ sudo apt install fail2ban
$ sudo systemctl enable --now fail2ban

# 配置一律写 jail.local,不要改 jail.conf(升级会被覆盖)
$ sudo nano /etc/fail2ban/jail.local
[DEFAULT]
# 务必加上自己的 IP 或公司网段,防止误封自己
ignoreip = 127.0.0.1/8 10.0.0.0/8

# 封禁 1 小时(秒,-1 为永久);10 分钟内失败 3 次触发
bantime  = 3600
findtime = 600
maxretry = 3

# 封禁方式(Ubuntu 22.04 用 ufw 或 iptables 均可)
banaction = ufw

[sshd]
enabled  = true
port     = 2222
filter   = sshd
logpath  = /var/log/auth.log
maxretry = 3
bantime  = 86400

注意 [sshd] 段里写的 port、maxretry、bantime 会覆盖 [DEFAULT] 中的同名值——上例中 sshd jail 实际生效的是封 86400 秒,而不是 [DEFAULT] 的 3600 秒。只想改全局默认时,只写 [DEFAULT],jail 段里不重复这些键。

[!WARNING] ignoreip 里务必加上你自己的固定出口 IP 或公司网段,否则手滑输错三次密码就把自己封在外面。真被封了只能换网络出口,或通过云控制台 VNC 登录后解封。

# 改完重启生效,并查看 sshd jail 状态(含被封 IP 列表)
$ sudo systemctl restart fail2ban
$ sudo fail2ban-client status sshd

# 手动解封 / 封禁
$ sudo fail2ban-client set sshd unbanip <被封IP>
$ sudo fail2ban-client set sshd banip 203.0.113.66

# 查看 fail2ban 自身日志
$ sudo tail -n 50 /var/log/fail2ban.log

参数说明

参数 作用
ignoreip 永不封禁的 IP 白名单(必须加自己的 IP)
bantime 封禁时长(秒),-1 为永久
findtime 统计窗口(秒),这段时间内累计失败次数
maxretry 触发封禁的失败次数
banaction 用什么方式封(ufw / iptables / nftables)
logpath 监控的日志文件(Ubuntu 是 /var/log/auth.log)
jail.local 自定义配置,优先于 jail.conf,升级不覆盖

验证方法

$ sudo fail2ban-client status sshd

预期输出:

Status for the jail: sshd
|- Filter
|  |- Currently failed: 2
|  |- Total failed:     47
|  `- File list:        /var/log/auth.log
`- Actions
   |- Currently banned: 3
   |- Total banned:     3
   `- Banned IP list:   203.0.113.66 198.51.100.23 192.0.2.44

常见问题

现象 根因 解决
没人被封、没生效 logpath 不对,或禁密码后根本没失败日志 确认 logpath = /var/log/auth.log
把自己封在外面了 ignoreip 没加自己的 IP 换出口网络,或 VNC 登录后 unbanip
status sshd 说 jail 不存在 enabled = true 没写或没重载 检查 jail.local 后重启 fail2ban
改了 SSH 端口后不工作 jail 里的 port 还是 22 改成实际端口
被封的 IP 立刻又来 bantime 太短 调大 bantime,或用 recidive jail 累进加长

文件传输:scp / sftp / rsync

是什么

三种基于 SSH 的文件传输方式,都复用 SSH 的加密通道和认证,不需要额外开端口。

工具 特点 适用场景
scp 语法像 cp,最简单 临时传一两个文件
sftp 交互式,像 FTP 但有加密 手工浏览、上传下载
rsync 增量传输,只传变化部分,可断点续传 部署代码、备份大目录(推荐)

怎么做

# scp 上传 / 下载(端口参数是 -P 大写,与 ssh 的 -p 不同!)
$ scp ./app.jar <用户名>@<服务器IP>:/opt/app/
$ scp -P 2222 ./app.jar <用户名>@<服务器IP>:/opt/app/
$ scp <用户名>@<服务器IP>:/var/log/nginx/access.log ./

# sftp 交互式
$ sftp -P 2222 <用户名>@<服务器IP>

# rsync 增量同步:先干跑看会同步什么,确认后再真跑
$ rsync -avz --dry-run ./dist/ <用户名>@<服务器IP>:/var/www/html/
$ rsync -avz --progress ./dist/ <用户名>@<服务器IP>:/var/www/html/

# rsync 指定 SSH 端口
$ rsync -avz -e "ssh -p 2222" ./dist/ <用户名>@<服务器IP>:/var/www/html/

[!WARNING] rsync --delete 会删除目标端存在、但源端没有的文件,用错方向可能清空服务器数据。永远先加 --dry-run 看一遍它会删什么。 另外注意源路径结尾斜杠的语义:./dist/(带斜杠)同步目录内容,./dist(不带斜杠)同步目录本身。

参数说明

参数 作用
scp -P <端口> 大写 P,与 ssh -p(小写)不同,容易记混
rsync -a archive 模式,保留权限、时间、软链接(最常用)
rsync -v / -z 显示过程 / 传输时压缩
rsync --progress 显示传输进度
rsync --dry-run 干跑,只显示会做什么
rsync --delete 删除目标端多余文件(危险)
rsync -e "ssh -p 2222" 指定远程 shell 及端口

验证方法

$ rsync -avz --dry-run ./dist/ <用户名>@<服务器IP>:/var/www/html/

预期输出:

sending incremental file list
./
index.html
assets/
assets/app.js

sent 220 bytes  received 25 bytes  490.00 bytes/sec
total size is 15,432  speedup is 63.00

常见问题

现象 根因 解决
scp 提示 Permission denied 目标目录对该用户不可写 先传到 /tmp 再移动
scp -p 2222 报错 scp 的端口参数是大写 -P 改成 -P 2222
rsync: command not found 某一端没装 两端都 sudo apt install rsync
rsync 每次都全量传 没用 -a,时间戳没保留 加上 -a
传完文件权限不对 没用 -a rsync -a 会保留权限

连接排错

是什么

把 SSH 连不上时的三大症状与原因对应起来,避免盲目试错。

症状 含义 排查方向
Connection refused 主机在线,端口上没有服务 服务没启动、端口写错
Connection timed out 包被丢弃,主机或端口不可达 安全组 / 防火墙 / IP 错
Permission denied (publickey) 连上了,但认证失败 密钥路径、权限、用户名

怎么做

# 一、服务端:确认 sshd 在跑、端口对
$ sudo systemctl status ssh
$ sudo ss -lntp | grep sshd

# 二、客户端:确认端口可达,并看详细握手
$ nc -zv <服务器IP> 2222
$ ssh -vvv -p 2222 <用户名>@<服务器IP>

# 三、服务端:另开窗口实时看认证日志,然后让客户端试连
$ sudo tail -f /var/log/auth.log

ssh -vvv 输出里最该关注的部分:

debug1: Offering public key: /home/<用户名>/.ssh/id_ed25519 ED25519 SHA256:xxx
debug1: Server accepts key: /home/<用户名>/.ssh/id_ed25519 ED25519 SHA256:xxx
debug1: Authentication succeeded (publickey).

卡在 Connecting → 网络/防火墙问题;出现 Offering 但没有 Server accepts key → 公钥没登记或权限不对;完全没出现 Offering → 客户端没找到私钥,用 -i 指定。

服务端日志里权限问题的铁证(客户端只看得到 Permission denied (publickey)):

Authentication refused: bad ownership or modes for directory /home/<用户名>/.ssh

[!NOTE] 关于 Too many authentication failures:本地有多个密钥(或 ssh-agent 加载了很多)时,SSH 会依次尝试每一个,超过服务端 MaxAuthTries 就被断开。解决办法是在 ~/.ssh/config 里用 IdentityFile 明确指定私钥,并加 IdentitiesOnly yes 让它只用这一把。

参数说明

命令 / 参数 作用
nc -zv <IP> <端口> 测试 TCP 端口是否可达
ssh -vvv 输出完整握手与认证过程
IdentitiesOnly yes 只用指定密钥,避免多密钥导致的失败
/var/log/auth.log Ubuntu 认证日志,登录成功/失败都在这
journalctl -u ssh systemd 视角的 sshd 日志
last / lastb 查看登录成功 / 失败历史

验证方法

# 在服务器上实时观察登录尝试
$ sudo tail -f /var/log/auth.log

预期输出(密钥登录成功 / 密码失败,后者通常来自扫描器):

Sep 29 10:35:11 ubuntu sshd[12399]: Accepted publickey for <用户名> from 203.0.113.1 port 55210 ssh2: ED25519 SHA256:xxx
Sep 29 10:31:02 ubuntu sshd[12345]: Failed password for invalid user admin from 203.0.113.66 port 54322 ssh2

常见问题

现象 根因 解决
改端口后连不上 ssh 没加 -p,或防火墙/安全组没放行新端口 ssh -p 2222 ...;sudo ufw allow 2222 + 控制台放行
密钥登不上但密码可以 authorized_keys 没写进去或权限不是 700/600 按「SSH 密钥登录」一节修正
重启 sshd 后当前连接会不会断? 不会断 —— 已有连接由独立子进程处理 放心重启,但务必新开窗口验证新连接
Too many authentication failures 本地密钥太多,尝试次数超限 配 IdentityFile + IdentitiesOnly yes
Connection refused sshd 没运行或端口不对 systemctl status ssh + ss -lntp | grep sshd
Connection timed out 安全组/防火墙拦截 见 16 防火墙 四步排查
被 fail2ban 封了不知道 输错密码次数过多 sudo fail2ban-client set sshd unbanip <IP>
RHEL / Rocky / AlmaLinux 9 对照
Ubuntu 22.04 RHEL / Rocky / AlmaLinux 9 说明
sudo systemctl restart ssh sudo systemctl restart sshd 服务名不同
sudo apt install openssh-server sudo dnf install openssh-server 安装服务端
/var/log/auth.log /var/log/secure 认证日志路径不同
banaction = ufw banaction = firewallcmd-rich-rules fail2ban 封禁动作不同
sudo ufw allow 2222 sudo firewall-cmd --permanent --add-port=2222/tcp && sudo firewall-cmd --reload 防火墙放行
改端口无需额外操作 semanage port -a -t ssh_port_t -p tcp 2222 RHEL 有 SELinux,改 SSH 端口要登记

本章命令速查

命令 作用
ssh <用户名>@<服务器IP> 登录远程服务器
ssh -p 2222 <用户名>@<服务器IP> 指定端口登录
ssh -i ~/.ssh/id_ed25519 <用户名>@<服务器IP> 指定私钥登录
ssh -vvv <用户名>@<服务器IP> 输出详细握手过程(排错用)
ssh-keygen -t ed25519 -C "注释" 生成 ed25519 密钥对
ssh-copy-id <用户名>@<服务器IP> 推送公钥到服务器
ssh-copy-id -p 2222 -i ~/.ssh/xxx.pub <用户名>@<服务器IP> 指定端口和公钥推送
chmod 700 ~/.ssh 修正 .ssh 目录权限(必须)
chmod 600 ~/.ssh/authorized_keys 修正授权文件权限(必须)
ssh-keygen -R <服务器IP> 删除本地记录的旧指纹
sudo ssh-keygen -l -f /etc/ssh/ssh_host_ed25519_key.pub 查看服务器主机密钥指纹
sudo sshd -t 检查 sshd 配置语法(重启前必做)
sudo sshd -T | grep <选项> 查看某选项实际生效值
sudo systemctl restart ssh 重启 SSH 服务(Ubuntu 服务名 ssh)
sudo tail -f /var/log/auth.log 实时看认证日志
sudo apt install fail2ban 安装 fail2ban
sudo fail2ban-client status sshd 查看 sshd jail 与被封 IP
sudo fail2ban-client set sshd unbanip <IP> 解封某个 IP
scp -P 2222 file <用户名>@<服务器IP>:/path/ 上传文件(-P 大写)
rsync -avz -e "ssh -p 2222" ./dir/ <用户名>@<服务器IP>:/path/ 增量同步目录

相关章节

📄 Java 运行环境(JDK)安装与配置 20-jdk.md


title: 20 JDK 安装与配置 updated: 2026-09 applies-to: Ubuntu 22.04 LTS

[!IMPORTANT] 本章所有版本号截至 2026-09。全篇版本信息的唯一权威来源是 版本速查表。

装 Java 只要一条命令,难的是后面三件事:装哪个版本、系统怎么在多个版本里选、Maven / Tomcat / systemd 怎么找到它。


版本选择

本章版本基准

项目 本章基准 Ubuntu 22.04 源默认 推荐获取方式
推荐(稳妥) JDK 21 LTS openjdk-21-jdk 可直接装 sudo apt install openjdk-21-jdk
最新 JDK 25 LTS(2025-09 GA) 源里没有 Temurin 源 或 手动解压
仍广泛使用 JDK 17 LTS openjdk-17-jdk 可直接装 apt;Oracle Premier Support 2026-09 到期
老项目兼容 JDK 8 / 11 LTS openjdk-11-jdk(源里没有 JDK 8) apt;JDK 8 需 Temurin 等外部源
不要用于生产 22 / 23 / 24 / 26 无 —— 半年功能版;22/23/24 支持期已过,26(2026-03 发布)支持期到 2026-09 结束,即将到期

[!NOTE] 完整跨组件版本对照见 91 版本速查表。本教程约定:生产环境默认装 JDK 21。

选择依据一:装 JDK 而不是 JRE

一句话:JRE 只能跑 Java 程序,JDK 才能开发 Java 程序。 JRE(Java Runtime Environment)= JVM + 运行用类库,装了能把 jar 跑起来;JDK(Java Development Kit)= JRE + 编译器 javac + 调试器 + 打包工具等整套开发工具。

服务器上选 JDK。因为很多服务端程序运行时也需要编译器:Tomcat 首次访问 JSP 要现场编译成 class、部分框架依赖动态编译能力。Ubuntu 22.04 源里的 -jre-* 包不含 javac,选包名认准结尾的 -jdk。

选择依据二:只选 LTS 版本

Java 从 9 起改成「每 6 个月发一版」的节奏,但不是每版都长期维护:

类型 版本号 支持时长 适合谁
LTS(长期支持) 8、11、17、21、25 数年到十几年(视发行商而定) 生产环境
半年功能版 22、23、24、26 发布后约 6 个月 尝鲜、写 demo

[!WARNING] 不要用非 LTS 版本部署生产。 半年功能版到期后不再有任何安全补丁,等于把已知漏洞留在服务器上。

为什么推荐 21 而不是最新的 25? 这是「新特性收益 vs 稳定性风险」的取舍:21 已发布三年(2023-09 GA),Spring Boot 3.2+、Kafka 4.x、Elasticsearch、Flink 都已完整适配,踩过的坑搜索引擎里都有答案;而 25 刚 GA 一年,部分中间件的兼容验证还在追,出问题可参考的资料明显少。Java 向后兼容性很好,但**「好」不等于「完美」**,跨 LTS 升级偶尔会遇到反射、字节码库(ASM/CGLIB)、GC 参数相关的坑。选 25 的场景同样明确:新项目 + 确实需要虚拟线程正式版等新特性,且团队有人兜得住。另外 JDK 17 虽是 LTS,但 Oracle 对它的 Premier Support 已在 2026-09 到期,新项目不建议再选它起步。

选择依据三:按框架反推版本

版本不是看心情选的,是看你跑的东西要什么:

你要跑的东西 最低 JDK 要求 建议安装
Spring Boot 3.x 17(硬性) 21
Spring Boot 2.x 8(部分 2.7 支持 17) 11 或 17,不要上 21(2.x 官方不支持)
Kafka 3.x / 4.x 17(4.0 起要求) 21
Elasticsearch 8.x 自带 JDK 或 17/21 21
Tomcat 10.1 11 17 或 21
Hadoop 3.x 8 / 11 11

[!IMPORTANT] 先看框架文档的版本要求,再定 JDK 版本。 反过来做(先装最新 JDK,再发现 Spring Boot 2.x 起不来)会浪费大量时间。

RHEL / Rocky / AlmaLinux 9 对照
Ubuntu (apt) RHEL (dnf) 说明
sudo apt update sudo dnf check-update 刷新软件源索引
sudo apt install openjdk-21-jdk sudo dnf install java-21-openjdk-devel RHEL 侧包名带 -devel 才是 JDK
sudo apt install openjdk-21-jre-headless sudo dnf install java-21-openjdk-headless 只要运行时
sudo apt install default-jdk sudo dnf install java-latest-openjdk-devel 22.04 的 default-jdk 实际是 openjdk-11;右侧装的是最新版本
/usr/lib/jvm/java-21-openjdk-amd64 /usr/lib/jvm/java-21-openjdk RHEL 路径无 -amd64 后缀
sudo update-alternatives --config java sudo alternatives --config java 命令名不同,用法一致

安装

是什么

OpenJDK 是 Java 的开源参考实现,功能与 Oracle JDK 几乎一致,且完全免费商用(Oracle JDK 商用需付费订阅)。三条路线按优先级:能用包管理器就别手动解压,能用解压就别源码编译(OpenJDK 自行编译过程复杂且难维护,本教程不推荐)。

方式 适用场景 优点 缺点
① apt 官方源 需要 JDK 21/17/11/8 一条命令、依赖自动解决、自动获得安全补丁 版本受限于发行版,没有 25
② Temurin 源 需要 JDK 25,又想保持包管理 官方构建、自动更新、多版本共存 多配一个第三方源
③ 手动解压 tar.gz 内网离线、指定精确小版本、多版本共存 装到哪自己定、卸载就是删目录 不自动更新,需自己配 JAVA_HOME 与 alternatives

怎么做

方式一:apt 安装(推荐首选)。 装之前先看清源里有什么版本,别上来就装:

$ apt search openjdk-.*-jdk

# 安装 JDK 21(含 javac 的完整开发套件)—— 本章推荐
$ sudo apt install openjdk-21-jdk

# 要 17 就装 -17-jdk;注意 default-jdk 在 22.04 实际装的是 openjdk-11,要 21 请显式 apt install openjdk-21-jdk
$ sudo apt install openjdk-17-jdk
$ sudo apt install default-jdk

方式二:Temurin(Adoptium)源安装 JDK 25。 Ubuntu 22.04 源里最高只有 21,要装更高版本就得加外部源;Temurin 由 Eclipse Adoptium 社区构建,免费、无商标限制。

# 创建存放 apt 源密钥的目录(已存在也不会报错)
$ sudo install -m 0755 -d /etc/apt/keyrings

# 下载 Adoptium 的 GPG 公钥,转成 apt 能识别的二进制格式
$ curl -fsSL https://packages.adoptium.net/artifactory/api/gpg/key/public | \
    gpg --dearmor | sudo tee /etc/apt/keyrings/adoptium.gpg > /dev/null

# 设置只读权限
$ sudo chmod 0644 /etc/apt/keyrings/adoptium.gpg

# 写入源配置:用 signed-by 把「这条源」和「这个密钥」绑定
$ echo "deb [signed-by=/etc/apt/keyrings/adoptium.gpg] https://packages.adoptium.net/artifactory/deb $(lsb_release -cs) main" | \
    sudo tee /etc/apt/sources.list.d/adoptium.list > /dev/null

# 刷新索引并安装
$ sudo apt update
$ sudo apt install temurin-25-jdk

方式三:手动解压 tar.gz。 JDK 本质是绿色软件 —— 不往系统目录写东西,整个目录复制走或删掉都不影响别的软件,这正是多版本共存的基础。

# 创建规范的安装目录(/opt 专用于第三方软件)
$ sudo mkdir -p /opt/jdk

# 下载 Temurin 21(示例 URL,请到 adoptium.net 取最新链接)
$ curl -fSL -o /tmp/jdk21.tar.gz \
    "https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21.0.8%2B9/OpenJDK21U-jdk_x64_linux_hotspot_21.0.8_9.tar.gz"

# 解压到 /opt/jdk,会得到 /opt/jdk/jdk-21.0.8+9/ 这样的目录;tar 参数见下方参数说明
$ sudo tar -xzf /tmp/jdk21.tar.gz -C /opt/jdk

# 看看解压出来叫什么名字,后面配 JAVA_HOME 要用
$ ls /opt/jdk

参数说明

apt 包名后缀与 Temurin 源配置片段:

片段 含义 / 作用
-jdk 完整开发工具包(含 javac)。服务器上选这个
-jre 仅运行环境,没有 javac
-headless 不含图形界面支持。服务器必选,省几十 MB 且少一堆 X11 依赖
-headful 含图形库,桌面环境才用
/etc/apt/keyrings/ 存放第三方源密钥的约定目录。apt 已不再读 /etc/apt/trusted.gpg
signed-by= 指定「这条源用哪个密钥验签」,实现一源一密钥
$(lsb_release -cs) 自动展开为当前发行版代号,22.04 上即 jammy。比手写不易错
gpg --dearmor 把 ASCII 公钥转成二进制 keyring 格式,是 signed-by= 要求的格式

方式三用到的参数:tar -x 解压、-z 先过 gzip、-f 指定文件名且必须放在最后、-C /opt/jdk 表示解压前先 cd 到目标目录;curl -fSL 分别是「报错时不生成垃圾文件」「出错时显示原因」「跟随重定向」。

[!WARNING] 绝对不要用 apt-key add。 apt-key 已在 Ubuntu 22.04 被废弃,它的致命问题是:加进去的密钥会成为全局信任,可以给系统里任意软件包签名,一旦泄露 apt 会信任攻击者伪造的所有包。看到老教程里的 apt-key add 请一律替换成上面的 signed-by= 写法。

[!WARNING] 不要只装 -jre-headless。 只装 JRE 时 java -version 正常返回,你会以为成功了;等到 Tomcat 要编译 JSP、或某框架调用 javax.tools.JavaCompiler,才会抛出「找不到编译器」。

验证方法

方式一,看版本号(小版本号如 21.0.8 会随系统安全更新变化,只要开头的 21 对就行):

$ java -version

预期输出:

openjdk version "21.0.8" 2026-07-21
OpenJDK Runtime Environment (build 21.0.8+9-Ubuntu-222.04)
OpenJDK 64-Bit Server VM (build 21.0.8+9-Ubuntu-222.04, mixed mode, sharing)

方式二,确认源已经生效:

$ apt policy temurin-25-jdk

预期输出:

temurin-25-jdk:
  Installed: 25.0.1.0.0+8-1
  Candidate: 25.0.1.0.0+8-1
  Version table:
 *** 25.0.1.0.0+8-1 500
        500 https://packages.adoptium.net/artifactory/deb jammy/main amd64 Packages

方式三不要用 java -version 验证 —— 此时 PATH 还没配,查到的会是系统里别的 JDK,必须调用绝对路径:

$ /opt/jdk/jdk-21.0.8+9/bin/java -version

预期输出:

openjdk version "21.0.8" 2026-07-21
OpenJDK Runtime Environment (Temurin-21.0.8+9) (build 21.0.8+9)
OpenJDK 64-Bit Server VM (Temurin-21.0.8+9) (build 21.0.8+9, mixed mode, sharing)

输出中有 Temurin 字样,确认是刚解压的这一份。


环境变量与 PATH

这一节最容易出问题,请慢慢读。

是什么

两个机制解决两件事。

① update-alternatives:决定命令 java 是谁。 服务器上常同时存在多个 JDK,敲 java 时该用哪一个?Debian 系的答案是 alternatives 机制:

/usr/bin/java  →  /etc/alternatives/java  →  /usr/lib/jvm/java-21-openjdk-amd64/bin/java

即 /usr/bin/java 不是真实文件而是软链接,跳到 /etc/alternatives/java,后者再跳到真实 JDK。换 JDK 不需要动 PATH、不需要删文件,只改中间那个链接的指向 —— 好处是 /usr/bin/java 这个入口永远稳定,写死它的脚本、cron、systemd unit 都不用改。

② JAVA_HOME:告诉第三方工具 JDK 在哪。 它指向 JDK 安装根目录(注意:不是 bin 目录),Maven / Gradle / Tomcat / Kafka / Elasticsearch 靠它找 JDK。它们为什么不直接读 PATH 里的 java?因为需要的往往不只是可执行文件,还有 JDK 里 lib/ 下的模块与工具、release 文件等根目录资源 —— 只有知道根目录才能找到这些子目录。

怎么做

第 1 步:把每个 JDK 注册进 alternatives(数字越大优先级越高):

# 注册 apt 装的 JDK 21
$ sudo update-alternatives --install /usr/bin/java java /usr/lib/jvm/java-21-openjdk-amd64/bin/java 21

# 注册手动解压的那个(如果有)
$ sudo update-alternatives --install /usr/bin/java java /opt/jdk/jdk-21.0.8+9/bin/java 25

# 同样的方式注册编译器 javac —— 很多人漏了这一步
$ sudo update-alternatives --install /usr/bin/javac javac /usr/lib/jvm/java-21-openjdk-amd64/bin/javac 21
$ sudo update-alternatives --install /usr/bin/javac javac /opt/jdk/jdk-21.0.8+9/bin/javac 25

第 2 步:切换版本,二选一:

# 交互式选择(推荐新手)
$ sudo update-alternatives --config java

# 非交互式:直接指定用哪一个
$ sudo update-alternatives --set java /usr/lib/jvm/java-21-openjdk-amd64/bin/java

第 3 步:确定 JAVA_HOME 该填什么。 这是新手最常卡住的一步,推荐顺着 java 反查两层目录:

$ dirname $(dirname $(readlink -f $(which java)))

预期输出:

/usr/lib/jvm/java-21-openjdk-amd64

拆解:which java 得 /usr/bin/java(软链接)→ readlink -f 追到底得 .../bin/java → 第一层 dirname 去掉文件名得 .../bin → 第二层 dirname 去掉 bin,剩下的就是 JAVA_HOME。也可以 ls -1 /usr/lib/jvm 列出来肉眼挑,但要用带具体版本号的目录如 java-21-openjdk-amd64,不要用 default-java —— 后者也是软链接,指向会随系统更新悄悄变化,这正是「我没动环境,服务却突然报错」的典型来源。

第 4 步:写入配置。 三个位置按适用范围选择,关键是别写错地方:

位置 生效范围 适用场景
/etc/profile.d/jdk.sh 所有用户的所有登录 shell 服务器第一推荐,系统级统一
~/.bashrc 只有当前用户的 bash 多用户机器上各自用不同 JDK
systemd unit 的 Environment= 只有这一个服务 服务需与命令行用不同版本时

[!TIP] 为什么不直接改 /etc/profile? 它是系统自带文件,apt 升级 base-files 包时可能覆盖它,改动会丢失或引发冲突;而它末尾有一段代码会自动遍历执行 /etc/profile.d/*.sh —— 这是官方预留给用户自定义的位置。

系统级推荐写法(用户级则把同样两行追加到 ~/.bashrc,效果同上但只影响当前用户):

$ sudo tee /etc/profile.d/jdk.sh > /dev/null <<'EOF'
export JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64
export PATH=$JAVA_HOME/bin:$PATH
EOF

# 0644 即可,脚本是被 /etc/profile source 执行的,无需可执行位
$ sudo chmod 0644 /etc/profile.d/jdk.sh

第 5 步:让配置生效。 写完文件不会立刻生效,在当前 shell 里用 source 启用:

$ source /etc/profile.d/jdk.sh
$ source ~/.bashrc

为什么必须 source?执行脚本是在子进程里跑的,子进程结束后变量随之消失;source(等价于 .)是在当前 shell 里逐行执行,所以变量能留下来。它只对当前这一个终端窗口有效 —— 新开 SSH 会话是全新的登录 shell,会自动执行 /etc/profile.d/*.sh。

参数说明

alternatives 的命令开关:

参数 作用
--install <链接> <名称> <真实路径> <优先级> 注册一个候选,四个参数缺一不可
--config <名称> 弹出菜单让你选;自动模式下默认选中优先级数字最大的那个
--set <名称> <真实路径> 设为手动模式,不再按优先级自动选
--display <名称> 查看当前配置和所有候选,排查版本混乱时优先用这个
--remove <名称> <真实路径> 注销某个候选

自动模式下系统永远选优先级数字最大的那个;一旦用 --config 手动选过就转为手动模式,此后再装新 JDK 也不会自动切走 —— 这正是「我装了新版怎么没生效」的原因。

jdk.sh 里两行配置的含义:export 把变量导出给子进程(不写则启动的程序读不到);JAVA_HOME= 指向 JDK 根目录,不要带 /bin;PATH=$JAVA_HOME/bin:$PATH 把 JDK 的 bin 接到 PATH 前面,因为 shell 从左到右扫描 PATH,排在前面的优先命中。

[!IMPORTANT] 写成 export PATH=$PATH:$JAVA_HOME/bin(拼后面)多数时候也能跑,但一旦系统里有别的 JDK 排在 PATH 前面,java 就会命中它 —— JAVA_HOME 说用 21、PATH 实际跑了 25,这种不一致排查非常耗时。请统一拼在前面。

为什么会有「时有时无」的现象? 这是 JAVA_HOME 不生效的头号原因。登录 shell(SSH 登录后得到的那个)加载顺序是 /etc/profile → /etc/profile.d/*.sh → ~/.bash_profile;非登录 shell(登录后又敲 bash、脚本里、docker exec、su 出来的)只读 ~/.bashrc,不走 /etc/profile.d/。于是重新 SSH 登录后有值,在某个脚本里执行却为空。稳妥做法是写到 /etc/profile.d/jdk.sh(管住登录 shell)并且在 systemd unit 里用 Environment= 显式声明(管住服务进程),两者都配,服务就不依赖任何 shell 的加载顺序。

验证方法

看 alternatives 的当前选择,再顺着软链接看到底:

$ update-alternatives --display java
$ readlink -f /usr/bin/java

预期输出:

java - auto mode
  link currently points to /usr/lib/jvm/java-21-openjdk-amd64/bin/java
/usr/lib/jvm/java-21-openjdk-amd64/bin/java - priority 21
/opt/jdk/jdk-21.0.8+9/bin/java - priority 25

/usr/lib/jvm/java-21-openjdk-amd64/bin/java

确认 JAVA_HOME 与 PATH 已经对齐:

$ echo $JAVA_HOME
$ which java

预期输出:

/usr/lib/jvm/java-21-openjdk-amd64
/usr/lib/jvm/java-21-openjdk-amd64/bin/java

[!NOTE] 如果 which java 输出的是 /usr/bin/java,说明 $JAVA_HOME/bin 没排到 PATH 前面,回去检查 export PATH 那一行的顺序;另外 apt 安装的 JDK 通常已自动注册 alternatives,不需要手动 --install,动手前先 --display 看看有没有,避免重复注册。


最小必要配置

JDK 是「装完即用」,没有配置文件要改。做完这三件事就够了:

  1. 选定默认版本 —— sudo update-alternatives --config java(及 javac),让两者指向同一个 JDK
  2. 配好 JAVA_HOME —— 写在 /etc/profile.d/jdk.sh,内容是「根路径 + PATH 前置」两行
  3. 保持两者一致 —— 最容易翻车的一条:java -version 显示的版本,必须与 $JAVA_HOME 指向同一个 JDK

服务器专属建议:生产环境尽量只留一个 JDK。 多版本共存是给开发机的便利,在生产服务器上是纯粹的复杂度来源。若某老服务确实需要 JDK 11,宁可在它的 systemd unit 里用绝对路径指定,也不要在系统层面做全局切换。JVM 参数(-Xms/-Xmx、GC 策略、字符集)应放在具体应用的启动脚本或 systemd unit 里,不在 JDK 全局层面改。


验证方法

按顺序做完这三段,才算真的安装好了。

1. 版本命令

$ java -version
$ javac -version
$ echo $JAVA_HOME

预期输出:

openjdk version "21.0.8" 2026-07-21
OpenJDK Runtime Environment (build 21.0.8+9-Ubuntu-222.04)
OpenJDK 64-Bit Server VM (build 21.0.8+9-Ubuntu-222.04, mixed mode, sharing)
javac 21.0.8
/usr/lib/jvm/java-21-openjdk-amd64

[!IMPORTANT] java -version 和 javac -version 必须报同一个版本号。不一致说明 PATH 里混了两个 JDK,回到 update-alternatives --config 那节重新对齐。

2. 实际编译并运行一个程序

版本号对不代表能用。 下面这段一次性验证「编译器存在 → 能生成 class → JVM 能加载执行 → 当前目录有写权限」整条链路:

$ cat > ~/HelloWorld.java <<'EOF'
public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello, Linux Server!");
    }
}
EOF

$ cd ~
$ javac HelloWorld.java
$ java HelloWorld

预期输出:

Hello, Linux Server!

编译后目录里会多出 HelloWorld.class —— 这是 Java 字节码,JVM 执行的正是它。

3. 用 jshell 快速验证

jshell 是 JDK 9 起自带的交互式 REPL,只有 JDK 有、JRE 没有,所以它是「我装的是不是真 JDK」的天然检测器:

$ jshell

预期输出(提示符是 jshell 自己的,不是 shell):

|  Welcome to JShell -- Version 21.0.8

jshell> System.out.println("JDK is working")
JDK is working

jshell> /exit

能进入 jshell> 提示符,说明是完整 JDK;输入 /exit 退出。三段一起做下来基本覆盖所有安装失败场景,养成习惯:新机器先跑一遍 HelloWorld,再去部署真正的服务。


托管为 systemd 服务

Java 应用(Spring Boot jar、Tomcat 等)在服务器上由 systemd 托管:开机自启、崩溃自动拉起、日志统一进 journald。详见 systemd 服务管理。

这里必须再提一次 JAVA_HOME,因为 systemd 不走任何 shell 配置文件 —— 你在 /etc/profile.d/jdk.sh 里写得再正确,服务进程也读不到,它启动进程时用的是自己的一套最小 PATH。所以必须在 unit 文件里显式写出来。

怎么做

假设 Jar 位于 /opt/myapp/myapp.jar:

[Unit]
Description=My Java Application
After=network.target

[Service]
Type=simple
User=appuser
WorkingDirectory=/opt/myapp

# systemd 不读 /etc/profile.d,必须在这里显式指定
Environment="JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64"
Environment="JAVA_OPTS=-Xms256m -Xmx512m -Dfile.encoding=UTF-8"

# 用绝对路径启动,不依赖 PATH
ExecStart=/usr/lib/jvm/java-21-openjdk-amd64/bin/java $JAVA_OPTS -jar /opt/myapp/myapp.jar

Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
# 写完 reload 让 systemd 重新读取配置
$ sudo systemctl daemon-reload

# 设为开机自启并立即启动
$ sudo systemctl enable --now myapp.service

# 查看状态
$ sudo systemctl status myapp.service

参数说明

配置项 作用
Environment= 给这个服务单独设环境变量。多个变量写多行,一行只能写一个
$JAVA_OPTS $JAVA_OPTS 与 ${JAVA_OPTS} 均可,systemd 在 ExecStart 行内做变量展开,展开的是本 unit Environment= 定义的值
ExecStart 用绝对路径 不依赖 PATH,避免「命令行跑得好好的,systemd 里说找不到 java」
Restart=always 进程退出就拉起;配合 RestartSec=5 防止重启风暴

验证方法

$ sudo systemctl is-active myapp.service
$ sudo systemctl show myapp.service -p Environment

预期输出:

active
Environment=JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64 JAVA_OPTS=-Xms256m -Xmx512m -Dfile.encoding=UTF-8

第二行确认服务进程拿到的 JAVA_HOME 确实是我们指定的那个。


常见问题

现象 根因 解决
echo $JAVA_HOME 输出为空 ①忘了 source;②在非登录 shell(脚本、su)里,不走 /etc/profile.d/;③写进 ~/.bashrc 却用另一用户登录 先 source /etc/profile.d/jdk.sh;用 echo $0 确认 shell 类型;服务请在 unit 里加 Environment=
which java 是 /usr/bin/java,但 JAVA_HOME 指向另一目录 PATH 里 $JAVA_HOME/bin 没拼在前面,被 alternatives 或其他 JDK 抢先命中 改成 export PATH=$JAVA_HOME/bin:$PATH(前置);再看 update-alternatives --display java
装了新 JDK 但 java -version 还是旧的 alternatives 处于手动模式,不再自动切到高优先级版本 sudo update-alternatives --config java 重选;想恢复自动模式用 --auto java
java 能跑,但 javac: command not found 装的是 JRE(-jre-headless)而非 JDK;或只给 java 注册了 alternatives、漏了 javac 装 openjdk-21-jdk;补 sudo update-alternatives --install /usr/bin/javac javac <路径>/bin/javac 21
Maven 报 The JAVA_HOME environment variable is not defined correctly JAVA_HOME 多了 /bin、指向不存在目录、或指向 JRE 用 dirname $(dirname $(readlink -f $(which java))) 重取,修正后 source
Tomcat 启动报找不到 JDK / 用了错的 JDK Tomcat 优先读 CATALINA_HOME/bin/setenv.sh,那里可能写死了旧路径 在 setenv.sh 里显式 export JAVA_HOME=<正确路径>,比依赖系统环境可靠
cannot execute binary file: Exec format error 架构不匹配:x86 装了 arm64 包,或 32 位系统装了 64 位 JDK uname -m 确认架构(服务器一般 x86_64),重新下载对应架构的包
解压后执行报 Permission denied 可执行位丢失,或目录权限不足无法进入 sudo chmod -R 755 /opt/jdk/jdk-*/;确认父目录有 o+rx。不要图省事 chmod 777
Error: Could not find or load main class ①类名与文件名不一致;②不在 class 所在目录;③CLASSPATH 被污染 确认 HelloWorld.java 里是 public class HelloWorld;cd 到 class 目录;不要乱设 CLASSPATH
程序输出中文变 ??? 或方块 JVM 默认字符集非 UTF-8(容器、最小化安装常见) 启动加 -Dfile.encoding=UTF-8;同时确认系统 locale 已配 UTF-8

本章命令速查

命令 作用
apt search openjdk-.*-jdk 列出源里可用的 JDK 包
sudo apt install openjdk-21-jdk apt 安装 JDK 21(推荐方式)
sudo apt install temurin-25-jdk 从 Temurin 源安装 JDK 25
ls -1 /usr/lib/jvm 列出所有已安装 JDK 的根目录
dirname $(dirname $(readlink -f $(which java))) 反查 JAVA_HOME 该填什么(最实用)
readlink -f $(which java) 查看 java 命令最终指向的真实文件
update-alternatives --display java 查看 java 的所有候选与当前选择
sudo update-alternatives --install /usr/bin/java java <路径> 21 注册一个 JDK 候选
sudo update-alternatives --config java 交互式切换 java 版本
source /etc/profile.d/jdk.sh 让 JAVA_HOME 在当前 shell 立即生效
java -version / javac -version 查看运行时 / 编译器版本(两者必须一致)
echo $JAVA_HOME 确认 JAVA_HOME 已生效
jshell 进入交互式 REPL,顺带验证装的是完整 JDK
javac X.java && java X 编译 + 运行,验证整条工具链
sudo systemctl show <unit> -p Environment 确认服务实际拿到的环境变量

相关章节

📄 Python 环境安装与配置 21-python.md


title: 21 Python 环境安装与配置 updated: 2026-09 applies-to: Ubuntu 22.04 LTS

[!IMPORTANT] 本章所有版本号截至 2026-09。全篇版本信息的唯一权威来源是 版本速查表。

[!WARNING] 在读任何命令之前,先记住这一条:永远不要删除、替换或覆盖系统自带的 /usr/bin/python3。

原因不是「不推荐」,而是「会炸」:apt 背后的多个系统工具(add-apt-repository、unattended-upgrades、云初始化工具 cloud-init 等)是用 Python 写的,且 apt 本身依赖 python3-apt,它们内部写死了调用 /usr/bin/python3。你一旦把它换成 3.13,就会出现因版本差异导致的语法不兼容,或者误删了它依赖的标准库。

后果是:apt 挂了、add-apt-repository 挂了、系统可能连有问题的包都修不回来,只能重装系统。这不是危言耸听,是运维圈里年年有人踩的经典坑。

正确做法是「加法不是替换」:在系统 Python 之外另外装一个版本,然后每个项目用虚拟环境 venv 隔离。本文所有方案都遵循这条原则。


版本选择

本章版本基准

项目 本章基准 Ubuntu 22.04 源默认 推荐获取方式
系统自带(不许动) Python 3.10 /usr/bin/python3 —— 已预装,apt 等系统工具依赖它
推荐(稳妥) Python 3.12 / 3.13 源里没有 deadsnakes PPA / pyenv / 源码编译到 /opt
最新 Python 3.14 源里没有 deadsnakes PPA / pyenv / 源码编译
已 EOL,不要用 3.8 及更早(含 2.7) 无 —— 已无安全更新

[!NOTE] 完整跨组件版本对照见 91 版本速查表。本教程约定:新项目默认用 3.12 或 3.13。

选择依据一:为什么不用系统自带的 3.10

先明确:3.10 本身没坏,它只是「系统的」,不是「你的」。

  • 它属于 apt 包管理体系。你往里塞第三方包,就和包管理器产生冲突 —— 哪天 apt 要升级系统组件,可能和你塞进去的包版本打架
  • 你没法升级它。Ubuntu 22.04 上的 python3 在整个 LTS 生命周期里基本锁死在 3.10(只有安全补丁),想要新语法特性只能另外装
  • 依赖会冲突。你为了跑 A 项目装了 requests==2.25,B 项目需要 requests==2.31,两者无法同时存在于同一个解释器里

所以规矩是:系统 Python 留给操作系统,你的应用另起炉灶。

选择依据二:3.12 / 3.13 还是 3.14

按 91 版本速查表 的建议:稳妥选 3.12 或 3.13。

  • 3.12 / 3.13 已发布足够久,主流库(numpy、pandas、torch、Django、FastAPI)都有对应的预编译 wheel,国内源的 wheel 覆盖也更全
  • 3.14 是最新版,很多第三方库还没提供适配它的二进制包,届时 pip 会退回去下载源码在你的机器上现场编译 —— 编译 numpy 这类 C 扩展动辄十几分钟,还容易因缺依赖失败
  • 确实想用 3.14,先在小项目上验证:pip install 你真正要用的那几个包,确认装的是 wheel(几秒装完)而不是源码编译

[!WARNING] 绝对不要用已经 EOL 的版本(Python 3.8 及更早,包括早已停止维护的 2.7)。 它们不再接收任何安全更新。老项目迁移确实痛苦,但延期不等于免费 —— 用 EOL 版本上线,等于把一堆已知且公开利用方式的漏洞留在服务器上。

选择依据三:按场景反推版本

场景 建议版本
全新项目,无历史包袱 3.13(主流库支持已完善)
要用 PyTorch / TensorFlow 等重依赖 3.12(框架厂商的预编译包覆盖最广,踩坑最少)
已有项目,目标是跟上 vendor 支持 3.12 或 3.13
明确的内部老系统要求版本 按其文档(但请同步推动升级)

安装

是什么

三条路线,先看对比:

方式 适用场景 优点 缺点
① deadsnakes PPA 服务器上固定用一个版本(最常见) apt 管理、卸载干净、升级方便 第三方源,有信任风险;版本更新略滞后
② pyenv 开发者本机、频繁切换多个版本 用户级安装不动系统、切换极其方便 编译耗时;服务器上不如 PPA 直观
③ 源码编译到 /opt 内网离线、需要特定编译参数、或源里根本没有的版本 完全可控、可装任意小版本 步骤多、要自己装编译依赖、不随 apt 更新

[!TIP] 服务器部署首选 ① deadsnakes PPA。 环境变量天然正确、不需要额外配置、卸载一条命令,运维负担最小。只有当你确实要在一台机器上跑多个不同 Python 版本的项目时,才考虑 ②。

怎么做

方式一:deadsnakes PPA(服务器首选)。 Ubuntu 官方源在一个 LTS 生命周期内不会新增 Python 大版本(为了稳定性,这是有意为之),要在 22.04 上装 3.12/3.13 就得加第三方源。deadsnakes 是 Ubuntu 社区里维护 Python 打包历史最久、口碑最好的 PPA。

[!WARNING] PPA 是个人或团队维护的第三方源,不是 Ubuntu 官方源。 添加它意味着你信任 deadsnakes 的维护者:他们提供的包拥有 root 权限,会在你的系统上运行安装脚本。请选择用户量大的知名 PPA,不要在生产服务器上随便加来路不明的源。若公司有安全合规要求,优先用方式三或官方 Docker 镜像。

# 先安装添加 PPA 所需的工具(最小化安装可能没有)
$ sudo apt install software-properties-common

# 添加 deadsnakes PPA(按提示回车确认即可)
$ sudo add-apt-repository ppa:deadsnakes/ppa

# 刷新源索引
$ sudo apt update

# 安装 3.13 —— 注意要装三个包,别只装第一个
$ sudo apt install python3.13 python3.13-venv python3.13-dev

方式二:pyenv(多版本频繁切换)。 它是纯用户级的版本管理器:所有 Python 版本装到 ~/.pyenv/versions/ 下,完全不碰 /usr/bin/,通过在该用户 PATH 前插桩实现切换。

# 安装编译 Python 需要的依赖
$ sudo apt install make build-essential libssl-dev zlib1g-dev libbz2-dev \
    libreadline-dev libsqlite3-dev liblzma-dev libffi-dev curl git wget

# 安装 pyenv
$ curl -fsSL https://pyenv.run | bash

# 把初始化代码写进 shell 配置,并让它立即生效
$ cat >> ~/.bashrc <<'EOF'
export PYENV_ROOT="$HOME/.pyenv"
export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init -)"
EOF
$ exec $SHELL

# 安装并使用 3.13(会现场编译,耗时较长)
# 补丁号以 python.org / 91 章核对方法为准,把 x 换成实际补丁号
$ pyenv install 3.13.x
$ pyenv global 3.13.x

方式三:源码编译到 /opt。 这是唯一能安装任意版本的途径(内网离线机尤其适用)。关键是把安装目录指定到 /opt/python3.13 这样的独立目录,而不是默认的 /usr/local —— 这样它和系统 Python 井水不犯河水。

# 安装编译依赖(与方式二那条相同,取其一即可)
$ sudo apt install build-essential libssl-dev zlib1g-dev libbz2-dev \
    libreadline-dev libsqlite3-dev liblzma-dev libffi-dev wget

# 下载并解压源码(补丁号以 python.org / 91 章核对方法为准,把 x 换成实际补丁号)
$ cd /tmp
$ wget https://www.python.org/ftp/python/3.13.x/Python-3.13.x.tgz
$ tar -xzf Python-3.13.x.tgz
$ cd Python-3.13.x

# 配置:--prefix 指定独立安装目录,--enable-optimizations 开启性能优化
$ ./configure --prefix=/opt/python3.13 --enable-optimizations --with-ensurepip=install

# 并行编译后安装 —— 务必写成 altinstall,原因见下方警告
$ make -j$(nproc)
$ sudo make altinstall

参数说明

参数 / 包名 作用
python3.13 解释器本体。不装这个别说 hello world
python3.13-venv 提供 venv 虚拟环境模块。 Ubuntu 的打包惯例把 venv 拆成独立包,漏装会得到「No module named venv」
python3.13-dev 提供 Python.h 等头文件。凡是 pip 要编译 C 扩展(numpy、psycopg2、lxml、mysqlclient…)都必须有它,否则报 fatal error: Python.h: No such file or directory
pyenv install <版本> 下载源码编译安装到 ~/.pyenv/versions/<版本>/
pyenv global <版本> 设当前用户全局默认版本;pyenv local 只作用于当前目录(写 .python-version),pyenv versions 列出所有已装版本
--prefix=/opt/python3.13 安装到独立目录,不占用 /usr/local,方便日后整个删除
--enable-optimizations 开启 PGO 优化,运行性能提升约 10–20%,代价是编译时间翻倍
--with-ensurepip=install 装完自动带上 pip,省得再单独引导
make -j$(nproc) nproc 输出 CPU 核心数,-j 并行编译
make altinstall 见下方警告,这是本节最重要的一条

[!IMPORTANT] 方式一的三个包是一套,只装 python3.13 是新手最常见的疏漏,表现为:能 import 标准库,一装第三方包就各种奇怪报错。

[!WARNING] 必须用 make altinstall,绝对不能用 make install。 两者唯一差别是:altinstall 不会创建不带版本号的符号链接。而 make install 会生成 /usr/local/bin/python3 —— 在 PATH 里 /usr/local/bin 排得比 /usr/bin 靠前,于是它会抢在系统 Python 前面生效。表面看只是版本变了,实际却是所有系统工具(apt、cloud-init…)从此开始用你的新解释器,一旦版本不兼容就全线崩溃(回忆本章开头的警告)。altinstall 只生成 /usr/local/bin/python3.13,多一个新命令,绝不影响 python3 —— 这正是我们要的「加法」。

验证方法

方式一,务必用带版本号的命令验证,同时确认系统 Python 安然无恙:

$ python3.13 --version
$ python3 --version

预期输出:

Python 3.13.x
Python 3.10.x

[!NOTE] 两个版本并存是正常且正确的结果。 python3 仍指向 3.10(系统用),多出来的 python3.13 给你的应用用。补丁号可能不同:python3.13 以实际安装的补丁号为准(核对方法见 python.org / 91 章),python3 以 3.10 开头为准。如果 python3 --version 变成了 3.13,说明哪里出错了,请立刻检查是不是有人动过 /usr/bin/python3。

方式二,看已安装版本列表(* 标记当前生效的):

$ pyenv versions

预期输出:

  system
* 3.13.x (set by /home/ubuntu/.pyenv/version)

方式三,验证的同时确认没有污染系统 —— 最后一行的报错才是正确的,说明没有生成那个会破坏系统工具的软链接:

$ /opt/python3.13/bin/python3.13 --version
$ which python3
$ ls -l /usr/local/bin/python3 2>&1

预期输出:

Python 3.13.x
/usr/bin/python3
ls: cannot access '/usr/local/bin/python3': No such file or directory

确认无误后把它加进 PATH 方便手动使用:

$ sudo tee /etc/profile.d/python313.sh > /dev/null <<'EOF'
export PATH=/opt/python3.13/bin:$PATH
EOF

$ source /etc/profile.d/python313.sh
RHEL / Rocky / AlmaLinux 9 对照
Ubuntu (apt) RHEL (dnf) 说明
sudo apt install python3.13 python3.13-venv python3.13-dev sudo dnf install python3.12 python3.12-devel RHEL 9 AppStream 自带 3.9 / 3.11 / 3.12,不需要 PPA
python3.13-venv 通常已包含在 python3.12-libs 里 RHEL 打包方式不同,venv 一般不独立成包
python3.13-dev python3.12-devel 头文件包命名不同
sudo apt install build-essential sudo dnf groupinstall "Development Tools" 编译器套件
sudo apt install libssl-dev sudo dnf install openssl-devel 缺了会导致 SSL module not available
make altinstall 同左 这条规则跨发行版通用
deadsnakes PPA 无此概念,用 AppStream module 或源码 dnf module list python

环境变量与 PATH

是什么

PATH:决定敲 python 时跑哪个。 shell 从左到右扫描这些目录,命中第一个就停,所以先后顺序直接决定版本优先级 —— 这正是上面 make install 会出大事的原因(/usr/local/bin 排在 /usr/bin 前面,悄悄就把系统 Python 顶掉了)。

$ echo $PATH

预期输出:

/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

PYTHONPATH:一般不需要设。 它的作用是给 import 语句额外增加搜索目录。初学者请遵循不要设置它:它会污染所有 Python 进程的模块搜索路径;它是全局且隐式的 —— 半年后你自己都忘了设过它,然后对着一个「明明没装却 import 得到」的模块百思不得其解;而且虚拟环境(下节)已经更干净地解决了「找不到自己写的模块」这个问题。唯一例外是需要临时加目录时,用行内前缀、不 export:

# 只对这一条命令生效,不留后患
$ PYTHONPATH=/opt/myproj/src python3 script.py

VIRTUAL_ENV:虚拟环境的标记变量。 激活 venv 后 shell 里会多出这个变量,内容是那个虚拟环境目录的路径,由 activate 脚本自动设置,你不要手动改。程序可以用它判断自己是否运行在虚拟环境里,pip、python 等工具也会参考它。

验证方法

$ source ~/myapp/.venv/bin/activate
$ echo $VIRTUAL_ENV
$ python3 -c "import sys; print(sys.executable)"

预期输出:

/home/ubuntu/myapp/.venv
/home/ubuntu/myapp/.venv/bin/python

[!TIP] 判断当前终端是否处于虚拟环境中,看命令行提示符开头的 (.venv) 最直观 —— 这比 echo 变量快得多。sys.executable 永远输出「当前正在运行的这个解释器的实际路径」,是定位「我到底在用谁」最权威的方式,比 which 更准。


最小必要配置

Python 层面真正的「必要配置」,核心就是每个项目一个独立的 venv,外加一份顺手的 pip 配置。

虚拟环境 venv

是什么。 虚拟环境是一个目录,里面装着一份「轻量的 Python」:

/home/ubuntu/myapp/.venv/
├── bin/python   → 指向创建它的那个解释器
├── bin/pip      → 只往这个环境装包
├── bin/activate → 激活脚本
└── lib/python3.13/site-packages/   ← 第三方包只装在这里

为什么非用不可?① 版本冲突 —— A 项目要 Django 4.2、B 项目要 Django 5.1,而一个包在一个解释器里只能有一个版本;② 不污染系统 —— pip 默认往系统级 site-packages 装包,装多了系统 Python 会变成一锅不知道谁放的依赖的杂烩,且难以回滚。类比:每个项目一个 venv,就像每个工程一个独立的工具箱,工具互不干扰,丢了一个箱子也不影响别的工程。

怎么做。 假设项目在 /home/ubuntu/myapp:

$ cd /home/ubuntu/myapp

# 用 3.13 创建名为 .venv 的虚拟环境(-m venv 是标准写法)
$ python3.13 -m venv .venv

# 激活(激活后提示符前面会出现环境名)
$ source .venv/bin/activate

# 停用
$ deactivate

激活后的提示符长这样:

(.venv) ubuntu@server:~/myapp$

activate 到底做了什么。 理解这个,你就不会再遇到「明明 activate 了却还是系统 Python」。它只做两件事:① 把 .venv/bin 插到 PATH 最前面 —— 于是敲 python / pip 命中的是这个虚拟环境里的而不是系统的;② 改 shell 提示符加 (.venv) —— 一个纯视觉提醒。仅此而已,虚拟环境不改变你的代码,它就是一个 PATH 技巧。

[!WARNING] 正因为 activate 是靠改 PATH 生效的,它只对当前这一个终端窗口有效。新开 SSH 会话、在 cron 里、在 systemd 服务里都不会继承它 —— 必须在那里重新激活,或者干脆用绝对路径(见 systemd 一节)。

参数说明(python3.13 -m venv .venv):

片段 作用
python3.13 用哪个解释器创建,环境里就是哪个版本。 想建 3.12 环境就写 python3.12 -m venv
-m venv 以模块方式运行 venv。不要直接写 venv .venv,那种老写法已过时且该命令可能不存在
.venv 目录名。点开头是为了默认隐藏,且它不该提交到 Git;叫这个名字还因为主流编辑器会自动识别它
--system-site-packages(可选) 允许访问系统包。默认不用,代价是失去隔离性

包的安装与导出。 装依赖务必先激活环境;最可靠的做法是把依赖交给 requirements.txt:

$ source .venv/bin/activate
$ pip install requests flask gunicorn

# 把当前环境里所有包及精确版本导出成文件
$ pip freeze > requirements.txt

# 换台机器 / 新环境时,一键还原
$ pip install -r requirements.txt

[!IMPORTANT] pip freeze 导出的是所有已安装包及其精确版本(包括依赖的依赖),价值在于可复现:另一台机器上 pip install -r 能得到完全一致的环境。请务必把 requirements.txt 提交到 Git —— 这是团队协作和灾难恢复的救命文件。

[!WARNING] 永远不要用 sudo pip install。 sudo 会以 root 身份执行 pip,此时虚拟环境的 PATH 设置被重置,pip 于是把包装进了系统 Python 的 site-packages。双重打击:① 违背 venv 隔离的初衷,你的虚拟环境里根本没装上这个包;② 可能覆盖 apt 管理的系统包,导致 apt 后续升级失败。症状通常是「虚拟环境里 pip install 成功了,但 import 还是找不到」。如果 pip 报权限错误,说明你还没激活虚拟环境,而不该用 sudo 解决。

pip 配置

pip 默认从官方 PyPI 拉包,国内直连常常很慢甚至超时,换镜像源是最简单有效的加速手段,详见 国内镜像加速。

# 永久配置(推荐):写入 ~/.config/pip/pip.conf,之后所有 pip 命令都生效
$ pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple

# 临时指定:只这一次生效
$ pip install flask -i https://pypi.tuna.tsinghua.edu.cn/simple

# 顺手把 pip 自己升级到最新
$ python -m pip install --upgrade pip
命令片段 作用
pip config set global.index-url <地址> 写入用户级配置文件,之后所有 pip 命令都生效
-i <地址> index-url 的简写,只对当前这条命令生效
python -m pip 比裸 pip 可靠,原因见下方 TIP

[!TIP] 优先用 python -m pip 而不是裸 pip。 裸 pip 无法保证它对应哪个解释器 —— 尤其在多版本环境里,pip 可能是 3.10 的而你的虚拟环境是 3.13,于是包装错了地方。python -m pip 的含义是「用当前这个 python 解释器去运行 pip 模块」,两者必定一致,此法永远不会出错。

核对配置是否写成功:

$ pip config list

预期输出:

global.index-url='https://pypi.tuna.tsinghua.edu.cn/simple'

验证方法

1. 版本与路径

$ python3.13 --version
$ python3 --version
$ which python3
$ python3 -c "import sys; print(sys.executable)"

预期输出:

Python 3.13.x
Python 3.10.x
/usr/bin/python3
/usr/bin/python3

[!NOTE] 第二行必须以 3.10 开头(补丁号可能不同)、第三行必须是 /usr/bin/python3 —— 这证明系统 Python 没被动过。如果变了,回到本章开头的 WARNING 重新读一遍。

2. 虚拟环境隔离验证

$ cd ~/myapp
$ source .venv/bin/activate
$ which python
$ python -c "import sys; print(sys.prefix)"
$ deactivate
$ which python3

预期输出:

/home/ubuntu/myapp/.venv/bin/python
/home/ubuntu/myapp/.venv
/usr/bin/python3

[!IMPORTANT] 激活后 sys.prefix 必须是 .venv 路径。如果输出的是 /usr 或 /usr/local,说明没有真正激活虚拟环境 —— 此时 pip 会往系统里装包,请在搞砸系统之前回到上一节检查。

3. 跑一个最小 Web 服务并用 curl 验证

光看版本号不足以证明「这东西能干活」。下面起一个真实的 HTTP 服务并用 curl 打一个真实请求过去 —— 这一步同时验证了:虚拟环境生效、第三方包能装上能用、端口能监听、HTTP 响应正确。

$ source ~/myapp/.venv/bin/activate
$ pip install flask
$ cat > ~/myapp/app.py <<'EOF'
from flask import Flask, jsonify
import sys

app = Flask(__name__)

@app.route("/")
def index():
    return jsonify({"python": sys.version.split()[0],
                    "executable": sys.executable})

if __name__ == "__main__":
    app.run(host="127.0.0.1", port=5000)
EOF

$ python ~/myapp/app.py

预期输出(注意最后一行,服务开始监听):

 * Serving Flask app 'app'
 * Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment.
 * Running on http://127.0.0.1:5000

另开一个 SSH 窗口(或先把上面这个进程用 Ctrl+Z + bg 放后台),请求它:

$ curl -s http://127.0.0.1:5000/

预期输出:

{"executable":"/home/ubuntu/myapp/.venv/bin/python","python":"3.13.x"}

看到 "python":"3.13.x"(补丁号以实际安装为准)且 "executable" 指向 .venv,就证明了 Flask 装在虚拟环境里、跑的就是这个虚拟环境的解释器、HTTP 链路全程通畅。验证完按 Ctrl+C 停止服务。

[!IMPORTANT] app.run(...) 这种 Flask 自带的开发服务器仅供调试,性能和安全性都不足以承载生产流量。正式部署请换成 gunicorn / uWSGI(WSGI 服务器)作为应用容器,再用 systemd 托管并用 Nginx 做反向代理,流程见下文和 29 Nginx 反代。另外上面示例只监听 127.0.0.1,外部访问不到;要让外网能访问需调整监听地址并做好防火墙,详见 16 防火墙。


托管为 systemd 服务

Python Web 应用上线同样交给 systemd 托管,详见 systemd 服务管理。

[!TIP] 在 systemd 里不要用 source .venv/bin/activate,直接写虚拟环境里 Python 的绝对路径。 原因:activate 本质是个 shell 脚本,靠修改 PATH 生效;而 systemd 启动进程时不经过任何 shell,也不继承你终端里的环境变量 —— 你在终端里 activate 得再熟练,跟服务进程没有半点关系。直接写 /home/ubuntu/myapp/.venv/bin/python(或 gunicorn 的路径)则完全不依赖 PATH、不依赖任何初始化顺序,这也是解决「服务起不来但命令行跑得好好的」这类经典问题的关键。

怎么做

以 gunicorn 为例,app.py 位于 /home/ubuntu/myapp:

[Unit]
Description=My Python Flask Application
After=network.target

[Service]
Type=simple
User=ubuntu
Group=ubuntu
WorkingDirectory=/home/ubuntu/myapp

# 关键:全部用绝对路径,直接调用虚拟环境内的可执行文件
ExecStart=/home/ubuntu/myapp/.venv/bin/gunicorn \
    --workers 2 \
    --bind 127.0.0.1:8000 \
    --access-logfile - \
    --error-logfile - \
    app:app

Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
$ sudo systemctl daemon-reload
$ sudo systemctl enable --now myapp.service
$ sudo systemctl status myapp.service

参数说明

配置项 作用
ExecStart=.../.venv/bin/gunicorn gunicorn 装在 venv 里,写它的绝对路径;误写成 /usr/bin/gunicorn 会导致启动失败
--workers 2 worker 进程数。经验公式 CPU 核心数 × 2 + 1
--bind 127.0.0.1:8000 只监听本地,由 Nginx 对外反代。这是更安全的做法
--access-logfile - 输出到 stdout,交给 journald 统一收集
app:app 前半是文件名 app.py,后半是文件里的 Flask 实例变量名
User= / Group= 指定运行用户。虚拟环境目录必须对该用户可读

验证方法

$ sudo systemctl is-active myapp.service
$ curl -s -o /dev/null -w "HTTP %{http_code}\n" http://127.0.0.1:8000/
$ sudo journalctl -u myapp.service -n 20 --no-pager

预期输出:

active
HTTP 200

第一行确认服务活着,第二行确认真的能响应请求,第三行看有没有异常日志。


常见问题

现象 根因 解决
敲 python 报 command not found Ubuntu 只有 python3,故意不提供 python 想省事可用 python-is-python3 或 alternatives;推荐:不要纠结,习惯用 python3
python 存在但指向 Python 2 老系统上 python 可能指向已 EOL 的 Python 2 别用裸 python,一律写 python3
apt search python3.12 找不到包 22.04 官方源没有 3.12+ 添加 deadsnakes PPA,或用 pyenv / 源码编译
add-apt-repository: command not found 缺 software-properties-common sudo apt install software-properties-common
No module named venv 漏装了 python3.X-venv 包 sudo apt install python3.13-venv
pip 装包报 fatal error: Python.h: No such file or directory 缺头文件包 sudo apt install python3.13-dev
No module named _ssl / SSL module not available 编译前缺 OpenSSL 开发库 sudo apt install libssl-dev 后重新 configure + make
pip install 慢到超时 连的是国外官方源 pip config set global.index-url ...,见 33 镜像加速
pip 装包报 externally-managed-environment Ubuntu 启用 PEP 668 保护,禁止往系统 Python 里装包。补注:EXTERNALLY-MANAGED 标记是 Ubuntu 23.04+/Debian 12 引入,22.04 自带的 pip 22.0.2 不报此错;若在 22.04 遇到请先想 venv 这是保护机制,别用 --break-system-packages 绕过。 正确做法是建 venv
明明 source activate 了,which python 还是 /usr/bin/python ①用了 sh 而非 bash(activate 是 bash 脚本);②误用 sudo;③在脚本里 source 后没在同一进程继续跑 用 echo $0 确认当前是 bash;不要用 sh activate;systemd / 脚本场景改用绝对路径
venv 里 pip install 成功,运行时却 ModuleNotFoundError pip 和 python 不是同一个解释器(最常源于 sudo pip 或裸 pip) 一律用 python -m pip install;用 which python 与 pip --version 核对路径一致
ModuleNotFoundError: No module named 'myapp' 不在项目根目录运行,或该目录不在搜索路径 先 cd 到项目根目录;生产代码建议打包安装(pip install -e .)而不是设 PYTHONPATH
不小心 make install 覆盖了系统 python3 生成了破坏性软链 /usr/local/bin/python3 立刻检查 which python3 是否变成 /usr/local/bin/python3;若是,谨慎处理该软链使 /usr/bin/python3 重新生效并核查系统包完整性。操作前先备份数据,此问题切勿再拖延扩大

本章命令速查

命令 作用
python3 --version 查看系统 Python 版本(应为 3.10,不变才对)
python3.13 --version 查看新装版本(用带版本号的命令,别只看 python3)
which python3 查看系统调用的解释器路径(应为 /usr/bin/python3)
python3 -c "import sys; print(sys.executable)" 权威确认当前运行的解释器实际路径
sudo add-apt-repository ppa:deadsnakes/ppa 添加 deadsnakes PPA 源
sudo apt install python3.13 python3.13-venv python3.13-dev 安装 Python 3.13(三个都要装)
pyenv install <版本> / pyenv global <版本> 安装 / 切换 pyenv 管理的版本
./configure --prefix=/opt/python3.13 --enable-optimizations 源码安装的配置步骤(关键是 --prefix)
sudo make altinstall 安装但不覆盖系统 python3(绝不能写成 install)
python3.13 -m venv .venv 创建虚拟环境
source .venv/bin/activate / deactivate 激活 / 退出虚拟环境
python -m pip install <包> 装包(比裸 pip 可靠,保证与当前解释器一致)
pip install -r requirements.txt 按清单批量还原依赖
pip freeze > requirements.txt 导出依赖清单(提交到 Git)
pip config set global.index-url <地址> 永久配置镜像源
echo $VIRTUAL_ENV 查看虚拟环境目录(激活时才有值)

相关章节

📄 22 Node.js 运行环境安装与配置 22-nodejs.md


title: 22 Node.js 运行环境安装与配置 updated: 2026-09 applies-to: Ubuntu 22.04 LTS

部署 Node 项目只需要三件事:装上运行时 → 装依赖 → 让进程别死。本章讲清这三步,以及服务器上最容易踩的坑:nvm 装的东西,systemd 看不见。

[!NOTE] 本章所有版本号截至 2026-09,以 版本速查表 为唯一权威来源。


一、版本选择:为什么是 Node.js 24 LTS

是什么

Node.js 用奇偶区分版本性质:偶数版(20 / 22 / 24)是 LTS,有约 30 个月安全更新;奇数版(21 / 23 / 25)是短期版,只有约 6 个月支持期。这样设计是因为 Node 每 6 个月一个大版本,不区分就没人记得住该用哪个。记住一句话:偶数能上线,奇数只能玩。(注意:Node 20 已于 2026-04 停止支持(EOL),老项目仍能运行但不再有安全补丁,新项目请选 24。)

怎么做

  1. 看项目 package.json 的 engines 字段,确认它对 Node 版本的要求
  2. 没有特殊要求就装 24 LTS(截至 2026-09 的当前 LTS 线);装之前先 apt policy nodejs 看源里有什么,别盲装
# 看系统源里 nodejs 包的候选版本
$ apt policy nodejs

参数说明

项目 值 说明
本章推荐 Node.js 24 LTS 偶数 = LTS,奇数 = 短期版
Ubuntu 22.04 源默认 12.x(早已 EOL,视同没有) 版本极旧且已停止维护,按「源里没有」处理
获取方式 NodeSource 官方源 / nvm 见下一节对比
npm 随 Node.js 一起提供 不需要单独安装
版本核对 版本速查表 全篇唯一权威来源

版本号 24.x.y:24 主版本(决定 LTS 与否)/x 次版本(新特性)/y 补丁(修 bug 与安全,建议跟随升级)。

验证方法

$ node -v

预期输出:

v24.13.0

常见问题

现象 根因 解决
apt install nodejs 装出来是 12.x 用了系统源里的旧包 卸掉,改用 NodeSource 源或 nvm
项目要求 Node 18,我装了 24 版本不匹配 用 nvm 多版本共存切换
别人用 22 也能跑,我选哪个 LTS 线同时有多个 选 engines 允许范围内最新的 LTS
想跟上补丁版本 小版本只修 bug 和安全 sudo apt update && sudo apt upgrade nodejs

二、三条安装路线对比

是什么

三条路线本质是「装到哪、谁能用」的区别:

方式 适用场景 优点 缺点
NodeSource 官方源 生产服务器、只需一个版本 装到 /usr/bin,所有用户和 systemd 都能找到;可 apt 统一升级 换大版本要换源;需要 root
nvm 开发机、需要多版本切换 一个命令切版本;不用 sudo;不污染系统目录 只装在当前用户家目录,其他用户和 systemd 看不到
系统源 apt install nodejs 无 一条命令,最简单 版本极旧且已停止维护,装了等于埋雷

怎么做

路线一:NodeSource 官方源(服务器推荐)

# 下载并执行 NodeSource 源配置脚本
$ curl -fsSL https://deb.nodesource.com/setup_24.x | sudo -E bash -

# 安装,nodejs 包自带 npm
$ sudo apt install -y nodejs

[!NOTE] 管道执行脚本有风险:你没看到内容就让它跑了。正规做法是先 curl -fsSL ... -o setup.sh、less setup.sh 看过,再 sudo -E bash setup.sh。习惯:任何 curl ... | bash 都先下载、看过、再执行。

路线二:nvm(开发机 / 多版本推荐)

# 安装 nvm(装到 ~/.nvm,并自动写入 ~/.bashrc);版本号以 nvm 的 GitHub releases 为准
$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash

# 让当前终端立即生效,然后安装、切换、设为默认
$ source ~/.bashrc
$ nvm install 24
$ nvm use 24
$ nvm alias default 24

[!IMPORTANT] nvm 装的 Node 只存在于当前用户的 ~/.nvm/versions/node/... 目录里,这是服务器上最常见的坑:换一个用户登录 node 就不存在;systemd 服务、crontab、CI 脚本里也找不到 node —— 它们不加载 ~/.bashrc。

生产服务器请用 NodeSource 路线。 已经用了 nvm 的,systemd unit 里必须写 node 的绝对路径。

[!WARNING] 路线三(系统源 sudo apt install -y nodejs npm)不要在服务器上用。 它装出来的是早已停止维护的旧版本,不再收到安全补丁,且 npm install 常因引擎不兼容直接失败。

参数说明

片段 含义
curl -fsSL / sudo -E bash - -f 失败不输出错误页、-s 静默、-L 跟随重定向;-E 保留环境变量(如代理)
nvm install 24 装 24 主版本下的最新 LTS
nvm use 24 只影响当前终端,关掉就还原
nvm alias default 24 改默认版本,影响新开的终端
RHEL / Rocky / AlmaLinux 9 对照
Ubuntu (apt) RHEL (dnf) 说明
curl -fsSL https://deb.nodesource.com/setup_24.x | sudo -E bash - curl -fsSL https://rpm.nodesource.com/setup_24.x | sudo bash - 两个体系都提供源脚本
sudo apt install -y nodejs sudo dnf install -y nodejs 包名相同,都自带 npm
sudo apt remove -y nodejs sudo dnf remove -y nodejs 卸载

验证方法

$ which node

预期输出(NodeSource 路线;nvm 路线会是 /home/ubuntu/.nvm/versions/node/v24.13.0/bin/node,看到家目录路径就要警惕 systemd 找不到):

/usr/bin/node

常见问题

现象 根因 解决
node: command not found nvm 刚装完没加载 source ~/.bashrc,或重开终端
sudo node 找不到而 node 可以 nvm 在普通用户家目录,sudo 的 PATH 不同 服务器改用 NodeSource;或用 node 绝对路径
换用户登录后没有 node nvm 只对安装它的那个用户生效 换 NodeSource 路线
curl: (60) SSL certificate problem CA 证书过旧或系统时间不对 sudo apt install -y ca-certificates;date 核对时间

三、环境变量与 NODE_ENV

是什么

环境变量是「告诉程序当前在哪跑」的开关。Node 生态里最重要的是 NODE_ENV:设为 production 时,npm 装依赖会跳过 devDependencies(babel、eslint 这类开发期工具不装),框架也会关闭调试输出、启用缓存。

怎么做

NodeSource 方式:node 在 /usr/bin,本来就在默认 PATH 里,不需要配任何环境变量。

nvm 方式:安装脚本已往 ~/.bashrc 写入加载片段,新终端自动生效。若没写进去,手动补(随后 source ~/.bashrc 生效):

# 追加 nvm 加载片段到 ~/.bashrc
$ cat >> ~/.bashrc <<'EOF'
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
EOF
$ source ~/.bashrc

给应用设置生产环境:命令行里写 NODE_ENV=production node app.js;systemd 服务则写在 unit 的 [Service] 段里,一行 Environment=NODE_ENV=production。

参数说明

变量 作用 建议值
PATH 命令搜索路径 NodeSource 无需改;nvm 由加载片段自动追加
NVM_DIR nvm 安装根目录 ~/.nvm,一般不用改
NODE_ENV 运行环境标识 服务器一律 production
NODE_OPTIONS 传给 node 的额外参数 内存不足时 --max-old-space-size=2048

验证方法

$ node -p "process.env.NODE_ENV"

预期输出:

production

常见问题

现象 根因 解决
设了 NODE_ENV 但框架没切换 变量没传进进程 printenv | grep NODE_ENV 确认;systemd 要写 Environment=
systemd 服务里 node 找不到 systemd 不读 .bashrc,nvm 的 node 不在 PATH unit 里写绝对路径 /home/<用户>/.nvm/versions/node/v24.13.0/bin/node
依赖里混进很多开发工具包 忘了 NODE_ENV=production 部署时显式设置,或 npm ci --omit=dev
等号两边加了空格导致失效 shell 语法要求紧贴 写成 NODE_ENV=production,等号两侧不留空格

四、最小必要配置

装完 Node 到能投产,三件事配齐即可:

# ① 生产环境标识:部署与运行统一 production(systemd 场景写 Environment=NODE_ENV=production)
$ NODE_ENV=production npm ci --omit=dev

# ② 生产启动方式:不要裸跑 node(崩了不会自起),用 PM2 托管(见下一节),启动命令固定进部署脚本
$ pm2 start app.js --name myapp

# ③ 日志位置:PM2 默认把 stdout/stderr 落在 ~/.pm2/logs/ 下,别只让日志打在终端里
$ pm2 logs myapp

五、npm 基础、pnpm 与换源

是什么

  • npm:Node 自带的包管理器,负责下载依赖、执行脚本;pnpm 是另一个包管理器,用硬链接共享同一个 store,装得快、省磁盘,命令与 npm 高度一致
  • 换源:把下载地址从国外的 registry.npmjs.org 指向国内镜像站,内容一样但快得多

怎么做

# 装依赖(读 package.json,写 package-lock.json)
$ npm install

# 【部署/CI 用这个】严格按 lock 文件安装,版本分毫不差
$ npm ci

# 安装全局命令 / 执行 package.json 里定义的脚本 / 装 pnpm
$ npm install -g pm2
$ npm run build
$ npm i -g pnpm

# 换国内源(registry.npmmirror.com 是国内最常用的 npm 镜像)
$ npm config set registry https://registry.npmmirror.com

[!TIP] npm ci 与 npm install 的区别是部署时必须知道的:install 会按语义化版本更新 lock 里允许范围内的版本(本地开发用);ci 不改 lock 且会先删掉 node_modules 再精确安装,保证「测试装的 = 生产装的」。 另外 Node.js 自带 corepack,一条 corepack enable 就能让 pnpm / yarn 按项目声明的版本自动切换,不必手动全局安装。(corepack 自 Node 25 起不再随发行版捆绑;本章基线的 Node 24 内可用。)

等价的用户级配置文件是 ~/.npmrc:

registry=https://registry.npmmirror.com

更多镜像与排错见 国内镜像加速。

参数说明

命令 / 参数 含义
install / i 安装依赖
-g 全局安装(供命令行直接使用)
--save-dev / -D 装成开发依赖,生产环境不装
ci 按 lock 精确安装,先清 node_modules
run <脚本名> 执行 package.json 的 scripts 里定义的脚本
registry 包下载地址配置项

验证方法

$ npm config get registry

预期输出:

https://registry.npmmirror.com/

常见问题

现象 根因 解决
EACCES: permission denied 写 /usr/lib/node_modules 用过 sudo npm install -g,目录属主变 root 不要用 sudo npm,改用下方「修复 EACCES」
npm install 极慢或卡住 默认 registry 在国外 换国内镜像
npm ci 报 lock 与 package.json 不一致 有人只改了 package.json 本地跑 npm install 生成新 lock 并提交
换源后拉私有包失败 镜像站没有私有包 单独配 @scope:registry=<私有源>

修复 EACCES 的正确做法(不是加 sudo):

$ mkdir -p ~/.npm-global
$ npm config set prefix ~/.npm-global
$ echo 'export PATH="$HOME/.npm-global/bin:$PATH"' >> ~/.bashrc
$ source ~/.bashrc

六、PM2 进程守护(重点)

是什么

直接在终端跑 node app.js 有两个致命问题:进程崩了不会自己起来、服务器重启后不会自启。PM2 是 Node 生态的进程管理器,负责崩溃自动重启、开机自启、日志归集、多实例负载均衡。

[!IMPORTANT] 本章以 PM2 承担 systemd 托管职能(对应全书七段结构中的「systemd 托管」段):pm2 startup 底层同样是生成 systemd unit 并 enable(见下文),崩溃自启、开机自启、日志归集由它统一负责。若更倾向直接手写 systemd unit(日志统一进 journald),见 进程守护;PM2 的优势在于一台机器跑多个 Node 应用、集群模式(多核多实例)、或团队已熟悉 PM2。

怎么做

# 全局安装 pm2
$ npm i -g pm2

# 启动脚本文件并起个名字;靠 npm scripts 启动的用引号包住整条命令
$ pm2 start app.js --name myapp
$ pm2 start "npm run start" --name myapp

# 查看列表 / 实时日志 / 终端仪表盘
$ pm2 list
$ pm2 logs myapp
$ pm2 monit

# 重启 / 停止 / 删除
$ pm2 restart myapp
$ pm2 stop myapp
$ pm2 delete myapp

开机自启(两步缺一不可):

# 生成一个 systemd unit 并 enable 它
$ pm2 startup

# 把当前应用列表存盘,重启后按这份列表恢复
$ pm2 save

[!NOTE] pm2 startup 会打印一条 sudo env PATH=... pm2 startup ... 命令,照着执行那一条才真正生效。它做的事是:在 /etc/systemd/system/ 下生成 pm2-<用户>.service 并 systemctl enable,所以你也能用 systemctl status pm2-<用户> 看它状态。

参数说明

命令 / 参数 含义
--name myapp 给进程起名,后续操作用它指代
--watch 文件变化自动重启(生产别用,开发用)
-i max / -i 2 集群模式:按 CPU 核数 / 指定数量起多实例
--max-memory-restart 500M 内存超阈值自动重启,防内存泄漏
pm2 save 把应用列表写入 ~/.pm2/dump.pm2
pm2 startup 生成 systemd unit 实现开机自启

验证方法

$ pm2 list

预期输出(看两点:status 是 online、↺ 重启次数是 0):

┌────┬───────┬──────┬───────┬────────┬────────┐
│ id │ name  │ mode │ pid   │ uptime │ status │
├────┼───────┼──────┼───────┼────────┼────────┤
│ 0  │ myapp │ fork │ 12345 │ 3m     │ online │
└────┴───────┴──────┴───────┴────────┴────────┘

用最小 HTTP 服务验证整条链路(hello.js 监听 3000 并返回 hello node):

$ node hello.js &
$ curl localhost:3000

预期输出:

hello node

常见问题

现象 根因 解决
pm2: command not found npm 全局目录不在 PATH,或 nvm 装的其他用户看不到 npm config get prefix 查路径并加进 PATH
重启机器后服务没起来 只跑了 startup,没跑 pm2 save 补 pm2 save,用 pm2 resurrect 验证能恢复
↺ 重启次数猛涨 启动即崩溃(端口占用、配置缺失) pm2 logs myapp --lines 100 看报错
systemd 服务里 node 找不到 nvm 的 node 不在 systemd 的 PATH 写绝对路径 /home/<用户>/.nvm/versions/node/v24.13.0/bin/node
EADDRINUSE :::3000 端口已被占用 sudo ss -lntp | grep 3000 查占用者,换端口或停旧的
JavaScript heap out of memory 默认堆上限不够 NODE_OPTIONS=--max-old-space-size=2048
listen EACCES 监听 80/443 1024 以下端口只有 root 能绑定 见下方 WARNING,不要 root 跑 node

[!WARNING] 绝对不要用 root 运行 Node.js 进程。 应用一旦有漏洞(任意文件读取、命令注入),攻击者拿到的就是 root,整台机器沦陷。 需要对外提供 80/443 时,正确做法是:应用监听 3000 等高端口 + Nginx 反向代理,见 Nginx 反代。


七、用 ecosystem.config.js 管理多应用

是什么

一台机器跑多个 Node 应用、或要给应用配一堆环境变量时,命令行参数会又长又容易忘。 ecosystem.config.js 把应用列表、启动方式、环境变量写进文件,一条命令全拉起。

怎么做

module.exports = {
  apps: [
    {
      name: 'api',
      script: './app.js',
      instances: 2,
      exec_mode: 'cluster',
      env: { NODE_ENV: 'development', PORT: 3001 },
      env_production: { NODE_ENV: 'production', PORT: 3001 }
    },
    {
      name: 'worker',
      script: './worker.js',
      env_production: { NODE_ENV: 'production' }
    }
  ]
};
# 用默认 env 启动配置里的所有应用
$ pm2 start ecosystem.config.js

# 用 env_production 段的变量启动,改完配置再重启 + 保存
$ pm2 start ecosystem.config.js --env production
$ pm2 restart ecosystem.config.js --env production
$ pm2 save

参数说明

字段 含义
name 应用名,pm2 命令里用它指代
instances 实例数;max 表示按 CPU 核数
exec_mode fork(单进程)或 cluster(多实例共享端口)
env / env_production 默认变量 / --env production 时生效并覆盖同名项
max_memory_restart 内存超阈值自动重启,如 '500M'

验证方法

$ pm2 list

预期输出(api 有两个 pid,说明 cluster 模式生效):

┌────┬────────┬─────────┬───────┬────────┐
│ id │ name   │ mode    │ pid   │ status │
├────┼────────┼─────────┼───────┼────────┤
│ 0  │ api    │ cluster │ 12345 │ online │
│ 1  │ api    │ cluster │ 12346 │ online │
└────┴────────┴─────────┴───────┴────────┘

常见问题

现象 根因 解决
启动报语法错误 用了 JSON 语法或中文逗号 必须是 module.exports = { ... } 的 JS 语法
--env production 后变量没变 段名写错 段名必须叫 env_production,与命令参数对应
cluster 模式报端口冲突 instances > 1 却用 fork 多实例共享端口只能用 cluster
重启机器后应用没了 忘记 pm2 save 改完配置一定要 pm2 save

本章命令速查

命令 作用
node -v / npm -v 查看 Node.js / npm 版本
apt policy nodejs 查看系统源里的 nodejs 版本
curl -fsSL https://deb.nodesource.com/setup_24.x | sudo -E bash - 添加 NodeSource 24.x 官方源
sudo apt install -y nodejs 安装 Node.js(含 npm)
nvm install 24 / nvm use 24 / nvm alias default 24 安装 / 切换 / 设为默认版本
npm install / npm ci 安装依赖 / 按 lock 精确安装
npm run build 执行 package.json 里的 build 脚本
npm config set registry <url> / get registry 设置 / 查看 npm 镜像源
npm i -g pnpm / pnpm install 安装 pnpm / 用它装依赖
pm2 start app.js --name myapp 用 PM2 启动应用并命名
pm2 start "npm run start" --name myapp 用 npm 脚本启动
pm2 list / pm2 logs myapp / pm2 monit 看列表 / 看日志 / 终端仪表盘
pm2 restart | stop | delete myapp 重启 / 停止 / 删除应用
pm2 startup / pm2 save / pm2 resurrect 生成自启 unit / 保存列表 / 恢复列表
pm2 start ecosystem.config.js --env production 按配置文件启动并加载生产变量

相关章节

📄 MySQL 安装与配置 23-mysql.md


title: 23 MySQL 安装与配置 updated: 2026-09 applies-to: Ubuntu 22.04 LTS

MySQL 是 Web 项目里最常见的开源关系型数据库。本章走完一条完整链路:选版本 → 安装 → 安全初始化 → 最小必要配置 → 建库建用户 → 备份 → 托管给 systemd。

[!IMPORTANT] 本章版本号截至 2026 年 9 月。版本信息的唯一权威来源是 版本速查表,冲突时以速查表为准。

版本选择

是什么

MySQL 从 8.0 之后把版本线拆成两条:LTS 线(长期支持,只修 bug 与安全问题)和创新线(Innovation,频繁加特性,支持期很短)。选错线是新手第一个坑。

项目 值 说明
推荐版本 8.0.x 生态最成熟,驱动、ORM、运维脚本兼容性最好
新项目可选 8.4 LTS 更新,支持期更长,但老驱动兼容性更差
生产不建议 9.x 创新版,支持期短,只适合尝鲜
维护周期 8.0 已进入生命周期后段,8.4 LTS 支持更久 精确到期日查官方文档或 endoflife.date
License 社区版 GPL v2,另有商业授权 自用免费;闭源分发或要官方兜底支持就买商业许可

怎么做

装之前先确认源里有什么版本 —— 这是避免「装完发现版本不对」的唯一办法:

# 看系统源里 mysql-server 的候选版本
$ apt policy mysql-server
预期输出:
mysql-server:
  Installed: (none)
  Candidate: 8.0.4x-0ubuntu0.22.04.1

Candidate 就是 apt install 会装上的版本。小版本号随系统安全更新变化,大版本是 8.0 即符合本章基线;完整的 Version table 会列出它来自哪个源。

参数说明

场景 选哪个 为什么
学习、跟教程做、老项目迁移 8.0.x 网上绝大多数教程都基于 8.0,出问题最好搜
全新项目、团队能自己排错 8.4 LTS 支持周期更长,不用过两年就被迫大版本迁移
想尝鲜新特性 9.x 可以,但别放生产:创新版到期后没有安全补丁
已有 8.0 生产库要升级 不要原地升 见下方 WARNING

[!WARNING] 数据库大版本不要原地 apt upgrade。 正确做法:新版本另起实例 → 导数据 → 业务切换 → 观察。原地升到 8.4 可能直接起不来,且数据目录格式升级后无法回退。

[!NOTE] 8.4 的一个兼容性变化:8.4 起 mysql_native_password 默认不再启用。若老驱动只能走旧插件,8.4 上需额外启用它(写法以官方文档为准),否则优先升级应用侧驱动。

验证方法

# 装完回头核对真实版本
$ mysql --version
预期输出:
mysql  Ver 8.0.4x for Linux on x86_64 ((Ubuntu))

常见问题

现象 根因 解决
apt policy 里只有 8.0,想要 8.4 系统源只提供一条版本线 添加 MySQL 官方 apt 源,见「安装」
装的居然是 MariaDB 某些镜像源/面板把 mysql-server 指向 MariaDB apt policy mysql-server 看 Candidate 的源地址,卸载后重装

安装

是什么

方式 拿到的版本 复杂度 适合
系统源 apt 8.0.x 低 新手首选,一条命令,跟着系统安全更新走
MySQL 官方 apt 源 8.0 / 8.4 自选 中 需要 8.4 LTS 或紧跟官方补丁
Docker 任意 中 本地开发、多版本并存,见 Docker

怎么做

方式一:系统源(推荐)

# 刷新索引,再装服务端(会连带装好客户端)
$ sudo apt update
$ sudo apt install -y mysql-server

装完服务会自动启动并设为开机自启,可用 sudo systemctl status mysql 确认。

方式二:MySQL 官方 apt 源(需要 8.4 时用)

[!WARNING] 不要用 apt-key add。 apt-key 已废弃并在新版 Ubuntu 上被移除;把密钥加进全局钥匙环,等于信任它签名的所有源。正确做法:密钥放 /usr/share/keyrings/,源条目里用 signed-by= 显式指定,一把钥匙只开一把锁。

方式二 A —— 官方源配置包(mysql-apt-config,有交互界面):

# 包版本号会更新,先到 dev.mysql.com/downloads/repo/apt/ 复制最新 .deb 链接
$ wget <官方页面复制的 mysql-apt-config 链接>

# 安装源配置包,弹出界面里选版本(如 mysql-8.4-lts)后确认退出
$ sudo dpkg -i mysql-apt-config_*.deb

# 加完源必须刷新索引,否则 apt 看不到新版本
$ sudo apt update

方式二 B —— 手写 signed-by 源:

# 1. 下载官方 GPG 密钥,转成二进制钥匙环
$ curl -fsSL <官方 GPG 密钥 URL> | sudo gpg --dearmor -o /usr/share/keyrings/mysql.gpg

# 2. 写源条目,组件名(mysql-8.4-lts)以官方文档为准
$ echo "deb [arch=amd64 signed-by=/usr/share/keyrings/mysql.gpg] https://repo.mysql.com/apt/ubuntu jammy mysql-8.4-lts" | sudo tee /etc/apt/sources.list.d/mysql.list

# 3. 刷新并确认候选版本,再安装
$ sudo apt update
$ apt policy mysql-server
$ sudo apt install -y mysql-server

[!NOTE] 密钥 URL 和组件名会随官方调整,务必从官方文档复制。装之前先看 apt policy 的 Candidate 是不是你要的版本。

方式三:Docker

一句带过,见 Docker:docker run -d --name mysql8 -e MYSQL_ROOT_PASSWORD='<强密码>' -p 127.0.0.1:3306:3306 mysql:8.0。

[!WARNING] 上面端口特意写成 127.0.0.1:3306:3306。不要写 -p 3306:3306 —— 那会把数据库暴露到所有网卡,等于对公网开放 3306。

参数说明

包名 作用
mysql-server 服务端 + 客户端 + 数据目录初始化脚本
mysql-client 客户端命令(mysql、mysqladmin、mysqldump)

验证方法

$ mysql --version
$ dpkg-query -W -f='${Version}\n' mysql-server
预期输出:
mysql  Ver 8.0.4x for Linux on x86_64 ((Ubuntu))
8.0.4x-0ubuntu0.22.04.1

常见问题

现象 根因 解决
apt update 报 GPG 错误 密钥放错位置或 signed-by 路径写错 检查 /usr/share/keyrings/mysql.gpg 是否存在、路径是否一致
加了官方源还是装到 8.0 交互界面没选 8.4,或系统源优先级更高 apt policy 看 Candidate,用 apt install mysql-server=版本号 精确指定
RHEL / Rocky / AlmaLinux 9 对照
Ubuntu (apt) RHEL (dnf) 说明
sudo apt install mysql-server sudo dnf install mysql-server RHEL 9 源里默认是 MariaDB 血统,要用 Oracle 版 MySQL 需启用官方 yum 源
服务名 mysql 服务名 mysqld 包名一样但服务名不同,最容易写错
/etc/mysql/mysql.conf.d/mysqld.cnf /etc/my.cnf + /etc/my.cnf.d/* 配置拆分方式不同
/var/log/mysql/error.log /var/log/mysqld.log 日志默认路径不同

环境变量与 PATH

是什么

apt 安装时 mysql、mysqladmin、mysqldump 都在 /usr/bin,而 /usr/bin 默认就在 PATH 里,所以通常什么都不用配。本节只在源码安装(装到 /usr/local/mysql)或脚本免交互连接时才有用。

怎么做

# 只有源码安装才需要:追加而不是覆盖原有 PATH
$ echo 'export PATH=/usr/local/mysql/bin:$PATH' >> ~/.bashrc
$ source ~/.bashrc

脚本里要免交互连接,把凭据写进配置文件而不要用环境变量传密码(~/.my.cnf,权限 600):

[client]
user = root
password = <强密码>
$ chmod 600 ~/.my.cnf

[!WARNING] MYSQL_PWD 极不安全。 密码会明文出现在 ps 输出、shell 历史(.bash_history)和进程环境里,同机任何用户都能看到。一律改用权限 600 的 ~/.my.cnf。

参数说明

变量 / 文件 作用 建议
PATH 决定敲 mysql 时去哪找可执行文件 apt 安装无需改动
MYSQL_PWD 环境变量传密码 不要用,改 ~/.my.cnf
~/.my.cnf 客户端默认连接参数 权限必须 600,否则被忽略

验证方法

$ command -v mysql && mysql -e "SELECT 1;"
预期输出:
/usr/bin/mysql
1
1

(第一行是命令路径,后两行是 SELECT 1; 的结果:列名与值都是 1。)

常见问题

现象 根因 解决
mysql: command not found 只装了精简版,或 PATH 被改坏 sudo apt install mysql-client;echo $PATH 看 /usr/bin 在不在
配了 ~/.my.cnf 仍要输密码 / 脚本里找不到 mysql 权限太宽松被忽略 / cron 的 PATH 精简 chmod 600 ~/.my.cnf;脚本里写绝对路径 /usr/bin/mysql

最小必要配置

是什么

装完的 MySQL 能跑,但默认状态不适合接业务。最小必要配置只做四件事:数据目录在哪、监听谁的连接、谁能登进来、用什么字符集存。做完这四件事(外加备份与日志),就可以安全地把库交给应用。

怎么做

第一步:安全初始化

$ sudo mysql_secure_installation

向导逐项含义:

向导提问 建议 为什么
启用 VALIDATE PASSWORD 组件 学习环境 No,生产 Yes 强制密码强度,但会让后续设密码变麻烦
设置 root 密码 设强密码 注意:Ubuntu 上 root 走 auth_socket,这一步可能直接跳过
删除匿名用户 Yes 匿名用户任何人都能连,是历史遗留后门
禁止 root 远程登录 Yes root 只留本机,应用一律用专用账号
删除 test 数据库 Yes 默认人人可读写,纯属攻击面
立即重载权限表 Yes 让改动立刻生效,不用重启服务

第二步:字符集 —— 必须用 utf8mb4

编辑 /etc/mysql/mysql.conf.d/mysqld.cnf,在 [mysqld] 段下加:

[mysqld]
character-set-server = utf8mb4
collation-server     = utf8mb4_0900_ai_ci

为什么不是 utf8:MySQL 的 utf8 是最多 3 字节的伪 UTF-8,存不了 emoji 和部分生僻汉字,写入时报 Incorrect string value。utf8mb4 才是真正的 4 字节 UTF-8。建库时也要显式指定,不要指望默认值:

CREATE DATABASE appdb CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;

第三步:认证与首次登录(重点)

Ubuntu 上 root 默认用 auth_socket 插件:它不校验密码,而是校验「当前 Linux 用户是不是 root」。所以 mysql -u root -p 会失败,必须这样进:

# 把身份提为 root,插件校验通过,无需密码
$ sudo mysql

确实需要密码登录时(如某些运维脚本),改插件:

ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '<强密码>';
FLUSH PRIVILEGES;

[!NOTE] 交叉引用:MySQL 8.4 默认禁用 mysql_native_password 插件(见本章开头 NOTE)。上面的 ALTER 示例适用于 8.0;8.4 用户勿照抄此示例——需先按官方文档启用该插件,否则该语句会执行失败。

[!WARNING] 改 root 认证插件是高风险操作:改错的瞬间你就被锁在数据库外面,恢复要靠 --skip-grant-tables 走后门。更稳妥的是不动 root,另建管理员账号日常使用,root 留给 sudo mysql 兜底。

第四步:caching_sha2_password 兼容问题(重点难点)

MySQL 8 默认认证插件是 caching_sha2_password,比旧的 mysql_native_password 更安全,但老客户端和老驱动不认识它,连接时报:

Authentication plugin 'caching_sha2_password' cannot be loaded

按驱动能力二选一:

-- 方式 A(首选):驱动够新(Connector/J 8+、mysqlclient 2+、PHP 7.4+),用默认插件
CREATE USER 'appuser'@'localhost' IDENTIFIED BY '<强密码>';

-- 方式 B(兜底):驱动太老(老版 mysqlclient、PHP 5.x、部分老 ORM),显式指定旧插件
CREATE USER 'appuser'@'localhost' IDENTIFIED WITH mysql_native_password BY '<强密码>';

[!TIP] 优先方式 A 并升级驱动,方式 B 是权宜之计。已建好的用户也能改:ALTER USER 'appuser'@'localhost' IDENTIFIED WITH mysql_native_password BY '<强密码>';

第五步:建库建用户与最小授权

CREATE DATABASE appdb CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
CREATE USER 'appuser'@'localhost' IDENTIFIED BY '<强密码>';
GRANT SELECT, INSERT, UPDATE, DELETE ON appdb.* TO 'appuser'@'localhost';
FLUSH PRIVILEGES;

三条原则:不用 root 跑应用(root 权限无限大,一次 SQL 注入就能 DROP DATABASE);不给 ALL PRIVILEGES(应用只需增删改查,多给的 DROP、ALTER、FILE 都是事故放大器,建表迁移可临时给、迁完收回);不给 WITH GRANT OPTION(它能让 appuser 把权限转授别人,权限会被无限扩散)。末尾的 FLUSH PRIVILEGES; 在 GRANT/CREATE USER 之后其实不必需(只有直接改 mysql.user 表才需要),写上是为了保险。

需要远程连接时,主机部分差别很大,必须配合 bind-address 和防火墙一起改:

-- 允许任意主机连(危险面最大,只在防火墙/安全组已收敛时用)
CREATE USER 'appuser'@'%' IDENTIFIED BY '<强密码>';

-- 只允许内网网段连(推荐)
CREATE USER 'appuser'@'192.168.1.%' IDENTIFIED BY '<强密码>';

[!IMPORTANT] MySQL 账号是 '用户名'@'主机' 二元组:'appuser'@'localhost' 和 'appuser'@'%' 是两个完全不同的账号,密码和权限各自独立。连不上时先确认客户端 IP 对应的 host 有没有账号。

第六步:备份、恢复与日志

# 备份单个库(会提示输入密码)
$ mysqldump -u root -p appdb > appdb.sql

# InnoDB 生产库推荐:一致性快照 + 带上存储过程和触发器
$ mysqldump -u root -p --single-transaction --routines --triggers appdb > appdb.sql

# 恢复(前提:目标库已存在且字符集正确)
$ mysql -u root -p appdb < appdb.sql

--single-transaction 在一个事务里做一致性快照,不锁表,业务照常写入;不加它会锁表,导致备份期间应用卡死。日志:错误日志 /var/log/mysql/error.log(排错第一站);慢查询日志需先开 slow_query_log = 1。定时备份见 备份与定时任务,日志怎么看见 日志基础。

参数说明

/etc/mysql/mysql.conf.d/mysqld.cnf 的 [mysqld] 段常用参数:

参数 示例值 作用与注意
bind-address 127.0.0.1 只监听本机;0.0.0.0 表示监听所有网卡(见 WARNING)
port / datadir 3306 / /var/lib/mysql 端口与数据目录;数据目录不要手动删改
max_connections 151(默认) 并发连接上限,报 Too many connections 时调大
innodb_buffer_pool_size 物理内存的 50%–70% InnoDB 缓存池,单机专用库最重要的性能参数;别超过内存否则 OOM
slow_query_log 1 开慢查询日志,配合 long_query_time = 2 定位慢 SQL
character-set-server utf8mb4 服务端默认字符集

[!WARNING] 不要把 3306 对公网开放。 bind-address = 0.0.0.0 加上防火墙放行 3306,等于把数据库摆在互联网上,几小时内就会被自动化脚本扫到并暴力破解。正确做法三选一:① 保持 127.0.0.1,应用与数据库同机;② 只监听内网 IP,防火墙只放行内网网段(见 防火墙);③ 跨公网时用 SSH 隧道或内网专线。

验证方法

# 生效的字符集
$ sudo mysql -e "SHOW VARIABLES LIKE 'character_set_server'; SHOW VARIABLES LIKE 'collation_server';"

# 各账号用的认证插件
$ sudo mysql -e "SELECT user, host, plugin FROM mysql.user;"

# 备份文件不是空文件
$ ls -lh appdb.sql && head -n 3 appdb.sql
预期输出:
+----------------------+--------------------+
| Variable_name        | Value              |
+----------------------+--------------------+
| character_set_server | utf8mb4            |
| collation_server     | utf8mb4_0900_ai_ci |
+----------------------+--------------------+
| user    | host      | plugin                |
+---------+-----------+-----------------------+
| root    | localhost | auth_socket           |
| appuser | localhost | caching_sha2_password |
+---------+-----------+-----------------------+
-rw-r--r-- 1 root root 12K Sep 29 10:00 appdb.sql
-- MySQL dump 10.13  Distrib 8.0.4x, for Linux (x86_64)

常见问题

现象 根因 解决
改配置重启后起不来 / unknown variable 参数名拼错、值非法,或写进了 [client] 段 看 /var/log/mysql/error.log 末尾并注释掉刚加的行;服务端参数必须写在 [mysqld] 段下
设了 utf8mb4 但 emoji 存不进 连接或表仍是 utf8 库、表、连接串三处都要 utf8mb4;SHOW CREATE TABLE 核对
备份文件 0 字节 / 恢复后乱码 密码或库名写错;目标库建成 utf8 去掉重定向看 stderr;cron 里写绝对路径 /usr/bin/mysqldump

验证方法

是什么

验证按「由内到外」分四层:程序版本 → 服务活着 → 本机连得上 → 应用账号能读写。任何一层失败就停在那里排查,别往下走。

怎么做

按四层顺序逐条执行下面的命令即可(见「验证方法」);注意最后一条会提示输入密码,不要把密码写在 -p 后面(会进 shell 历史)。

参数说明

mysqladmin status 输出字段的含义:

字段 含义
Uptime 已运行秒数,突然变小说明刚重启过(可能崩过)
Threads / Questions 当前连接数 / 累计查询数
Slow queries 慢查询累计条数,不为 0 就该去看慢日志

验证方法

$ mysql --version
$ sudo systemctl is-active mysql
$ sudo mysqladmin status
$ mysql -u appuser -p -e "SHOW DATABASES;"
预期输出:
mysql  Ver 8.0.4x for Linux on x86_64 ((Ubuntu))
active
Uptime: 1234  Threads: 3  Questions: 57  Slow queries: 0  Opens: 148  Flush tables: 3  Open tables: 141  Queries per second avg: 0.046
Enter password:
+--------------------+
| Database           |
+--------------------+
| information_schema |
| appdb              |
+--------------------+

[!NOTE] appuser 能看到 information_schema 是正常现象(所有用户都可见),但它只能看到 appdb 的业务数据 —— 这正是最小授权生效的证据。

常见问题

现象 根因 解决
is-active 返回 inactive 服务没启动 sudo systemctl start mysql,再看日志
mysqladmin: connect to server failed socket 路径不对或服务没起 sudo systemctl status mysql;SHOW VARIABLES LIKE 'socket';
应用账号看不到 appdb 授权没生效或账号 host 不匹配 SHOW GRANTS FOR 'appuser'@'localhost'; 核对

托管为 systemd 服务

是什么

apt 安装的 MySQL 自带 systemd unit(mysql.service),装完就在跑。你只需掌握两件事:日常怎么启停、怎么开机自启。unit 结构与 Restart= 的原理见 systemd 服务管理。

怎么做

# 启停、看状态(含最近几行日志,排错第一命令)、开机自启
$ sudo systemctl start mysql
$ sudo systemctl restart mysql
$ sudo systemctl status mysql
$ sudo systemctl enable mysql

[!NOTE] 服务名 Ubuntu 与 RHEL 不同:Ubuntu 是 mysql,RHEL / Rocky / AlmaLinux 是 mysqld;写错会报 Unit mysql.service not found,用 systemctl list-units | grep -i mysql 找真实名称。另外改配置用 restart,MySQL 没有可靠的 reload 语义。

参数说明

命令 作用 开机生效
start / stop / restart 现在启停 / 加载新配置 否
enable 建开机启动软链 是,但不会现在启动
enable --now 开机自启 + 立刻启动 是

enable 的本质是在 /etc/systemd/system/multi-user.target.wants/ 下建软链接,原理见 systemd 服务管理。

验证方法

$ sudo systemctl is-active mysql
$ sudo systemctl is-enabled mysql
预期输出:
active
enabled

常见问题

现象 根因 解决
Unit mysql.service not found 服务名写错或没装服务端 `systemctl list-units
Job for mysql.service failed / 改了配置没生效 配置错误、数据目录权限不对,或忘了 restart journalctl -u mysql -n 50 --no-pager 看具体报错;确认改的是 mysqld.cnf 再 restart

常见问题

是什么

这一节是「连不上 / 用不对」的集中排查表。先按下面的固定顺序定位,再查表。

怎么做

固定排查顺序(由内到外,每层通过再下一层):① 服务活着吗 systemctl is-active mysql → ② 本机连得上吗 sudo mysql → ③ 账号对不对 SELECT user, host, plugin FROM mysql.user; → ④ 网络通不通 ss -lntp | grep 3306 → ⑤ 防火墙 sudo ufw status → ⑥ 云安全组(控制台)。

远程连接失败时下面四层必须同时满足,缺一层就表现为「连不上」:

层 检查点
MySQL 配置 bind-address 不是 127.0.0.1
MySQL 账号 存在 'user'@'<客户端IP或网段>' 账号
主机防火墙 ufw 放行 3306(且只放行内网网段)
云安全组 控制台放行对应来源 IP

参数说明

报错关键词 指向的层
Can't connect to local MySQL server through socket 服务没起或 socket 路径错了,与网络无关
Access denied for user / Host ... is not allowed 账号 / 密码 / host 三者之一不对
Authentication plugin ... cannot be loaded 客户端驱动太老
Too many connections 连接打满:参数偏小或连接池泄漏

验证方法

$ sudo ss -lntp | grep 3306
预期输出:
LISTEN 0 151 127.0.0.1:3306 0.0.0.0:* users:(("mysqld",pid=1234,fd=22))

看到 127.0.0.1:3306 说明远程连接必然失败 —— 先改 bind-address。

常见问题

现象 根因 解决
root 登不上,Access denied for user 'root'@'localhost' root 用了 auth_socket,密码被忽略 用 sudo mysql;或 ALTER USER 改插件(有风险,见上文 WARNING)
Authentication plugin 'caching_sha2_password' cannot be loaded 客户端驱动太老 升级驱动;或建用户时 IDENTIFIED WITH mysql_native_password
Can't connect to local MySQL server through socket '...' 服务没起 / socket 挪走 / 数据目录损坏 sudo systemctl start mysql;journalctl -u mysql -n 50;查 socket 变量
远程连不上 四层之一没配:bind-address、账号 host、ufw、云安全组 按上表逐层核对
中文乱码、emoji 存不进 用了 3 字节 utf8,或连接未指定 utf8mb4 服务端 + 建库显式指定 + 连接串带 charset=utf8mb4
Too many connections max_connections 太小或连接池泄漏 调大参数;SHOW PROCESSLIST 看 Sleep 连接;修应用连接池
忘记 root 密码 没有任何可用凭据 见下方 WARNING 的挽救步骤

[!WARNING] 忘记 root 密码的挽救步骤(高风险,最后手段):--skip-grant-tables 会跳过所有权限校验,期间任何能连上本机的人都能读写全部数据。要点:停服务 → mysqld_safe --skip-grant-tables --skip-networking 启动(必须带 --skip-networking,否则等于把无鉴权的库挂在网上)→ 本地改密码 → 立刻正常重启 → 验证鉴权已恢复。

本章命令速查

命令 作用
apt policy mysql-server 看源里能装到哪个版本
sudo apt install -y mysql-server 安装 MySQL
sudo mysql_secure_installation 安全初始化向导
sudo mysql 以 root 免密登录(auth_socket)
mysql -u <用户> -p -e "<SQL>" 执行一条 SQL 后退出
sudo systemctl start|restart|status mysql 服务启停与状态
sudo systemctl enable --now mysql 开机自启并立刻启动
sudo systemctl is-active mysql 是否在运行
sudo ss -lntp | grep 3306 看监听地址与端口
SELECT user, host, plugin FROM mysql.user; 查账号与认证插件
mysqldump -u root -p --single-transaction appdb > appdb.sql 一致性备份单库
mysql -u root -p appdb < appdb.sql 恢复单库

相关章节

📄 PostgreSQL 安装与配置 24-postgresql.md


title: 24 PostgreSQL 安装与配置 updated: 2026-09 applies-to: Ubuntu 22.04 LTS

PostgreSQL 是功能最完善的开源关系型数据库,复杂查询、JSON、GIS、事务语义都比 MySQL 更严谨。本章走完:选版本 → 用 PGDG 官方源安装 → 最小必要配置(含远程连接的两个文件)→ 建角色建库 → 备份 → 托管给 systemd。

[!IMPORTANT] 本章版本号截至 2026 年 9 月。版本信息的唯一权威来源是 版本速查表,冲突时以速查表为准。

版本选择

是什么

PostgreSQL 每年发一个大版本(17、18、19……),社区对每个大版本维护约 5 年。它不像 MySQL 有 LTS / 创新版之分,所以选型问题主要在于「用系统源自带的老版本」还是「用 PGDG 源的新版本」。

项目 值 说明
推荐版本 17 / 18(18.4 为当前稳定) 两者都在官方维护期内
Ubuntu 22.04 源默认 14 太旧,且已接近维护尾声,不建议直接用
获取方式 PGDG 官方源 系统源里没有 17/18,必须加官方源
维护周期 每个大版本约 5 年 精确到期日查 postgresql.org 或 endoflife.date
License PostgreSQL License(类 BSD) 比 GPL 更宽松,商用无限制

怎么做

# 先看系统源里有什么版本 —— 这一步会暴露「只有 14」的问题
$ apt policy postgresql
预期输出:
postgresql:
  Installed: (none)
  Candidate: 14+238ubuntu0.1

Candidate 是 14,说明系统源不够用,需要加 PGDG 官方源(见下一节)。

参数说明

场景 选哪个 为什么
稳妥优先、依赖多(插件/ORM/运维工具) 17 已发布一两年,生态适配最全,坑被踩完
新项目、团队能自己排错 18 支持期更长,性能与新特性更好
已有旧版本要升大版本 不要原地升级 见下方 WARNING

[!WARNING] PostgreSQL 大版本不要原地 apt upgrade。 数据目录格式跨大版本不兼容,原地升会直接起不来。正确做法:新版本另起实例 → pg_dump 导出 → 导入新实例 → 业务切换 → 观察一段时间。只有小版本(18.3 → 18.4)才能原地升。

验证方法

# 装完之后核对真实版本
$ psql --version
预期输出:
psql (PostgreSQL) 18.4

常见问题

现象 根因 解决
apt install postgresql 装出来是 14 用了系统源 加 PGDG 官方源后重装,见下一节
不确定选 17 还是 18 两者都在维护期 看依赖:用到的扩展/驱动是否声明支持 18

安装

是什么

方式 拿到的版本 适合
系统源 apt 14(太旧) 基本不推荐,除非你确实只要 14
PGDG 官方源(推荐) 17 / 18 自选 生产与学习的正解
Docker 任意 本地开发,见 Docker

怎么做

方式一:PGDG 官方源(推荐)

postgresql-common 包自带一个官方脚本,会自动配好 PGDG 源和密钥:

# 1. 装公共包(提供 pg_ctlcluster、pg_lsclusters 等集群管理工具)
$ sudo apt install -y postgresql-common

# 2. 执行官方脚本,自动添加 PGDG 源与密钥(-y 表示非交互)
$ sudo /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -y

# 3. 刷新索引后安装指定大版本
$ sudo apt update
$ sudo apt install -y postgresql-18

也可以手写 signed-by 源(效果和官方脚本一致,便于写进自动化脚本):

# 下载 PGDG 官方密钥到独立钥匙环,不要用 apt-key
$ curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo gpg --dearmor -o /usr/share/keyrings/pgdg.gpg

# jammy-pgdg 是给 Ubuntu 22.04 用的仓库名,其他版本见官网
$ echo "deb [arch=amd64 signed-by=/usr/share/keyrings/pgdg.gpg] https://apt.postgresql.org/pub/repos/apt jammy-pgdg main" | sudo tee /etc/apt/sources.list.d/pgdg.list

$ sudo apt update
$ sudo apt install -y postgresql-18

[!NOTE] 上面的脚本名、密钥地址和仓库名以 PGDG 官网(postgresql.org/download/linux/ubuntu/)为准,官方可能调整。另外,官方现行推荐写法已迁移到 deb822 格式(/etc/apt/sources.list.d/pgdg.sources)+ 密钥存 /usr/share/postgresql-common/pgdg/,与本节传统写法等价,任选其一即可。装之前先 apt policy postgresql-18,确认 Candidate 是你要的版本。

方式二:Docker

一句带过,见 Docker:docker run -d --name pg18 -e POSTGRES_PASSWORD='<强密码>' -p 127.0.0.1:5432:5432 postgres:18。

[!WARNING] 端口特意写成 127.0.0.1:5432:5432。不要写 -p 5432:5432 —— 那会把数据库暴露到所有网卡,等于对公网开放 5432。

参数说明

包名 作用
postgresql-18 18 的服务端 + 客户端 + 一个默认 main 集群
postgresql-client-18 只有客户端(psql、pg_dump)
postgresql-common 集群管理框架(pg_lsclusters、pg_ctlcluster)与源配置脚本

验证方法

$ psql --version
$ apt policy postgresql-18
预期输出:
psql (PostgreSQL) 18.4
postgresql-18:
  Installed: 18.4-1.pgdg22.04+1
  Candidate: 18.4-1.pgdg22.04+1

常见问题

现象 根因 解决
apt install postgresql-18 说找不到包 没加 PGDG 源或没 apt update 重跑官方脚本,sudo apt update 后再装
apt update 报 GPG 错误 密钥路径与 signed-by 不一致 检查 /usr/share/keyrings/pgdg.gpg 是否存在
RHEL / Rocky / AlmaLinux 9 对照
Ubuntu (apt) RHEL (dnf) 说明
apt.postgresql.org.sh(PGDG 源) dnf install -y https://download.postgresql.org/pub/repos/yum/reporpms/EL-9-x86_64/pgdg-redhat-repo-latest.noarch.rpm 都是装 PGDG 源,命令不同
sudo apt install -y postgresql-18 sudo dnf install -y postgresql18-server RHEL 的包名带 -server 后缀
装完自动建集群并启动 需手动 postgresql-18-setup initdb RHEL 不会自动初始化数据目录
sudo pg_ctlcluster 18 main restart sudo systemctl restart postgresql-18 RHEL 没有 pg_ctlcluster 框架
/etc/postgresql/18/main/ /var/lib/pgsql/18/data/ 配置与数据目录位置不同

环境变量与 PATH

是什么

PGDG 装的 psql、pg_dump 通过 postgresql-common 的包装器链到 /usr/bin,所以敲命令一般没问题。需要手动设置的是连接参数:psql 默认用当前 Linux 用户名当数据库用户、用本地 socket 连接,脚本里必须显式指定连谁、连哪个库。

怎么做

# 脚本里常用:一行指定主机、端口、用户、库
$ export PGHOST=127.0.0.1 PGPORT=5432 PGUSER=appuser PGDATABASE=appdb
$ psql -c "\conninfo"

密码不要用 PGPASSWORD 传,改用 ~/.pgpass:

# 格式:hostname:port:database:username:password
127.0.0.1:5432:appdb:appuser:<强密码>
$ chmod 600 ~/.pgpass

[!WARNING] PGPASSWORD 不安全。 密码会以明文出现在 ps 输出、shell 历史(.bash_history)和进程环境里,同机任何用户都能看到。必须用 ~/.pgpass 或 PGPASSFILE 指向的凭据文件,且文件权限必须是 600 —— 权限不对时 PostgreSQL 会直接忽略该文件。

参数说明

变量 / 文件 作用 建议
PGHOST / PGPORT 连接地址与端口 不设则走本地 Unix socket
PGUSER / PGDATABASE 默认用户与库 不设则取当前 Linux 用户名
PGPASSWORD 环境变量传密码 不要用,改 ~/.pgpass
~/.pgpass 密码文件(每行一条) 权限必须 600,可用 PGPASSFILE 换路径

验证方法

$ psql -c "\conninfo"
预期输出:
You are connected to database "appdb" as user "appuser" on host "127.0.0.1" at port "5432".

常见问题

现象 根因 解决
psql: command not found 只装了服务端精简包或 PATH 被改坏 sudo apt install postgresql-client-18;echo $PATH
配了 ~/.pgpass 仍要输密码 文件权限不是 600,或格式不对 chmod 600 ~/.pgpass;确认是四段冒号分隔
cron 里 pg_dump 失败 cron 的 PATH 精简 写绝对路径 /usr/bin/pg_dump,见 备份与定时任务

最小必要配置

是什么

PostgreSQL 装完会自动初始化一个数据目录(称为 cluster/集群)并启动,但默认只监听本机、且只有 postgres 一个角色。最小必要配置做四件事:监听谁的连接、谁能从哪连进来(认证)、建业务角色与库、能备份。

怎么做

第一步:首次登录(重点)

安装程序会自动创建名为 postgres 的 Linux 系统用户,以及同名的数据库角色。本机默认认证方式是 peer(用操作系统身份判断你是谁),所以必须切换成 postgres 用户再进:

# 不要 sudo su - 之后再敲 psql,也不要用 root 直接登
$ sudo -u postgres psql

[!IMPORTANT] 不要直接用 root 登录 PostgreSQL。 默认角色里没有 root,psql 用 root 跑会报 role "root" does not exist 或 peer authentication failed。正确做法永远是 sudo -u postgres psql。

进入后你会看到提示符变成 postgres=#,此时执行的就是 SQL 和 psql 元命令。

第二步:远程连接必须改两个文件(重点)

配置文件在 /etc/postgresql/18/main/ 下。只改 postgresql.conf 不改 pg_hba.conf,依然连不上 —— 这是新手卡住的头号原因。

postgresql.conf —— 决定「监听哪里」:

listen_addresses = 'localhost'      # 改成 '*' 或 'localhost,192.168.1.10'
port = 5432

pg_hba.conf —— 决定「谁可以从哪来、用什么方式认证」(hba = host based authentication):

# TYPE  DATABASE  USER      ADDRESS           METHOD
host    appdb     appuser   192.168.1.0/24    scram-sha-256
字段 含义
TYPE local = Unix socket;host = TCP 连接
DATABASE / USER 这条规则作用于哪个库、哪个角色
ADDRESS 客户端网段,如 192.168.1.0/24;0.0.0.0/0 表示全部
METHOD scram-sha-256(推荐)、md5、peer、trust(绝不用)

改完的生效方式因文件而异:pg_hba.conf 的改动只需 reload(不断开现有连接):

$ sudo systemctl reload postgresql

或在 psql 里执行 SELECT pg_reload_conf();,效果相同。只有 postgresql.conf 里 listen_addresses 这类必须重启才生效的参数(官方文档标注为 postmaster 级)才需要:

$ sudo systemctl restart postgresql

[!IMPORTANT] 两个文件是「与」的关系,但生效方式不同,不要一刀切 restart:postgresql.conf 不监听,连接根本到不了;pg_hba.conf 没有匹配规则,连到了也会被拒(no pg_hba.conf entry for host)。远程访问两者都要改;日常调整 pg_hba.conf 用 reload 即可,restart 会断掉所有现有连接。

[!WARNING] 不要把 5432 对公网开放。 listen_addresses = '*' 加上防火墙放行 5432,等于把数据库暴露在公网。正确做法:只监听内网 IP(listen_addresses = 'localhost,192.168.1.10'),pg_hba.conf 里只写内网网段,防火墙只放行内网(见 防火墙),云安全组同样只放行内网来源。

第三步:建角色与数据库

CREATE ROLE appuser WITH LOGIN PASSWORD '<强密码>';
CREATE DATABASE appdb OWNER appuser;
GRANT ALL PRIVILEGES ON DATABASE appdb TO appuser;

PostgreSQL 里的 role(角色)= 用户 + 组的合体:加 WITH LOGIN 它才能登录(相当于用户),不加则只是一个可以被继承的权限集合(相当于组)。这也解释了为什么没有 CREATE USER 也能用 —— CREATE USER 只是 CREATE ROLE ... WITH LOGIN 的别名。

改密码用 psql 元命令,避免明文密码进 shell 历史:

\password appuser

[!TIP] 应用账号不要直接用 postgres 超级用户,理由和 MySQL 一样:权限无限大,一次注入就能毁掉所有库。按库建角色,只给它需要的权限。

第四步:psql 元命令速记

元命令 作用
\l 列出所有数据库
\c appdb 切换到 appdb
\dt 列出当前库的表
\du 列出所有角色
\conninfo 显示当前连接信息(排错很好用)
\password <角色> 改密码(交互式,不留明文)
\q 退出
\? 查看全部元命令帮助

[!NOTE] 元命令以反斜杠开头,不要加分号;SQL 语句才以 ; 结尾。这是新手最常见的混淆点。

第五步:备份、恢复与日志

# 逻辑备份单个库(SQL 文本格式)
# 注意:不加 -h 默认走本地 socket + peer 认证,appuser 不是系统用户会被拒;
# 显式加 -h 127.0.0.1 走 TCP 密码认证(前提是 pg_hba.conf 里有对应 host 规则)
$ pg_dump -h 127.0.0.1 -U appuser appdb > appdb.sql

# 自定义格式(-Fc):压缩、可并行、支持按对象恢复,配合 pg_restore
$ pg_dump -Fc -h 127.0.0.1 -U appuser appdb > appdb.dump
$ pg_restore -h 127.0.0.1 -U appuser -d appdb appdb.dump

# 备份整个实例的所有库 + 角色定义
$ pg_dumpall -U postgres > alldb.sql

日志默认在 /var/log/postgresql/postgresql-18-main.log(服务起不来、认证失败都先看它)。定时备份见 备份与定时任务,日志查看见 日志基础。

参数说明

/etc/postgresql/18/main/postgresql.conf 常用参数:

参数 示例值 作用与注意
listen_addresses 'localhost' 监听哪些地址;'*' 是全部网卡(见 WARNING)
port / data_directory 5432 / /var/lib/postgresql/18/main 端口与数据目录,数据目录不要手动改
password_encryption scram-sha-256 新建密码的加密方式,要与 pg_hba.conf 的 METHOD 匹配
max_connections 100 并发连接上限,连接池要算进去
shared_buffers 物理内存的 25% PostgreSQL 自己的缓存(与系统页缓存互补,不是越大越好)
log_min_duration_statement 2000 记录执行超过 2 秒的 SQL,轻量版慢查询日志

验证方法

# 实际生效的监听地址、端口与密码加密方式
$ sudo -u postgres psql -c "SHOW listen_addresses;" -c "SHOW port;" -c "SHOW password_encryption;"

# 本机是否在听 5432
$ sudo ss -lntp | grep 5432
预期输出:
 listen_addresses
------------------
 localhost,192.168.1.10
(1 row)

port = 5432
password_encryption = scram-sha-256
LISTEN 0 244 192.168.1.10:5432 0.0.0.0:* users:(("postgres",pid=1234,fd=6))

常见问题

现象 根因 解决
改了配置没生效 忘了 restart,或改了别的集群目录 确认路径是 /etc/postgresql/18/main/,再 sudo systemctl restart postgresql
no pg_hba.conf entry for host pg_hba.conf 里没有匹配的规则 按上面的格式加一行,注意网段写全(192.168.1.0/24)
备份文件 0 字节 用户名或库名错 去掉重定向跑一次看 stderr;cron 里写绝对路径 /usr/bin/pg_dump

验证方法

是什么

验证按「由内到外」分三层:程序版本 → 服务与集群状态 → 本机连得上 → 从客户端机器连得上。失败就停在那一层。

怎么做

按下面四条命令依次执行,见「验证方法」的预期输出。最后一条从另一台机器连,用来验证远程配置是否真的生效;-h 写服务器 IP 而不是 localhost,否则走的是 socket、测不到 TCP 与 pg_hba.conf。

参数说明

检查命令 说明
psql --version 客户端版本,与服务端大版本最好一致
sudo -u postgres psql -c "SELECT version();" 服务端真实版本与编译信息
sudo ss -lntp | grep 5432 监听地址是 localhost 还是内网 IP
psql -h <IP> -U appuser -d appdb 端到端:TCP + 认证 + 授权,一次性验证三件事

验证方法

$ psql --version
$ sudo -u postgres psql -c "SELECT version();"
$ sudo ss -lntp | grep 5432
$ psql -h 192.168.1.10 -U appuser -d appdb -c "\conninfo"
预期输出:
psql (PostgreSQL) 18.4
                                                 version
----------------------------------------------------------------------------------------------------------
 PostgreSQL 18.4 on x86_64-pc-linux-gnu, compiled by gcc (Ubuntu 11.4.0) 11.4.0, 64-bit
LISTEN 0 244 192.168.1.10:5432 0.0.0.0:* users:(("postgres",pid=1234,fd=6))
Password for user appuser:
You are connected to database "appdb" as user "appuser" on host "192.168.1.10" at port "5432".

最后一行出现即代表:监听、认证(scram-sha-256)、授权全部通过。

常见问题

现象 根因 解决
SELECT version(); 报连不上 服务没起,或集群状态是 down sudo pg_lsclusters 看状态,见下一节
本地能连、-h <IP> 连不上 listen_addresses 没改或 pg_hba.conf 没规则 两个文件都改并重启
ss 里没有 5432 服务没起 / 端口被改 sudo systemctl status postgresql

托管为 systemd 服务

是什么

PGDG 安装的 PostgreSQL 会在 systemd 里注册两层:一个外层 postgresql.service,以及每个集群一个 postgresql@<版本>-<集群名>.service(如 postgresql@18-main.service)。日常启停用外层即可。原理见 systemd 服务管理。

怎么做

# 启停与状态(Ubuntu 上作用于所有集群)
$ sudo systemctl start postgresql
$ sudo systemctl restart postgresql
$ sudo systemctl status postgresql
$ sudo systemctl enable postgresql

Ubuntu 特有的集群管理工具(postgresql-common 提供,RHEL 没有):

# 列出所有集群:版本、名字、端口、状态、数据目录、日志文件
$ sudo pg_lsclusters

# 只重启某一个集群(一个版本可以有多个 cluster,各自独立的数据目录和端口)
$ sudo pg_ctlcluster 18 main restart

[!NOTE] 一个 PostgreSQL 版本可以有多个 cluster。 每个 cluster 是独立的数据目录 + 端口 + 配置,互不干扰;这正是 pg_lsclusters 存在的原因。默认只有一个叫 main 的集群(端口 5432)。多集群场景下用 pg_ctlcluster 精确操作,别用 systemctl restart postgresql 一把梭。

参数说明

命令 作用
systemctl enable postgresql 开机自启(不会现在启动,--now 才两者都做)
pg_lsclusters 看有几个集群、各自端口与状态
pg_ctlcluster <版本> <集群> restart 精确重启单个集群,不影响其他集群

验证方法

$ sudo systemctl is-active postgresql
$ sudo pg_lsclusters
预期输出:
active
Ver Cluster Port Status Owner    Data directory              Log file
18  main    5432 online postgres /var/lib/postgresql/18/main /var/log/postgresql/postgresql-18-main.log

常见问题

现象 根因 解决
Job for postgresql@18-main.service failed 配置写错或数据目录权限不对 journalctl -u postgresql@18-main -n 50 --no-pager;再查 /var/log/postgresql/ 下的日志
pg_ctlcluster: cluster does not exist 集群名写错 sudo pg_lsclusters 看真实名字(默认 main)
重启后没起来 没 enable sudo systemctl enable --now postgresql

常见问题

是什么

这一节是「连不上 / 认证失败」的集中排查表。先按下一节的固定顺序定位,再查表。

怎么做

远程连接失败时下面四层必须同时满足,缺一层就表现为「连不上」:① postgresql.conf 的 listen_addresses 包含目标网卡 → ② pg_hba.conf 有匹配客户端 IP 的规则 → ③ 主机防火墙放行 5432(只放行内网) → ④ 云安全组放行对应来源。而且改完必须重启服务。

参数说明

报错关键词 含义
no pg_hba.conf entry for host 监听没问题,但 pg_hba.conf 没规则 —— 只改了 conf
FATAL: password authentication failed 有规则,但密码错或加密方式与 METHOD 不匹配
peer authentication failed 走的是本地 peer 认证,当前系统用户对不上数据库角色
could not connect to server 根本没连到:服务没起、监听地址不对或端口不通

验证方法

# 一条命令同时看监听与连接来源,定位在四层中的哪一层
$ sudo ss -lntp | grep 5432
$ sudo tail -n 20 /var/log/postgresql/postgresql-18-main.log
预期输出:
LISTEN 0 244 192.168.1.10:5432 0.0.0.0:* users:(("postgres",pid=1234,fd=6))
2026-09-29 10:00:01.123 UTC [5678] appuser@appdb [local] LOG: connection authorized

日志里出现 no pg_hba.conf entry 或 password authentication failed 字样时,就能直接定位到认证层。

常见问题

现象 根因 解决
远程连不上 只改了 postgresql.conf 没改 pg_hba.conf;改完没重启;防火墙;云安全组 两个文件都改 → sudo systemctl restart postgresql → 查 ufw 与安全组
FATAL: password authentication failed for user "appuser" 密码错,或 pg_hba.conf 的 METHOD(md5/scram-sha-256)与密码实际加密方式不匹配 用 \password appuser 重设密码;让 METHOD 与 password_encryption 一致
peer authentication failed for user "postgres" 没切换系统用户就登 用 sudo -u postgres psql;或改 pg_hba.conf 里 local 行的 METHOD
端口 5432 被占用 已有另一个集群或别的程序占用 sudo ss -lntp | grep 5432 看是谁;改新集群的 port 或停掉冲突服务
psql: command not found 客户端包没装,或 PATH 被改坏 sudo apt install postgresql-client-18;echo $PATH 看 /usr/bin
磁盘满了起不来 数据目录所在分区 100% df -h 定位,清理/扩容后再启动,见 磁盘排错

[!WARNING] 不要为了「先让它连上」把 pg_hba.conf 的 METHOD 改成 trust。 trust 表示不校验密码,任何人连上来都是你写的那个角色,等于把数据库完全公开。测试完忘了改回去是常见的生产事故,宁可多花五分钟把 scram-sha-256 配对。

本章命令速查

命令 作用
apt policy postgresql-18 看源里能装到哪个版本
sudo apt install -y postgresql-18 安装 PostgreSQL 18
sudo -u postgres psql 以 postgres 角色登录(首次登录必须这样)
psql -h <IP> -U <角色> -d <库> 远程连接
\l / \c <库> / \dt / \du 列库 / 切库 / 列表 / 列角色
\conninfo / \password <角色> / \q 连接信息 / 改密码 / 退出
sudo systemctl start|restart|status postgresql 服务启停与状态
sudo systemctl enable --now postgresql 开机自启并立刻启动
sudo pg_lsclusters 列出所有集群及状态
sudo pg_ctlcluster 18 main restart 精确重启单个集群
sudo ss -lntp | grep 5432 看监听地址与端口
SHOW listen_addresses; / SHOW password_encryption; 查生效的监听与加密方式
pg_dump -U appuser appdb > appdb.sql 备份单库(SQL 格式)
pg_dump -Fc -U appuser appdb > appdb.dump 备份单库(自定义格式)
pg_restore -U appuser -d appdb appdb.dump 从自定义格式恢复
pg_dumpall -U postgres > alldb.sql 备份整个实例

相关章节

📄 MongoDB 安装与配置 25-mongodb.md


title: 25 MongoDB 安装与配置 updated: 2026-09 applies-to: Ubuntu 22.04 LTS

MongoDB 是最常用的文档型 NoSQL 数据库,数据以 JSON 风格的「文档」存储,适合字段不固定、快速迭代的项目。本章走完整链路:选版本 → 官方源安装 → 创建用户 → 开启认证 → 远程访问 → 备份。

[!IMPORTANT] 本章版本号截至 2026 年 9 月。版本信息的唯一权威来源是 版本速查表,冲突时以速查表为准。

版本选择

是什么

MongoDB 用大版本号(如 8.0)划分功能线,每个大版本有自己的维护周期。生产环境只跟「当前维护的大版本」走,不追最新,也不用最老的。

项目 值 说明
推荐版本 8.0.x(截至 2026-09;补丁号以官方源为准,核对方法见 91 章) 当前维护的稳定大版本,生态兼容性最好
旧版本 7.0 / 6.0 已进入或临近维护尾声,新装不要选
License SSPL(Server Side Public License) 自用免费;把它做成对外服务再分发才涉及授权问题
获取渠道 MongoDB 官方 apt 源 Ubuntu 22.04 官方源里没有 MongoDB,这是它和 MySQL 最大的不同

怎么做

先确认一个事实:Ubuntu 官方源里搜不到 MongoDB。

# 系统源里没有 mongodb-org 这个包
$ apt policy mongodb-org
预期输出:
mongodb-org:
  Installed: (none)
  N: 无法找到软件包 mongodb-org

所以必须添加 MongoDB 官方 apt 源,具体命令见下一节「安装」。选版本时只需要记住一点:源地址里的大版本号决定了你装的是什么(/8.0/ 就是 8.0 线),之后 apt 只会在 8.0.x 内做小版本更新。

参数说明

场景 选哪个 为什么
新项目、跟本章做 8.0.x 官方 apt 源当前主推,教程和驱动兼容性最好
老项目迁移 跟原项目版本走 MongoDB 跨大版本的数据格式有差异,迁移要单独规划
在意开源许可 评估 SSPL 对你的影响 只在自己服务器上跑业务一般不受影响

验证方法

版本选择阶段的验证,就是确认「官方源添加后能装到 8.0.x」:

# 添加官方源之后,查看候选版本
$ apt policy mongodb-org
预期输出:
mongodb-org:
  Installed: (none)
  Candidate: 8.0.x
  Version table:
     8.0.x 500
        500 https://repo.mongodb.org/apt/ubuntu jammy/mongodb-org/8.0/multiverse amd64 Packages

(8.0.x 代表实际补丁号,以官方源当时提供的为准。)

常见问题

现象 根因 解决
apt install mongodb 报 Unable to locate package 属正常:22.04 源里没有这个包(MongoDB 改用 SSPL 后被从 Ubuntu 官方源移除),与本章「系统源搜不到 MongoDB」一致 走本章的官方源装 mongodb-org

安装

是什么

通过 MongoDB 官方 apt 源安装。这样做的好处:后续 apt upgrade 能直接收到官方安全更新,不用手动下载 tar 包。

怎么做

按官方文档的步骤走:装依赖 → 导入 GPG 公钥 → 添加源 → 安装。

# 安装依赖与 GPG 公钥(不要用 apt-key,已废弃)
$ sudo apt-get install gnupg curl
$ curl -fsSL https://www.mongodb.org/static/pgp/server-8.0.asc | sudo gpg -o /usr/share/keyrings/mongodb-server-8.0.gpg --dearmor

# 添加源(Ubuntu 22.04 代号为 jammy)
$ echo "deb [ arch=amd64,arm64 signed-by=/usr/share/keyrings/mongodb-server-8.0.gpg ] https://repo.mongodb.org/apt/ubuntu jammy/mongodb-org/8.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-8.0.list

$ sudo apt-get update
$ sudo apt-get install -y mongodb-org

[!NOTE] 如果你在国内且官方源下载慢,可参考 镜像与加速 换用镜像站,但源地址的目录结构必须保持一致。

安装完成后先不要急着开认证,直接看下一节,顺序错了会把自己锁在外面。

参数说明

mongodb-org 是一个「元包」,它自己不含程序,只负责把全套组件一起装上:

组件包 作用
mongodb-org-server mongod:数据库服务端本体(systemd 服务就是它)
mongodb-org-mongos mongos:分片集群的路由组件(单机用不到)
mongodb-org-shell 旧版 mongo shell 的兼容入口(传统 shell 已停止维护)
mongodb-mongosh mongosh:现行官方命令行客户端,连库、建用户都用它(正主是这个包)
mongodb-org-tools 工具元包:引入 mongodb-database-tools 等,mongodump / mongorestore 备份工具来自 mongodb-database-tools 包
mongodb-org-database-tools-extra 只含 install_compass 脚本(Compass 图形客户端安装器),不是备份工具

验证方法

# 确认 mongod 与 mongosh 都已就位
$ mongod --version
$ mongosh --version
预期输出:
db version v8.0.x
Build Info: {...}
---
2.5.x

(8.0.x、2.5.x 代表实际版本号,以官方源为准,核对方法见 91 章。)

常见问题

现象 根因 解决
apt update 报 GPG 签名错误(NO_PUBKEY) 公钥没导入成功,或导入路径和源里的 signed-by 不一致 重跑 curl ... | sudo gpg ... 那条命令,核对路径逐字符一致
apt-get install 报 Unable to locate package 忘了 apt-get update,或源文件里代号不是 jammy 先 sudo apt-get update;检查 /etc/apt/sources.list.d/mongodb-org-8.0.list

[!IMPORTANT] 装完立刻固定版本,防止大版本被自动升级。 数据库大版本原地升级是高危操作,格式变了可能直接起不来。官方建议把装到的组件包全部「钉住」(完整列表):

$ sudo apt-mark hold mongodb-org mongodb-org-server mongodb-org-mongos mongodb-org-tools mongodb-org-database-tools mongodb-org-database-tools-extra mongodb-mongosh

本章正文示例只钉 mongodb-org mongodb-org-server 两个核心包,是为求简短做的取舍;要彻底防止组件各自升大版本,用上面的完整列表。以后想换大版本,走「新实例 → 导数据 → 切换」流程。包管理详见 软件包管理。

[!NOTE] RHEL / CentOS 用户:MongoDB 官方同样提供 yum/dnf 源,配置方式见 MongoDB 官方文档「Install on Red Hat」,步骤思想与本章一致(导入 GPG key → 加 repo 文件 → 安装)。

RHEL 对照:添加 MongoDB 官方源(点击展开)
# 创建 repo 文件(RHEL 9 为例,8.0 线)
$ sudo tee /etc/yum.repos.d/mongodb-org-8.0.repo <<'EOF'
[mongodb-org-8.0]
name=MongoDB Repository
baseurl=https://repo.mongodb.org/yum/redhat/9/mongodb-org/8.0/x86_64/
gpgcheck=1
enabled=1
gpgkey=https://www.mongodb.org/static/pgp/server-8.0.asc
EOF

$ sudo dnf install -y mongodb-org

环境变量与 PATH

是什么

apt 安装的 MongoDB 会把所有可执行文件放进 /usr/bin,这个目录本来就在系统 PATH 里,所以本章不需要手动配任何环境变量。

怎么做

# 确认命令能直接找到即可,无需额外配置
$ which mongod mongosh mongodump
预期输出:
/usr/bin/mongod
/usr/bin/mongosh
/usr/bin/mongodump

验证方法

任意目录下执行 mongosh --version 能出版本号,就说明 PATH 没问题。

常见问题

现象 根因 解决
command not found 安装未成功,或用的是手动解压的 tar 包且没加 PATH 重新走官方源安装,或参考 源码编译安装 的 PATH 配置

最小必要配置

是什么

MongoDB 的配置文件是 /etc/mongod.conf,YAML 格式。最小必要配置只关心四件事:数据放哪、日志放哪、监听哪里、要不要认证。

怎么做

第一步:理解默认配置。 包安装后的关键默认值:

# /etc/mongod.conf 中的关键段(摘录,非完整文件)
storage:
  dbPath: /var/lib/mongodb
systemLog:
  destination: file
  logAppend: true
  path: /var/log/mongodb/mongod.log
net:
  port: 27017
  bindIp: 127.0.0.1

第二步(重要):先启动服务、先建管理员,再开认证。 顺序绝对不能反,原因见下面的 WARNING。

# 1. 先以「无认证」状态启动服务
$ sudo systemctl enable --now mongod

# 2. 此时才进 mongosh 创建管理员用户(命令见下文)

第三步:在认证开启前创建管理员。 进 mongosh 执行:

use admin
db.createUser({
  user: "admin",
  pwd: "<强密码>",
  roles: [ { role: "userAdminAnyDatabase", db: "admin" }, { role: "readWriteAnyDatabase", db: "admin" } ]
})
预期输出:
Successfully added user: { "user" : "admin", ... }

第四步:开启认证。 编辑 /etc/mongod.conf,找到 #security: 那行,改成:

# 去掉 security 前的注释号,并新增一行 authorization
# 注意:authorization 前面是两个空格缩进
security:
  authorization: enabled
# 5. 重启服务使认证生效
$ sudo systemctl restart mongod

第五步:创建业务用户和业务库。 用 admin 登录后执行:

use appdb
db.createUser({
  user: "appuser",
  pwd: "<强密码>",
  roles: [ { role: "readWrite", db: "appdb" } ]
})

参数说明

核心顺序坑 —— 本章最重要的一段:

[!WARNING] 正确顺序:启动服务 → 创建管理员 → 开启认证。 如果先写 authorization: enabled 再重启,此时库里一个用户都没有,认证开启后你自己也登不进去,只能把 security.authorization: disabled 临时改回去、重启、建完用户再开 —— 等于全部重来。记住口诀:「先有人,再锁门」。

security.authorization: enabled 的 YAML 缩进:

[!WARNING] YAML 对缩进极其敏感:security: 顶格,authorization: 前面恰好两个空格(不能用 Tab)。缩进错了 mongod 直接拒绝启动,这是新手第一大翻车点。改完配置先用 sudo systemctl restart mongod,马上看状态确认起来了。

role(角色)概念速记:

角色 权限范围 给谁用
readWrite 指定库的读写 业务应用账号(如 appuser),权限最小化
dbAdmin 指定库的管理(索引、统计) 运维某库的人
userAdminAnyDatabase 管理所有库的用户 管理员账号
readWriteAnyDatabase 所有库的读写 管理员账号
root 一切权限 日常不要用,应急才用

其余配置要点:

配置项 默认值 说明
net.bindIp 127.0.0.1 默认只允许本机连接。要远程访问改成 127.0.0.1,<内网IP>
net.port 27017 默认端口,一般不改
storage.dbPath /var/lib/mongodb 数据目录,磁盘要够大;改路径需同步改目录属主
systemLog.path /var/log/mongodb/mongod.log 排错第一现场,起不来先看这里

[!WARNING] 绝对不要把 bindIp 改成 0.0.0.0 而不开启认证。 历史上 MongoDB 爆发过大规模勒索事件:攻击者扫描全网 27017 端口,发现无认证的实例就直接清空数据、留下勒索信索要赎金。正确姿势永远是:认证开着 + 只监听本机或内网 IP + 防火墙/安全组兜底。

验证方法

配置完成后的完整验证链:

# 1. 带认证登录管理员
$ mongosh -u admin -p --authenticationDatabase admin
预期输出:
Enter password: ********
Current Mongosh Log ID: ...
connecting to: mongodb://127.0.0.1:27017/
// 2. 在 mongosh 里确认连接身份
db.runCommand({ connectionStatus: 1 })
预期输出:
{ authInfo: { authenticatedUsers: [ { user: 'admin', db: 'admin' } ], authenticatedUserRoles: [...] }, ok: 1 }
// 3. 关键反向验证:不带凭据登录后查库,应被拒绝
show dbs
预期输出:
MongoServerError: command listDatabases requires authentication

能登录、身份正确、未认证被拒 —— 三条全过,认证才算配好。

常见问题

现象 根因 解决
开了认证但登录报 Authentication failed 没指定 --authenticationDatabase admin,客户端默认向当前库验证 登录命令补上 --authenticationDatabase admin
远程机器连不上 四层排查:bindIp 只听 127.0.0.1 / 防火墙没放行 / 云安全组没放行 / 用户没授权远程来源 逐层检查:配置 bindIp 加内网 IP → 防火墙 放行 27017 限内网 → 安全组同规则 → 确认用户角色
systemctl restart mongod 后服务起不来 YAML 缩进错误(Tab、多空格、少空格) tail -50 /var/log/mongodb/mongod.log 看报错行号,修配置文件缩进
服务运行中突然停止,日志有磁盘错误 数据目录所在分区写满 清理磁盘后重启;日常用监控/日志轮转防复发,见 备份与定时任务
执行命令报 Code=13 Unauthorized 当前用户没有该命令对应的角色权限 换 admin 账号登录,或给业务账号补最小必需角色

验证方法

是什么

独立的整体验证环节:从「服务活着」到「认证生效」到「远程可达」逐层确认,而不是装完就拍脑袋认为没问题。

怎么做

# 1. 服务状态
$ sudo systemctl status mongod

# 2. 版本
$ mongosh --version

# 3. 认证登录
$ mongosh -u admin -p --authenticationDatabase admin

验证方法

上面第 3 步的预期输出:

预期输出:
Enter password: ********
Current Mongosh Log ID: ...
connecting to: mongodb://127.0.0.1:27017/admin

进入 mongosh 后执行 show dbs 能列出 admin 等系统库,即代表「服务 + 认证」双验证通过。

常见问题

现象 根因 解决
systemctl status mongod 显示 failed 或 activating (auto-restart) mongod 启动失败或异常退出后正处于自动重试中,不会显示 active (exited) 查 /var/log/mongodb/mongod.log 定位退出原因(多为配置文件或权限问题)

托管为 systemd 服务

是什么

apt 包安装时已自动注册 mongod.service,开机自启和崩溃拉起都由 systemd 负责,不需要自己写 unit 文件。服务的名字是 mongod(不是 mongodb,也不是 mongodb-org),这个拼写细节新手经常记错。

怎么做

# 开机自启并立即启动(安装时其实已自动执行,这里用于确认)
$ sudo systemctl enable --now mongod

# 日常管理
$ sudo systemctl status mongod
$ sudo systemctl restart mongod
$ sudo systemctl stop mongod

验证方法

$ sudo systemctl is-active mongod
预期输出:
active

常见问题

现象 根因 解决
改了配置重启没生效 改的是别的路径下的配置文件 确认改的是 /etc/mongod.conf,且重启了服务

systemd 的启动原理、日志查看(journalctl)、自定义 unit 等通用知识见 systemd 服务管理。

数据备份

是什么

官方工具 mongodump(导出)与 mongorestore(恢复),按库导出 BSON 文件,是 MongoDB 最常用的备份方式。

怎么做

# 备份单个业务库(带认证时需指定账号与验证库)
$ mongodump --db appdb -u appuser -p --authenticationDatabase appdb -o /backup/

# 恢复到同名库
$ mongorestore --db appdb -u admin -p --authenticationDatabase admin --drop /backup/appdb/

参数说明

参数 作用
--db appdb 只备份/恢复这一个库
-o /backup/ 输出目录,会自动建 appdb 子目录
--drop 恢复前先清空目标集合,避免数据叠加
--authenticationDatabase 指定向哪个库验证账号,漏掉必报认证失败

验证方法

# 看备份产物是否生成
$ ls /backup/appdb/
预期输出:
collection1.bson    collection1.metadata.json    ...

常见问题

现象 根因 解决
mongodump 报认证失败 没写 --authenticationDatabase 补上该参数

定时自动备份、备份轮转、上传异地,见 备份与定时任务。

常见问题

是什么

前面各节已分散列出高频坑,这里汇总一张总表,排错时先查这里。

怎么做

按「现象 → 根因 → 解决」三栏快速定位,多数问题在日志(/var/log/mongodb/mongod.log)和 systemctl 状态里都能找到直接线索。

验证方法

每次修改后,固定跑一遍验证链:systemctl is-active mongod → 带认证登录 → show dbs 正常返回。

常见问题

现象 根因 解决
认证开了连不上 --authenticationDatabase 没指定 登录命令加 --authenticationDatabase admin
远程连不上 bindIp / 防火墙 / 安全组 / 用户 host 四层中某层没通 见「最小必要配置」一节的四层排查清单
mongod 起不来 YAML 缩进错误 看日志报错行,修 /etc/mongod.conf 缩进
磁盘满导致服务停止 数据目录分区写满 清理磁盘 → 重启 → 建立定时备份与监控
Code=13 Unauthorized 账号角色权限不足 用正确角色登录,或补授最小权限

本章命令速查

# 安装(官方源三步:key → 源 → 装)
$ curl -fsSL https://www.mongodb.org/static/pgp/server-8.0.asc | sudo gpg -o /usr/share/keyrings/mongodb-server-8.0.gpg --dearmor
$ echo "deb [ arch=amd64,arm64 signed-by=/usr/share/keyrings/mongodb-server-8.0.gpg ] https://repo.mongodb.org/apt/ubuntu jammy/mongodb-org/8.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-8.0.list
$ sudo apt-get update && sudo apt-get install -y mongodb-org
$ sudo apt-mark hold mongodb-org mongodb-org-server mongodb-org-mongos mongodb-org-tools mongodb-org-database-tools mongodb-org-database-tools-extra mongodb-mongosh

# 服务管理
$ sudo systemctl enable --now mongod
$ sudo systemctl status mongod
$ sudo systemctl restart mongod

# 用户与认证
$ mongosh
> use admin
> db.createUser({ user: "admin", pwd: "<强密码>", roles: [...] })
# 然后在 /etc/mongod.conf 中开启 security.authorization: enabled 并重启

$ mongosh -u admin -p --authenticationDatabase admin
$ mongosh -u appuser -p --authenticationDatabase appdb appdb

# 验证(mongosh 内命令用 > 提示符)
$ mongosh
> db.runCommand({ connectionStatus: 1 })

# 备份恢复
$ mongodump --db appdb -u appuser -p --authenticationDatabase appdb -o /backup/
$ mongorestore --db appdb --drop /backup/appdb/

相关章节

章节 内容
版本速查表 全书软件版本基线的唯一权威来源
软件包管理 apt-mark hold、apt 源配置原理
防火墙 远程访问 27017 时的端口放行
systemd 服务管理 mongod 服务的启动原理与日志
源码编译安装 不用包管理器时的安装思路
备份与定时任务 mongodump 自动化与异地备份

📄 Redis 安装与配置 26-redis.md


title: 26 Redis 安装与配置 updated: 2026-09 applies-to: Ubuntu 22.04 LTS

Redis 是最流行的内存键值数据库,常做缓存、会话存储、限流计数器。它快,但也因为「默认无密码」出过海量安全事故。本章目标:装最新稳定版 → 把安全配扎实 → 会验证、会备份。

[!IMPORTANT] 本章版本号截至 2026 年 9 月。版本信息的唯一权威来源是 版本速查表,冲突时以速查表为准。

版本选择

是什么

Redis 在 2024-03 把许可证从 BSD 改成了 RSALv2 / SSPLv1 双许可,社区因此分叉出了 BSD 许可的 Valkey;2025-05 随 Redis 8.0 又新增 AGPLv3,才形成现在的「三选一」。选择前先搞清楚两件事:版本线、许可影响。

项目 值 说明
推荐版本 8.x(截至 2026-09;小版本以官方源为准,核对方法见 91 章) 当前稳定大版本,功能与性能最好
Ubuntu 22.04 系统源 6.0 太旧,安全更新和命令支持都落后,不要用
License Redis 8.0 起 AGPLv3(另可选 RSALv2/SSPLv1) AGPLv3 对一般商用(自己服务器跑业务)是自由的
BSD 替代品 Valkey Linux 基金会托管的社区分叉,命令与 Redis 兼容,在意宽松许可可选它
获取渠道 官方 apt 源(packages.redis.io) 系统源版本太旧,必须加官方源

怎么做

先看系统源里默认装的是什么版本:

# 查看 Ubuntu 22.04 系统源的 Redis 候选版本
$ apt policy redis-server
预期输出:
redis-server:
  Installed: (none)
  Candidate: 5:6.0.16-1ubuntu1+deb12u3
  Version table:
     5:6.0.16-1ubuntu1+deb12u3 500
        500 http://cn.archive.ubuntu.com/ubuntu jammy/universe amd64 Packages

Candidate 是 6.0,落后当前稳定版两个大版本,这就是必须加官方源的理由。加源命令见下一节。

参数说明

场景 选哪个 为什么
新项目、缓存场景 Redis 8.x(官方源) 最新特性(如内建查询能力)+ 持续安全更新
在意宽松 BSD 许可 Valkey 命令兼容,客户端几乎无感,切换成本低
只想快速试玩 系统源 6.0 勉强可用 生产环境不要用,版本太旧

验证方法

加完官方源后确认能装到 8.x:

$ apt policy redis-server
预期输出:
redis-server:
  Installed: (none)
  Candidate: 8:8.x-1rl1~jammy1
  Version table:
     8:8.x-1rl1~jammy1 500
        500 https://packages.redis.io/deb jammy/main amd64 Packages

(8.x 代表实际小版本号,以官方源当时提供的为准。)

常见问题

现象 根因 解决
apt install redis-server 装成了 6.0 没加官方源,或加了源但没 apt-get update 补官方源并 update,再确认 apt policy 的 Candidate

安装

是什么

走 Redis 官方 apt 源(packages.redis.io)安装,后续 apt upgrade 即可收到官方更新。

怎么做

$ sudo apt-get install lsb-release curl gpg
$ curl -fsSL https://packages.redis.io/gpg | sudo gpg --dearmor -o /usr/share/keyrings/redis-archive-keyring.gpg
$ echo "deb [signed-by=/usr/share/keyrings/redis-archive-keyring.gpg] https://packages.redis.io/deb $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/redis.list
$ sudo apt-get update
$ sudo apt-get install redis

[!NOTE] 注意包名:官方源安装用 apt-get install redis(不是 redis-server,那是 Ubuntu 系统源的旧包名),它会把服务端、客户端一起装好并注册 systemd 服务。

备选安装方式(适合官方源不可达的场景):

  • 源码编译:下载 tar 包 → make → make install 三步,自由度最高但升级麻烦,见 源码编译安装。
  • Docker:docker run -d -p 127.0.0.1:6379:6379 redis:8 一行起服务,适合隔离测试,见 Docker。

参数说明

安装命令各部分作用:

片段 作用
lsb-release curl gpg 拿系统代号(jammy)、下载 key、解 key 的工具
gpg --dearmor -o ...keyring.gpg 把 ASCII 公钥转成 apt 可用的二进制 keyring(apt-key 已废弃)
$(lsb_release -cs) 自动取系统代号,输出 jammy
signed-by= 显式声明这个源只认哪个 key,比全局 key 更安全

验证方法

# 客户端和服务端版本都要确认
$ redis-server --version
$ redis-cli --version
预期输出:
Redis server v=8.x ...
Redis client v=8.x ...

常见问题

现象 根因 解决
apt update 报签名错误 key 没导入成功或路径不符 重跑 curl ... gpg --dearmor 那条命令,核对 -o 路径与源文件里 signed-by 一致

[!NOTE] RHEL / CentOS 用户:服务名是 redis 而不是 redis-server(Ubuntu 为 redis-server);官方也提供 yum 源(或用 EPEL),配置思路与本章相同。

RHEL 对照:安装与服务名差异(点击展开)
# RHEL 系可加 EPEL 获得较新版本(企业内更常见用 Remi 源)
$ sudo dnf install -y redis

# 服务名不同:RHEL 是 redis,Ubuntu 是 redis-server
$ sudo systemctl enable --now redis

环境变量与 PATH

是什么

apt 安装会把 redis-server、redis-cli 放进 /usr/bin,本就在 PATH 中,无需配置环境变量。

怎么做

$ which redis-server redis-cli
预期输出:
/usr/bin/redis-server
/usr/bin/redis-cli

验证方法

任意目录执行 redis-cli --version 能出版本号即可。

常见问题

现象 根因 解决
command not found 走了源码编译但没装到系统路径 确认 make install 是否执行,或参考 源码编译安装

最小必要配置

是什么

Redis 的配置文件是 /etc/redis/redis.conf。默认状态下它是无密码 + 只监听 127.0.0.1——这两个默认值是它历史上无数次被黑的原因,也是本章要重点处理的部分。

[!WARNING] 为什么这么紧张:Redis 勒索/入侵史。 Redis 默认无密码,历史上大量运维把 bind 改成 0.0.0.0 又不设密码,6379 端口直接裸奔公网。攻击者用 redis-cli 连上后,通过 CONFIG SET dir 把 Redis 的落盘目录改到 /root/.ssh 或 /var/spool/cron,再写入 SSH 公钥或定时任务,直接拿下整台服务器。所以本章规则只有一条:改监听地址之前,必须先设密码;两者缺一不可,且 6379 永远不对公网开放。

怎么做

第一步:设置访问密码(二选一,推荐先学 requirepass)。

编辑 /etc/redis/redis.conf:

# 设置一个强密码(requirepass 后面直接跟密码,没有等号)
requirepass <强密码>

Redis 6+ 支持 ACL(访问控制列表),能按「用户 / 键模式 / 命令组」三个粒度控制,比单一密码强得多:

# 方式 A:关闭默认用户,强制所有连接走 ACL 账号
user default off

# 方式 B(更简单):保留 requirepass,同时为应用建受限用户
# 语法:user <名字> on >密码 ~键模式 +命令组
user appuser on ><强密码> ~appdb:* +@read +@write

上述 appuser 只能读写键名以 appdb: 开头的键。用 redis-cli 交互式创建也一样:

$ redis-cli
127.0.0.1:6379> ACL SETUSER appuser on '>应用密码' ~appdb:* +@read +@write
127.0.0.1:6379> ACL SAVE

[!NOTE] ACL SAVE 的前提是在 redis.conf 里配置了 aclfile(如 aclfile /etc/redis/users.acl)并把用户定义迁移过去 —— 默认配置没有 aclfile 时执行会报错。若不想引入 aclfile,可改用 CONFIG REWRITE 方案:ACL 用户定义会随其他配置一起写回 redis.conf。

第二步:确认监听地址。

# 只监听本机;确需内网访问时追加内网 IP,逗号分隔
bind 127.0.0.1 <内网IP>

# 保护模式:既没 bind 内网又没设密码时,拒绝外部连接(保持 yes)
protected-mode yes

第三步:禁用或改名危险命令。

# 置空 = 禁用该命令(清空全部数据、改运行时配置,被入侵时是首选武器)
rename-command FLUSHALL ""
rename-command CONFIG ""

[!WARNING] rename-command CONFIG "" 置空后,CONFIG REWRITE 与依赖 CONFIG 命令的部分监控 / 排查工具将不可用。若运维还需要这些能力,至少改名保留而非置空,例如 rename-command CONFIG "REDIS_CONFIG_9f3a" —— 新名字只有你自己知道,效果接近禁用又不失运维手段。

第四步:调整内存与持久化(推荐基线)。

# 最多使用物理内存的 50-70%,给系统和持久化留余量(示例:8G 内存机器给 4G)
maxmemory 4gb

# 缓存场景:内存满时淘汰最久未用的键
maxmemory-policy allkeys-lru

# 开启 AOF 追加持久化,掉电最多丢 1 秒数据
appendonly yes
# 重启生效
$ sudo systemctl restart redis-server

参数说明

maxmemory-policy 六种淘汰策略对比:

策略 内存满时的行为 适用场景
noeviction(默认) 拒绝写入,报错 不能丢数据的场景(队列、计数),必须配监控
allkeys-lru 淘汰全库中最久未使用的键 典型缓存,绝大多数项目选它
volatile-lru 只在设了过期时间的键里淘汰 LRU 部分键不许丢、部分可丢
allkeys-lfu 淘汰全库中访问频率最低的键 热点数据缓存,比 LRU 更抗偶发扫描
volatile-ttl 优先淘汰剩余存活时间最短的键 想让「快过期的先走」
allkeys-random / volatile-random 随机淘汰 几乎不用

持久化方式取舍:

方式 原理 优点 缺点
RDB(默认开) 定时把全量数据快照存盘 文件小、恢复快 两次快照之间宕机,这段数据全丢
AOF 每条写命令追加到日志 最多丢 1 秒(everysec) 文件大、恢复慢
推荐 纯缓存只留 RDB;当数据存储用则 RDB + AOF 双开 — —

其他关键参数:

参数 建议值 说明
port 6379 默认端口,一般不改
daemonize no 由 systemd 托管时保持 no(Ubuntu 包默认已配好,不用动)
supervised 按 systemd 需要配置 Ubuntu 官方包已处理好,不要照搬老教程去改

验证方法

# 1. 不带密码 ping,应被拒绝
$ redis-cli ping
预期输出:
(error) NOAUTH Authentication required
# 2. 带密码 ping
$ redis-cli -a '<强密码>' ping
预期输出:
PONG
# 3. 用 URL 方式连(密码含特殊字符时更稳)
$ redis-cli -u "redis://:<强密码>@127.0.0.1:6379" ping
预期输出:
PONG

常见问题

现象 根因 解决
设了密码后本机应用连不上 应用配置里没写密码 应用连接串补上密码
-a 报 (error) ERR Client sent AUTH, but no password is set 配置没重启生效 sudo systemctl restart redis-server

验证方法

是什么

安装与安全配置完成后的整体验收:版本、连通性、认证、内存策略逐项过。

怎么做

# 版本
$ redis-server --version

# 认证连通
$ redis-cli -a '<强密码>' ping

# 版本细节(服务端运行时信息)
$ redis-cli -a '<强密码>' INFO server | grep redis_version

验证方法

预期输出:
PONG
redis_version:8.x

常见问题

现象 根因 解决
ping 返回 PONG 但不带密码也能连 密码配置没生效 检查配置文件是否写对段、是否重启

托管为 systemd 服务

是什么

apt-get install redis 已自动注册 systemd 服务,Ubuntu 上服务名是 redis-server,开机自启无需额外配置。它由包自带 unit 文件托管,不要自己再写一个。

怎么做

# 确认自启已开启并立即启动
$ sudo systemctl enable --now redis-server

# 日常管理
$ sudo systemctl status redis-server
$ sudo systemctl restart redis-server
$ sudo systemctl stop redis-server

验证方法

$ sudo systemctl is-active redis-server
预期输出:
active

常见问题

现象 根因 解决
systemctl start redis-server 报 unit 不存在 装的是系统源旧包或源码编译没配 unit 用官方源安装;源码场景参考 systemd 服务管理 自己写 unit

systemd 通用知识(unit 结构、journalctl 日志、资源限制)见 systemd 服务管理。

基本使用

是什么

五分钟掌握最常用的六个命令,覆盖缓存读写的基本闭环:写入、读取、删除、过期。

怎么做

$ redis-cli -a '<强密码>'

# 写入字符串
127.0.0.1:6379> SET greeting "hello"
# 读取
127.0.0.1:6379> GET greeting
# 删除
127.0.0.1:6379> DEL greeting
# 设置 60 秒过期
127.0.0.1:6379> SET session:abc "user1" EX 60
# 查看剩余存活秒数
127.0.0.1:6379> TTL session:abc
# 列出匹配键
127.0.0.1:6379> KEYS "session:*"

参数说明

命令 作用 备注
SET key value 写入 EX 60 可同时设 60 秒过期
GET key 读取 不存在返回 nil
DEL key 删除 —
EXPIRE key 60 给已有键设过期 TTL 查剩余秒数,-1 表示永不过期
KEYS pattern 按模式列键 见下方 TIP,生产禁用

[!TIP] 生产环境禁用 KEYS *。 它会一次性遍历全部键并阻塞 Redis,键多时整个服务卡死数秒。线上排查用 SCAN 0 MATCH "session:*" COUNT 100 渐进式遍历,不阻塞。

验证方法

127.0.0.1:6379> SET greeting "hello"
OK
127.0.0.1:6379> GET greeting
"hello"

常见问题

现象 根因 解决
SET 报 NOAUTH 连接后没 AUTH 先执行 AUTH <密码> 或登录时用 -a

数据备份

是什么

Redis 的持久化文件本身就是备份产物:RDB 快照在 /var/lib/redis/dump.rdb,AOF 在同目录的 appendonly.aof。备份 = 定期把这些文件拷走。

怎么做

# 手动触发一次 RDB 快照(阻塞极短)
$ redis-cli -a '<强密码>' BGSAVE

# 确认落盘时间
$ redis-cli -a '<强密码>' INFO persistence | grep rdb_last_save_time

# 拷贝快照到备份目录
$ sudo cp /var/lib/redis/dump.rdb /backup/redis-$(date +%F).rdb

参数说明

方式 场景
BGSAVE 后台 fork 子进程做快照,生产用这个;SAVE 会阻塞主进程,禁用
拷 .rdb 文件 恢复时停服、替换文件、重启即可

验证方法

$ ls -lh /backup/
预期输出:
-rw-r--r-- 1 root root 1.2M Sep 29 10:00 redis-2026-09-29.rdb

常见问题

现象 根因 解决
BGSAVE 报错或日志出现 MISCONF 数据目录磁盘满或属主不对 清磁盘 / chown redis:redis /var/lib/redis 后重启

定时备份与轮转见 备份与定时任务。

常见问题

是什么

汇总全章高频故障,排错先查这张表,再查日志(journalctl -u redis-server)。

怎么做

按「现象 → 根因 → 解决」三栏定位;远程连不上一律走「bind → protected-mode → 防火墙 → 云安全组」四层排查。

验证方法

每次改动后固定跑:systemctl is-active redis-server → 带密码 ping 返回 PONG。

常见问题

现象 根因 解决
远程连不上 bind 没含内网 IP / protected-mode 拦截 / 防火墙 / 安全组 四层逐项排查;确认确实需要远程再放行,且必须已设密码
NOAUTH Authentication required 连接未带密码 redis-cli -a '<密码>' 或连接后 AUTH
密码含特殊字符时 -a 报错 shell 把 !、$ 等做了历史展开/变量替换 改用 -u "redis://:<URL编码后的密码>@127.0.0.1:6379",特殊字符做 URL 编码
写入报 (error) OOM command not allowed 内存达到 maxmemory 且策略是 noeviction 纯缓存场景改 maxmemory-policy allkeys-lru;数据场景扩内存并加监控
MISCONF Redis is configured to save RDB snapshots RDB 落盘失败:磁盘满或目录无写权限 清理磁盘、修正 /var/lib/redis 属主,重启后 BGSAVE 验证
RDB 保存一直失败 同上,另有内核 vm.overcommit_memory 限制 sysctl vm.overcommit_memory=1(写入 sysctl 配置持久化)

本章命令速查

# 安装(官方源)
$ sudo apt-get install lsb-release curl gpg
$ curl -fsSL https://packages.redis.io/gpg | sudo gpg --dearmor -o /usr/share/keyrings/redis-archive-keyring.gpg
$ echo "deb [signed-by=/usr/share/keyrings/redis-archive-keyring.gpg] https://packages.redis.io/deb $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/redis.list
$ sudo apt-get update && sudo apt-get install redis

# 服务管理(Ubuntu 服务名是 redis-server)
$ sudo systemctl enable --now redis-server
$ sudo systemctl status redis-server
$ sudo systemctl restart redis-server

# 验证
$ redis-server --version
$ redis-cli -a '<强密码>' ping
$ redis-cli -u "redis://:<强密码>@127.0.0.1:6379" ping
$ redis-cli -a '<强密码>' INFO server | grep redis_version

# 基本使用
$ redis-cli -a '<强密码>'
> SET key value
> GET key
> DEL key
> TTL key
> SCAN 0 MATCH "prefix:*" COUNT 100

# 备份
$ redis-cli -a '<强密码>' BGSAVE
$ sudo cp /var/lib/redis/dump.rdb /backup/redis-$(date +%F).rdb

相关章节

章节 内容
版本速查表 全书软件版本基线的唯一权威来源
Docker 用容器跑 Redis 的替代方案
systemd 服务管理 redis-server 服务与日志管理
源码编译安装 官方源不可达时的编译安装
备份与定时任务 RDB 自动备份与轮转
防火墙 内网放行 6379 的正确姿势

📄 RabbitMQ 安装与配置 27-rabbitmq.md


title: 27 RabbitMQ 安装与配置 updated: 2026-09 applies-to: Ubuntu 22.04 LTS

RabbitMQ 是最常用的开源消息队列之一,负责在应用之间「异步传话」:下单后发短信、日志上报、任务分发,都可以丢给消息队列慢慢处理。本章带你走完:选版本 → 装 Erlang → 装 RabbitMQ → 最小配置 → 建用户和 vhost → 验证 → 交给 systemd 托管。

[!IMPORTANT] 本章版本号截至 2026 年 9 月。版本信息的唯一权威来源是 版本速查表,冲突时以速查表为准。

版本选择

是什么

RabbitMQ 不是独立程序,它是跑在 Erlang/OTP 运行时上的应用。Erlang 是一门为电信级高并发设计的语言,RabbitMQ 借用它获得了极强的消息吞吐和故障恢复能力 —— 代价是:装 RabbitMQ 之前必须先装一个版本匹配的 Erlang。版本不匹配轻则报依赖错误,重则运行时不稳定,这是 RabbitMQ 安装阶段 90% 的问题来源。

项目 值 说明
推荐版本 4.3.x(如 4.3.5 / 4.3.6+) 截至 2026-09 的稳定线
不建议 3.13.x 及更早 已陆续 EOL,别在新机器上装
依赖运行时 Erlang/OTP 27.x(推荐) 见下表
Ubuntu 22.04 系统源 版本很旧 必须走 RabbitMQ 官方源

怎么做

先看版本对应表,确定「RabbitMQ 版本 ↔ Erlang 版本」的搭配,再去安装:

RabbitMQ 版本 最低 Erlang 最高 Erlang 说明
4.3.x(4.3.0 – 4.3.5) 26.2 27.x 4.3 全系完整支持 Erlang 27
4.3.6 及以上 26.2 28 / 29(28 仅限全新集群) 从 4.3.6 起才支持 Erlang 28/29
4.2.x 26.2 27.x 上一稳定线,仍可运行但不建议新装
3.13.x 25.x 26.x Erlang 26 已于 2026-05 EOL,不要再用

结论:装 Erlang 27.x + RabbitMQ 4.3.x(尽量 4.3.6+)。理由:

  1. Erlang 27 是当前支持期最长的推荐线;Erlang 26 已 EOL,没有安全补丁;
  2. Erlang 28 虽然从 4.3.6 起可用,但官方注明仅限全新集群,升级场景不支持,初学者没必要趟这个水;
  3. 官方源会自动解析匹配的 Erlang 依赖,跟着本章走基本不会踩版本坑。

验证方法

装完后用下面的命令核对版本(详见「验证方法」一节):

$ rabbitmqctl version
预期输出:
RabbitMQ 4.3.6

常见问题

  • 能不能用 apt install erlang 装系统源的 Erlang? 不要。Ubuntu 22.04 源里的 Erlang 版本很旧,且依赖树里混入大量无关包,容易和官方 RabbitMQ 的依赖要求冲突。
  • 装最新 Erlang 一定更好吗? 不是。以对应表为准,超范围反而起不来。

安装

是什么

安装分两步:先装 Erlang(来自 RabbitMQ 官方 Erlang 源),再装 RabbitMQ(来自 RabbitMQ 官方服务器源)。两个源都是 Team RabbitMQ 官方维护的,签名可信、版本匹配。

[!WARNING] 安装顺序铁律:先配好官方 Erlang 源、绝不混入系统源的 Erlang,再一条命令装 rabbitmq-server(apt 会自动解析 Erlang 依赖)。 直接装而没配 Erlang 源,或中途混用系统源的 Erlang,会出现依赖冲突导致装不上(典型报错是 rabbitmq-server : Depends: erlang-... 一长串)。如果已经装坏,先 sudo apt purge 'erlang*' rabbitmq-server 清干净再按本章顺序重来。

怎么做

第 1 步:添加 RabbitMQ 官方 Erlang 源。

# 更新包索引并安装基础工具
$ sudo apt update && sudo apt install -y curl gnupg apt-transport-https

# 下载 RabbitMQ 团队签名的 GPG 密钥到 keyrings 目录
$ curl -1sLf "https://keys.openpgp.org/vks/v1/by-fingerprint/0A9AF2115F4687BD29803A206B73A36E6026DFCA" | sudo gpg --dearmor | sudo tee /usr/share/keyrings/io.rabbitmq.rabbitmq-erlang.gpg > /dev/null

# 添加 Erlang 官方源(jammy 是 Ubuntu 22.04 的代号)
$ echo "deb [signed-by=/usr/share/keyrings/io.rabbitmq.rabbitmq-erlang.gpg] https://ppa1.rabbitmq.io/rabbitmq/rabbitmq-erlang/deb/ubuntu jammy main" | sudo tee /etc/apt/sources.list.d/rabbitmq-erlang.list

[!NOTE] 源地址和密钥指纹以 RabbitMQ 官方文档(Install: Debian and Ubuntu 页面)为准,官方偶尔会更换密钥托管方式;本章写法截至 2026-09 可用。

第 2 步:添加 RabbitMQ 官方服务器源。

# 下载 RabbitMQ 服务器源的签名密钥
$ curl -1sLf "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0xf77f1eda57ebb1c7" | sudo gpg --dearmor | sudo tee /usr/share/keyrings/io.rabbitmq.rabbitmq-server.gpg > /dev/null

# 添加 RabbitMQ 服务器源
$ echo "deb [signed-by=/usr/share/keyrings/io.rabbitmq.rabbitmq-server.gpg] https://ppa1.rabbitmq.io/rabbitmq/rabbitmq-server/deb/ubuntu jammy main" | sudo tee /etc/apt/sources.list.d/rabbitmq-server.list

# 刷新包索引
$ sudo apt update

第 3 步:安装。官方源会自动带上匹配版本的 Erlang 依赖,这是推荐路径 —— 你不需要手动装 Erlang。

# 确认候选版本来自官方源、是 4.3.x
$ apt policy rabbitmq-server

# 安装(apt 会自动解析并安装匹配的 Erlang 27.x)
$ sudo apt install -y rabbitmq-server

参数说明

步骤 为什么这么做
GPG 密钥放 /usr/share/keyrings/ apt 只信任 signed-by 指定密钥签名的包,避免全局信任
Erlang 单独建源 保证拿到 RabbitMQ 团队验证过的 Erlang 版本,而不是系统旧版
用官方 server 源一键装 apt 自动解决「RabbitMQ 4.3 ↔ Erlang 27」的依赖匹配,不易装错

验证方法

# 查看装上的版本
$ rabbitmqctl version
预期输出:
RabbitMQ 4.3.6
# 确认 Erlang 版本在 27.x 支持范围内
$ erl -noshell -eval 'io:format("~s~n", [erlang:system_info(otp_release)]), halt().'
预期输出:
27

常见问题

  • 报 Depends: erlang-nox (= 1:27.x) 之类:说明混了系统源 Erlang,见上方 WARNING 清理重来。
  • apt update 报 GPG 签名无效:密钥下载不完整,重新执行下载密钥那条命令。

环境变量与 PATH

是什么

RabbitMQ 的高级行为(节点名、端口、文件路径等)可以通过环境变量调整;而日常运维参数(端口、内存阈值、监听地址)更推荐写在配置文件里。两者优先级不同,初学者容易混淆。

怎么做

绝大多数情况你不需要设置任何环境变量,用默认值即可。只有改节点名 / 端口时才需要。环境变量写在 /etc/rabbitmq/rabbitmq-env.conf 里:

# 节点名(默认 rabbit@主机名,跨主机组集群时才有必要改)
NODENAME=rabbit@node1

# AMQP 监听端口(默认 5672,一般不改)
NODE_PORT=5672

改完环境变量必须重启服务才生效:

$ sudo systemctl restart rabbitmq-server

日常运维参数写 /etc/rabbitmq/rabbitmq.conf,本章「最小必要配置」给完整示例。

参数说明

配置优先级(从高到低):

层级 位置 格式 初学者建议
1 advanced.config Erlang 语法 不要碰,只在 rabbitmq.conf 表达不了的极少数场景用
2 rabbitmq.conf key = value 日常配置全写这里,新式主配置
3 rabbitmq-env.conf 环境变量 只放 NODENAME / NODE_PORT 等启动期变量

[!TIP] 网上老教程常让你改 Erlang 格式的配置文件,那是旧版做法。RabbitMQ 3.7 之后统一用 rabbitmq.conf 的 key = value 格式,直观得多。

验证方法

# 确认配置文件被正确解析(会列出当前生效配置)
$ sudo rabbitmqctl environment | grep -A2 vm_memory_high_watermark
预期输出:
{vm_memory_high_watermark,{relative,0.6}},

常见问题

现象 根因 解决
改了配置不生效 改完没重启 sudo systemctl restart rabbitmq-server
改完服务起不来 配置文件语法写错 看日志 /var/log/rabbitmq/ 定位行号,改回合法格式

最小必要配置

是什么

一台刚装好的 RabbitMQ 要做三件事才算「可用且安全」:调好资源阈值、开管理控制台、把默认 guest 换成你自己的管理员账号。vhost(virtual host,虚拟主机)是 RabbitMQ 的隔离单元 —— 类似数据库实例里的 schema,不同业务的交换机和队列放在不同 vhost 里互不干扰。

怎么做

第 1 步:写基础配置文件 /etc/rabbitmq/rabbitmq.conf。

$ sudo tee /etc/rabbitmq/rabbitmq.conf > /dev/null <<'EOF'
listeners.tcp.default = 5672
management.tcp.port   = 15672
vm_memory_high_watermark.relative = 0.6
disk_free_limit.absolute = 2GB
EOF
参数 含义 为什么这么设
listeners.tcp.default AMQP 服务端口 5672 是标准端口,客户端都默认连它
management.tcp.port Web 控制台端口 15672 是管理界面标准端口
vm_memory_high_watermark.relative 内存高水位(占系统内存比例) 默认 0.4 偏保守,小内存机器可放到 0.6;超线后 RabbitMQ 会阻塞生产者自我保护
disk_free_limit.absolute 磁盘剩余空间下限 低于 2GB 节点会自动停止写入,防止磁盘写满把整台机器拖垮

第 2 步:启用管理插件,重启服务。

# 开启 15672 Web 控制台
$ sudo rabbitmq-plugins enable rabbitmq_management

# 应用新配置
$ sudo systemctl restart rabbitmq-server

# 设置开机自启并立即启动
$ sudo systemctl enable --now rabbitmq-server

第 3 步:创建管理员账号,删除默认 guest。

[!IMPORTANT] 默认账号 guest/guest 禁止远程登录 —— 这是 RabbitMQ 的默认安全策略:只允许从本机 localhost 登录。所以装完第一件事就是建自己的管理员,并把 guest 删掉,别指望用 guest 远程连控制台。

# 创建管理员用户(密码里的单引号防止特殊字符被 shell 解析)
$ sudo rabbitmqctl add_user admin '你的强密码'

# 赋予 administrator 标签(控制台管理权限)
$ sudo rabbitmqctl set_user_tags admin administrator

# 给 admin 在根 vhost(/)上配置/读/写全部权限
$ sudo rabbitmqctl set_permissions -p / admin '.*' '.*' '.*'

# 删除默认 guest 账号
$ sudo rabbitmqctl delete_user guest

第 4 步:建业务用户与业务 vhost(应用连 RabbitMQ 时用它们,不要用 admin)。

# 创建业务 vhost
$ sudo rabbitmqctl add_vhost appvhost

# 创建业务用户
$ sudo rabbitmqctl add_user appuser '业务用户的强密码'

# 只给业务用户在 appvhost 上的权限,根 vhost 一概不可见
$ sudo rabbitmqctl set_permissions -p appvhost appuser '.*' '.*' '.*'

第 5 步:防火墙按需放行。

[!WARNING] 15672 管理控制台绝不能对公网开放。 5672 也只应放行给应用所在的网段。消息队列被公网扫到后,弱口令爆破和恶意建队列的事故非常多。放行方法见 防火墙。

# 只对应用网段放行 AMQP 端口(把网段换成你的实际值)
$ sudo ufw allow from 10.0.0.0/24 to any port 5672 proto tcp

# 只对运维 IP 放行管理控制台
$ sudo ufw allow from 203.0.113.10 to any port 15672 proto tcp

参数说明

set_permissions -p <vhost> <user> '<conf>' '<write>' '<read>' 三个正则分别控制配置、写、读;'.*' 表示全部放行。生产上可以收紧,比如只允许写 app.* 开头的交换机。

验证方法

逐项验证配置是否生效:

# 查看用户与标签
$ rabbitmqctl list_users
预期输出:
Listing users ...
user    tags
admin   [administrator]
appuser []
# 查看 vhost 列表
$ rabbitmqctl list_vhosts
预期输出:
Listing vhosts ...
name
/
appvhost

常见问题

现象 根因 解决
超过内存水位后程序发消息卡住 RabbitMQ 阻塞生产者是设计行为 消费跟上后自动恢复;长期卡说明消费端太慢
业务用户登录控制台看不到队列 控制台只展示该用户有权限的 vhost 正常现象;或给它加对应 vhost 权限

验证方法

怎么做

1. 版本与运行状态:

# 版本
$ rabbitmqctl version

# 诊断探活
$ rabbitmq-diagnostics ping
预期输出:
Ping succeeded for node rabbit@myserver
# 状态总览
$ sudo rabbitmqctl status

重点看四段:Runtime 里的 Erlang 版本(应为 27.x)、Listeners 里的监听端口、Queues 里的队列数、Memory 里的内存占用。日常巡检看这四项就够。

2. 端口监听:

# 确认 AMQP 和管理端口都在监听
$ ss -lntp | grep -E '5672|15672'
预期输出:
LISTEN 0 1024 0.0.0.0:5672   0.0.0.0:* users:(("beam.smp",pid=1234,fd=40))
LISTEN 0 1024 0.0.0.0:15672  0.0.0.0:* users:(("beam.smp",pid=1234,fd=41))

3. 管理 API 连通性(用你的管理员账号):

# 用 REST API 查询集群概要
$ curl -s -u admin:'你的强密码' http://localhost:15672/api/overview | head -c 300; echo
预期输出:
{"management_version":"4.3.6","rates_mode":"basic",...,"rabbitmq_version":"4.3.6",...}

4. 浏览器访问 http://<服务器IP>:15672,用 admin 登录看到 Overview 仪表盘即成功。若打不开,按顺序排查:管理插件是否启用 → 防火墙是否放行你的 IP → 是否误用了 guest 账号。

托管为 systemd 服务

是什么

apt 安装的 RabbitMQ 自带 systemd 服务单元,不需要你手写 unit 文件,直接用即可。

怎么做

# 开机自启并立即启动
$ sudo systemctl enable --now rabbitmq-server

# 查看 / 重启 / 停止
$ systemctl status rabbitmq-server
$ sudo systemctl restart rabbitmq-server
$ sudo systemctl stop rabbitmq-server

systemd 的启停原理、日志查看(journalctl -u rabbitmq-server)见 systemd 服务管理。

验证方法

$ systemctl is-active rabbitmq-server && systemctl is-enabled rabbitmq-server
预期输出:
active
enabled

常见问题

现象 根因 解决
状态显示 activating (auto-restart) 反复重启 配置文件语法错误 看 /var/log/rabbitmq/ 日志修正 rabbitmq.conf

常见问题

是什么

下面汇总安装与运行阶段的高频故障。排查通用思路:先 systemctl status,再看 /var/log/rabbitmq/ 日志,最后对照本表。

怎么做

现象 根因 解决
装 RabbitMQ 时报 Erlang 依赖错误 混用了系统源旧 Erlang,或 Erlang 版本超出对应表范围 卸载系统源 Erlang,按「安装顺序铁律」用官方 Erlang 源重装;对照版本对应表
Web 控制台登录不了(远程) guest 禁止远程登录 建独立管理员账号(见「最小必要配置」),删 guest
控制台 15672 打不开 没启用 management 插件 / 防火墙没放行 rabbitmq-plugins enable rabbitmq_management;检查 ufw 规则
rabbitmqctl 报 unable to connect to node 服务没起;epmd 端口被占;主机名解析失败(经典坑) 先 systemctl status;ping $(hostname) 不通就在 /etc/hosts 加一行 127.0.1.1 <主机名>
改过主机名后节点启动失败 hostname 变更导致 Erlang cookie / 节点名不匹配 把 /var/lib/rabbitmq/.erlang.cookie 权限保持 400 且与节点匹配;或改回主机名后重启
节点突然自动停止写入 磁盘剩余低于 disk_free_limit 清理磁盘或下调阈值;rabbitmqctl start_app 恢复

[!WARNING] 删除 vhost 会连带删除其中所有交换机、队列和消息,不可恢复。 执行 rabbitmqctl delete_vhost 前务必确认 vhost 里没有业务数据。

验证方法

修复后统一跑一遍健康检查:

$ rabbitmq-diagnostics ping && sudo rabbitmqctl status | head -5
预期输出:
Ping succeeded for node rabbit@myserver
Status of node rabbit@myserver ...
Runtime
 OS PID: 1234
 OS: Linux
 Uptime (seconds): 3600

常见问题

本节本身就是问题汇总;若上面表格没覆盖你的报错,把 /var/log/rabbitmq/ 里最后 50 行日志带去搜索,基本都能找到同款。

本章命令速查

# ── 安装 ──────────────────────────────
$ sudo apt install -y rabbitmq-server        # 官方源装 4.3.x(自动带 Erlang 27)
$ rabbitmqctl version                        # 查看 RabbitMQ 版本

# ── 服务管理 ──────────────────────────
$ sudo systemctl enable --now rabbitmq-server
$ sudo systemctl restart rabbitmq-server
$ sudo rabbitmqctl status                    # 运行状态(Erlang 版本/端口/队列/内存)
$ rabbitmq-diagnostics ping                  # 节点探活

# ── 用户与 vhost ──────────────────────
$ sudo rabbitmq-plugins enable rabbitmq_management   # 开 15672 控制台
$ sudo rabbitmqctl add_user admin '密码'
$ sudo rabbitmqctl set_user_tags admin administrator
$ sudo rabbitmqctl set_permissions -p / admin '.*' '.*' '.*'
$ sudo rabbitmqctl add_vhost appvhost
$ sudo rabbitmqctl delete_user guest         # 删除默认账号
$ rabbitmqctl list_users                     # 用户列表
$ rabbitmqctl list_vhosts                    # vhost 列表

# ── 验证 ──────────────────────────────
$ ss -lntp | grep -E '5672|15672'
$ curl -s -u admin:'密码' http://localhost:15672/api/overview

相关章节

RHEL / CentOS 系对照(点击展开)
  • 官方同样提供 EL 系列 RPM 源:https://ppa1.rabbitmq.io/rabbitmq/rabbitmq-erlang/rpm/el/9 与 .../rabbitmq-server/rpm/el/9,配 .repo 文件后 dnf install rabbitmq-server。
  • 防火墙用 firewall-cmd --permanent --add-port=5672/tcp 后 firewall-cmd --reload。
  • 服务名与配置文件路径(/etc/rabbitmq/rabbitmq.conf)与 Ubuntu 一致。

📄 Kafka 安装与配置(KRaft 模式) 28-kafka.md


title: 28 Kafka 安装与配置(KRaft 模式) updated: 2026-09 applies-to: Ubuntu 22.04 LTS

Kafka 是大数据与日志采集领域事实上的标准消息系统,擅长高吞吐、可回放的流式数据传输。本章从零装起:下二进制包 → KRaft 模式格式化存储 → 写 systemd 服务 → 建 topic 收发验证。全程只用 KRaft 模式。

[!WARNING] 网上大量教程还在教 ZooKeeper 模式 —— 出现 --zookeeper localhost:2181、zookeeper-server-start.sh 这类命令的,全部是 Kafka 3.x 及更早时代的做法。Kafka 4.0 起已彻底移除 ZooKeeper,只能跑 KRaft 模式,照老教程敲会直接报参数错误。看到 ZooKeeper 相关步骤,直接关掉那个教程。

[!IMPORTANT] 本章版本号截至 2026 年 9 月。版本信息的唯一权威来源是 版本速查表,冲突时以速查表为准。

版本选择

是什么

Kafka 4.x 是「无 ZooKeeper」时代的第一个大版本线:4.0 彻底删掉了 ZooKeeper 相关代码,元数据管理完全交给 Kafka 自身的 KRaft(Kafka Raft)协议。选版本时只需要记住一条:4.x 只能 KRaft,不需要也不存在「装 ZooKeeper」这一步。

项目 值 说明
推荐版本 4.2 / 4.3(4.x 线;具体补丁号以官网 downloads 页为准) 截至 2026-09 的当前稳定线
运行环境 Java 17+(17 / 21 / 25 受支持) Java 11 仅部分模块可用,别用
发布形式 官方二进制包(Scala 2.13 构建) Ubuntu 源里没有 Kafka,必须官网下载
安装包命名 kafka_2.13-4.x.x.tgz 2.13 是 Scala 版本,与你的 JDK 无关

[!NOTE] Kafka 4.x 对 ZooKeeper 模式零支持:server.properties 里写 zookeeper.connect 会直接被拒绝。找配置示例时认准带 process.roles 的 KRaft 写法。

怎么做

新装机器无脑选 4.2+,唯一前置依赖是 JDK 17+,推荐 JDK 21:

# 确认 Java 版本满足 17+
$ java -version
预期输出:
openjdk version "21.0.x" 2026-xx-xx
OpenJDK Runtime Environment (build 21.0.x+xx-Ubuntu-...)

没装或版本低于 17,先按 JDK 安装 装好再回来。

参数说明

场景 选哪个 为什么
新机器、新集群 4.2 / 4.3 当前稳定线,bug 修复和安全补丁都覆盖
跟着老教程学习 仍然 4.x + KRaft 把老教程的 ZooKeeper 步骤跳过,命令按本章写法

验证方法

确认准备就绪:Java 版本(见上)+ 能访问 Apache 下载站:

# 探测下载域名可达
$ curl -sI https://downloads.apache.org/kafka/ | head -1
预期输出:
HTTP/1.1 200 OK

常见问题

现象 根因 解决
启动即报 Java 版本相关错误 JDK 低于 17 换 JDK 21,见 20 JDK
找不到 zookeeper-server-start.sh Kafka 4.x 已移除 正常现象,改走 KRaft 流程

安装

是什么

Kafka 官方以 tar.gz 二进制包发布,解压即用。装到 /opt 下并做一个软链接指向它:路径固定为 /opt/kafka,将来升级只需换软链接指向,所有脚本和 systemd 配置都不用改(目录规划思路见 Shell 与文件系统)。

怎么做

# 下载二进制包(版本号以 https://kafka.apache.org/downloads 为准;下文用 4.x.x 代表实际版本号)
$ wget -P /tmp https://downloads.apache.org/kafka/4.x.x/kafka_2.13-4.x.x.tgz

# 解压到 /opt
$ sudo tar -xzf /tmp/kafka_2.13-4.x.x.tgz -C /opt

# 建软链接 /opt/kafka 指向带版本号的目录
$ sudo ln -s /opt/kafka_2.13-4.x.x /opt/kafka

# 创建专用运行用户(systemd 托管时用,不建 Linux 登录)
$ sudo useradd -r -d /opt/kafka -s /usr/sbin/nologin kafka

# 把安装目录交给 kafka 用户
$ sudo chown -R kafka:kafka /opt/kafka_2.13-4.x.x

[!TIP] 把 /opt/kafka/bin 加入 PATH 可以少敲路径。编辑 ~/.bashrc 加一行 export PATH=$PATH:/opt/kafka/bin,source ~/.bashrc 生效。本章为清晰起见统一用绝对路径。

参数说明

决策 理由
装 /opt 第三方软件的标准位置,与系统目录隔离
软链接 /opt/kafka 升级时换指向即可,PATH / systemd 配置零改动
专用 kafka 用户 服务以最小权限运行,被攻破也不波及系统

验证方法

# 确认解压结果和脚本可执行
$ ls /opt/kafka/bin/kafka-server-start.sh && /opt/kafka/bin/kafka-topics.sh --version
预期输出:
/opt/kafka/bin/kafka-server-start.sh
4.x.x(实际版本号,以官网 downloads 为准)

常见问题

现象 根因 解决
官网 404 或下载极慢 版本已更替 / 网络问题 以官网 downloads 页列出的链接为准;国内可配镜像(见 镜像加速)
tar 报不是 gzip 文件 下载到的是 HTML 错误页 file /tmp/kafka_*.tgz 确认,重新下载

环境变量:确认 JAVA_HOME

Kafka 的启动脚本优先靠 JAVA_HOME 找 JDK(未设置时才退回 PATH 里的 java)。顺着 java 命令反查两层目录,即可拿到当前生效的 JAVA_HOME:

$ dirname $(dirname $(readlink -f $(which java)))
预期输出:
/usr/lib/jvm/java-21-openjdk-amd64

输出的就是 JAVA_HOME 的值。设置 JAVA_HOME、多版本切换的完整做法见 20 JDK;后面用 systemd 托管 Kafka 时,记得在 unit 里用 Environment=JAVA_HOME=... 显式声明(systemd 不读 .bashrc)。

最小必要配置

是什么

Kafka 的全部行为都写在 config/server.properties 里。单机 KRaft 模式下,这个进程同时扮演两个角色:broker(存数据、服务客户端)和 controller(用 Raft 协议管理集群元数据),所以 process.roles 要同时写两个。

怎么做

编辑 /opt/kafka/config/server.properties,关注以下几项(其余保持默认):

# 单机模式:同一进程兼任 broker 和 controller
process.roles=broker,controller

# 节点唯一 ID(集群内不得重复)
node.id=1

# 元数据仲裁地址(单机指向自己;4.x 新版也可用 controller.quorum.bootstrap.servers,以官方配置文档为准)
controller.quorum.voters=1@localhost:9093

# 对内监听与对外宣告地址
listeners=PLAINTEXT://0.0.0.0:9092,CONTROLLER://0.0.0.0:9093
advertised.listeners=PLAINTEXT://<服务器IP或域名>:9092

# controller 与 broker 之间的安全协议映射
listener.security.protocol.map=CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT

# 数据目录(日志段文件存放处)
log.dirs=/opt/kafka/data

# 每个 topic 默认分区数
num.partitions=3

# 消息保留时间(过期自动删除)
log.retention.hours=168

# 关闭「自动创建 topic」,所有 topic 显式创建
auto.create.topics.enable=false

先建数据目录:

$ sudo mkdir -p /opt/kafka/data && sudo chown kafka:kafka /opt/kafka/data

格式化存储(首次启动前必做一次):

[!IMPORTANT] KRaft 模式首次启动前必须用集群 UUID 格式化存储目录。跳过这一步直接启动,日志会报 INCONSISTENT_CLUSTER_ID 或提示 Format the cluster,broker 起不来。

# 生成集群 UUID
$ /opt/kafka/bin/kafka-storage.sh random-uuid
预期输出:
dV2kQ4m8R8yF3fN6xVwJ2A
# 用该 UUID 格式化存储目录(单机 standalone 模式)
$ sudo /opt/kafka/bin/kafka-storage.sh format --standalone \
    -t dV2kQ4m8R8yF3fN6xVwJ2A -c /opt/kafka/config/server.properties

参数说明

参数 含义 为什么重要
process.roles 进程角色 单机合并 broker+controller,省一个进程
node.id 节点编号 元数据仲裁和副本分配都靠它认节点
controller.quorum.voters 谁来投票管理元数据 格式 节点id@主机:9093,单机就是 1@localhost:9093
listeners 实际绑定的地址端口 0.0.0.0 表示监听所有网卡
advertised.listeners 宣告给客户端的地址 客户端先连 bootstrap 地址拿元数据,再用宣告地址收发消息。这是 Kafka 最经典的坑:写 localhost 的话,远程客户端拿到元数据后会去连它自己的 localhost,表现就是连得上但拉不到数据、反复元数据失败。跨机/公网访问必须写真实 IP 或域名
log.dirs 数据目录 放在容量足够、IO 好的盘上
num.partitions 默认分区数 3 是常见起步值,分区是并行消费的单位
log.retention.hours 保留时长 168 即 7 天,按磁盘容量和业务需求调
auto.create.topics.enable=false 禁止隐式建 topic 防止写错 topic 名悄悄建出一堆垃圾 topic

验证方法

# 确认格式化结果
$ sudo ls /opt/kafka/data
预期输出:
meta.properties
# 核对元数据里的集群 ID
$ sudo cat /opt/kafka/data/meta.properties | head -3
预期输出:
cluster.id=dV2kQ4m8R8yF3fN6xVwJ2A
node.id=1
version=1

常见问题

现象 根因 解决
启动报 INCONSISTENT_CLUSTER_ID 数据目录残留旧集群 ID,与新 UUID 不一致 清空 log.dirs 目录后重新 format(确认无数据可丢)
启动报 Format the cluster 类错误 首次启动前没 format 按上文执行 kafka-storage.sh format
改了 server.properties 不生效 没重启 broker sudo systemctl restart kafka

验证方法

是什么

Kafka 不像 RabbitMQ 有统一的「status」命令,验证分三层:进程在听端口 → broker 能响应协议 → 真正建 topic 收发消息。三层都过才算部署成功。

怎么做

1. 端口监听(先启动服务,见下一节的 systemd 托管):

$ ss -lntp | grep 9092
预期输出:
LISTEN 0 50 *:9092  *:* users:(("java",pid=5678,fd=120))

2. broker 协议连通性:

# 列出 broker 支持的 API 版本,能输出即协议层连通
$ /opt/kafka/bin/kafka-broker-api-versions.sh --bootstrap-server localhost:9092 | head
预期输出:
localhost:9092 (id: 1 rack: null) -> ...
Produce(0): 0 to 11 [usable: 11]
Fetch(1): 0 to 16 [usable: 16]
...

3. topic 收发闭环:

# 创建测试 topic:3 分区、单副本(单机只能 1 副本)
$ /opt/kafka/bin/kafka-topics.sh --create --topic test \
    --bootstrap-server localhost:9092 --partitions 3 --replication-factor 1

# 列出所有 topic
$ /opt/kafka/bin/kafka-topics.sh --list --bootstrap-server localhost:9092

# 查看 topic 详情
$ /opt/kafka/bin/kafka-topics.sh --describe --topic test --bootstrap-server localhost:9092
预期输出:
test

Topic: test  TopicId: x8...  PartitionCount: 3  ReplicationFactor: 1
  Topic: test  Partition: 0  Leader: 1  Replicas: 1  Isr: 1
  Topic: test  Partition: 1  Leader: 1  Replicas: 1  Isr: 1
  Topic: test  Partition: 2  Leader: 1  Replicas: 1  Isr: 1
# 控制台生产者:输入几行文字后 Ctrl+C 退出
$ /opt/kafka/bin/kafka-console-producer.sh --topic test --bootstrap-server localhost:9092
预期输出:
>hello kafka
>message two
>
# 控制台消费者:能收到刚才的消息即成功,Ctrl+C 退出
$ /opt/kafka/bin/kafka-console-consumer.sh --topic test --from-beginning --bootstrap-server localhost:9092
预期输出:
hello kafka
message two

[!IMPORTANT] 所有客户端命令一律用 --bootstrap-server localhost:9092。老教程里的 --zookeeper 参数在 4.x 已被移除,使用会直接报错。

常见问题

现象 根因 解决
--list 能通但远程程序连不上 advertised.listeners 配错(写了 localhost 或内网 IP) 见「常见问题」一节的专项排查
消费者收不到消息 topic 名打错 / 消费了别的 group offset 用 --from-beginning 重试,核对 topic 名

托管为 systemd 服务

是什么

Kafka 官方不带服务管理脚本,kafka-server-start.sh 只是前台启动进程,SSH 一断就死。生产环境必须交给 systemd 托管:开机自启、崩溃自动拉起、日志进 journal。systemd 基础见 systemd 服务管理。

怎么做

创建 /etc/systemd/system/kafka.service:

$ sudo tee /etc/systemd/system/kafka.service > /dev/null <<'EOF'
[Unit]
Description=Apache Kafka (KRaft)
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=kafka
Group=kafka
Environment=JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64
ExecStart=/opt/kafka/bin/kafka-server-start.sh /opt/kafka/config/server.properties
ExecStop=/opt/kafka/bin/kafka-server-stop.sh
Restart=on-failure
RestartSec=10
LimitNOFILE=100000

[Install]
WantedBy=multi-user.target
EOF
# 重载 unit 定义并启动
$ sudo systemctl daemon-reload
$ sudo systemctl enable --now kafka

参数说明

指令 作用 为什么这么写
User=kafka 以专用用户运行 最小权限原则,避免 root 跑 Java 服务
Environment=JAVA_HOME=... 显式指定 JDK systemd 不读 .bashrc,环境变量必须在 unit 里给全,否则脚本找不到 java
ExecStart 绝对路径 不依赖 PATH 同上,systemd 环境 PATH 很精简
Restart=on-failure 崩溃自动拉起 异常退出 10 秒后重启,正常 stop 不重启
LimitNOFILE=100000 提高文件句柄上限 Kafka 并发读写大量日志段文件,默认 1024 远远不够

验证方法

$ systemctl status kafka
预期输出:
● kafka.service - Apache Kafka (KRaft)
     Loaded: loaded (/etc/systemd/system/kafka.service; enabled)
     Active: active (running) since ...
   Main PID: 5678 (java)

常见问题

现象 根因 解决
systemd 起不来但手动能跑 unit 里没设 JAVA_HOME,找不到 java 加 Environment=JAVA_HOME=...(确认路径:readlink -f $(which java) 的父目录)
status 显示 203/EXEC ExecStart 路径不对或脚本没执行权限 核对软链接路径;ls -l /opt/kafka/bin/*.sh 看权限
启动后几秒又退出 storage 没 format 或 server.properties 语法错 journalctl -u kafka -n 50 看具体报错

常见问题

是什么

汇总 KRaft 单机部署的高频故障。排查第一入口永远是:journalctl -u kafka -n 100。

怎么做

现象 根因 解决
启动报 storage 未 format / INCONSISTENT_CLUSTER_ID 首次启动没 format,或数据目录混了旧集群 ID 执行 kafka-storage.sh format;有旧数据则先确认可丢再清目录
本机能连,远程客户端超时 / 反复拉元数据失败 advertised.listeners 写了 localhost 或错 IP 改成客户端能路由到的 IP/域名,重启后重连。排查步骤见下
启动直接失败 Java 版本低于 17 升级 JDK 21(见 20 JDK)
磁盘满后写入报错 broker 触发磁盘保护,或日志没清理 清磁盘;核对 log.retention.hours 和磁盘余量
生产者报 NotEnoughReplicas 副本同步数不足(单机 1 副本时通常是 min.insync.replicas 配大) 单机保持默认;集群场景核对 ISR 与 min.insync.replicas
broker 起来但建 topic 失败 controller 角色未生效 / node.id 与 voters 不一致 核对 process.roles 含 controller、controller.quorum.voters 与 node.id 匹配
systemd 起不来,手动能跑 JAVA_HOME 未在 unit 中设置 见上一节

advertised.listeners 专项排查(客户端连不上时按序执行):

# 1. 在服务器上先确认 broker 本机协议连通
$ /opt/kafka/bin/kafka-broker-api-versions.sh --bootstrap-server localhost:9092 | head -1

# 2. 看宣告出去的地址是什么(客户端会拿这个地址连)
$ grep advertised.listeners /opt/kafka/config/server.properties

# 3. 在客户端机器上测试宣告地址是否可达
$ nc -zv <宣告的IP> 9092
预期输出(第 3 步):
Connection to 203.0.113.50 9092 port [tcp/*] succeeded!

三步中任何一步不通,对应修:第 1 步不通查 broker 日志;第 2 步是 localhost 就改成真实地址;第 3 步不通查防火墙与云安全组(见 防火墙)。

[!WARNING] 9092 端口不要对公网裸奔。 Kafka 默认无任何认证,公网可达等于数据对所有人开放读写。生产环境建议:防火墙只放行应用网段;跨网需求上 SASL/SSL 认证加密(配置见官方 Security 文档,初学阶段先把端口收好)。

[!WARNING] 重置消费者组 offset(kafka-consumer-groups.sh --reset-offsets)会让组内消费者从指定位置重新读消息,可能造成重复消费或漏消息,执行前务必停掉消费组并确认偏移量目标。

消费者组速览

生产者发消息,消费者以「组」为单位消费:同组内的消费者分摊分区,各组的消费进度(offset)独立记录。日常最常用的是看积压:

# 查看消费者组详情与积压
$ /opt/kafka/bin/kafka-consumer-groups.sh --describe --group mygroup --bootstrap-server localhost:9092
预期输出:
GROUP    TOPIC  PARTITION  CURRENT-OFFSET  LOG-END-OFFSET  LAG
mygroup  test   0          120             125             5
mygroup  test   1          88              88              0

LAG 列就是积压量:LOG-END-OFFSET(已写入的最新位置)减 CURRENT-OFFSET(组消费到的位置)。LAG 长期增长说明消费速度跟不上生产速度,优先排查消费者性能或加分区。

验证方法

做完一轮故障修复后,跑一遍完整闭环确认:

$ systemctl is-active kafka && /opt/kafka/bin/kafka-topics.sh --list --bootstrap-server localhost:9092
预期输出:
active
test

本章命令速查

# ── 首次部署(4.x.x 代表实际版本号,以官网 downloads 为准)──
wget -P /tmp https://downloads.apache.org/kafka/4.x.x/kafka_2.13-4.x.x.tgz
sudo tar -xzf /tmp/kafka_2.13-4.x.x.tgz -C /opt
sudo ln -s /opt/kafka_2.13-4.x.x /opt/kafka
/opt/kafka/bin/kafka-storage.sh random-uuid                              # 生成集群 UUID
sudo /opt/kafka/bin/kafka-storage.sh format --standalone \
  -t <UUID> -c /opt/kafka/config/server.properties                       # 格式化存储(首次必做)
sudo systemctl daemon-reload && sudo systemctl enable --now kafka

# ── topic 管理 ────────────────────────
/opt/kafka/bin/kafka-topics.sh --create --topic test \
  --bootstrap-server localhost:9092 --partitions 3 --replication-factor 1
/opt/kafka/bin/kafka-topics.sh --list --bootstrap-server localhost:9092
/opt/kafka/bin/kafka-topics.sh --describe --topic test --bootstrap-server localhost:9092

# ── 收发验证 ──────────────────────────
/opt/kafka/bin/kafka-console-producer.sh --topic test --bootstrap-server localhost:9092
/opt/kafka/bin/kafka-console-consumer.sh --topic test --from-beginning --bootstrap-server localhost:9092

# ── 消费者组 ──────────────────────────
/opt/kafka/bin/kafka-consumer-groups.sh --describe --group mygroup --bootstrap-server localhost:9092

# ── 健康检查 ──────────────────────────
ss -lntp | grep 9092
/opt/kafka/bin/kafka-broker-api-versions.sh --bootstrap-server localhost:9092 | head
systemctl status kafka
journalctl -u kafka -n 100

相关章节

RHEL / CentOS 系对照(点击展开)
  • 安装方式完全相同(官方二进制包不区分发行版),仅需自备 JDK 17+(dnf install java-21-openjdk-headless)。
  • 防火墙用 firewall-cmd --permanent --add-port=9092/tcp 后 firewall-cmd --reload。
  • systemd unit 内容与本章一致,无需改动。

📄 Nginx 安装与配置 29-nginx.md


title: 29 Nginx 安装与配置 updated: 2026-09 applies-to: Ubuntu 22.04 LTS

Nginx 是最流行的 Web 服务器与反向代理。本章走完整链路:选版本 → 官方源安装 → 看懂配置结构 → 服务管理与平滑重载 → server/location 核心概念 → 静态站点与反向代理 → 日志与日常排错。

[!IMPORTANT] 本章版本号截至 2026 年 9 月。版本信息的唯一权威来源是 版本速查表,冲突时以速查表为准。

版本选择

是什么

Nginx 把版本线拆成两条:**stable(稳定版)**和 mainline(主线版)。stable 不是"老版本",而是从 mainline 分出来、只接收 bug 修复的分支;mainline 新特性多,但变化也快。

项目 值 说明
推荐版本 1.30 stable(截至 2026-09) 生产环境首选,只修 bug 不加新特性
尝鲜可选 1.31 mainline(截至 2026-09) 新特性先在这条线上出现,适合测试环境
Ubuntu 22.04 系统源 1.18 太旧,且已停止功能更新,不建议新项目使用
License 类 BSD 开源协议 免费商用;另有商业版 Nginx Plus

怎么做

先看系统源里是什么版本,确认"源里只有 1.18"这个前提:

# 查看 apt 会装哪个版本的 nginx
$ apt policy nginx
预期输出:
nginx:
  Installed: (none)
  Candidate: 1.18.0-6ubuntu14.4
  Version table:
     1.18.0-6ubuntu14.4 500
        500 http://cn.archive.ubuntu.com/ubuntu jammy/main amd64 Packages

Candidate 是 1.18,说明系统源太旧。要拿 1.30 stable,需要添加 nginx.org 官方源,见下一节。

参数说明

场景 选哪条线 为什么
生产环境 1.30 stable 只修 bug,行为可预期,升级风险最小
学习、本机实验 1.30 stable 或 1.31 mainline 跟教程走用 stable,出问题最好搜
需要最新特性 1.31 mainline 新模块先上线,但要承担不稳定风险
使用系统源的 1.18 不推荐 缺新特性与修复,且目录结构和官方源不同(见下文)

验证方法

装好之后确认版本符合基线:

# 查看 nginx 版本与编译参数
$ nginx -v
预期输出:
nginx version: nginx/1.30.x

只要大版本是 1.30,就符合本章的 stable 基线。

常见问题

现象 根因 解决
装完版本还是 1.18 用了 Ubuntu 系统源 按下文添加 nginx.org 官方源后重装
apt policy 找不到 nginx 源索引未更新 sudo apt update 后重试
stable 和 mainline 版本号一样大 版本号是平行线不是先后关系 看小版本和分支名,不要只比大小

安装:nginx.org 官方源

是什么

nginx.org 提供自己的 apt 源,能拿到最新的 1.30 stable。添加第三方源的本质是两件事:放进一枚 GPG 公钥(用来验签软件包,防止包被篡改)和写一个 .list 源文件(告诉 apt 去哪下载)。

[!WARNING] 不要使用 apt-key add。 老教程里常见的 curl ... | sudo apt-key add - 已被 Ubuntu 22.04 废弃:用 apt-key 加的密钥会被当作全局信任,且升级系统时会报 apt-key is deprecated 警告。正确做法是把密钥放进 /usr/share/keyrings/,并在源文件里用 signed-by= 指定,让信任范围只限于这一个源。

怎么做

# 安装下载与验签所需的工具
$ sudo apt update
$ sudo apt install curl gnupg2 ca-certificates lsb-release ubuntu-keyring

# 从 nginx 官方下载 GPG 公钥,放到受信任的密钥环目录
$ curl https://nginx.org/keys/nginx_signing.key | gpg --dearmor \
    | sudo tee /usr/share/keyrings/nginx-archive-keyring.gpg > /dev/null

# 写入官方源,signed-by 把验签范围限定在这枚密钥上
$ echo "deb [signed-by=/usr/share/keyrings/nginx-archive-keyring.gpg] \
    http://nginx.org/packages/ubuntu jammy nginx" \
    | sudo tee /etc/apt/sources.list.d/nginx.list > /dev/null

# 更新源索引并安装
$ sudo apt update
$ sudo apt install nginx

参数说明

步骤 关键点 为什么
gpg --dearmor 把 ASCII 文本密钥转成二进制格式 apt 的 signed-by 需要二进制 keyring
/usr/share/keyrings/ 系统级密钥环目录,root 管理 密钥集中存放、权限可控,替代废弃的 apt-key
signed-by= 声明本源只用这枚密钥验签 密钥泄露时影响范围只有这一个源
jammy Ubuntu 22.04 的代号 nginx.org 按代号提供不同版本的包
nginx 源里的组件名 官方源只提供 nginx 一个组件

验证方法

# 确认包来自官方源且版本正确
$ apt policy nginx
预期输出:
nginx:
  Installed: 1.30.x-1~jammy
  Candidate: 1.30.x-1~jammy
  Version table:
 *** 1.30.x-1~jammy 500
        500 http://nginx.org/packages/ubuntu jammy/nginx amd64 Packages
        100 /var/lib/dpkg/status
     1.18.0-6ubuntu14.4 500
        500 http://cn.archive.ubuntu.com/ubuntu jammy/main amd64 Packages

Installed 显示 1.30.x 且来源是 nginx.org,说明官方源生效。

# 启动服务并设置开机自启
$ sudo systemctl enable --now nginx
# 本地请求一次首页
$ curl -I localhost
预期输出:
HTTP/1.1 200 OK
Server: nginx/1.30.x
Content-Type: text/html

[!NOTE] RHEL / CentOS 用户:nginx.org 同样提供 yum 源,安装 nginx.org/packages/rhel 对应的 repo 文件即可;目录结构、配置语法与本章完全一致,差别只在包管理命令换成 dnf。

RHEL 对照
# 创建官方 yum 源文件后安装
$ sudo dnf install nginx
$ sudo systemctl enable --now nginx

常见问题

现象 根因 解决
NO_PUBKEY 报错 GPG 密钥没放对位置 检查 /usr/share/keyrings/nginx-archive-keyring.gpg 是否存在
源加了但还装 1.18 存在旧的 nginx 源文件或优先级设置 grep -r nginx /etc/apt/sources.list.d/ 排查重复源
80 端口被占,nginx 起不来 Apache 等服务先占了 80 `ss -lntp
curl: (7) Connection refused 服务没启动 systemctl status nginx 看状态与报错日志

配置文件结构

是什么

官方源和 Ubuntu 源装出来的 Nginx,配置目录结构完全不同——这是新手最迷惑的一点,网上教程互相矛盾多半因为此。两者主配置 /etc/nginx/nginx.conf 都一样,差别在虚拟主机(server 块)放哪。

对比项 nginx.org 官方源 Ubuntu 系统源
虚拟主机目录 /etc/nginx/conf.d/*.conf /etc/nginx/sites-available/
启用方式 直接把 .conf 文件放进 conf.d/ 软链接到 sites-enabled/
默认站点 无 有一个 default 站点
本教程 以此为准 仅了解

软链机制本身值得理解:sites-available/ 存放所有站点配置,sites-enabled/ 里只放指向它们的软链接。想启用某站点就 ln -s 建一条软链,想停用就删软链——配置本体不丢。官方源的 conf.d/ 则靠 nginx.conf 里一行 include /etc/nginx/conf.d/*.conf; 把目录下所有 .conf 全部加载,放进去就生效,改名为 .conf.bak 才算停用。

怎么做

先看主配置的关键部分:

# 查看主配置中 include 相关的行
$ grep include /etc/nginx/nginx.conf
预期输出:
include /etc/nginx/mime.types;
include /etc/nginx/conf.d/*.conf;

推荐的做法:不要直接改 nginx.conf,每个站点在 conf.d/ 下建一个独立的 .conf 文件,出问题好定位、回滚也容易。

# 备份默认示例配置,避免默认 server 块和自己的站点抢 Host
$ sudo mv /etc/nginx/conf.d/default.conf /etc/nginx/conf.d/default.conf.bak

参数说明

文件/目录 作用 要点
/etc/nginx/nginx.conf 主配置,定义全局参数 负责 include conf.d/*.conf,一般不动
/etc/nginx/mime.types 扩展名 → Content-Type 映射 让浏览器正确识别 css/js/图片类型
/etc/nginx/conf.d/*.conf 每个站点一个文件 官方源的虚拟主机都放这里
/var/log/nginx/ access.log 与 error.log 排错第一现场,见日志一节

验证方法

改完任何配置文件后,先做语法检查再看实际加载情况:

# 检查配置语法
$ sudo nginx -t
预期输出:
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

常见问题

现象 根因 解决
改了配置但站点没变 改到了不生效的文件 官方源下改 sites-available 根本不会被加载,应改 conf.d/*.conf
nginx -t 报 unknown directive 配置里有拼写错误或少了分号 按报错行号检查,nginx 配置每条指令以 ; 结尾
两个站点打开是同一个 server_name 没写或写重了 每个 server 块写清各自的 server_name
default.conf.bak 仍被加载 include 只匹配 *.conf 之外的通配写法被改过 确认 include 是 *.conf;.bak 后缀不会被加载

服务管理:reload vs restart

是什么

Nginx 由 systemd 托管,日常只有三个动作:启动、重载、重启。reload 与 restart 的区别是本章必须建立的直觉:

  • reload:主进程重新读配置,平滑换掉旧 worker 进程,已建立的连接不断开,秒级完成,失败还能回退旧配置。
  • restart:整个进程杀掉再起,所有连接中断。只有改了 nginx.conf 里不可热更的全局项(如 listen 的某些底层参数)或 nginx 完全卡死时才需要。

日常改配置一律 reload,这就是"为什么":生产环境一次 restart 会掐断所有正在处理的请求,而 reload 用户完全无感。

怎么做

# 首次启动并设置开机自启
$ sudo systemctl enable --now nginx

# 改配置后的标准三步:检查 → 重载 → 确认
$ sudo nginx -t
$ sudo systemctl reload nginx
$ curl -I localhost

参数说明

命令 效果 适用场景
sudo systemctl enable --now nginx 设自启 + 立即启动 首次安装
sudo nginx -t 只检查语法,不动进程 每次改配置后、reload 前必做
sudo systemctl reload nginx 平滑重载,不断连接 日常改配置
sudo nginx -s reload 效果同上,绕过 systemd 也可用,推荐统一走 systemctl
sudo systemctl restart nginx 进程级重启,连接中断 仅限 reload 无效或全局参数变更

验证方法

# 确认 nginx 正在监听 80 端口
$ ss -lntp | grep nginx
预期输出:
LISTEN 0      511        0.0.0.0:80        0.0.0.0:*    users:(("nginx",pid=1234,fd=6))
LISTEN 0      511           [::]:80           [::]:*    users:(("nginx",pid=1234,fd=7))

[!IMPORTANT] 改配置后必须先 sudo nginx -t 再 reload。 nginx -t 不通过时 reload 会静默失败——旧配置继续生效,你却以为改成功了。把 nginx -t && sudo systemctl reload nginx 养成肌肉记忆。

常见问题

现象 根因 解决
reload 后行为没变 没先 nginx -t,reload 因语法错失败 先 -t 看报错,修好再 reload
nginx: [emerg] bind() to 0.0.0.0:80 failed 80 被占用 `ss -lntp
restart 后站点 502 一小会儿 重启瞬间后端连接被掐 用 reload 代替 restart

核心概念:server 块与 location 匹配

是什么

server 块 = 虚拟主机。一台服务器上跑多个网站,全靠 server 块区分:Nginx 收到请求后,按请求头里的 Host 字段(即浏览器访问的域名)去匹配 server_name,命中的 server 块处理这次请求。一个 .conf 文件里可以写多个 server 块。

location = URL 路由规则。同一个站点内,不同路径交给谁处理(静态文件?转发后端?),由 location 决定。location 有多种写法,匹配优先级不同,这是 Nginx 配置的第一大坑。

语法 含义 优先级
location = /exact 精确匹配 最高,命中立即停止
location ^~ /prefix/ 前缀匹配且不再查正则 第二
location ~ \.php$ 正则匹配(区分大小写) 第三,按出现顺序取先命中者
location /prefix 普通前缀匹配 最低,取最长前缀兜底

怎么做

一个同时体现优先级的例子:

server {
    listen 80;
    server_name demo.example.com;

    # 优先级 1:精确匹配,只有访问 /health 才进来
    location = /health {
        return 200 "ok";
    }

    # 优先级 2:^~ 前缀,/static/ 下的一切都按静态文件处理
    location ^~ /static/ {
        root /var/www/demo;
    }

    # 优先级 3:正则,图片类后缀的请求直接按静态文件处理
    location ~ \.(jpg|png)$ {
        root /var/www/demo;
    }

    # 优先级 4:普通前缀,其余所有请求的兜底
    location / {
        root /var/www/demo;
        index index.html;
    }
}

判断顺序:先看有没有 = 精确命中;否则记下最长前缀,若它带 ^~ 就直接用它;否则按顺序试正则,正则都不中才回落到最长前缀。

参数说明

指令 作用 常见取值
listen 监听的端口 80(HTTP)、443 ssl(HTTPS)
server_name 匹配 Host 头的域名 可写多个,空格分隔;_ 作兜底
root 站点文件根目录 如 /var/www/demo
index 目录默认文件 如 index.html index.htm
return 直接返回状态码/内容 常用于跳转和健康检查

验证方法

把上面的配置放进 /etc/nginx/conf.d/demo.conf 后:

# 检查语法并重载
$ sudo nginx -t && sudo systemctl reload nginx

# 验证精确匹配与兜底路由
$ curl localhost/health
$ curl -I -H "Host: demo.example.com" localhost/
预期输出:
ok
HTTP/1.1 200 OK
Server: nginx/1.30.x

常见问题

现象 根因 解决
访问域名却打开默认页 没有任何 server_name 匹配,落入默认 server 检查 server_name 拼写,删掉或改掉默认 server 块
正则 location 抢了静态目录 忘了 ^~ 静态目录前缀改用 location ^~ /static/
所有域名都打到同一站点 server_name 写了 _ 或没写 每个站点写明确域名

静态站点与反向代理

是什么

Nginx 两大用途:直接发静态文件(HTML/CSS/JS,root 指到目录即可)和反向代理(站在后端应用前面,替它接客)。生产里后者更常见:你的 Java/Node/Python 应用监听 8080,Nginx 占住 80/443 对外服务,顺便做 TLS、压缩和限流。

反向代理里最容易被忽略的是转发头:请求经过 Nginx 后,后端看到的"来源"变成了 Nginx 本机(127.0.0.1)。如果不在代理时把原始信息带过去,应用拿到的 Host 是 127.0.0.1、客户端 IP 是 127.0.0.1——登录校验、日志统计、IP 限流全部失效。

怎么做

最小静态站点:

server {
    listen 80;
    server_name www.example.com;

    root /var/www/example;
    index index.html;
}

反向代理(重点配置,逐行解释见下):

server {
    listen 80;
    server_name api.example.com;

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

逐行解释:

  • proxy_pass:把请求转给本机 8080 端口的后端应用。
  • proxy_set_header Host $host:把用户实际访问的域名传给后端。不配的话后端收到的 Host 是 127.0.0.1:8080,依赖域名做路由(多租户、子域分发)的应用会直接错乱。
  • proxy_set_header X-Real-IP $remote_addr:传真实客户端 IP。不配的话日志和限流全错——所有请求看起来都来自 127.0.0.1。
  • proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for:传完整代理链(客户端 IP + 之前每一跳代理的 IP 追加成列表),多层代理时的标准做法。
  • proxy_set_header X-Forwarded-Proto $scheme:告诉后端用户走的是 http 还是 https。不配的话,https 用户也会被后端当成 http,生成的重定向链接可能错误。

这四条就是"proxy_set_header 四件套",任何反代配置都应原样带上。

proxy_pass 结尾带不带 /,路径拼接结果完全不同(重点坑):

# 写法 A:结尾无斜杠,完整路径原样转发
location /api/ {
    proxy_pass http://127.0.0.1:8080;
}
# 请求 /api/users → 后端收到 /api/users

# 写法 B:结尾有斜杠,替换掉 location 匹配的部分
location /api/ {
    proxy_pass http://127.0.0.1:8080/;
}
# 请求 /api/users → 后端收到 /users

后端接口本身定义在 /api/users 就用写法 A;后端不认识 /api 前缀就用写法 B。404 排查时先看这里。

WebSocket 需要额外两行(把 HTTP 升级为长连接协议):

location /ws/ {
    proxy_pass http://127.0.0.1:8080;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
}

参数说明

指令 作用 默认值/建议
proxy_pass 后端地址 一般指向 127.0.0.1:端口,不暴露到公网
proxy_read_timeout 等后端响应的超时 默认 60s;长任务接口需调大
proxy_set_header 覆盖/新增发给后端的请求头 四件套必配
client_max_body_size 允许上传的请求体大小 默认 1m,文件上传场景要调大

验证方法

# 本机模拟带 Host 的请求,验证代理链路
$ curl -I -H "Host: api.example.com" localhost/
预期输出:
HTTP/1.1 200 OK
Server: nginx/1.30.x

再在后端应用侧确认它收到了正确的转发头:应用日志里的客户端 IP 应该是你的真实 IP 而不是 127.0.0.1。

# 在 access.log 里看代理请求的来源
$ sudo tail -2 /var/log/nginx/access.log
预期输出:
203.0.113.7 - - [29/Sep/2026:10:00:00 +0800] "GET / HTTP/1.1" 200 612 "-" "curl/7.81.0"

常见问题

现象 根因 解决
后端拿到 Host 是 127.0.0.1 没配 proxy_set_header Host $host 补上四件套
日志/限流里全是 127.0.0.1 没配 X-Real-IP / X-Forwarded-For 补上四件套
加了 / 后接口 404 proxy_pass 结尾斜杠改变了路径拼接 按"写法 A/B"对照修正
WebSocket 连不上就断 没加 Upgrade 头 补 proxy_http_version 1.1 与 Upgrade/Connection 头

日志与日常运维

是什么

Nginx 日志分两种:access.log 记录每个请求(谁、何时、访问什么、返回码),error.log 记录错误与启动信息。排错顺序永远是先看 error.log 再看 access.log。自定义 log_format 可以把响应时间、转发头等字段记下来,配合 日志分析 做统计与告警。

怎么做

# 实时跟踪错误日志
$ sudo tail -f /var/log/nginx/error.log

# 打印当前生效的完整配置(排错神器)
$ sudo nginx -T

自定义日志格式示例(加入响应耗时字段):

# 在 http {} 级别定义,server 里用 access_log 引用
log_format timed '$remote_addr - $status $request_time '
                 '"$request" "$http_user_agent"';

server {
    listen 80;
    server_name api.example.com;
    access_log /var/log/nginx/api.access.log timed;

    location / {
        proxy_pass http://127.0.0.1:8080;
    }
}

参数说明

命令/文件 作用
sudo nginx -T 打印全部生效配置(含 include 展开后的结果),配置"到底加载了什么"一锤定音
sudo nginx -t 只查语法,不展开
`ss -lntp grep nginx`
curl -I localhost 最快烟雾测试,看响应头
/var/log/nginx/access.log 请求流水账
/var/log/nginx/error.log 错误与启动失败原因

验证方法

# 三连验证:配置、端口、响应
$ sudo nginx -t
$ ss -lntp | grep nginx
$ curl -I localhost
预期输出:
nginx: configuration file /etc/nginx/nginx.conf test is successful
LISTEN 0 511 0.0.0.0:80 0.0.0.0:* users:(("nginx",pid=1234,fd=6))
HTTP/1.1 200 OK
Server: nginx/1.30.x

常见问题

现象 根因 解决
403 Forbidden 目录权限不足,或目录下没有 index 指定的文件 检查 root 目录的读/执行权限与索引文件是否存在;RHEL 系另查 SELinux(setsebool -P httpd_can_network_connect on)
404 但文件明明存在 root 与 alias 用混了 见下方重点说明
502 Bad Gateway 后端没启动或端口写错 curl 127.0.0.1:8080 直接探后端,不通就先修后端
504 Gateway Timeout 后端太慢超过 60s 调大 proxy_read_timeout,同时排查后端为何慢
改配置不生效 忘了 -t+reload,或改了不加载的文件 用 nginx -T 确认生效配置里有没有你的修改
80 端口起不来 Apache 等先占 80 `ss -lntp

root 与 alias 的区别(404 的头号根因):root 把 location 路径拼在根目录后面;alias 把 location 匹配到的部分替换掉。

# root:请求 /static/a.css → 实际找 /var/www/static/a.css
location /static/ {
    root /var/www;
}

# alias:请求 /static/a.css → 实际找 /var/www/files/a.css
location /static/ {
    alias /var/www/files/;
}

记忆法:root 是"根 + 全路径",alias 是"换个目录名"。用 alias 时结尾的 / 要与 location 保持一致,否则同样会 404。

常见问题汇总

把本章散落各节的高频问题集中成一张表,排错时先对号入座,再跳回对应小节看细节:

现象 一句话排查方向 详见
502 Bad Gateway 先 curl 127.0.0.1:8080 直接探后端:后端没起、端口写错、或 proxy_pass 结尾斜杠导致路径拼接错 「静态站点与反向代理」
504 Gateway Timeout 后端处理超过 proxy_read_timeout(默认 60s);调大超时的同时排查后端为何慢 「静态站点与反向代理」
80/443 端口被占,nginx 起不来 ss -lntp | grep :80 找占用者(常见是 Apache),停掉或换端口 「安装」「服务管理」
403 permission denied root 目录缺少读/执行权限,或目录下没有 index 指定的文件;RHEL 系另查 SELinux 「日志与日常运维」
nginx -t 通过但 reload 后行为没变 多半改了不被加载的文件(如 Ubuntu 源布局的 sites-available);用 nginx -T 确认生效配置里有没有你的修改 「配置文件结构」「服务管理」
reload 报 bind() to 0.0.0.0:80 failed 端口被占或配置里 listen 冲突 「服务管理」
访问域名却打开默认页 / 别的站点 server_name 拼写错误或缺失,请求落入默认 server 块 「核心概念:server 块」
404 但文件明明存在 root 与 alias 语义用混,见本章末尾的重点说明 「日志与日常运维」
后端拿到的客户端 IP 全是 127.0.0.1 反代没带 proxy_set_header 四件套 「静态站点与反向代理」

📄 Docker 安装与使用 30-docker.md


title: 30 Docker 安装与使用 updated: 2026-09 applies-to: Ubuntu 22.04 LTS

Docker 把应用连同它的运行环境打包成容器,彻底解决"在我机器上是好的"问题。本章走完整链路:选版本 → 官方源安装 → 服务管理与非 root 使用 → daemon.json 与镜像加速 → 核心命令 → 数据卷 → Compose → 磁盘清理 → 避坑。

[!IMPORTANT] 本章版本号截至 2026 年 9 月。版本信息的唯一权威来源是 版本速查表,冲突时以速查表为准。

版本与发行版

是什么

Docker 分免费社区版 CE 和付费企业版 EE(一句话:个人与绝大多数公司用 CE 就够了,EE 多的是官方商业支持与安全加固镜像)。本章只讲 CE。此外要建立一个新习惯:Compose 已是 Docker 的子命令插件,版本基线为 Compose v5.x,命令是 docker compose(带空格);老的 docker-compose(带横杠)独立二进制已弃用,网上老教程请自动换算。

项目 值 说明
推荐版本 Docker Engine 29.x(截至 2026-09) 官方 apt 源安装即为此线
Compose v5.x 插件,命令 docker compose 老命令 docker-compose 已弃用
Ubuntu 22.04 系统源 docker.io 20.x 太旧,不建议
License CE 为 Apache 2.0 免费商用

怎么做

先确认系统源里的旧版本情况,做到心里有数:

# 查看 apt 会装到的 docker 版本
$ apt policy docker.io
预期输出:
docker.io:
  Installed: (none)
  Candidate: 20.10.x-0ubuntu1~22.04.2

参数说明

场景 选什么 为什么
个人服务器、学习 Docker CE(官方源) 免费、最新、教程都基于它
需要官方 SLA 的大企业 Docker EE 花钱买支持,功能核心与 CE 相同
想用 Compose 编排 装官方源里的 docker-compose-plugin 获得 docker compose 子命令
使用系统源 docker.io 不推荐 20.x 缺新特性与新修复

验证方法

装好后用两条命令确认版本基线:

# 查看 Engine 与 Compose 版本
$ docker --version
$ docker compose version
预期输出:
Docker version 29.x.x, build xxxxxxx
Docker Compose version v5.x.x

常见问题

现象 根因 解决
docker compose 提示无此命令 没装 compose 插件 安装 docker-compose-plugin 包
教程里 docker-compose 能跑我的不能 老教程用的是弃用的独立二进制 换成 docker compose(空格)
装完版本是 20.x 误用了系统源 docker.io 卸载后按官方源流程重装

安装:官方 apt 源

是什么

和 Nginx 一样,拿新版本要加 Docker 官方 apt 源。流程同样是三步:卸旧 → 加 GPG 密钥 → 写源文件。Docker 官方把密钥放在 /etc/apt/keyrings/ 并用 signed-by= 声明,这是当前 Linux 发行版推荐的标准做法。

怎么做

完整流程如下,逐块执行:

# 卸载旧版(系统源里的 docker.io 版本太旧,混装会冲突)
$ sudo apt remove docker docker-engine docker.io containerd runc

# 安装依赖
$ sudo apt update
$ sudo apt install ca-certificates curl gnupg

# 添加官方 GPG 密钥
$ sudo install -m 0755 -d /etc/apt/keyrings
$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
$ sudo chmod a+r /etc/apt/keyrings/docker.gpg

# 添加官方源(自动适配架构与系统代号)
$ echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo $VERSION_CODENAME) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

# 更新索引并安装 Engine 与插件
$ sudo apt update
$ sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

参数说明

包名 作用
docker-ce 守护进程与核心,容器真正跑在这里
docker-ce-cli docker 命令行客户端
containerd.io 底层容器运行时,Docker 依赖它
docker-buildx-plugin 新一代镜像构建器
docker-compose-plugin 提供 docker compose 子命令

验证方法

# 启动服务并设置开机自启
$ sudo systemctl enable --now docker

# 跑一个最小容器做烟雾测试
$ sudo docker run --rm hello-world
预期输出:
Hello from Docker!
This message shows that your installation appears to be working correctly.

看到 Hello from Docker! 说明拉镜像、创建容器、运行、输出整条链路都通了。

常见问题

现象 根因 解决
NO_PUBKEY 报错 GPG 密钥没生成成功 重跑 curl ... gpg --dearmor 那步
Unable to locate package docker-ce 源没生效 确认 /etc/apt/sources.list.d/docker.list 内容与 apt update 无报错
Cannot connect to the Docker daemon 守护进程没起 sudo systemctl status docker 看状态与报错

非 root 使用

是什么

Docker 守护进程默认只允许 root(或 sudo)访问,所以刚装完每条 docker 命令都要加 sudo。官方做法是把用户加入 docker 组,该组对守护进程的 socket 有读写权,之后即可免 sudo。

[!WARNING] docker 组的权限等同 root。 组内用户可以把宿主机任意文件挂载进容器随意读写,等于完全控制这台机器。只给真正需要跑容器的用户加组,不要为了让别人"试用一下"就随手加。

怎么做

# 把当前用户加入 docker 组
$ sudo usermod -aG docker $USER

# 组变更需重新登录才生效,登出再登录,或临时用 newgrp 验证
$ newgrp docker

参数说明

命令片段 含义
usermod -aG -a 追加(不覆盖已有组),-G 指定组列表
docker 目标组名,安装 docker-ce 时自动创建
newgrp docker 当前 shell 立即套用新组,仅供临时验证

验证方法

重新登录后,不加 sudo 执行:

# 免 sudo 运行烟雾测试
$ docker run --rm hello-world
预期输出:
Hello from Docker!
This message shows that your installation appears to be working correctly.

常见问题

现象 根因 解决
加组后仍报 permission denied 没重新登录,组变更未生效 登出重登,或 newgrp docker
usermod: group 'docker' does not exist docker 服务从未启动过,组未创建 先 sudo systemctl start docker

daemon.json 与镜像加速

是什么

/etc/docker/daemon.json 是 Docker 守护进程的总配置文件,管两件本章关心的事:从哪拉镜像(registry-mirrors 加速器)和容器日志怎么存(log-driver 与轮转)。国内服务器不配加速器,docker pull 经常超时甚至失败,所以这步几乎是必做。

{
  "registry-mirrors": ["https://<你的加速器地址>"],
  "log-driver": "json-file",
  "log-opts": { "max-size": "10m", "max-file": "3" }
}

[!IMPORTANT] 改完 daemon.json 必须 sudo systemctl restart docker。 只执行 systemctl daemon-reload 是没用的——那只让 systemd 重读自己的单元文件,Docker 守护进程并不会重新加载 daemon.json。restart 后,未配置 restart 策略(默认为 no)的容器会被停止且不会自动拉起;希望 daemon 重启后容器自动恢复的,创建容器时加 --restart=always(Compose 里写 restart: always)。请挑合适时间操作。这是新手最常见的坑。

另注:2024 年之后不少公共镜像加速器已下线或改为仅登录可用,pull 失败先检查这里配的加速器还活着没有,可用加速器清单见 镜像与软件源加速。

[!NOTE] 环境变量方面,Docker CLI 主要认一个 DOCKER_HOST:它用于指向远程 Docker daemon(如 export DOCKER_HOST=ssh://user@10.0.0.5),单机部署可忽略,保持默认的本地 socket 即可。

怎么做

# 创建配置文件并写入上面的 JSON 内容
$ sudo nano /etc/docker/daemon.json

# 重启守护进程使配置生效
$ sudo systemctl restart docker

# 确认加速器已被读取
$ docker info
预期输出(docker info 中应出现):
 Registry Mirrors:
  https://<你的加速器地址>/

参数说明

键 作用 建议
registry-mirrors 镜像拉取加速地址列表 按顺序尝试,放可用的地址
log-driver 容器日志驱动 json-file 兼容 docker logs
log-opts.max-size 单个日志文件上限 10m 起步,量大可调
log-opts.max-file 日志轮转保留份数 3 即单容器日志最多约 30m

[!WARNING] 不配 log-opts,容器日志会无限增长撑爆磁盘。 某些应用每秒输出多行日志,几个月就能写满 /var/lib/docker 所在分区,最终整机卡死。上面的 max-size + max-file 就是给日志套上"滚动覆盖"的缰绳。磁盘已被撑爆的处理见 磁盘空间排查。注意 log-opts 只对新创建的容器生效,存量容器需重建。

验证方法

# 确认守护进程读到配置,且日志驱动对新建容器生效
$ docker info | grep -A1 "Registry Mirrors"
# 注意不能用 --rm:容器一退出就被删除,inspect 会报 No such object
$ docker run --name logtest alpine echo hi
$ docker inspect logtest --format '{{.HostConfig.LogConfig.Config}}'
$ docker rm logtest
预期输出:
 Registry Mirrors:
  https://<你的加速器地址>/
 hi
 map[max-file:3 max-size:10m]

常见问题

现象 根因 解决
改了 daemon.json 没生效 只 daemon-reload 没 restart sudo systemctl restart docker
json: cannot unmarshal 启动失败 JSON 语法错误(多余逗号等) 用 `cat /etc/docker/daemon.json
pull 仍然超时 加速器已下线 换 镜像与软件源加速 里列的可用地址

核心命令:镜像与容器

是什么

Docker 日常命令就两组:镜像命令(管理"模板",只读)和容器命令(管理"运行实例",由镜像启动)。容器是镜像的一次运行,同一镜像可起任意多个容器。

镜像命令速览:

# 从仓库拉取镜像(不写 tag 默认 latest)
$ docker pull nginx

# 列出本地已有镜像
$ docker images

# 删除镜像(被容器占用的删不掉)
$ docker rmi nginx:latest

# 给镜像打别名(推送到自己仓库前常用)
$ docker tag nginx:latest myrepo/nginx:1.0

容器命令速览:

# 后台启动一个 nginx 容器:宿主 8080 映射容器 80,站点目录挂载进去
$ docker run -d --name web --restart=always -p 8080:80 -v /srv/web:/usr/share/nginx/html nginx

# 列出容器(-a 含已停止的)
$ docker ps -a

# 停止 / 启动 / 重启
$ docker stop web
$ docker start web
$ docker restart web

# 删除已停止的容器
$ docker rm web

# 进入容器内部排查
$ docker exec -it web bash

# 跟踪日志,只看最近 100 行
$ docker logs -f --tail 100 web

# 查看容器完整信息(IP、挂载、重启策略等)
$ docker inspect web

# 实时查看各容器 CPU/内存占用
$ docker stats

docker run 的关键参数,值得逐个记住:

参数 作用 说明
-d 后台运行 不加则占住当前终端,Ctrl+C 即停
-p 8080:80 端口映射 宿主:容器 外部访问 8080 即达容器 80
-v /srv/web:/usr/share/nginx/html 挂载 宿主目录:容器目录 数据落在宿主机,容器删除不丢
--name web 指定容器名 免去记随机名;同名会冲突
--restart=always 宕机/开机自动拉起 守护进程重启时也自动恢复
-e KEY=value 注入环境变量 应用配置的标准入口
--rm 退出后自动删除容器 适合一次性任务
-it 分配交互式终端 配合进入容器执行命令

[!NOTE] --restart=always 与 systemd 的关系:容器内服务不需要再写 systemd 单元——Docker 自己负责拉起,开机自启交给 systemctl enable docker 即可。进程托管的三种层次对比见 进程守护。

参数说明(补充)

命令 关键参数 用途
docker logs -f 跟踪、--tail N 最近 N 行、-t 带时间戳 看容器输出
docker exec -it 交互、容器后接命令 进容器排查;alpine 镜像用 sh 不是 bash
docker inspect --format 取指定字段 脚本化取 IP、挂载等
docker ps -a 含已停止、-q 只列 ID 配合批量清理

验证方法

# 确认 web 容器在跑、端口映射正确、可访问
$ docker ps
$ curl -I localhost:8080
预期输出:
CONTAINER ID   IMAGE   ...   NAMES
9f3c1e...      nginx   ...   web
HTTP/1.1 200 OK
Server: nginx/1.30.x

常见问题

现象 根因 解决
port is already allocated 宿主端口被占用或被另一容器占用 `ss -lntp
exec: bash: not found 容器是无 shell 的精简镜像 换 docker exec -it web sh
docker logs 没输出 应用日志写进了文件而非 stdout 进容器看应用自己的日志文件
容器起来立刻退出 主进程结束或配置错误 docker logs 容器名 看退出原因

数据卷持久化

是什么

容器的文件系统是临时的:容器一删,里面所有写入全部蒸发。要保住数据(数据库文件、上传的图片、配置),必须在创建容器时用 -v 挂载出来。两种方式:

方式 写法 特点
绑定挂载 -v /srv/web:/usr/share/nginx/html 宿主机具体路径,直观、好备份,路径必须已存在
命名卷 -v webdata:/var/lib/mysql Docker 统一管理(默认在 /var/lib/docker/volumes/),适合数据库类数据

[!WARNING] 删容器数据不会丢的前提是你用了 -v;不用 -v 就一定会丢。 容器是可抛弃的(升级 = 删旧起新),没挂载出来的数据随容器删除一起消失。给数据库容器配 -v 不是可选项,是保命项。

怎么做

# 绑定挂载:站点目录放宿主机
$ docker run -d --name web -v /srv/web:/usr/share/nginx/html nginx

# 命名卷:数据库数据交给 Docker 管理(mysql 镜像必须给 root 密码,否则容器启动即退出)
$ docker volume create dbdata
$ docker run -d --name db -e MYSQL_ROOT_PASSWORD=<强密码> -v dbdata:/var/lib/mysql mysql

# 卷管理三件套
$ docker volume ls
$ docker volume inspect dbdata
$ docker volume prune

参数说明

写法片段 含义
-v 宿主路径:容器路径 绑定挂载,以 / 开头的是宿主绝对路径
-v 卷名:容器路径 命名卷,不含 / 的名字由 Docker 创建管理
volume create 显式创建命名卷(run 时写不存在卷名会自动建)
volume inspect 查看卷在宿主机的真实路径
volume prune 清理未被引用的数据卷(执行前先 volume ls 核对,可能删掉重要数据)

验证方法

# 验证数据真的落在宿主机:写入 → 删容器 → 重建 → 数据还在
$ echo hello | sudo tee /srv/web/test.txt
$ docker rm -f web
$ docker run -d --name web -p 8080:80 -v /srv/web:/usr/share/nginx/html nginx
$ curl localhost:8080/test.txt
预期输出:
hello

常见问题

现象 根因 解决
升级容器后数据全没了 当初没配 -v 无解,重导数据;今后务必挂载
绑定挂载目录是空的 宿主路径不存在,Docker 自动建了空目录并覆盖 先建好目录再放入数据
权限报错 Permission denied 容器内进程 uid 与宿主目录属主不匹配 chown 对应 uid 或用命名卷
prune 后数据没了 被清的卷已无容器引用 依赖备份;prune 前先 volume ls 核对

Docker Compose v5

是什么

Compose 把"一组容器 + 它们的参数"写进一个 YAML 文件,一条命令起停全家桶。多容器项目(应用 + 数据库 + 缓存)用 docker run 逐个敲又长又容易错,Compose 文件进版本库,团队与服务器之间一致可复现。当前基线为 v5.x 插件,命令 docker compose(带空格)。

怎么做

项目目录下创建 compose.yaml(v5 的标准文件名):

services:
  app:
    image: myapp:1.0
    ports:
      - "8080:8080"
    environment:
      - DATABASE_URL=mysql://root:secret@db:3306/app
      - REDIS_HOST=cache
    volumes:
      - ./uploads:/app/uploads
    depends_on:
      - db
      - cache
    restart: always

  db:
    image: mysql:8.0
    environment:
      - MYSQL_ROOT_PASSWORD=secret
    volumes:
      - dbdata:/var/lib/mysql
    restart: always

  cache:
    image: redis:8
    restart: always

volumes:
  dbdata:

要点:services 下每个键就是容器名;db、cache 这类服务名同时是内部 DNS 名,app 容器里直接用 db:3306 访问数据库;顶层 volumes 声明命名卷;depends_on 只管启动顺序,不管"就绪"。

# 启动全部服务(-d 后台)
$ docker compose up -d

# 查看本项目容器状态
$ docker compose ps

# 跟踪全部服务日志
$ docker compose logs -f

# 停止并删除容器与网络
$ docker compose down

# 连同数据卷一起删(危险!)
$ docker compose down -v

参数说明

字段/命令 作用
image 使用哪个镜像;换成 build: . 则从当前目录 Dockerfile 构建
ports 端口映射,格式同 -p
environment 环境变量,格式同 -e
volumes 挂载,格式同 -v
depends_on 启动依赖顺序
restart: always 等价 run 的 --restart=always
compose up -d 创建并启动;文件有改动时自动重建受影响容器
compose down 停止删除容器与网络,不动数据卷
compose down -v 额外删除数据卷

[!WARNING] docker compose down -v 会删掉数据卷,数据库数据随之蒸发。 日常停服只用 down;-v 只在明确要清空数据重来时使用,执行前确认已有备份。

验证方法

# 启动并确认三个服务都 Up
$ docker compose up -d
$ docker compose ps
预期输出:
NAME            IMAGE        STATUS          PORTS
myapp-app-1     myapp:1.0    Up 10 seconds   0.0.0.0:8080->8080/tcp
myapp-db-1      mysql:8.0    Up 12 seconds   3306/tcp
myapp-cache-1   redis:8      Up 12 seconds   6379/tcp

常见问题

现象 根因 解决
app 连不上 db 用了 localhost:3306 容器里 localhost 是自己,改用服务名 db:3306
no configuration file provided 文件名不对或不在项目目录 文件名为 compose.yaml 且在项目根目录执行
up 后 db 一直重启 忘了配 MYSQL_ROOT_PASSWORD 等必填变量 看 docker compose logs db 补环境变量

磁盘清理

是什么

Docker 的镜像、容器、卷、构建缓存全部堆在 /var/lib/docker,只增不减:每次 pull 的新版本、构建的中间层、停掉的容器都会留存量。用 docker system df 看占用,用 prune 系列回收——这是服务器磁盘爆满的头号来源之一。

怎么做

# 查看 Docker 各类资源的磁盘占用
$ docker system df

# 只清理悬空镜像(无标签的中间层,安全)
$ docker image prune

# 清理所有未被容器使用的镜像 + 停止容器 + 无用网络(危险)
$ docker system prune -a

[!WARNING] docker system prune -a 会删除所有未被任何容器使用的镜像。 下次启动这些服务时必须重新 pull 全部镜像——国内网络下可能很慢甚至失败。常规清理先用 docker image prune,-a 只在确认磁盘告急时使用。

参数说明

命令 清理范围 风险
docker system df 只读统计(加 --verbose 看明细) 无
docker image prune 悬空镜像(<none> 标签) 基本无风险
docker container prune 所有已停止容器 已停容器会被删
docker volume prune 未被引用的数据卷 可能删掉重要数据
docker system prune -a 上述大部分 + 所有无容器引用的镜像 高,见 WARNING

验证方法

# 查看各类资源占用与可回收量
$ docker system df
预期输出(RECLAIMABLE 明显下降):
TYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE
Images          8         2         2.1GB     1.6GB (76%)
Containers      3         2         120MB     60MB (50%)

常见问题

现象 根因 解决
磁盘满但 df 找不到大文件 占用藏在 /var/lib/docker sudo du -sh /var/lib/docker/* 定位后 prune
prune 后空间没释放 删除的文件仍被运行中进程持有 重启对应容器或 systemctl restart docker
构建缓存巨大 buildx 缓存不在普通 prune 范围 docker builder prune

重要安全坑:Docker 与防火墙

是什么

Docker 会绕过 ufw。 你用 ufw deny 8080 挡了端口,但只要容器以 -p 8080:80 发布过,公网照样能访问——因为 Docker 直接往 iptables 写 NAT 规则,在 ufw 的规则链之前生效,ufw 对 Docker 发布的端口形同虚设。这是"我明明关了防火墙端口为什么还能访问"的标准答案。详见 防火墙。

怎么做

对策很简单:不需要暴露到公网的端口,发布时只绑本机回环地址。

# 只允许本机访问(配合 Nginx 反代最常用)
$ docker run -d --name api -p 127.0.0.1:8080:8080 myapp

# 对比:这样写会监听所有网卡,公网可直达
$ docker run -d --name api -p 8080:8080 myapp

参数说明

写法 监听范围 公网可达
-p 8080:80 0.0.0.0 所有网卡 是,ufw 拦不住
-p 127.0.0.1:8080:80 仅本机回环 否,只有本机和反代可达
不写 -p,容器间走内部网络 不映射宿主端口 否

验证方法

# 确认端口只绑在回环地址上
$ ss -lntp | grep 8080
预期输出:
LISTEN 0 4096 127.0.0.1:8080  0.0.0.0:* users:(("docker-proxy",pid=2345,fd=4))

关键看地址列是 127.0.0.1:8080 而不是 0.0.0.0:8080;从另一台机器 curl 服务器IP:8080 应连接失败。

常见问题

现象 根因 解决
ufw deny 了端口仍可访问 Docker 直写 iptables 绕过 ufw -p 改绑 127.0.0.1,外层用反代
想彻底关掉 Docker 的 iptables 干预 不推荐改 "iptables": false 会破坏容器联网,用绑定回环替代
反代转发到容器 502 后端容器绑了回环但 Nginx 访问的是别的地址 反代指向 127.0.0.1:端口,与 -p 绑定一致

常见问题速查

现象 根因 解决
permission denied ... docker.sock 用户不在 docker 组,或加组后没重新登录 sudo usermod -aG docker $USER 后重新登录
docker pull 超时/失败 无加速器或加速器已下线 检查 daemon.json 的 registry-mirrors,换可用地址(见 33 章)
容器端口映射冲突 宿主端口已被占 ss -lntp 查占用,换宿主端口
-p 发布的端口 ufw 拦不住 Docker 直写 iptables,绕过 ufw 只绑回环 -p 127.0.0.1:8080:80,反代对外(见 16 章)
/var/lib/docker 撑爆磁盘 镜像/日志/构建缓存堆积 docker system df 定位,image prune、builder prune 回收(见 55 章)
容器时间与宿主差 8 小时 容器默认 UTC 时区 run 时加 -e TZ=Asia/Shanghai
daemon.json 改完没生效 只 daemon-reload 没重启 Docker sudo systemctl restart docker(注意容器会重启)
容器日志无限膨胀 没配 log-opts 轮转 daemon.json 配 max-size/max-file,重建存量容器

📄 31 Git、Maven/Gradle 与构建工具链 31-git-and-build-tools.md


title: 31 Git、Maven/Gradle 与构建工具链 updated: 2026-09 applies-to: Ubuntu 22.04 LTS

服务器上除了软件本身,还有两类「基础设施」必备:Git(把代码弄到服务器上)和构建工具链(把源码变成可运行的产物)。本章先讲 Git 与 SSH 密钥,再讲 build-essential、Maven、Gradle。

[!NOTE] 本章所有版本号截至 2026-09,版本信息以 版本速查表 为准。


一、Git 安装与基础配置

是什么

Git 是版本控制系统。在服务器上它的角色很简单:把代码从仓库取下来(部署时)、看一下改动历史(排错时)。 版本选择上它有个特点:对版本几乎不敏感,新老版本在 clone/pull 上完全兼容,所以系统源里的 2.34+ 直接用,不需要加第三方源。

怎么做

# 安装 git
$ sudo apt install -y git

# 设置提交者身份(换成你自己的)
$ git config --global user.name "Your Name"
$ git config --global user.email "you@example.com"

# 新建仓库时默认分支叫 main
$ git config --global init.defaultBranch main

# Windows 用户必备:提交时把 CRLF 转成 LF
$ git config --global core.autocrlf input

[!IMPORTANT] core.autocrlf input 是 Windows 用户必配项。 Windows 换行符是 CRLF(\r\n),Linux 是 LF(\n)。 带 CRLF 的 shell 脚本传到 Linux 执行会报 /bin/bash^M: bad interpreter —— ^M 就是回车符,排查极费时间。设成 input 后 Git 提交时统一转成 LF,从源头杜绝污染。

参数说明

版本基准:

项目 值 说明
推荐版本 Git 2.34+ Ubuntu 22.04 系统源默认即可满足,不需要额外加源
Ubuntu 22.04 源默认 2.34.x 直接 apt 装
版本核对 版本速查表 全篇唯一权威来源

常用配置项:

配置项 作用 建议值
user.name / user.email 提交记录的作者信息 与代码平台账号一致
init.defaultBranch git init 时的默认分支名 main(现代约定)
core.autocrlf 换行符自动转换 Windows 设 input;纯 Linux 可不设
--global 作用域:当前用户(~/.gitconfig) 改为 --system 则对全机生效

验证方法

$ git --version

预期输出:

git version 2.34.1
$ git config --global --list

预期输出:

user.name=Your Name
user.email=you@example.com
init.defaultBranch=main
core.autocrlf=input

常见问题

现象 根因 解决
git: command not found 没装 sudo apt install -y git
提交后作者显示 root <root@localhost> 没配 user.name/email 按上面的命令配置
脚本报 /bin/bash^M: bad interpreter 文件是 CRLF 换行 设 core.autocrlf input;已污染的用 sed -i 's/\r$//' 文件 修
配置不生效 仓库自己的 .git/config 覆盖全局 git config --list --show-origin 看生效来源

二、SSH 密钥配置(重点)

是什么

HTTPS 拉私有仓库每次都要输密码,自动化脚本里根本没法输。SSH 密钥的做法是:生成一对密钥,私钥留在自己机器、公钥放到代码平台,之后平台靠密钥认机器。

怎么做

# 生成 ed25519 密钥对(比 RSA 短且安全,当前推荐)
$ ssh-keygen -t ed25519 -C "you@example.com"

# 查看公钥,整行复制
$ cat ~/.ssh/id_ed25519.pub

# 测试连通性(先把公钥贴到平台的 SSH Keys 页面)
$ ssh -T git@github.com

[!NOTE] ssh-keygen 会问保存路径(回车用默认 ~/.ssh/id_ed25519)和 passphrase 口令(可留空)。私钥 绝不能外传,往平台贴的是 .pub 公钥。

[!IMPORTANT] 密钥文件权限不对时 SSH 会直接拒绝使用:~/.ssh 必须 700,私钥必须 600。见 SSH 加固。

参数说明

命令 / 参数 含义
-t ed25519 密钥类型,ed25519 是当前推荐的椭圆曲线算法
-C "邮箱" 备注,仅用于标识钥匙归属,不影响认证
-f <路径> 指定保存路径;多平台各用一把钥匙时用它区分
ssh -T / ssh-add <私钥> 只做认证测试不分配终端 / 把私钥交给 ssh-agent

验证方法

$ ssh -T git@github.com

预期输出(GitLab 提示 Welcome to GitLab, @username!):

Hi username! You've successfully authenticated, but GitHub does not provide shell access.

常见问题

现象 根因 解决
Permission denied (publickey) 公钥没加到平台,或私钥没被加载 ssh -vT git@github.com 看它试了哪些 key;ssh-add -l 确认
UNPROTECTED PRIVATE KEY FILE 私钥权限太开放 chmod 600 ~/.ssh/id_ed25519、chmod 700 ~/.ssh
有多把钥匙但用了错的 默认只试几个固定名字 在 ~/.ssh/config 为不同 Host 指定 IdentityFile

三、Git 常用命令速览

是什么

服务器上 90% 的场景只用得到下面这些,分三层:看状态(status/diff/log)、记录改动(add/commit)、与远端同步(pull/push)。

怎么做

# 取代码 / 同步 / 看状态与历史
$ git clone git@github.com:user/repo.git
$ git pull
$ git status
$ git diff
$ git log --oneline --graph

# 记录改动 / 分支 / 暂存 / 标签
$ git add .
$ git commit -m "说明这次改了什么"
$ git switch -c feature-x
$ git switch main
$ git stash
$ git tag v1.0.0

参数说明

命令 作用 备注
clone / pull 下载远端仓库 / 拉取并合并 clone 加 --depth 1 只取最近一次提交
status / diff 看哪些文件改了 / 看改动内容 不确定就先跑 status
add . / commit -m "信息" 放进暂存区 / 记录成一次提交 信息写「为什么改」
log --oneline --graph 一行一条看历史与分支图 加 -10 只看最近 10 条
branch / switch -c 列分支 / 新建并切换(老写法 checkout -b) switch main 切回主分支
stash / stash pop / tag 临时收起改动 / 取回 / 打版本标签 发布时用 tag 标记提交

[!TIP] 推荐用 switch / restore 而不是老写法 checkout —— checkout 一个命令承担太多职责,容易误操作。

验证方法

$ git log --oneline --graph -3

预期输出:

* 3f2a1c9 (HEAD -> main, origin/main) fix: 修正登录超时
* 8b7e2d1 feat: 新增导出功能
* 1a0c4b3 docs: 补充部署说明

常见问题

现象 根因 解决
git pull 报 CONFLICT 冲突 本地与远端改了同一处 git status 找冲突文件,手工去掉 <<<<<<< 标记后 git add 再 git commit
git push 被拒(non-fast-forward) 远端有新提交你没拉 先 git pull 合并再 push
想撤销未提交的改动 / detached HEAD 误改文件 / 直接切到了某提交 git restore <文件>(改动不可恢复);git switch main 回分支

四、服务器上的典型用法

是什么

服务器上用 Git 的目标很单一:把某个版本的代码放到指定目录,然后构建、启动。不要在服务器上改代码 —— 改了下次 pull 就冲突。

怎么做

# 首次部署:浅克隆,只取最近一次提交(省时省带宽)
$ git clone --depth 1 git@github.com:user/repo.git /opt/app

# 后续更新
$ cd /opt/app
$ git pull

[!TIP] deploy key 是服务器拉私有仓库的正确姿势:在仓库 Settings 里加一把只读专用密钥,只对该仓库有效,出事不波及你的其他仓库,还能单独吊销。别用个人密钥配服务器。

参数说明

参数 / 做法 含义
--depth 1 浅克隆:只要最近一次提交,不要完整历史
--branch <名> 指定要克隆的分支
deploy key 仓库级别的专用只读密钥;部署目录建议 /opt/<应用名>

验证方法

$ git log -1 --oneline

预期输出:

3f2a1c9 (HEAD -> main, origin/main) fix: 修正登录超时

常见问题

现象 根因 解决
大仓库 clone 极慢 默认拉完整历史(含大文件) 加 --depth 1
部署机 git pull 冲突 有人在服务器上直接改过代码 备份后 git checkout -- . 丢弃改动再 pull
想回滚到上一个版本 新版本有问题 git log 找版本号 → git switch --detach <版本号> → 重新构建

五、build-essential 与编译依赖

是什么

build-essential 是 Ubuntu 的元包(本身没内容,只依赖一堆包),装上它等于装齐编译 C/C++ 的基本工具:gcc、g++、make、libc 开发头文件。源码编译、Python 带 C 扩展的包、Node 的本地编译模块都依赖它。

怎么做

# 基本编译工具链
$ sudo apt install -y build-essential

# 其他常见编译依赖(按需装,别一次全装)
$ sudo apt install -y libssl-dev zlib1g-dev libffi-dev pkg-config cmake

参数说明

包 什么时候需要
build-essential 任何源码编译前必装(gcc / g++ / make / libc6-dev)
libssl-dev 用到 OpenSSL 的程序(HTTPS、加密)
zlib1g-dev 用到压缩(Nginx、Python 的 zlib 模块)
libffi-dev Python 的 ctypes / cffi 类库
pkg-config / cmake 自动查找库位置与编译参数 / 项目用 CMake 构建时

[!NOTE] 只有带 -dev 后缀的包才含头文件(.h):运行时装不带 -dev 的库包就够,编译时才需要 -dev。

验证方法

$ gcc --version | head -1
$ make --version | head -1

预期输出:

gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0
GNU Make 4.3

常见问题

现象 根因 解决
make: command not found 没装 build-essential sudo apt install -y build-essential
fatal error: xxx.h: No such file or directory 缺对应的 -dev 开发包 按报错里的库名装 -dev 包,如 libssl-dev
make: *** missing separator. Stop. Makefile 里该用 Tab 的行用了空格 把缩进换成真正的 Tab 字符
C compiler cannot create executables 编译器异常或磁盘满 确认 gcc --version;df -h 看磁盘

六、Maven(Java 项目构建)

是什么

Maven 是 Java 项目的构建与依赖管理工具,做三件事:按 pom.xml 下载依赖、编译打包成 jar、跑测试。产物在 target/,依赖缓存在 ~/.m2/repository。

[!IMPORTANT] Maven 依赖 JDK:必须先装好 JDK 并配好 JAVA_HOME,再装 Maven。见 JDK。

怎么做

# 方式一:apt(最简单,但 22.04 源里装到的是 3.6.3,要 3.9.x 请走方式二)
$ sudo apt install -y maven

# 方式二:官网二进制包解压到 /opt(推荐,可控版本;版本号以官网 3.9.x 为准)
$ wget https://dlcdn.apache.org/maven/maven-3/3.9.x/binaries/apache-maven-3.9.x-bin.tar.gz
$ sudo tar -xzf apache-maven-3.9.x-bin.tar.gz -C /opt
$ sudo ln -s /opt/apache-maven-3.9.x /opt/maven

配置环境变量(为什么放 /etc/profile.d/ 见下一节):

# 一个软件一个文件,互不干扰
# Maven 3.x 只需把 bin 加进 PATH,不再需要 M2_HOME / MAVEN_HOME
$ sudo tee /etc/profile.d/maven.sh > /dev/null <<'EOF'
export PATH=$PATH:/opt/maven/bin
EOF
$ source /etc/profile.d/maven.sh

[!NOTE] 老教程常见的 M2_HOME / MAVEN_HOME 在 Maven 3.x 已不再需要,仅当确有老脚本引用这两个变量时才补上(export M2_HOME=/opt/maven),纯兼容用途。

配置国内镜像(~/.m2/settings.xml,用户级):

<settings>
  <mirrors>
    <mirror>
      <id>mirror-id</id>
      <url>https://maven.aliyun.com/repository/public</url>
      <mirrorOf>central</mirrorOf>
    </mirror>
  </mirrors>
</settings>

[!NOTE] 镜像地址以镜像站官方帮助页为准(常见域名:mirrors.aliyun.com、mirrors.tuna.tsinghua.edu.cn、mirrors.cloud.tencent.com)。更多见 国内镜像加速。

参数说明

命令 / 配置 含义
mvn -v 查看 Maven / JDK / 系统信息
mvn clean package 清理后编译、测试、打包
-DskipTests 跳过测试(部署提速用,别在 CI 里常态化)
-U 强制更新 SNAPSHOT 快照依赖
~/.m2/settings.xml 用户级配置:镜像、仓库路径、认证
M2_HOME / MAVEN_HOME Maven 3.x 不再需要(仅兼容老脚本时保留);依赖缓存在 ~/.m2/repository

验证方法

$ mvn -v

预期输出:

Apache Maven 3.9.9 (…)
Maven home: /opt/maven
Java version: 21.0.8, vendor: Eclipse Adoptium, runtime: /opt/jdk-21
OS name: "linux", arch: "amd64", family: "unix"

重点看两行:Maven home 指向你解压的目录,且 Java version 是正常版本号(而不是报错)。

常见问题

现象 根因 解决
Error: JAVA_HOME is not defined correctly 没装 JDK 或 JAVA_HOME 没配 先按 JDK 装好 JDK 并配 JAVA_HOME
mvn: command not found PATH 没生效 source /etc/profile.d/maven.sh;新终端自动生效
下载依赖超时 / 极慢 默认连国外的 Maven Central 配 ~/.m2/settings.xml 镜像
依赖下了一半构建失败 / 测试失败 缓存残缺 / 单测没过 删 ~/.m2/repository 对应目录后重跑;测试问题确认后再 -DskipTests

七、Gradle 与环境变量写法

是什么

Gradle 是另一个 Java 构建工具(Android 与很多新项目的默认选择),推荐版本 8.x(截至 2026-09)。安装方式与 Maven 同构:二进制包 → 解压 /opt → 软链 → 写 profile.d 脚本。

怎么做

# 前置:下载用 wget、解压 zip 用 unzip
$ sudo apt install -y wget unzip

# 下载、解压到 /opt 并软链(版本号以 Gradle 官网给出的 8.x 为准)
$ wget https://services.gradle.org/distributions/gradle-8.x-bin.zip
$ sudo unzip -q gradle-8.x-bin.zip -d /opt
$ sudo ln -s /opt/gradle-8.x /opt/gradle

# 写 profile.d 脚本
$ sudo tee /etc/profile.d/gradle.sh > /dev/null <<'EOF'
export GRADLE_HOME=/opt/gradle
export PATH=$GRADLE_HOME/bin:$PATH
EOF
$ source /etc/profile.d/gradle.sh

[!IMPORTANT] 为什么用 /etc/profile.d/*.sh 而不是直接改 /etc/profile? /etc/profile 是系统文件,升级可能被覆盖; profile.d 是「一个软件一个文件」,互不干扰,卸载时删一个文件即可,排查时一眼看出谁配了什么。 注意:profile.d 只在登录 shell 自动加载,systemd 与 crontab 里不生效 —— 那些场景写 Environment= 或用绝对路径。

参数说明

命令 / 配置 含义
gradle -v / gradle build 查看版本 / 构建(-x test 跳过测试)
GRADLE_USER_HOME Gradle 缓存目录,默认 ~/.gradle
source /etc/profile.d/<名字>.sh 让脚本在当前终端立即生效
新开一个登录 shell 另一种让 profile.d 生效的方式

验证方法

$ gradle -v

预期输出:

Gradle 8.14.2

JVM:          21.0.8 (Eclipse Adoptium 21.0.8+9)
OS:           Linux 5.15.0-… amd64

常见问题

现象 根因 解决
改了 profile.d 但当前终端没生效 只在登录时加载 source /etc/profile.d/gradle.sh,或重开终端
systemd 服务里 mvn / gradle 找不到 profile.d 对非登录 shell 不生效 unit 里写 Environment=PATH=... 或用绝对路径
Gradle 也报 JDK 相关错误 没装 JDK 同 Maven,先按 JDK 装好
Maven 与 Gradle 会冲突吗 不会 各自一个 profile.d 脚本,PATH 追加即可

本章命令速查

命令 作用
sudo apt install -y git 安装 Git
git config --global user.name/email "..." 设置提交者身份
git config --global init.defaultBranch main 默认分支设为 main
git config --global core.autocrlf input 提交时 CRLF 转 LF(Windows 必配)
ssh-keygen -t ed25519 -C "邮箱" 生成 SSH 密钥对
cat ~/.ssh/id_ed25519.pub 查看公钥(贴到代码平台)
ssh -T git@github.com 测试 SSH 密钥是否配置成功
git clone --depth 1 <url> 浅克隆,只取最近一次提交
git status / git diff / git log --oneline --graph 看状态 / 看改动 / 看历史图
git add . / git commit -m "..." 暂存 / 提交
git switch -c <分支> / git stash 新建分支 / 临时收起改动
sudo apt install -y build-essential / gcc --version / make --version 装编译工具 / 验证编译器与 make
mvn -v / mvn clean package -DskipTests / mvn -U Maven 版本 / 打包 / 强制更新
gradle -v / gradle build Gradle 版本 / 构建
source /etc/profile.d/<名字>.sh 让环境变量脚本在当前终端生效

相关章节

📄 HTTPS 证书(Let's Encrypt / certbot) 32-https-certbot.md


title: 32 HTTPS 证书(Let's Encrypt / certbot) updated: 2026-09 applies-to: Ubuntu 22.04 LTS

HTTPS 给网站加上加密与身份证明,如今是标配而非可选项。本章走完整链路:确认前置条件 → 安装 certbot → 一键签发 → 验证 → 自动续期 → 证书文件与手动配置 → 过期排查与避坑。

[!IMPORTANT] 本章版本号截至 2026 年 9 月。版本信息的唯一权威来源是 版本速查表,冲突时以速查表为准。

前置条件与原理

是什么

HTTPS 证书是由 CA(证书颁发机构)签发的"服务器身份证明":浏览器通过它确认"我连的真是这个域名的主人,且通信被加密"。Let's Encrypt 是免费、自动化的公共 CA,唯一"代价"是证书有效期只有 90 天——这倒逼你配置自动续期,而自动续期正是 certbot 的核心价值。为什么免费还要 90 天一换?短周期降低密钥泄露的长期风险,也让"证书错了能快速作废重发"成为可能。

前置条件清单(缺一不可,签发前逐条核对):

条件 为什么需要 怎么确认
有域名且 A 记录已解析到本服务器 验证方式是"证明你控制该域名" dig +short example.com 返回你的服务器 IP
80 端口可从公网访问 HTTP-01 验证走 HTTP 公网机器 curl -I http://example.com 能通
服务器时间准确 TLS 握手校验证书时间窗,时间错会直接失败 timedatectl 看 synchronized
Nginx 已安装运行 certbot 要改它的配置并重载 Nginx 章节 完成安装

怎么做

逐条自检:

# 确认域名解析到了本机公网 IP
$ dig +short example.com

# 确认 80 端口在监听且时间同步正常
$ ss -lntp | grep :80
$ timedatectl
预期输出:
203.0.113.10
LISTEN 0 511 0.0.0.0:80 0.0.0.0:* users:(("nginx",pid=1234,fd=6))
               Local time: Tue 2026-09-29 10:00:00 CST
        NTP service: active
        System clock synchronized: yes

云服务器还要检查安全组/防火墙放行了 80 端口——ss 显示监听只代表本机在听,云平台安全组是更外面的一道门(见 防火墙)。

参数说明

术语 含义
CA 证书颁发机构,签发并背书证书的第三方
HTTP-01 验证 在 http://域名/.well-known/acme-challenge/ 放一个临时文件供 CA 回访,证明你控制该域名
有效期 90 天 Let's Encrypt 的强制周期,到期前 30 天可续
域名解析 A 记录 域名 → IP 的 DNS 记录

验证方法

最硬的验证是从外网访问 80 端口(可用手机流量热点,不要在服务器本机测):

# 在外网环境执行
$ curl -I http://example.com
预期输出:
HTTP/1.1 200 OK
Server: nginx/1.30.x

常见问题

现象 根因 解决
dig 返回的 IP 不对 A 记录没改或没生效 到域名服务商改 A 记录,等 DNS 生效
外网访问 80 超时 安全组/防火墙没放行 云控制台安全组 + ufw allow 80/tcp
System clock synchronized: no 没开时间同步 sudo timedatectl set-ntp on

安装 certbot

是什么

certbot 是与 Let's Encrypt 交互的官方客户端,负责"申请验证文件 → 回应 CA 校验 → 下载证书 → 改 Nginx 配置 → 到期自动续"全套流程。安装方式两种:snap 安装(官方推荐),证书插件、依赖随 snap 自更新,不会被系统源版本卡住;apt 安装更符合习惯,但版本随 Ubuntu 仓库走。

怎么做

推荐 snap 方式:

# 确认 snapd 正常(Ubuntu 22.04 自带)
$ sudo snap install core && sudo snap refresh core

# 安装 certbot,--classic 授予完整系统权限(改 nginx 配置需要)
$ sudo snap install --classic certbot

# 确认 certbot 命令可用
$ certbot --version
预期输出(4.x 或更新均正常,版本随官方更新):
certbot 4.x.x

若偏好 apt(两者选其一即可,不要混装):

# 从系统源安装 certbot 与 nginx 插件
$ sudo apt install certbot python3-certbot-nginx

参数说明

方式 优点 注意
snap install --classic certbot 官方推荐,自动更新,插件齐全 --classic 是必须的,否则无权改配置
apt install certbot python3-certbot-nginx 熟悉的包管理方式 版本可能落后;必须带 nginx 插件包
certbot --version 确认安装成功 4.x 或更新均可(截至 2026-09)

[!WARNING] snap 版与 apt 版不要同时安装。 两套 certbot 会互相覆盖续期配置,导致"看着都装了、续期却悄悄失败"。切换方式时先彻底卸载另一套(sudo snap remove certbot 或 sudo apt remove certbot)。

验证方法

# 确认 certbot 可执行且 nginx 插件在列
$ certbot --version
$ certbot plugins
预期输出(应能看到 nginx 插件):
* nginx
Description: Nginx Web Server plugin
Interfaces: IAuthenticator, IInstaller, IPlugin

常见问题

现象 根因 解决
snap: command not found snapd 未安装 sudo apt install snapd
snap 安装后命令找不到 /snap/bin 不在 PATH 重新登录 shell,或用全路径 /snap/bin/certbot
certbot plugins 看不到 nginx 只装了核心没装插件 补装 python3-certbot-nginx(apt 方式)

一键签发:nginx 插件

是什么

certbot --nginx 是最省心的签发方式:certbot 自动找到 Nginx 配置里对应域名的 server 块,临时改配置完成 HTTP-01 验证,签发成功后再自动把证书写进配置(可选附带 HTTP→HTTPS 重定向)。整个过程你不用碰任何证书文件。

怎么做

# 一键为两个域名签发证书并自动配置 nginx
$ sudo certbot --nginx -d example.com -d www.example.com

交互过程会依次询问(回答要点):

提问 怎么答 说明
Enter email address 输入你的邮箱 到期前收提醒邮件,强烈建议填真实邮箱
Please read the Terms of Service 输入 Y 同意 不同意无法签发
Would you be willing to share your email... Y 或 N 均可 是否订阅 EFF 新闻,随意
请选择重定向选项 选 2(Redirect) 选 2 后 80 端口自动 301 跳转 HTTPS;不选的话 http 仍以明文可访问

成功后 certbot 会输出证书位置与到期日。它对 Nginx 配置的改动:在你原有 server 块的 listen 80 基础上新增 listen 443 ssl 与证书路径,并生成一个 80 跳转 443 的块。

两种特殊场景的替代方案(一句带过):

  • webroot 方式:sudo certbot certonly --webroot -w /var/www/example -d example.com,验证文件落在你的站点目录,适合 Nginx 配置复杂、不想让 certbot 自动改文件的情况。
  • DNS-01 方式:sudo certbot certonly --dns-dnsprovider ...(需对应 DNS 插件与 API 凭据),通过 DNS TXT 记录验证,适合无法开放 80 端口或需要泛域名证书(*.example.com)的场景。

参数说明

参数 作用
--nginx 使用 nginx 插件:自动验证 + 自动改配置 + 自动重载
-d 指定域名,可写多个;每个都要能解析到本机
certonly 只签证书不改配置,配合 --webroot/--dns-* 使用
-w webroot 模式的站点根目录

验证方法

# 从外网验证 HTTPS 可访问且证书生效
$ curl -I https://example.com
预期输出:
HTTP/2 200
server: nginx/1.30.x

返回 HTTP/2 200 且无证书报错即为成功;http:// 版本应返回 301 跳转。

常见问题

现象 根因 解决
Certbot doesn't know how to find the server block Nginx 里没有该域名的 server 块 先配好 80 端口带正确 server_name 的 server 块再跑 certbot
Failed authorization procedure 80 端口不通或 DNS 未生效 回看前置条件清单逐项排查
某个 -d 域名验证失败 多个域名中有一个解析不对 先只签主域名,其余修好再加签
签发成功但 http 没跳 https 重定向选了 1(No redirect) 重跑 sudo certbot --nginx -d 域名,选 2

验证证书

是什么

签发完成要在三个层面确认:命令行可用(curl)、证书信息正确(certbot 自带查询)、浏览器可信(锁标)。certbot certificates 一条命令列出所有已签证书的域名、文件路径与到期日,是日常查得最多的命令。

怎么做

# 列出本机所有证书与到期日
$ sudo certbot certificates
# 命令行验证 HTTPS 响应
$ curl -I https://example.com

浏览器层面:直接访问 https://example.com,地址栏应出现锁形图标,点开可查看证书详情(颁发者 Let's Encrypt、有效期、覆盖域名)。

参数说明

命令 作用
sudo certbot certificates 列出证书名、覆盖域名、到期日、文件路径
sudo certbot delete --cert-name example.com 删除指定证书(含续期配置)
curl -I 只取响应头,-I 不会下载页面内容

验证方法

# 查看证书清单
$ sudo certbot certificates
预期输出:
Saving debug log to /var/log/letsencrypt/letsencrypt.log

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Found the following certs:
  Certificate Name: example.com
    Serial Number: 4f8a2b1c9d0e1f2a3b4c5d6e7f8a9b0c1d2e
    Key Type: ECDSA
    Domains: example.com www.example.com
    Expiry Date: 2026-12-28 08:00:00+00:00 (VALID: 89 days)
    Certificate Path: /etc/letsencrypt/live/example.com/fullchain.pem
    Private Key Path: /etc/letsencrypt/live/example.com/privkey.pem
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

重点看 Expiry Date 是否约 90 天后、Domains 是否包含你要的所有域名。

常见问题

现象 根因 解决
curl 正常但证书只覆盖一个域名 签发时只写了一个 -d sudo certbot --nginx -d 主域 -d www域 扩签
certbot certificates 显示 0 个证书 签发实际失败或被误删 重新执行一键签发流程
锁标点了显示"部分不安全" 页面里有 http:// 引用的资源 页面内资源全部改用 https 或协议相对路径

自动续期

是什么

90 天有效期意味着没有自动续期就必然过期——这是本章反复强调的核心理由。好消息是续期基本零配置:certbot 安装时自带 systemd timer,每天两次检查是否有证书进入"到期前 30 天"窗口,有就自动续。你要做的只有两件事:确认 timer 在跑、用干跑演练验证续期链路真的通。

怎么做

# 确认 certbot 的续期定时器在运行
$ systemctl list-timers | grep certbot
预期输出(snap 版安装的,timer 名带 snap 前缀):
snap.certbot.renew.timer     Wed 2026-09-30 06:32:11 CST  20h left  Tue 2026-09-29 03:14:02 CST  8h ago

[!NOTE] timer 名按安装方式不同:snap 版是 snap.certbot.renew.timer,apt 版才是 certbot.timer。两种都能完成自动续期,别因为名字带 snap. 就以为装错了。

然后做一次干跑演练——用测试环境 CA 完整走一遍续期流程,不消耗正式签发额度:

# 干跑演练续期
$ sudo certbot renew --dry-run
预期输出:
Congratulations, all simulated renewals succeeded:
  /etc/letsencrypt/live/example.com/fullchain.pem (success)

续期后自动重载 Nginx:证书换新后 Nginx 仍持有旧证书在内存里,必须 reload。certbot 提供 hook 目录,放进 deploy 的脚本会在每次成功续期后自动执行:

# 创建部署钩子目录并写入自动 reload 脚本
$ sudo tee /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh > /dev/null <<'EOF'
#!/bin/bash
systemctl reload nginx
EOF
$ sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh

参数说明

命令/机制 作用
certbot.timer / snap.certbot.renew.timer systemd 定时器(apt 版 / snap 版的名字),每天随机时间触发两次续期检查
certbot renew 检查并续期所有临近到期的证书,不要手动 cron 重复跑
--dry-run 对测试 CA 演练,不占正式签发额度
renewal-hooks/deploy/ 续期成功后执行的钩子目录(另有 pre/ authentication/post 阶段目录)

[!IMPORTANT] 签发成功后立即跑一次 sudo certbot renew --dry-run。 干跑能提前暴露续期链路的所有问题(80 端口不通、DNS 变更、hook 脚本没权限),而不是让证书在 90 天后的深夜静默过期。每次改动 DNS、防火墙或 Nginx 配置后,也建议再干跑一次。

验证方法

# 确认 timer 活跃、干跑成功、钩子可执行
$ systemctl list-timers | grep certbot
$ sudo certbot renew --dry-run
$ sudo run-parts --test /etc/letsencrypt/renewal-hooks/deploy/
预期输出:
snap.certbot.renew.timer     Wed 2026-09-30 06:32:11 CST  20h left ...
Congratulations, all simulated renewals succeeded.
/etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh

第三条命令列出钩子脚本名即代表权限与命名无误。

常见问题

现象 根因 解决
list-timers 里找不到 certbot timer 被禁或安装异常。注意 timer 名按安装方式不同:snap 版是 snap.certbot.renew.timer,apt 版才是 certbot.timer snap 版:sudo systemctl enable --now snap.certbot.renew.timer;apt 版:sudo systemctl enable --now certbot.timer
dry-run 报 Failed authorization 续期验证走 80 端口被挡 检查防火墙/安全组与 DNS,与首次签发的排查相同
续期成功但站点还是旧证书 没有 deploy 钩子,Nginx 没 reload 按上文创建 reload-nginx.sh 钩子
续期静默失败没人知道 邮箱没填或填错 sudo certbot update_account -m 新邮箱

证书文件与手动配置 Nginx

是什么

certbot 把证书统一存放在 /etc/letsencrypt/live/<域名>/ 下。目录里是四个文件的软链,各有分工:

文件 内容 用途
fullchain.pem 服务器证书 + 中间证书链 Nginx ssl_certificate 用这个
privkey.pem 私钥 ssl_certificate_key 用;绝不能泄露
cert.pem 仅服务器证书,无中间链 单独用会导致浏览器报不安全
chain.pem 仅中间证书链 调试证书链时用

自动改配置之外,你也可以手写 443 的 server 块(webroot/certonly 方式签的证书就需要)。

怎么做

server {
    listen 443 ssl;
    server_name example.com www.example.com;

    # 证书用 fullchain(含中间链),不要用 cert.pem
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    root /var/www/example;
    index index.html;
}

# 80 端口只做一件事:301 跳转到 HTTPS
server {
    listen 80;
    server_name example.com www.example.com;
    return 301 https://$host$request_uri;
}

改完走标准流程:sudo nginx -t && sudo systemctl reload nginx(Nginx 章节 的肌肉记忆)。

[!WARNING] privkey.pem 是私钥,泄露即身份被盗。 不要提交进 git、不要发到聊天工具、不要用 cat 贴到网上求助(发 cert.pem 或用 openssl x509 查看公开部分是安全的)。

参数说明

指令 作用 要点
listen 443 ssl 监听 HTTPS 端口 必须带 ssl 关键字
ssl_certificate 证书文件路径 用 fullchain.pem,原因见常见问题表
ssl_certificate_key 私钥路径 与证书成对
return 301 https://$host$request_uri 保留原路径整页跳转 80 块里不需要 root/location

验证方法

# 语法检查并重载,然后验证跳转与 HTTPS
$ sudo nginx -t && sudo systemctl reload nginx
$ curl -I http://example.com
$ curl -I https://example.com
预期输出:
nginx: configuration file /etc/nginx/nginx.conf test is successful
HTTP/1.1 301 Moved Permanently
Location: https://example.com/
HTTP/2 200

常见问题

现象 根因 解决
浏览器提示不安全,curl 却正常 ssl_certificate 误用了 cert.pem,中间链不完整 改用 fullchain.pem 并 reload
nginx: [emerg] cannot load certificate 证书路径写错或域名目录名不符 用 certbot certificates 核对 Certificate Path
访问 443 连接被拒绝 443 未放行 安全组与 ufw allow 443/tcp

排查与常见问题

是什么

证书问题最常见的两类:快过期/已过期和续期/签发失败。前者一条 openssl 命令看穿真实生效的证书有效期;后者几乎都能归到"80 端口不通、DNS 变了、限频"三种根因上。

怎么做

不依赖 certbot,直接问服务器上的真实证书(浏览器看到的也是它):

# 读取 443 端口实际返回的证书有效期
$ echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -dates
预期输出:
notBefore=Sep 29 08:00:00 2026 GMT
notAfter=Dec 28 08:00:00 2026 GMT

notAfter 距今不足 30 天,说明自动续期没工作,回上一节排查 timer 与 dry-run。

参数说明

命令片段 作用
openssl s_client -connect 模拟 TLS 客户端连接,打印握手信息
-servername example.com 发送 SNI,服务器按域名返回对应证书
openssl x509 -noout -dates 从证书中只提取起止日期

验证方法

综合排查路径:先 openssl 看真实有效期 → sudo certbot certificates 看 certbot 认知是否一致 → sudo certbot renew --dry-run 验证续期链路 → sudo tail -50 /var/log/letsencrypt/letsencrypt.log 看失败原因。

常见问题

现象 根因 解决
--nginx 报找不到 server 块 没有该域名的 80 server 块 先配好 listen 80 + 正确 server_name,再跑 certbot
验证失败 Timeout during connect 防火墙/云安全组没放行 80 放行 80 端口后重试
验证失败 DNS problem DNS 未生效或解析到了别处 dig +short 域名 核对,等生效再签
走 CDN 时验证失败 CDN 拦截了 ACME 回访 签发期间临时切 DNS 直连,或改用 DNS-01
too many certificates 限频 同一域名一周签发超 5 张 Let's Encrypt 限频;等一周窗口过去,平时多用 --dry-run 测试
续期失败 80 被其他服务占用,或域名解析已变更 `ss -lntp
浏览器不安全但 curl 正常 Nginx 用了 cert.pem 而非 fullchain.pem 换 fullchain.pem 并 reload
证书已过期站点打不开 timer 失效且无人发现 手动 sudo certbot renew,随后修 timer 并干跑验证

📄 33 国内镜像与加速源 33-mirrors-acceleration.md


title: 33 国内镜像与加速源 updated: 2026-09 applies-to: Ubuntu 22.04 LTS

镜像站把国外软件源「复制」一份放在国内,让你下载时不用跨洋。本章讲 apt、pip、npm、Maven、Docker 五类源的加速配置,以及换源出问题后怎么快速恢复。

[!NOTE] 本章的配置全部是可选的。镜像地址以镜像站官方帮助页为准;文中只给出域名,具体路径请到官方帮助页复制(截至 2026-09)。


一、开篇定位与重要前提

是什么

镜像加速是一种可选优化,不是部署的必做步骤。判断标准很简单:你的服务器下载官方源时慢不慢。海外服务器、或国内机器访问官方源本来就不慢时,完全没必要改 —— 多改一处就多一处出错的可能。

怎么做

先测速再决定:

# 测下载官方源一个文件的耗时(秒)
$ curl -o /dev/null -s -w '%{time_total}\n' http://archive.ubuntu.com/ubuntu/ls-lR.gz

耗时超过几秒、或 apt update 明显卡住才换。换源遵循三条原则:一次只换一类源、改前先备份、以镜像站官方帮助页给出的配置为准。

[!IMPORTANT] 不要盲目替换系统默认的 /etc/apt/sources.list。 镜像站的 URL 会变(网上抄的教程常常已过期); 与 Docker、Nginx 等第三方源混用会导致 404 或签名错误;出问题时你还分不清是镜像没同步还是配错了。 正确做法:只替换 Ubuntu 官方那几行,第三方源保持原样,改之前先备份。

参数说明

常见镜像站(到官方帮助页复制最新配置,不要照抄过期教程):清华大学 TUNA https://mirrors.tuna.tsinghua.edu.cn/、 阿里云 https://mirrors.aliyun.com/(开发者社区另有 developer.aliyun.com/mirror/)、腾讯云 https://mirrors.cloud.tencent.com/。

验证方法

$ apt policy nginx

预期输出(换源后 500 后面那行会变成镜像域名):

     1.18.0-6ubuntu14.4 500
        500 http://archive.ubuntu.com/ubuntu jammy-updates/main amd64 Packages

常见问题

现象 根因 解决
不知道该不该换源 没测过实际速度 用上面的 curl 测一下,慢才换
换源后反而更慢 镜像站到你的机器链路不佳 换另一个镜像站,或用备份恢复默认源
换了源但速度没变 改错文件 / 没 apt update 确认改的是生效的那个文件,改完必须 apt update

二、apt 镜像

是什么

apt 的软件源地址写在 /etc/apt/sources.list 和 /etc/apt/sources.list.d/*.list 里。所谓「换 apt 源」,就是把里面 archive.ubuntu.com 这类官方域名换成镜像站域名。

[!NOTE] Ubuntu 22.04 用 /etc/apt/sources.list(一行一条的传统格式);24.04 起改成 /etc/apt/sources.list.d/ubuntu.sources 的 deb822 格式,写法不同,改之前先 ls /etc/apt/sources.list.d/ 确认。

怎么做

# 1) 先备份(这步不能省)
$ sudo cp /etc/apt/sources.list /etc/apt/sources.list.bak

# 2) 把官方域名替换成镜像域名(以官方帮助页给出的为准)
$ sudo sed -i 's|http://archive.ubuntu.com/ubuntu|https://mirrors.aliyun.com/ubuntu|g' /etc/apt/sources.list
$ sudo sed -i 's|http://security.ubuntu.com/ubuntu|https://mirrors.aliyun.com/ubuntu|g' /etc/apt/sources.list

# 3) 改完必须更新索引
$ sudo apt update

替换后的内容形如(以 Ubuntu 22.04 jammy 为例):

deb https://mirrors.aliyun.com/ubuntu/ jammy main restricted universe multiverse
deb https://mirrors.aliyun.com/ubuntu/ jammy-updates main restricted universe multiverse
deb https://mirrors.aliyun.com/ubuntu/ jammy-security main restricted universe multiverse

[!WARNING] 只替换 Ubuntu 官方那几行。 /etc/apt/sources.list.d/ 下 Docker、Nginx、MySQL 等第三方源有各自的域名和签名密钥, 把它们一起 sed 掉会直接导致 apt update 报 NO_PUBKEY 或 404。用 grep -r deb /etc/apt/sources.list.d/ 先看看有哪些第三方源。

参数说明

片段 含义
deb 二进制包源(deb-src 是源码包源,一般不用开)
jammy 发行版代号;jammy-updates 更新、-backports 回溯、-security 安全更新
main / universe 等 组件:官方支持 / 社区维护 / 非自由软件
sources.list.bak 备份文件,恢复时直接用它覆盖

验证方法

$ apt policy nginx

预期输出(500 那行已变成镜像域名):

     1.18.0-6ubuntu14.4 500
        500 https://mirrors.aliyun.com/ubuntu jammy-updates/main amd64 Packages

常见问题

现象 根因 解决
apt update 报 NO_PUBKEY 第三方源被一起替换了,签名对不上 用备份恢复,只改官方那几行
报 Hash Sum mismatch / 散列不符 镜像同步到一半,索引与包不一致 等一会儿重试;不行就换回官方源
报 404 Not Found 镜像站路径变了或该版本已下线 去官方帮助页复制最新配置
想恢复默认源 —— sudo cp /etc/apt/sources.list.bak /etc/apt/sources.list && sudo apt update

三、pip 镜像

是什么

Python 的 pip 默认从 pypi.org 下载包。换镜像有两种粒度:临时(只对这一条命令生效)和永久(写进配置文件)。

怎么做

# 临时:只对这一次安装生效
$ pip install -i https://mirrors.aliyun.com/pypi/simple/ requests

# 永久:写进 pip 配置(用户级)
$ pip config set global.index-url https://mirrors.aliyun.com/pypi/simple/

等价的配置文件 ~/.config/pip/pip.conf(系统级是 /etc/pip.conf):

[global]
index-url = https://mirrors.aliyun.com/pypi/simple/

(示例用的是 https 镜像,不需要 trusted-host;只有镜像站走 http 或证书异常时才加 trusted-host = <域名>,见下表。)

[!WARNING] 不要用 sudo pip install 往系统 Python 里装包,它可能覆盖 apt 管理的依赖,把系统工具搞坏。 正确做法是用虚拟环境或 --user 安装,详见 Python。

参数说明

配置 / 参数 含义
-i <url> 临时指定索引地址
index-url 永久默认索引地址
trusted-host 信任该主机(镜像站用 http 或证书异常时才需要)
~/.config/pip/pip.conf / /etc/pip.conf 用户级 / 系统级配置文件

常用镜像域名(路径一般为 /pypi/simple/):mirrors.aliyun.com、pypi.tuna.tsinghua.edu.cn、mirrors.cloud.tencent.com。

验证方法

$ pip config list

预期输出:

global.index-url='https://mirrors.aliyun.com/pypi/simple/'

常见问题

现象 根因 解决
装包报 Connection timed out 默认源在国外 配 index-url
报证书错误 SSL: CERTIFICATE_VERIFY_FAILED 镜像站证书链问题 加 trusted-host,或改用 https 地址
配了但不生效 / 部分包找不到 多配置文件覆盖 / 镜像同步延迟 pip config list -v 看读取了哪些文件;临时换回官方源

四、npm 镜像

是什么

npm 的包下载地址由 registry 决定,默认连国外的 registry.npmjs.org。换镜像就是改这一项。

怎么做

# 设置国内镜像
$ npm config set registry https://registry.npmmirror.com

# 查看当前值
$ npm config get registry

等价的用户级配置文件 ~/.npmrc:

registry=https://registry.npmmirror.com

[!TIP] 需要在多个源之间来回切时,可以装 nrm(npm i -g nrm),用 nrm ls 看可选源、nrm use <名字> 一键切换。 项目级的做法是在项目根目录放一个 .npmrc,只对该项目生效。详见 Node.js。

参数说明

配置 含义
registry 包下载地址
~/.npmrc 用户级配置
项目内 .npmrc 项目级配置,优先级高于用户级
@scope:registry 为某个作用域单独指定源

验证方法

$ npm config get registry

预期输出:

https://registry.npmmirror.com/

常见问题

现象 根因 解决
换源后装包 404 镜像同步延迟 等一会儿,或临时换回官方源
私有包拉不到 / 换源没生效 镜像没有私有包 / 项目内 .npmrc 覆盖 配 @scope:registry=<私有源>;检查项目根目录
npm ci 仍然慢 lock 文件里写着旧的 resolved 地址 重新生成 lock 或加 --registry 参数

五、Maven 镜像

是什么

Maven 默认从国外的 Maven Central 下载依赖。换镜像要改 ~/.m2/settings.xml 的 <mirrors> 段,让对 central 的请求改走镜像站。

怎么做

<settings>
  <mirrors>
    <mirror>
      <id>cn-mirror</id>
      <name>国内公共仓库</name>
      <url>https://maven.aliyun.com/repository/public</url>
      <mirrorOf>central</mirrorOf>
    </mirror>
  </mirrors>
</settings>

[!NOTE] <url> 以镜像站官方帮助页为准(常见域名:maven.aliyun.com、mirrors.tuna.tsinghua.edu.cn、mirrors.cloud.tencent.com)。详见 Git 与构建工具。

参数说明

元素 含义
<id> 镜像的唯一标识,随便起
<url> 镜像仓库地址
<mirrorOf>central</mirrorOf> 只代理 id 为 central 的仓库
<mirrorOf>*</mirrorOf> 代理所有仓库(私有仓库也会被代理,慎用)
<localRepository> 本地依赖缓存路径,默认 ~/.m2/repository

[!IMPORTANT] 优先用 <mirrorOf>central</mirrorOf> 而不是 *。用 * 会把公司私有仓库的请求也转发到公共镜像,导致私有依赖永远拉不到。

验证方法

# 看生效后的镜像配置
$ mvn help:effective-settings | grep -A4 "<mirrors>"

预期输出:

  <mirrors>
    <mirror>
      <url>https://maven.aliyun.com/repository/public</url>
      <mirrorOf>central</mirrorOf>

常见问题

现象 根因 解决
下载依赖超时 默认连国外 Central 配置 <mirrors>
私有依赖拉不到 mirrorOf 写成了 * 改成 central,或加 <mirrorOf>*,!私有仓库id</mirrorOf>
改了 settings.xml 没生效 / 依赖残缺 文件放错位置 / 缓存下载了一半 必须在 ~/.m2/settings.xml;删 ~/.m2/repository 对应目录后 mvn -U

六、Docker 镜像加速

是什么

Docker 拉镜像默认走国外的 Docker Hub。加速方式是往 /etc/docker/daemon.json 里配 registry-mirrors,让 Docker 先去镜像站找。

怎么做

{
  "registry-mirrors": [
    "https://<镜像加速器地址>"
  ]
}
# 改完必须重启 Docker(reload 不会重新读取 daemon.json)
$ sudo systemctl restart docker

[!IMPORTANT] 改 daemon.json 后必须 sudo systemctl restart docker,reload 无效。 这一点和 Nginx 相反(Nginx 是 reload 就够了),因为 Docker 只在守护进程启动时读取一次该配置文件。 详见 Docker。

[!WARNING] 2024 年之后多家公共加速器已下线或改为需登录。docker pull 失败时优先怀疑这一项:用 docker info 看 Registry Mirrors 是否为空。可靠做法是到云厂商控制台申请专属加速器地址,或给仓库配认证。

参数说明

字段 / 命令 含义
registry-mirrors 镜像加速器地址数组,按顺序尝试
/etc/docker/daemon.json Docker 守护进程配置文件
sudo systemctl restart docker 重启守护进程使配置生效
docker pull <镜像> 拉取镜像,可观察是否走了加速器

验证方法

$ docker info | grep -A3 "Registry Mirrors"

预期输出:

 Registry Mirrors:
  https://<镜像加速器地址>/

若该项为空数组或没有输出,说明配置没生效 —— 检查 JSON 是否合法(多余逗号会导致 Docker 起不来)。

常见问题

现象 根因 解决
配完 docker pull 依旧超时 没 restart,只是 reload sudo systemctl restart docker
重启后 Docker 起不来 daemon.json 有语法错误(如多余逗号) journalctl -u docker -n 50 看报错,修正 JSON
Registry Mirrors 为空 配置文件路径或字段名写错 确认是 /etc/docker/daemon.json 的 registry-mirrors
加速器地址失效 / docker info 报权限错误 公共加速器下线 / 用户不在 docker 组 换云厂商专属地址;用 sudo 或把用户加入 docker 组

七、Go 与其他工具

是什么

其他语言的包管理器也有类似的「镜像地址」开关,思路完全一样:找到它读取源地址的配置项,改成镜像域名。

怎么做

# Go:设置模块代理(goproxy.cn 是国内常用的公共代理,也可用镜像站提供的 Go 代理)
$ go env -w GOPROXY=https://goproxy.cn,direct

# 查看
$ go env GOPROXY

其他工具同理:先查它的「源/镜像」配置项(如 Ruby 的 gem sources、Rust 的 ~/.cargo/config.toml),再按官方帮助页给出的地址替换。

参数说明

配置 含义
GOPROXY Go 模块代理地址
,direct 代理找不到时直接连源站,作为兜底

验证方法

$ go env GOPROXY

预期输出:

https://goproxy.cn,direct

常见问题

现象 根因 解决
go mod download 超时 默认代理在国外 设 GOPROXY
私有模块拉不到 / 改了没生效 代理转发私有请求 / 环境变量覆盖 设 GOPRIVATE=<私有域名>;检查是否 export 了同名 GOPROXY

八、通用排错与恢复默认源

是什么

换源引入的问题有个共同特点:报错指向「源」本身(签名、散列、404、超时),而不是你的应用代码。掌握「快速恢复默认源」这一招,就能把风险降到最低。

怎么做

# apt:一键恢复默认源(前提是改之前备份过)
$ sudo cp /etc/apt/sources.list.bak /etc/apt/sources.list
$ sudo apt update

# pip / npm:恢复官方源
$ pip config unset global.index-url
$ npm config set registry https://registry.npmjs.org

参数说明

报错关键词 含义 处理
NO_PUBKEY / 签名无效 签名密钥不匹配,通常是第三方源被误改 恢复该第三方源原样
Hash Sum mismatch 索引与包不一致,镜像同步中 稍后重试,或换回官方源
404 Not Found 路径变了 / 版本下线 去官方帮助页复制新地址
Release file ... is not valid yet 机器时间与源服务器差太多 校准时间后重试,见 网络配置

验证方法

# 确认已回到官方源且索引正常
$ apt update && apt policy nginx

预期输出(末尾无报错,来源回到官方域名):

命中:1 http://archive.ubuntu.com/ubuntu jammy InRelease
     1.18.0-6ubuntu14.4 500
        500 http://archive.ubuntu.com/ubuntu jammy-updates/main amd64 Packages

常见问题

现象 根因 解决
忘了备份就想恢复 没留退路 手动把域名改回 archive.ubuntu.com / security.ubuntu.com
换了源之后什么都装不上 改坏了整个 sources.list 恢复备份;第三方源单独还原
只有一个包装不上 / 不确定是不是源的问题 镜像延迟 / 判断不清 等几小时;先恢复默认源再试,能装上就说明是源的问题
时间不对导致 not valid yet 系统时钟漂移 校准时间后 apt update

本章命令速查

命令 作用
curl -o /dev/null -s -w '%{time_total}\n' <url> 测下载耗时,判断是否需要换源
sudo cp /etc/apt/sources.list /etc/apt/sources.list.bak 备份 apt 源
sudo sed -i 's|archive.ubuntu.com/ubuntu|<镜像域名>/ubuntu|g' /etc/apt/sources.list 替换 apt 源域名
sudo apt update 改源后必须更新索引
apt policy <包> 看候选版本与来源 URL(判断是否换成功)
pip config set global.index-url <url> / pip config list 设置 / 查看 pip 镜像
npm config set registry <url> / npm config get registry 设置 / 查看 npm 镜像
mvn help:effective-settings | grep -A4 "<mirrors>" 查看 Maven 生效的镜像配置
docker info | grep -A3 "Registry Mirrors" 查看加速器是否生效
sudo systemctl restart docker 让 daemon.json 生效(reload 无效)
go env -w GOPROXY=<url> / go env GOPROXY 设置 / 查看 Go 模块代理
pip config unset global.index-url 恢复 pip 默认源
sudo cp /etc/apt/sources.list.bak /etc/apt/sources.list 恢复 apt 默认源

相关章节

📄 systemd 服务管理 40-systemd.md


title: 40 systemd 服务管理 updated: 2026-09 applies-to: Ubuntu 22.04 LTS

前面章节装好的 MySQL、PostgreSQL,以及后面要部署的每一个应用,在 Ubuntu 上最终都交给同一套机制来启动、守护和开机自启 —— 这就是 systemd。本章是全书骨架:所有部署章都会说「写一个 unit 文件、交给 systemd」,具体怎么做都在这里。

[!IMPORTANT] 本章命令适用于 Ubuntu 22.04 自带的 systemd v249,截至 2026-09 仍是 LTS 默认版本。systemd 命令在 Ubuntu 与 RHEL 系上几乎完全一致,可放心迁移。

systemd 是什么

是什么

开机时,内核只启动一个进程,就是 PID 1 —— 在 Ubuntu 22.04 上它是 systemd。其他所有进程都由它直接或间接拉起,所以它天然是「进程的总管家」。

systemd 把管理对象抽象成 unit(单元),每种 unit 一个用途:

  • .service:守护进程,如 mysql.service、nginx.service —— 本章主角;
  • .timer:定时器,是 cron 的现代替代品;
  • .socket / .mount / .path:按需启动、挂载、路径变化触发,部署中偶尔遇到;
  • .target:一组 unit 的集合,相当于「运行级别」,如 multi-user.target 代表多用户命令行环境。

为什么全书所有软件都教 systemctl?因为不管你装的是 MySQL 还是自研 Java 应用,在 systemd 眼里都只是一个 unit —— 一套命令管所有软件,不用再记每个软件自带的启停脚本。

怎么做

先确认自己机器上的 PID 1 确实是 systemd,再看系统里有哪些服务:

# 查看 1 号进程的名字
$ ps -p 1 -o comm=

# 列出当前已加载的服务 unit
$ systemctl list-units --type=service

参数说明

list-units 输出每行是一个 unit,各列含义:

列 常见值 含义
UNIT nginx.service unit 名,systemctl 操作时写它
LOAD loaded / not-found 配置是否成功加载
ACTIVE active (running) / failed / inactive 运行状态,部署最关心这个
SUB running / dead / exited 细分状态:running 常驻运行、exited 跑完就退

验证方法

# 只看「正在运行」的服务,输出短得多
$ systemctl list-units --type=service --state=running
预期输出:
  UNIT                  LOAD   ACTIVE SUB     DESCRIPTION
  cron.service          loaded active running Regular background program processing
  mysql.service         loaded active running MySQL 8.0 Database Server
  ssh.service           loaded active running OpenBSD Secure Shell server

常见问题

现象 根因 解决
System has not been booted with systemd 在 WSL1 或容器里没有真正的 systemd WSL2 在 /etc/wsl.conf 加 [boot] systemd=true;容器交给宿主机管理
服务名提示不存在 名字拼错或软件没装 systemctl list-unit-files | grep <关键词> 模糊查找

服务管理九连

是什么

日常管服务就 9 个动词,分两组记:

  • 生命周期:start(启动)、stop(停止)、restart(重启)、reload(重读配置);
  • 开机自启:enable(设为自启)、disable(取消自启)、is-enabled(查自启状态);加上查状态的 status 和 is-active,凑成九连。

reload 和 restart 的区别必须搞清:

  • restart:杀掉进程重新拉起,已有连接全部断开;
  • reload:优先执行 unit 里 ExecReload= 定义的命令(服务未定义 ExecReload= 时才退回给进程发 SIGHUP 信号),让它重读配置文件但不断开连接,适合改完 nginx、MySQL 配置后平滑生效;
  • 不是所有服务都支持 reload(取决于软件自己有没有实现),不支持时 reload 会失败,只能 restart。

怎么做

# 启动 nginx
$ sudo systemctl start nginx

# 改完配置平滑重载,不断连接;不支持 reload 的服务改用 restart
$ sudo systemctl reload nginx

# 开机自启 + 立即启动,一步到位
$ sudo systemctl enable --now nginx

参数说明

命令 作用 是否断开现有连接
systemctl start <服务> 启动 -
systemctl stop <服务> 停止 是
systemctl restart <服务> 重启进程 是
systemctl reload <服务> 发 SIGHUP 重读配置 否(需服务支持)
systemctl status <服务> 状态与最近日志 -
systemctl enable <服务> 设置开机自启 -
systemctl disable <服务> 取消开机自启 -
systemctl is-active <服务> 只回答运行与否,适合写脚本 -

[!TIP] enable 只管「开机拉起」,不会立刻启动;start 只管「现在拉起」,不会设自启。部署时用 enable --now 一步到位。

验证方法

# 确认 nginx 运行中且已设自启
$ systemctl is-active nginx
$ systemctl is-enabled nginx
预期输出:
active
enabled

这两条命令只输出一个词、退出码可判断,是脚本里最常用的检查方式。

常见问题

现象 根因 解决
reload 执行后报 Job 失败 该服务没实现 reload 改用 restart
start 卡很久后报 Job failed 启动超时,常伴随端口占用或配置错误 看 systemctl status 与 journalctl -u
stop 之后进程还在 有子进程没被杀干净,或被别的守护工具拉着 systemctl kill <服务>;排查是否重复守护(见 42)

看懂 status 输出

是什么

systemctl status 是排错第一站:一屏给出服务状态、进程号、资源占用和最近几行日志。看懂它,80% 的「服务怎么挂了」当场就有答案。

怎么做

# 查看 nginx 服务的完整状态
$ systemctl status nginx

参数说明

以下是一次典型输出,逐段解读:

预期输出:
● nginx.service - A high performance web server and a reverse proxy server
     Loaded: loaded (/lib/systemd/system/nginx.service; enabled; preset: enabled)
     Active: active (running) since Tue 2026-09-29 10:00:03 CST; 2 days ago
   Main PID: 1234 (nginx)
      Tasks: 5 (limit: 4915)
     Memory: 12.5M
        CPU: 3.201s
     CGroup: /system.slice/nginx.service
             ├─1234 "nginx: master process"
             └─1235 "nginx: worker process"
Sep 29 10:00:03 web01 systemd[1]: Starting A high performance web server...
Sep 29 10:00:03 web01 systemd[1]: Started A high performance web server...
行 怎么读
● nginx.service - ... 圆点颜色即状态:绿=运行、红=失败、白=未运行;后面是描述
Loaded: unit 文件路径 + 自启状态(enabled 表示开机自启已开)
Active: 当前状态 + 持续时间 ——「2 days ago」说明两天没重启过
Main PID: 主进程号,top 定位、排查都用它
Tasks / Memory / CPU 资源占用,判断内存泄漏先看这里
末尾几行 最近日志,只是预览,完整日志用 journalctl -u

验证方法

# 停掉 nginx 后再看状态,观察变化
$ sudo systemctl stop nginx
$ systemctl status nginx
预期输出:
● nginx.service - A high performance web server and a reverse proxy server
     Loaded: loaded (/lib/systemd/system/nginx.service; enabled; preset: enabled)
     Active: inactive (dead) since Tue 2026-09-29 15:00:00 CST; 3s ago

常见问题

现象 根因 解决
Active: failed (Result: exit-code) 启动进程退出码非 0 立刻 journalctl -u <服务> -n 50 看完整日志
Active: activating (auto-restart) 起不来但配了 Restart,反复重试 查日志找根因,别等它自己好
status 正常但功能不通 进程活着但监听端口或配置不对 ss -tlnp 查端口,回查应用自身配置

开机自启 enable 的本质

是什么

systemctl enable 只做一件事:在 /etc/systemd/system/multi-user.target.wants/ 目录下,为你的 unit 文件建一个软链接。开机最后阶段,systemd 启动 multi-user.target(多用户命令行环境),并顺带拉起它的 .wants/ 目录里链接的所有 unit —— 所以「自启」=「把自己的 unit 链接进 wants 目录」。

[!NOTE] 传统运行级别(runlevel 0-6)在 systemd 里由 target 对应:multi-user.target ≈ 级别 3(命令行),graphical.target ≈ 级别 5(图形界面)。服务器默认进入 multi-user.target。

怎么做

# 设置开机自启
$ sudo systemctl enable nginx

# 开机自启 + 立即启动,部署最常用
$ sudo systemctl enable --now nginx

# 取消开机自启,正在运行的服务不会被停掉
$ sudo systemctl disable nginx

参数说明

命令 动作 目录变化
enable 建软链 multi-user.target.wants/ 下出现该服务的链接
disable 删软链 上述链接消失
enable --now 建软链 + 启动 链接出现且服务 active

--now 是在 enable/disable 之上追加一条 start/stop。

验证方法

亲手看一眼软链,理解就扎实了:

# 查看 wants 目录里有没有 nginx 的软链,再确认自启状态
$ ls -l /etc/systemd/system/multi-user.target.wants/ | grep nginx
$ systemctl is-enabled nginx
预期输出:
lrwxrwxrwx 1 root root 35 Sep 29 10:00 nginx.service -> /lib/systemd/system/nginx.service
enabled

常见问题

现象 根因 解决
enable 了但重启后没起来 unit 缺 [Install] 段或 WantedBy 写错 补 WantedBy=multi-user.target,改后 daemon-reload 再 enable
enable 报 No such file or directory unit 文件路径不对 先 systemctl cat <服务> 确认实际加载路径
想让应用比数据库先起来 启动顺序由 After= 决定,与 enable 无关 unit 里写 After=mysql.service

unit 文件详解(核心)

是什么

unit 文件就是服务的「说明书」:怎么启动、以谁的身份跑、挂了怎么办、要不要自启。systemd 按以下优先级查找,同名时上面的覆盖下面:

优先级 目录 用途
1(最高) /etc/systemd/system/ 管理员自己写的放这里
2 /run/systemd/system/ 运行时临时生成,重启即消失
3 /usr/local/lib/systemd/system/ 本机手动安装的软件(如 make install)自带的 unit 放这里
4 /lib/systemd/system/ apt 装的软件自带,别直接改(升级会被覆盖)

自己写的服务放 /etc/systemd/system/;改软件自带的 unit 用 systemctl edit 生成覆盖片段(见「排查工具箱」一节),不动原文件。

怎么做

以一个自建 Java 应用为例,这是全书部署章的模板。先建专用用户,再写 unit 文件:

# 创建无登录权限的系统用户,专用于跑应用
$ sudo useradd -r -s /usr/sbin/nologin appuser

# 新建 unit 文件
$ sudo nano /etc/systemd/system/myapp.service
[Unit]
Description=My Java App
After=network.target mysql.service

[Service]
Type=simple
User=appuser
Group=appuser
WorkingDirectory=/opt/myapp
ExecStart=/usr/bin/java $JAVA_OPTS -jar /opt/myapp/app.jar
Restart=always
RestartSec=5
Environment="JAVA_OPTS=-Xms512m -Xmx1024m"
EnvironmentFile=-/opt/myapp/app.env
LimitNOFILE=65536

[Install]
WantedBy=multi-user.target

让 systemd 识别新文件并启动:

# 重载 unit 定义,新增或修改 unit 后必须执行
$ sudo systemctl daemon-reload

# 设置自启并立即启动
$ sudo systemctl enable --now myapp

参数说明

逐行拆解这份模板,每一行都回答「为什么」:

[Unit] 段 —— 元信息与依赖

  • Description=:status 和日志里显示的名字,写清楚是什么;
  • After=network.target mysql.service:排序依赖,等网络就绪、MySQL 启动后再启动本服务,应用连库不失败。注意 After 只管顺序,不管「MySQL 起不来我就不启动」——那是 Requires= 的职责,部署场景配 After 通常够用。

[Service] 段 —— 怎么跑

  • Type=simple:ExecStart 直接前台运行,systemd 认定主进程就是它。五种 Type 怎么选:
    • simple:前台常驻,现代应用(Java/Go/Python/Node)的默认选择;
    • exec:与 simple 类似,但 systemd 会等到 exec() 成功才标记启动,能捕捉「二进制不存在、无执行权限」等失败(v249+ 可用);
    • forking:进程自己 fork 到后台、父进程退出(老式 daemon 脚本风格);
    • notify:服务就绪后主动通知 systemd(需程序调用 sd_notify API,状态更精确);
    • oneshot:跑一次就退出,如初始化脚本,常配 RemainAfterExit=yes。
  • User= / Group=:以哪个用户跑。用 root 跑应用,一旦被攻破就是整机沦陷,专用低权用户是底线;
  • WorkingDirectory=:进程的工作目录,应用读相对路径的配置和静态文件都基于它;
  • ExecStart=:必须绝对路径,且不支持管道、重定向、通配符等 shell 特性;但支持 Environment= 定义的 $VAR/${VAR} 变量展开(上面的 $JAVA_OPTS 就是)。用 which java 确认真实路径再填;
  • Restart=always 与 Restart=on-failure 的区别:
    • always:无论怎么退出都拉起,包括正常退出,适合「永远该活着」的常驻服务;
    • on-failure:只在异常退出(非 0 退出码、被信号杀死)时拉起,正常退出不管;
    • 常驻应用选 always,不会错。
  • RestartSec=5:每次拉起间隔 5 秒,给依赖服务(如 MySQL)留恢复时间;
  • Environment="JAVA_OPTS=-Xms512m -Xmx1024m":直接写死的环境变量。值含空格时必须整体加引号,否则 -Xmx1024m 会被当成第二条(非法的)赋值而静默忽略,堆参数只剩一半。写好后别忘了在 ExecStart= 里引用 $JAVA_OPTS 才会传给 java;若不想引入变量,60 章端到端实战的做法是把堆参数直接写进 ExecStart;
  • EnvironmentFile=-/opt/myapp/app.env:从文件批量读环境变量(数据库密码等敏感信息放这里,该文件不要进 git)。开头的 - 表示文件不存在也不报错,首次部署友好;
  • LimitNOFILE=65536:进程可打开的文件句柄数上限(每个 TCP 连接、每个打开的文件都占一个)。默认 1024,高并发应用必须调大,否则报 Too many open files。

[Install] 段 —— 装进哪个 target

  • WantedBy=multi-user.target:enable 时把软链建到它的 .wants/ 目录,服务器标准写法。

验证方法

启动后用 status 确认,重点看 Loaded 路径和 Main PID:

$ systemctl status myapp

# 确认进程确实以 appuser 身份在跑
$ ps -o user= -p $(systemctl show -p MainPID --value myapp)
预期输出:
● myapp.service - My Java App
     Loaded: loaded (/etc/systemd/system/myapp.service; enabled; preset: enabled)
     Active: active (running) since Tue 2026-09-29 16:20:00 CST; 10s ago
   Main PID: 5678 (java)

appuser

Loaded 应指向你写的 /etc/systemd/system/myapp.service,末行 appuser 确认进程没有以 root 运行。

常见问题

现象 根因 解决
Failed with result 'exit-code' 应用自己崩了,systemd 只是如实汇报 journalctl -u myapp -n 50 看应用报错
Loaded 显示 not-found unit 文件名拼错或没 daemon-reload 核对文件名,sudo systemctl daemon-reload
应用读不了某些文件 User= 指定的用户权限不足,或没设 WorkingDirectory chown -R appuser:appuser <部署路径>;unit 里补 WorkingDirectory=

资源限制

是什么

除了 LimitNOFILE(文件句柄数),systemd 还能直接在 unit 里限制服务的内存和 CPU,被托管的进程超限时由 systemd 兜底,不需要额外装监控组件:

键 作用 超限行为
LimitNOFILE=65536 可打开的文件句柄上限 进程报 Too many open files
MemoryMax= 内存硬上限 超出的进程被 OOM kill
MemoryHigh= 内存软限(throttle 阈值) 超限时内核回收页缓存并限流,不杀进程
CPUQuota= CPU 时间配额,如 100% 表示单核满载 超限时进程被节流,不会报错

怎么做

在 [Service] 段追加,daemon-reload + restart 生效:

[Service]
# 内存硬顶 512M,超限直接杀;软限 400M 起开始回收
MemoryMax=512M
MemoryHigh=400M
# 最多用满一个核(200% = 两个核)
CPUQuota=100%

改完可用 systemctl status <服务> 看 Memory 是否被压住,或 systemctl show <服务> -p MemoryMax -p MemoryHigh -p CPUQuotaPerSecUSec 核对实际生效值。

[!TIP] 生产上给应用设 MemoryMax 时,记得比 JVM 的 -Xmx 留出余量(堆外内存、元空间、线程栈都要占内存),否则 java 本体没超限也会被 systemd 杀掉。

改了 unit 文件必须 daemon-reload

是什么

systemd 启动后会把所有 unit 文件缓存在内存里。你用编辑器改了磁盘上的文件,内存里的还是旧的 —— 这是新手最常踩的坑:改完直接 restart,要么报错,要么行为诡异。

[!IMPORTANT] 新增、修改、删除 unit 文件后,必须执行 sudo systemctl daemon-reload,让 systemd 重新读盘,然后再 restart。固定顺序:改文件 → daemon-reload → restart。

怎么做

# 修改 unit 后的标准三连
$ sudo nano /etc/systemd/system/myapp.service
$ sudo systemctl daemon-reload
$ sudo systemctl restart myapp

参数说明

命令 作用 何时需要
daemon-reload 重读所有 unit 文件 unit 文件有任何增删改
restart 重启服务进程 改了影响运行行为的键

注意:daemon-reload 只是「重新理解说明书」,不会重启进程;要让新配置作用到进程还需 restart。

验证方法

改完先看 systemd 实际加载的内容,确认是新的:

# 查看该 unit 实际生效的完整配置
$ systemctl cat myapp
预期输出:
# /etc/systemd/system/myapp.service
[Unit]
Description=My Java App
...

常见问题

现象 根因 解决
改了 ExecStart,restart 后仍跑旧命令 忘了 daemon-reload 三连:改文件 → daemon-reload → restart
restart 报 unit changed on disk systemd 检测到文件与缓存不一致 先 daemon-reload 再 restart
怎么改都不生效 改的是 /lib/systemd/system/ 里的原文件,被 /etc 里的同名文件覆盖 用 systemctl cat 看真正生效的是哪个文件

服务日志:journalctl

是什么

systemd 启动的服务,其 stdout/stderr 会被 journald 自动收走,统一存进二进制日志,用 journalctl 查询 —— 不用找每个软件自己的日志文件在哪,一条命令按服务过滤。完整用法见 51 journalctl。

怎么做

# 看 myapp 最近 50 行日志
$ journalctl -u myapp -n 50

# 实时跟踪日志,排错时开着
$ journalctl -u myapp -f

参数说明

参数 作用
-u <服务> 只看指定 unit 的日志
-f 持续跟踪新输出(类似 tail -f)
-n <行数> 只显示末尾 N 行
-b 只看本次开机以来的
--since "10 min ago" 按时间过滤

验证方法

# 跟踪日志,另开终端 restart 服务,能看到启动日志滚出
$ journalctl -u myapp -f
预期输出:
Sep 29 16:20:00 web01 systemd[1]: Started My Java App.
Sep 29 16:20:01 web01 java[5678]: Started MyJavaApp in 2.3 seconds

[!TIP] 服务起不来时,第一反应永远是 journalctl -u <服务> -n 50,日志里往往直接写着原因。

常见问题

现象 根因 解决
-u 查不到任何日志 unit 名拼错或服务从未启动过 systemctl list-units --all | grep <关键词> 确认名字
日志占满磁盘 journald 默认上限为磁盘 10%(封顶 4G),小磁盘上仍可能偏大 在 journald.conf 显式收紧 SystemMaxUse=,详见 51

排查工具箱

是什么

除了 status 和 journalctl,还有四件趁手兵器:找失败的服务、找拖慢开机的服务、看实际生效的配置、无侵入修改配置。

怎么做

# 一、列出所有启动失败的服务
$ systemctl list-units --type=service --state=failed

# 二、看各服务拖慢开机多少,启动耗时排行
$ systemd-analyze blame

# 三、看某服务实际生效的 unit 内容(含覆盖片段的合并结果)
$ systemctl cat myapp

# 四、无侵入修改:生成 override 覆盖片段
$ sudo systemctl edit myapp

systemctl edit 打开编辑器,写入的键值保存为 /etc/systemd/system/myapp.service.d/override.conf,只覆盖你写的键,其余沿用原文件。

参数说明

为什么推荐 systemctl edit 而不是直接改原文件:

方式 软件升级后 可追溯性
直接改 /lib/systemd/system/xxx.service 改动被包升级覆盖丢失 无痕迹,难回滚
systemctl edit 生成 override 原文件更新不受影响,override 依然生效 override 独立成文件,删掉即回滚

apt 装的软件,原 unit 属于包管理器不该手改;override 把「你的定制」和「官方原件」分离,升级互不踩。

验证方法

# 看 override 是否已被加载
$ systemctl cat myapp
预期输出:
# /etc/systemd/system/myapp.service
...
# /etc/systemd/system/myapp.service.d/override.conf
[Service]
RestartSec=10

末尾出现 override.conf 段,说明覆盖已加载。改 override 同样要 daemon-reload + restart。

常见问题

现象 根因 解决
Start request repeated too quickly 短时间反复崩溃触发频率限制(默认 10 秒内 5 次) 先修崩溃根因;临时解锁 sudo systemctl reset-failed <服务>(只清计数与失败状态,不解决崩溃);确需放宽就在 [Unit] 段加 StartLimitIntervalSec= 与 StartLimitBurst=
unit 里读了环境变量但程序拿不到 systemd 启动的进程不走 .bashrc/.profile,shell 里 export 的变量一概不继承 用 Environment= 或 EnvironmentFile= 显式提供,见下方 WARNING
日志含 Failed at step EXEC 或 203 EXEC ExecStart 路径不存在,或文件没有执行权限 ls -l 核对路径与执行位;脚本确认 shebang 存在
Failed with result 'exit-code',手动跑却正常 systemd 与终端的运行环境不同:用户、工作目录、环境变量都不同 逐项核对 User / WorkingDirectory / Environment

[!WARNING] 经典坑:systemd 不读 .bashrc。 你在终端 export DB_PASSWORD=xxx 后「明明配好了」,服务里依然读不到 —— systemd 启动的进程和你的登录 shell 是两个世界。服务所需的环境变量,必须写进 unit 的 Environment= 或 EnvironmentFile=。

定时任务:systemd timer

是什么

.timer 是 systemd 提供的定时器,配合同名 service unit 实现「到点跑任务」,是 cron 的现代替代:日志进 journald、依赖管理统一、错过的任务可在开机后补跑(Persistent=true)。与 cron 的取舍详见 44 备份与定时任务。

怎么做

最简示例:每天凌晨 3 点跑备份。写两个文件:mybackup.timer(什么时候)和 mybackup.service(干什么)。

# 新建定时器文件
$ sudo nano /etc/systemd/system/mybackup.timer

# 只 enable timer 即可,service 由 timer 拉起
$ sudo systemctl enable --now mybackup.timer
[Unit]
Description=Run mybackup daily

[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true

[Install]
WantedBy=timers.target

参数说明

键 作用
OnCalendar= 日历时间表达式,*-*-* 03:00:00 即每天 3 点
Persistent=true 错过的执行(如关机期间)开机后补跑一次
timer 与 service 同名前缀 mybackup.timer 自动触发 mybackup.service

验证方法

# 查看所有定时器及下次触发时间
$ systemctl list-timers
预期输出:
NEXT                        LEFT LAST                       PASSED UNIT            ACTIVATES
Wed 2026-09-30 03:00:00 CST 10h  Tue 2026-09-29 03:00:00 CST 19h ago mybackup.timer mybackup.service

NEXT 列与设置的触发点一致,即配置正确。

常见问题

现象 根因 解决
timer 激活了但任务没跑 对应 .service 没写或名字对不上 两文件同名前缀,systemctl list-timers --all 检查
改了 OnCalendar 不生效 忘了 daemon-reload daemon-reload 后 restart timer
想立即手动跑一次 timer 只按表触发 直接 sudo systemctl start mybackup.service
RHEL / Rocky / AlmaLinux 9 对照
Ubuntu (apt) RHEL (dnf) 说明
systemctl 全部命令 完全相同 systemd 命令两家一致,本章内容通用
/lib/systemd/system/ /usr/lib/systemd/system/ 发行版 unit 目录略异;/etc/systemd/system/ 相同
apache2.service httpd.service 少数软件服务名不同,以 systemctl list-unit-files 实查为准

本章命令速查

命令 作用
systemctl start|stop|restart <服务> 启动 / 停止 / 重启(restart 断连接)
systemctl reload <服务> 平滑重读配置,不断连接(需服务支持)
systemctl status <服务> 看状态、主进程与最近日志
systemctl is-active <服务> 脚本里判断是否运行
systemctl is-enabled <服务> 判断是否开机自启
systemctl enable --now <服务> 开机自启 + 立即启动
systemctl disable <服务> 取消开机自启
sudo systemctl daemon-reload 改 unit 文件后必须执行
systemctl cat <服务> 看实际生效的 unit 内容
sudo systemctl edit <服务> 生成 override 覆盖片段
systemctl list-units --state=failed 列出失败的服务
systemd-analyze blame 开机各服务耗时排行
journalctl -u <服务> -f 实时跟踪服务日志
journalctl -u <服务> -n 50 看服务最近 50 行日志
sudo systemctl reset-failed <服务> 清除失败状态与频率限制
systemctl list-timers 查看定时器下次触发时间

相关章节

📄 源码编译安装 41-source-compile-install.md


title: 41 源码编译安装 updated: 2026-09 applies-to: Ubuntu 22.04 LTS

apt 装不到的版本、要自定义模块的软件,只剩源码编译一条路。编译本身只有三步,真正让人栽跟头的是它特有的三类坑:动态库找不到、卸不掉、编一半失败。本章一次讲清:什么时候才值得编译、标准流程,以及每类坑的成因与解法。

[!IMPORTANT] 安装方式优先级:apt 官方源 > 软件官方维护的第三方源 > 官方二进制包 > 源码编译。能用包管理就别编译 —— 包管理自带依赖解决、自动安全更新、干净卸载,这些好处编译全都要自己扛。

什么时候需要源码编译

是什么

源码编译 = 拿到软件的 C/C++ 源代码,在你自己的机器上把它「翻译」成可执行文件。只有三种场景值得这么做:

  1. apt 源里没有这个软件,也没有官方 apt 源;
  2. 源里的版本太老,生产需要新版本的特性或安全修复;
  3. 需要自定义编译选项,比如给 nginx 加第三方模块、裁掉用不到的功能。

其余情况永远优先 apt。版本策略详见 17 包管理。

怎么做

动手编译前,先花一分钟确认真的需要:

# 查 apt 源里有没有这个软件、候选版本是多少
$ apt policy <软件名>

版本满足需求就 sudo apt install <软件名>,本章到此结束;不满足再往下走。

参数说明

四种获取方式对比,理解优先级为什么这么排:

获取方式 版本新鲜度 维护成本 适用场景
apt 官方源 落后但稳定 最低 默认选择
软件官方第三方源(如 nodesource、pgdg) 新 低 官方提供时优先于编译
官方二进制包(现成的可执行文件) 新 中 编译太慢、机器环境太素
源码编译 任意、可定制 最高 前三条都走不通

验证方法

$ apt policy nginx
预期输出:
nginx:
  Installed: (none)
  Candidate: 1.18.0-6ubuntu14.5
  Version table:
     1.18.0-6ubuntu14.5 500
        500 http://archive.ubuntu.com/ubuntu jammy-updates/main amd64 Packages

Candidate 是 apt install 会装上的版本。若它低于你的要求且找不到官方第三方源,才进入编译路线。

常见问题

现象 根因 解决
源里的版本落后一大截 Ubuntu LTS 冻结大版本,只推安全补丁 先找软件官方第三方源,再考虑编译
装了新版,apt upgrade 又被降回去 源优先级冲突 见 17 包管理 的 apt pinning
「教程都说要编译」 多为过时经验 先确认该教程年代,新系统通常有官方源了

前置依赖:编译工具链与 -dev 包

是什么

编译需要两类材料:

  • 工具链:gcc(C 编译器)、g++(C++ 编译器)、make(按清单指挥编译)。Ubuntu 把它们打包成 build-essential,一键装齐;
  • 开发库:软件要用到加密(OpenSSL)、压缩(zlib)等现成功能,编译时需要读这些库的头文件(.h)和链接文件。Ubuntu 把它们打成 libssl-dev 这类 -dev 包 —— -dev 包 = 头文件 + 链接库,专供编译用;对应的运行时包只含程序跑起来必需的 .so 文件。

这就解释了常见困惑:「我明明装了 libssl,怎么还编译失败?」—— 你缺的不是「能运行」的部分,而是「能编译」的部分。

怎么做

# 安装编译工具链,gcc/g++/make 一次到位
$ sudo apt install build-essential

# 安装最常见的几个开发库和构建辅助工具
$ sudo apt install libssl-dev zlib1g-dev libffi-dev pkg-config

参数说明

包 提供什么 哪些软件编译需要
build-essential gcc、g++、make、libc 头文件 几乎所有 C/C++ 软件
libssl-dev OpenSSL 头文件与库 一切涉及 https/TLS 的软件
zlib1g-dev zlib 压缩库 nginx、Python 等
libffi-dev 外部函数接口库 Python(CPython 硬依赖)
pkg-config 查询已装库的编译参数 configure 阶段定位依赖库

验证方法

# 确认工具链就位,顺便记下 gcc 版本
$ gcc --version | head -1
$ make --version | head -1
预期输出:
gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0
GNU Make 4.3

两条都有版本号输出,工具链可用。截至 2026-09,Ubuntu 22.04 默认 gcc 为 11.4。

常见问题

现象 根因 解决
C compiler cannot create executables gcc 缺失或工具链不完整 sudo apt install --reinstall build-essential
报找不到某个 .h 文件 缺对应的 -dev 包 apt search <关键词> 找到 dev 包安装
装了 -dev 还是找不到库 pkg-config 缺失或缓存未更新 装 pkg-config 后重新 ./configure

标准三步:configure、make、make install

是什么

几乎所有 C/C++ 开源软件都遵循同一套流程:

  1. ./configure —— 探测你的系统(编译器在吗、依赖库齐吗),按你给的选项生成 Makefile(编译清单);
  2. make —— 按 Makefile 真正编译,产物生成在源码目录里,此时还没装进系统;
  3. sudo make install —— 把编译好的文件复制到安装位置。

怎么做

# 解压源码包并进入目录(以 foo 1.2.3 为例,最新版本以官方发布页为准)
$ tar xf foo-1.2.3.tar.gz
$ cd foo-1.2.3

# 第一步:配置,指定独立安装目录
$ ./configure --prefix=/opt/<软件名>

# 第二步:按 CPU 核数并行编译,nproc 输出核数
$ make -j$(nproc)

# 第三步:把编译产物复制到 --prefix 指定的目录
$ sudo make install

--prefix=/opt/<软件名> 为什么重要:所有文件(bin、lib、share)都收进这一个独立目录,不散落进 /usr;卸载等于删目录;多版本还能并存(/opt/foo-1.2 与 /opt/foo-1.3)。这是编译路线唯一的「后悔药」,不写它才是真正的坑。

altinstall 的意义:以 Python 为例,sudo make install 会安装 python3 并可能覆盖系统默认链接,而 apt 的一堆系统工具依赖默认 Python,一旦被顶掉,系统工具集体报错。sudo make altinstall 安装为 python3.12 这种带版本号的名字,不碰系统默认链接,是编译 Python 的唯一正确姿势。完整教训见 21 Python。

参数说明

命令 / 参数 作用
./configure --prefix=<路径> 探测环境并生成 Makefile,指定安装根目录
make -j$(nproc) 并行编译,任务数 = CPU 核数,大幅提速
sudo make install 复制产物到 prefix,写 /opt 需要 root
sudo make altinstall 安装但不注册为默认版本名(Python 必用)
./configure --help 查看该软件支持的全部编译选项

验证方法

安装前先验证产物可用(详见下一节),安装后确认:

# 直接执行 prefix 下的二进制验证
$ /opt/<软件名>/bin/<软件名> --version
预期输出:
<软件名> version 1.2.3

能正确输出版本号,安装成功。

常见问题

现象 根因 解决
make 报 missing separator Makefile 损坏,常见于解压方式不对或手改过文件 删目录重新解压官方包
configure 通过但 make 失败 某依赖库版本不满足 找第一条 error,通常是缺 -dev 包
make -j 机器卡死 并行任务太多,内存耗尽 减小 -j 数值(如 -j2),见「编译失败排查」

安装前先验证产物

是什么

make 成功 ≠ 能用。编译产物在装进系统之前,就能直接在源码目录里运行验证 —— 这一步能提前暴露「编出来了但跑不起来」的问题(典型:动态库缺失),避免把坏产物装进 /opt 之后才发现要重来。

怎么做

产物位置因软件而异,常见在源码根目录或 src/ 子目录:

# 两种常见位置,试运行验证
$ ./src/<软件名> --version
$ ./<软件名> --version

# 若要推倒重来,先清理上次产物
$ make clean

参数说明

操作 作用
make clean 清掉上次编译产物,从干净状态重编
make distclean 连 configure 生成的 Makefile 一并清掉,回到解压原始态
find . -maxdepth 2 -type f -perm -u+x -newer configure 找出 configure 之后新生成的可执行文件,即产物

验证方法

# 在源码目录里直接跑产物
$ ./src/<软件名> --version
预期输出:
<软件名> version 1.2.3

能输出版本号 = 二进制完整可执行;若报 error while loading shared libraries,先别装,直接跳到下一节解决动态库问题。

常见问题

现象 根因 解决
./src/ 下没有产物 该软件产物放在别的子目录 用上表 find 命令定位
运行时报库缺失,但 make 没报错 编译期链接通过、运行期找不到库,两者机制不同 见下一节三种解法
--version 报非法选项 老软件用 -V 或 -v 看 ./<软件名> --help

动态库:装完跑不起来的头号原因

是什么

Linux 程序运行时要动态加载共享库(.so 文件)。加载器按固定顺序找库:环境变量 LD_LIBRARY_PATH → /etc/ld.so.cache 缓存(由 ldconfig 生成)→ 默认目录 /lib、/usr/lib。

--prefix 装到 /opt/<软件名>/lib 的库不在默认搜索路径里,于是运行时报:

error while loading shared libraries: libxxx.so.1: cannot open shared object file: No such file or directory

库明明装了,只是加载器不知道它在哪 —— 这是自编译软件最常见的报错。

怎么做

三种解法,按推荐顺序:

解法一(首选):系统缺的库,直接用 apt 装。 系统级依赖交给包管理:

# 按报错里的库名搜索提供它的包
$ apt search libxxx

# 安装运行时包(通常叫 libxxx1 这类带数字的名字)
$ sudo apt install libxxx1

解法二(自编译的库):注册进系统库缓存。

# 告诉加载器额外去这个目录找库,一行一个路径
$ echo "/opt/<软件名>/lib" | sudo tee /etc/ld.so.conf.d/<软件名>.conf

# 重建库缓存,立即生效
$ sudo ldconfig

解法三(临时救急):设 LD_LIBRARY_PATH。

# 临时为当前会话追加库搜索路径
$ export LD_LIBRARY_PATH=/opt/<软件名>/lib:$LD_LIBRARY_PATH

为什么不推荐解法三:它只对当前 shell 生效 —— systemd、supervisor、cron 或别的用户启动程序时没有这个变量,「换个方式启动就又跑不起来」;写进全局配置又会影响系统上所有程序,等于给每台进程埋雷。生产环境只认解法一、二。

参数说明

命令 作用
ldd <可执行文件> 列出它依赖的所有 .so 及实际解析路径,not found 就是问题所在
ldconfig -p | grep <关键词> 查缓存里注册了哪些库
/etc/ld.so.conf.d/*.conf 额外库目录配置,一行一个路径,供 ldconfig 使用

验证方法

# 检查所有依赖是否都能解析,无输出即全部就位
$ ldd /opt/<软件名>/bin/<软件名> | grep "not found"
预期输出:
(无任何输出,说明没有缺失的库)

看完整依赖与缓存:

$ ldd /opt/<软件名>/bin/<软件名> | head -5
预期输出:
        libssl.so.3 => /lib/x86_64-linux-gnu/libssl.so.3 (0x00007f...)
        libz.so.1 => /lib/x86_64-linux-gnu/libz.so.1 (0x00007f...)
$ ldconfig -p | grep libssl
预期输出:
        libssl.so.3 (libc6,x86-64) => /lib/x86_64-linux-gnu/libssl.so.3

常见问题

现象 根因 解决
cannot open shared object file 库没装,或装了但不在搜索路径 按上文解法一 → 二 → 三依次处理
库文件明明存在,ldd 仍报 not found 目录没注册进缓存 写 ld.so.conf.d 配置后 sudo ldconfig
装的是 libxxx.so,程序要 libxxx.so.1 无版本号的 .so 是 -dev 包给编译用的;运行时加载带版本号的 安装对应运行时包(如 libxxx1)

卸载困境

是什么

apt remove 依赖安装记录,卸得干净;make install 不留任何记录 —— 它只是把文件复制过去,系统不知道装了什么、装到了哪。这就是编译安装「装时一时爽,卸时火葬场」:

  • 部分 Makefile 提供 make uninstall,靠的是安装时生成的清单文件,前提是源码目录还在且 configure 时的参数没变;
  • 不支持卸载的,只能手动找文件删:漏删留僵尸文件,删错伤系统文件。

—— 这正是上一节强调 --prefix 隔离的价值:整个软件收在一个目录里,卸载就是删目录,干净利落。

怎么做

按以下顺序尝试:

# 方案一:回到源码目录执行卸载目标,仅当 Makefile 支持时有效
$ cd <源码目录>
$ sudo make uninstall

# 方案二:prefix 隔离的,直接删目录,卸载完成
$ sudo rm -rf /opt/<软件名>

[!WARNING] rm -rf 不可逆。执行前先 ls /opt/<软件名> 确认目录内容没有误判,并核对完整路径后再回车 —— 手滑少一个字符可能删掉完全不同的目录。

参数说明

方案 前提 干净程度
make uninstall 源码目录还在,且 Makefile 支持 uninstall 目标 高
删除 prefix 目录 当初 configure 用了 --prefix 高
手动 find 全盘找文件删 无 低,易留残渣、易误删

冷门但实用的技巧:源码目录已删、又想干净卸载时,重新解压同版本源码,用与当初相同的 --prefix 执行 configure + make install,系统就重新有了安装清单,再 make uninstall 即可。

验证方法

# 卸载后确认系统里已无此软件
$ which <软件名>
$ ls /opt/<软件名>
预期输出:
which: no <软件名> in (/usr/local/sbin:...)

ls: cannot access '/opt/<软件名>': No such file or directory

两条都「找不到」,卸载完成。

常见问题

现象 根因 解决
make uninstall 报 No rule to make target 该软件 Makefile 不支持卸载 删 prefix 目录
源码目录早删了,uninstall 没法跑 卸载依赖安装清单 重新解压同版本源码,相同 prefix 装一遍再 uninstall
卸载后别的程序坏了 那个程序依赖你卸掉的库 ldd 排查依赖方,重装该库或改用 apt 版

编译失败通用排查

是什么

编译失败千奇百怪,但 90% 落进三类:缺头文件、编译器太老、内存不足。认得这三类的报错长相,大部分失败能自己定位,不必逐条搜索。

怎么做

套路一:缺头文件。 报错特征是 fatal error: xxx.h: No such file or directory:

# 按头文件所属的库名搜索并安装对应 dev 包
$ apt search libssl | grep dev
$ sudo apt install libssl-dev

套路二:GCC 太老。 软件要求 GCC 12+,而 Ubuntu 22.04 默认 11:

# 官方源里装新版编译器
$ sudo apt install gcc-12 g++-12

# configure 时临时指定编译器
$ CC=gcc-12 CXX=g++-12 ./configure --prefix=/opt/<软件名>

套路三:内存不足(OOM,out of memory)。 编译器进程被系统杀掉,日志里常见 Killed 或 internal compiler error。内存不足先降并行度,还不行就临时加 swap:

# 4GB 以下小内存机器,把并行度降到 2
$ make -j2
# 仍失败则临时加 2G swap,编译完即删
$ sudo fallocate -l 2G /swapfile
$ sudo chmod 600 /swapfile
$ sudo mkswap /swapfile
$ sudo swapon /swapfile
# 编译结束后回收临时 swap
$ sudo swapoff /swapfile
$ sudo rm /swapfile

参数说明

报错信号 判断 处理
fatal error: xxx.h 缺头文件 装对应 -dev 包
大量 C++ 模板报错、does not name a type GCC 版本过老 装新版 GCC,CC= 指定
Killed / virtual memory exhausted 内存不足 降 -j,或临时加 swap
C compiler cannot create executables 工具链缺失或损坏 重装 build-essential 与 libc6-dev

验证方法

重编时把完整输出存档,失败时只看第一条 error —— 后面的报错多为连锁反应:

# 编译输出同时写入日志文件,便于回查第一条 error
$ make -j$(nproc) 2>&1 | tee /tmp/build.log
预期输出:
(编译过程滚动输出,末尾无 Error 1 字样并回到提示符即成功)

常见问题

现象 根因 解决
C compiler cannot create executables gcc 缺失/损坏,或缺 libc 开发文件 sudo apt install --reinstall build-essential libc6-dev
Makefile:8: *** missing separator. Stop. Makefile 被破坏,或复制粘贴时制表符变空格 重新解压源码包,不要手改 Makefile
装完执行 command not found prefix 的 bin 目录不在 PATH export PATH=/opt/<软件名>/bin:$PATH 并写入 ~/.profile。注意两者加载时机不同(00 章有对照表):登录 shell 读 ~/.profile,非登录交互 shell 只读 ~/.bashrc;登录与非登录都覆盖的最稳做法是两处都写,或只写 ~/.bashrc(Ubuntu 22.04 默认的 ~/.profile 会 source 它)
运行时 cannot open shared object file 动态库路径问题 用上文三种解法
RHEL / Rocky / AlmaLinux 9 对照
Ubuntu (apt) RHEL (dnf) 说明
sudo apt install build-essential sudo dnf group install "Development Tools" 编译工具链一组装齐
libssl-dev openssl-devel Ubuntu 叫 -dev,RHEL 叫 -devel
zlib1g-dev zlib-devel 同上
libffi-dev libffi-devel 同上
apt search libxxx dnf provides "*/libxxx*" 按文件/库名反查所属包

本章命令速查

命令 作用
sudo apt install build-essential 安装 gcc/g++/make 工具链
sudo apt install libssl-dev zlib1g-dev libffi-dev pkg-config 安装常用开发库
apt policy <软件名> 查源里的候选版本,判断是否需要编译
./configure --prefix=/opt/<软件名> 配置并指定独立安装目录
./configure --help 查看该软件全部编译选项
make -j$(nproc) 按 CPU 核数并行编译
sudo make install 安装到 prefix 目录
sudo make altinstall 安装但不覆盖系统默认版本名(Python 必用)
./src/<软件名> --version 安装前先在源码目录验证产物
make clean / make distclean 清理编译产物 / 连 Makefile 一起清
ldd <二进制> 查看动态库依赖与缺失
ldconfig -p | grep <库> 查库缓存
echo <路径> | sudo tee /etc/ld.so.conf.d/<软件名>.conf 注册自编译库目录
sudo ldconfig 重建库缓存
sudo make uninstall 源码卸载(仅部分软件支持)
CC=gcc-12 ./configure ... 指定新版编译器
make -j2 低内存机器降低并行度

相关章节

📄 进程守护方案选型(systemd / supervisor / PM2) 42-process-supervision.md


title: 42 进程守护方案选型 updated: 2026-09 applies-to: Ubuntu 22.04 LTS

应用跑起来只是第一步,让它「一直活着」才是生产环境的要求。本章回答三个问题:进程为什么会丢、三种主流守护方案怎么选、以及 supervisor 和 PM2 的落地配置。systemd 的完整教程在上一章 40 systemd,本章不重复。

为什么需要进程守护

是什么

不配守护的进程,常见三种「死亡方式」:

  1. SSH 会话退出:终端里前台跑的进程随会话被杀(shell 退出时向子进程发 SIGHUP);
  2. 程序自己崩溃:内存溢出、未捕获异常、依赖服务闪断后自己退出;
  3. 服务器重启:内存里的一切进程清零。

nohup <命令> & 只解决场景 1 —— 它让进程「不随会话退出」,但程序崩了没人拉起、服务器重启后一切归零。nohup 让你「不立刻死」,守护方案让你「死了能复活、重启能回来」,这是两个层次的需求。

怎么做

先认清目标进程的「裸奔」现状:

# 看进程的父进程是谁,PPID 是判断谁在管它的关键
$ ps -o pid,ppid,cmd -p <PID>

PPID 是 1(由 systemd 或 init 直接收养)且没有对应的守护配置,它挂了就没人管。

参数说明

需求层级 手段 覆盖场景
不随会话退出 nohup / & 只防 SSH 退出
崩溃自动拉起 守护方案的 autorestart / Restart 防崩溃
开机自动恢复 守护方案的 enable / autostart 防重启丢失
日志收集、状态查看、批量管理 守护方案自带 生产可运维

验证方法

亲手体会 nohup 的能力边界:

# 用 nohup 放一个测试进程到后台
$ nohup sleep 3600 &
预期输出:
[1] 12345
nohup: ignoring input and appending output to 'nohup.out'

退出 SSH 重连后 ps -p 12345 进程还在 —— 场景 1 被覆盖;然后:

# 手动杀掉它,观察有没有任何东西把它拉起来
$ kill -9 12345
$ ps -p 12345
预期输出:
PID TTY          TIME CMD
[进程消失,无输出]

进程死了就死了,没有任何机制拉起 —— 场景 2、3 依然裸奔,这就是必须上守护方案的原因。

常见问题

现象 根因 解决
nohup 的进程重连后不见了 不是被杀,是程序自己崩了 查应用日志,交给守护方案托管
nohup.out 疯狂增长 程序 stdout 全写进了它 换守护方案,由它接管日志(记得配轮转)
& 放后台,关终端还是被杀 shell 退出时向任务发 SIGHUP 用 nohup,或直接上守护方案

三种方案对比与选型

是什么

三款主流守护方案,定位各不相同:

维度 systemd supervisor PM2
维护方 系统内置,无额外依赖 第三方软件(Python 编写,apt 安装) Node.js 生态,npm 安装
配置方式 unit 文件(ini 风格) conf.d/ 下的 ini 片段 命令行为主,可选 JS/JSON 配置文件
日志 自动进 journald,journalctl -u 查 写文件,需自配 logrotate 写文件,pm2 logs 查,需自配轮转
开机自启 enable 原生支持 supervisor 本身由 systemd 拉起 pm2 startup 生成 systemd unit
适用语言 任意可执行程序 任意可执行程序 Node.js 为主(也能守护任意程序)
学习成本 中(需理解 unit 语法) 低,配置直观 低,Node 开发者零门槛
管理界面 无(有第三方 Webmin 等) 内置 Web 界面(需手动开启) PM2 Plus(云端,商业化)
生产推荐度 系统级服务首选 多语言团队常选 Node.js 项目标准方案

怎么做

按场景决策:

  • 系统级服务(MySQL、nginx、自研后端)一律 systemd:零额外依赖,与开机流程、journald 日志、安全策略集成最好,是 Ubuntu 的「母语」;
  • 多语言混合团队(Java + Go + Python 共存)、或需要给非命令行同事一个 Web 管理页 → supervisor:所有应用一份统一配置格式,上手快;
  • Node.js 项目 → PM2:独家 cluster 模式自动起多个进程吃满多核,pm2 reload 零停机重启,与 22 Node.js 的生态无缝衔接。

[!WARNING] 同一台机器上,一个进程只允许一种守护方案管理。 若 systemd 的 unit 和 supervisor 的 program 配了同一个进程,两边都带自动拉起,就会出现「A 拉起 B 杀的、B 拉起 A 杀的」循环互殴:进程反复重启、日志交错混乱、端口冲突,极难排查。接管前先确认旧方案已 stop + disable。

参数说明

不确定时按这张表对号入座:

你的情况 推荐 理由
单机少量服务,运维就是你自己 systemd 不装新东西,教程与生态最全
多语言应用统一纳管、要 Web 界面 supervisor 配置格式统一,supervisorctl 心智负担低
Node.js 项目,要多核与零停机发布 PM2 cluster 与 reload 是独有能力

验证方法

接手一台机器时,先查清现有的守护格局,避免混用:

# 是否已有 supervisor 在运行
$ systemctl is-active supervisor
预期输出:
inactive
# 是否已有 PM2 管理的进程(以部署用户身份执行)
$ pm2 list
预期输出:
[PM2] Spawning PM2 daemon
[PM2] PM2 Successfully daemonized
┌────┬──────────┬─────────┬─────────┬───────────┬──────────┐
│ id │ name     │ status  │ restart │ uptime    │ memory   │
└────┴──────────┴─────────┴─────────┴───────────┴──────────┘

空列表或 inactive,说明该方案尚未启用,新服务可自由选型;若已有进程在册,新增服务沿用同一方案。

常见问题

现象 根因 解决
进程被反复重启,日志时间戳交错 两套守护同时管它 只保留一套,另一套停用并删配置
分不清进程归谁管 多方案并存 ps -o ppid= -p <PID> 看父进程:1 通常是 systemd,supervisord 的子进程 PPID 是 supervisord 的 PID
团队成员各用各的方案 无团队约定 部署文档写明「本机唯一守护栈」,新增服务先查现状

supervisor:安装与配置

是什么

supervisor 由 Python 编写:一个 supervisord 守护进程统一管理 N 个子进程,supervisorctl 客户端负责指挥。配置直观、上手快。注意它的定位:supervisor 自己也是一个被 systemd 管理的普通服务(下一节验证),它是「包工头」,systemd 才是「老板」。

怎么做

# 安装 supervisor,截至 2026-09,Ubuntu 22.04 源内为 4.x 系列
$ sudo apt install supervisor
# 为应用新建配置文件,conf.d 下所有 .conf 都会被加载
$ sudo nano /etc/supervisor/conf.d/myapp.conf
[program:myapp]
; 启动命令,可执行文件必须写绝对路径
command=/usr/bin/java -jar /opt/myapp/app.jar
; 工作目录,应用读相对路径的基准
directory=/opt/myapp
; 以哪个用户运行,不要用 root
user=appuser
; supervisord 启动时自动拉起该进程(开机自启的载体)
autostart=true
; 进程退出后自动重启
autorestart=true
; 启动后须存活满 1 秒才算启动成功(默认值,这里显式写出);存活不足 1 秒即退出视为启动失败
startsecs=1
; 启动失败最多重试 3 次,超次标记 FATAL
startretries=3
; stdout 日志文件,注意会无限增长,必须配 logrotate
stdout_logfile=/var/log/myapp/out.log
; stderr 日志文件
stderr_logfile=/var/log/myapp/err.log
; 环境变量,多个用逗号分隔
environment=JAVA_OPTS="-Xms512m -Xmx1024m",APP_ENV="prod"
# 让 supervisor 扫描配置变化并应用(两条都要执行)
$ sudo supervisorctl reread
$ sudo supervisorctl update

[!WARNING] supervisor 内置 Web 管理界面,在 /etc/supervisor/supervisord.conf 中配置 [inet_http_server] 段即可开启。绝不要对公网开放 —— 它能启停任意被管进程,等于把服务器控制权暴露出去;只绑 127.0.0.1 或内网地址,并配好账号密码。

参数说明

键 作用 注意事项
command 启动命令 不要带 nohup、不要加 &,supervisor 自己管前台进程
directory 工作目录 应用读相对路径全靠它
user 运行身份 日志目录、应用目录都要给该用户权限
autostart supervisord 启动时拉起 开机自启依赖它(supervisor 由 systemd 拉起)
autorestart 挂了自动拉起 可设 unexpected(仅异常退出才拉)
startretries 连续失败几次后标记 FATAL FATAL 后不再重试,需人工介入
stdout/stderr_logfile 日志路径 须配 logrotate 轮转,见本章末节

验证方法

# 查看所有被管进程状态
$ sudo supervisorctl status
预期输出:
myapp                            RUNNING   pid 12345, uptime 0:00:30

RUNNING 且 uptime 持续增长即正常;显示 FATAL 说明反复启动失败,按下节方法排错。

常见问题

现象 根因 解决
FATAL (exit status 1) command 本身跑不起来 以相同用户在相同目录手动执行 command,报错一目了然
配置改了不生效 忘了 reread + update 两条命令缺一不可
日志写不进去 user 指定的用户对日志目录无写权限 sudo mkdir -p /var/log/myapp && sudo chown appuser /var/log/myapp

supervisor:日常操作

是什么

所有日常操作通过 supervisorctl 完成,它连接本机的 supervisord 执行指令 —— 类比:supervisorctl 之于 supervisord,就像 systemctl 之于 systemd。reread/update 的关系也和 daemon-reload 类似:一个重读配置,一个应用变化。

怎么做

# 查看所有被管进程状态
$ sudo supervisorctl status

# 启动 / 停止 / 重启指定程序
$ sudo supervisorctl start myapp
$ sudo supervisorctl stop myapp
$ sudo supervisorctl restart myapp

# 实时跟踪程序 stdout,相当于 tail -f
$ sudo supervisorctl tail -f myapp

# 改过配置文件后,重读并应用
$ sudo supervisorctl reread
$ sudo supervisorctl update

reread 只扫描磁盘配置找出差异(不动作);update 真正应用差异(新增/变更的程序会被启动或重启)。改配置后两条必须都执行。

参数说明

命令 作用
status 列出全部程序及状态(RUNNING / FATAL / STOPPED)
start / stop / restart <程序名> 生命周期操作
tail -f <程序名> 跟踪 stdout 日志
reread 扫描配置变化,只读不动作
update 应用配置变化,会重启受影响程序
reload 重启 supervisord 自身(会短暂中断全部被管进程,慎用)

验证方法

先确认 supervisor 自身归 systemd 管、处于运行中:

$ systemctl status supervisor
预期输出:
● supervisor.service - Supervisor process control system for UNIX
     Loaded: loaded (/lib/systemd/system/supervisor.service; enabled; ...)
     Active: active (running) since Tue 2026-09-29 14:00:00 CST; 3h ago
   Main PID: 890 (supervisord)

再实测守护能力 —— 手动杀进程,看它几秒内被拉起:

# 杀掉 myapp 的进程
$ pkill -f app.jar

# 稍等几秒再看状态
$ sudo supervisorctl status
预期输出:
myapp                            RUNNING   pid 12346, uptime 0:00:05

pid 从 12345 变成 12346、uptime 归零重新计时 —— 自动拉起生效,守护闭环。

常见问题

现象 根因 解决
error: <class 'PermissionError'>, .../supervisor.sock 没加 sudo,连不上管理 socket 命令前加 sudo
update 后程序毫无变化 reread 判定配置无差异,改错了文件 确认改的是 conf.d/ 下被加载的 .conf
stop 了进程又自己起来 它同时被 systemd、cron 或别的工具拉起 按本章开头 WARNING 排查并移除重复守护

PM2:Node.js 生态的守护方案

是什么

PM2 是 Node.js 世界的进程管理器,npm i -g pm2 即装即用。对 Node 应用有独门能力:cluster 模式自动按 CPU 核数起多个进程并做负载均衡,pm2 reload 零停机滚动重启。安装与基础用法见 22 Node.js,本节只补守护链路的关键一环:开机自启的原理与操作。

怎么做

# 生成开机自启配置,会输出一条需要你执行的 sudo 命令
$ pm2 startup

# 按提示原样执行输出的那行 sudo env PATH=... pm2 startup ... 命令

# 把当前进程列表存盘,作为开机恢复的依据
$ pm2 save

原理:pm2 startup 生成一个 systemd unit(命名如 pm2-<用户名>.service),开机时由 systemd 拉起 pm2 守护进程;pm2 save 把当前进程快照写入 ~/.pm2/dump.pm2,pm2 启动时按快照恢复全部应用。所以本质仍是 systemd 负责开机链路,PM2 是第二层管家 —— 这也再次说明:任何守护方案最终都落在 systemd 之上。

参数说明

命令 作用
pm2 startup 生成开机自启 systemd unit,输出一条待执行的 sudo 命令
pm2 save 快照当前进程列表,供开机恢复
pm2 resurrect 手动按快照恢复进程
pm2 list 查看全部被管进程
pm2 logs <应用名> 查看应用日志
pm2 restart <应用名> 硬重启,有瞬断
pm2 reload <应用名> 零停机滚动重启(cluster 模式下)

验证方法

# 确认开机自启 unit 已生成并启用
$ systemctl list-unit-files | grep pm2
预期输出:
pm2-root.service                          enabled enabled

服务名中的 root 换成你执行 pm2 startup 的用户名,enabled 即开机生效。

# 确认快照已保存
$ pm2 save
预期输出:
[PM2] Saving current process list...
[PM2] Successfully saved in /root/.pm2/dump.pm2

常见问题

现象 根因 解决
重启服务器后应用没回来 配完进程忘了 pm2 save,快照是空的或旧的 每次增删改进程后都执行 pm2 save
pm2 startup 后开机仍不自启 输出的 sudo 命令只是模板,没有实际执行 把提示的那行命令原样复制执行
pm2 logs 看不到应用输出 应用自己把日志写进了别的文件 检查应用日志配置,或统一交给 PM2 接管 stdout

日志轮转:别让日志撑爆磁盘

是什么

supervisor 的 stdout_logfile 和 PM2 的日志文件都会无限增长 —— 一个高流量应用几周就能写满几十 GB,最终撑爆磁盘拖垮整机。轮转(logrotate)按天/按大小把旧日志改名、压缩、超期删除,是生产必配项。logrotate 完整教程见 54 logrotate。

怎么做

为 supervisor 应用的日志建一条轮转规则:

# 新建轮转规则文件,文件名随意,放对目录即生效
$ sudo nano /etc/logrotate.d/myapp
/var/log/myapp/*.log {
    daily
    rotate 7
    compress
    missingok
    notifempty
    copytruncate
}

其中 copytruncate 对 supervisor 场景尤其关键:logrotate 的常规做法是「改名旧文件、让进程重开新文件」,但 supervisor 不会重开日志句柄,会继续往改名后的旧文件里写;copytruncate 改为「复制旧内容后把原文件清空」,进程的句柄始终有效,不丢日志。

PM2 一句带过:pm2 flush 手动清空全部日志;自动轮转安装模块 pm2 install pm2-logrotate 即可,无需写规则文件。

参数说明

指令 作用
daily 每天轮转一次
rotate 7 保留 7 份,更旧的自动删除
compress 轮转出的旧日志 gzip 压缩
copytruncate 复制后清空原文件,适配不重开日志句柄的进程
missingok 日志文件不存在时不报错
notifempty 空文件不轮转

验证方法

# 强制立即轮转一次,验证规则
$ sudo logrotate -f /etc/logrotate.d/myapp

# 查看轮转结果
$ ls -lh /var/log/myapp/
预期输出:
-rw-r--r-- 1 appuser appuser    0 Sep 29 17:00 out.log
-rw-r--r-- 1 appuser appuser 120K Sep 28 23:59 out.log.1.gz

原文件被清空(大小 0)、旧内容进了 .1.gz 压缩包 —— 轮转生效,且进程日志未中断。

常见问题

现象 根因 解决
轮转后日志仍写进旧文件 进程不重开日志句柄,规则没用 copytruncate 规则里加 copytruncate
磁盘还是被日志吃满 单日日志量太大,按天轮转来不及 加 maxsize 100M,与 daily 并用即可「时间或大小先到先切」。注意前提:logrotate 默认每天只运行一次,要让 maxsize 在当天内生效,需把规则挂到 cron.hourly 或配置更高频的 timer(与 54 logrotate 口径一致)
PM2 日志巨大无从下手 未配置轮转 pm2 install pm2-logrotate,或 pm2 flush 应急清空

本章命令速查

命令 作用
sudo apt install supervisor 安装 supervisor
sudo supervisorctl status 查看被管进程状态
sudo supervisorctl start|stop|restart <程序> 程序生命周期操作
sudo supervisorctl tail -f <程序> 实时跟踪程序输出
sudo supervisorctl reread 扫描配置变化(只读)
sudo supervisorctl update 应用配置变化(改配置后必做)
systemctl status supervisor 确认 supervisord 本身在运行
pm2 startup 生成开机自启 systemd unit
pm2 save 快照进程列表供开机恢复
pm2 list 查看 PM2 全部进程
pm2 logs <应用名> 查看 PM2 应用日志
pm2 reload <应用名> 零停机重启(cluster 模式)
pm2 flush 清空 PM2 日志
pm2 install pm2-logrotate 安装 PM2 日志轮转模块
sudo logrotate -f /etc/logrotate.d/<应用名> 手动触发一次日志轮转
ps -o ppid= -p <PID> 判断进程归哪个守护方案管

相关章节

📄 运维工具箱 43-ops-toolbox.md


title: 43 运维工具箱 updated: 2026-09 applies-to: Ubuntu 22.04 LTS

前面章节讲了怎么「装」和怎么「跑」,这一章讲怎么「查」。服务器出问题时,你需要在几分钟内回答四个问题:CPU 谁在吃?内存谁在占?磁盘谁在写?端口谁在用? 本章的工具箱就是为这四个问题准备的,每个工具都围绕「什么时候用、怎么用、看到什么算正常」来讲。

[!IMPORTANT] 本章工具版本均以 Ubuntu 22.04 官方源为准,截至 2026-09。所有工具都可以 sudo apt install 直接安装,不需要添加第三方源。

htop / btop:交互式资源监控

是什么

top 是 Linux 自带的资源监控,但界面原始、操作别扭。htop 是它的现代化替代:彩色显示、鼠标可点、按键排序、直接杀进程。btop 界面更华丽,还带网络和磁盘图表,二者选一个用顺手即可,新手推荐先学 htop。

为什么运维离不开它?因为「服务器卡了」的第一反应永远是打开 htop,30 秒内就能看出是 CPU、内存还是负载的问题,再决定下一步用什么工具深挖。

怎么做

# 安装 htop(需要管理员权限)
$ sudo apt install htop

# 启动,直接全屏交互界面
$ htop

常用交互键(在 htop 界面内直接按,不用回车):

按键 作用
F6 或 > 选择排序列(默认按 CPU)
M 立刻按内存排序
P 立刻按 CPU 排序
F5 树形视图,看清谁是谁的子进程
/ 搜索进程名
F9 杀进程(先选中,再选信号,一般选 15 SIGTERM)
q 退出

验证方法

# 查看版本号,确认安装成功(交互界面不适合脚本,--version 适合)
$ htop --version
预期输出:
htop 3.0.5

[!TIP] 内存条区域的颜色有讲究:绿色是正常使用,蓝色是缓冲,黄色是缓存,红色越来越满才需要担心。只看百分比会误判,Linux 会把空闲内存拿去做缓存,「看起来满了」不等于「内存不足」。

iotop:看谁在写磁盘

是什么

CPU 和内存忙不忙看 htop 就行,但「磁盘在狂转、IO 等待飙高,却不知道是哪个进程干的」就得用 iotop。它把每个进程的实时读写速度列出来,和 htop 的关系是:htop 看 CPU/内存,iotop 看磁盘。

怎么做

# 安装
$ sudo apt install iotop

# -o 只显示正在产生 IO 的进程,不刷一屏 0
# -P 按进程统计(默认按线程)
# -a 显示累计读写量,而不是瞬时速度
$ sudo iotop -oPa
参数 含义
-o 只显示有 IO 的行,界面干净
-P 按进程聚合(不加则按线程,行数爆炸)
-a 累计量模式,便于发现「总量惊人」的进程
-d 2 刷新间隔 2 秒

验证方法

# 人为制造一点磁盘 IO,再开 iotop -o 就能看到 dd 这个进程
$ dd if=/dev/zero of=/tmp/testfile bs=1M count=500
预期输出(iotop -oPa 中出现类似行):
  2234 be/4 root      0.00 B/s    0.00 B/s     0.00 %     0.36 % dd if=/dev/zero of=/tmp/testfile bs=1M count=500

ncdu:交互式磁盘空间分析

是什么

磁盘满了,第一反应是 du -sh /* 一层层猜。ncdu 把这个过程变成交互式界面:自动扫描、按大小排序、目录可进入可展开,还能直接删除。磁盘排错的效率完全不是一个量级,详见 55 磁盘满排查。

怎么做

# 安装
$ sudo apt install ncdu

# 从根目录扫描(全盘扫描需要几分钟,也可以只扫某个可疑目录)
$ sudo ncdu /
按键 作用
上下方向键 / 回车 进入/退出目录
d 删除选中的文件或目录
n 按名字排序(再按切回大小排序)
g 切换百分比/图形条显示
q 退出

[!WARNING] ncdu 里的 d 键是直接删除,不进回收站,不可恢复。在扫描结果里手滑按到 d,选中的目录就没了。生产机上建议只看不动手,确认清楚再删;或者改用「先扫描、记录、再单独 rm」的流程。

验证方法

# 只扫描 /var,通常日志堆在这里,扫描范围小、速度快
$ sudo ncdu /var
预期输出:
ncdu 1.15.1 ~ Use the arrow keys to navigate, press ? for help
--- /var -----------------------------------------------------------------------
  912.0 MiB [##########] /lib
  483.7 MiB [#####     ] /log
  ...
Total disk usage:   2.1 GiB   Apparent size:   2.1 GiB   Items: 23456

lsof:谁在用这个文件/端口

是什么

lsof(list open files)回答一个其他工具回答不了的问题:「这个文件/端口/目录,现在被哪个进程占着?」Linux 一切皆文件,所以端口、日志、甚至已删除的文件都能查。它是本章性价比最高的工具 —— 一条命令就能定位「Address already in use」「磁盘满但找不到大文件」这类经典问题。

怎么做

# 谁占用了 8080 端口(最常用的场景:启动服务报端口被占)
$ sudo lsof -i:8080

# 某用户打开的所有文件
$ lsof -u www-data

# 哪个进程打开了这个日志文件(判断日志是否还在被写入)
$ sudo lsof /var/log/syslog

# 某个目录下被打开的所有文件(慢,只对小目录用)
$ sudo lsof +D /var/www
参数 含义
-i:端口 查占用该端口的进程,服务部署排错首选
-i tcp 查所有 TCP 连接
-u 用户 按用户过滤
+D 目录 递归查目录,量大时慢
` grep deleted`

重点场景:已删除但仍被占用的文件

这是磁盘排查的经典悬案:df -h 显示磁盘满了,du -sh /* 加起来却小很多。根因通常是——某个大日志文件被删了,但写它的进程还没重启,文件句柄没释放,磁盘空间不归还。用下面的命令一锤定音:

# 找出所有「已删除但仍被进程打开」的文件
$ sudo lsof | grep deleted
预期输出:
java      1234  appuser   12w   REG   8,1   21474836480   5242890 /var/log/app/app.log (deleted)

看到的 21474836480 就是 20 GB 空间悬空的元凶。解决办法是重启对应进程(这里是 systemctl restart 应用服务),空间立刻归还。

[!NOTE] 这个场景的完整排查流程(du 和 df 对不上的处理)在 55 磁盘满排查 有详细展开。

验证方法

# 确认 8080 被占用时,能输出进程名和 PID 就说明命令生效
$ sudo lsof -i:8080
预期输出:
COMMAND   PID   USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
java     1234 appuser   45u  IPv6  45210      0t0  TCP *:8080 (LISTEN)

strace:进程系统调用跟踪

是什么

程序「卡住了」「启动就退出」「日志什么都不打」,常规手段无能为力时,strace 是终极手段。它能看到进程正在进行的每一次系统调用——读写文件、网络连接、锁等待,等于给进程装了行车记录仪,一句话定位「程序到底卡在哪」。

怎么做

# 附着到一个正在卡住的进程,实时看它在干什么(Ctrl+C 停止观察)
$ sudo strace -p 1234

# 跟踪一个新命令,只关注打开文件和网络连接两类调用
$ strace -f -e trace=openat,connect ls /tmp

# 统计模式:跑完后给出每类系统调用的耗时排名
$ strace -c curl -s http://example.com > /dev/null
参数 含义
-p PID 附着到已运行的进程
-f 跟踪子进程(服务类程序必加)
-e trace=xxx 只看指定调用,减少噪音
-c 统计汇总,不逐条输出
-T 显示每次调用耗时,超过 1 秒的行就是卡点

看输出的经验法则:

  • 卡在 connect(...) 不返回 → 网络问题(对端不通或防火墙丢包);
  • 卡在 futex(...) → 锁等待(程序内部死锁或资源竞争);
  • 卡在 read(...) 且 fd 指向 socket → 在等对端响应,问题多半在对方;
  • 大量 EAGAIN/EACCES → 权限或资源限制问题。

验证方法

# 用一个必失败的连接验证 strace 能否抓住网络错误
$ strace -e trace=connect nc -zv 192.0.2.1 9999
预期输出(能看到 connect 返回错误):
connect(3, {sa_family=AF_INET, sin_port=htons(9999), sin_addr=inet_addr("192.0.2.1")}, 16) = -1 ETIMEDOUT (Connection timed out)

[!WARNING] strace 会显著拖慢被跟踪进程的速度(几十倍),只用于排错,绝不要长期附着在生产进程上。排错完记得 Ctrl+C 分离。

tcpdump:抓包分析

是什么

「端口是通的,但返回的内容不对」——ping 通、nc 通、应用层却异常,这时候要看网络里实际传输的字节。tcpdump 就是命令行抓包工具,把网卡上的数据包原样抓给你看。更复杂的图形化分析,可以先用 -w 存成 pcap 文件,再用 Wireshark 打开。

怎么做

# -i any 监听所有网卡
# -nn 不解析主机名和端口名,输出快且不受 DNS 影响
# -c 100 抓满 100 个包自动退出
$ sudo tcpdump -i any port 8080 -nn -c 100
参数 含义
-i any 监听所有网卡(不确定流量走哪块网卡时用它)
-nn 禁止 DNS/服务名反解,IP 和端口原样显示
-c N 抓 N 个包后停止,避免刷屏
-w file.pcap 写入文件供 Wireshark 分析
port 8080 抓包过滤条件,类似语法还有 host x.x.x.x、src/dst

典型场景:服务监听正常(ss -lntp 能看到),客户端却收到奇怪响应,抓包看是「三次握手成功后,应用返回了 RST/错误内容」,就说明问题不在网络而在应用本身。

验证方法

# 一次终端抓包,另一个终端 curl localhost:8080,能看到包就正常
$ sudo tcpdump -i any port 8080 -nn -c 4
预期输出:
tcpdump: data link type LINUX_SLL2
listening on any, link-type LINUX_SLL2 (Linux cooked v2), snapshot length 262144 bytes
12:00:01.123456 lo    In  IP 127.0.0.1.52844 > 127.0.0.1.8080: Flags [S], seq 123456, win 65495, length 0
12:00:01.123478 lo    Out IP 127.0.0.1.8080 > 127.0.0.1.52844: Flags [S.], seq 654321, ack 123457, win 65483, length 0
4 packets captured

[!TIP] 抓包文件会包含明文数据,可能含密码和 token,不要随手把 pcap 上传到公开平台。

fail2ban:看封禁情况与日常运维

是什么

18 SSH 加固 已经讲了 fail2ban 的安装与配置,本章只讲装好之后的日常:谁被 ban 了?误封怎么解?封禁动作去哪查?这三件事是运维阶段的高频操作。

怎么做

# 看整体状态:启用了哪些 jail
$ sudo fail2ban-client status

# 看 sshd 这个 jail 的封禁情况
$ sudo fail2ban-client status sshd

# 误封了自己/同事的 IP,手动解封
$ sudo fail2ban-client set sshd unbanip 203.0.113.50

status sshd 输出里的 Banned IP list 就是当前封禁名单;所有历史封禁动作(时间、IP、规则)都记录在 /var/log/fail2ban.log,事后回溯「服务为什么连不上」先翻它。

验证方法

$ sudo fail2ban-client status sshd
预期输出:
Status for the jail: sshd
|- Filter
|  |- Currently failed: 2
|  |- Total failed:     148
|  |- File list:        /var/log/auth.log
`- Actions
   |- Currently banned: 3
   |- Total banned:     3
   `- Banned IP list:   198.51.100.7 203.0.113.99 192.0.2.140

tmux / screen:会话保持

是什么

SSH 断线(网络抖动、笔记本休眠、VPN 掉线)时,终端里正在跑的任务会跟着收到 SIGHUP 被杀掉。14 进程管理 讲过 nohup 可以让任务「死不了」,但它有个短板:你回不到那个任务跟前看进度了。

tmux 解决的是「既要活下来,又要回得去」:它把任务跑在一个独立会话里,SSH 断了会话照常运行,重连后 attach 回去,现场原样保留——输出还在滚动,交互程序还在等你输入。这就是「长任务必须放 tmux」的原因。

怎么做

# 安装
$ sudo apt install tmux

# 新建一个名为 work 的会话
$ tmux new -s work

# 在会话里跑一个长任务(示例),然后按 Ctrl+b 再按 d 分离(任务继续跑)
$ sleep 600

# 列出所有会话
$ tmux ls

# 重新接入 work 会话
$ tmux attach -t work

常用按键(都是先按 Ctrl+b 松开,再按下一个键):

按键 作用
Ctrl+b d 分离会话(任务后台继续跑)
Ctrl+b c 新建窗口
Ctrl+b n / p 下一个 / 上一个窗口
Ctrl+b % 左右分屏
Ctrl+b " 上下分屏
Ctrl+b 方向键 在分屏间移动
Ctrl+b [ 进入翻页模式(滚轮/方向键翻历史输出,q 退出)

screen 是更古老的前辈,功能类似(screen -S work / Ctrl+a d / screen -r work),会话保留机制相同。没有特殊理由的话,新项目直接用 tmux。

[!TIP] 养成肌肉记忆:跑大迁移、编译、备份之前,先 tmux new -s xxx,再敲命令。 十秒钟的动作,断网不慌,睡个觉回来 attach 一下看结果。「临时跑一下很快的」是每个运维都吃过亏的错觉。

验证方法

# 分离后列出会话,能看到 work 且状态 attached 变回 detached 即成功
$ tmux ls
预期输出:
work: 1 windows (created Tue Sep 29 12:00:00 2026)

sar / vmstat / iostat:sysstat 套件与历史性能数据

是什么

htop、iotop 都只能看「现在」。但真实事故往往是这样反馈的:「昨天凌晨三点服务特别慢,现在好了,帮我看看当时发生了什么。」sysstat 套件的 sar 就是干这个的:后台定时采样 CPU、内存、磁盘数据,事后回溯任何时间点的系统状态。vmstat 和 iostat 则是同族的实时快照工具。

怎么做

# 安装 sysstat 套件(含 sar、iostat、pidstat 等;vmstat 属于 procps 包,系统自带,无需在此安装)
$ sudo apt install sysstat

# 打开周期采集(Ubuntu 上默认关闭,把 ENABLED 改为 true)
$ sudo sed -i 's/false/true/' /etc/default/sysstat

# 重启采集服务
$ sudo systemctl restart sysstat

# 看今天 CPU 使用率的全天曲线(-u 表示 CPU)
$ sar -u

# 看内存使用(-r)
$ sar -r

# 看磁盘 IO(-d)
$ sar -d

实时观测的两个搭档:

# vmstat 每 1 秒刷新一次,看 si/so 列判断是否在换页(不为 0 说明内存吃紧)
$ vmstat 1

# iostat 每 1 秒刷新,-x 显示扩展统计,看 %util 判断哪块磁盘被打满
$ iostat -x 1
命令 看什么
sar -u 历史 CPU(%iowait 高 = IO 瓶颈)
sar -r 历史内存
sar -d 历史磁盘
sar -q 历史负载(load average)
vmstat 1 实时综合快照,si/so 看换页
iostat -x 1 实时磁盘,%util 看瓶颈盘

[!NOTE] sysstat 默认每 10 分钟采一次样,历史数据存放在 /var/log/sysstat/ 下按天分文件。排查「昨晚的问题」时,sar -u -f /var/log/sysstat/sa27 可以指定看 27 号那天的数据。

验证方法

$ sar -u | tail -3
预期输出:
12:00:01        all      3.52    0.00    1.75    0.10    0.00    0.00    0.00    0.00    0.00   94.63
12:10:01        all      2.98    0.00    1.40    0.05    0.00    0.00    0.00    0.00    0.00   95.57
Average:        all      3.21    0.00    1.52    0.08    0.00    0.00    0.00    0.00    0.00   95.19

nc / telnet:端口连通性测试

是什么

排查「服务连不上」时,要先把问题切成两半:是网络不通,还是服务本身的问题?nc(netcat)就是切分工具——端口能连通,问题在应用层;连不通,问题在网络/防火墙层。15 网络配置 讲的网络排错流程里,它是最常用的一环。telnet 是更老的同位工具,现在主要被 nc 取代。

怎么做

# -z 只测端口不发数据,-v 显示过程
$ nc -zv 192.168.1.10 5432
预期输出(成功):
Connection to 192.168.1.10 5432 port [tcp/postgresql] succeeded!
预期输出(失败,超时一般是防火墙拦截;Connection refused 是端口没监听):
nc: connect to 192.168.1.10 port 5432 (tcp) failed: Connection refused

验证方法

# 两种失败形态要分清:refused 找服务,timeout 找防火墙
$ nc -zv 127.0.0.1 22

常见问题

现象 根因 解决
sudo apt install htop 提示 Unable to locate package 软件源列表为空或没更新 先执行 sudo apt update;仍失败检查 /etc/apt/sources.list 源配置(参考 17 包管理)
tcpdump 报错 You don't have permission to capture on that device 抓包需要 root 权限 用 sudo tcpdump ...;或给二进制授权 sudo setcap cap_net_raw,cap_net_admin+eip /usr/bin/tcpdump
strace 附着后输出几条就没动静 附加的进程本来就在正常等待(如空闲的守护进程) 先用 htop 确认目标进程确实在忙;或改用 strace -p PID -e trace=network(只看网络调用)或 -e trace=file(只看文件调用)缩小范围再观察
strace -p 报 Operation not permitted 目标进程属主不同或容器内权限隔离 加 sudo;容器内进程需 docker exec 进同一容器再执行
tmux 里颜色丢失、方向键变成字母 远端 TERM 环境变量设置不当 确认 SSH 终端为 xterm 系;必要时在 shell 配置里 export TERM=xterm-256color 后重新 attach
nc -zv 一律超时,本机测试也是 本机防火墙(ufw)拦截 sudo ufw status 检查,放行对应端口(参考 16 防火墙)
iotop 满屏都是 0.00 B/s 的行,分不清谁在写盘 不加 -o 时会显示全部进程(含 IO 为 0 的行),这是正常的 加 -o 只显示有 IO 的进程;再配 -d 2 拉长刷新间隔观察

本章命令速查

命令 作用
htop 交互式看 CPU/内存/进程,M 排内存、P 排 CPU、F9 杀进程
sudo iotop -oPa 按进程看磁盘 IO 的实时与累计量
sudo ncdu / 交互式分析磁盘空间占用,d 删除(危险)
sudo lsof -i:8080 查谁占用某端口
sudo lsof | grep deleted 找已删除但被进程占用的文件(磁盘悬空空间)
sudo strace -p PID 跟踪进程系统调用,定位「卡在哪」
strace -c 命令 统计系统调用耗时分布
sudo tcpdump -i any port N -nn -c 100 抓指定端口的包
tcpdump -w a.pcap 抓包存文件,供 Wireshark 分析
sudo fail2ban-client status sshd 查看封禁名单
sudo fail2ban-client set sshd unbanip IP 解封 IP
tmux new -s work 新建会话
tmux ls / tmux attach -t work 列出/重新接入会话
sar -u / sar -r / sar -d 回溯历史 CPU/内存/磁盘
vmstat 1 / iostat -x 1 实时综合快照/实时磁盘
nc -zv IP 端口 测端口连通性

相关章节

📄 备份与定时任务 44-backup-and-cron.md


title: 44 备份与定时任务 updated: 2026-09 applies-to: Ubuntu 22.04 LTS

定时任务是服务器自动化的起点,备份是运维的底线。这两件事天然绑在一起:备份要靠定时任务来自动跑。本章先讲清备份的思路,再讲 cron 和 systemd timer 两种定时机制,最后给出可直接抄走的数据备份脚本。数据库版本以 91 版本速查表 为准(安装见 23 MySQL ~ 26 Redis 各章),截至 2026-09。

[!IMPORTANT] 没有经过恢复演练的备份,等于没有备份。 备份文件存在,不代表它能恢复出可用的数据。本章最后会讲怎么演练,请把「定期恢复一次」当成备份工作的一半,而不是附加项。

备份三问与 3-2-1 原则

是什么

动手写任何备份脚本之前,先回答三个问题:

  1. 备份什么? 数据库数据文件、应用配置、上传的文件——先盘点,列清单。「编译出来的代码和系统本身」不用备,坏了可以重装重编。
  2. 备到哪里? 本机另一块盘、另一台机器、还是对象存储?只备份到本机同一块盘,磁盘一坏全完。
  3. 多久恢复一次演练? 备份的价值只体现在恢复成功的那一刻,不演练就永远不知道备份是坏的。

对应的行业标准是 3-2-1 原则:至少 3 份副本,存放在 2 种不同介质上,其中 1 份异地(另一机房/另一可用区/云对象存储)。小项目至少做到「本机一份 + 异机/云端一份」。

验证方法

# 盘点最大的数据目录,确定「备份什么」
$ sudo du -sh /var/lib/mysql /var/lib/postgresql /var/www /etc 2>/dev/null
预期输出:
1.8G    /var/lib/mysql
620M    /var/lib/postgresql
4.1G    /var/www
14M     /etc

cron 定时任务

是什么

cron 是 Linux 最老牌的定时任务系统:每分钟醒来一次,检查有没有到点要执行的任务。它不依赖你登录,也不依赖 systemd 服务编写,一行五段式表达式就能让命令每天/每小时自动执行,是备份脚本最经典的载体。

怎么做

当前用户的定时任务用 crontab -e 管理(会用 vi 编辑器,基础操作见 10 shell 与文件系统):

# 编辑当前用户的定时任务(首次会让你选编辑器,选 vim 或 nano 均可)
$ crontab -e

# 列出当前用户的定时任务
$ crontab -l

五段语法:分 时 日 月 周,各段用空格隔开:

表达式 含义
0 3 * * * 每天 3:00
*/5 * * * * 每 5 分钟
0 8 * * 1 每周一 8:00
0 0 1 * * 每月 1 号 0:00
0 */2 * * 1-5 工作日每 2 小时整点
30 4 * * 0 每周日 4:30

系统级定时任务不走 crontab,而是放文件:

  • /etc/cron.d/:系统级 cron 文件,语法多一段「用户名」字段,推荐系统级部署放这里;
  • /etc/cron.daily/、/etc/cron.weekly/、/etc/cron.hourly/:把可执行脚本丢进目录,自动按天/周/时执行;
  • /etc/crontab:系统全局 crontab。

分工建议:当前用户自己的任务用 crontab -e;随软件部署、root 运行的系统任务放 /etc/cron.d/,一个任务一个文件,便于配置管理和卸载。

参数说明(三大经典坑)

坑 1:PATH 不全。 cron 执行命令时 PATH 只有 /usr/bin:/bin,你平时能用的 /usr/local/bin 下命令全部找不到,报错 command not found。解决:命令写绝对路径,或在 crontab 首行显式设置:

# crontab 首行设置 PATH,后面任务就能直接用 mysql 等命令
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

# 也可以不设 PATH,直接用绝对路径调用
0 3 * * * /usr/bin/mysqldump --defaults-extra-file=/root/.my.cnf appdb > /backup/a.sql

坑 2:% 必须转义。 在 crontab 里,% 是换行符,后面内容会被当成下一条命令。所以 date +%F 必须写成 date +\%F:

# % 必须写成 \%,否则任务在第一个 % 处被截断,文件名丢失日期
0 3 * * * /opt/backup.sh $(date +\%F)

坑 3:没有环境变量。 cron 不读你的 .bashrc/.profile,00 前言 里配置的别名、代理、JAVA_HOME 等,在 cron 环境里统统不存在。解决:环境变量在脚本内部自己 source 或显式 export,不要指望 cron 继承。

输出去向

cron 的任何输出(包括报错)默认会发邮件给本机用户,存进 /var/mail/<用户>——而大部分服务器根本没人看邮件。务必把输出重定向到日志文件:

# >> 追加写入日志,2>&1 把错误输出也合并进去,方便事后排查
# 注意:普通用户无权写 /var/log,这条任务应放在 root 的 crontab 里;非 root 用户可改写到自家目录(如 ~/backup.log)
0 3 * * * /opt/backup.sh >> /var/log/backup.log 2>&1

验证方法

# 1. 确认 cron 服务在运行
$ systemctl status cron

# 2. 用一条「两分钟后执行」的任务验证整条链路,先看当前时间
$ date '+%F %T'
预期输出:
2026-09-29 12:04:30

在 crontab 里加一行(时间改成两分钟后,注意 \% 转义):

6 12 * * * echo "cron works $(date +\%T)" >> /tmp/cron-test.log

等 12:06 过后查看:

$ cat /tmp/cron-test.log
预期输出:
cron works 12:06:01

[!WARNING] crontab -r 会删除当前用户全部定时任务,且没有任何确认提示。执行前先备份:

# 先导出备份,再编辑/删除
$ crontab -l > ~/crontab.bak
$ crontab -r

systemd timer:现代替代方案

是什么

systemd 自带定时器 .timer,是 cron 的现代替代。与 cron 相比优势明确:执行记录直接进 journal(不用自己重定向)、可以设置依赖关系(等网络就绪再跑)、同一任务不会重复叠加执行(防重入)。40 systemd 已讲过 unit 体系,timer 就是其中一种。

对比项 cron systemd timer
日志 重定向自己管,默认发邮件 自动进 journal,journalctl -u 直接查
依赖管理 无 After=/Requires= 等完整依赖
防重入 可能叠跑(上次没完又到点) 天然防重入,上一轮没完下一轮跳过
编写成本 一行搞定 需要两个文件

怎么做

一对文件实现「每天 3:00 执行备份脚本」:

# /etc/systemd/system/mybackup.service —— 定义「做什么」
[Unit]
Description=Daily backup job

[Service]
Type=oneshot
ExecStart=/opt/backup.sh
# /etc/systemd/system/mybackup.timer —— 定义「什么时候做」
[Unit]
Description=Run mybackup daily at 03:00

[Timer]
# OnCalendar 语法:年-月-日 时:分:秒,* 表示任意
OnCalendar=*-*-* 03:00:00
# 开机时错过的任务是否补跑
Persistent=true

[Install]
WantedBy=timers.target
# 启用并立刻启动 timer(注意 enable 的是 .timer,不是 .service)
$ sudo systemctl enable --now mybackup.timer

验证方法

# 列出所有 timer,看下次触发时间
$ systemctl list-timers mybackup.timer
预期输出:
NEXT                        LEFT  LAST PASSED UNIT              ACTIVATES
Wed 2026-09-30 03:00:00 CST 14h   -    -      mybackup.timer    mybackup.service

[!NOTE] 手动测试时可以不依赖定时,直接 sudo systemctl start mybackup.service 立即执行一次,再用 journalctl -u mybackup.service 看输出——这是 timer 方案排错比 cron 舒服的地方。

数据库备份实战

是什么

备份的核心对象是数据库。四种主流数据库各有一套官方备份工具,用它们而不是直接拷数据目录——直接拷目录要求停库且版本敏感,工具导出则是热备份、跨版本可恢复。

怎么做

MySQL / MariaDB:

# --single-transaction 用一致性快照导出,InnoDB 下不锁表,业务无感
# --routines --triggers 连存储过程和触发器一起导出,漏了会恢复不全
# 注意:-p'<密码>' 仅作演示,密码会暴露在 ps 与历史记录里;生产脚本按下方「密码安全」一节用 --defaults-extra-file / ~/.my.cnf
$ mysqldump -u root -p'<密码>' --single-transaction --routines --triggers appdb | gzip > /backup/appdb_$(date +%F).sql.gz

# 恢复:解压导入
$ gunzip < /backup/appdb_2026-09-29.sql.gz | mysql -u root -p appdb

--single-transaction 的原理:开启一个一致性读事务,导出全程读的是事务开始时的快照,写入照常进行,所以不锁表。前提是表引擎为 InnoDB(默认就是),MyISAM 表仍会锁。

PostgreSQL:

# -Fc 是自定义压缩格式,支持选择性恢复,优于纯 SQL
# Ubuntu 的 PostgreSQL 默认 peer 认证,必须以 postgres 系统用户身份执行,否则报 role "xxx" does not exist
$ sudo -u postgres pg_dump -Fc appdb > /backup/appdb_$(date +%F).dump

# 恢复
$ sudo -u postgres pg_restore -d appdb /backup/appdb_2026-09-29.dump

MongoDB:

# --gzip --archive 打包成单个压缩文件,便于传输和清理
$ mongodump --db appdb --gzip --archive=/backup/appdb_$(date +%F).gz

# 恢复
$ mongorestore --gzip --archive=/backup/appdb_2026-09-29.gz

Redis: 数据就是一个 dump.rdb 文件,先触发落盘再拷贝:

# BGSAVE 在后台生成 RDB 快照,不阻塞写入
$ redis-cli BGSAVE

# 用 LASTSAVE 前后对比确认快照已落盘(时间戳变化说明本次生成完成)
$ redis-cli LASTSAVE

# dump.rdb 只有 redis 用户可读,拷贝需要 sudo
$ sudo cp /var/lib/redis/dump.rdb /backup/redis_$(date +%F).rdb

密码安全

[!WARNING] 数据库密码不要直接写在命令行里。 命令行参数在执行期间对全机用户可见(任何人都可用 ps aux 看到 -p'密码'),历史记录里也会留下明文。脚本中务必用配置文件方式。

MySQL 推荐用 ~/.my.cnf,权限收紧到 600:

# /root/.my.cnf —— mysqldump 自动读取,命令行不再需要 -p
[client]
user=root
password=你的密码
# 权限收紧到 600:全局可写(world-writable)的配置文件会被客户端忽略并告警,600 是最佳实践
$ chmod 600 /root/.my.cnf

# 之后命令行可以不带密码直接用
$ mysqldump --single-transaction appdb | gzip > /backup/appdb_$(date +%F).sql.gz

验证方法

# 备份后第一件事:看文件大小是否正常,0 字节说明备份失败
$ ls -lh /backup/appdb_$(date +%F).sql.gz
预期输出:
-rw-r--r-- 1 root root 245M Sep 29 03:12 /backup/appdb_2026-09-29.sql.gz

文件备份

是什么

数据库之外的配置文件、上传文件,用 tar 打包或 rsync 同步。11 文件操作 已讲过基础用法,这里聚焦备份场景。

怎么做

# tar 打包:czf = 创建 gzip 压缩包,配合日期变量命名
$ tar czf /backup/www_$(date +%F).tar.gz /var/www

# rsync 到备份机:-a 保留权限时间戳,-v 显示过程,-z 传输压缩
# --delete 让备份端与源端完全一致,源端删了备份端也删(慎用,见下)
$ rsync -avz --delete /var/www/ backup@192.168.1.20:/data/www/

--link-dest 可以做增量快照:指向上一次的备份目录,未变化的文件只创建硬链接,不占新空间——每天一份完整目录,总磁盘占用却接近一份全量加每日增量。

# 增量快照核心逻辑:与上次快照做硬链接去重
$ rsync -avz --link-dest=/backup/www/2026-09-28 /var/www/ /backup/www/2026-09-29/

定期清理

备份不清理,迟早把备份盘撑满(排查见 55 磁盘满排查)。常见策略是「保留最近 7 天」:

[!WARNING] 下面的命令会删除 7 天前的所有备份,无法恢复。首次执行前,先不带 -delete 跑一遍 find -mtime +7 -print 预览要删的清单,并确认备份目录路径没有写错——路径写错就是灾难。

# 先预览会删除哪些文件(不带 -delete,只看列表)
$ find /backup/www -mtime +7 -print

# 确认无误后再真正执行删除
$ find /backup/www -mtime +7 -delete

备份脚本完整骨架

把前面的内容组装成一个可直接抄的脚本,逐段说明:

#!/bin/bash
# /opt/backup.sh —— 每日数据库 + 文件备份骨架

# 任一命令失败(含管道中段)立即中止,避免「dump 失败但 gzip 继续产出 0 字节文件」
set -euo pipefail

# 统一的日期后缀和基础路径
DATE=$(date +%F)
BACKUP_DIR=/backup
KEEP_DAYS=7

# 建立备份目录,不存在则创建
mkdir -p "$BACKUP_DIR"

# 数据库备份:密码走 /root/.my.cnf,不出现在命令行
/usr/bin/mysqldump --single-transaction --routines --triggers appdb \
  | gzip > "$BACKUP_DIR/appdb_${DATE}.sql.gz"

# 文件备份
tar czf "$BACKUP_DIR/www_${DATE}.tar.gz" /var/www

# 同步到备份机(3-2-1 中的异地一份,路径按实际环境修改)
rsync -az "$BACKUP_DIR" backup@192.168.1.20:/data/backup/

# 清理本地过期备份(保留 7 天)
find "$BACKUP_DIR" -mtime +"$KEEP_DAYS" -delete

# 跑到这里说明全部成功,写一行日志
echo "$(date '+%F %T') backup ok" >> /var/log/backup.log
# 赋予执行权限(cron 里直接跑,缺执行位会报 Permission denied)
$ sudo chmod 700 /opt/backup.sh

# 手动跑一次验证,再交给 cron/timer
$ sudo /opt/backup.sh

[!TIP] set -euo pipefail 三个开关各管一件事:-e 出错即停、-u 用到未定义变量报错、-o pipefail 让管道里任何一段失败都算整体失败。备份脚本不写这行,失败会被静默吞掉,产出 0 字节文件而你毫无察觉。

恢复演练

怎么做

  • 频率:至少每月一次,重要系统每周;
  • 场地:在一台测试机/容器上恢复,绝不在生产库直接演练;
  • 校验:恢复后核对表数量、关键表行数、应用能否正常连接读写;
  • 文档:把恢复步骤写成文档(命令、顺序、预计时长),事故发生时照着做,不靠记忆。
# 演练示例:在测试机上恢复并抽查行数
$ gunzip < appdb_2026-09-29.sql.gz | mysql -u root -p appdb_restore_test
$ mysql -u root -p -e "SELECT COUNT(*) FROM orders" appdb_restore_test
预期输出:
COUNT(*)
38214

常见问题

现象 根因 解决
cron 任务到点没执行 cron 服务未运行、时间未到、表达式写错 systemctl status cron 确认服务;date 对比服务器时间;crontab -l 核对表达式
手动能跑,cron 里 command not found cron 的 PATH 只有 /usr/bin:/bin 命令写绝对路径,或 crontab 首行设 PATH=(见坑 1)
cron 执行了但失败,不知道报什么 输出发去了没人看的本地邮件 重定向到日志:>> /var/log/xxx.log 2>&1;历史报错去 /var/mail/root 翻
备份文件是 0 字节 mysqldump 失败但管道后续照常执行 脚本加 set -euo pipefail;先手动执行脚本看真实报错(常见是密码/权限)
% 后面内容丢失,文件名没日期 crontab 中 % 被解析为换行符 写成 \% 转义(见坑 2)
备份时间与预期差了 8 小时 服务器时区是 UTC,本地是 CST timedatectl set-timezone Asia/Shanghai 后重启 cron(参考 15 网络配置)
备份盘被撑满,服务器又挂了 只备份不清理,或保留策略过宽 加 find -mtime +N -delete 定期清理;备份目录单独分区,防止拖垮系统盘
crontab -r 后任务全没了 该命令删全部且无确认 平时 crontab -l > ~/crontab.bak;有 git 的把 crontab 内容纳入版本管理

本章命令速查

命令 作用
crontab -e / -l / -r 编辑 / 列出 / 删除(全部,无确认)当前用户定时任务
systemctl status cron 确认 cron 服务在运行
systemctl enable --now mybackup.timer 启用并启动 systemd 定时器
systemctl list-timers 查看所有 timer 的下次触发时间
journalctl -u mybackup.service 查看 timer 任务的执行日志
mysqldump --single-transaction appdb MySQL 不锁表导出
pg_dump -Fc appdb / pg_restore PostgreSQL 导出 / 恢复
mongodump --gzip --archive=xx.gz MongoDB 导出为压缩归档
redis-cli BGSAVE 触发 Redis 后台落盘 RDB
tar czf x.tar.gz 目录 打包压缩目录
rsync -avz --delete 源 目标 同步到备份机
find 目录 -mtime +7 -delete 删除 7 天前的旧备份(先 -print 预览)
chmod 600 /root/.my.cnf 收紧数据库密码文件权限

相关章节

📄 Linux 日志体系基础 50-log-fundamentals.md


title: 50 Linux 日志体系基础 updated: 2026-09 applies-to: Ubuntu 22.04 LTS

从这一章开始进入 PART 4「日志」。前面几十章,你的服务器上跑起了 Nginx、MySQL、Redis,还有自己部署的应用 —— 它们出了问题去哪找线索?答案只有一个:日志。本章先建立全局认识:日志都在哪、谁在写、怎么分级,后面两章再学具体怎么查。

[!IMPORTANT] 排查服务器问题的第一动作永远是看日志,而不是重启。重启能掩盖问题,日志才能定位问题。养成习惯:任何异常,先看日志,再动手。本章内容适用于 Ubuntu 22.04 自带的 rsyslog 8.2110 与 systemd-journald v249,截至 2026-09 仍是 LTS 默认组件。

为什么日志是运维的眼睛

是什么

日志就是程序给自己写的「工作日记」:谁在什么时间做了什么、成功了还是失败了、报了什么错。服务器上的一切——登录、请求、数据库慢查询、进程崩溃——只要程序愿意说,日志里都有记录。

为什么它重要?因为服务器 7x24 小时运行,问题往往发生在你不在场的时候:半夜进程崩了、凌晨有人尝试爆破 SSH、请求突然变慢。只有日志忠实地记下了这一切。没有日志,排查等于蒙着眼睛修车。

怎么做

先建立肌肉记忆:遇到问题,脑子里要立刻浮现「该看哪个日志」。常见问题与对应日志的映射,下一节的目录地图里完整给出。

验证方法

看一眼你机器上的日志目录,感受一下「日志的存在感」:

# 列出 /var/log 下所有日志文件,按人类可读大小显示
$ ls -lh /var/log
预期输出:
total 3.2M
-rw-r--r--  1 root   root              0 Sep 28 08:00 alternatives.log
-rw-r--r--  1 root   adm           312K Sep 28 10:15 auth.log
-rw-r--r--  1 root   adm           1.2M Sep 28 10:20 syslog
drwxr-xr-x  2 root   root          4.0K Sep 20 08:00 nginx/
drwxr-xr-x  2 root   systemd-journal 4.0K Sep 20 08:00 journal/
...

只要 /var/log 不是空的,说明日志系统在工作。注意 journal/ 这一行:开启 journald 持久化后才会出现(见下文 Storage= 说明),默认装机没有它属正常。

常见问题

现象 根因 解决
找不到日志 不知道该看哪个文件 对照下一节目录地图,按「问题类型」找文件
日志是空的 应用没启动,或日志写到了别处 先 systemctl status 确认服务状态
以为日志会无限留 日志会滚动清理 理解 logrotate 机制,见 54 logrotate

/var/log 目录地图

是什么

/var/log 是 Linux 约定俗成的日志大本营。Ubuntu 上的布局如下,每一行都标注了「什么场景去看它」——这张表建议收藏:

文件/目录 记录什么 什么场景去看它
syslog Ubuntu 综合日志,大部分系统消息都汇聚于此 不确定看哪个文件时,先在这里 grep
auth.log 登录、sudo、SSH 认证记录 怀疑被入侵、SSH 爆破、查 sudo 操作,先看这个
kern.log 内核消息 硬件异常、驱动问题、OOM(内存不足杀进程)
dmesg 内核环形缓冲区(本次开机) 开机失败、磁盘 I/O 报错,sudo dmesg 直接看
ufw.log 防火墙 UFW 拦截记录 端口不通,怀疑被防火墙拦了
nginx/ Nginx 的 access.log / error.log 网站打不开、看请求状态码、502 排查
mysql/ MySQL 错误日志 error.log MySQL 起不来、崩溃恢复记录
postgresql/ PostgreSQL 日志 PG 启动失败、慢查询记录
mongod.log MongoDB 日志(单文件) mongod 起不来、连接异常
redis/redis-server.log Redis 日志 Redis 启动失败、持久化报错
journal/ journald 的二进制日志 不能直接 cat,必须用 journalctl 查(下一章)
apt/ 软件包安装/升级历史 「这机器上周装过什么?」history.log 一目了然
fail2ban.log fail2ban 封禁动作记录 查 IP 被谁封、封了多久
cron.log 定时任务执行记录 定时任务没跑?默认需手动开启(见下)

[!NOTE] cron.log 在 Ubuntu 22.04 默认不单独落盘,定时任务的输出混在 syslog 里。要单独开启,编辑 /etc/rsyslog.d/50-default.conf,取消 #cron.* 那行的 # 注释后重启 rsyslog。

怎么做

背不下整张表没关系,记住三个入口即可:

# 入口一:登录/入侵问题,看 auth.log
$ sudo tail -50 /var/log/auth.log

# 入口二:不确定哪的问题,在综合日志里搜关键词
$ sudo grep -i error /var/log/syslog

# 入口三:某软件的问题,进它的专属目录
$ sudo tail -50 /var/log/nginx/error.log

参数说明

目录地图里的权限规律值得一提:系统日志(syslog、auth.log 等)属主是 root:adm 组。adm 组的普通用户可以免 sudo 读日志,这是给运维同事授权看日志的标准做法:

# 把用户 deploy 加进 adm 组,之后不用 sudo 也能读 /var/log 下的日志
$ sudo usermod -aG adm deploy

验证方法

logger 命令可以手写一条测试日志,是验证日志链路通不通的利器:

# 手动向 syslog 写一条测试消息
$ logger "hello from cli"

# 去综合日志里找这条消息(系统日志普通用户无权读,需要 sudo)
$ sudo grep "hello from cli" /var/log/syslog
预期输出:
Sep 28 10:30:01 myserver deploy: hello from cli

能 grep 到,说明「应用 → rsyslog → /var/log/syslog」整条链路是通的。这个技巧在调试应用日志配置时非常实用。

常见问题

现象 根因 解决
/var/log/syslog 不存在 rsyslog 没安装或没启动 sudo apt install rsyslog && sudo systemctl enable --now rsyslog
读取日志提示 Permission denied 普通用户默认无权读系统日志 前面加 sudo,或把自己加入 adm 组
某应用日志不在 /var/log 下 应用自己配置了日志路径 查应用的配置文件,或用 journalctl -u 应用名 兜底

journald 与 rsyslog 的分工与共存

是什么

Ubuntu 22.04 上同时跑着两套日志系统,初学者最容易在这里犯迷糊。它们不是竞争关系,而是流水线的上下游:

  • journald(systemd-journald):systemd 自带的日志收集器,所有服务的 stdout/stderr、内核消息都先汇到这里。它存的是二进制格式,放在内存(/run/log/journal)或 /var/log/journal,不能用 cat 直接看,必须用 journalctl 查询——这是下一章的主角。
  • rsyslog:传统日志守护进程,从 journald 接过消息,按照规则写成纯文本文件,落在 /var/log/*.log(syslog、auth.log、kern.log……)。

流程一句话:journald 先收,rsyslog 再落盘成文本。两份内容同源,只是格式和查询方式不同。

为什么两种都要会查?因为现实世界很分裂:系统服务短平快的问题,journalctl -u xxx 最快;而需要 grep、备份、导出分析,或者应用直接写文件不走 journald 时,文本日志才是主角。只会其一,总有一天会「找不到日志」。

怎么做

确认两套系统都在正常工作:

# 查 journald 状态
$ systemctl status systemd-journald

# 查 rsyslog 状态
$ systemctl status rsyslog

两者都应显示 active (running)。

参数说明

journald 的存储位置由 /etc/systemd/journald.conf 里的 Storage= 决定:

取值 行为
auto(默认) /var/log/journal 目录存在才持久化,否则只存内存,重启即丢
persistent 日志持久化到 /var/log/journal,重启不丢
volatile 只存内存,重启即丢

Ubuntu 22.04 默认装了 rsyslog 并已把 journald 消息转发给它,所以你两种日志都有。

验证方法

# 用 journalctl 查 journald 里的 nginx 日志(二进制侧)
$ journalctl -u nginx -n 3 --no-pager

# 用 tail 查 rsyslog 落盘的文本(文本侧)
$ tail -3 /var/log/syslog
预期输出(两侧都能看到内容,只是格式不同):
-- Logs begin at Mon 2026-09-01 09:00:01 CST, end at Tue 2026-09-29 10:00:00 CST. --
Sep 28 10:00:01 myserver systemd[1]: nginx.service: Succeeded.
Sep 28 10:00:01 myserver systemd[1]: Stopped A high performance web server.

常见问题

现象 根因 解决
journalctl 只有本次开机的日志 Storage=auto 且 /var/log/journal 不存在 sudo mkdir -p /var/log/journal && sudo systemctl restart systemd-journald
/var/log/syslog 不存在 rsyslog 未安装或未启动 安装并启动 rsyslog(见上节常见问题)
journalctl 报错没有权限 普通用户默认只能看自己的日志 加 sudo,或加入 adm、systemd-journal 组
同一条消息在两处都有 正常现象,journald 和 rsyslog 同源各存一份 不是重复配置错误,不用处理

日志优先级

是什么

日志按严重程度分为八个级别(syslog 标准),从高到低:

级别 名称 含义 典型例子
0 emerg 系统不可用 内核 panic
1 alert 必须立即处理 数据库损坏
2 crit 危急 硬件故障、磁盘满
3 err 错误 服务启动失败、请求报 500
4 warning 警告,暂不影响运行 磁盘使用率超 80%
5 notice 正常但值得注意 服务重启完成
6 info 一般信息 收到一个请求
7 debug 调试细节 变量值、SQL 明细

记忆建议:排错时先看 err 及以上(0-3 级),这是「已经出事」的部分;warning 级(4)是「快出事」,值得扫一眼;info 及以下平时不用看,量大但通常无害。

怎么做

用 journalctl 的 -p 参数按级别过滤(下一章详讲):

# 只看 err 及更严重的日志
$ sudo journalctl -p err -b

验证方法

# 查看本次开机以来 err 级以上的日志条数
$ sudo journalctl -p err -b --no-pager | wc -l
预期输出:
23

数字不大是正常的——健康系统 err 以上日志很少。如果这个数在快速增长,说明有东西在持续报错。

常见问题

现象 根因 解决
日志里全是 info 刷屏,找不到重点 没按级别过滤 用 -p err 或 grep ERROR 先收敛范围
明明出错了却没看到 err 日志 应用没按级别打日志 检查应用日志级别配置,见下节
warning 大量重复,埋没了 err 噪音太多 先修刷 warning 的来源,再用 -p err 过滤

facility 一句话带过

日志还有一个「来源分类」维度叫 facility:auth(认证)、daemon(守护进程)、kern(内核)、mail(邮件)、cron(定时任务)等。它的作用是让 rsyslog 决定「哪类消息写进哪个文件」——auth 类进 auth.log,kern 类进 kern.log。日常排查只需要知道这个概念,用来理解「为什么这条日志会出现在这个文件里」,不必深记。

应用日志放哪、怎么写

是什么

系统软件的日志位置是发行版定好的,而你自己部署的应用,日志放哪、什么格式,由你的配置决定。这里有一条全行业通行的约定,遵守它,后面章节的排查命令才能通用。

位置约定:放 /var/log/<应用名>/ 目录。为什么?所有运维工具(logrotate、监控、备份)都默认在 /var/log 下找日志,放别处等于自绝于生态。

属主约定:目录属主给运行该应用的用户(比如 appuser,与 40/42/60 章的部署用户一致),让应用自己能写,别人不能随便改。

格式约定:一行一条,按「时间戳 级别 线程/模块 消息」排列。为什么一行一条?因为 grep、wc -l、tail 这些工具都是按行处理日志的,一行一条事件是所有文本分析的前提。

怎么做

创建规范的日志目录:

# 创建应用日志目录,属主给运行用户 appuser,权限 750(属主可写,组可读,其他人无权)
$ sudo mkdir -p /var/log/myapp
$ sudo chown appuser:appuser /var/log/myapp
$ sudo chmod 750 /var/log/myapp

先看一个坏日志长什么样——多行混杂、没有时间戳、没有级别:

2026-09-28 10:00:01 ERROR something failed
    at com.example.Service.run(Service.java:42)
    at com.example.Main.main(Main.java:10)
done, took 3s
ERROR something failed again

问题:第二段错误没时间戳没法定位、普通消息和错误混在一起没法 grep、堆栈断行导致一行一条被破坏。

再看好日志——一行一条,字段齐全:

2026-09-28 10:00:01 ERROR [worker-1] OrderService: 订单 1024 处理失败, 原因=超时
2026-09-28 10:00:03 INFO  [http-8080] RequestFilter: GET /api/health 200 3ms
2026-09-28 10:00:05 ERROR [worker-1] OrderService: 订单 1025 处理失败, 原因=库存不足

每行都能独立被 grep 到、能按时间排序、能按级别统计。堆栈可以跨行,但每条日志的首行必须带全时间戳和级别。

[!NOTE] 更进一步的做法是结构化(JSON)日志:每行一个 JSON 对象,字段固定,便于日志系统(如 ELK)解析分析。文本日志够用之前不必上,知道有这回事即可。

参数说明

应用写日志的两条路径,选其一:

方式 做法 适用
直接写文件 应用日志库配置输出到 /var/log/myapp/app.log 应用有完善日志库(Logback、winston 等)
交给 journald 应用只往 stdout/stderr 打印,由 systemd 收进 journal,由 journald 自动限额滚动(见 51 journalctl;journal 是二进制,logrotate 管不到) 简单脚本、容器化应用

42 进程守护方案选型 讲进程托管时会回到这个选择。

验证方法

部署后验证日志确实按约定落盘:

# 触发一条业务请求后,看日志最新 3 行
$ sudo tail -3 /var/log/myapp/app.log
预期输出:
2026-09-28 10:00:01 INFO  [main] Application: started on port 8080
2026-09-28 10:00:05 INFO  [http-8080] RequestFilter: GET /api/health 200 3ms
2026-09-28 10:00:09 ERROR [worker-1] OrderService: 订单 1024 处理失败, 原因=超时

时间戳对、级别分明、一行一条,即符合约定。

常见问题

现象 根因 解决
日志目录权限拒绝,应用启动失败 属主不是运行用户 chown -R appuser:appuser /var/log/myapp
日志全是重复同一条 应用 bug 或配置错误导致刷屏 先修应用,再谈查日志;临时可用 grep -v 排除
堆栈被打成一行或断成乱行 日志框架配置不当 调整布局(pattern)配置,保证首行带时间戳与级别
日志文件时间戳与应用行为对不上 应用用了 UTC 时区 统一时区,见 15 网络配置

日志的两难:详细与精简

是什么

日志配置永恒的两难:

  • 排查时,恨不得把级别开到 debug,每个细节都打出来;
  • 平时,只想要 info,否则日志几十 GB,又费磁盘又费眼睛。

没有两全的静态配置,正确姿势是:平时 info + 磁盘滚动清理,出事时临时调 debug,查完改回来。而「滚动清理」靠 logrotate:把大日志切割成 app.log.1、app.log.2.gz 并定期删除旧的,防止磁盘被日志撑爆——这是 54 logrotate 的主题。

怎么做

临时调高日志级别的思路(以你的应用支持动态配置为前提):

# 平时:日志级别 info
log.level = info

# 排查期间:临时改成 debug,复现问题
log.level = debug

# 查完:立刻改回 info,避免刷盘
log.level = info

验证方法

排查前后各检查一次磁盘占用,确认没把磁盘吃满:

# 看应用日志目录占用
$ du -sh /var/log/myapp/
预期输出:
128M    /var/log/myapp/

常见问题

现象 根因 解决
日志把磁盘撑满,服务挂了 没配滚动清理 立即清理旧日志,并配置 logrotate(见 54 logrotate)
排查时信息太少 平时级别太高 临时调 debug 复现,查完改回
debug 全开后性能下降 日志量太大拖慢应用 尽量用低峰期排查,或只对单个模块开 debug

本章命令速查

命令 作用
ls -lh /var/log 查看日志目录全景
tail -50 /var/log/syslog 看综合日志最新 50 行
sudo tail -50 /var/log/auth.log 查登录与 sudo 记录(查入侵第一站)
sudo dmesg 看本次开机内核消息
logger "消息" 手写一条测试日志
grep 关键词 /var/log/syslog 在综合日志中搜索
sudo usermod -aG adm 用户 授权普通用户免 sudo 读日志
journalctl -u 服务名 查 journald 中某服务日志(下一章)
du -sh /var/log/应用名/ 查应用日志磁盘占用

相关章节

📄 journalctl 系统日志查询 51-journalctl.md


title: 51 journalctl 系统日志查询 updated: 2026-09 applies-to: Ubuntu 22.04 LTS

上一章说过:systemd 时代,每个服务的 stdout/stderr 都被 journald 收进了二进制日志库。想看这些日志,唯一入口就是 journalctl。它是排查「服务为什么起不来」「半夜为什么重启」的第一工具——本章把它一次讲透。

[!IMPORTANT] journalctl 默认会进入 less 分页浏览模式,看完记得按 q 退出。所有 journalctl 查询都建议加 sudo(或把用户加入 systemd-journal、adm 组),否则普通用户看不到系统级日志。本章命令适用于 Ubuntu 22.04 的 systemd v249,截至 2026-09 参数行为一致。

journalctl 是什么

是什么

journalctl 是 systemd 的日志查询工具,从 journald 的二进制数据库里按条件取日志。它能做的,grep /var/log/syslog 也能做一部分,但 journalctl 有三大优势:

  • 带结构:每条日志自带时间、主机、进程、PID、优先级字段,可以像查数据库一样过滤(-u、-p、--since);
  • 跨服务关联:出事那一分钟,内核、systemd、你的服务各说了什么,时间线一页看全;
  • 不依赖落盘:服务刚被 systemd 拉起就崩了,可能还没来得及写文件,journal 里已经记下了。

为什么 systemd 时代必须会它?因为「服务起不来」的报错根本不会出现在任何文本文件里——服务连日志文件都没建立,journalctl 是唯一能看到它死前遗言的地方。

怎么做

最朴素的用法,直接敲:

# 从最旧到最新浏览全部日志,自动进入 less 分页
$ sudo journalctl

进分页后,常用 less 操作:

按键 作用
空格 / b 向下 / 向上翻一页
/关键词 向下搜索,回车确认
n / N 跳到下一个 / 上一个匹配
G / g 跳到末尾 / 开头
q 退出

参数说明

不加任何参数时,journalctl 从最旧的日志开始显示——这对查历史有用,但日常排错几乎总想看最新的,所以后面小节的参数才是主角。先给一张总览:

参数 作用
-n 50 只显示最近 50 行
-e 直接跳到日志末尾
-f 实时跟踪新日志(类似 tail -f)
-r 反序输出(最新在最前)
--no-pager 不进分页器,直接打印(适合脚本/管道)

验证方法

# 看最近 20 行系统日志,不分页
$ sudo journalctl -n 20 --no-pager
预期输出:
-- Logs begin at Mon 2026-09-01 09:00:01 CST, end at Tue 2026-09-29 10:00:00 CST. --
Sep 29 09:59:41 myserver systemd[1]: Starting Daily apt download activities...
Sep 29 09:59:41 myserver systemd[1]: apt-daily.service: Succeeded.
Sep 29 09:59:41 myserver CRON[4821]: (root) CMD (command -v debian-sa1 > /dev/null)
...

能正常输出且格式为「时间 主机 进程[PID]: 消息」,说明 journald 工作正常。

常见问题

现象 根因 解决
输出一半卡住不动 进入了 less 分页器,这是正常行为 按 q 退出,或加 --no-pager
提示 Permission denied / 看不到系统日志 普通用户无权读系统 journal 前面加 sudo,或加入 adm 组
输出是乱码般的超长行 某条日志含二进制内容 加 -o cat 只看消息体,或 --no-pager 管道处理

基础查询:看最近、看末尾、实时跟踪

是什么

排错的 90% 场景是「刚刚发生了什么」——不需要翻完整历史,只需要最新几行或实时盯着。journalctl 用 -n、-e、-f、-r 四个参数覆盖这些场景,其中 -f(follow)是监视服务时最常用的:新日志一到就滚出来,等价于 tail -f。

怎么做

# 看最近 50 行日志
$ sudo journalctl -n 50

# 等价写法:直接跳到末尾看
$ sudo journalctl -e

# 反序:最新日志在最前面,适合扫一眼刚发生的
$ sudo journalctl -r -n 20

# 实时跟踪(Ctrl+C 退出),盯着系统正在发生什么
$ sudo journalctl -f

-n 后面的数字可换,-n 200 就是最近 200 行。

参数说明

参数 说明
-n N 显示最近 N 行,默认 10
-e end,跳到日志末尾再显示一屏
-r reverse,倒序,常与 -n 搭配
-f follow,持续跟踪,Ctrl+C 退出;可与 -u 等过滤条件叠加

验证方法

# 反序看最近 3 行
$ sudo journalctl -r -n 3 --no-pager
预期输出:
Sep 29 10:00:00 myserver systemd[1]: Starting Cleanup of Temporary Directories...
Sep 29 10:00:00 myserver systemd[1]: systemd-tmpfile-clean.service: Deactivated successfully.
Sep 29 10:00:00 myserver CRON[4830]: (root) CMD (test -x /usr/sbin/anacron)
...

第一行就是最新的日志,说明 -r 生效。

常见问题

现象 根因 解决
-f 挂着却一直没新输出 应用日志走了 stdout 缓冲,或没有新事件产生 触发一次请求制造日志;若是缓冲问题见本章末常见问题
-n 50 显示的还是旧的 误与 -r 组合理解反了 -r 只是反转顺序,取的仍是最近 N 行
终端被日志刷屏停不下来 忘了 -f 会持续输出 Ctrl+C 退出

按时间窗查询(重点)

是什么

「昨晚 10 点到 11 点之间发生了什么」——这是日志查询最常见的形状。journalctl 的 --since / --until 就是在时间轴上切一刀,只取这个窗口内的日志。文本日志里做同样的事,你要肉眼比对时间戳;journalctl 一条参数搞定,这就是结构化日志的好处。

怎么做

支持绝对时间和相对时间两种写法:

# 绝对时间:看某天某小时的日志
$ sudo journalctl --since "2026-09-28 10:00:00" --until "2026-09-28 11:00:00"

# 只给日期,默认从当天 00:00:00 起
$ sudo journalctl --since "2026-09-28"

# 相对时间:最近 1 小时
$ sudo journalctl --since "1 hour ago"

# 今天零点以来的日志
$ sudo journalctl --since today

# systemd 支持的简写:最近 15 分钟
$ sudo journalctl --since -15min

# 组合过滤:最近 1 小时内 nginx 的日志
$ sudo journalctl -u nginx --since "1 hour ago"

参数说明

写法 含义
"2026-09-28 10:00:00" 绝对时间,精确到秒;秒可省略
"2026-09-28" 当天零点起
"1 hour ago" / "2 days ago" 相对当前时刻往回推
today / yesterday 今天 / 昨天零点起
-15min / -2h 最近 15 分钟 / 2 小时(systemd 简写)
--until 窗口右边界,常与 --since 成对出现

时间按系统本地时区解释。如果日志时间和你的手表对不上,先查服务器时区(见 15 网络配置)。

验证方法

# 验证时间窗过滤:取最近 1 小时,只显示第一行和总行数
$ sudo journalctl --since "1 hour ago" --no-pager | head -1
$ sudo journalctl --since "1 hour ago" --no-pager | wc -l
预期输出:
Sep 29 09:01:02 myserver CRON[4102]: (root) CMD (cd / && run-parts --report /etc/cron.hourly)
137

首行时间落在 1 小时内、总行数远小于全天日志量,说明窗口生效。

常见问题

现象 根因 解决
结果为 -- No entries -- 时间窗内确实没有日志,或时区理解错 拉大窗口重试;核对 date 输出的时区
--since 报错无法解析 引号缺失或格式拼错 时间串必须加引号,格式参照参数说明表
查到的日志时间「超前/落后几小时」 服务器是 UTC 时区 统一时区,见 15 网络配置

按 unit 查询(重点)

是什么

journalctl -u 服务名 只看某一个 systemd 服务的日志。这是排查服务起不来的第一命令:服务启动失败时,它自己往 stdout/stderr 打的报错、systemd 替它记的死亡原因,全都能用 -u 一网打尽。它和 40 systemd 的 systemctl status 是黄金搭档——status 告诉你「挂了」,journalctl 告诉你「为什么挂」。

怎么做

# 看 nginx 的全部日志
$ sudo journalctl -u nginx

# 看最近 20 行并直达末尾(最常用组合)
$ sudo journalctl -u nginx -n 20 -e

# 实时盯 nginx 日志(改配置反复重启调试时的标配)
$ sudo journalctl -u nginx -f

# 同时看多个服务,把关联服务的日志按时间排在一起
$ sudo journalctl -u mysql -u redis

# 查服务为什么起不来:限定本次开机,只看这个 unit
$ sudo journalctl -u myapp -b --no-pager

-u 可叠加多个,unit 名不带 .service 后缀也行。普通用户自己启动的 --user 服务用 journalctl --user -u 服务名 查看,服务器部署场景较少,知道即可。

参数说明

参数 说明
-u 名称 按(unit)过滤;一个 -u 一个服务,可写多个
-u 名称 -f 过滤 + 实时跟踪,调试服务时最常用
-u 名称 -b 限定本次开机,便于聚焦「这次启动发生了什么」
--user 查用户级(非系统级)服务的日志

验证方法

# 不分页查看 nginx 最近 5 行
$ sudo journalctl -u nginx -n 5 --no-pager
预期输出:
-- Logs begin at Mon 2026-09-01 09:00:01 CST, end at Tue 2026-09-29 10:00:00 CST. --
Sep 29 09:30:01 myserver systemd[1]: Reloading A high performance web server and a reverse proxy server.
Sep 29 09:30:01 myserver systemd[1]: Reloaded A high performance web server and a reverse proxy server.
Sep 29 09:45:00 myserver systemd[1]: nginx.service: Deactivated successfully.
...

输出里只出现 nginx 相关条目,说明 -u 过滤生效。

常见问题

现象 根因 解决
journalctl -u 应用名 输出 -- No entries -- 应用写自己的日志文件,没往 stdout/stderr 打印——journalctl 只收 stdout/stderr 和 syslog,写文件的应用日志不会进 journal 去 /var/log/应用名/ 看文件,查法见 52 日志查询技巧
服务明明在跑却查不到日志 unit 名写错(如把 mysql 写成 mysqld) 用 `systemctl list-units --type=service
日志停在服务启动那一刻 服务把日志写到文件后不再输出 stdout 同第一条,改看文件日志或调整应用日志输出

按优先级查询

是什么

上一章讲过日志八级。journalctl 用 -p 直接按级别过滤,一行命令只留「值得看的」。排错先看 err 以上,这条经验在这里落地。

怎么做

# 只看 err(3)及以上:emerg/alert/crit/err
$ sudo journalctl -p err

# 本次开机内的 err 及以上
$ sudo journalctl -p err -b

# 范围写法:warning 到 emerg 之间
$ sudo journalctl -p warning..emerg

# 组合:本次开机的 err 及以上,看 nginx 的
$ sudo journalctl -u nginx -p err -b --no-pager

参数说明

-p 接受级别名(emerg、alert、crit、err、warning、notice、info、debug)或数字(0-7):

写法 含义
-p err err 及更严重的所有日志(即 0-3)
-p warning..emerg 区间,4 到 0 级
-p 3 与 -p err 等价

验证方法

# 本次开机 err 及以上的日志条数
$ sudo journalctl -p err -b --no-pager | wc -l
预期输出:
23

输出远小于 journalctl -b | wc -l 的结果,说明过滤生效。

常见问题

现象 根因 解决
-p err 结果为空但服务确实挂了 应用把报错打成了普通 info 说明应用日志不规范,改用 -g 关键词搜(见下节)
-p emerg..warning 报错 优先级区间写反了(必须从轻到重:warning..emerg) 调换顺序,写成 -p warning..emerg

按开机批次查询:-b

是什么

服务器半夜重启过,你想知道重启前发生了什么。journald 给每次开机编了号(0 = 本次,-1 = 上一次……),-b 按批次取日志——这是文本日志做不到的,因为文本文件里两次开机的日志混在一起。

怎么做

# 只看本次开机以来的日志(默认行为就是 -b 0)
$ sudo journalctl -b

# 上一次开机的日志:半夜重启后查原因必备
$ sudo journalctl -b -1

# 列出 journal 里保存的所有开机记录
$ sudo journalctl --list-boots

# 上次开机的最后 50 行:往往就是崩溃/重启前的现场
$ sudo journalctl -b -1 -n 50 --no-pager

参数说明

写法 含义
-b / -b 0 本次开机
-b -1 上一次开机
-b -2 上上次,依此类推
--list-boots 列出所有开机批次及时间范围

验证方法

# 列出开机记录
$ sudo journalctl --list-boots
预期输出:
-2 8a3f... Mon 2026-09-15 09:00:01 CST—Wed 2026-09-24 03:12:44 CST
-1 c41d... Wed 2026-09-24 03:15:02 CST—Sun 2026-09-28 02:40:11 CST
 0 97e2... Sun 2026-09-28 02:42:03 CST—Tue 2026-09-29 10:00:00 CST

每行一个开机批次:负号开头是历史开机,0 是当前。-1 行的结束时间与 0 行的开始时间之间的断档,就是上次停机时刻。

常见问题

现象 根因 解决
--list-boots 只有一行 0 日志没持久化,历史开机记录丢了 开启持久化,见本章「磁盘占用与清理」节
-b -1 报错不存在 该批次已被清理或从未持久化 核对 --list-boots 输出中的有效编号
上次开机日志很少 上次开机太早,早于持久化开启时间 无解,今后开启持久化即可积累

按字段与关键词查询

是什么

journald 给每条日志打了结构化字段(进程、PID、用户……),journalctl 支持用 字段=值 精确定位,还有 -g 做关键词搜索。当「按服务、按时间」都框不住目标时,字段过滤是最后的手术刀。

怎么做

# 按进程名过滤:所有 ssh 相关进程的日志
$ sudo journalctl _COMM=ssh

# 按 PID 过滤:某次崩溃进程的完整生命周期
$ sudo journalctl _PID=1234

# 按用户 ID 过滤:uid 1000 用户相关的日志
$ sudo journalctl _UID=1000

# 关键词搜索(grep 模式),大小写不敏感
$ sudo journalctl -g -i "out of memory"

# 组合:本次开机内含 error 字样的 nginx 日志
$ sudo journalctl -u nginx -b -g error --no-pager

参数说明

写法 说明
_COMM=名 进程(可执行文件)名,如 ssh、sshd、nginx
_PID=号 进程 ID,精确定位某次运行
_UID=号 用户 ID;id -u 用户名 可查
-g PATTERN 在消息正文里做模式匹配(需 systemd 237+,v249 支持);配合 -i 忽略大小写

常用字段远不止这些(journalctl --fields 可列出全部),部署工作中以上四个最常用。

验证方法

# 验证字段过滤:查 sshd 进程最近 5 行
$ sudo journalctl _COMM=sshd -n 5 --no-pager
预期输出:
Sep 29 09:12:33 myserver sshd[3901]: Accepted publickey for deploy from 203.0.113.5 port 51234 ssh2
Sep 29 09:12:33 myserver sshd[3901]: pam_unix(sshd:session): session opened for user deploy
...

每行进程名都是 sshd,说明 _COMM= 过滤生效。

常见问题

现象 根因 解决
-g 报不支持的选项 systemd 版本过老(低于 237) Ubuntu 22.04 不存在此问题;老系统用 journalctl 管道给 grep
_PID= 查不到 PID 每次重启都变,旧 PID 已无意义 配合 -b 用,或改用 _COMM=
_COMM=ssh 结果为空 服务进程名是 sshd 不是 ssh 先 ps 或 systemctl status 确认进程真实名字

输出格式控制:-o

是什么

journalctl 默认输出是给人看的;但要做统计、喂给脚本、导出分析时,格式就得换。-o 控制输出格式,四个常用档位覆盖「人看」和「机读」。

怎么做

# 紧凑单行时间戳(带时区偏移),日常推荐的机器友好格式
$ sudo journalctl -u nginx -n 5 -o short-iso --no-pager

# 只看消息体,不带时间主机等前缀(适合管道 grep)
$ sudo journalctl -u nginx -n 5 -o cat --no-pager

# 机读 JSON,给脚本和日志平台用
$ sudo journalctl -u nginx -n 1 -o json --no-pager

# JSON 美化缩进版,人工检查字段时用
$ sudo journalctl -u nginx -n 1 -o json-pretty --no-pager

参数说明

格式 说明 适用
(默认) 类 syslog 的人类可读格式 人直接看
short-iso ISO 8601 时间戳(如 2026-09-29T10:00:00+08:00),带时区 推荐;跨时区对时间线
cat 只输出消息正文 管道给 grep/awk
json / json-pretty 每条一个 JSON 对象 / 带缩进 程序处理、导出分析

验证方法

# 对比同一查询的两种格式
$ sudo journalctl -u nginx -n 1 -o short-iso --no-pager
预期输出:
2026-09-29T09:30:01+08:00 myserver systemd[1]: Reloading A high performance web server.
$ sudo journalctl -u nginx -n 1 -o cat --no-pager
预期输出:
Reloading A high performance web server.

常见问题

现象 根因 解决
-o json 输出挤成一行无法读 json 格式默认单行 换 json-pretty,或管道给 python3 -m json.tool
想把全部日志导出成文本 用 -o 不够,还要指定范围 sudo journalctl -o short-iso > all.log

磁盘占用与清理(重点)

是什么

journald 默认上限为磁盘大小的 10%(封顶 4G),并非无限增长;但小磁盘机器上这个默认值仍可能偏大——磁盘被 journal 挤占后,数据库写不进、服务起不来,全服务器瘫痪。所以「显式收紧 journal 上限」是每台服务器开局的推荐配置,不是可选项。

怎么做

先看现状,再做限制:

# 查 journal 当前占了多少磁盘
$ sudo journalctl --disk-usage

方法一(推荐):配置文件设永久上限。 编辑 /etc/systemd/journald.conf,在 [Journal] 段设置:

[Journal]
# 日志最多占 500M,超出自动删最旧的
SystemMaxUse=500M
# 同时开启持久化:重启后日志不丢(否则默认只有本次开机的)
Storage=persistent

生效需要重启 journald:

$ sudo systemctl restart systemd-journald

方法二:一次性清理。 立刻把 journal 压缩到指定大小或时间范围:

# 只保留 200M 的最新日志,其余删掉
$ sudo journalctl --vacuum-size=200M

# 只保留最近 30 天
$ sudo journalctl --vacuum-time=30d

注意:方法二只清一次,不改上限,过几个月又会涨回来。所以方法二适合救急,方法一才是治本,两者常常配合用。

参数说明

配置/命令 说明
SystemMaxUse=500M journal 持久化的磁盘上限,超出删最旧;500M、2G 均可
Storage=persistent 日志持久化到 /var/log/journal,重启不丢;默认 auto(目录不存在就不持久)
--disk-usage 显示 journal 当前占用
--vacuum-size=200M 清理到只剩 200M(一次性)
--vacuum-time=30d 清理掉 30 天前的日志(一次性)

[!WARNING] journald 默认上限为磁盘 10%、封顶 4G,对小磁盘机器仍可能偏大。新装机建议显式设置 SystemMaxUse(如 500M-2G,按磁盘大小酌情),避免数月后 journal 挤占业务数据空间。

[!IMPORTANT] 默认 Storage=auto 且 /var/log/journal 目录不存在时,日志只存内存,重启即全部丢失。服务器半夜重启后想用 -b -1 查原因,前提就是持久化已开启——这也是部署完成后第一天的功课。

验证方法

改完配置后三步验证:

# 1. 确认 journald 正常运行
$ systemctl status systemd-journald --no-pager

# 2. 确认持久化目录已建立
$ ls -lh /var/log/journal/

# 3. 确认上限生效后的占用
$ sudo journalctl --disk-usage
预期输出(status):
* systemd-journald.service - Journal Service
     Active: active (running) since Tue 2026-09-29 08:00:00 CST; 2h ago

预期输出(disk-usage):
Archived and active journals take up 412.0M in the file system.

占用值小于等于 SystemMaxUse 即符合预期。

常见问题

现象 根因 解决
重启后 journalctl 只剩本次开机日志 Storage=auto 且 /var/log/journal 不存在 建目录并设 Storage=persistent,重启 journald
磁盘被 journal 撑满 没设 SystemMaxUse 先 --vacuum-size 救急,再配 SystemMaxUse 治本
改了 journald.conf 不生效 配置写错了段,或没重启 journald 确认写在 [Journal] 段下,执行 sudo systemctl restart systemd-journald
--vacuum-size 清完占用反而没降多少 日志文件被占用无法立即收缩,或活跃 journal 未归档 重启 journald 让其归档后再清理;属正常现象

本章命令速查

命令 作用
journalctl 分页浏览全部日志(less 中 / 搜索、G 末尾、q 退出)
journalctl -n 50 / -e 看最近 50 行 / 直达末尾
journalctl -f / -r 实时跟踪 / 反序显示
journalctl --since "1 hour ago" 按时间窗过滤
journalctl -u nginx / -u a -u b 按服务过滤 / 多服务(排服务故障第一命令)
journalctl -p err -b 本次开机的 err 及以上
journalctl -b -1 上一次开机的日志(查重启原因)
journalctl --list-boots 列出所有开机批次
journalctl _COMM=sshd / _PID= / _UID= 按字段精确过滤
journalctl -g 关键词 全文模式搜索
journalctl -o short-iso / -o cat / -o json 切换输出格式
journalctl --disk-usage 查看 journal 磁盘占用
journalctl --vacuum-size=200M / --vacuum-time=30d 一次性清理(不改上限)
systemctl restart systemd-journald 修改 journald.conf 后重启生效

相关章节

📄 应用日志查看与过滤技巧 52-log-query-recipes.md


title: 52 应用日志查看与过滤技巧 updated: 2026-09 applies-to: Ubuntu 22.04 LTS

系统日志有 journalctl 这把瑞士军刀,但应用日志(写文件的那些)只能靠基础文本工具硬查。好消息是:三板斧——tail、grep、less——就能覆盖 90% 的场景。本章是「场景 → 命令」式的速查手册,建议对照自己服务器上的日志边看边敲。

[!IMPORTANT] 本章所有示例的 app.log 泛指应用日志文件,请替换成你自己的路径(如 /var/log/myapp/app.log)。读别人目录下的日志需要权限:加 sudo,或把自己加入 adm 组(见 50 日志体系基础)。

日志查询三板斧

是什么

应用日志排查的固定套路,由三个工具分工完成:

  • tail —— 看最新:日志是追加写的,最新内容永远在文件末尾;
  • grep —— 过滤:几百万行里只留含 ERROR 的那几百行;
  • less —— 浏览:打开整份日志慢慢翻、搜、跳。

这三板斧解决「单机、看」的问题。当需求升级到「统计今天每种错误出现了多少次」「跨几十个文件聚合分析」,就该上 53 日志统计分析 的统计手段了。

怎么做

三者的基本姿势:

# tail:看日志最后 100 行(-n 指定行数)
$ tail -n 100 app.log

# grep:只留含 ERROR 的行
$ grep ERROR app.log

# less:分页浏览,后续节详讲
$ less app.log

参数说明

命令 关键参数 说明
tail -n N 显示末尾 N 行,-n 100 可简写为 -100
tail -f / -F 跟踪新内容(两者区别见下节,重要)
grep -i 忽略大小写
grep -v 反选,排除匹配行
grep -n 显示行号
less /关键词、n、N 搜索、下一个、上一个匹配

验证方法

用 logger 或自己的应用造一条测试日志,确认工具链工作正常:

# 追加一条测试行到日志
$ echo "2026-09-29 10:00:00 ERROR [test] 手工造的测试日志" >> app.log

# 用 grep 能找到它
$ grep "手工造的测试日志" app.log
预期输出:
2026-09-29 10:00:00 ERROR [test] 手工造的测试日志

常见问题

现象 根因 解决
grep 卡住半天没结果 日志文件太大(GB 级) 先用 less + /搜索,或按时间段缩小范围(见后文)
输出哗哗刷屏看不清 没过滤直接 cat/less 到末尾 用 grep 收敛,或 tail 只看末尾
提示 Permission denied 无读权限 sudo,或加入 adm 组

实时跟踪(重点)

是什么

调试的黄金姿势:一边操作,一边盯着日志滚动。页面上点一下按钮,马上看日志里打出了什么。tail -f 是这里的主角,但它有个致命的坑——日志轮转,必须用 -F 才能躲过。

怎么做

# 跟踪 app.log 的新增内容(Ctrl+C 退出)
$ tail -f app.log

# 跟踪会轮转的日志,必须用 -F(原因见参数说明)
$ tail -F app.log

# 同时跟踪两个文件,tail 会以 ==> 文件名 <== 分隔显示
$ tail -F app.log error.log

# 边跟踪边过滤:只看 ERROR,且实时输出
$ tail -f app.log | grep --line-buffered ERROR

[!TIP] 记住一条铁律:跟踪会轮转的日志,永远用 tail -F。logrotate 一旦切割日志,-f 就会悄悄失联,你会误以为「应用没在打日志」,白白排查半天。用 -F 没有任何代价,养成习惯即可。

参数说明

-f 与 -F 的区别(本章最重要的一处): 日志轮转后,app.log 会被改名成 app.log.1,再新建一个空的 app.log 继续写。-f(follow)跟踪的是文件描述符——也就是旧文件,改名后它照样盯着已经不再更新的 app.log.1,从此再无输出,这叫「失联」。-F(follow + retry)跟踪的是文件名——发现旧文件不更新就重试打开同名新文件,轮转后无缝衔接。

写法 跟踪对象 轮转后表现
tail -f app.log 文件描述符(旧文件) 失联,不再有新输出
tail -F app.log 文件名 自动切换到新文件,继续跟踪

--line-buffered 的必要性: 管道中的 grep 为了效率会攒够一大块(几 KB)才输出一次。交互式跟踪时,这意味着日志要等一会儿才「跳」出来,看起来像卡顿。--line-buffered 强制 grep 收到一行就立刻输出,恢复实时感。

less +F app.log 是另一个好用的跟踪姿势:进入 less 并自动跟踪;中途按 Ctrl+C 停下来用 /关键词 搜索,看完按 F 恢复跟踪——比 tail -f 只能 Ctrl+C 重来灵活得多。多文件重度跟踪可装 multitail(多窗口同屏,一句带过,sudo apt install multitail)。

验证方法

# 开两个终端:终端 A 跟踪,终端 B 造日志
$ tail -F app.log | grep --line-buffered ERROR
# 终端 B:写入一条测试行
$ echo "2026-09-29 10:01:00 ERROR [test] 实时跟踪验证" >> app.log
预期输出(终端 A 立刻滚出):
2026-09-29 10:01:00 ERROR [test] 实时跟踪验证

常见问题

现象 根因 解决
tail -f 突然没输出了 日志被轮转,-f 跟着旧文件失联 改用 tail -F,从此不再失联
管道 grep 后输出一阵一阵的 grep 块缓冲导致的延迟 加 --line-buffered
跟踪时文件被删了报错 应用重建了日志文件 -F 会自动重试;确认应用日志路径没配错

上下文查看(重点)

是什么

grep Exception 有个经典陷阱:你只 grep 到了「Exception 那一行」,而 Java/Python 的异常堆栈是后面的十几行——单独一行 Exception 几乎没有排查价值,有价值的是堆栈。所以要给 grep 加上下文:把它前后的行一起捞出来。

怎么做

-A 取匹配行之后的 N 行(After),-B 取之前 N 行(Before),-C 前后各 N 行(Context):

# 抓 Exception 及其后 20 行堆栈(最常用组合),再交给 less 慢慢看
$ grep -A 20 -i "exception" app.log | less

# 报错前 3 行:看看出错前程序在干什么
$ grep -B 3 ERROR app.log

# 前后各 10 行,完整的现场
$ grep -C 10 "Connection refused" app.log

[!TIP] 遇到异常先条件反射:grep -A 20 -i "exception" app.log | less。Java 堆栈动辄几十行,-A 20 是经验起步值,不够就加大。加 -i 是因为有的框架打的是 EXCEPTION 或小写。

参数说明

参数 全称 说明
-A N After 每个匹配行后附 N 行
-B N Before 每个匹配行前附 N 行
-C N Context 前后各 N 行
-- — 输出分隔符,不是参数:grep 在多次匹配的分组之间输出 -- 分隔行,便于区分每次匹配的上下文

验证方法

构造一个带堆栈的假日志验证上下文参数:

# 向测试文件写入两行
$ printf 'line before\nERROR boom\nstack frame 1\n' > test.log

# 抓 ERROR 及其后 1 行
$ grep -A 1 ERROR test.log
预期输出:
ERROR boom
stack frame 1

ERROR 后的一行跟着出现,说明 -A 生效。

常见问题

现象 根因 解决
grep 到 Exception 却看不到堆栈 没加 -A,上下文被过滤掉了 grep -A 20 起步
同一个错误重复出现,上下文输出太长 高频报错,每组都带 20 行 管道给 less 翻看,或先 grep -c 看总数
堆栈行本身不含 ERROR,单独 grep 搜不到 这是正常的,堆栈靠上下文带出 不要单独搜堆栈行,搜首行加 -A

大文件浏览策略

是什么

日志大到几百 MB 甚至几 GB 时,cat app.log 会把终端刷成瀑布,grep 全量扫描也很慢。正确策略:先摸清规模,再用 less 定点浏览。less 只加载当前屏,再大的文件也是秒开。

怎么做

# 先看规模:行数和大小,心里有数
$ wc -l app.log
$ du -h app.log

# 打开浏览
$ less app.log

进入 less 后的常用操作:

按键 作用
/ERROR 向下搜索(回车确认)
n / N 下一个 / 上一个匹配
G / g 跳到文件末尾 / 开头
F 进入跟踪模式(等价 tail -f),Ctrl+C 退回浏览
-n 回车 关闭行号显示(超大文件更快)

验证方法

# 摸底:看行数与大小
$ wc -l app.log && du -h app.log
预期输出:
2385171 app.log
1.2G    app.log

230 万行、1.2 GB——这个量级,绝不 cat,直接 less + / 搜索。

常见问题

现象 根因 解决
less 打开超大文件卡顿 行号计算在大文件上开销大 打开后输入 -n 回车关闭行号;或用 sed 直接取段
误操作 cat 了 1 GB 日志 想看内容随手 cat 了 Ctrl+C 中断;养成先 wc -l、du -h 的习惯
/ 搜索等很久 关键词太泛,全文反复扫描 换更精确的关键词,或先按时间定位到区域再搜

历史压缩日志

是什么

logrotate 轮转过的旧日志是 .gz 压缩包(app.log.2.gz……),不能直接 grep。为它们准备的 z 系列工具(zgrep、zcat、zless)用法与原版完全一样,只是先解压再处理——不需要手动 gunzip 出一堆临时文件。

怎么做

# 在压缩日志里搜错误
$ zgrep ERROR app.log.2.gz

# 浏览压缩日志
$ zless app.log.2.gz
# 等价写法:解压到管道
$ zcat app.log.2.gz | less

# 跨多天的归档统计:所有 .gz 里 ERROR 出现了多少次
$ zgrep -h "ERROR" app.log.*.gz | wc -l

参数说明

工具 说明
zgrep = gunzip + grep,参数与 grep 相通(-A、-i 都能用)
zcat = gunzip + cat,常接管道
zless = gunzip + less,分页浏览
-h grep 选项:不显示文件名前缀,让多文件结果干净些

验证方法

# 造一个压缩日志并搜索
$ echo "ERROR in old log" | gzip > app.log.2.gz
$ zgrep ERROR app.log.2.gz
预期输出:
ERROR in old log

常见问题

现象 根因 解决
grep 直接读 .gz 输出乱码 grep 不认 gzip 二进制 换 zgrep
想搜的错既不在当前文件也不在归档里 已超过保留期被 logrotate 删除 无解;重要日志应在轮转策略中延长保留或归档备份

按时间定位

是什么

文本日志没有索引,「看 10:00 到 10:01 之间的日志」不能像 journalctl 那样一条参数搞定。套路是两步:先用 grep 拿到目标时间出现在第几行,再按行号区间取出来。

怎么做

# 第一步:找时间点对应的行号(-n 显示行号,head -1 取第一个)
$ grep -n "2026-09-28 10:0" app.log | head -1
预期输出:
50000:2026-09-28 10:00:01 INFO [http] request received
# 第二步(方式一):sed 按行号区间打印 50000 到 50200 行
$ sed -n '50000,50200p' app.log

# 第二步(方式二):less 直接跳到 50000 行(+Ng 跳行号)
$ less +50000g app.log

# 第二步(方式三):最简单——直接在 less 里搜时间串
$ less app.log
# 进入后输入:/2026-09-28 10:01 回车,按 n 逐个跳

参数说明

写法 说明
grep -n "时间前缀" -n 输出「行号:内容」;用时间前缀而非完整秒,一次命中多条
sed -n 'a,bp' 只打印第 a 到 b 行,p 是 print
less +Ng 启动时跳到第 N 行
head -1 / head -3 只取前 1 行 / 3 行,避免刷屏

验证方法

# 造数据并走一遍完整流程
$ printf '2026-09-29 09:59:00 INFO ok\n2026-09-29 10:00:01 ERROR boom\n' > app.log
$ grep -n "2026-09-29 10:00" app.log | head -1
预期输出:
2:2026-09-29 10:00:01 ERROR boom

行号 2 正确命中,再 sed -n '2,2p' app.log 即可取出该行。

常见问题

现象 根因 解决
grep 时间串一个都搜不到 日志时间格式与写法不一致 先 tail -3 app.log 看真实的格式照着写
定位到的时间点不对 日志时间戳是 UTC,与本地差 8 小时 换算时差再搜;根治见 15 网络配置
sed 取的段落不含目标时间 日志乱序写入(多线程) 适当扩大区间,或在结果里再 grep 一次

多条件过滤与排除噪音

是什么

真实日志里充满噪音:心跳、健康检查、成功请求刷屏,把真正的错误淹没。多条件组合——错误之间取并集、排除已知噪音、统计数量——让日志从瀑布变成清单。

怎么做

# 取并集:ERROR 或 FATAL 都要
$ grep -E "ERROR|FATAL" app.log

# 串联排除:是 ERROR,但排除心跳相关的已知噪音
$ grep ERROR app.log | grep -v "Heartbeat"

# 计数:今天 ERROR 出现了多少次
$ grep -c ERROR app.log

# 组合拳:错误、去重统计按次数排序(简单统计,进阶见 53 章)
$ grep ERROR app.log | sort | uniq -c | sort -rn | head

参数说明

写法 说明
`grep -E "A B"`
grep -v 反选:排除匹配行,可串联多层
grep -c 只输出匹配行数,不显示内容
`sort uniq -c

验证方法

# 造含噪音的日志,验证过滤链
$ printf 'ERROR timeout\nERROR Heartbeat lost\nINFO ok\n' > app.log
$ grep ERROR app.log | grep -v "Heartbeat"
预期输出:
ERROR timeout

Heartbeat 那条被排除,只剩真正的错误。

常见问题

现象 根因 解决
排除噪音后什么都不剩 过滤条件太狠,把错误也排掉了 逐个去掉 -v 条件二分排查
`grep "ERROR FATAL"` 匹配不到 默认 BRE 中 `
统计结果每次不一样 应用还在持续写日志 统计前明确时间范围,或先拷贝一份静态文件

二进制与乱码日志处理

是什么

偶尔遇到「日志」根本不是纯文本:二进制格式(某 SDK 的 trace 文件)或编码不是 UTF-8(GBK 的 Windows 日志)。硬 grep 只会输出乱码或报「binary file matches」。处理思路:二进制先用 strings 抽取可读字符串;编码不对用 iconv 转码。这两招够应付偶发场景,深入日志解析交给 53 日志统计分析。

怎么做

# 从二进制文件中抽出可读字符串再 grep
$ strings trace.bin | grep -i error

# 把 GBK 编码的日志转成 UTF-8 再看
$ iconv -f GBK -t UTF-8 app_gbk.log | less

参数说明

命令 说明
strings 文件 打印文件中所有长度 ≥ 4 的可打印字符串
iconv -f 源编码 -t 目标编码 字符编码转换;-c 可跳过无法转换的字符

验证方法

# 造一个 GBK 编码文件验证转码(iconv 可用时)
$ printf '\xc4\xe3\xba\xc3' | iconv -f GBK -t UTF-8
预期输出:
你好

乱码字节变成了可读中文,说明转码链路可用。

常见问题

现象 根因 解决
grep 提示 binary file matches 文件含二进制字节 grep -a 强制按文本处理,或先 strings
中文全显示成问号或乱码 文件是 GBK,终端是 UTF-8 iconv -f GBK -t UTF-8 转码后查看

实用组合速查表

高频场景直接抄,不必现场推导:

场景 命令
看最新 100 行错误 tail -100 app.log | grep ERROR
实时盯错误(轮转安全) tail -F app.log | grep --line-buffered ERROR
找今天的异常堆栈 grep -A 20 "2026-09-29" app.log | grep -A 20 -i exception | less
看某时间段日志 grep -n "2026-09-28 10:0" app.log | head -1 再 sed -n '起,止p'
轮转前的错误还在吗 zgrep ERROR app.log.2.gz
两个日志按时间合并看 cat a.log b.log | sort | less
统计今天错误总数 grep -c "2026-09-29.*ERROR" app.log
错误去重排行 grep ERROR app.log | sort | uniq -c | sort -rn | head
排除健康检查噪音 grep ERROR app.log | grep -v -E "health|heartbeat"
在大文件里定位关键词 less app.log 后 /关键词,n 跳下一个
监控同时能随时搜索 less +F app.log,Ctrl+C 搜索,F 恢复
找 404 最多的 URL(nginx) grep " 404 " /var/log/nginx/access.log | awk '{print $7}' | sort | uniq -c | sort -rn

本章命令速查

命令 作用
tail -n 100 app.log 看最新 100 行
tail -F app.log 跟踪日志(轮转安全,优先于 -f)
tail -f a.log b.log 同时跟踪多个文件
tail -f app.log | grep --line-buffered ERROR 跟踪并实时过滤
less +F app.log 可暂停搜索的跟踪模式
grep -A 20 -i "exception" app.log 抓异常及堆栈
grep -C 10 关键词 app.log 取前后各 10 行上下文
wc -l app.log / du -h app.log 先摸行数与大小
zgrep ERROR app.log.2.gz 在压缩归档里搜索
zgrep -h ERROR app.log.*.gz | wc -l 跨归档统计错误数
grep -n "时间串" app.log | head -1 定位时间点行号
sed -n '50000,50200p' app.log 按行号取时间段
grep -E "ERROR|FATAL" app.log 多关键词并集
grep ERROR app.log | grep -v "Heartbeat" 排除噪音
grep -c ERROR app.log 错误计数
strings x.bin | grep -i error 二进制文件抽取文本
iconv -f GBK -t UTF-8 x.log 转码查看

相关章节

📄 日志统计与分析 53-log-analysis.md


title: 53 日志统计与分析 updated: 2026-09 applies-to: Ubuntu 22.04 LTS

上一章(52 应用日志查看与过滤技巧)解决的是「找到那几行日志」;本章解决的是「日志整体长什么样」——今天有多少请求?5xx 占比多少?哪个 IP 访问最猛?哪个接口最慢?这些问题的答案都藏在统计数字里,而不在某一行日志里。

[!IMPORTANT] 本章所有示例基于 Nginx 的 access.log。请把 access.log 替换成你自己的路径(如 /var/log/nginx/access.log)。读 Nginx 日志可能需要 sudo(原因见 50 日志体系基础)。

样例行与字段对照

是什么

awk 按「空白字符」把每行拆成字段,$1 是第 1 个字段、$2 是第 2 个……所有统计命令都靠字段编号取数。所以动手之前,先拿一行真实日志,弄清楚每个位置是什么。下面是 Nginx 默认 combined 格式再追加一个请求耗时($request_time)的样例行(截至 2026-09 的主流配置写法):

192.168.1.25 - - [29/Sep/2026:10:00:03 +0800] "GET /api/items?page=2 HTTP/1.1" 200 1876 "https://example.com/list" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36" 0.058

怎么做

把样例行按空格拆开,逐个编号对照:

字段 值 含义
$1 192.168.1.25 客户端 IP
$2 - 远端用户标识(通常为空)
$3 - 认证用户名(通常为空)
$4 [29/Sep/2026:10:00:03 时间开头(带 [)
$5 +0800] 时区(带 ])
$6 "GET 请求方法(带引号)
$7 /api/items?page=2 请求路径与参数
$8 HTTP/1.1" 协议版本(带引号)
$9 200 状态码
$10 1876 响应字节数
$11 "https://example.com/list" Referer(仅当值不含空格时成立,含空格则其后字段整体偏移)
$12 "Mozilla/5.0 ..." User-Agent(仅当值不含空格时才成立;真实 UA 几乎都含空格,完整取法见「User-Agent 与爬虫识别」一节的 -F'"')
$NF 0.058 请求耗时(秒)。UA 含空格时耗时不在固定编号上,只能用 $NF(最后一个字段)取

参数说明

  • $NF 表示「最后一个字段」:日志行很长时数编号容易错,取最后一个字段直接用 $NF 更稳;
  • $(NF-3) 表示「倒数第 4 个字段」,安全排查一节会用到;
  • 如果你的日志格式和本例不同(比如加了 $http_x_forwarded_for),字段号会整体偏移,请先按自己的日志重新做一张对照表,再套用本章命令。

验证方法

不确定字段号对不对?用 awk 打印单个字段看看:

$ awk '{print $1, $9}' access.log
预期输出:
192.168.1.25 200
203.0.113.8 404
192.168.1.25 500

打印出来的正是「IP + 状态码」,说明字段编号没数错。

常见问题

现象 根因 解决
取出的值和预期错位 你的日志格式不是本例格式,字段号偏移 用本节方法重新对照一遍字段编号

统计三件套:核心范式

是什么

本章 80% 的统计都是同一条流水线的变形:

grep 过滤 | awk 取字段 | sort | uniq -c | sort -rn

五个环节分工明确:grep 只留需要的行 → awk 把每行缩成关心的那个字段 → sort 把相同值排到一起 → uniq -c 数个数 → sort -rn 按次数从高到低排。

怎么做

最典型的问题:「哪个 URL 被请求得最多?」

$ awk '{print $7}' access.log | sort | uniq -c | sort -rn | head -10
预期输出:
   3512 /api/items
   2107 /api/items?page=2
   1880 /

参数说明

环节 作用 关键点
sort 排序 uniq 只会合并相邻的相同行,所以必须先 sort
uniq -c 相邻去重并计数 输出格式是「次数 + 值」
sort -rn 按数值逆序 -n 按数字比(否则 9 会排在 10 后面),-r 从大到小
head -10 只看前 10 名 大文件必加,否则全量输出刷屏

[!IMPORTANT] 「先 sort 再 uniq」是初学者最容易踩的坑:uniq 不做全量去重,只合并相邻行。漏掉中间的 sort,你会得到一堆重复的、零零散散的计数结果,而不是一张干净的排行榜。

验证方法

拿样例日志里的几行手工核对:如果 /api/items 在日志里肉眼可见地出现最频繁,而命令的第一名也是它,流水线就是对的。

常见问题

现象 根因 解决
结果里同一个值出现多次 忘了先 sort 在 uniq -c 前补上 sort
排序结果 9 在 10 前面 sort 没加 -n,按字典序比数字 改用 sort -rn
终端刷屏停不下来 没加 head,全量输出 管道末尾加 head -N

流量统计:QPS 与状态码

是什么

运营和排障最常用的两组数字:每分钟请求数(流量曲线,看峰值、看是否被打)和状态码分布(健康度,5xx 多说明服务在报错)。

怎么做

每分钟请求数。Nginx 时间字段 $4 形如 [29/Sep/2026:10:00:03,用 substr 从第 2 个字符开始截 17 个字符,正好得到 [29/Sep/2026:10:00(精确到分钟):

$ awk '{print substr($4,2,17)}' access.log | sort | uniq -c
预期输出:
    812 29/Sep/2026:10:00
    834 29/Sep/2026:10:01
   3010 29/Sep/2026:10:02

状态码分布:

$ awk '{print $9}' access.log | sort | uniq -c | sort -rn
预期输出:
   8631 200
    712 301
    402 404
     88 500
     21 502

只揪出 5xx,连出错的 URL 一起看:

$ awk '$9 >= 500 {print $9, $7}' access.log | sort | uniq -c | sort -rn | head
预期输出:
     88 500 /api/items
     21 502 /api/orders
      4 503 /api/items?page=2

再进一步:每个状态码的每分钟曲线(把分钟和状态码一起取出来再计数),一眼定位「几点几分开始爆炸」:

$ awk '{print substr($4,2,17), $9}' access.log | sort | uniq -c
预期输出:
    790 29/Sep/2026:10:00 200
     22 29/Sep/2026:10:00 500
    340 29/Sep/2026:10:02 500

参数说明

  • substr($4,2,17):从 $4 的第 2 个字符起截 17 个字符。第 1 个字符是 [,跳过;17 个字符恰好是「日/月/年:时:分」;
  • $9 >= 500:awk 允许在 {动作} 前写条件,只有满足条件的行才执行动作——相当于内置的 grep;
  • 想看秒级就把 17 改成 20(29/Sep/2026:10:00:03 含秒共 20 个字符),但秒级输出行数会暴涨,一般加 head 控制。

验证方法

统计总数是否对得上,是最可靠的验证:所有状态码计数相加,应等于日志总行数:

$ wc -l access.log
预期输出:
9854 access.log

上面状态码分布示例中 8631+712+402+88+21 = 9854,对上了。

常见问题

现象 根因 解决
每分钟统计的行数特别碎 忘了 sort,uniq 没合并相邻行 加上 sort
5xx 过滤没结果但明明有 500 $9 位置取错,取到了别的字段 先 awk '{print $9}' 抽查确认

TOP N 排行:客户端 IP 与慢请求

是什么

两类排行榜:谁访问最多(TOP N 客户端 IP,找爬虫、找攻击源、看真实用户分布)和什么请求最慢(TOP N 慢请求,性能优化的靶子)。

怎么做

访问量前 20 的 IP:

$ awk '{print $1}' access.log | sort | uniq -c | sort -rn | head -20
预期输出:
   5201 203.0.113.8
    812 192.168.1.25
    445 198.51.100.3

最慢的请求(耗时从高到低)。本例日志的最后一个字段 $NF 是请求耗时(纯数字秒):

$ awk '($NF+0 >= 1){print $NF, $7}' access.log | sort -rn | head
预期输出:
12.452 /api/report/export
8.310 /api/items?page=100000
3.092 /api/orders

参数说明

  • $NF+0 >= 1:$NF 是字符串,+0 把它转成数字再比较,筛出耗时≥1 秒的请求;如果你的日志把耗时记成 0.058s 这种带单位的格式,可改用 $NF ~ /s$/ 匹配;
  • sort -rn 对「耗时 IP」这种以数字开头的行同样适用,按行首数值逆序;
  • head 默认取前 10 行,head -20 取前 20 行。

验证方法

抽排行榜第一名的 IP,回日志里数一数:

$ grep -c "^203.0.113.8 " access.log
预期输出:
5201

计数一致,说明 $1 取对了。

常见问题

现象 根因 解决
第一名是本机或负载均衡 IP 请求经过了反向代理,$1 是代理地址 改用 X-Forwarded-For 真实 IP 字段(需在 Nginx 日志格式里加 $http_x_forwarded_for)
慢请求排行是空的 耗时字段位置不对,或所有请求都小于 1 秒 `awk '{print $NF}' access.log

时间窗截取:只看某几分钟的日志

是什么

出事故时,你只关心「10:00 到 10:05 之间发生了什么」。从上千万行日志里截出这个时间窗,是日志分析的高频动作。有两种方法。

怎么做

方法一:awk 字符串比较,精确截取(推荐):

$ awk '$4 >= "[29/Sep/2026:10:00" && $4 < "[29/Sep/2026:10:05"' access.log > window.log

方法二:grep 粗取,先把范围收窄:

$ grep "29/Sep/2026:10:0" access.log > window.log

参数说明

方法一为什么能用字符串比较比时间?因为这种日志格式是固定的:日/月/年:时:分:秒 每一位的字符宽度含义都固定,月永远是三个字母、数字都补齐到位——字符串的字典序就等于时间先后序。"[29/Sep/2026:10:03" >= "[29/Sep/2026:10:00" 和「10:03 晚于 10:00」是同一件事。前提是日志保持这个固定宽度格式;如果你的时间格式是 9/Sep 这种不补零的,字典序就不等于时间序,方法一会出错。

  • 方法一:$4 是时间字段,左右都带 [,所以条件里也要写 [;
  • 方法二:grep 匹配子串,10:0 能同时命中 10:00~10:09 整十分钟,快但边界粗,适合先粗后细。

[!TIP] 实战顺序:先用方法二粗取一个较小的时间段存成 window.log,后面所有统计都在这个小文件上跑——比每次都全量扫描大日志快得多。

验证方法

截取完检查窗口两端是否正确:

$ head -1 window.log
$ tail -1 window.log
预期输出:
192.168.1.25 - - [29/Sep/2026:10:00:01 +0800] "GET / HTTP/1.1" 200 1876 "-" "Mozilla/5.0" 0.003
203.0.113.8 - - [29/Sep/2026:10:04:59 +0800] "GET /api/items HTTP/1.1" 500 320 "-" "python-requests/2.31" 1.412

首行是 10:00 出头、末行是 10:04 结尾,窗口截对了。

常见问题

现象 根因 解决
awk 截取结果为空或不全 日志时间不补零,字典序≠时间序 改用 grep 粗取法,或先确认格式固定
截到了前一天/后一天的数据 条件里没带日期,只写了 10:00 条件必须写全年月日:[29/Sep/2026:10:00

错误日志聚合:TOP N 异常类型

是什么

应用日志(如 Java 的异常堆栈)一行就是一个报错,成千上万行里到底有几种错、各出现多少次?这需要对「匹配到的片段」聚合,而不是对整行聚合。

怎么做

统计出现最多的异常类型:

$ grep -oE "Exception: [A-Za-z.]+" app.log | sort | uniq -c | sort -rn | head
预期输出:
    412 Exception: NullPointerException
    187 Exception: TimeoutException

参数说明

  • grep -o:只输出匹配到的部分,而不是包含匹配的整行——这是本命令的灵魂。没有 -o,每行日志整行都被输出,时间戳、堆栈全都不一样,uniq 根本合并不了;
  • grep -E:扩展正则,[A-Za-z.]+ 表示「字母或点号,一个及以上」,正好吃下 java.lang.NullPointerException 这类全限定名。该模式假定日志形如「Exception: 类型名」(常见于自定义日志封装);Java 原生堆栈的异常行是 java.lang.NullPointerException: msg 开头,建议改用 [A-Za-z.]+Exception 类模式匹配;
  • 想按「哪种报错出现在哪几小时」交叉分析,可以把前面学的时间字段一起输出再计数。

验证方法

核对某一种异常的真实出现次数:

$ grep -c "NullPointerException" app.log
预期输出:
412

常见问题

现象 根因 解决
统计不出类别,全是 1 没加 -o,整行参与 uniq grep 加 -o 只留匹配片段
类别名被截断 正则没覆盖数字或下划线 扩大字符集,如 [A-Za-z0-9_.]+

按字段求和与平均

是什么

前面都是「数行数」,这一节是「算数值」:平均响应体多大?哪个接口累计吃掉了最多的耗时?这类需求用 awk 的累加变量和关联数组完成。

怎么做

平均响应大小($10 是响应字节数):

$ awk '{sum+=$10} END {print sum/NR}' access.log
预期输出:
1523.7

按接口($7)累计总耗时,找出「耗时大户」:

$ awk '{a[$7]+=$NF} END {for(k in a) print a[k], k}' access.log | sort -rn | head
预期输出:
45210.3 /api/report/export
12890.5 /api/items
4102.2 /api/orders

参数说明

  • sum+=$10:awk 里变量不用声明,+= 对每行累加 $10 的值;
  • NR:awk 内置变量,当前已读的总行数,到 END 时就是全部行数,sum/NR 即平均值;
  • a[$7]+=$NF:a 是关联数组(下标可以是字符串的数组),效果是「以 URL 为 key,把每行的耗时累加进去」;
  • END {for(k in a) ...}:END 块在所有行读完之后执行一次;for(k in a) 遍历数组所有 key,打印「累计耗时 + URL」。

验证方法

用小样本验证:造一个两行的文件手算核对。

$ printf 'x - - [t] "GET /a HTTP/1.1" 200 100 "r" "ua" 1.0\nx - - [t] "GET /b HTTP/1.1" 200 300 "r" "ua" 3.0\n' > t.log
$ awk '{sum+=$10} END {print sum/NR}' t.log
预期输出:
200

(100+300)/2=200,公式正确,验证完删除测试文件 rm t.log。

常见问题

现象 根因 解决
平均值算出 0 或除零错 字段号取到了非数字列 对照字段表确认 $10 确实是字节数
累计耗时排序乱 结果是「数值 + 文本」,忘了 -n 用 sort -rn 按行首数字排

User-Agent 与爬虫识别

是什么

日志的第 12 个字段是 User-Agent(浏览器标识)。正常的浏览器 UA 千姿百态但都含 Mozilla;爬虫、扫描器、脚本则常常报出 python-requests、curl、Go-http-client 或干脆留空。按 UA 排行,能快速发现「谁在机器式地扫你」。

怎么做

UA 字段内部含空格,普通空格分列取 $12 只能取到 Mozilla/5.0 一个词。改用 -F'"' 按双引号分列:整行被引号切成若干段,请求行、Referer、UA 都成了完整字段,UA 恰好是第 6 段($6):

$ awk -F'"' '{print $6}' access.log | sort | uniq -c | sort -rn | head
预期输出:
   6210 Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36
   1402 python-requests/2.31.0
    553 Go-http-client/2.0
    402 -
    187 sqlmap/1.8

python-requests 数千次、还有 sqlmap(知名注入扫描工具,截至 2026-09 仍是最常见的扫描器之一)——这两位就该去防火墙里拉黑了。

参数说明

  • -F'"':指定 awk 的字段分隔符为双引号。按引号切分后:$1 是引号前的头、$2 是请求行(GET /xx HTTP/1.1)、$4 是 Referer、$6 是 UA;
  • - 是「UA 为空」的请求,大多数客户端工具不发 UA 头,大量空 UA 通常不是真人;
  • UA 排行只做粗筛,确认可疑后再 grep 该 UA 抽出具体请求路径和 IP 看行为。

验证方法

确认按引号分列的段号:

$ awk -F'"' '{print $2, $6}' access.log | head -3
预期输出:
GET / HTTP/1.1 Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36
GET /api/items HTTP/1.1 python-requests/2.31.0

第 2 段是请求行、第 6 段是 UA,分列正确。

常见问题

现象 根因 解决
$6 取出来是请求行而不是 UA 引号配对数量和预期不同(如 Referer 缺失) awk -F'"' '{print $2,$4,$6}' 抽查每段内容再定段号
想过滤 UA 但引号转义报错 shell 引号嵌套冲突 用 -F'"' 这种「外单内双」写法,避免外层也是双引号

安全排查:登录失败统计

是什么

/var/log/auth.log 记录了 SSH 登录。公网机器每天被成千上万次暴力破解尝试是常态(截至 2026-09 依然如此),统计「谁失败得最多」,是安全自查的基本功。

怎么做

登录失败来源 IP 排行。auth.log 的失败行形如 ... Failed password for invalid user admin from 203.0.113.5 port 41024 ssh2,倒数第 4 个字段($(NF-3))正是来源 IP:

$ grep "Failed password" /var/log/auth.log | awk '{print $(NF-3)}' | sort | uniq -c | sort -rn | head
预期输出:
   8412 203.0.113.5
    320 198.51.100.77
     45 192.168.1.25

参数说明

  • $(NF-3):括号必不可少。$NF-3 会被理解成「$NF 的值减 3」(对文本是 0),而 $(NF-3) 才是「先算 NF-3 再取那个字段」;
  • 字段是从行尾倒数定位的,所以不受用户名长短(for root / for invalid user admin)影响——这就是为什么不用固定的 $10。

验证方法

抽一个 IP 确认它真的在失败记录里:

$ grep "Failed password" /var/log/auth.log | grep "203.0.113.5" | head -2
预期输出:
Sep 29 03:11:02 web sshd[2881]: Failed password for invalid user admin from 203.0.113.5 port 41024 ssh2
Sep 29 03:11:05 web sshd[2917]: Failed password for root from 203.0.113.5 port 41038 ssh2

常见问题

现象 根因 解决
输出的是端口数字而不是 IP 你的系统日志格式不同,倒数位置偏移 手工看一行失败日志,重新数 NF 位置
排行第一名是自己的 IP 自己忘了密码重试多次 正常现象,关注陌生公网 IP 即可
找不到 auth.log rsyslog 未安装或未运行 22.04 默认装有 rsyslog,auth.log 应存在(24.04 起才默认不装);先 systemctl status rsyslog 确认在运行,或改查 journald:journalctl -u ssh(见 51 journalctl)

发现大量失败尝试后的加固手段(改端口、禁密码登录、fail2ban)见 18 SSH 加固。

结果输出与对比

是什么

统计结果直接打在终端上是「看完即走」;把结果存进文件,才能留档和对比——比如今天的 TOP IP 和昨天比,新增面孔就是可疑信号。

怎么做

用重定向 > 把结果写到文件:

$ awk '$9 >= 500 {print $9, $7}' access.log | sort | uniq -c | sort -rn > today_5xx.txt

对比两天的结果:

$ diff today_5xx.txt yesterday_5xx.txt

参数说明

  • >:把标准输出写入文件,覆盖已有内容;追加用 >>;
  • diff 文件1 文件2:逐行比较,< 开头是文件 1 独有,> 开头是文件 2 独有——新增的错误接口一目了然。

验证方法

$ cat today_5xx.txt | head -3
预期输出:
     88 500 /api/items

常见问题

现象 根因 解决
文件被覆盖,昨天的数据没了 用了 > 留档用 >> 追加,或文件名带上日期
diff 输出看不懂 不熟悉 </> 标记 < 是第一个文件的内容,> 是第二个文件的

超出一行流:专业工具

是什么

本章的一行流命令适合临时、单机、事后的分析。如果你需要实时大盘、持续趋势、跨多台机器聚合,该换专业工具了,不必用 awk 硬扛。

怎么做

按投入从低到高,三个台阶(截至 2026-09):

  • GoAccess:终端里直接跑,实时分析 access.log 生成报表,一行命令零部署,个人服务器首选;
  • Loki + Grafana:日志集中存储 + 图形化查询,适合几台到几十台机器;
  • ELK(Elasticsearch + Logstash + Kibana):功能最全也最吃资源,大规模团队方案。

参数说明

GoAccess 最常用的一条:

$ goaccess /var/log/nginx/access.log --log-format=COMBINED

验证方法

$ goaccess --version
预期输出:
GoAccess - 1.5.5

(版本以 Ubuntu 22.04 官方源 apt install goaccess 安装的为准;官方源外自装的新版以 goaccess --version 实际输出为准,版本号参考 91 版本速查表 的核对方法。)

常见问题

现象 根因 解决
GoAccess 报格式不识别 日志不是标准 combined 格式 用 --log-format 手工描述各字段
ELK 服务器内存吃紧 Elasticsearch 默认堆太大 调低 JVM 堆,或降级用 Loki

本章命令速查

命令 作用
awk '{print $1}' f | sort | uniq -c | sort -rn | head 统计三件套:TOP N 排行
awk '{print substr($4,2,17)}' f | sort | uniq -c 每分钟请求数
awk '{print $9}' f | sort | uniq -c | sort -rn 状态码分布
awk '$9 >= 500 {print $9, $7}' f | sort | uniq -c | sort -rn 5xx 及出错 URL
awk '{print substr($4,2,17), $9}' f | sort | uniq -c 每分钟×状态码曲线
awk '$4 >= "[D/M/Y:H:M" && $4 < "[D/M/Y:H:M"' 按时间窗截取日志
grep -oE "pattern" f | sort | uniq -c | sort -rn 异常类型聚合(-o 是关键)
awk '{sum+=$10} END {print sum/NR}' f 求平均值
awk '{a[$7]+=$NF} END {for(k in a) print a[k],k}' f 按字段分组累计
awk -F'"' '{print $6}' f | sort | uniq -c | sort -rn User-Agent 排行
grep "Failed password" /var/log/auth.log | awk '{print $(NF-3)}' | sort | uniq -c | sort -rn SSH 登录失败 IP 排行
cmd > file / diff a b 结果存档与对比

相关章节

📄 日志轮转(logrotate) 54-logrotate.md


title: 54 日志轮转(logrotate) updated: 2026-09 applies-to: Ubuntu 22.04 LTS

上一章(53 日志统计与分析)教你怎么读日志,本章解决一个更根本的问题:怎么让日志文件不把磁盘吃光。答案是 logrotate——Ubuntu 自带、几乎所有软件包都在用的日志轮转工具。学完本章,你能给任何应用配一条「自动切割、压缩、清理」的日志流水线。

为什么需要日志轮转

是什么

日志是追加写的:服务每处理一个请求就往文件末尾加一行,只进不出。没有轮转机制的日志文件只有一个结局——无限增长。三大件随之而来:

  • 切割:把不断长大的 app.log 定期改名成 app.log-20260929,再新建一个空的 app.log 接着写;
  • 压缩:把切出来的旧文件 gzip 压缩(文本日志压缩比通常 10:1 起);
  • 清理:太老的归档直接删,只保留最近 N 份。

怎么做

先感受一下不轮转的后果——找找你机器上已经失控的日志:

$ sudo du -sh /var/log/* 2>/dev/null | sort -rh | head
预期输出:
14G     /var/log/myapp
3.2G    /var/log/journal
1.1G    /var/log/syslog

(du -sh /var/log/* 的通配只匹配第一层,myapp 显示为目录本身;想看目录内部明细用 sudo du -ah --max-depth=2 /var/log。)一个 14G 的 myapp 目录(里面一个失控的 app.log 就占掉绝大部分),就是下一章(55 磁盘满排查)「磁盘占满」事故的经典起因。

参数说明

轮转解决三个问题:

问题 无轮转的后果 轮转后的效果
单文件过大 grep/less 打开卡死 每个文件都不大,按日期定位
磁盘被吃光 服务写日志失败、进程崩溃 旧文件压缩 + 过期删除,总量有上界
排查困难 混在一个巨文件里 按天切分,天然对应「哪天出的事」

验证方法

确认系统里 logrotate 已就位(Ubuntu 22.04 默认自带,截至 2026-09):

$ logrotate --version
预期输出:
logrotate 3.19.0

常见问题

现象 根因 解决
日志文件几十 GB 才发现 没人看磁盘,应用自己不轮转 按本章配置 logrotate,并加磁盘监控
删了大日志磁盘没释放 进程握着旧文件句柄 见 55 磁盘满排查 的 deleted 场景

工作机制

是什么

logrotate 本身不是常驻进程,而是一个被定时任务周期性拉起的命令。每次运行时,它读取配置,检查哪些日志「该切了」,然后执行切割、压缩、清理。

怎么做

它由两部分驱动:

# Ubuntu 22.04 的定时入口(systemd 系统用 timer 调度)
$ systemctl cat logrotate.timer
预期输出:
# /lib/systemd/system/logrotate.timer
[Unit]
Description=Daily rotation of log files

[Timer]
OnCalendar=daily
Persistent=true

配置则分两层:

# 主配置:全局默认值 + 收编分文件
/etc/logrotate.conf

# 分文件配置:每个软件一份(安装软件包时通常自带)
/etc/logrotate.d/
├── nginx
├── rsyslog
├── mysql-server
└── apt

参数说明

  • /etc/cron.daily/logrotate:传统 cron 系统的每日触发脚本;systemd 系统(包括 Ubuntu 22.04)由 logrotate.timer 每天触发一次,效果等价;
  • /etc/logrotate.conf 末尾有一句 include /etc/logrotate.d,把目录下所有分文件配置收编进来——所以日常你只改 /etc/logrotate.d/ 下的文件,不动主配置;
  • 软件包自带配置(如 nginx、rsyslog)开箱即用;你自己写的应用,需要自己放一份配置进去,这正是本章的重点。

验证方法

确认 timer 在跑、以及上次轮转的时间:

$ systemctl status logrotate.timer
预期输出:
● logrotate.timer - Daily rotation of log files
     Active: active (waiting) since Mon 2026-09-28 06:25:03 CST
    Trigger: Tue 2026-09-29 06:25:11 CST; 8h left

Active: active (waiting) 且有下次触发时间,机制正常。

常见问题

现象 根因 解决
改了配置一直没生效 timer 没跑或机器不到触发点 见本章「调试与验证」,用 -d/-f 手动验证
找不到 /etc/logrotate.d/ logrotate 未安装 sudo apt install logrotate

配置指令详解

是什么

一份 logrotate 配置 = 全局指令 + 针对某个日志文件的块。指令不多,但每条都值得逐个讲透——尤其 size 与 daily 的关系、delaycompress、copytruncate 这几个反直觉的点。

怎么做

先看一个典型配置长什么样:

/var/log/myapp/app.log {
    daily
    rotate 14
    compress
    delaycompress
    missingok
    notifempty
    dateext
    create 0640 appuser appgroup
    postrotate
        systemctl reload myapp
    endscript
}

参数说明

逐条拆解(截至 2026-09,logrotate 3.19 的常用指令):

指令 含义 要点
daily / weekly 每天/每周轮转一次 按时间触发
rotate 14 保留最近 14 份归档 第 15 份出现时最老的被删;daily + rotate 14 ≈ 保留两周
size 100M 超过 100M 才轮转 只看大小:配置了 size 后,时间间隔(daily/weekly)失效,仅当超过尺寸才切
maxsize 100M 与时间指令并用:时间或大小,先到先切 兼得「按天轮转」与「尺寸兜底」的正解
minsize 100M 与时间指令并用:到时间且同时超过尺寸才切 与 maxsize 相反的「且」关系
missingok 日志文件不存在就跳过 不报错。没有它,文件一缺失 logrotate 就返回错误
notifempty 空文件不轮转 省得切出一堆无意义的空归档
compress 归档用 gzip 压缩 文本日志压缩比极高,强烈建议
delaycompress 最近一份归档暂不压缩 见下方专段解释
dateext 归档后缀用日期 app.log-20260929 而不是 app.log.1,一眼看出是哪天的
create 0640 appuser appgroup 切割后新建文件并指定权限和属主 应用是以 appuser 跑的,新文件必须归它,否则它写不进
copytruncate 复制再清空,不动原文件 与 create 模式二选一,见下节
postrotate / endscript 轮转后执行的脚本 用来通知进程重开日志文件

[!IMPORTANT] logrotate 默认每天只被 timer 拉起检查一次——所以配了 size/maxsize 也不是「一到 100M 立刻切」,而是「下次检查时发现超了才切」。要让尺寸条件在当天内及时生效,需把规则挂到 cron.hourly 或配置更高频的 timer。

验证方法

用 -d(dry run)检查配置语法,后面专讲。这里先验证指令拼写:任何拼错的指令都会在 dry run 中报 unknown option。

$ sudo logrotate -d /etc/logrotate.d/myapp 2>&1 | tail -5
预期输出:
reading config info for /var/log/myapp/app.log
Handling 1 logs
rotating pattern: /var/log/myapp/app.log  after 1 days (14 rotations)
empty log files are not rotated, old logs are removed

配置被正确解析、模式与预期一致,语法就没问题。

常见问题

现象 根因 解决
报 unknown option 'daily' 指令缩进或花括号位置错了 指令前用空格缩进,花括号独占一行,分号脚本用 endscript 收尾
轮转从不发生 size 条件没达到,或 daily 还没到期 cat /var/lib/logrotate/status 看上次轮转时间
新日志文件应用写不进 create 的属主/权限与应用进程不匹配 改成运行应用的用户的可写权限

copytruncate vs postrotate:核心难点

是什么

轮转有个绕不开的矛盾:进程打开日志文件后握着文件句柄(fd)一直写,你把文件改了名,进程照样往原名对应的 inode 里写——它不知道文件被换了。logrotate 有两种哲学解决这个问题,选错一种就出事故。

怎么做

方式一(默认,create 模式):改名旧文件 → 新建同名空文件 → postrotate 里通知进程重新打开日志文件:

/var/log/nginx/access.log {
    daily
    rotate 14
    compress
    delaycompress
    missingok
    notifempty
    dateext
    create 0640 www-data adm
    sharedscripts
    postrotate
        # 通知 nginx 重新打开日志文件(USR1 信号)
        [ -f /var/run/nginx.pid ] && kill -USR1 `cat /var/run/nginx.pid`
    endscript
}

方式二(copytruncate 模式):先把原文件内容拷贝成归档,再把原文件原地清空。进程全程不知道发生了什么,继续往原 fd 写(只是文件长度归零了):

/var/log/myapp/app.log {
    daily
    rotate 14
    compress
    delaycompress
    missingok
    notifempty
    dateext
    copytruncate
}

参数说明

两种方式逐项对比:

create + postrotate(默认) copytruncate
原理 改名旧文件、建新文件,通知进程切换 fd 拷贝内容后原地清空,进程无感知
丢日志风险 无(切换瞬间日志也由旧 fd 落盘,不丢) 拷贝到清空之间写入的那几条日志会随清空丢失
通用性 必须知道怎么通知进程(信号/reload 命令) 完全通用,应用不用配合
适用 nginx、rsyslog 等支持信号重开的成熟软件 自研应用、第三方闭源程序、不便 reload 的进程
postrotate 需要 不需要

delaycompress 为什么存在?create 模式下,轮转后到进程切换 fd 之前,可能还有最后几条日志写进旧文件;立刻压缩会导致这几条读不出来或压缩出错。delaycompress 让最近一份归档保持明文,下次轮转时才压缩它——多留一天,避开句柄窗口期。

sharedscripts:多个日志文件匹配同一个块时,postrotate 脚本只执行一次(而不是每个文件执行一次)。

验证方法

强制轮转一次,亲眼看看两种方式各自的产物:

$ sudo logrotate -f /etc/logrotate.d/myapp
$ ls -lh /var/log/myapp/
预期输出:
total 12K
-rw-r----- 1 appuser appgroup    0 Sep 29 10:00 app.log
-rw-r----- 1 appuser appgroup 2.1M Sep 28 23:59 app.log-20260929

app.log 变成 0 字节、昨天的内容进了带日期的归档,轮转成功。

常见问题

现象 根因 解决
轮转后应用还在往归档文件里写 create 模式但没通知进程(缺 postrotate) 补 postrotate,或改用 copytruncate
copytruncate 后日志内容错乱/丢条目 拷贝瞬间应用正在写 可接受就没问题;不可接受就换 create 模式
nginx 轮转后新日志没进新文件 USR1 信号没发出去(pid 文件路径不对) 确认 cat /var/run/nginx.pid 拿到的是真实 pid

完整配置示例:自定义应用

是什么

把你自己的 Java/Python 应用接入 logrotate,只需往 /etc/logrotate.d/ 放一份配置文件。下面是一份可抄的模板,逐行注释。

怎么做

新建 /etc/logrotate.d/myapp(内容如下,假设应用以 appuser 运行、无法重开日志文件,故用 copytruncate):

# 要轮转的日志路径,可以写多个,每行一个
/var/log/myapp/app.log
/var/log/myapp/error.log
{
    # 每天轮转一次
    daily
    # 保留最近 14 份归档,更旧的自动删除
    rotate 14
    # 尺寸兜底:与 daily 并用,时间或大小先到先切
    # (注意 logrotate 默认每天只检查一次,要让它在当天内及时生效需更高频触发,见上文 IMPORTANT)
    maxsize 100M
    # 文件不存在就跳过,不报错
    missingok
    # 空文件不轮转
    notifempty
    # 归档用 gzip 压缩
    compress
    # 最近一份归档延迟到下次轮转才压缩(避开写入窗口)
    delaycompress
    # 归档后缀用日期,如 app.log-20260929
    dateext
    # 拷贝再清空,应用无需配合;接受极小概率的丢日志窗口
    copytruncate
}

[!WARNING] copytruncate 与 postrotate/create 不要混用:同一块里 copytruncate 会让 create 失效,语义混乱。二选一,想清楚再写。

参数说明

  • 配置文件权限应为 0644 root root,logrotate 对配置属主有安全要求,普通用户写的会被忽略;
  • maxsize 100M 与 daily 并存时是「或」:要么到一天,要么到 100M,先到先切——上面模板用的就是它。若换成 size,时间条件会被覆盖、只剩大小维度;想要「按天且必须超过尺寸才切」的「且」关系,用 minsize 100M + daily。

验证方法

dry run 这份新配置,确认它被正确解析、且计划动作符合预期:

$ sudo logrotate -d /etc/logrotate.d/myapp
预期输出:
reading config file /etc/logrotate.d/myapp
Reading state from file: /var/lib/logrotate/status
Handling 2 logs
rotating pattern: /var/log/myapp/app.log  after 1 days (14 rotations)
...
rotating pattern: /var/log/myapp/error.log  after 1 days (14 rotations)

两个日志文件都被纳入、模式正确,配置即生效(真实轮转等 timer 明天执行,或用 -f 立即验证)。

常见问题

现象 根因 解决
配置被无视 放错目录(不在 /etc/logrotate.d/)或权限不对 移到正确目录,chown root:root + chmod 644
归档越来越多没清理 rotate 值太大或没写 rotate 明确保留份数,如 rotate 14
应用重启后日志文件找不到了 应用自己会重建日志,与 create 模式冲突 改用 copytruncate,或让应用支持信号重开

调试与验证

是什么

logrotate 的配置改动一天只被真正执行一次,写错了要等到明天才发现——这是最难受的调试节奏。好在 logrotate 提供了 dry run 和强制执行,把反馈周期缩到几秒钟。

怎么做

三步调试点:

# 第一步:dry run,只打印将要执行的动作,不改任何文件
$ sudo logrotate -d /etc/logrotate.d/myapp

# 第二步:确认无误后,强制立刻轮转一次,验证真实效果
$ sudo logrotate -f /etc/logrotate.d/myapp

# 第三步:查看轮转状态账本,确认时间和记录
$ cat /var/lib/logrotate/status
预期输出:
logrotate state -- version 2
"/var/log/myapp/app.log" 2026-9-29-6:25:11
"/var/log/nginx/access.log" 2026-9-29-6:25:11

参数说明

  • -d(dry run):只打印不执行。每次改完配置必跑一遍,看输出的「rotating pattern」和计划动作是否符合预期;
  • -f(force):无视时间条件,立即轮转一次。注意它会真的切割文件,生产上挑业务低峰执行;
  • /var/lib/logrotate/status:logrotate 的记账本,记录每个日志文件上次轮转的时间。「为什么没轮转?」先查这里——时间没到,logrotate 就是不动手,这是设计行为。

验证方法

强制轮转后检查三件事:归档文件出现、原文件变空、应用仍能正常写入:

$ ls -lh /var/log/myapp/
预期输出:
total 2.2M
-rw-r----- 1 appuser appgroup  512 Sep 29 10:05 app.log
-rw-r----- 1 appuser appgroup 2.1M Sep 28 23:59 app.log-20260929

app.log 是新的小文件且有新内容写入(不是 0 字节挂着),说明应用写日志正常。

常见问题

现象 根因 解决
-d 没报错但真实轮转不发生 dry run 只做语法与计划检查,不代表条件已满足 看 status 里的时间是否到期;不等到期可用 -f
-f 之后立即又想再切一次没反应 正常,status 已记录新时间 这正是记账机制在工作,无需处理
日志里有 error: stat 失败 配置里的日志路径不存在 核对路径,或加 missingok

与应用日志框架的关系

是什么

很多日志框架(logback、log4j2、Python logging 的 RotatingFileHandler、Node 的 winston-daily-rotate-file)自带「按大小/按天滚动」能力。logrotate 和应用内置滚动是竞争关系,只能二选一。

怎么做

选型判断:

  • 应用不会自己滚动(输出重定向、普通 FileHandler、nohup app > app.log):用 logrotate,即本章方案;
  • 应用内置滚动(logback 的 RollingFileAppender 等):让框架自己管,配好 maxHistory/totalSizeCap 清理旧归档,logrotate 不要碰这个文件。
<!-- logback 示例:框架自己滚动并清理,无需 logrotate 介入 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
        <fileNamePattern>app.%d{yyyy-MM-dd}.log</fileNamePattern>
        <maxHistory>14</maxHistory>
        <totalSizeCap>2GB</totalSizeCap>
    </rollingPolicy>
</appender>

参数说明

  • maxHistory:框架侧的「保留份数」,等价于 logrotate 的 rotate 14;
  • totalSizeCap:归档总大小上限,logrotate 反而没有直接的等价物——这是框架滚动的一个优势。

验证方法

确认应用是否内置滚动,看它旁边有没有自动出现的归档:

$ ls -lh /var/log/myapp/
预期输出:
total 20M
-rw-r----- 1 appuser appgroup 2.1M Sep 29 10:00 app.2026-09-29.log
-rw-r----- 1 appuser appgroup 2.0M Sep 28 23:59 app.2026-09-28.log

日期后缀的归档在自动增长——应用在内置滚动,别再叠 logrotate。

常见问题

现象 根因 解决
同一天的日志出现两套归档,内容交错 logrotate 与框架在双重滚动 删掉其中一方(推荐留框架侧)
框架滚出来的文件从不清理 没配 maxHistory/totalSizeCap 补上清理策略
logrotate 切割后框架报 IO 错误 框架握着 fd 不放,与 create 冲突 框架自滚的场景直接移除 logrotate 配置

[!WARNING] 双重滚动会互相打架:logrotate 把 app.log 改名,框架却还按自己的命名规则滚动同名文件,轻则归档命名混乱、重则日志丢失。上线前确认只有一方在管这个文件。

本章命令速查

命令 作用
sudo logrotate -d /etc/logrotate.d/myapp dry run:只打印计划动作,不执行(改配置后必做)
sudo logrotate -f /etc/logrotate.d/myapp 强制立刻轮转一次
cat /var/lib/logrotate/status 查看各日志上次轮转时间
systemctl status logrotate.timer 确认每日轮转定时器在运行
ls /etc/logrotate.d/ 列出所有分文件配置
kill -USR1 $(cat /var/run/nginx.pid) 通知 nginx 重开日志文件(postrotate 用)
gzip -d app.log-20260929.gz 解压查看归档日志
zcat app.log-20260929.gz | grep ERROR 不解压直接查归档内容

相关章节

📄 磁盘占满排查 55-disk-full-troubleshooting.md


title: 55 磁盘占满排查 updated: 2026-09 applies-to: Ubuntu 22.04 LTS

磁盘 100% 是服务器上最高频的线上事故:日志写不进、数据库停止服务、应用莫名崩溃,而现象往往只是「网站变慢」或「接口 500」。这一章给你一条固定的排错路线——从发现到定位到处理,照着走就能解决绝大多数情况。

[!IMPORTANT] 排错路线图,请按顺序走: ① df -h 确认真的满了 → ② du 逐级定位大目录 → ③ 分类处理(删/清/扩容)→ ④ 加轮转与监控防复发(见 54 logrotate)。

第一步:df 确认哪个分区满

是什么

df(disk free)按分区汇报空间使用情况。排错第一步永远是它:先搞清楚「哪个挂载点满了」,再去那个文件系统里找元凶。

怎么做

$ df -h
预期输出:
Filesystem      Size  Used Avail Use% Mounted on
/dev/vda1        40G   40G     0 100% /
tmpfs           909M     0  909M   0% /dev/shm
/dev/vdb1       100G   12G   83G  12% /data

根分区 / 用了 100%,数据盘 /data 还很空——元凶在 / 里。只想单看一个分区:

$ df -h /
预期输出:
Filesystem      Size  Used Avail Use% Mounted on
/dev/vda1        40G   40G     0 100% /

参数说明

  • -h(human-readable):字节换算成 K/M/G,人能直接读;
  • 各列含义:Size 总容量、Used 已用、Avail 可用、Use% 使用率、Mounted on 挂载点(分区在目录树里的入口);
  • -i:看的是 inode 用量而不是空间。inode 是文件的「户口」,每个文件占一个,小文件再多也只占一个户口——大量小文件能把 inode 耗光而空间还空着;
  • 查 inode:
$ df -i /
预期输出:
Filesystem     Inodes IUsed IFree IUse% Mounted on
/dev/vda1      2560000 42015 2517985    2% /

验证方法

服务报 No space left on device 时,两种 df 都要跑:

$ df -h / ; df -i /
预期输出:
Filesystem      Size  Used Avail Use% Mounted on
/dev/vda1        40G   40G     0 100% /
Filesystem     Inodes IUsed IFree IUse% Mounted on
/dev/vda1      2560000 42015 2517985    2% /

空间满、inode 还有大量剩余——是普通的空间占满,走 du 路线;反之则是 inode 耗尽,跳到本章「inode 耗尽的定位」一节。

常见问题

现象 根因 解决
df -h 显示还有空间但报 No space left on device inode 被海量小文件耗光 df -i 确认,见本章 inode 一节
看不出哪个分区满 挂载点多,输出太长 df -h | grep -E "100%|9[0-9]%" 只看高危行

[!WARNING] inode 耗尽是经典陷阱:df -h 看起来空间充足,但 inode 用光后,创建任何新文件(写日志、建临时文件)照样报 No space left on device。两种 df 必须都看。

第二步:du 逐级定位

是什么

df 告诉你「哪个分区满」,du(disk usage)告诉你「空间被谁吃了」。思路是逐级下钻:从 / 开始,找出最大的子目录,钻进去,再找最大的,直到挖到具体的大文件。

怎么做

核心命令链,在每一级目录重复执行:

$ sudo du -xh --max-depth=1 / 2>/dev/null | sort -rh | head -20
预期输出:
17G     /
6.4G    /var
4.1G    /home
3.2G    /usr
1.9G    /opt
...(略)

/var 最大,钻进去:

$ sudo du -xh --max-depth=1 /var 2>/dev/null | sort -rh | head -20
预期输出:
6.4G    /var
4.8G    /var/log
1.1G    /var/lib
...(略)

/var/log 最大——八成是日志没轮转,处理方式见下一节。

参数说明

参数 作用 为什么需要
-x 不跨文件系统 限制在当前分区内统计。否则 /data、挂载的 U 盘等会被算进来,干扰定位
--max-depth=1 只汇总到下一级目录 不加的话会列出全部子目录,输出爆炸
-h 人类可读单位 配合下一步的排序
2>/dev/null 丢弃权限报错 注意:普通用户对 /var/lib/mysql 等目录不可读,叠加 2>/dev/null 会静默漏统计、结果偏小——排查磁盘务必整体加 sudo 再用
sort -rh 按人类可读数值逆序 -h 让 sort 识别 4.1G/680M 这类单位,-r 从大到小

验证方法

钻到最底层后,直接看目录里最大的文件,确认元凶:

$ ls -lhS /var/log/ | head
预期输出:
total 4.8G
-rw-r----- 1 appuser appgroup 4.2G Sep 29 10:00 app.log
-rw-r----- 1 syslog adm      301M Sep 29 06:25 syslog
...

-S 按文件大小排序,-h 人类可读。4.2G 的 app.log 就是目标。

常见问题

现象 根因 解决
du 统计很慢 目录里文件太多 耐心等,或先在更小的范围(如 /var)开始
du 总和远小于 df 显示的已用量 有已删除但未释放的文件,或挂载点下藏了文件 见本章「文件已删但空间没释放」一节
命令报一堆 Permission denied 普通用户权限不足 前面加 sudo,或保留 2>/dev/null

常见元凶与处理

是什么

定位到大目录后,接下来的处理是有套路的。线上磁盘占满的元凶,90% 落在下面七类里,每类一条处理路径。

怎么做

元凶一:journal 日志堆积(systemd 系统日志)。查看占用、按上限清理:

$ sudo journalctl --disk-usage
$ sudo journalctl --vacuum-size=200M
预期输出:
Archived and active journals take up 3.2G in the file system.
Vacuuming done, freed 3.0G of archived journals from /var/log/journal.

再在 /etc/systemd/journald.conf 里设 SystemMaxUse=200M 永久限额,详见 51 journalctl。

元凶二:Docker 镜像/容器/卷/构建缓存。先看四类资产各占多少:

$ docker system df
预期输出:
TYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE
Images          18        4         9.21GB    7.05GB (76%)
Containers      6         2         1.2GB     890MB (74%)
Local Volumes   8         3         4.05GB    2.1GB (51%)
Build Cache     45        0         3.8GB     3.8GB (100%)

一条命令粗清理(只删已停止容器、悬空镜像、无用网络、构建缓存):

$ docker system prune

四类分别清理的精确命令:

# 删除所有未被容器使用的镜像(注意:会删掉你重启时才用的镜像)
$ docker image prune -a

# 删除已停止的容器
$ docker container prune

# 删除未被任何容器使用的卷(可能含数据,慎用)
$ docker volume prune

# 清空构建缓存
$ docker builder prune

[!WARNING] docker system prune -a 会删除所有未被运行中容器引用的镜像,下次重启某服务时需要重新拉取/构建。数据卷(volume)里可能有数据库数据,清理前务必确认。

详见 30 Docker。

元凶三:应用日志没轮转。长期方案是配 logrotate(见 54 logrotate);当下应急,用清空代替删除:

$ sudo truncate -s 0 /var/log/myapp/app.log

或等效的:

$ sudo sh -c '> /var/log/myapp/app.log'

元凶四:文件已删但空间没释放——高频且反直觉,专开一节,见下节。

元凶五:数据库大表与 binlog。MySQL 的二进制日志会无限堆积:

# root 经 auth_socket 免密直连(23 章的默认安装);普通用户需 mysql -u root -p
$ sudo mysql -e "PURGE BINARY LOGS BEFORE NOW() - INTERVAL 3 DAY;"

PostgreSQL 侧留意 WAL 归档堆积与 pg_wal 目录(归档失败时它会疯长)。细节见 23 MySQL、24 PostgreSQL 与各自官方文档(截至 2026-09 版本)。

元凶六:备份文件堆积。/backup 里躺着三个月的手工备份包?定保留策略(比如只留最近 7 天),自动化方案见 44 备份与定时任务。

元凶七:临时文件与 core dump。/tmp 里的旧文件、崩溃产生的 core 文件:

# 删除 /tmp 下 7 天前的文件(删除操作不可逆,先 ls 预览再 -delete)
$ find /tmp -mtime +7 -delete
# 找出遗留的 core dump 文件
$ find / -xdev -name "core*" -size +10M 2>/dev/null

[!WARNING] find ... -delete 直接删除且不可恢复。动手前先把 -delete 换成 -ls 预览一遍要删的清单,确认无误再删。

参数说明

  • journalctl --vacuum-size=200M:把 journal 压缩到 200M 以内,从最旧的开始删;对应的 --vacuum-time=2weeks 按时间清理;
  • truncate -s 0:把文件截断为 0 字节,但保留文件本身和 inode——正在写它的进程不受影响,这是和 rm 的本质区别(原因见下节);
  • find -mtime +7:修改时间在 7 天以前。

验证方法

每处理完一类,立刻用 df 复查回收效果:

$ df -h /
预期输出:
Filesystem      Size  Used Avail Use% Mounted on
/dev/vda1        40G   31G  6.9G  82% /

Use% 从 100% 降到 82%,处理有效。

常见问题

现象 根因 解决
清理后过几天又满了 只治了标,没设轮转/限额 journal 限额 + logrotate + 监控,见「应急 vs 治本」
rm 大日志后空间没变 进程握着已删文件的 fd 见下一节
docker prune 后容器起不来 镜像被 -a 清掉 重新 pull/build 该镜像

文件已删但空间没释放

是什么

最迷惑的场景:rm 掉了 10G 的日志,df -h 显示空间纹丝不动。原理:Linux 里删除文件只是删掉目录项(文件名到 inode 的映射),只要还有进程握着这个文件的 fd,inode 和数据就不会释放——进程还在往一个「已经没有名字的文件」里写。

怎么做

完整排查链:

# 第一步:找出谁握着已删除的文件(deleted 状态)
$ sudo lsof | grep deleted | head
预期输出:
java    1234 appuser   15w   REG  253,1  10485760000  671089  /var/log/myapp/app.log (deleted)

java 进程(pid 1234)握着一个已删除的、10G 的文件。窄化查找可用 lsof +L1(列出 link count < 1 的文件,即已删除的):

$ sudo lsof +L1
预期输出:
COMMAND PID   USER   FD   TYPE DEVICE  SIZE/NLK NODE NAME
java    1234 appuser   15w   REG  253,1 10485760000  671089 /var/log/myapp/app.log

处理办法二选一:

# 办法一:截断文件,空间立即释放,进程不用动(应急首选)
$ sudo truncate -s 0 /proc/1234/fd/15
# 办法二:重启该进程,fd 关闭,空间随之释放(需要安排停机/重启)
$ sudo systemctl restart myapp

参数说明

  • 为什么 rm 没用而 >/truncate 有用:rm 只删目录项(名字),inode 的引用计数里还有那个进程的 fd 在撑着,内核不释放数据块;> file(全称 : > file)和 truncate -s 0 是把文件内容清零——数据块当场归还,文件名和 inode 都还在,进程继续写也只是从文件头追加;
  • /proc/1234/fd/15:进程 1234 的第 15 号文件描述符,指向那个幽灵文件。对它执行 truncate 等于对幽灵文件本身截断;
  • 根治:该日志配上 logrotate 的 copytruncate 模式(见 54 logrotate),以后就不会再出现这个场景。

验证方法

截断后确认 fd 已不再是巨型、df 已回收:

$ sudo lsof +L1 | head
$ df -h /
预期输出:
COMMAND PID   USER   FD   TYPE DEVICE  SIZE/NLK NODE NAME
java    1234 appuser   15w   REG  253,1         0  671089 /var/log/myapp/app.log

Filesystem      Size  Used Avail Use% Mounted on
/dev/vda1        40G   29G  8.9G  77% /

SIZE 变为 0、磁盘回收约 10G,处理成功。

常见问题

现象 根因 解决
lsof 输出里没有 deleted 但空间还是对不上 看下面的「du 与 df 数值差很大」 检查挂载点覆盖问题
truncate 后进程又把文件写大了 源头在疯写,截断只是止血 排查应用为何狂打日志,并配轮转
/proc/1234/fd/15 报权限错误 fd 编号取错或非 root 加 sudo,用 lsof 输出的真实 FD 列编号

inode 耗尽的定位

是什么

df -i 显示 IUse% 接近 100%,就是 inode 耗尽:文件数量爆炸(通常是几百万个小文件),每个文件吃掉一个 inode。此时哪怕空间充足,新建文件也会报 No space left on device。

怎么做

先确认(见第一步的 df -i),然后找小文件爆炸的目录——逐目录统计文件个数:

$ cd /var
$ for d in */; do echo "$(find "$d" -xdev | wc -l) $d"; done | sort -rn | head
预期输出:
1812342 lib/
20451   log/
1832    cache/
...(略)

/var/lib 下藏着一百八十万个文件,继续对 /var/lib 逐级重复这条命令,直到锁定具体目录。常见元凶:

  • Web 应用的 session 文件(每个访客一个小文件,长期不清理);
  • 邮件队列堆积(postfix 的队列目录);
  • 缓存碎片(各种框架的 cache 目录、图片缩略图)。

定位后按业务清理 session/cache,或给队列限长。

参数说明

  • find "$d" -xdev | wc -l:统计目录内文件总数,-xdev 不跨文件系统;
  • for d in */:遍历当前目录下所有子目录;echo "$(find | wc -l) $d" 输出「数量 + 目录名」两列,方便 sort -rn 排序。

验证方法

清理后 inode 应明显回落:

$ df -i /
预期输出:
Filesystem     Inodes  IUsed   IFree IUse% Mounted on
/dev/vda1      2560000 1823035 736965   72% /

IUse% 从 99% 降到 72%,新建文件恢复正常。

常见问题

现象 根因 解决
找不到哪个目录文件多 只在顶层跑了 for 循环 对最大的目录逐级重复执行
清理后 IUse% 不降 文件删了但同样握着 deleted fd 用 lsof +L1 查(见上一节)
session 清理后又积满 应用没配过期清理 cron 定期删过期 session(见 44 备份与定时任务)

应急 vs 治本

是什么

应急是「今天先让服务活下来」,治本是「让它永远不再发生」。磁盘事故的处理必须两段都做,只做前者的人每周都会见到同一个警报。

怎么做

应急动作(按见效速度):journal 清理 → 截断/清空大日志 → docker prune → 删过期备份。目标是把 Use% 降到 80% 以下,先恢复服务写入。

治本三件套:

  • 轮转:所有写文件的日志都接入 logrotate(见 54 logrotate);
  • 限额:journal 设 SystemMaxUse,数据库设 binlog 过期时间,备份设保留策略;
  • 监控:对 df 的 Use% 设告警,阈值 80%——留出处置时间,别等到 100% 才知道(监控体系搭建不在本教程范围,一句带过)。

参数说明

应急与治本对照:

阶段 动作 特点
应急 清 journal、截断日志、prune 几分钟见效,不解决复发
治本 轮转 + 限额 + 监控 一次配置,长期生效

验证方法

治本配置完成的标志:强制跑一轮 logrotate 无报错、journal 限额生效、监控能收到 df 指标。

$ sudo logrotate -d /etc/logrotate.d/myapp
预期输出:
rotating pattern: /var/log/myapp/app.log  after 1 days (14 rotations)

常见问题

现象 根因 解决
每周都磁盘告警 只做应急没做治本 补齐轮转与限额
扩了容量又满了 业务增长,监控阈值没跟上 容量规划 + 80% 告警前移

[!NOTE] 扩容一句话:云盘支持在线扩容,但扩完分区和文件系统不会自动变大,需要 growpart 扩分区 + resize2fs(ext4)或 xfs_growfs(xfs)扩文件系统。具体路径各云厂商控制台文档都有逐步指引,操作前快照一份。

本章命令速查

命令 作用
df -h 按分区看空间使用率(第一步)
df -h / 单看根分区
df -i 看 inode 使用量(小文件陷阱)
du -xh --max-depth=1 / | sort -rh | head 逐级定位大目录(第二步)
ls -lhS 目录 | head 列出目录内最大的文件
journalctl --disk-usage 查看 journal 日志占用
sudo journalctl --vacuum-size=200M 把 journal 清理到 200M 以内
docker system df 查看 Docker 四类资产占用
docker system prune 清理已停止容器/悬空镜像/构建缓存
sudo truncate -s 0 文件 清空文件(保住 inode,进程无感)
sudo lsof | grep deleted / sudo lsof +L1 找已删除但未释放空间的文件
sudo truncate -s 0 /proc/PID/fd/N 截断幽灵文件,立即释放空间
find /tmp -mtime +7 -delete 删除 /tmp 下 7 天前的文件(先 -ls 预览)
for d in */; do echo "$(find "$d" -xdev | wc -l) $d"; done | sort -rn | head 找文件数最多的目录(inode 排查)

相关章节

📄 60 端到端实战:部署一个完整项目 60-hands-on-project.md


title: 60 端到端实战:部署一个完整项目 updated: 2026-09 applies-to: Ubuntu 22.04 LTS

这是全书收官章。前面每一章都教「一个点」,这一章把所有点串成一条线:从一台全新的 Ubuntu 22.04 服务器出发,最终交付一个可公网 HTTPS 访问的 Java 后端服务。照着做到底,你将得到:跑在 systemd 下的 Java 应用(Spring Boot 风格 jar)+ MySQL 8.0 + Redis 8.x + Nginx 1.30 stable 反向代理 + certbot 签发的 HTTPS,外加日志、健康检查与每日备份(版本均截至 2026-09)。

每个大步骤末尾都有「验收检查」,确认本步全部通过再往下走。中途翻车时,直接跳到文末「常见翻车现场」对照排查。


项目与目标

部署什么

一个提供 REST API 的 Java 应用(以「文章管理 API」为例):有健康检查接口 /api/health,能读写 MySQL,用 Redis 做缓存。应用本身怎么写、怎么打成 app.jar 不在本章范围内(见 31 Git 与构建工具),我们聚焦部署侧。

架构小图

用户 ──HTTPS 443 / HTTP 80──> Nginx 1.30        ← 唯一对公网开放的组件
                                 │ http://127.0.0.1:8080
                                 ▼
                             Java 应用 app.jar(systemd 托管,专用用户 appuser)
                                 │                    │
             127.0.0.1:3306 ◄────┘                    └────> 127.0.0.1:6379
                  ▼                                        ▼
           MySQL 8.0(库 appdb)                 Redis 8.x(requirepass)

安全架构的底座

[!IMPORTANT] 每个组件只监听 127.0.0.1,只有 Nginx 对外开放——这是本章安全设计的底座:应用即使有漏洞公网也摸不到,MySQL/Redis 不暴露端口就无从爆破,防火墙只放行 22/80/443。网上让 MySQL 开 0.0.0.0、Redis 裸奔的教程,是被扫去挖矿的标准姿势。

最终验收总清单

全部完成后,逐条核对:

  • [ ] dig +short <你的域名> 返回 <服务器IP>
  • [ ] https://<你的域名>/api/health 返回 200,HTTP 自动 301 跳转 HTTPS
  • [ ] Java 进程以 appuser 运行,systemd 托管且开机自启
  • [ ] MySQL、Redis 均只监听 127.0.0.1,均有独立密码
  • [ ] ss -tlnp 里对公网监听的只有 nginx(80/443)和 sshd
  • [ ] journalctl -u myapp 可查应用日志
  • [ ] certbot renew --dry-run 通过,证书自动续期
  • [ ] /var/backups/mysql/ 下有每日备份文件,且恢复演练通过
  • [ ] 健康检查脚本一键输出全部服务状态

第 0 步:准备服务器与域名

服务器要求

一台云服务器:Ubuntu 22.04 LTS、2 核 4G 起(Java + MySQL + Redis 同机,1G 内存会很痛苦)、40G+ 磁盘。云安全组先只放行 22,80/443 到第 4 步要用时再放行。安全组与系统内的 ufw 是两道独立的门,两边都要放行才通(三层防火墙模型见 16 防火墙)。

域名解析

在域名服务商处添加一条 A 记录指向 <服务器IP>,然后在本地电脑验证:

# 验证域名是否已解析到服务器 IP
$ dig +short <你的域名>

预期输出就是 <服务器IP>。没输出说明解析未生效,等几分钟再试——域名解析不通,后面 HTTPS 一步都做不了。

先用云厂商给的初始方式(密码或临时密钥)能 SSH 登录即可,第一步会立刻做密钥加固(详见 18 SSH 加固)。

验收检查

  • [ ] 能 SSH 登录服务器,lsb_release -a 显示 22.04
  • [ ] free -h 显示内存 ≥ 3.5G,df -h / 显示磁盘 ≥ 30G 可用
  • [ ] 云安全组只放行了 22 端口
  • [ ] dig +short <你的域名> 返回 <服务器IP>

第 1 步:服务器初始化(安全基线)

把「出厂状态」变成「有基本防护」。每条命令背后都有对应章节,这里只走主线。

1.1 系统更新

# 刷新软件源索引并升级全部已装软件包
$ sudo apt update && sudo apt upgrade -y

包管理细节见 17 包管理;国内下载慢可换镜像源,见 33 镜像加速。

1.2 建专用 sudo 用户

不要长期用 root 干活。建一个日常用户并赋予 sudo 权限(用户与权限体系见 13 用户与权限):

# 创建用户 deploy 并加入 sudo 组(adduser 会同时创建家目录)
$ sudo adduser deploy
$ sudo usermod -aG sudo deploy

1.3 SSH 密钥登录

在本地电脑上推送公钥,之后禁用密码登录:

# 在本地电脑执行:推送公钥到服务器
$ ssh-copy-id deploy@<服务器IP>

# 在服务器上执行:确认密钥能登录后,禁用密码认证
$ sudo sed -i 's/^#\?PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config

# 22.04 云镜像常在 drop-in 里覆盖主配置,必须一并检查
$ grep -ri PasswordAuthentication /etc/ssh/sshd_config.d/ || echo "无 drop-in 覆盖"
# 若 50-cloud-init.conf 输出了 PasswordAuthentication yes,同步改掉它:
$ sudo sed -i 's/^PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config.d/50-cloud-init.conf

# 核对最终生效值(sshd -T 输出实际生效配置,必须是 passwordauthentication no)
$ sudo sshd -T | grep -i passwordauthentication

$ sudo systemctl reload ssh

[!WARNING] 禁用密码登录前,先开一个新终端确认密钥能登录。把自己锁在外面是新手第一大事故。完整加固(禁 root 直登、改端口、fail2ban)见 18 SSH 加固。

1.4 时区与时间同步

时间不对会导致日志混乱、HTTPS 校验失败:

# 设置时区并确认时间同步在运行
$ sudo timedatectl set-timezone Asia/Shanghai
$ timedatectl

NTP service: active 即正常(时间同步细节见 15 网络配置)。

1.5 启用 ufw 防火墙

云安全组之外,系统内再启用 ufw 这道门。铁律:先放行 22 再 enable,否则启用瞬间会把自己锁在外面:

# 先放行 SSH,再启用防火墙(顺序不能反)
$ sudo ufw allow 22/tcp
$ sudo ufw enable

# 确认状态:22/tcp 为 ALLOW,其余默认拒绝
$ sudo ufw status

启用后保持当前 SSH 会话不断开,新开一个终端确认还能正常登录,再继续往下走(「先放 22 再 enable」的完整铁律见 16 防火墙)。80/443 到第 4 步 Nginx 上线时再放行。

验收检查

  • [ ] apt upgrade 完成,无报错
  • [ ] 用 deploy 账号 + 密钥能登录,密码登录已被拒绝
  • [ ] sudo sshd -T | grep -i passwordauthentication 输出 passwordauthentication no
  • [ ] date 显示上海时区与正确时间
  • [ ] timedatectl 显示 NTP service: active
  • [ ] sudo ufw status 显示 active 且 22/tcp 为 ALLOW,新开终端仍能 SSH 登录

第 2 步:安装运行环境(JDK / MySQL / Redis)

三个组件装在同一台机器上,且都只监听本机。安装细节各有一整章,这里给最短可执行路径。

2.1 JDK 21

JDK 21 是当前最稳妥的 LTS(截至 2026-09),22.04 源里已包含:

# 安装 OpenJDK 21 并确认版本
$ sudo apt install -y openjdk-21-jdk
$ java -version

输出含 openjdk version "21.x" 即成功。生产建议另配 JAVA_HOME,多版本切换见 20 JDK。

2.2 MySQL 8.0

安装与初始化的完整讲解(含 caching_sha2_password 兼容坑)见 23 MySQL:

# 安装 MySQL Server 8.0,然后运行安全初始化向导
$ sudo apt install -y mysql-server
$ sudo mysql_secure_installation

执行 sudo mysql 进入命令行,建业务库和专用账号——账号只授权 localhost,密码替换为强密码:

-- 创建 utf8mb4 字符集的业务库
CREATE DATABASE appdb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

-- 创建应用专用账号,仅允许本机连接('localhost' 是关键)
CREATE USER 'appuser'@'localhost' IDENTIFIED BY '<强密码>';

-- 只授予 appdb 库的常规权限,不给全局权限
GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, INDEX, ALTER ON appdb.* TO 'appuser'@'localhost';
FLUSH PRIVILEGES;
EXIT;

[!IMPORTANT] 确认 bind-address 保持 127.0.0.1(8.0 默认即如此,别照抄网上教程改成 0.0.0.0):sudo ss -tlnp | grep 3306 的输出必须是 127.0.0.1:3306。

本机连通验证:

# 用应用账号从本机连接,验证账号密码与库权限
$ mysql -u appuser -p -h 127.0.0.1 appdb -e "SELECT 1;"

2.3 Redis 8.x

22.04 源里只有 6.0,需 Redis 官方源装 8.x(源配置见 26 Redis):

# 添加官方源后安装(过程见第 26 章)
$ sudo apt install -y redis

编辑 /etc/redis/redis.conf:

# 只监听本机回环地址
bind 127.0.0.1

# 设置访问密码(替换为强密码)
requirepass <强密码>
# 重启生效,然后验证:无密码 ping 应被拒,带密码应返回 PONG
$ sudo systemctl restart redis-server
$ redis-cli ping
$ redis-cli -a '<强密码>' ping

验收检查

  • [ ] java -version 显示 21.x
  • [ ] systemctl is-active mysql redis-server 都输出 active
  • [ ] sudo ss -tlnp | grep -E '3306|6379' 只显示 127.0.0.1
  • [ ] mysql -u appuser -p -h 127.0.0.1 appdb -e "SELECT 1;" 连通
  • [ ] redis-cli -a '<强密码>' ping 返回 PONG

第 3 步:部署应用

本章假设你已有构建好的 app.jar(Spring Boot 风格可执行 fat jar,换任何能跑的 jar 同理;如何构建见 31 Git 与构建工具)。

3.1 上传 jar 包

先传到 /tmp 中转,确认无误后再放到正式目录(scp 细节见 11 文件操作):

# 在本地电脑执行:上传 jar 到服务器临时目录
$ scp -P 22 target/app.jar deploy@<服务器IP>:/tmp/

3.2 建专用运行用户

为应用建一个系统用户:-r 表示系统账号(不生成家目录、无过期),-s /usr/sbin/nologin 禁止登录 shell——即使应用被攻破,攻击者也拿不到能登录的账号(原理见 13 用户与权限):

# 创建不可登录的系统用户,专门用于运行应用
$ sudo useradd -r -s /usr/sbin/nologin appuser

3.3 目录与权限

# 创建应用目录与日志目录,jar 移到正式目录,全部归属 appuser
$ sudo mkdir -p /opt/myapp /var/log/myapp
$ sudo mv /tmp/app.jar /opt/myapp/app.jar
$ sudo chown -R appuser:appuser /opt/myapp /var/log/myapp

3.4 环境变量文件

数据库密码不写死在 jar 内的配置文件里,而是通过环境变量注入——换环境不用重新打包:

# 创建环境变量文件,写入敏感配置
$ sudo tee /opt/myapp/app.env > /dev/null <<'EOF'
SPRING_PROFILES_ACTIVE=prod
SPRING_DATASOURCE_URL=jdbc:mysql://127.0.0.1:3306/appdb?useSSL=true&serverTimezone=Asia/Shanghai
SPRING_DATASOURCE_USERNAME=appuser
SPRING_DATASOURCE_PASSWORD=<强密码>
SPRING_DATA_REDIS_HOST=127.0.0.1
SPRING_DATA_REDIS_PASSWORD=<强密码>
EOF
# 收紧权限:只有 root 可读,由 systemd 以 root 读取后注入进程环境
$ sudo chmod 600 /opt/myapp/app.env
$ sudo chown root:root /opt/myapp/app.env

[!TIP] chmod 600 + 属主 root 看似与应用用户矛盾,但 systemd 读 EnvironmentFile 是以 root 身份读的,读完注入给 appuser 进程——敏感文件恰好做到「应用能用值、但读不到文件」。

3.5 systemd unit 文件

创建 /etc/systemd/system/myapp.service(unit 逐行讲解、Type= 区别、Restart 策略见 40 systemd):

[Unit]
# 描述信息,systemctl status 里显示的第一行
Description=MyApp Java Service
# 网络与数据库就绪后再启动
After=network.target mysql.service redis-server.service

[Service]
# 以专用低权限用户运行
User=appuser
Group=appuser
# simple:ExecStart 的进程即主进程,Spring Boot 前台运行正合适
Type=simple
# 工作目录,应用内的相对路径都以此为基准
WorkingDirectory=/opt/myapp
# 启动命令:绝对路径的 java,避免 PATH 问题;堆内存按机器规格调整
ExecStart=/usr/bin/java -Xms512m -Xmx1024m -jar /opt/myapp/app.jar
# 从环境文件注入变量(敏感配置都在这里)
EnvironmentFile=/opt/myapp/app.env
# 异常退出总是自动拉起,10 秒后重启
Restart=always
RestartSec=10
# Java 需要较多文件描述符(连接、jar 内资源等)
LimitNOFILE=65536

[Install]
# 开机自启,挂在多用户运行级别下
WantedBy=multi-user.target
# 重新加载 unit 定义,启动服务并设为开机自启
$ sudo systemctl daemon-reload
$ sudo systemctl enable --now myapp

3.6 看启动日志

# 状态应为 active (running);再跟踪日志,看到 "Started ...Application" 即启动完成
$ systemctl status myapp
$ journalctl -u myapp -f

按 Ctrl+C 退出跟踪;journalctl 的过滤技巧见 51 journalctl。

3.7 本机验证

# 请求健康检查接口
$ curl http://127.0.0.1:8080/api/health

预期输出 {"status":"UP"}。

验收检查

  • [ ] ps -o user= -p $(pgrep -f app.jar) 输出 appuser
  • [ ] systemctl is-enabled myapp 输出 enabled
  • [ ] systemctl is-active myapp 输出 active
  • [ ] curl http://127.0.0.1:8080/api/health 返回 {"status":"UP"}
  • [ ] kill 或异常退出后,应用由 Restart=always 自动拉起(验证:sudo pkill -f app.jar,几秒后 systemctl is-active myapp 仍为 active)

第 4 步:Nginx 反向代理

4.1 安装 Nginx 1.30 stable

22.04 源默认是 1.18,需添加 nginx.org 官方源装 1.30 stable(源配置见 29 Nginx):

# 添加官方源后安装(过程见第 29 章),确认版本
$ nginx -v

输出应为 nginx version: nginx/1.30.x。

4.2 反向代理配置

创建 /etc/nginx/conf.d/myapp.conf。先用 HTTP 跑通,下一步再上 HTTPS(conf.d/ 与 sites-available/ 软链到 sites-enabled/ 两种位置均可生效,29 Nginx 演示的是后者的发行版惯例,这里选 conf.d/ 是为了单文件自包含):

server {
    # 监听 80 端口
    listen 80;

    # 匹配你的域名
    server_name <你的域名>;

    # 允许最大 20M 请求体(文件上传场景按需调整)
    client_max_body_size 20m;

    location / {
        # 转发到本机的 Java 应用
        proxy_pass http://127.0.0.1:8080;

        # 四件套:让应用拿到真实协议、域名与客户端 IP
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

[!IMPORTANT] proxy_set_header 四件套缺一不可:没有它们,应用看到的客户端 IP 全是 127.0.0.1、生成的跳转链接会丢协议。日志统计、限流、鉴权都依赖真实 IP。

4.3 检查并生效

# 语法检查(必须显示 test is successful),然后平滑重载
$ sudo nginx -t
$ sudo systemctl reload nginx

nginx -t 是改配置后的肌肉记忆;reload 与 restart 的区别见 29 Nginx。

4.4 公网验证与防火墙

先去云控制台安全组放行 80/443,再放行系统防火墙。放行前务必确认 22 已放行,否则会把自己锁在外面(ufw 完整用法与「先放 22 再 enable」的铁律见 16 防火墙):

# 先确认 22 已放行(第 1 步已启用 ufw),再放行 Web 端口
# Nginx Full 是 ufw 内置 profile,同时涵盖 80 和 443;显式写法为 sudo ufw allow 80,443/tcp,二者等价、选一个执行即可
$ sudo ufw status
$ sudo ufw allow 'Nginx Full'

在本地电脑验证:

# 在本地电脑执行:验证公网 HTTP 访问
$ curl -I http://<你的域名>/api/health

预期 HTTP/1.1 200 OK。若 502,翻到文末翻车表。

验收检查

  • [ ] sudo nginx -t 通过
  • [ ] 云安全组已放行 80/443,ufw 里 22 与 Nginx Full 均为 allow
  • [ ] 本地 curl -I http://<你的域名>/api/health 返回 200
  • [ ] sudo ss -tlnp | grep nginx 监听 0.0.0.0:80(对公网,符合预期)

第 5 步:上 HTTPS

证书申请、自动续期原理、过期排查全流程见 32 HTTPS 证书,这里一条命令搞定:

# 用 certbot 的 nginx 插件一键签发并自动改 Nginx 配置
$ sudo certbot --nginx -d <你的域名>

交互中问「是否把 HTTP 重定向到 HTTPS」时,选 Redirect(重定向)——certbot 会自动把 80 端口改成 301 跳转,Nginx 配置无需手改。

验证自动续期

Let's Encrypt 证书有效期 90 天,certbot 自带 systemd timer 每天检查续期:

# 演练续期流程(不会真的换证书),通过即说明自动续期没问题
$ sudo certbot renew --dry-run

验证 HTTPS

在本地电脑:

# 验证 HTTPS 直连,以及 HTTP 会 301 跳转
$ curl -I https://<你的域名>/api/health
$ curl -I http://<你的域名>/api/health

预期:第一条 HTTP/2 200,第二条 HTTP/1.1 301 且 Location 为 https 地址。

验收检查

  • [ ] certbot --nginx 签发成功,选择了 Redirect
  • [ ] certbot renew --dry-run 显示 all simulated renewals succeeded
  • [ ] curl -I https://<你的域名>/api/health 返回 200,HTTP 301 跳转
  • [ ] 浏览器地址栏显示锁形图标,证书有效期 90 天内

第 6 步:日志与监控验收

上线只是开始,出问题时「能不能快速看到发生了什么」才是运维水平。

6.1 应用日志去向

本章的 systemd 配置让应用日志全部进 journald:

# 查看应用最近日志(--since 支持人类可读时间),-p err 只看 err 及以上
$ journalctl -u myapp --since "1 hour ago"
$ journalctl -u myapp -p err

[!NOTE] 如果应用额外写日志文件(如 /var/log/myapp/app.log),必须配 logrotate,否则日志无限增长会撑爆磁盘。配置模板与 copytruncate 的坑见 54 logrotate。

6.2 Nginx 日志分析

访问日志在 /var/log/nginx/access.log,一条最常用的状态码分布统计(awk 统计的完整玩法见 53 日志分析):

# 统计各 HTTP 状态码出现次数,按数量倒序
$ awk '{print $9}' /var/log/nginx/access.log | sort | uniq -c | sort -rn

200 应占绝大多数;若 502 持续增长,先看 journalctl -u myapp 是不是应用挂了。

6.3 健康检查脚本

把日常巡检固化成脚本,存为 /opt/myapp/healthcheck.sh:

#!/bin/bash
# 一键健康检查:应用接口 + 服务状态 + 磁盘水位;异常项输出 [FAIL]

echo "== HTTP 健康接口 =="
curl -fsS -m 5 https://<你的域名>/api/health && echo || echo "[FAIL] health 接口异常"

echo "== systemd 服务状态 =="
for svc in myapp mysql redis-server nginx; do
    systemctl is-active --quiet "$svc" && echo "[OK]   $svc" || echo "[FAIL] $svc"
done

echo "== 磁盘使用率 =="
df -h / | tail -1
# 根分区使用率超 85% 提示排查
usage=$(df / | awk 'NR==2 {gsub("%",""); print $5}')
[ "$usage" -ge 85 ] && echo "[FAIL] 根分区使用率 ${usage}%,排查见 55 章"
# 赋予执行权限并运行一次
$ sudo chmod +x /opt/myapp/healthcheck.sh
$ sudo /opt/myapp/healthcheck.sh

验收检查

  • [ ] journalctl -u myapp 能查到启动与应用日志
  • [ ] awk 统计跑通,能看清状态码分布
  • [ ] 健康检查脚本输出全部 [OK]
  • [ ] (如应用写文件日志)logrotate 就位且 logrotate -d 干跑无报错

第 7 步:备份上线

没有验证过恢复的备份等于没有备份。cron 语法、% 转义、3-2-1 策略见 44 备份与 cron。

7.1 免密备份配置

cron 里不能交互输密码,把凭据放进 root 家目录的 /root/.my.cnf——下面 7.2 的备份任务跑在 root 的 crontab 里,mysqldump 以 root 身份执行、读的正是 /root/.my.cnf;写进 deploy 家目录的话 root 反而读不到(~/.my.cnf 只对各自的主人生效):

# 写入 /root/.my.cnf(普通用户无法在 /root 下直接建文件,用 sudo tee)
$ sudo tee /root/.my.cnf > /dev/null <<'EOF'
[client]
# 备份专用凭据(按最小权限原则自建 backup 账号更佳)
user=appuser
password=<强密码>
host=127.0.0.1
EOF
# 锁权限:组和其他用户不可读
$ sudo chmod 600 /root/.my.cnf

7.2 每日备份 cron

# 编辑 root 的 crontab(备份以 root 跑,避免目录权限问题)
$ sudo crontab -e
# 每天 3:00 备份 appdb 并 gzip 压缩,文件名带日期(--routines --triggers 连存储过程和触发器一起导出,漏了恢复不全)
0 3 * * * mysqldump --single-transaction --routines --triggers appdb | gzip > /var/backups/mysql/appdb_$(date +\%Y\%m\%d).sql.gz
# 每天 3:30 清理 7 天前的旧备份
30 3 * * * find /var/backups/mysql -name "appdb_*.sql.gz" -mtime +7 -delete

[!IMPORTANT] 注意 \% 转义——cron 里 % 有特殊含义,不转义命令会被静默截断,这是 cron 第一大坑,见 44 备份与 cron。

先建目录并手动跑一次确认产物:

# 创建备份目录并手动执行一次验证
$ sudo mkdir -p /var/backups/mysql
# 目录属主是 root,而管道右侧的 gzip 和重定向由当前用户 shell 执行,
# 所以整条管道要放进 sudo sh -c,否则重定向一步会 Permission denied
$ sudo sh -c 'mysqldump --single-transaction --routines --triggers appdb | gzip > /var/backups/mysql/appdb_manual.sql.gz'
$ ls -lh /var/backups/mysql/

7.3 恢复演练(必做)

备份的价值只有在恢复成功那一刻才成立。恢复到临时库验证:

# 创建临时恢复库,解压导入备份文件(建库/导入是全局操作,appuser 只有 appdb.* 的权限,改用 root 执行)
$ sudo mysql -e "CREATE DATABASE appdb_restore CHARACTER SET utf8mb4;"
$ gunzip < /var/backups/mysql/appdb_manual.sql.gz | sudo mysql appdb_restore

# 对比两边行数是否一致(示例:articles 表;原库走 appuser,恢复库走 root)
$ mysql -u appuser -p appdb -e "SELECT COUNT(*) FROM articles;"
$ sudo mysql appdb_restore -e "SELECT COUNT(*) FROM articles;"

行数一致即演练通过,最后清理临时库:

# 演练完成后删除临时库(删库是全局操作,用 root 执行)
$ sudo mysql -e "DROP DATABASE appdb_restore;"

验收检查

  • [ ] /root/.my.cnf 存在且权限 600,sudo mysqldump --single-transaction appdb 免密可用
  • [ ] sudo crontab -l 能看到两条备份任务
  • [ ] /var/backups/mysql/ 下有今天日期的 .sql.gz 且大小不为 0
  • [ ] 恢复到 appdb_restore 成功,行数与原库一致
  • [ ] 临时库已清理

常见翻车现场

排错方法论(现象 → 范围 → 证据 → 假设 → 验证)见 92 排错手册,下表是本章场景的快速索引:

现象 最可能原因 排查入口
访问域名返回 502 Bad Gateway Java 应用挂了或没监听 8080 systemctl status myapp + journalctl -u myapp -n 50;40 systemd、51 journalctl
浏览器连接超时(80/443 都不通) 安全组或 ufw 没放行 先查云控制台安全组,再 sudo ufw status;16 防火墙
应用日志报 Communications link failure / Access denied 连接串、账号或密码错;MySQL 没监听 127.0.0.1 sudo ss -tlnp | grep 3306 + 手动 mysql -u appuser -p 验证;23 MySQL
页面白屏但本机 curl 127.0.0.1:8080 正常 Nginx 没转发、server_name 不匹配、忘了四件套 sudo nginx -t + 核对第 4 步配置;29 Nginx
HTTPS 证书续期失败 域名解析变了、80 被占、timer 没在跑 看 sudo certbot renew --dry-run 报错;32 HTTPS 证书
重启服务器后服务没起来 忘了 systemctl enable,或依赖顺序问题 systemctl is-enabled myapp;40 systemd
磁盘一周就满 日志无轮转、备份堆积、journal 占用超限 df -h + du 逐级定位;55 磁盘满排查、54 logrotate、51 journalctl

[!TIP] 排错铁律:从现象所在的那一层开始查,一次只改一个变量。 502 先查应用活没活再查 Nginx;连接超时先查防火墙再查监听。


汇总验收清单

全部步骤完成后,用这六项做最终交付核对(92 章排错手册引用的正是这份清单):

# 验收项 怎么核对
1 应用可访问 curl -I https://<你的域名>/api/health 返回 200(或本机 curl http://127.0.0.1:8080/api/health 返回 {"status":"UP"})
2 HTTPS 有效 浏览器地址栏显示锁形图标,或 `openssl s_client -connect <你的域名>:443 -servername <你的域名> </dev/null 2>/dev/null
3 systemd 自启 systemctl is-enabled myapp nginx mysql redis-server 全部输出 enabled
4 日志在写 journalctl -u myapp --since "5 min ago" 请求后能看到新日志滚动
5 备份在跑 ls -lh /var/backups/mysql/ 有当日 .sql.gz 且大小不为 0,sudo crontab -l 能看到备份任务
6 防火墙最小开放 sudo ufw status 只见 22/tcp 与 80/443(Nginx Full),无多余放行

六项全绿,这次端到端部署才算真正交付。


收尾:下一步做什么

走到这里,你完成了一次合格的单机生产部署——这正是本教程的边界。业务长大之后的进阶方向:

  • 容器化:应用 + MySQL + Redis 用 Docker Compose 编排,环境一致性问题一劳永逸,见 30 Docker
  • 从单机到多机:应用无状态化后,Nginx upstream 做负载均衡,数据库拆到独立实例
  • CI/CD:把「构建 → 上传 → 重启 → 验证」这套第 3 步的动作交给流水线自动执行
  • 监控告警:健康检查脚本是被动查看,Prometheus + Grafana 能在磁盘快满时主动通知你

不管走哪条路,本章建立的安全底座(最小监听、专用用户、防火墙、备份演练)都不会过时。回头查阅请回到 目录与阅读路线。

📄 命令速查表 90-cheatsheet-commands.md


title: 命令速查表 updated: 2026-09 applies-to: Ubuntu 22.04 LTS

本表是索引,不是教程:每条命令一行「命令 + 一句话作用 + 所属章节」,想看原理和完整用法请点进章节。命令写法约定($ 提示符、sudo、注释独立成行)见 00 前言与约定。

[!TIP] 记不住命令时:先来这张表找,再进对应章节看「为什么」。版本号问题去 91 版本速查,服务器出故障去 92 排错手册。


1. 文件与目录

详见 10 Shell 与文件系统、11 文件操作、12 文本处理。

命令 作用 章节
$ ls -lah 列出文件(含隐藏、人类可读大小) 10
$ pwd 显示当前所在目录绝对路径 10
$ cd /var/log 切换目录(绝对路径) 10
$ cd - 回到上一次所在目录 10
$ cp -a src dst 复制并保留权限/时间戳/软链接(备份首选) 11
$ mv old new 移动或重命名(同盘是改名,瞬间完成) 11
$ rm -i file 删除前逐个确认(比裸 rm 安全) 11
$ mkdir -p a/b/c 递归创建多级目录,已存在不报错 11
$ ln -s /real/path link 创建软链接(Nginx 站点启用就靠它) 10
$ find /var -name "*.log" -mtime +7 按名字/时间找文件 12
$ tar czf backup.tar.gz dir/ 打包并 gzip 压缩(czf = 创建) 11
$ tar xzf backup.tar.gz 解压 .tar.gz(xzf = 解开) 11
$ tar tf backup.tar.gz 只看包内容不解压(解压前先确认) 11
$ rsync -avz src/ dst/ 增量同步(结尾斜杠=同步目录内容) 11
$ scp file user@host:/tmp/ 走 SSH 复制文件到远程 11
$ du -sh /var 查目录总占用(一句话汇总) 10
$ df -h 查各挂载点磁盘使用率 10
$ stat file 查看文件的权限/属主/时间戳细节 11
$ unzip app.zip -d /opt/ 解压 zip 包 11
$ which nginx 查命令的可执行文件在 PATH 的哪个位置 10
$ rsync -avz --delete src/ dst/ 同步并删除目标端多余文件(镜像目录,慎用 --delete) 11

2. 文本处理与日志检索

详见 12 文本处理、52 日志查询配方、53 日志分析。

命令 作用 章节
$ grep -rn "ERROR" /var/log/app/ 递归搜索并显示行号 12
$ grep -C 5 "Exception" app.log 显示匹配行前后各 5 行上下文 52
$ tail -f app.log 实时跟踪日志(文件被轮转后跟错对象) 52
$ tail -F app.log 实时跟踪且自动跟随轮转后的新文件 52
$ less +F app.log 交互式跟踪,Ctrl+C 可暂停回翻 52
$ zgrep "ERROR" app.log.2.gz 直接搜压缩过的历史日志 52
$ awk '{print $1}' access.log | sort | uniq -c | sort -rn 统计第一列(IP)出现次数 TOP N 53
$ sed -i 's/old/new/g' file 原地替换文本(改配置常用) 12
$ cat access.log | awk '{print $9}' | sort | uniq -c 按状态码聚合 Nginx 访问日志 53
$ find . -name "*.tmp" | xargs rm 把查找结果逐个交给下一条命令处理 12
$ sudo tee /etc/sysctl.d/99.conf < local.conf 以普通用户身份写入 root 文件(> 重定向会权限不够) 12
$ wc -l access.log 数行数(快速估算请求量) 12
$ grep -v "^#" config.conf 反向匹配:过滤掉注释行 12
$ head -100 access.log 看文件前 100 行 12
$ awk -F: '{print $1, $3}' /etc/passwd 指定分隔符后取字段(冒号分隔取第 1、3 列) 12

[!NOTE] sort | uniq -c | sort -rn 是日志统计三件套:先排序(uniq 只认相邻)、计数、再按次数倒序。原理见 53 日志分析。


3. 用户与权限

详见 13 用户与权限。

命令 作用 章节
$ sudo useradd -m -s /bin/bash app 建用户并创建家目录、指定 shell 13
$ sudo usermod -aG docker app 把用户追加进组(漏了 -a 会清空其他组) 13
$ sudo passwd app 设置/修改用户密码 13
$ chmod 755 script.sh 所有者全权,其他人可读可执行(目录标配) 13
$ chmod 644 config.conf 所有者可写,其他人只读(配置文件标配) 13
$ sudo chown -R app:app /opt/myapp 递归改文件属主属组 13
$ sudo -l -U app 查看用户有哪些 sudo 权限 13
$ sudo visudo 安全编辑 sudoers(保存前自动查语法) 13
$ umask 查看新建文件的默认权限掩码 13
$ id app 查用户 uid/gid 及所属组 13
$ groups app 只列用户所属的组 13
$ su - app 切换到 app 用户(带 - 加载其环境变量) 13
$ sudo -i 切换到 root 的完整环境(用完即退) 13
$ getfacl /opt/myapp 查看文件的 ACL 细粒度授权 13

[!WARNING] 永远不要 chmod -R 777。为什么 755 够用而 777 危险,见 13 章「为什么 755 不是 777」。


4. 进程与资源

详见 14 进程管理、43 运维工具箱。

命令 作用 章节
$ ps aux | grep java 快照式查看进程 14
$ top 实时看 CPU/内存/负载(按 P 按CPU排、M 按内存排) 14
$ htop 更好用的 top(彩色、可滚动、可搜) 43
$ kill 12345 发 SIGTERM 优雅终止(默认信号) 14
$ pkill -f myapp.jar 按完整命令行匹配杀进程 14
$ jobs / $ fg / $ bg 管理当前终端的后台任务 14
$ nohup java -jar app.jar & 挂断 SSH 后进程不死(临时跑一次用) 14
$ sudo lsof -i:8080 查谁占用了 8080 端口 43
$ sudo strace -p 12345 追踪进程的系统调用(卡死时看它在等什么) 43
$ vmstat 1 每秒刷新看 CPU/内存/IO 概览(重点看 wa 列) 43
$ iostat -x 1 磁盘 IO 详细统计 43
$ free -h 看内存(重点看 available,不是 free) 14
$ uptime 看 load average 三个数 14
$ ps -ef --forest 树状显示进程父子关系 14
$ kill -l 列出全部信号(SIGTERM=15,SIGKILL=9) 14
$ sudo pkill -u app 杀掉某用户的所有进程 14
$ iotop 实时看哪个进程在读写磁盘 43

[!IMPORTANT] kill -9 是 SIGKILL,进程没有清理机会。数据库进程用 kill -9 可能损坏数据,先试默认的 kill。信号详解见 14 章。


5. 网络与防火墙

详见 15 网络配置、16 防火墙。

命令 作用 章节
$ ip a 查网卡和 IP 地址(取代 ifconfig) 15
$ ip r 查路由表和默认网关 15
$ ss -lntp 查监听中的端口及对应进程(取代 netstat) 15
$ curl -I http://localhost 只看响应头(快速验证 HTTP 通不通) 15
$ nc -zv 10.0.0.5 3306 测试 TCP 端口连通性 15
$ ping -c 4 example.com 发 4 个包测试连通(服务器别裸跑 ping) 15
$ dig example.com 查域名解析结果 15
$ sudo ufw status verbose 查防火墙规则和开关状态 16
$ sudo ufw allow 22/tcp 放行端口(先放 SSH 再 enable!) 16
$ sudo ufw delete allow 8080/tcp 删除一条规则 16
$ hostnamectl 查看/修改主机名 15
$ timedatectl 查看时区与时间同步状态 15
$ wget -c https://example.com/big.tar.gz 断点续传下载大文件 15
$ sudo chronyc tracking 查看时间同步精度与上游 NTP 源 15
$ resolvectl status 查看当前 DNS 服务器配置 15

[!WARNING] 开 ufw 前必须先 ufw allow 22/tcp,否则 enable 的瞬间 SSH 断连。完整顺序见 16 章。


6. 软件包管理

详见 17 包管理。

命令 作用 章节
$ sudo apt update 刷新软件源索引(装东西前先跑) 17
$ sudo apt install nginx 安装软件包 17
$ sudo apt remove nginx 卸载软件但保留配置 17
$ sudo apt purge nginx 卸载并删除配置文件 17
$ apt policy nginx 查已装版本和可装版本 17
$ apt-cache madison nginx 列出源里所有可用版本 17
$ sudo apt-mark hold nginx 锁定版本不被 apt upgrade 升级 17
$ dpkg -L nginx 列出某包装到了哪些文件 17
$ dpkg -S /usr/sbin/nginx 反查某文件属于哪个包 17
$ sudo apt -f install 修复损坏的依赖关系 17
$ apt search redis 按关键字搜索源里的软件包 17
$ apt list --installed 列出本机已安装的全部软件包 17
$ apt show redis-server 查看某包的版本、依赖、大小详情 17

[!TIP] 国内下载慢先看 33 镜像加速,里面有 apt / pip / npm / Maven / Docker 各家镜像源。


7. SSH

详见 18 SSH 加固。

命令 作用 章节
$ ssh -p 2222 user@host 指定端口连接(改过端口必带 -p) 18
$ ssh-keygen -t ed25519 生成密钥对(现代算法,比 rsa 短且快) 18
$ ssh-copy-id user@host 把公钥装到服务器,实现免密登录 18
$ sudo sshd -t 校验 sshd 配置语法(restart 前必跑) 18
$ sudo fail2ban-client status sshd 查看爆破封禁情况 18
$ ssh -v user@host 输出详细连接过程(排错第一命令) 18
$ ssh user@host "df -h" 远程执行一条命令后立即返回(批量巡检用) 18
$ cat ~/.ssh/id_ed25519.pub 查看本地公钥(往服务器 authorized_keys 手工添加时用) 18

8. systemd 服务

详见 40 systemd、51 journalctl。

命令 作用 章节
$ sudo systemctl start myapp 启动服务 40
$ sudo systemctl stop myapp 停止服务 40
$ sudo systemctl restart myapp 重启服务(进程全新起) 40
$ sudo systemctl reload nginx 重载配置不断连接(仅支持 reload 的服务) 40
$ systemctl status myapp 看服务状态和最近日志 40
$ sudo systemctl enable --now myapp 设开机自启并立即启动(一步到位) 40
$ sudo systemctl daemon-reload unit 文件改完后让 systemd 重新读取 40
$ journalctl -u myapp -f 实时跟踪某服务日志 51
$ journalctl -u myapp --since "10 min ago" 查最近 10 分钟的日志 51
$ systemctl cat myapp 看 unit 文件实际内容(含自动生成的部分) 40
$ systemctl list-units --failed 列出所有启动失败的服务 40
$ systemd-analyze blame 看各服务开机耗时(谁拖慢了启动) 40
$ systemctl is-active myapp 脚本化检查服务是否在跑(适合写进巡检) 40
$ systemctl is-enabled myapp 检查是否设置了开机自启 40
$ sudo systemctl reset-failed myapp 清除失败状态,解除崩溃限流后可重新启动 40

9. 各软件验证命令(装完必跑)

安装与验证细节见 PART 2 各章:20 21 22 23 24 25 26 27 28 29 30 31 32。

命令 作用 章节
$ java -version 验证 JRE 可用 20
$ javac -version 验证 JDK 编译器(只有 JRE 的话这条会失败) 20
$ python3 --version 验证 Python 版本 21
$ node -v 验证 Node.js 版本 22
$ mysql --version 查看 MySQL 客户端版本 23
$ sudo mysql -e "SELECT 1;" 免密验证 MySQL 服务可用(root 走 auth_socket) 23
$ psql --version 查看 PostgreSQL 客户端版本 24
$ sudo -u postgres psql -c "SELECT version();" 验证 PostgreSQL 服务可用 24
$ mongosh --version 验证 MongoDB Shell 可用 25
$ redis-cli ping 应返回 PONG,验证 Redis 活着 26
$ sudo rabbitmqctl status 查看 RabbitMQ 运行状态 27
$ kafka-topics.sh --list --bootstrap-server localhost:9092 列出 topic,验证 Kafka 可连(4.x 无 ZooKeeper 参数) 28
$ sudo nginx -t 校验 Nginx 配置语法(reload 前必跑) 29
$ nginx -v 查看 Nginx 版本 29
$ docker version 验证 Docker Engine 可用 30
$ docker compose version 验证 Compose v5 插件(注意 compose 是子命令,老写法 docker-compose 已弃用) 30
$ sudo certbot certificates 列出已托管证书及到期时间 32
$ mvn -v 验证 Maven(含其使用的 JDK) 31
$ git --version 验证 git 可用 31
$ pip3 --version 验证 pip 可用及其绑定的 Python 21
$ npm -v 验证 npm 可用 22

10. 备份与定时任务

详见 44 备份与定时。

命令 作用 章节
$ sudo mysqldump db > db.sql 逻辑备份 MySQL 库(root 默认 auth_socket,用 sudo 免密) 44
$ pg_dump dbname > db.sql 逻辑备份 PostgreSQL 库 44
$ mongodump --out /backup/mongo 备份 MongoDB(BSON 目录形式) 44
$ crontab -e 编辑当前用户的定时任务 44
$ crontab -l 列出当前用户的定时任务 44
$ systemctl list-timers 列出所有 systemd timer(certbot 续期在这看) 44
$ tar czf /backup/$(date +%F).tar.gz /opt/myapp 按日期打包目录 44
$ sudo crontab -u app -l 查看指定用户的定时任务 44
$ mysql -u root -p db < db.sql 从备份恢复 MySQL 库 44

[!WARNING] crontab -r 会整表删除定时任务且无确认,请用 crontab -e 删对应行。更多 cron 坑(PATH 缺失、% 转义)见 44 章。


11. 磁盘与清理

详见 55 磁盘满排错、51 journalctl、30 Docker、43 运维工具箱、17 包管理。

命令 作用 章节
$ df -h 查磁盘空间使用率 55
$ df -i 查 inode 使用率(小文件塞满时 df -h 却显示没满) 55
$ sudo du -xh --max-depth=1 / | sort -rh | head -20 逐级定位大目录(-x 不跨文件系统) 55
$ sudo journalctl --vacuum-size=200M 把 journal 压到指定大小以内 51
$ docker system df 查看 Docker 镜像/容器/卷占了多少磁盘 30
$ docker system prune 清理无用镜像、停止的容器(在用镜像不会被删) 30
$ sudo lsof +L1 找出「已删除但空间未释放」的文件 55
$ sudo ncdu / 交互式磁盘分析(方向键钻目录) 43
$ sudo apt clean 清空 apt 下载缓存 17
$ sudo apt autoremove 删除不再需要的依赖包 17
$ sudo journalctl --vacuum-time=7d 删除 7 天以前的 journal 日志 51
$ docker volume ls 列出所有数据卷(占空间大户) 30
$ docker image prune -a 删除所有未被容器引用的镜像 30

危险命令红名单

以下命令在错误场景下会让你失去数据或服务器控制权。逐条的风险说明见 00 前言「危险命令红名单」。

命令 危险点 安全做法
$ rm -rf / 或路径带变量 变量为空时变成删根,不可恢复 rm 前先 echo 打印变量;危险路径用绝对路径
$ sudo chmod -R 777 /dir 全目录开放写权限,等于裸奔 按需 755/644 + chown,见 13 章
$ crontab -r 整表删除定时任务,无确认 用 crontab -e 删单行;改前 crontab -l > cron.bak
$ kill -9 <数据库进程> 数据库跳过落盘清理,可能损坏数据 先 kill(SIGTERM),等它优雅退出,见 14 章
$ git push --force 覆盖远程他人提交 用 --force-with-lease,先沟通
$ sudo apt remove python3 Ubuntu 系统工具依赖它,可能带崩半个系统 绝不动系统 python3,新版本并行装,见 21 章
$ ufw enable(未放行 22 端口) 当场把自己 SSH 踢下线 先 ufw allow 22/tcp 再 enable,见 16 章
$ dd if=... of=/dev/sda 目标盘写错直接清空整块盘 写 of= 前用 lsblk 反复核对设备名

[!IMPORTANT] 执行任何「删除、覆盖、递归」类命令前,先停一秒:路径对吗?有备份吗?能回滚吗? 改前备份改后验证,见 92 排错手册三原则。

📄 版本速查表 91-cheatsheet-versions.md


title: 91 版本速查表(全篇唯一权威来源) updated: 2026-09 applies-to: Ubuntu 22.04 LTS

[!IMPORTANT] 本文件是全篇版本信息的唯一权威来源。各部署章节开头均引用本表。 如果发现某章写着的版本与本表不一致,以本表为准并修正该章。

所有版本号截至 2026 年 9 月。 软件版本持续变化,请务必学会自己核对(见文末「如何自己核对版本」)。


一、操作系统

项目 值 说明
本教程基准 Ubuntu 22.04 LTS (jammy) apt 系,云服务器最常见的入门选择
标准支持到期 2027-04 标准支持到 2027-04(以官方页面为准);届时需 Ubuntu Pro/ESM 才能继续获得安全更新
ESM(Ubuntu Pro) 到 2032 年 个人和小企业可免费用于最多 5 台机器
新机器建议 24.04 LTS 2026-09 起新购服务器建议优先考虑,支持周期更长
内核 5.15(22.04 HWE 可升至 6.x) uname -r 查看

[!NOTE] 截至 2026-09,最新的 Ubuntu LTS 已是 26.04 LTS(2026-04 发布,默认 OpenJDK 25、Python 3.14、内核 7.0)。 本教程仍以 22.04 为基准,是因为它存量最大、且 22.04/24.04 上的命令绝大多数通用。


二、运行时与语言

组件 推荐版本 Ubuntu 22.04 源默认 获取方式 关键提醒
Java (JDK) 21 LTS(稳妥)
25 LTS(最新)
openjdk-11/17/21 apt(21 及以下是 apt;25 需 Temurin 源或手动安装) Oracle 对 17 的 Premier Support 2026-09 到期;源里没有 JDK 8(需 Temurin 等外部源);必须配 JAVA_HOME;多版本用 update-alternatives
Python 3.12 / 3.13(稳妥)
3.14(最新)
3.10 deadsnakes PPA / pyenv / 源码编译到 /opt 绝不动系统自带 python3,apt 等系统工具依赖它
Node.js 24 LTS 12.x(已 EOL,勿用) NodeSource 官方源 / nvm 长期服务用 PM2 守护;Node 20 已于 2026-04 停止支持
Go 1.25+ 1.18 官网二进制包 解压到 /usr/local 即可;仅列本教程验证过的版本线,最新版以官网为准
Erlang(RabbitMQ 依赖) 27.x 24.x RabbitMQ 官方 Erlang 源 必须先于 RabbitMQ 安装;Erlang 26 已于 2026-05 EOL

三、数据库与缓存

组件 推荐版本 Ubuntu 22.04 源默认 获取方式 关键提醒
MySQL 8.0.x(生态最成熟)
8.4 LTS(新项目可选)
8.0.x 官方 apt 源 默认认证插件 caching_sha2_password,老客户端连不上;字符集统一 utf8mb4;Oracle Premier Support 2026-04 已到期(社区/发行版仍在维护);新项目可优先 8.4 LTS
PostgreSQL 17 / 18(18.4 当前稳定) 14 PGDG 官方源 postgresql.conf 与 pg_hba.conf 必须同时改才生效
MongoDB 8.0.x 无 官方 apt 源 先建管理员用户,再开启 auth;License 为 SSPL
Redis 8.x 6.0 官方源 / 源码编译 8.0 起 License 改为 AGPLv3(另可选 RSALv2/SSPLv1);BSD 替代品是 Valkey
RabbitMQ 4.3.x 无 官方 apt 源(+ 官方 Erlang 源) 4.3.6 起才支持 Erlang 28/29;Erlang 27.x 最稳妥

四、中间件与基础服务

组件 推荐版本 Ubuntu 22.04 源默认 获取方式 关键提醒
Kafka 4.2 / 4.3 无(官方二进制包) 官方二进制包(Scala 2.13 构建) 4.0 起彻底移除 ZooKeeper,只能跑 KRaft;需 Java 17+;所有 CLI 用 --bootstrap-server
Nginx 1.30 stable(mainline 1.31) 1.18 nginx.org 官方源 改配置先 nginx -t,再 reload 而非 restart
Docker Engine 29.x 20.x 官方 apt 源 Compose 是 v5 插件,命令为 docker compose(带空格)
Docker Compose v5.x 随插件(无独立源版本) 随 Engine 一起装的插件 老写法 docker-compose(带横杠)已弃用

五、工具链

组件 推荐版本 / 说明 获取方式
Git 2.34+(22.04 源默认即可) apt
Maven 3.9.x(仅列本教程验证过的版本线,最新版以官网为准) 官网二进制包 / apt
Gradle 8.x(仅列本教程验证过的版本线,最新版以官网为准) 官网二进制包
build-essential 含 gcc/g++/make apt
certbot 最新版 推荐 snap install certbot,或 apt

六、版本选择的三条通用原则

原则一:生产环境只选 LTS / 稳定线

LTS(Long Term Support) 的含义是厂商承诺在该版本上持续提供安全更新和问题修复数年。

  • Java:8 / 11 / 17 / 21 / 25 是 LTS,中间的 18/19/20/22/23/24/26 是半年一换的功能版
  • MySQL:8.0 与 8.4 是 LTS,9.x 是创新版(Innovation),不适合生产
  • PostgreSQL:每年一个大版本,社区维护 5 年
  • Node.js:偶数版(20/22/24)是 LTS,奇数版(21/23)是短期版

[!WARNING] 非 LTS 版本只适合学习和尝鲜。 它们通常只有约 6 个月的支持期,到期后不再有安全补丁,生产环境用了等于裸奔。

原则二:跟着「生态兼容性」走,而不是追最新

最新 ≠ 最合适。选版本前问两个问题:

  1. 我的业务框架支持它吗? 例如 Spring Boot 2.x 不支持 JDK 21+,老项目的 MySQL 驱动可能不认 caching_sha2_password
  2. 团队里其他人能维护吗? 一个没人会排错的新版本,风险远大于收益

推荐做法:选上一个 LTS。它已经过一两年的社区验证,坑被踩完了,文档也齐全。

原则三:先在测试环境验证,再上生产

版本升级(尤其是数据库大版本)永远遵循:

备份 → 测试环境升级 → 跑通业务 → 观察一段时间 → 生产停机窗口升级 → 验证 → 回滚预案待命

[!IMPORTANT] 数据库大版本不要原地升级。 正确做法是:新版本实例 + 数据导入 + 业务切换。原地 apt upgrade 把 MySQL 8.0 升到 8.4 可能直接起不来。


七、如何自己核对版本

教程会过期,但方法不会。这是本节最重要的内容。

查某个包在系统源里的可用版本

# 查看已装版本 + 候选版本 + 源里所有可用版本
$ apt policy nginx

预期输出(节选):

nginx:
  Installed: (none)
  Candidate: 1.18.0-6ubuntu14.4
  Version table:
     1.18.0-6ubuntu14.4 500
        500 http://archive.ubuntu.com/ubuntu jammy-updates/main amd64 Packages

Candidate 就是 apt install nginx 会装上的版本。

查源里所有可用版本(含其他源)

$ apt-cache madison postgresql

查已装软件的精确版本

# Debian 系
$ dpkg -l | grep nginx

# 只看版本号
$ dpkg-query -W -f='${Version}\n' nginx

查命令对应的版本(最常用)

$ java -version
$ python3 --version
$ node -v
$ nginx -v          # 小写 v,只输出版本号
$ nginx -V          # 大写 V,输出版本号 + 编译参数 + 模块列表
$ mysql --version
$ psql --version
$ redis-server --version
$ docker --version
$ docker compose version

查官方最新稳定版

  • 各项目官网的 Release Notes / Downloads 页面
  • endoflife.date 这类站点可以查各软件的支持到期时间

[!TIP] 养成习惯:装之前先 apt policy <包名>,确认源里的版本是不是你要的。 很多「装完发现版本不对」的问题,根源是没先看源里有什么,直接 apt install 装了系统默认的旧版。


八、常见版本相关问题

现象 根因 解决
装完发现是 1.18,想要 1.30 用了系统默认源 添加 nginx.org 官方源,见 29-nginx
apt install python3.12 说找不到包 22.04 源里没有 用 deadsnakes PPA 或 pyenv,见 21-python
RabbitMQ 装不上,报 Erlang 版本冲突 先装了系统源里的旧 Erlang 卸载旧 Erlang,改用 RabbitMQ 官方 Erlang 源,见 27-rabbitmq
Kafka 教程里的 --zookeeper 参数报错 Kafka 4.x 已移除 ZooKeeper 改用 --bootstrap-server,见 28-kafka
docker-compose 命令找不到 Compose v2+ 是插件形式 用 docker compose(空格),见 30-docker
老程序连不上 MySQL 8 认证插件 caching_sha2_password 不兼容 建用户时指定 mysql_native_password,见 23-mysql
apt install 装的 Redis 是 6.0 系统源太旧 用 Redis 官方源或源码编译,见 26-redis

本章命令速查

命令 作用
lsb_release -a 查看系统发行版与版本号
uname -r 查看内核版本
apt policy <包> 查看该包已装版本、候选版本及来源
apt-cache madison <包> 列出源里所有可用版本
dpkg -l | grep <关键字> 查看已安装包及版本
dpkg-query -W -f='${Version}\n' <包> 只输出已装包的版本号
<命令> --version / -v / -V 查各组件版本
apt list --upgradable 列出可升级的包

相关章节

📄 通用排错手册 92-troubleshooting-playbook.md


title: 通用排错手册 updated: 2026-09 applies-to: Ubuntu 22.04 LTS

本手册分两部分:第一部分教思路(排错方法论,适用于任何故障),第二部分是症状索引(按症状查原因和排查命令,链接到各详细章节)。

[!TIP] 救火时直接跳到第二部分对症状。但强烈建议第一部分读一遍——方法论学会了,没有索引表的故障也能排。


第一部分:排错方法论

五步法

步骤 做什么 关键问题
① 现象 搞清到底什么不对 报错原文是什么?有没有截图?从什么时间开始?是渐渐变差还是突然坏掉?
② 范围 圈定问题边界 一台还是多台?一个服务还是全部服务?最近改了什么?
③ 证据 收集客观数据 日志说了什么?状态命令输出什么?监控曲线哪个点开始异常?
④ 假设 提出候选原因 按概率从高到低排序,一次只验证一个假设
⑤ 验证与记录 修复并沉淀 问题解决了吗?写复盘文档了吗?

② 里的核心问题:「最近改了什么?」

这是排错的第一问。运维经验中,绝大多数故障由最近一次变更引起:

  • 昨天改过配置文件 → 先 diff 回看,最可能就是它
  • 上周 apt upgrade 过 → 版本行为变了,查 91 版本速查 核对
  • 刚部署了新应用 → 端口冲突 / 内存挤占 / 依赖缺失
  • 什么都没改却坏了 → 再看「外部变了没」:磁盘满了?证书到期了?上游挂了?
# 找最近 3 天内被修改过的配置文件
$ sudo find /etc /opt -type f -mtime -3 -name "*.conf" 2>/dev/null

# 查最近登录与执行过的 sudo 命令
$ last -20
$ sudo grep sudo /var/log/auth.log | tail -50

④ 假设:一次只验证一个

同时改三个地方再重启,好了也不知道是哪个救的,坏了更排查不清。正确姿势:

  1. 排出候选原因(按概率)
  2. 验证 A → 不对 → 恢复现场 → 再验证 B
  3. 每次只动一个变量

⑤ 记录:修完不算完

复盘文档最小字段:
- 现象(报错原文 + 时间)
- 根因(一句话说清为什么)
- 修复动作(执行过的命令)
- 预防措施(怎么保证不再犯)

排错三原则

  1. 先看日志再猜。 报错信息就写在日志里,瞎猜重启只是在掷骰子。日志怎么看见 50 章 和 51 journalctl。
  2. 一次只改一个变量。 见上文步骤 ④。
  3. 改前备份,改后验证。
# 改前备份:带日期后缀,永不覆盖旧备份
$ sudo cp /etc/nginx/nginx.conf /etc/nginx/nginx.conf.bak.$(date +%F-%H%M)

# 改后验证:Nginx 的例子
$ sudo nginx -t
$ sudo systemctl reload nginx
$ curl -I http://localhost

通用排查命令组合拳

不确定从哪下手时,按故障类型直接复制这四组「第一条命令」。

服务类(某个服务不对劲)

# 一条链:状态 → 最近 50 行日志 → 端口在听吗
$ systemctl status myapp
$ journalctl -u myapp -n 50 --no-pager
$ sudo ss -lntp | grep <端口>

网络类(连不上 / 访问不到)

# 一条链:本机 IP → 监听端口 → 防火墙 → 本机 curl 自测
$ ip a
$ sudo ss -lntp
$ sudo ufw status verbose
$ curl -I http://localhost:<端口>

磁盘类(写不进去 / 报 No space left)

# 一条链:空间 → inode → 找大目录 → 找已删除未释放的文件
$ df -h
$ df -i
$ sudo du -xh --max-depth=1 / | sort -rh | head -10
$ sudo lsof +L1 | head

详细步骤见 55 磁盘满排错。

性能类(卡 / 慢 / 负载高)

# 一条链:负载 → 内存 → CPU/IO 明细 → 谁在吃资源
$ uptime
$ free -h
$ vmstat 1 5
$ top

详细步骤见 14 进程管理、43 运维工具箱。


第二部分:症状索引表

用法:找到症状 → 看「最可能原因」(按概率排序,最多 3 个)→ 跑「排查命令」→ 需要细节再点进章节。

1. 连不上服务器(SSH)

详见 18 SSH 加固、16 防火墙、15 网络配置。

[!IMPORTANT] SSH 已经断开时,systemctl status ssh、ufw status 这类命令无法再通过 SSH 执行——须经云厂商控制台的 VNC / 救援模式登录服务器后运行,然后再从本地重新连接。

症状 最可能原因(按概率) 排查命令 章节
Connection refused ① sshd 没起来 ② sshd 改了端口而你还连 22 ③ 机器根本没开机 经控制台 VNC 登录服务器后:sudo systemctl status ssh、sudo ss -lntp | grep sshd;确认端口后再从本地 ssh -p <端口> user@host 18
Connection timed out ① 云厂商安全组没放行 ② ufw 没放行 ③ IP 写错 / 实例已释放 经控制台 VNC 登录服务器后:sudo ufw status verbose;再到控制台核对安全组;从本地 ping -c 4 <IP> 看是否完全不通 16
Permission denied (publickey) ① 服务器上没有你的公钥 ② 私钥权限太开放 ③ 禁用了密码登录而你没配密钥 ssh -v user@host 看卡在哪一步;本地 ls -l ~/.ssh/,私钥须 600:chmod 600 ~/.ssh/id_ed25519 18
Too many authentication failures ssh-agent 里塞了太多密钥被服务器拒绝 ssh -o IdentitiesOnly=yes -i ~/.ssh/指定私钥 user@host 18
突然连不上、服务器其他服务正常 ① fail2ban 把自己的 IP 误封(多次输错密码 / 爆破误判) 经控制台 VNC 登录服务器后:sudo fail2ban-client status sshd 看封禁列表;解封 sudo fail2ban-client set sshd unbanip <被封IP> 18

快速处置:

# SSH 已连不上时,以下命令须经云厂商控制台 VNC/救援模式登录服务器后执行
# 确认 sshd 活着、在听哪个端口
$ systemctl status ssh
$ sudo ss -lntp | grep sshd
# 确认防火墙是否放行 22
$ sudo ufw status verbose

2. 网站 / API 打不开

详见 29 Nginx、40 systemd、16 防火墙。

症状 最可能原因(按概率) 排查命令 章节
白屏 / 连接被拒绝 ① 后端应用没起来 ② 端口没监听 ③ 反代 upstream 写错端口 systemctl status myapp;sudo ss -lntp | grep <端口>;curl -I http://127.0.0.1:<后端端口> 直连后端验证 29
502 Bad Gateway ① 后端挂了 ② Nginx proxy_pass 端口/地址错 ③ 后端返回了 Nginx 不认的响应 journalctl -u myapp -n 50 --no-pager;sudo tail -20 /var/log/nginx/error.log;直连后端 curl -I 对比 29
504 Gateway Timeout ① 后端处理太慢(慢 SQL、死锁) ② proxy_read_timeout 太短 看后端慢请求日志 53 章;临时调大 proxy_read_timeout 再查根因 29
403 Forbidden ① Nginx 无权读目录(属主不是 www-data) ② 目录缺 x 权限 ③ autoindex 关闭且无 index 文件 ls -l 看站点目录属主与权限;sudo tail -20 /var/log/nginx/error.log 找 permission denied 29、13
404 Not Found ① root/alias 路径写错 ② sites-enabled 没建软链 ③ 前端路由没配 try_files ls -l /etc/nginx/sites-enabled/ 确认软链;curl -I 看是 Nginx 404 还是应用 404 29

快速处置:

# 判断是 Nginx 的问题还是后端的问题
$ curl -I http://127.0.0.1:<后端端口>    # 直连后端
$ sudo nginx -t && sudo tail -20 /var/log/nginx/error.log

3. 服务起不来(systemd)

详见 40 systemd、50 日志基础、51 journalctl。

症状 最可能原因(按概率) 排查命令 章节
status=203/EXEC ① ExecStart 路径写错或不存在 ② 没用绝对路径 ③ 脚本没有执行权限 systemctl cat myapp 看 ExecStart;ls -l <路径> 核对存在且可执行 40
status=1/FAILURE 立即退出 ① 配置文件语法错 ② 依赖的数据库/端口连不上 ③ 工作目录或用户不对 journalctl -u myapp -n 100 --no-pager,报错在日志里,不在 status 里 51
Start request repeated too quickly 反复崩溃触发限流,systemd 拒绝再拉起 sudo systemctl reset-failed myapp 后先手动 systemctl start 看真实报错 40
Address already in use 端口被旧进程 / 别的服务占着 sudo lsof -i:<端口> 找占用者;旧进程 kill 掉或换端口 43
改了 unit 文件没生效 忘了 daemon-reload sudo systemctl daemon-reload && sudo systemctl restart myapp 40
开机自启没生效 只 enable 了一个,或 enable 的本质没理解(建软链) systemctl is-enabled myapp;systemctl list-units --failed 40

快速处置:

# 服务起不来的标准三连
$ systemctl status myapp
$ journalctl -u myapp -n 100 --no-pager
$ systemctl cat myapp

4. 数据库连不上

详见 23 MySQL、24 PostgreSQL、25 MongoDB、26 Redis。

症状 最可能原因(按概率) 排查命令 章节
MySQL Access denied ① root 走 auth_socket,用密码连当然失败 ② 新用户建了但没 FLUSH PRIVILEGES/授权范围不对 ③ caching_sha2 与老客户端不兼容 $ sudo mysql(本机免密进)看用户表;核对授权语句见 23 章 23
MySQL 远程连不上 ① bind-address 还是 127.0.0.1 ② 云安全组/ufw 没放 3306 ③ 用户授权 host 不是 % sudo ss -lntp | grep 3306 看绑定地址;nc -zv <IP> 3306 测端口 23、16
PG Peer authentication failed 本地连接走 peer 认证,系统用户名 ≠ 数据库用户名 sudo -u postgres psql 以 postgres 系统用户进;或改 pg_hba 为 md5/scram,见 24 章 24
PG 远程连不上 ① 只改了 listen_addresses 没改 pg_hba ② 反之只改了 pg_hba 两个文件必须同时改:postgresql.conf + pg_hba.conf,见 24 章 24
Mongo Authentication failed ① 先开了 auth 才建用户,把自己锁外面 ② 连的库名不对(用户建在哪个库就 authenticate 到哪个库) 先关 auth 进去核对用户:use admin; show users;;正确顺序见 25 章 25
Redis NOAUTH / WRONGPASS ① 客户端没传 requirepass 的密码 ② 改过密码客户端还用旧的 redis-cli 后 AUTH <密码>;redis-cli -a <密码> ping 应返回 PONG 26

快速处置:

# 先确认服务本体活着,再谈认证
$ systemctl status mysql      # 或 postgresql / mongod / redis
$ sudo ss -lntp | grep -E "3306|5432|27017|6379"

5. 磁盘满了

详见 55 磁盘满排错。

症状 最可能原因(按概率) 排查命令 章节
No space left on device(df -h 满) ① 日志暴涨 ② Docker 镜像/卷堆积 ③ 备份文件没清理 df -h 确认哪个分区;sudo du -xh --max-depth=1 / | sort -rh | head 逐级钻 55
写文件仍报满但 df -h 显示没满 inode 耗尽(海量小文件) df -i 确认 IUse%;find <目录> -type f | wc -l 数文件 55
删了大文件空间没释放 进程还握着已删除文件的句柄 sudo lsof +L1 找到进程,重启该服务即释放 55

快速处置:

# 磁盘满标准四连(按顺序跑)
$ df -h
$ df -i
$ sudo du -xh --max-depth=1 / | sort -rh | head -10
$ sudo lsof +L1 | head

6. CPU / 内存 / 负载高

详见 14 进程管理、43 运维工具箱。

症状 最可能原因(按概率) 排查命令 章节
负载高但 CPU 不高 ① IO 等待(wa 高)② 磁盘/网络卡 vmstat 1 看 wa 列;iostat -x 1 看哪块盘 util 高 43
CPU 100%,某个进程吃满 ① 业务流量正常增长 ② 死循环/正则回溯 ③ 被挖矿程序入侵 top 按 P 排序找元凶;陌生进程先 lsof -p <PID> 看它打开的文件 14
内存耗尽、进程莫名消失 OOM killer 杀了它(内存不足时系统挑进程杀) sudo dmesg -T | grep -i "killed process";journalctl -k --since today | grep -i oom 14
free -h 显示 free 很少 可能是正常缓存占用,先看 available 列再决定 free -h:available 够就不用管;不够再 top 按 M 排序找进程 14

快速处置:

# 性能四连
$ uptime            # 负载几个 9?
$ free -h           # available 还剩多少
$ vmstat 1 5        # wa 高不高
$ top               # 谁在吃资源(P=CPU,M=内存)

7. 权限错误(Permission denied 四层排查)

详见 13 用户与权限。遇到 Permission denied,按顺序问四层:

层 检查什么 排查命令
① 文件权限 属主/属组/其他人,当前用户落在哪一档 $ ls -l <文件>;$ id 看自己是谁、在哪个组
② 目录路径权限 路径上每一级目录都需要 x(进入)权限 $ namei -l /opt/myapp/logs/app.log 逐级显示权限
③ 进程属主 服务以什么用户在跑?它有权限吗(而不是「我」) $ ps aux | grep myapp 看第一列用户名
④ SELinux/AppArmor Ubuntu 默认 AppArmor,可能限制服务行为 $ sudo aa-status;journalctl -k | grep -i apparmor

快速处置:

# 权限四连
$ id
$ ls -l <文件>
$ namei -l <完整路径>
$ ps aux | grep myapp

[!WARNING] 别用 chmod -R 777 「一键解决」。正确做法是 chown 对属主 + 权限给到 755/644,见 13 章。

8. 网络不通

详见 15 网络配置、16 防火墙、30 Docker。

症状 最可能原因(按概率) 排查命令 章节
能 ping 通但 curl 不通 ① 服务没监听该端口 ② 防火墙拦了 TCP ③ 服务只绑了 127.0.0.1 sudo ss -lntp(本地确认监听地址);nc -zv <IP> <端口>(远端测端口) 15、16
DNS 解析失败 ① resolv.conf 配置错或被覆盖 ② 上游 DNS 挂了 dig example.com、dig @8.8.8.8 example.com 对比;resolvectl status 15
端口不通四层排查 ① 应用没监听 → ② 本机 ufw → ③ 云安全组 → ④ 中间网络 逐层验证:ss -lntp → ufw status → 控制台安全组 → nc -zv 16
Docker 映射端口防火墙拦不住 / ufw 规则对 Docker 无效 Docker 直接改 iptables,绕过了 ufw sudo iptables -L DOCKER -n;要么信任内网要么绑定 127.0.0.1:端口 30、16

快速处置:

# 端口不通四连(从内到外)
$ sudo ss -lntp | grep <端口>        # ① 在听吗,绑的哪个地址
$ sudo ufw status verbose            # ② 本机防火墙
$ nc -zv <服务器IP> <端口>           # ③ 从外部测
# ④ 还不通 → 去云控制台看安全组

9. 日志相关

详见 50 日志基础、51 journalctl、52 查询配方、54 logrotate。

症状 最可能原因(按概率) 排查命令 章节
journalctl -u xxx 没输出 ① unit 名写错 ② journald 没持久化(重启即丢) ③ 服务输出没进 journal systemctl cat xxx 核对名字;ls /var/log/journal/ 是否存在(不存在=未持久化) 51
应用日志文件没内容 ① 应用写到别的路径 ② stdout 被 nohup 重定向到别处 ③ 日志级别过滤掉了 ls -l /proc/<PID>/fd/1 看进程 stdout 实际指向哪 50
磁盘被日志撑爆 ① 某应用 debug 级别狂刷 ② logrotate 没配 / 没生效 sudo du -sh /var/log/* | sort -rh | head;sudo logrotate -d /etc/logrotate.conf 干跑验证 54、55
journal 占用几十 GB 默认无上限或上限太大 journalctl --disk-usage;sudo journalctl --vacuum-size=200M 立即回收 51
历史日志被打包找不到内容 logrotate 压成了 .gz zgrep "ERROR" app.log.2.gz 直接搜压缩包 52

快速处置:

# 日志三连
$ journalctl --disk-usage
$ sudo du -sh /var/log/* | sort -rh | head
$ sudo journalctl --vacuum-size=200M   # 确认要清理再执行

10. 时间 / 证书问题

详见 32 HTTPS 证书、15 网络配置。

症状 最可能原因(按概率) 排查命令 章节
浏览器提示证书过期(但 certbot 装了) ① 自动续期 timer 没跑 ② Nginx reload 没执行,新证书没加载 ③ certbot 装的证书路径和 Nginx 配的不一致 sudo certbot certificates 看到期时间;systemctl list-timers | grep certbot;sudo nginx -t && sudo systemctl reload nginx 32
续期失败(domain 验证不过) ① 80 端口被占/被防火墙拦 ② DNS 没指到本机 curl -I http://<域名>;dig +short <域名> 对比本机公网 IP 32、16
HTTPS 报 not yet valid / 时间诡异 服务器时间漂移(重启后丢时间、NTP 没同步) timedatectl 看 System clock synchronized 是否 yes;sudo chronyc tracking 15
API 调用报签名/时间戳错误 本机时间与标准时间偏差过大 timedatectl 对比手机时间;sudo chronyc makestep 立即校准 15

快速处置:

# 时间与证书三连
$ timedatectl
$ sudo certbot certificates
$ systemctl list-timers | grep certbot

11. 版本 / 依赖问题

详见 17 包管理、91 版本速查、27 RabbitMQ、28 Kafka。

症状 最可能原因(按概率) 排查命令 章节
apt 报 NO_PUBKEY 第三方源的 GPG 密钥没装或过期 按报错补钥:signed-by 方式导入,步骤见 17 章 17
装到的版本和教程不一样 Ubuntu 官方源自带旧版(如 PostgreSQL、Node) apt policy <包名> 看候选版本来自哪个源;对照 91 版本速查 91
RabbitMQ 起不来 / Erlang 崩溃 Erlang 版本和 RabbitMQ 不匹配 先查 RabbitMQ 与 Erlang 版本对应表,先装匹配的 Erlang 再装 RabbitMQ 27
Kafka 命令报 ZooKeeper 参数错误 网上老教程是 3.x 的;4.0 起移除 ZooKeeper,只能 KRaft 去掉 --zookeeper,改用 --bootstrap-server localhost:9092 28
升级后某软件突然行为异常 apt upgrade 把锁外的包升了级 apt policy <包名> 核对版本;需要锁版本用 apt-mark hold 17

快速处置:

# 版本核对三连
$ apt policy <包名>
$ <软件> --version
# 不确定该装哪个版本 → 查版本速查表

12. Docker 相关

详见 30 Docker、55 磁盘满排错。

症状 最可能原因(按概率) 排查命令 章节
permission denied ... /var/run/docker.sock 当前用户不在 docker 组(或还没重新登录) sudo usermod -aG docker $USER 后退出重登再试;id 确认组已生效 30
docker pull 超时 / 很慢 国内访问 Docker Hub 受阻,没配镜像加速 配置 daemon.json 镜像源后 systemctl restart docker,见 30 章 30、33
容器把磁盘撑爆 容器 stdout 日志无上限(json-file 默认不轮转) docker system df;sudo du -sh /var/lib/docker/containers/* 找巨大 json 日志 30、55
容器重启后数据丢了 数据写在容器层,没挂 volume docker inspect <容器> 看 Mounts;有状态数据一律挂 -v 卷 30
改了 daemon.json 没生效 改完执行了 reload,但 Docker 需要 restart sudo systemctl restart docker(注意会重启所有容器) 30

快速处置:

# Docker 排查四连
$ docker system df
$ sudo du -sh /var/lib/docker/containers/* | sort -rh | head
$ docker inspect <容器> --format '{{json .Mounts}}'
$ docker logs --tail 50 <容器>

结尾:修复后的三件事

问题修好 ≠ 排错结束。做完这三件事,这次故障才有价值:

1. 记录到复盘文档

把「现象 → 根因 → 修复动作 → 预防措施」四行写下来。下次同样的问题(一定会再遇到),五分钟内解决。模板见本手册第一部分「⑤ 记录」。

2. 补监控与告警

这次是用户/巡检才发现的,下次要让它主动报:

  • 磁盘超过 80% 告警(本教程不覆盖监控栈,但至少配一个 crontab 脚本检查 df -h 并发通知,见 44 章)
  • 关键服务 systemctl is-active 定时检查
  • 证书到期前 30 天告警(certbot timer 只负责续期,续期失败要能被发现,见 32 章)

3. 检查同类服务是否同病

  • 这台机器的日志会撑爆 → 另一台呢?
  • 这个服务的 logrotate 没配 → 一起配了,见 54 章
  • 这个证书快到期 → certbot certificates 把所有证书都看一遍

[!IMPORTANT] 做完完整部署演练后,请用 60 端到端实战 各阶段的验收检查与末尾的汇总验收清单逐项核对:应用可访问、HTTPS 有效、systemd 自启、日志在写、备份在跑、防火墙最小开放。清单全绿,才算真正交付。

📖 目录与阅读路线 README.md


title: 目录与阅读路线 updated: 2026-09 applies-to: Ubuntu 22.04 LTS

本教程共 39 个分章文件 + 本目录文件。所有版本号截至 2026 年 9 月,核对方法见 版本速查。


阅读路线

路线一:新手直通(推荐第一次完整读)

目标:从零到能独立部署一个项目。

00 前言与约定   →   10 Shell与文件系统  →  11 文件操作  →  12 文本处理
      ↓
13 用户与权限   →   17 包管理           →  40 systemd
      ↓
按需挑 PART 2 组件章(先 20-jdk / 21-python,再数据库、Nginx)
      ↓
29 Nginx 反代   →   32 HTTPS 证书       →  60 端到端实战
      ↓
50-55 日志(项目上线后必读)

路线二:有基础,只装某个环境

直接跳到 PART 2 对应章节,然后回看 40-systemd.md 把它托管成服务、开机自启。

我要装 去这章
Java 20
Python 21
Node.js 22
MySQL 23
PostgreSQL 24
MongoDB 25
Redis 26
RabbitMQ 27
Kafka 28
Nginx 29
Docker 30
Git / Maven 31
HTTPS 证书 32
下载太慢 33

路线三:服务器出问题了(救火)

症状 先去这章
连不上服务器 / SSH 断了 18、92
端口不通、服务访问不到 16、15
磁盘满了 55
服务起不来 40、92
要看报错在哪 50、51、52
要统计错误量 / 慢请求 53
日志文件太大撑爆磁盘 54
进程卡死 / CPU 飙高 14、43
权限不对、Permission denied 13
不知道该装哪个版本 91

PART 0 准备

文件 主题 行数级别
00-preface.md 阅读约定、安全基线、环境变量原理、PATH、危险命令红名单 大

[!IMPORTANT] 不要跳过 00 章。 它定义了全篇的命令写法约定、安全基线和「哪些命令会让你失去服务器控制权」。后面所有章节都依赖它。


PART 1 Linux 基础操作

文件 主题
10-shell-and-filesystem.md FHS 目录地图、绝对路径/相对路径、软链接 vs 硬链接(inode 视角)、du / df
11-file-operations.md 增删改查、tar / zip、scp / rsync(结尾斜杠语义差异)
12-text-processing.md 重定向与管道、grep / find / sed / awk / xargs / sort / uniq
13-users-and-permissions.md 用户与组、rwx 与八进制换算、为什么 755 不是 777、umask、setuid/setgid/sticky、ACL
14-process-management.md ps / top / kill、信号表 SIGTERM vs SIGKILL、前后台、nohup、load average
15-network-configuration.md ip / ss(取代 netstat)、netplan 静态 IP、DNS、curl / wget、chrony 时间同步
16-firewall.md 防火墙三层模型、ufw 完整用法、firewalld zone/service、iptables 四表五链、排错顺序
17-package-management.md apt / dpkg、apt policy 查版本、signed-by 取代 apt-key、dnf 对照、镜像源
18-ssh-hardening.md 密钥登录、禁 root 直登、改端口、fail2ban 防爆破、连接排错

PART 2 环境部署

每章统一结构:版本选择 → 安装 → 环境变量 → 最小必要配置 → 验证 → systemd 托管 → 常见问题(语言运行时与数据库类章节适用;Git/HTTPS/镜像加速等工具类与专题类章节按专题组织,不强制此模板)。

文件 主题 关键提醒
20-jdk.md JDK 17/21/25 Temurin 源、update-alternatives 多版本切换、JAVA_HOME
21-python.md Python 3.12/3.13 绝不动系统 python3;deadsnakes / pyenv / 源码三条路
22-nodejs.md Node.js 24 LTS nvm vs NodeSource、pnpm、PM2
23-mysql.md MySQL 8.0 / 8.4 LTS caching_sha2_password 兼容问题、utf8mb4、最小授权
24-postgresql.md PostgreSQL 17/18 PGDG 源;postgresql.conf 与 pg_hba.conf 必须同时改
25-mongodb.md MongoDB 8.0 先建管理员用户,再开 auth —— 顺序反了会把自己锁在外面
26-redis.md Redis 8.x 8.0 起 AGPLv3,可用 Valkey(BSD) 替代;maxmemory 策略
27-rabbitmq.md RabbitMQ 4.3.x 先装匹配版本 Erlang,再装 RabbitMQ;版本对应表必看
28-kafka.md Kafka 4.2/4.3 4.0 起移除 ZooKeeper,只能跑 KRaft;网上的 ZooKeeper 教程是 3.x 老做法
29-nginx.md Nginx 1.30 stable nginx.org 源、sites-available 软链机制、nginx -t、reload vs restart
30-docker.md Docker Engine 29.x docker compose 带空格;daemon.json 改完要 restart 不是 reload
31-git-and-build-tools.md git / Maven / Gradle / make SSH key、Maven 镜像、build-essential
32-https-certbot.md Let's Encrypt certbot 申请、自动续期 timer、证书过期排查
33-mirrors-acceleration.md 国内镜像加速 apt / pip / npm / Maven / Docker 镜像源(可选,不替换默认源)

PART 3 服务管理与运维

文件 主题
40-systemd.md unit 文件结构、Type= 区别、Restart=always 为什么这么配、enable 的本质是建软链、自定义 unit、资源限制
41-source-compile-install.md ./configure --prefix → make -j → make install、动态库 ldconfig、源码安装的卸载困境
42-process-supervision.md systemd / supervisor / PM2 三种守护方式对比与选型建议
43-ops-toolbox.md htop / btop / iotop / ncdu / lsof / strace / tcpdump / fail2ban / tmux
44-backup-and-cron.md crontab 五段语法与常见坑(PATH 缺失、% 转义)、systemd timer、3-2-1 备份策略、恢复演练

PART 4 日志查询与分析

文件 主题
50-log-fundamentals.md /var/log 目录地图、rsyslog 与 journald 的分工与共存、应用日志放哪、日志格式约定
51-journalctl.md --since/--until、-u、-p、-b、-f、持久化开启、--vacuum-size 清理
52-log-query-recipes.md tail -f vs tail -F、less +F、grep -A/-B/-C 上下文、zgrep 查历史压缩包
53-log-analysis.md awk 字段统计、错误 TOP N、按 IP / 状态码聚合、按分钟切时间窗、慢请求排序
54-logrotate.md 配置项逐条讲解、copytruncate vs postrotate kill -USR1 的区别与坑、logrotate -d 干跑
55-disk-full-troubleshooting.md df -h vs df -i、du 逐级定位、文件已删但空间没释放(`lsof

PART 5 实战

文件 主题
60-hands-on-project.md 端到端完整部署:准备服务器 → JDK → MySQL → Redis → 部署 Java 应用 → systemd 托管 → Nginx 反代 → HTTPS → 日志与备份验收

附录

文件 主题
90-cheatsheet-commands.md 跨章高频命令索引:命令 + 一句作用 + 所属章节链接
91-cheatsheet-versions.md 版本速查表(全篇唯一权威来源):推荐版本、Ubuntu 22.04 源默认版本、获取方式、关键提醒
92-troubleshooting-playbook.md 通用排错方法论(现象 → 范围 → 证据 → 假设 → 验证)+ 症状 → 章节索引表

本教程不覆盖

明确划清边界,避免范围蔓延:

  • Kubernetes / 容器编排 —— Docker 单机编排(Compose)会讲,K8s 不涉及
  • CI/CD 流水线 —— Jenkins / GitLab CI / GitHub Actions 不涉及
  • 监控告警栈 —— Prometheus / Grafana / ELK 只提一句,不做深度配置
  • 内核调优、性能压测 —— 只在运维工具章给出排查入口
  • 桌面 Linux —— 只讲服务器(无图形界面,SSH 远程操作)

维护约定

  • 每个文件头部有 updated 字段,改动时请同步更新
  • 版本信息只维护在 91,其他章节引用它,避免口径不一致
  • 新增组件章节时,同步更新:根 README.md、本文件、90 命令速查、91 版本速查

📁 配套文件目录

linux-server/
  README.md
  docs/
    00-preface.md
    10-shell-and-filesystem.md
    11-file-operations.md
    12-text-processing.md
    13-users-and-permissions.md
    14-process-management.md
    15-network-configuration.md
    16-firewall.md
    17-package-management.md
    18-ssh-hardening.md
    20-jdk.md
    21-python.md
    22-nodejs.md
    23-mysql.md
    24-postgresql.md
    25-mongodb.md
    26-redis.md
    27-rabbitmq.md
    28-kafka.md
    29-nginx.md
    30-docker.md
    31-git-and-build-tools.md
    32-https-certbot.md
    33-mirrors-acceleration.md
    40-systemd.md
    41-source-compile-install.md
    42-process-supervision.md
    43-ops-toolbox.md
    44-backup-and-cron.md
    50-log-fundamentals.md
    51-journalctl.md
    52-log-query-recipes.md
    53-log-analysis.md
    54-logrotate.md
    55-disk-full-troubleshooting.md
    60-hands-on-project.md
    90-cheatsheet-commands.md
    91-cheatsheet-versions.md
    92-troubleshooting-playbook.md
    README.md

完整源码 Gitee 仓库浏览 →