转自:马哥Linux运维
一、概述
1.1 背景介绍
带过好几个运维新人,从实习生到转岗的开发,这些坑每个人都会踩一遍。有人把生产环境的 /var 目录删了,有人把自己锁在 SSH 外面,有人改完 nginx 配置连备份都没留。同样的事故报告写了好多份,同样的善后流程走了无数遍。
与其每次事后复盘,不如提前把路铺好。这篇文章把最高频的 10 个坑位全部列出来,每个坑都附带真实案例、完整的修复命令、从根本上的预防方案。不是教科书式的理论,全是实操经验——有些是我自己踩过的,有些是团队成员踩完我帮着擦屁股的。
1.2 技术特点
-
10 个高频坑位:从 rm -rf误删到不看日志凭感觉排错,覆盖新人最常犯的错误 -
每个坑都有真实案例:不是编的场景,是线上真实发生过的事故 -
不是理论而是实操经验:所有命令都在生产环境验证过,所有脚本都在跑 -
统一结构:场景 → 踩坑过程 → 后果 → 正确做法 → 预防措施,方便速查
1.3 适用场景
-
初级运维:刚入行的运维工程师,对 Linux 有基本了解但缺少生产经验 -
转岗运维:从开发或测试转运维,有技术基础但不了解运维规范 -
运维培训:团队内部培训教材,新人入职必读清单 -
实习生入职指南:实习生第一周就应该看完的文档
1.4 环境要求
|
|
|
|
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
二、详细步骤
坑 1:rm -rf 误删——没有回收站意识
场景描述
清理服务器日志文件,目标是删除 /var/log/ 下的旧日志。这是新人最常接触的第一个高危操作。
踩坑过程
新人小王接到任务:清理 /var/log/ 下超过 30 天的日志文件。他打开终端,输入了这条命令:
rm -rf /var /log/
注意到没有?/var 和 /log/ 之间多了个空格。他本来想删的是 /var/log/,结果变成了删除 /var 目录和 /log/ 目录。
回车按下去的瞬间,终端没有任何提示。等他发现服务全部挂掉的时候,/var 下面的数据库文件、缓存文件、spool 目录已经全没了。
实测在 Ubuntu 22.04 上,rm -rf /var 执行大约 3-5 秒就能把整个目录清空,速度快到你根本来不及按 Ctrl+C。
后果
-
/var/lib/mysql/或/var/lib/postgresql/下的数据库数据全部丢失 -
/var/spool/下的邮件队列、cron 任务全部丢失 -
/var/run/下的 PID 文件丢失,所有服务无法正常管理 -
如果没有备份,数据不可恢复 -
生产环境出现这种事故,通常意味着 P0 级别故障
正确做法
方案一:使用 trash-cli 替代 rm
# Ubuntu/Debian 安装
sudo apt install -y trash-cli
# Rocky Linux 安装
sudo dnf install -y trash-cli
# 使用 trash-put 替代 rm
trash-put /var/log/old-app.log
# 查看回收站内容
trash-list
# 恢复误删文件
trash-restore
# 清空回收站(确认不需要后)
trash-empty 30 # 清除 30 天前的文件
方案二:安装 safe-rm 做保护
# 安装 safe-rm
sudo apt install -y safe-rm
# 配置保护目录列表
cat >> /etc/safe-rm.conf << 'EOF'
/
/bin
/boot
/dev
/etc
/home
/lib
/lib64
/proc
/root
/sbin
/sys
/usr
/var
/opt
EOF
# safe-rm 会拦截对受保护目录的删除操作
方案三:alias 加交互确认
# 在 ~/.bashrc 中添加
echo "alias rm='rm -i'" >> ~/.bashrc
source ~/.bashrc
# 删除时会逐个确认
rm -rf /var/log/old.log
# 输出:rm: remove regular file '/var/log/old.log'? y
预防措施
-
所有服务器统一安装 trash-cli,在系统初始化脚本中配置 -
生产环境禁止直接使用 rm -rf,通过 sudo 规则限制 -
关键目录配置 safe-rm保护 -
定期快照备份,误删后可以从快照恢复 -
使用配置管理工具(Ansible)统一推送 alias 配置
坑 2:直接在生产环境改配置不备份
场景描述
需要修改 nginx 配置增加一个反向代理,直接 vim /etc/nginx/nginx.conf 开改。
踩坑过程
新人小李要给新上线的服务加一条 nginx 反向代理规则。他直接打开配置文件:
vim /etc/nginx/nginx.conf
改了一堆 location 块,加了 upstream,改了 proxy_pass。保存退出后执行 nginx -s reload,结果报错:
nginx: [emerg] unknown directive "proyx_pass" in /etc/nginx/nginx.conf:47
拼写错误。他再次打开文件修改,改完又 reload,又报另一个错。来回改了四五次,这时候配置文件已经面目全非。他想回到最初的版本——没有备份。
这时候业务方反馈线上所有请求都返回 502 了。
后果
-
线上服务中断,用户请求全部报错 -
没有备份文件,无法快速回滚 -
紧急联系其他同事从其他渠道找到原始配置,恢复花了 40 分钟 -
这 40 分钟里所有用户都无法访问服务
正确做法
每次修改前,先备份
# 带时间戳的备份,绝对不会覆盖
cp /etc/nginx/nginx.conf /etc/nginx/nginx.conf.bak.$(date +%Y%m%d%H%M%S)
# 修改完后先测试语法
nginx -t
# 输出:nginx: configuration file /etc/nginx/nginx.conf test is successful
# 语法没问题再 reload
nginx -s reload
使用 etckeeper 做配置文件版本化
# 安装 etckeeper
sudo apt install -y etckeeper
# 初始化(自动用 git 管理 /etc 目录)
sudo etckeeper init
sudo etckeeper commit "Initial commit"
# 之后每次修改 /etc 下的文件,etckeeper 会自动追踪
# 手动提交变更
sudo etckeeper commit "Add reverse proxy for new-service"
# 查看历史
cd /etc && sudo git log --oneline
# 回滚到上一个版本
cd /etc && sudo git diff HEAD~1 nginx/nginx.conf
cd /etc && sudo git checkout HEAD~1 -- nginx/nginx.conf
使用 diff 对比变更
# 修改前备份,修改后对比差异
cp /etc/nginx/nginx.conf /tmp/nginx.conf.before
vim /etc/nginx/nginx.conf
diff /tmp/nginx.conf.before /etc/nginx/nginx.conf
预防措施
-
团队规范:任何配置变更前必须备份,把这条写进操作手册 -
生产环境部署 etckeeper,所有 /etc下的变更自动 git 追踪 -
用 Ansible/SaltStack 管理配置,配置文件以代码形式存在仓库里 -
nginx 修改后必须执行 nginx -t验证语法
坑 3:防火墙规则顺序搞错
场景描述
新开了一台服务器部署数据库,需要开放 3306 端口给应用服务器访问。加了规则但死活不生效。
踩坑过程
新人小赵在 iptables 里加了 ACCEPT 规则,但数据库还是连不上。他反复检查规则,语法没有问题:
# 小赵加的规则
iptables -A INPUT -s 10.0.1.100 -p tcp --dport 3306 -j ACCEPT
看起来完全正确,但就是不生效。查看完整规则列表才发现问题:
iptables -L -n --line-numbers
输出:
Chain INPUT (policy ACCEPT)
num target prot opt source destination
1 ACCEPT all -- 0.0.0.0/0 0.0.0.0/0 state RELATED,ESTABLISHED
2 ACCEPT icmp -- 0.0.0.0/0 0.0.0.0/0
3 ACCEPT all -- 0.0.0.0/0 0.0.0.0/0
4 ACCEPT tcp -- 0.0.0.0/0 0.0.0.0/0 state NEW tcp dpt:22
5 REJECT all -- 0.0.0.0/0 0.0.0.0/0 reject-with icmp-host-prohibited
6 ACCEPT tcp -- 10.0.1.100 0.0.0.0/0 tcp dpt:3306
第 5 条是 REJECT all,小赵的规则在第 6 条。iptables 是从上到下匹配的,到第 5 条就已经被拒绝了,第 6 条永远不会生效。
后果
-
数据库端口无法被应用服务器访问 -
如果新人不理解 iptables 规则顺序,可能会越改越乱 -
最坏情况下可能把自己的 SSH 规则也改没了
正确做法
# 插入规则到指定位置(在 REJECT 规则之前)
iptables -I INPUT 5 -s 10.0.1.100 -p tcp --dport 3306 -j ACCEPT
# 验证规则顺序
iptables -L -n --line-numbers
# 保存规则(Ubuntu)
sudo netfilter-persistent save
# 保存规则(Rocky Linux)
sudo iptables-save > /etc/sysconfig/iptables
更推荐:使用 firewalld 管理
# Rocky Linux 9 / Ubuntu 22.04+ 都可以用 firewalld
sudo systemctl enable --now firewalld
# 添加规则——按 zone 管理,不用操心顺序
sudo firewall-cmd --zone=public --add-rich-rule='rule family="ipv4" source address="10.0.1.100" port protocol="tcp" port="3306" accept' --permanent
# 重载配置
sudo firewall-cmd --reload
# 查看规则
sudo firewall-cmd --zone=public --list-all
更现代的选择:nftables
# Rocky Linux 9 默认后端已经是 nftables
# 直接编辑规则集
cat > /etc/nftables/mysql-access.nft << 'EOF'
table inet filter {
chain input {
type filter hook input priority 0; policy drop;
# 允许已建立连接
ct state established,related accept
# 允许 loopback
iif lo accept
# 允许 SSH
tcp dport 22 accept
# 允许特定 IP 访问 MySQL
ip saddr 10.0.1.100 tcp dport 3306 accept
# 允许 ICMP
ip protocol icmp accept
}
}
EOF
sudo nft -f /etc/nftables/mysql-access.nft
预防措施
-
不要直接用 iptables 命令行加规则,用 firewalld 或 nftables 的配置文件管理 -
每次修改防火墙后,从另一台机器测试连通性 -
改防火墙前保持一个备用 SSH 连接,防止把自己锁在外面 -
定期审计防火墙规则,删除不再需要的条目
坑 4:磁盘空间满了才发现
场景描述
服务器跑了几个月,某天凌晨突然所有服务都报错,SSH 也卡得不行。
踩坑过程
凌晨 2 点告警短信来了,应用返回 500。SSH 连上去输入命令响应极慢,好不容易敲出 df -h:
Filesystem Size Used Avail Use% Mounted on
/dev/sda1 50G 50G 0 100% /
根分区 100% 满了。查一下是什么占了空间:
du -sh /* 2>/dev/null | sort -rh | head -10
输出:
32G /var
8.5G /usr
4.2G /home
2.1G /opt
继续深入 /var:
du -sh /var/* 2>/dev/null | sort -rh | head -5
28G /var/log
2.1G /var/lib
1.2G /var/cache
28G 的日志文件。再看具体是哪个:
ls -lhS /var/log/*.log | head -5
-rw-r--r-- 1 root root 24G Mar 10 02:15 /var/log/app-debug.log
一个应用的 debug 日志写了 24G,因为开发部署的时候把日志级别设成了 DEBUG 忘了改回来。
后果
-
磁盘满导致所有写操作失败,数据库无法写入 -
应用无法写日志和临时文件,全部报 500 -
SSH 极度缓慢(bash history 也需要写磁盘) -
如果 /var/run在根分区,某些服务的 PID 文件也写不了
正确做法
紧急处理——快速腾出空间
# 1. 清理大日志文件(不要 rm,用 truncate 保持文件句柄)
truncate -s 0 /var/log/app-debug.log
# 2. 清理 apt 缓存
sudo apt clean
# 3. 清理旧内核(Ubuntu)
sudo apt autoremove --purge
# 4. 清理 journal 日志
sudo journalctl --vacuum-size=500M
# 5. 查找大于 100M 的文件
find / -type f -size +100M -exec ls -lh {} \; 2>/dev/null | sort -k5 -rh | head -20
# 6. 检查已删除但未释放的文件(进程还在写的文件 rm 了但空间不释放)
lsof +L1 | grep deleted
设置磁盘告警
# 简单的磁盘告警脚本(加入 crontab)
cat > /usr/local/bin/disk-alert.sh << 'SCRIPT'
#!/bin/bash
THRESHOLD=85
HOSTNAME=$(hostname)
df -h --output=pcent,target | tail -n +2 | whileread usage mount; do
usage_num=${usage%\%}
if [ "$usage_num" -gt "$THRESHOLD" ]; then
echo"[DISK ALERT] ${HOSTNAME}: ${mount} usage at ${usage}" | \
mail -s "Disk Alert: ${HOSTNAME}" ops-team@company.com
fi
done
SCRIPT
chmod +x /usr/local/bin/disk-alert.sh
# 每 10 分钟检查一次
echo"*/10 * * * * root /usr/local/bin/disk-alert.sh" > /etc/cron.d/disk-alert
预防措施
-
上线前必须配置 logrotate(见坑 8) -
部署 Prometheus + node_exporter,设置磁盘使用率超过 80% 告警 -
应用日志级别在生产环境设置为 INFO 或 WARN,绝不用 DEBUG -
单独挂载 /var/log分区,日志撑爆不影响根分区 -
设置 systemd-journald的SystemMaxUse=2G限制 journal 大小
坑 5:SSH 配置改错把自己锁在外面
场景描述
安全加固要求修改 SSH 端口和禁止 root 登录,改完之后再也连不上了。
踩坑过程
安全团队发了一份加固文档,新人小张按照文档操作:
vim /etc/ssh/sshd_config
改了三个地方:
Port 2222
PermitRootLogin no
PasswordAuthentication no
然后执行:
sudo systemctl restart sshd
退出当前 SSH 连接后再连——连不上。原因:
-
端口改成了 2222,但防火墙没放行 2222 -
禁止了 root 登录,但没有提前创建普通用户并配置 sudo -
禁止了密码认证,但没有提前配置 SSH 密钥
三条规则叠加,直接把所有登录方式堵死了。
后果
-
完全无法远程登录服务器 -
如果是物理服务器需要去机房接显示器键盘 -
如果是云服务器需要通过控制台的 VNC 功能登录 -
恢复过程需要 30 分钟到数小时不等
正确做法
# 第一步:修改前先验证语法
sudo sshd -t
# 如果有语法错误会直接报出来
# 第二步:修改前保持两个 SSH 连接
# 终端 1:做修改操作
# 终端 2:保持连接不动,作为备用
# 第三步:按正确顺序操作
# 3.1 先创建普通用户并配置 sudo
sudo useradd -m -s /bin/bash ops-admin
echo"ops-admin:StrongP@ssw0rd2026" | sudo chpasswd
sudo usermod -aG sudo ops-admin # Ubuntu
# sudo usermod -aG wheel ops-admin # Rocky Linux
# 3.2 配置 SSH 密钥(在本地机器执行)
ssh-copy-id -i ~/.ssh/id_ed25519.pub ops-admin@server-ip
# 3.3 测试新用户能否登录(在终端 2 用新用户测试)
ssh ops-admin@server-ip
# 3.4 确认新用户登录正常后,再修改 sshd_config
sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak.$(date +%Y%m%d%H%M%S)
# 3.5 修改配置
sudo tee -a /etc/ssh/sshd_config.d/hardening.conf << 'EOF'
Port 2222
PermitRootLogin no
PasswordAuthentication no
MaxAuthTries 3
ClientAliveInterval 300
ClientAliveCountMax 2
EOF
# 3.6 验证语法
sudo sshd -t
# 3.7 放行新端口
sudo firewall-cmd --permanent --add-port=2222/tcp
sudo firewall-cmd --reload
# 3.8 重启 sshd
sudo systemctl restart sshd
# 3.9 用终端 2 测试新端口
ssh -p 2222 ops-admin@server-ip
# 3.10 确认无误后,关闭旧端口
sudo firewall-cmd --permanent --remove-service=ssh
sudo firewall-cmd --reload
预防措施
-
SSH 配置修改永远保持第二个连接作为后路 -
修改完用 sshd -t验证语法后再 restart -
端口修改和防火墙规则必须同步操作 -
云服务器务必确认 VNC / 带外管理可用 -
用 /etc/ssh/sshd_config.d/目录下的独立文件管理配置,不直接改主配置文件
坑 6:时区和 NTP 没配导致日志时间混乱
场景描述
排查一个跨多台服务器的问题,发现各台服务器日志时间对不上,没法对齐事件时间线。
踩坑过程
线上出了一个偶发的请求超时问题,涉及 3 台服务器:Web 服务器、应用服务器、数据库服务器。翻日志准备对齐时间线的时候发现:
-
Web 服务器日志时间:UTC+8(北京时间) -
应用服务器日志时间:UTC(格林威治时间) -
数据库服务器日志时间:UTC-5(东部时间,谁装的系统选了这个时区)
同一个请求,Web 日志显示 14:30:05,应用日志显示 06:30:05,数据库日志显示 01:30:05。排查效率直接降到原来的三分之一。
更要命的是,数据库服务器的时间还快了 3 分钟,因为没配 NTP,系统时钟漂移了。
后果
-
跨服务故障排查效率大幅降低 -
日志聚合系统(ELK/Loki)的时间线完全混乱 -
定时任务执行时间不准确 -
分布式系统中可能导致证书校验失败(时间偏差超过容许范围) -
TLS 握手可能失败,因为证书有效期校验依赖系统时间
正确做法
# 1. 统一时区设置
sudo timedatectl set-timezone Asia/Shanghai
timedatectl
# 确认输出中 Time zone 是 Asia/Shanghai
# 2. 安装和配置 chronyd(替代 ntpd,更现代更准确)
# Ubuntu
sudo apt install -y chrony
# Rocky Linux(通常已预装)
sudo dnf install -y chrony
# 3. 配置 NTP 源
sudo tee /etc/chrony.conf << 'EOF'
# 国内 NTP 源
server ntp.aliyun.com iburst
server ntp.tencent.com iburst
server cn.ntp.org.cn iburst
server ntp.ntsc.ac.cn iburst
# 允许系统时钟步进调整(首次同步时如果偏差大于 1 秒)
makestep 1.0 3
# 记录时钟漂移率
driftfile /var/lib/chrony/drift
# 开启 RTC 同步
rtcsync
# 日志
logdir /var/log/chrony
EOF
# 4. 启动 chronyd
sudo systemctl enable --now chronyd
# 5. 验证同步状态
chronyc tracking
# 关注 System time 偏差,正常应该在毫秒级
chronyc sources -v
# 查看 NTP 源状态,*号标记的是当前使用的源
# 6. 强制立即同步(偏差较大时)
sudo chronyc makestep
预防措施
-
系统初始化模板必须包含时区设置和 NTP 配置 -
所有服务器统一使用 Asia/Shanghai(或统一 UTC,团队定一个标准就行) -
监控 NTP 同步状态,偏差超过 100ms 告警 -
使用 Ansible playbook 统一推送时间配置
# ansible playbook 片段
-name:Settimezone
timezone:
name:Asia/Shanghai
-name:Installandconfigurechrony
apt:
name:chrony
state:present
-name:Startchrony
systemd:
name:chronyd
state:started
enabled:yes
坑 7:服务重启顺序搞错导致依赖服务异常
场景描述
内核升级后需要重启服务器,或者需要批量重启一组互相依赖的服务。
踩坑过程
服务器上跑着 MySQL、Redis、应用服务 (Java)、Nginx。新人小陈做例行维护,需要重启所有服务。他用最简单粗暴的方式:
sudo systemctl restart nginx
sudo systemctl restart app-service
sudo systemctl restart redis
sudo systemctl restart mysql
结果 app-service 重启后立刻报错:
ERROR: Cannot connect to MySQL at 127.0.0.1:3306 - Connection refused
ERROR: Cannot connect to Redis at 127.0.0.1:6379 - Connection refused
因为 MySQL 和 Redis 还在重启过程中(MySQL 的 InnoDB 恢复需要时间),应用服务已经启动了,连不上依赖服务直接崩了。虽然应用有重连机制,但启动阶段的健康检查失败导致负载均衡把这台机器踢了。
后果
-
应用服务启动失败,健康检查不通过 -
负载均衡摘除节点,流量分配到其他节点造成压力 -
如果所有节点都这么重启,整个服务就挂了 -
恢复时间取决于 MySQL 的 crash recovery 速度
正确做法
理清依赖关系,按顺序重启
# 正确的重启顺序:依赖链从底向上
# 1. 先启动基础设施(数据库、缓存)
sudo systemctl restart mysql
# 等待 MySQL 完全就绪
until mysqladmin ping -h 127.0.0.1 --silent; do
echo"Waiting for MySQL..."
sleep 2
done
sudo systemctl restart redis
# 等待 Redis 就绪
until redis-cli ping | grep -q PONG; do
echo"Waiting for Redis..."
sleep 1
done
# 2. 再启动应用服务
sudo systemctl restart app-service
# 等待应用健康检查通过
until curl -sf http://127.0.0.1:8080/health > /dev/null; do
echo"Waiting for app..."
sleep 2
done
# 3. 最后启动反向代理
sudo systemctl restart nginx
用 systemd 配置依赖关系
# /etc/systemd/system/app-service.service
[Unit]
Description=Application Service
After=network.target mysql.service redis.service
Requires=mysql.service redis.service
Wants=redis.service
[Service]
Type=simple
User=app
ExecStartPre=/usr/local/bin/wait-for-deps.sh
ExecStart=/opt/app/bin/start.sh
Restart=on-failure
RestartSec=10
[Install]
WantedBy=multi-user.target
# /usr/local/bin/wait-for-deps.sh
#!/bin/bash
# 等待 MySQL 就绪
for i in $(seq 1 30); do
mysqladmin ping -h 127.0.0.1 --silent 2>/dev/null && break
echo"Waiting for MySQL... ($i/30)"
sleep 2
done
# 等待 Redis 就绪
for i in $(seq 1 30); do
redis-cli ping 2>/dev/null | grep -q PONG && break
echo"Waiting for Redis... ($i/30)"
sleep 1
done
# 重载 systemd 配置
sudo systemctl daemon-reload
预防措施
-
画出服务依赖图,贴在运维 wiki 上 -
用 systemd 的 After=、Requires=、Wants=声明依赖关系 -
服务启动脚本中加入依赖检查逻辑 -
批量重启用编排脚本,不要手动逐个执行 -
实测过 systemd-analyze dot可以生成服务依赖关系图:
systemd-analyze dot --to-pattern='*.service' | dot -Tpng -o deps.png
坑 8:日志不做轮转撑爆磁盘
场景描述
应用部署上线后,日志文件一直在增长,没有配置任何轮转策略。
踩坑过程
部署了一个 Java 应用,日志输出到 /opt/app/logs/app.log。上线初期一切正常,每天日志量大约 500MB。三个月后收到磁盘告警,一看 app.log 已经长到了 45GB。
ls -lh /opt/app/logs/app.log
# -rw-r--r-- 1 app app 45G Mar 10 14:22 /opt/app/logs/app.log
用 tail 查看这个文件极慢,grep 搜索关键字要等好几分钟。文件大到 vim 都打不开(vim 会尝试把整个文件加载到内存)。
更糟糕的是,因为 Java 应用的日志框架(Log4j2/Logback)的文件 appender 持有文件句柄,直接 rm 这个文件不会释放空间。
后果
-
磁盘空间被占满(同坑 4) -
日志文件过大无法有效搜索和分析 -
直接删除文件不释放空间(需要重启应用或通知 Java 重新打开文件句柄) -
如果日志框架没有配置大小限制,应用会一直写直到磁盘满
正确做法
方案一:配置 logrotate
# 创建应用的 logrotate 配置
sudo tee /etc/logrotate.d/app-service << 'EOF'
/opt/app/logs/*.log {
daily
rotate 30
compress
delaycompress
missingok
notifempty
create 640 app app
dateext
dateformat -%Y%m%d
sharedscripts
postrotate
# 通知应用重新打开日志文件
# 方式1:发送 USR1 信号(如果应用支持)
# kill -USR1 $(cat /opt/app/run/app.pid) 2>/dev/null || true
# 方式2:使用 copytruncate(适用于不支持信号的应用)
endscript
}
EOF
# 如果应用不支持信号通知,用 copytruncate
sudo tee /etc/logrotate.d/app-service << 'EOF'
/opt/app/logs/*.log {
daily
rotate 30
compress
delaycompress
missingok
notifempty
copytruncate
size 500M
dateext
dateformat -%Y%m%d
}
EOF
# 手动测试轮转(debug 模式不实际执行)
sudo logrotate -d /etc/logrotate.d/app-service
# 手动触发轮转
sudo logrotate -f /etc/logrotate.d/app-service
方案二:配置 journald 大小限制
# 编辑 journald 配置
sudo tee /etc/systemd/journald.conf.d/size-limit.conf << 'EOF'
[Journal]
SystemMaxUse=2G
SystemMaxFileSize=200M
MaxRetentionSec=30day
EOF
# 重启 journald
sudo systemctl restart systemd-journald
# 手动清理
sudo journalctl --vacuum-size=1G
sudo journalctl --vacuum-time=30d
方案三:应用层面配置日志轮转(以 Logback 为例)
<!-- logback-spring.xml -->
<configuration>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>/opt/app/logs/app.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>/opt/app/logs/app.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
<maxFileSize>200MB</maxFileSize>
<maxHistory>30</maxHistory>
<totalSizeCap>5GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
</configuration>
预防措施
-
任何应用上线前必须确认日志轮转配置 -
logrotate 配置纳入部署清单 -
应用日志框架本身也要配置大小限制和轮转 -
监控 /var/log和应用日志目录的磁盘占用
坑 9:权限设置过于宽松(chmod 777)
场景描述
应用启动报权限不足,新人图省事直接 chmod 777。
踩坑过程
新人小周部署一个 Python 应用,启动报错:
PermissionError: [Errno 13] Permission denied: '/opt/app/config/settings.yaml'
小周的解决方案:
chmod -R 777 /opt/app/
确实能跑了。但安全团队做审计扫描的时候,这台服务器全是红灯。更严重的是,/opt/app/config/settings.yaml 里面有数据库密码,777 权限意味着任何用户都能读到这些密码。
后来发现,这台服务器上还有一个低权限的日志采集 agent,因为 777 权限,这个 agent 可以读到并上报配置文件中的敏感信息。
后果
-
敏感信息泄露风险:数据库密码、API Key 等 -
安全审计不通过,需要整改 -
任何用户或进程都能修改应用文件,被篡改的风险极高 -
如果是 Web 目录给了 777,攻击者上传 webshell 后可以直接执行
正确做法
# 1. 搞清楚应用以哪个用户运行
ps aux | grep python
# 输出:app 12345 ... python /opt/app/main.py
# 应用以 app 用户运行
# 2. 设置正确的所有者
sudo chown -R app:app /opt/app/
# 3. 设置合理的权限
# 目录:所有者可读写执行,组可读执行,其他无权限
sudo find /opt/app -type d -exec chmod 750 {} \;
# 普通文件:所有者可读写,组可读,其他无权限
sudo find /opt/app -type f -exec chmod 640 {} \;
# 可执行文件:加上执行权限
sudo chmod 750 /opt/app/bin/*.sh
# 敏感配置文件:只有所有者可读
sudo chmod 600 /opt/app/config/settings.yaml
sudo chmod 600 /opt/app/config/credentials.json
# 4. 设置默认 umask(限制新创建文件的默认权限)
# 在应用的 systemd service 文件中
# [Service]
# UMask=0027
Linux 权限速查
权限值 含义 典型用途
600 rw------- 密钥文件、密码文件
640 rw-r----- 配置文件
644 rw-r--r-- 公开配置、静态文件
700 rwx------ 私有脚本目录
750 rwxr-x--- 应用目录
755 rwxr-xr-x 公共程序目录
使用 ACL 做精细控制
# 安装 ACL 支持
sudo apt install -y acl
# 给日志采集用户只读特定目录的权限
sudo setfacl -R -m u:log-agent:rx /opt/app/logs/
sudo setfacl -R -d -m u:log-agent:rx /opt/app/logs/
# 查看 ACL
getfacl /opt/app/logs/
预防措施
-
团队规范:禁止使用 chmod 777,在 code review 和配置审计中检查 -
配置 umask 为 0027 或 0077 -
敏感文件(密钥、密码)权限必须是 600 -
定期运行权限审计脚本,找出权限过于宽松的文件
# 查找 777 权限的文件
find /opt /var /home -perm 777 -type f 2>/dev/null
坑 10:不看日志凭感觉排错
场景描述
服务起不来或者出错了,新人不看日志直接开始”玄学排错”。
踩坑过程
小刘遇到应用服务起不来:
sudo systemctl start app-service
# Job for app-service.service failed
小刘的排错流程:
-
重启三遍——不行 -
换个端口——还是不行 -
检查 Java 版本——没问题 -
重新部署——还是不行 -
开始怀疑服务器有问题,提了工单要求换台机器
整整折腾了 2 小时。
最后老同事过来,一条命令搞定:
journalctl -u app-service --since "10 min ago" --no-pager
输出:
app-service[12345]: Error: Cannot bind to port 8080: Address already in use
端口被占了。另一个进程已经在用 8080。
ss -tlnp | grep 8080
# LISTEN 0 128 *:8080 *:* users:(("old-app",pid=9876,fd=6))
杀掉旧进程,服务正常启动。整个修复过程不到 2 分钟。
后果
-
浪费大量时间在无效操作上 -
反复重启可能加剧问题(比如数据库反复异常关闭导致数据损坏) -
对问题的认知是错的,可能引入新问题 -
团队对新人的专业能力产生怀疑
正确做法
标准排错流程
# 第一步:看服务状态
sudo systemctl status app-service
# 关注 Active 状态和最后几行日志
# 第二步:看详细日志
journalctl -u app-service --since "30 min ago" --no-pager -l
# 加 -l 不截断长行
# 第三步:看应用自己的日志
tail -100 /opt/app/logs/app.log
# 或者实时跟踪
tail -f /opt/app/logs/app.log
# 第四步:看系统日志(OOM、磁盘满等系统级问题)
dmesg -T | tail -50
journalctl --since "30 min ago" | grep -i -E "error|fail|kill|oom"
# 第五步:检查资源使用
df -h # 磁盘
free -m # 内存
ss -tlnp # 端口监听
ps aux --sort=-rss | head # 内存占用 top 10
快速定位问题类型的 checklist
# 端口冲突
ss -tlnp | grep <PORT>
# 权限问题
ls -la <文件路径>
namei -l <文件路径> # 检查完整路径每一级的权限
# 配置文件语法错误
nginx -t
httpd -t
named-checkconf
sshd -t
# 依赖缺失
ldd <二进制文件> # 检查动态库依赖
python -c "import <module>"# Python 模块
# 磁盘空间
df -h
df -i # inode 使用率
预防措施
-
培训新人的第一件事:出问题先看日志 -
在团队内部建立标准排错流程文档 -
定期组织故障排查演练,训练看日志的习惯 -
把 journalctl -u xxx -f这个命令刻进脑子里
三、示例代码和配置
3.1 安全删除脚本(基于 trash-cli 封装)
#!/bin/bash
# safe-delete.sh - 安全删除脚本
# 功能:替代 rm,将文件移到回收站而非直接删除
# 用法:safe-delete.sh <file_or_directory> [file2] [file3] ...
set -euo pipefail
# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
# 日志函数
log_info() { echo -e "${GREEN}[INFO]${NC} $*"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
log_error() { echo -e "${RED}[ERROR]${NC} $*"; }
# 检查 trash-cli 是否安装
if ! command -v trash-put &> /dev/null; then
log_error "trash-cli 未安装"
log_info "安装命令:"
log_info " Ubuntu/Debian: sudo apt install -y trash-cli"
log_info " Rocky Linux: sudo dnf install -y trash-cli"
exit 1
fi
# 参数检查
if [ $# -eq 0 ]; then
echo"用法: $0 <file_or_directory> [file2] [file3] ..."
echo"选项:"
echo" -l, --list 查看回收站内容"
echo" -r, --restore 恢复最近删除的文件"
echo" -e, --empty 清空回收站"
exit 1
fi
# 处理选项
case"${1:-}"in
-l|--list)
trash-list
exit 0
;;
-r|--restore)
trash-restore
exit 0
;;
-e|--empty)
read -p "确认清空回收站?(y/N) " confirm
if [ "$confirm" = "y" ] || [ "$confirm" = "Y" ]; then
trash-empty
log_info "回收站已清空"
else
log_info "已取消"
fi
exit 0
;;
esac
# 保护列表——这些路径绝对不能删
PROTECTED_PATHS=(
"/""/bin""/boot""/dev""/etc""/home""/lib""/lib64"
"/proc""/root""/sbin""/sys""/usr""/var""/opt""/srv"
)
# 执行安全删除
for target in"$@"; do
# 获取绝对路径
abs_path=$(realpath "$target" 2>/dev/null || echo"$target")
# 检查是否在保护列表中
for protected in"${PROTECTED_PATHS[@]}"; do
if [ "$abs_path" = "$protected" ]; then
log_error "拒绝删除受保护路径: $abs_path"
continue 2
fi
done
# 检查文件是否存在
if [ ! -e "$target" ]; then
log_warn "文件不存在: $target"
continue
fi
# 大文件警告(超过 1GB)
if [ -f "$target" ]; then
size=$(stat -c%s "$target" 2>/dev/null || echo 0)
if [ "$size" -gt 1073741824 ]; then
size_human=$(numfmt --to=iec "$size")
log_warn "大文件警告: $target ($size_human)"
read -p "确认移入回收站?(y/N) " confirm
if [ "$confirm" != "y" ] && [ "$confirm" != "Y" ]; then
log_info "已跳过: $target"
continue
fi
fi
fi
# 执行 trash-put
if trash-put "$target"; then
log_info "已移入回收站: $target"
else
log_error "删除失败: $target"
fi
done
3.2 配置备份自动化脚本
#!/bin/bash
# config-backup.sh - 配置文件修改前自动备份
# 用法:config-backup.sh <config_file> [edit]
# 加 edit 参数会备份后自动打开编辑器
set -euo pipefail
BACKUP_DIR="/var/backups/config-history"
LOG_FILE="/var/log/config-backup.log"
# 确保备份目录存在
mkdir -p "$BACKUP_DIR"
if [ $# -lt 1 ]; then
echo"用法: $0 <config_file> [edit]"
echo"示例: $0 /etc/nginx/nginx.conf edit"
exit 1
fi
CONFIG_FILE="$1"
ACTION="${2:-backup}"
if [ ! -f "$CONFIG_FILE" ]; then
echo"错误: 文件不存在 $CONFIG_FILE"
exit 1
fi
# 生成备份文件名
TIMESTAMP=$(date +%Y%m%d%H%M%S)
BASENAME=$(basename "$CONFIG_FILE")
DIRNAME=$(dirname "$CONFIG_FILE" | tr '/''_')
BACKUP_FILE="${BACKUP_DIR}/${DIRNAME}_${BASENAME}.${TIMESTAMP}"
# 执行备份
cp -p "$CONFIG_FILE""$BACKUP_FILE"
echo"[$(date '+%Y-%m-%d %H:%M:%S')] Backed up $CONFIG_FILE -> $BACKUP_FILE" >> "$LOG_FILE"
echo"备份完成: $BACKUP_FILE"
# 计算校验和
md5sum "$CONFIG_FILE" > "${BACKUP_FILE}.md5"
# 如果指定了 edit,打开编辑器
if [ "$ACTION" = "edit" ]; then
${EDITOR:-vim}"$CONFIG_FILE"
# 编辑后显示差异
echo""
echo"=== 变更内容 ==="
diff "$BACKUP_FILE""$CONFIG_FILE" || true
echo""
# 如果是 nginx/sshd 等,自动验证语法
case"$BASENAME"in
nginx.conf|*.nginx)
echo"=== Nginx 语法检查 ==="
nginx -t 2>&1
;;
sshd_config)
echo"=== SSH 语法检查 ==="
sshd -t 2>&1
;;
esac
fi
# 保留最近 50 个备份,清理旧的
BACKUP_COUNT=$(ls -1 "${BACKUP_DIR}/${DIRNAME}_${BASENAME}."* 2>/dev/null | grep -v '.md5$' | wc -l)
if [ "$BACKUP_COUNT" -gt 50 ]; then
ls -1t "${BACKUP_DIR}/${DIRNAME}_${BASENAME}."* | grep -v '.md5$' | tail -n +51 | whileread old; do
rm -f "$old""${old}.md5"
done
echo"已清理旧备份,保留最近 50 个版本"
fi
3.3 磁盘空间监控 + 自动清理脚本
#!/bin/bash
# disk-monitor.sh - 磁盘空间监控和自动清理
# 建议加入 crontab: */10 * * * * /usr/local/bin/disk-monitor.sh
set -euo pipefail
# 配置
WARN_THRESHOLD=80
CRIT_THRESHOLD=90
AUTO_CLEAN_THRESHOLD=95
LOG_FILE="/var/log/disk-monitor.log"
ALERT_EMAIL="ops-team@company.com"
HOSTNAME=$(hostname)
log() {
echo"[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"
}
send_alert() {
local level="$1"
local message="$2"
# 企业微信 webhook 告警
if [ -n "${WECHAT_WEBHOOK:-}" ]; then
curl -s -H "Content-Type: application/json" \
-d "{\"msgtype\":\"text\",\"text\":{\"content\":\"[${level}] ${HOSTNAME}: ${message}\"}}" \
"$WECHAT_WEBHOOK" > /dev/null 2>&1
fi
# 邮件告警
ifcommand -v mail &> /dev/null; then
echo"$message" | mail -s "[${level}] Disk Alert: ${HOSTNAME}""$ALERT_EMAIL"
fi
log"ALERT [${level}] ${message}"
}
auto_clean() {
local mount_point="$1"
log"开始自动清理 $mount_point"
# 清理 apt 缓存
if [ "$mount_point" = "/" ]; then
apt clean 2>/dev/null || dnf clean all 2>/dev/null || true
log"已清理包管理器缓存"
fi
# 清理超过 7 天的临时文件
find /tmp -type f -mtime +7 -delete 2>/dev/null || true
find /var/tmp -type f -mtime +7 -delete 2>/dev/null || true
log"已清理超过 7 天的临时文件"
# 压缩超过 3 天的日志
find /var/log -name "*.log" -mtime +3 -not -name "*.gz" -exec gzip {} \; 2>/dev/null || true
log"已压缩旧日志文件"
# 清理 journal 日志
journalctl --vacuum-size=1G 2>/dev/null || true
log"已清理 journal 日志"
# 清理已删除但未释放的文件描述符(需要重启对应进程)
deleted_space=$(lsof +L1 2>/dev/null | awk '{sum += $7} END {printf "%.0f", sum/1024/1024}')
if [ "${deleted_space:-0}" -gt 1024 ]; then
log"警告: 有 ${deleted_space}MB 的已删除文件未释放空间,需要重启相关进程"
fi
}
# 主检查逻辑
df -h --output=pcent,target,size,used,avail | tail -n +2 | whileread usage mount size used avail; do
usage_num=${usage%\%}
if [ "$usage_num" -ge "$AUTO_CLEAN_THRESHOLD" ]; then
send_alert "CRITICAL""$mount 使用率 ${usage}(${used}/${size}),触发自动清理"
auto_clean "$mount"
elif [ "$usage_num" -ge "$CRIT_THRESHOLD" ]; then
send_alert "CRITICAL""$mount 使用率 ${usage}(${used}/${size}),请立即处理"
elif [ "$usage_num" -ge "$WARN_THRESHOLD" ]; then
send_alert "WARNING""$mount 使用率 ${usage}(${used}/${size}),请关注"
fi
done
# 检查 inode 使用率
df -i --output=ipcent,target | tail -n +2 | whileread usage mount; do
usage_num=${usage%\%}
if [ "${usage_num:-0}" -ge 80 ]; then
send_alert "WARNING""$mount inode 使用率 ${usage}"
fi
done
3.4 系统初始化标准化脚本
#!/bin/bash
# server-init.sh - 服务器初始化标准化脚本
# 适用于 Ubuntu 22.04/24.04 和 Rocky Linux 9.x
# 用法:sudo bash server-init.sh
set -euo pipefail
echo"=============================="
echo" 服务器初始化脚本 v2.1"
echo" $(date '+%Y-%m-%d %H:%M:%S')"
echo"=============================="
# 检测发行版
if [ -f /etc/os-release ]; then
. /etc/os-release
DISTRO=$ID
VERSION=$VERSION_ID
else
echo"无法检测操作系统版本"
exit 1
fi
echo"[1/8] 检测到系统: $DISTRO $VERSION"
# ===== 1. 时区设置 =====
echo"[2/8] 配置时区..."
timedatectl set-timezone Asia/Shanghai
echo" 时区已设置为 Asia/Shanghai"
# ===== 2. NTP 时间同步 =====
echo"[3/8] 配置 NTP 时间同步..."
case"$DISTRO"in
ubuntu|debian)
apt install -y chrony > /dev/null 2>&1
;;
rocky|centos|rhel)
dnf install -y chrony > /dev/null 2>&1
;;
esac
cat > /etc/chrony.conf << 'CHRONY'
server ntp.aliyun.com iburst
server ntp.tencent.com iburst
server cn.ntp.org.cn iburst
makestep 1.0 3
driftfile /var/lib/chrony/drift
rtcsync
logdir /var/log/chrony
CHRONY
systemctl enable --now chronyd
chronyc makestep > /dev/null 2>&1 || true
echo" NTP 同步已配置"
# ===== 3. 日志轮转 =====
echo"[4/8] 配置日志轮转..."
# journald 大小限制
mkdir -p /etc/systemd/journald.conf.d/
cat > /etc/systemd/journald.conf.d/size-limit.conf << 'JOURNAL'
[Journal]
SystemMaxUse=2G
SystemMaxFileSize=200M
MaxRetentionSec=30day
JOURNAL
systemctl restart systemd-journald
# 通用应用日志轮转模板
cat > /etc/logrotate.d/app-logs << 'LOGROTATE'
/opt/*/logs/*.log /opt/*/log/*.log {
daily
rotate 30
compress
delaycompress
missingok
notifempty
copytruncate
size 200M
dateext
dateformat -%Y%m%d
}
LOGROTATE
echo" 日志轮转已配置"
# ===== 4. 安全基线 =====
echo"[5/8] 配置安全基线..."
# SSH 加固
cat > /etc/ssh/sshd_config.d/security.conf << 'SSHCONF'
MaxAuthTries 3
ClientAliveInterval 300
ClientAliveCountMax 2
PermitEmptyPasswords no
X11Forwarding no
SSHCONF
sshd -t && systemctl reload sshd
echo" SSH 安全配置完成"
# 配置 umask
echo"umask 0027" >> /etc/profile.d/umask.sh
# ===== 5. 基础工具安装 =====
echo"[6/8] 安装基础工具..."
case"$DISTRO"in
ubuntu|debian)
apt update -qq > /dev/null 2>&1
apt install -y -qq \
vim curl wget htop iotop sysstat \
net-tools lsof tree jq \
trash-cli safe-rm \
etckeeper unzip > /dev/null 2>&1
;;
rocky|centos|rhel)
dnf install -y -q \
vim curl wget htop iotop sysstat \
net-tools lsof tree jq \
trash-cli unzip > /dev/null 2>&1
;;
esac
echo" 基础工具安装完成"
# ===== 6. 内核参数优化 =====
echo"[7/8] 优化内核参数..."
cat > /etc/sysctl.d/99-server-tuning.conf << 'SYSCTL'
# 网络优化
net.core.somaxconn = 65535
net.core.netdev_max_backlog = 65535
net.ipv4.tcp_max_syn_backlog = 65535
net.ipv4.tcp_fin_timeout = 10
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_keepalive_time = 600
net.ipv4.tcp_keepalive_intvl = 30
net.ipv4.tcp_keepalive_probes = 3
# 文件描述符
fs.file-max = 1000000
fs.inotify.max_user_watches = 524288
# 内存
vm.swappiness = 10
vm.overcommit_memory = 0
SYSCTL
sysctl -p /etc/sysctl.d/99-server-tuning.conf > /dev/null 2>&1
echo" 内核参数优化完成"
# ===== 7. 系统资源限制 =====
cat > /etc/security/limits.d/99-server.conf << 'LIMITS'
* soft nofile 65535
* hard nofile 65535
* soft nproc 65535
* hard nproc 65535
LIMITS
# ===== 8. 验证 =====
echo"[8/8] 验证配置..."
echo""
echo"====== 系统信息 ======"
echo" 主机名: $(hostname)"
echo" 系统: $DISTRO $VERSION"
echo" 内核: $(uname -r)"
echo" 时区: $(timedatectl | grep 'Time zone' | awk '{print $3}')"
echo" NTP 同步: $(timedatectl | grep 'synchronized' | awk '{print $NF}')"
echo" Chrony: $(chronyc tracking 2>/dev/null | grep 'Leap status' | awk -F: '{print $2}' | xargs)"
echo""
echo"====== 磁盘使用 ======"
df -h --output=target,size,used,avail,pcent | head -10
echo""
echo"====== 内存使用 ======"
free -h
echo""
echo"初始化完成!建议重启服务器使所有配置生效。"
echo"重启命令: sudo reboot"
案例 1:rm -rf 误删 /usr/lib 的恢复过程
事故背景:测试环境一台 Ubuntu 22.04 服务器,运维实习生清理空间时执行了 rm -rf /usr/lib/python3/dist-packages/old-module,但补全出了问题,实际执行的是 rm -rf /usr/lib/python3/。
发现过程:
# 执行完 rm 后,系统命令开始报错
$ apt update
apt: error while loading shared libraries: libapt-pkg.so.6.0: cannot open shared object file
$ python3 --version
bash: /usr/bin/python3: No such file or directory
恢复步骤(这台服务器有 LVM 快照):
# 1. 因为 apt 已损坏,先从快照挂载恢复
# 在宿主机(如果是虚拟机)或通过 Live CD 启动
# 2. 挂载最近的 LVM 快照
lvcreate --snapshot --name recover-snap --size 10G /dev/vg0/root
# 3. 挂载快照
mkdir /mnt/recover
mount /dev/vg0/recover-snap /mnt/recover
# 4. 复制被删除的目录
cp -a /mnt/recover/usr/lib/python3/ /usr/lib/python3/
# 5. 卸载并删除快照
umount /mnt/recover
lvremove -f /dev/vg0/recover-snap
# 6. 验证恢复结果
python3 --version
apt update
如果没有快照:
# 只能重装受影响的包
# 先从另一台相同版本的机器拷贝 dpkg/apt 的库文件
scp backup-server:/usr/lib/x86_64-linux-gnu/libapt-pkg.so.6.0* /usr/lib/x86_64-linux-gnu/
# 恢复 apt 后重装
apt install --reinstall python3 python3-minimal libpython3-stdlib
教训:这次恢复花了 3 小时。如果装了 trash-cli 或 safe-rm,整个事故不会发生。
案例 2:防火墙规则导致数据库不可访问
事故背景:生产环境 MySQL 主从架构,安全团队要求收紧防火墙规则。运维工程师在主库上执行了防火墙变更,导致从库无法同步。
事故时间线:
14:00 安全团队提交防火墙变更工单
14:15 运维在 MySQL 主库执行 iptables 规则变更
14:16 从库复制延迟开始增长
14:25 监控告警:MySQL 从库复制延迟超过 60s
14:30 DBA 介入排查,发现从库连接主库超时
14:35 定位到防火墙规则问题
14:38 回滚防火墙规则,从库恢复同步
14:45 复制延迟追平,事故结束
根因分析:
# 运维执行的命令(错误的)
iptables -F INPUT # 清空了所有 INPUT 规则
iptables -P INPUT DROP # 设置默认策略为 DROP
iptables -A INPUT -p tcp --dport 22 -j ACCEPT # 只放行了 SSH
# 遗漏了 MySQL 3306 端口的放行规则
# 从库 IP 10.0.1.20 无法连接主库 3306 端口
正确的操作方式:
# 1. 变更前先列出现有规则并备份
iptables-save > /root/iptables-backup-$(date +%Y%m%d%H%M%S).rules
# 2. 梳理需要放行的端口清单
# SSH: 22
# MySQL: 3306(从库 IP: 10.0.1.20, 10.0.1.21)
# Node Exporter: 9100(监控服务器 IP: 10.0.2.10)
# 3. 写入新规则(不要 flush)
iptables -I INPUT 1 -m state --state ESTABLISHED,RELATED -j ACCEPT
iptables -I INPUT 2 -i lo -j ACCEPT
iptables -I INPUT 3 -p tcp --dport 22 -j ACCEPT
iptables -I INPUT 4 -s 10.0.1.20 -p tcp --dport 3306 -j ACCEPT
iptables -I INPUT 5 -s 10.0.1.21 -p tcp --dport 3306 -j ACCEPT
iptables -I INPUT 6 -s 10.0.2.10 -p tcp --dport 9100 -j ACCEPT
# 4. 在最后添加 DROP
iptables -A INPUT -j DROP
# 5. 从从库测试连通性
mysql -h 10.0.1.10 -u repl -p -e "SHOW SLAVE STATUS\G"
# 6. 确认无误后保存
iptables-save > /etc/iptables/rules.v4
四、最佳实践和注意事项
4.1 最佳实践
4.1.1 操作规范清单(变更前 Checklist)
每次生产环境操作前,过一遍这个清单:
变更前 Checklist
================
[ ] 是否有变更工单/审批记录?
[ ] 是否通知了相关方(业务方、DBA、安全团队)?
[ ] 是否准备了回滚方案?
[ ] 是否备份了要修改的配置文件?
[ ] 是否在测试环境验证过操作步骤?
[ ] 是否保持了备用的 SSH 连接?
[ ] 是否确认了操作时间窗口(业务低峰期)?
[ ] 是否准备好了验证命令?
[ ] 如果涉及重启,是否理清了服务依赖顺序?
[ ] 操作步骤是否已经写成文档?
4.1.2 变更管理流程
申请 → 审批 → 准备 → 实施 → 验证 → 确认
1. 申请:提交变更工单,说明变更内容、影响范围、操作时间
2. 审批:技术负责人审核变更方案和回滚方案
3. 准备:备份配置、准备脚本、在测试环境演练
4. 实施:按照操作手册逐步执行
5. 验证:执行预定义的验证步骤,确认变更效果
6. 确认:通知相关方变更完成,记录操作日志
4.1.3 文档习惯
-
每次操作记录到运维 wiki(时间、操作人、内容、结果) -
重复性操作整理成 SOP(Standard Operating Procedure) -
踩过的坑记录到团队知识库,避免重复踩坑 -
操作手册定期 review,删除过时内容
4.2 注意事项
4.2.1 常见错误汇总表
|
|
|
|
|---|---|---|
rm: cannot remove '/': Permission denied |
|
|
nginx: [emerg] bind() to 0.0.0.0:80 failed |
|
ss -tlnp | grep :80
|
ssh: connect to host xxx port 22: Connection refused |
|
|
No space left on device |
|
df -h
df -i 分别检查 |
Permission denied (publickey) |
|
~/.ssh/authorized_keys 权限(600) |
systemctl: command not found |
|
/usr/bin/systemctl |
Job for xxx.service failed |
|
journalctl -u xxx -n 50
|
Name or service not known |
|
/etc/resolv.conf 和网络连接 |
Too many open files |
|
/etc/security/limits.conf |
Connection timed out |
|
telnet host port
traceroute 排查 |
OOM killed process |
|
dmesg | grep oom
|
Read-only file system |
|
fsck
mount -o remount,rw / |
4.2.2 “三板斧”思维的危害
很多新人遇到问题的反应是:
-
重启服务 -
重启服务器 -
重装系统
这种”三板斧”思维的问题:
-
重启可能破坏故障现场:内存中的数据、进程状态都丢了,没法事后分析 -
重启不解决根因:问题还会再来,可能在更要命的时候 -
重装是最懒的方案:配置全丢了,之前所有的调优、加固都白做了 -
养成了不排查的习惯:技术能力永远得不到成长
正确的思路:先定位问题 → 理解原因 → 针对性修复 → 预防复发
五、故障排查和监控
5.1 新人必备排查工具链
|
|
|
|
|
|---|---|---|---|
|
|
|
top -c
htop |
|
|
|
|
sudo iotop -o |
|
|
|
|
ss -tlnp
ss -s |
|
|
|
|
lsof -i :8080
lsof -u app |
|
|
|
|
strace -p PID -e trace=open |
|
|
|
|
dmesg -T | tail -50 |
|
|
|
|
ps auxf |
|
|
|
|
free -h |
|
|
|
|
df -h
du -sh /var/* |
|
|
|
|
ping -c 5 host
mtr host |
|
5.2 系统健康检查脚本
#!/bin/bash
# health-check.sh - 一键输出系统状态报告
# 用法:sudo bash health-check.sh
echo"============================================"
echo" 系统健康检查报告"
echo" 主机: $(hostname)"
echo" 时间: $(date '+%Y-%m-%d %H:%M:%S %Z')"
echo" 系统: $(cat /etc/os-release | grep PRETTY_NAME | cut -d'"' -f2)"
echo" 内核: $(uname -r)"
echo" 运行时间: $(uptime -p)"
echo"============================================"
echo""
echo">>> CPU 信息"
echo" 核心数: $(nproc)"
echo" 负载: $(cat /proc/loadavg | awk '{print $1, $2, $3}')"
echo" CPU 使用率 Top 5:"
ps aux --sort=-%cpu | head -6 | awk 'NR>1 {printf " PID %-8s CPU %-6s MEM %-6s CMD %s\n", $2, $3, $4, $11}'
echo""
echo">>> 内存信息"
free -h | awk '
NR==1 {printf " %-15s %-10s %-10s %-10s %-10s\n", "", $1, $2, $3, $6}
NR==2 {printf " %-15s %-10s %-10s %-10s %-10s\n", "Memory:", $2, $3, $4, $7}
NR==3 {printf " %-15s %-10s %-10s %-10s\n", "Swap:", $2, $3, $4}
'
echo""
echo">>> 磁盘使用"
df -h --output=target,size,used,avail,pcent -x tmpfs -x devtmpfs | head -20
echo""
echo">>> 磁盘 inode"
df -i --output=target,itotal,iused,iavail,ipcent -x tmpfs -x devtmpfs | head -20
echo""
echo">>> 网络连接统计"
ss -s 2>/dev/null | head -5
echo""
echo">>> 监听端口"
ss -tlnp 2>/dev/null | awk 'NR>1 {printf " %-25s %s\n", $4, $NF}'
echo""
echo">>> 最近失败的服务"
systemctl list-units --state=failed --no-pager --no-legend 2>/dev/null | whileread line; do
echo" [FAILED] $line"
done
if [ -z "$(systemctl list-units --state=failed --no-pager --no-legend 2>/dev/null)" ]; then
echo" 所有服务正常运行"
fi
echo""
echo">>> NTP 同步状态"
ifcommand -v chronyc &> /dev/null; then
echo" $(chronyc tracking 2>/dev/null | grep 'Leap status')"
echo" $(chronyc tracking 2>/dev/null | grep 'System time')"
else
echo" $(timedatectl | grep 'synchronized')"
fi
echo""
echo">>> 最近的内核错误/警告"
dmesg -T --level=err,warn 2>/dev/null | tail -5 | whileread line; do
echo" $line"
done
if [ -z "$(dmesg -T --level=err,warn 2>/dev/null | tail -5)" ]; then
echo" 无内核错误"
fi
echo""
echo">>> 最近登录记录"
last -5 -w 2>/dev/null | head -5
echo""
echo">>> 安全检查"
# 检查 777 权限文件
count_777=$(find /opt /var/www /home -perm 777 -type f 2>/dev/null | wc -l)
echo" 777 权限文件数: $count_777"
# 检查空密码用户
empty_pass=$(awk -F: '($2 == "" || $2 == "!") {print $1}' /etc/shadow 2>/dev/null | wc -l)
echo" 空密码用户数: $empty_pass"
# 检查 root 直接登录
root_login=$(grep "^PermitRootLogin" /etc/ssh/sshd_config 2>/dev/null | awk '{print $2}')
echo" Root SSH 登录: ${root_login:-未设置(默认允许)}"
echo""
echo"============================================"
echo" 检查完成"
echo"============================================"
5.3 监控入门配置(Prometheus + node_exporter + Grafana 最小化部署)
安装 node_exporter
# 下载 node_exporter 1.9.x
NODE_EXPORTER_VERSION="1.9.0"
cd /tmp
wget https://github.com/prometheus/node_exporter/releases/download/v${NODE_EXPORTER_VERSION}/node_exporter-${NODE_EXPORTER_VERSION}.linux-amd64.tar.gz
tar xzf node_exporter-${NODE_EXPORTER_VERSION}.linux-amd64.tar.gz
sudo cp node_exporter-${NODE_EXPORTER_VERSION}.linux-amd64/node_exporter /usr/local/bin/
# 创建 systemd service
sudo tee /etc/systemd/system/node_exporter.service << 'EOF'
[Unit]
Description=Prometheus Node Exporter
After=network.target
[Service]
Type=simple
User=nobody
ExecStart=/usr/local/bin/node_exporter \
--collector.systemd \
--collector.processes \
--collector.tcpstat
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now node_exporter
# 验证
curl -s http://localhost:9100/metrics | head -5
Prometheus 配置(监控服务器上)
# /etc/prometheus/prometheus.yml
global:
scrape_interval:15s
evaluation_interval:15s
rule_files:
-"rules/*.yml"
alerting:
alertmanagers:
-static_configs:
-targets:
-"localhost:9093"
scrape_configs:
-job_name:"node"
static_configs:
-targets:
-"10.0.1.10:9100"
-"10.0.1.11:9100"
-"10.0.1.12:9100"
labels:
env:"production"
告警规则
# /etc/prometheus/rules/node-alerts.yml
groups:
-name:node-alerts
rules:
# 磁盘使用率超过 85%
-alert:DiskSpaceHigh
expr:(1-node_filesystem_avail_bytes/node_filesystem_size_bytes)*100>85
for:5m
labels:
severity:warning
annotations:
summary:"磁盘使用率过高 ({{ $labels.instance }})"
description:"分区 {{ $labels.mountpoint }} 使用率 {{ $value | printf \"%.1f\" }}%"
# 磁盘使用率超过 95%
-alert:DiskSpaceCritical
expr:(1-node_filesystem_avail_bytes/node_filesystem_size_bytes)*100>95
for:1m
labels:
severity:critical
annotations:
summary:"磁盘使用率严重过高 ({{ $labels.instance }})"
description:"分区 {{ $labels.mountpoint }} 使用率 {{ $value | printf \"%.1f\" }}%"
# 内存使用率超过 90%
-alert:MemoryHigh
expr:(1-node_memory_MemAvailable_bytes/node_memory_MemTotal_bytes)*100>90
for:5m
labels:
severity:warning
annotations:
summary:"内存使用率过高 ({{ $labels.instance }})"
description:"内存使用率 {{ $value | printf \"%.1f\" }}%"
# CPU 使用率超过 90%(持续 10 分钟)
-alert:CPUHigh
expr:100-(avgby(instance)(rate(node_cpu_seconds_total{mode="idle"}[5m]))*100)>90
for:10m
labels:
severity:warning
annotations:
summary:"CPU 使用率过高 ({{ $labels.instance }})"
description:"CPU 使用率 {{ $value | printf \"%.1f\" }}%"
# 服务器宕机
-alert:InstanceDown
expr:up==0
for:1m
labels:
severity:critical
annotations:
summary:"服务器不可达 ({{ $labels.instance }})"
description:"{{ $labels.instance }} 已超过 1 分钟无响应"
Grafana 快速导入 Dashboard
# 安装 Grafana 11.x
sudo apt install -y apt-transport-https software-properties-common
wget -q -O - https://apt.grafana.com/gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/grafana.gpg
echo "deb [signed-by=/etc/apt/keyrings/grafana.gpg] https://apt.grafana.com stable main" | sudo tee /etc/apt/sources.list.d/grafana.list
sudo apt update && sudo apt install -y grafana
sudo systemctl enable --now grafana-server
# 导入 Node Exporter Full Dashboard(ID: 1860)
# 浏览器打开 http://server-ip:3000
# 默认账号:admin / admin
# 添加 Prometheus 数据源 → Import Dashboard → 输入 1860
六、总结
6.1 技术要点回顾
-
删除操作:用 trash-cli替代rm,配置safe-rm保护关键目录 -
配置管理:每次修改前备份,用 etckeeper做版本化,修改后验证语法 -
防火墙:理解规则顺序,优先用 firewalld或nftables,变更后从外部验证 -
磁盘监控:配置告警阈值,日志做轮转,定期清理 -
SSH 安全:改配置前保持备用连接,按正确顺序加固,验证语法再重启 -
时间同步:统一时区,配置 chrony,监控同步状态 -
服务依赖:理清依赖关系,用 systemd 声明依赖,按顺序重启 -
日志轮转:上线前必须配置 logrotate,应用层面也要配置 -
权限管理:遵守最小权限原则,禁止 777,敏感文件 600 -
日志排查:出问题第一反应是看日志,不是重启
6.2 进阶学习方向
-
配置管理自动化:学习 Ansible/Terraform,实现基础设施即代码 -
学习资源:Ansible 官方文档 https://docs.ansible.com -
实践建议:从管理 5 台服务器开始,用 playbook 替代手动操作
-
-
监控体系建设:深入 Prometheus + Grafana + Alertmanager 全链路 -
学习资源:Prometheus 官方文档 https://prometheus.io/docs -
实践建议:先把 node_exporter 部署到所有服务器,再逐步加入业务监控
-
-
容器化运维:Docker + Kubernetes 的运维管理 -
学习资源:Kubernetes 官方教程 https://kubernetes.io/docs/tutorials -
实践建议:先在测试环境部署一套 K3s,练习基本操作
-
6.3 参考资料
-
Linux man pages – 命令参考的权威来源 -
ArchWiki – 虽然是 Arch 的 wiki,但通用知识非常全面 -
Red Hat System Administration Guide – 企业级 Linux 管理参考 -
Prometheus Alerting Rules – 告警规则编写指南
附录
A. 运维新人入职 Checklist
入职第一周
==========
[ ] 获取服务器 SSH 访问权限
[ ] 配置个人 SSH 密钥对
[ ] 阅读团队操作规范文档
[ ] 了解服务器列表和角色分工
[ ] 了解监控告警系统(Prometheus/Grafana 地址)
[ ] 了解工单系统和变更管理流程
[ ] 安装本地工具(终端、SSH 客户端、VPN)
[ ] 阅读本文档的 10 个坑位
入职第二周
==========
[ ] 在测试环境练习基本操作(服务管理、日志查看、磁盘管理)
[ ] 跟着老同事做一次生产变更(观摩)
[ ] 独立完成一次测试环境部署
[ ] 学会使用团队的 CI/CD 工具
[ ] 了解备份策略和恢复流程
入职第一个月
============
[ ] 独立完成至少一次生产变更
[ ] 参与一次故障排查
[ ] 整理一份常用命令速查表
[ ] 写一篇运维操作记录(wiki)
[ ] 参加一次团队内部分享
B. Linux 命令速查表(按场景分类)
# === 系统信息 ===
uname -r # 内核版本
cat /etc/os-release # 发行版信息
hostnamectl # 主机名和系统信息
uptime # 运行时间和负载
w # 谁在登录、在做什么
# === 进程管理 ===
ps auxf # 进程树
ps aux --sort=-%mem | head # 内存占用排序
top -bn1 | head -20 # 非交互式 top
htop # 交互式进程管理
kill -15 PID # 优雅终止进程
kill -9 PID # 强制杀进程(最后手段)
pgrep -a nginx # 按名称搜索进程
# === 磁盘管理 ===
df -h # 磁盘使用率
df -i # inode 使用率
du -sh /var/* | sort -rh # 目录大小排序
lsblk # 块设备列表
findmnt # 挂载点树
# === 网络诊断 ===
ss -tlnp # TCP 监听端口
ss -tnp state established # 已建立的连接
ip addr show # IP 地址
ip route show # 路由表
ping -c 5 8.8.8.8 # 连通性测试
traceroute target # 路由追踪
dig domain.com # DNS 查询
curl -I http://host:port # HTTP 探测
# === 服务管理 ===
systemctl status nginx # 服务状态
systemctl list-units --failed # 失败的服务
systemctl list-timers # 定时器列表
journalctl -u nginx -f # 实时服务日志
journalctl --since "1h ago" # 最近 1 小时的日志
# === 用户管理 ===
id username # 用户信息
who # 当前登录用户
last -10 # 最近登录记录
passwd -S username # 密码状态
# === 文件操作 ===
find /var -name "*.log" -size +100M # 找大文件
find /tmp -mtime +7 -type f # 7 天前的文件
stat filename # 文件详细信息
file filename # 文件类型检测
C. 术语表
|
|
|
|
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
版权申明:内容来源网络,版权归原创者所有,如有侵权请联系删除
想了解更多干货,可通过下方扫码关注

可扫码添加上智启元官方客服微信👇

17认证网








