运维新人最容易踩的 10 个坑,我替你整理好了17认证网

正规官方授权
更专业・更权威

运维新人最容易踩的 10 个坑,我替你整理好了

转自:马哥Linux运维

一、概述

1.1 背景介绍

带过好几个运维新人,从实习生到转岗的开发,这些坑每个人都会踩一遍。有人把生产环境的 /var 目录删了,有人把自己锁在 SSH 外面,有人改完 nginx 配置连备份都没留。同样的事故报告写了好多份,同样的善后流程走了无数遍。

与其每次事后复盘,不如提前把路铺好。这篇文章把最高频的 10 个坑位全部列出来,每个坑都附带真实案例、完整的修复命令、从根本上的预防方案。不是教科书式的理论,全是实操经验——有些是我自己踩过的,有些是团队成员踩完我帮着擦屁股的。

1.2 技术特点

  • 10 个高频坑位:从 rm -rf 误删到不看日志凭感觉排错,覆盖新人最常犯的错误
  • 每个坑都有真实案例:不是编的场景,是线上真实发生过的事故
  • 不是理论而是实操经验:所有命令都在生产环境验证过,所有脚本都在跑
  • 统一结构:场景 → 踩坑过程 → 后果 → 正确做法 → 预防措施,方便速查

1.3 适用场景

  • 初级运维:刚入行的运维工程师,对 Linux 有基本了解但缺少生产经验
  • 转岗运维:从开发或测试转运维,有技术基础但不了解运维规范
  • 运维培训:团队内部培训教材,新人入职必读清单
  • 实习生入职指南:实习生第一周就应该看完的文档

1.4 环境要求

组件
版本要求
说明
操作系统
Ubuntu 22.04 LTS / 24.04 LTS / Rocky Linux 9.x
文中命令以这三个发行版为准
systemd
255+
服务管理和日志查看
Bash
5.x
脚本执行环境
Python
3.10+
部分工具脚本依赖
Prometheus
3.x
监控告警相关章节
Grafana
11.x
可视化监控

二、详细步骤

坑 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

