从登录服务器到部署上线,站长必备的命令与排障套路。
| 章节 | |
|---|---|
| 01 SSH 与密钥登录 | 3 小节 |
| 02 常用命令速查 | 3 小节 |
| 03 服务与端口管理 | 2 小节 |
| 04 日志排查思路 | 3 小节 |
| 05 防火墙与安全组 | 3 小节 |
| 06 部署与回滚 | 4 小节 |
拿到一台新服务器,第一件事不是装环境,是把登录方式改对。密码登录的机器挂在公网上,几分钟内就会开始被爆破。
# 本地生成一对密钥,ed25519 比 rsa 更短更快,现代系统都支持
ssh-keygen -t ed25519 -C "laptop-2026" -f ~/.ssh/id_ed25519
# 把公钥推到服务器(会追加到对方的 ~/.ssh/authorized_keys)
ssh-copy-id -i ~/.ssh/id_ed25519.pub root@1.2.3.4
# 手动部署时注意权限,权限不对 sshd 会直接忽略这个文件
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys~/.ssh 必须 700、authorized_keys 必须 600,而且家目录本身不能对组和其他人可写。密钥登录不生效但又不报错,九成是权限问题,sudo tail -f /var/log/auth.log(CentOS 是 /var/log/secure)能看到原因。
确认密钥能登上去之后,再改 /etc/ssh/sshd_config:
Port 22022 # 换个端口,能挡掉绝大多数无脑扫描
PermitRootLogin prohibit-password # 允许 root 用密钥,禁密码;更严可以设 no
PasswordAuthentication no # 彻底关掉密码登录
PubkeyAuthentication yes
MaxAuthTries 3
ClientAliveInterval 60 # 防止连接被中间设备掐断
AllowUsers deploy # 只允许指定用户登录sshd -t # 先验语法,这一步别省
systemctl reload sshd改端口前先把防火墙和云安全组放行新端口,并且保留当前这个会话别断开,另开一个终端验证新端口能登进去,确认无误再关旧会话。这条规矩救过很多人。
# ~/.ssh/config
Host prod
HostName 1.2.3.4
Port 22022
User deploy
IdentityFile ~/.ssh/id_ed25519
ServerAliveInterval 60
Host db
HostName 10.0.0.5
User deploy
ProxyJump prod # 通过 prod 跳板访问内网机器之后 ssh prod、scp file prod:/tmp/ 就够了。ProxyJump 让内网机器完全不必暴露公网端口。
密钥也可以在页面上生成,见密钥生成器;新端口是否真的对外开放,用端口检测或在线端口扫描从外面验一次。
不求全,只挑排障时真正高频的。
# 整体负载:三个数是 1/5/15 分钟平均,和 CPU 核数比着看
uptime
# 实时进程,按 CPU 或内存排序;先按 1 看每个核的占用
top
# 更好用的话装 htop
# 内存,看 available 而不是 free(buff/cache 是可回收的)
free -h
# 磁盘空间与 inode,inode 满了也会报 No space left
df -h
df -i
# 找出谁在占空间
du -sh /var/* | sort -rh | head -10top 里的 %wa(iowait)高说明瓶颈在磁盘不在 CPU;load average 明显高于核数而 CPU 却不满,多半也是 IO 或者 D 状态进程堵住了。
# 按名字找文件
find /var/log -name '*.log' -mtime -1 # 一天内改过的
# 在文件里找内容,-r 递归 -n 显示行号 -I 跳过二进制
grep -rnI 'ConnectionRefused' /var/log/
# 找命令的位置与真实路径
which nginx; readlink -f $(which nginx)
# 看某个端口被谁占了
ss -lntp | grep :80
lsof -i :80ss 已经全面取代 netstat,参数好记:-l 监听、-n 不解析、-t TCP、-u UDP、-p 显示进程。
# 打包与解包
tar -czf backup.tgz /var/www/html
tar -xzf backup.tgz -C /tmp/restore
# 同步目录,-a 保留属性 -z 压缩 --delete 让目标和源完全一致
rsync -avz --delete ./dist/ deploy@prod:/var/www/html/
# 权限与属主
chmod 644 file; chmod 755 dir
chown -R www-data:www-data /var/www/htmlrsync 源路径结尾那个斜杠决定行为:./dist/ 是同步目录内容,./dist 是把 dist 这个目录本身放进去,写错会多套一层。
权限数字算不清的话用 Chmod 权限计算器,更完整的命令清单见 Linux 常用命令表,写脚本时排版用 Shell 脚本格式化。
现代发行版统一用 systemd 管服务,命令就那么几条:
systemctl status nginx # 状态 + 最近几行日志,排障第一条命令
systemctl start|stop|restart nginx
systemctl reload nginx # 不中断连接地重载配置,能 reload 就别 restart
systemctl enable --now nginx # 开机自启 + 立刻启动
systemctl list-units --failed # 有哪些服务起失败了# /etc/systemd/system/myapp.service
[Unit]
Description=My App
After=network.target
[Service]
Type=simple
User=deploy
WorkingDirectory=/var/www/myapp
ExecStart=/usr/bin/node server.js
Restart=always
RestartSec=3
Environment=NODE_ENV=production
[Install]
WantedBy=multi-user.targetsystemctl daemon-reload # 改了 unit 文件必须先 reload,这一步最容易忘
systemctl enable --now myapp
journalctl -u myapp -f # 跟踪日志Restart=always + RestartSec 是最简单的进程守护,不必再上 supervisor。
# 1. 本机在听吗?
ss -lntp | grep :8080
# 2. 本机连得上吗?(连不上说明是应用或本机防火墙的问题)
curl -sI http://127.0.0.1:8080/
# 3. 外面连得上吗?(本机通、外面不通 = 防火墙或安全组)
curl -sI http://<公网IP>:8080/第一步里要特别看监听地址:127.0.0.1:8080 只有本机能连,0.0.0.0:8080 才是对外开放。很多「端口开了但连不上」就卡在这里。
外部视角的验证交给在线端口扫描,各端口分别对应什么服务见常用端口号。
排障的顺序永远是:先看日志,再猜原因。反过来会浪费大量时间。
| 类型 | 路径 |
|---|---|
| systemd 托管的服务 | journalctl -u 服务名 |
| 系统与内核 | /var/log/syslog(Debian)/ /var/log/messages(RHEL)、dmesg |
| 登录与认证 | /var/log/auth.log / /var/log/secure |
| nginx | /var/log/nginx/access.log、error.log |
| 应用 | 自己配的路径,通常在项目 logs/ 或 /var/log/应用名/ |
# 只看最近 10 分钟、只看错误级别
journalctl -u myapp --since '10 min ago' -p err
# 跟踪并高亮关键字
tail -f /var/log/nginx/error.log | grep --color -i 'upstream'
# 内核有没有杀进程(OOM 是最容易被忽略的原因)
dmesg -T | grep -i -E 'oom|killed process'dmesg | grep oom 一定要查:服务莫名其妙消失、没有任何应用日志,多半是内存超了被内核干掉的。
# 状态码分布,先看整体有没有异常
awk '{print $9}' access.log | sort | uniq -c | sort -rn
# 5xx 都出在哪些 URL
awk '$9 ~ /^5/ {print $7}' access.log | sort | uniq -c | sort -rn | head -20
# 请求最多的 IP,揪出刷站的
awk '{print $1}' access.log | sort | uniq -c | sort -rn | head -20
# 最慢的请求(需要 log_format 里带 $request_time,默认没有)
awk '{print $NF, $7}' access.log | sort -rn | head -20最后一条的前提是先在 nginx 里把耗时加进日志格式:
log_format timed '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" "$http_user_agent" '
'rt=$request_time urt=$upstream_response_time';
access_log /var/log/nginx/access.log timed;$request_time 和 $upstream_response_time 一起看能直接分锅:两者都大 → 后端慢;只有 $request_time 大 → 客户端网络慢或者响应体太大。
磁盘写满会引发一连串莫名其妙的故障。logrotate 配好,或者至少设个上限:
# /etc/logrotate.d/myapp
/var/www/myapp/logs/*.log {
daily
rotate 14
compress
delaycompress
missingok
notifempty
copytruncate
}查到可疑 IP 之后,用 IP 地址查询看归属,用搜索引擎蜘蛛 IP 地址大全排除掉正常蜘蛛,再决定要不要封。
云服务器上通常有两层过滤:云平台的安全组(在机器外面)和系统自带的防火墙(在机器里面)。任何一层没放行都不通,排查时两边都要看。
Ubuntu / Debian 用 ufw:
ufw default deny incoming
ufw default allow outgoing
ufw allow 22022/tcp # 先放行 SSH!
ufw allow 80,443/tcp
ufw allow from 10.0.0.0/8 to any port 3306 # 只让内网连数据库
ufw enable
ufw status numberedRHEL / CentOS 用 firewalld:
firewall-cmd --permanent --add-port=80/tcp
firewall-cmd --permanent --add-rich-rule='rule family=ipv4 source address=10.0.0.0/8 port port=3306 protocol=tcp accept'
firewall-cmd --reload
firewall-cmd --list-all开防火墙前一定先放行 SSH 端口,否则 ufw enable 回车下去人就被关在门外了。没有云平台的 VNC 控制台可用时,这是要重装系统的。
| 端口 | 服务 | 为什么别暴露 |
|---|---|---|
| 3306 | MySQL | 弱口令爆破重灾区 |
| 6379 | Redis | 未授权访问可直接写 SSH 公钥拿 shell |
| 27017 | MongoDB | 同上,早年大量数据泄漏源头 |
| 9200 | Elasticsearch | 无认证即全量数据 |
| 2375 | Docker API | 等于把 root 交出去 |
这些服务一律绑定到 127.0.0.1 或内网地址,需要远程访问就用 SSH 隧道:
# 把远程的 3306 映射到本地 13306,只在这个终端有效
ssh -L 13306:127.0.0.1:3306 prod
mysql -h 127.0.0.1 -P 13306 -u root -papt install fail2ban
# /etc/fail2ban/jail.local
# [sshd]
# enabled = true
# maxretry = 5
# bantime = 3600
fail2ban-client status sshd配完从外部扫一次确认暴露面,用在线端口扫描;要给某个后台加一层基础认证,用 htpasswd 在线生成生成密码文件配到 nginx 的 auth_basic_user_file。
部署方案不用复杂,但必须能一条命令回滚。没有回滚手段的部署,出了事只能在线上现改代码,那才是真正的风险。
#!/bin/bash
set -euo pipefail # 出错即停,未定义变量报错
APP=/var/www/myapp
REL="$APP/releases/$(date +%Y%m%d%H%M%S)"
mkdir -p "$REL"
rsync -a --exclude='.git' ./ "$REL/" # 或者在这里 git clone / 解包
ln -sfn "$APP/shared/uploads" "$REL/uploads" # 用户数据放 shared,不随版本走
ln -sfn "$REL" "$APP/current.tmp"
mv -Tf "$APP/current.tmp" "$APP/current" # 原子替换,没有中间态
systemctl reload nginx
ls -dt "$APP"/releases/* | tail -n +6 | xargs -r rm -rf # 只留最近 5 个版本回滚就是把软链指回上一个目录:
PREV=$(ls -dt /var/www/myapp/releases/* | sed -n 2p)
ln -sfn "$PREV" /var/www/myapp/current.tmp
mv -Tf /var/www/myapp/current.tmp /var/www/myapp/current
systemctl reload nginx关键在 mv -Tf 这一步:直接 ln -sfn 覆盖已存在的软链在某些实现下不是原子的,先建临时链再 mv 才能保证请求要么看到旧版本、要么看到新版本,不会撞见半成品。
| 项 | 说明 |
|---|---|
| 配置文件先验语法 | nginx -t、php -l、systemd daemon-reload |
| 静态资源带指纹 | 否则长缓存会让用户看到旧文件 |
| 数据库变更向前兼容 | 先加字段再发代码,别在一次发布里既删字段又改代码 |
| reload 而不是 restart | 能平滑就别断连接 |
| 发布后立刻验一次 | 首页、登录、一个核心接口,各 curl 一次 |
for u in / /login /api/health; do
printf '%-14s %s\n' "$u" "$(curl -s -o /dev/null -w '%{http_code} %{time_total}s' https://example.com$u)"
donecrontab -e
# 分 时 日 月 周
0 3 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1
*/5 * * * * /usr/local/bin/healthcheck.shcron 的两个经典坑:环境变量和交互式 shell 完全不同(PATH 很短,node、php 都可能找不到,用绝对路径),以及不重定向输出的话错误信息会被邮件吞掉,永远记得 >> log 2>&1。表达式记不住就用 Cron 表达式生成与解析。
用 Docker 的话,回滚就是换个镜像 tag,天然具备版本能力——前提是别用 latest,镜像要带明确的版本号或 commit 号。
docker run -d --name myapp --restart=always -p 127.0.0.1:8080:8080 myapp:2026.09.28
docker logs -f --tail 100 myapp
docker exec -it myapp sh常用命令见 Docker 常用命令,把一长串 docker run 参数转成 compose 文件用 docker run 转 docker-compose,上了集群的看 Kubernetes 常用命令。从 Apache 迁到 nginx 时,规则转换用 htaccess 转 Nginx 配置。
本文由 ip438查询网 原创,正文中的命令与配置片段可直接取用。 内容会随实践持续修订,发现错漏欢迎反馈。