预防措施

  1. 所有服务器统一安装 trash-cli,在系统初始化脚本中配置
  2. 生产环境禁止直接使用 rm -rf,通过 sudo 规则限制
  3. 关键目录配置 safe-rm 保护
  4. 定期快照备份,误删后可以从快照恢复
  5. 使用配置管理工具(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

预防措施

  1. 团队规范:任何配置变更前必须备份,把这条写进操作手册
  2. 生产环境部署 etckeeper,所有 /etc 下的变更自动 git 追踪
  3. 用 Ansible/SaltStack 管理配置,配置文件以代码形式存在仓库里
  4. 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

预防措施

  1. 不要直接用 iptables 命令行加规则,用 firewalld 或 nftables 的配置文件管理
  2. 每次修改防火墙后,从另一台机器测试连通性
  3. 改防火墙前保持一个备用 SSH 连接,防止把自己锁在外面
  4. 定期审计防火墙规则,删除不再需要的条目

坑 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

预防措施

  1. 上线前必须配置 logrotate(见坑 8)
  2. 部署 Prometheus + node_exporter,设置磁盘使用率超过 80% 告警
  3. 应用日志级别在生产环境设置为 INFO 或 WARN,绝不用 DEBUG
  4. 单独挂载 /var/log 分区,日志撑爆不影响根分区
  5. 设置 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 连接后再连——连不上。原因:

  1. 端口改成了 2222,但防火墙没放行 2222
  2. 禁止了 root 登录,但没有提前创建普通用户并配置 sudo
  3. 禁止了密码认证,但没有提前配置 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

预防措施

  1. SSH 配置修改永远保持第二个连接作为后路
  2. 修改完用 sshd -t 验证语法后再 restart
  3. 端口修改和防火墙规则必须同步操作
  4. 云服务器务必确认 VNC / 带外管理可用
  5. 用 /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

预防措施

  1. 系统初始化模板必须包含时区设置和 NTP 配置
  2. 所有服务器统一使用 Asia/Shanghai(或统一 UTC,团队定一个标准就行)
  3. 监控 NTP 同步状态,偏差超过 100ms 告警
  4. 使用 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

预防措施

  1. 画出服务依赖图,贴在运维 wiki 上
  2. 用 systemd 的 After=Requires=Wants= 声明依赖关系
  3. 服务启动脚本中加入依赖检查逻辑
  4. 批量重启用编排脚本,不要手动逐个执行
  5. 实测过 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>

预防措施

  1. 任何应用上线前必须确认日志轮转配置
  2. logrotate 配置纳入部署清单
  3. 应用日志框架本身也要配置大小限制和轮转
  4. 监控 /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/

预防措施

  1. 团队规范:禁止使用 chmod 777,在 code review 和配置审计中检查
  2. 配置 umask 为 0027 或 0077
  3. 敏感文件(密钥、密码)权限必须是 600
  4. 定期运行权限审计脚本,找出权限过于宽松的文件
# 查找 777 权限的文件
find /opt /var /home -perm 777 -type f 2>/dev/null

坑 10:不看日志凭感觉排错

场景描述

服务起不来或者出错了,新人不看日志直接开始”玄学排错”。

踩坑过程

小刘遇到应用服务起不来:

sudo systemctl start app-service
# Job for app-service.service failed

小刘的排错流程:

  1. 重启三遍——不行
  2. 换个端口——还是不行
  3. 检查 Java 版本——没问题
  4. 重新部署——还是不行
  5. 开始怀疑服务器有问题,提了工单要求换台机器

整整折腾了 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 使用率

预防措施

  1. 培训新人的第一件事:出问题先看日志
  2. 在团队内部建立标准排错流程文档
  3. 定期组织故障排查演练,训练看日志的习惯
  4. 把 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
80 端口被占用
ss -tlnp | grep :80

 找到占用进程
ssh: connect to host xxx port 22: Connection refused
SSH 服务未运行或端口改了
通过 VNC/控制台检查 sshd 状态
No space left on device
磁盘满或 inode 耗尽
df -h

 和 df -i 分别检查
Permission denied (publickey)
SSH 密钥认证失败
检查 ~/.ssh/authorized_keys 权限(600)
systemctl: command not found
不在 PATH 中或系统太老
使用完整路径 /usr/bin/systemctl
Job for xxx.service failed
服务配置错误
journalctl -u xxx -n 50

 看日志
Name or service not known
DNS 解析失败
检查 /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 “三板斧”思维的危害

很多新人遇到问题的反应是:

  1. 重启服务
  2. 重启服务器
  3. 重装系统

这种”三板斧”思维的问题:

  • 重启可能破坏故障现场:内存中的数据、进程状态都丢了,没法事后分析
  • 重启不解决根因:问题还会再来,可能在更要命的时候
  • 重装是最懒的方案:配置全丢了,之前所有的调优、加固都白做了
  • 养成了不排查的习惯:技术能力永远得不到成长

正确的思路:先定位问题 → 理解原因 → 针对性修复 → 预防复发


五、故障排查和监控

5.1 新人必备排查工具链

场景
工具
关键命令
看什么
CPU 负载
top/htop
top -c

htop
%CPU 列、load average
磁盘 IO
iotop
sudo iotop -o
哪个进程在做大量 IO
网络连接
ss
ss -tlnp

ss -s
监听端口、连接状态统计
打开文件
lsof
lsof -i :8080

lsof -u app
端口占用、进程打开的文件
系统调用
strace
strace -p PID -e trace=open
进程卡在哪个系统调用
内核消息
dmesg
dmesg -T | tail -50
OOM、硬件错误、磁盘错误
进程状态
ps
ps auxf
进程树、资源使用
内存
free
free -h
used、available、swap
磁盘空间
df/du
df -h

du -sh /var/*
使用率、大文件
网络探测
ping/traceroute
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 进阶学习方向

  1. 配置管理自动化:学习 Ansible/Terraform,实现基础设施即代码
    • 学习资源:Ansible 官方文档 https://docs.ansible.com
    • 实践建议:从管理 5 台服务器开始,用 playbook 替代手动操作
  2. 监控体系建设:深入 Prometheus + Grafana + Alertmanager 全链路
    • 学习资源:Prometheus 官方文档 https://prometheus.io/docs
    • 实践建议:先把 node_exporter 部署到所有服务器,再逐步加入业务监控
  3. 容器化运维: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. 术语表

术语
英文
解释
OOM
Out Of Memory
内存不足,内核启动 OOM Killer 杀掉占用内存最多的进程
MTTR
Mean Time To Repair
平均修复时间,从故障发生到恢复的平均时长
SLA
Service Level Agreement
服务等级协议,约定服务可用性指标(如 99.9%)
SOP
Standard Operating Procedure
标准操作流程,固化的操作步骤文档
NTP
Network Time Protocol
网络时间协议,用于服务器间时间同步
ACL
Access Control List
访问控制列表,Linux 文件系统精细权限控制
LVM
Logical Volume Manager
逻辑卷管理器,灵活管理磁盘分区
inode
Index Node
索引节点,Linux 文件系统存储文件元数据的数据结构
umask
User file creation Mask
用户文件创建掩码,控制新建文件的默认权限
P0/P1/P2
Priority Level
告警/故障优先级等级
Runbook
运维操作手册,标准化的故障处理流程文档
Postmortem
故障复盘报告,记录事故的时间线、根因和改进措施

版权申明:内容来源网络,版权归原创者所有,如有侵权请联系删除

想了解更多干货,可通过下方扫码关注

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

未经允许不得转载:17认证网 » 运维新人最容易踩的 10 个坑,我替你整理好了
分享到:0

评论已关闭。

400-663-6632
咨询老师
咨询老师
咨询老师