阶段2(下):运维专题 Docker/Nginx/Linux/CI-CD/Git 的课程、企业场景、面试题和可运行示例
This commit is contained in:
1 parent
5eb67c5d08
commit
b8010a74f3
42 files changed
+3533
-55
No files matched your search
@@ -0,0 +1,103 @@
|
||||
---
|
||||
id: devops.linux.commands
|
||||
title: 常用命令:文件、文本处理、进程、端口、权限、systemd
|
||||
type: lesson
|
||||
domain: devops
|
||||
stack: [linux]
|
||||
tags: [grep, awk, ps, ss, chmod, systemd, journalctl]
|
||||
level: basic
|
||||
est_hours: 3
|
||||
related: [devops.linux.troubleshooting, devops.linux.interview]
|
||||
week: [14]
|
||||
order: 1
|
||||
---
|
||||
|
||||
# 常用命令:文件、文本处理、进程、端口、权限、systemd
|
||||
|
||||
## 解释说明
|
||||
|
||||
| 类别 | 命令 | 常用写法 |
|
||||
| --- | --- | --- |
|
||||
| 文件 | `ls` `cd` `cp` `mv` `rm` `find` | `find /var/log -name "*.log" -mtime +7`(7 天前的日志) |
|
||||
| 查看 | `cat` `less` `head` `tail` | `tail -f app.log`(实时跟踪)、`tail -n 200 app.log` |
|
||||
| 文本 | `grep` `awk` `sort` `uniq` `wc` `sed` | `grep -n "ERROR" app.log`、`awk '{print $1}'`(取第一列) |
|
||||
| 进程 | `ps` `top` / `htop` `kill` `pgrep` | `ps -ef | grep api`、`kill -15 PID`(先礼后兵,最后才 `-9`) |
|
||||
| 端口 / 网络 | `ss` `curl` `ping` `dig` | `ss -lntp`(谁在监听哪个端口)、`curl -v http://127.0.0.1:8080/healthz` |
|
||||
| 磁盘 | `df` `du` | `df -h`、`du -sh /var/log/* | sort -h` |
|
||||
| 权限 | `chmod` `chown` `sudo` | `chmod 600 .env`、`chown app:app /srv/app -R` |
|
||||
| 服务 | `systemctl` `journalctl` | `systemctl status api`、`journalctl -u api -f` |
|
||||
| 传输 | `scp` `rsync` `ssh` | `rsync -avz dist/ user@host:/srv/web/` |
|
||||
|
||||
**权限**:`rwx` 分别对应 4、2、1;`chmod 755` = 所有者 rwx、组和其他人 r-x。密钥文件用 `600`。服务不要用 root 运行,单独建一个用户。
|
||||
|
||||
**管道和重定向**:`|` 把前一个命令的输出交给后一个;`>` 覆盖写文件、`>>` 追加;`2>&1` 把错误输出也合并进来。
|
||||
|
||||
**systemd**:把程序注册成系统服务——开机自启、崩溃自动重启、日志统一交给 journald。
|
||||
|
||||
## 案例
|
||||
|
||||
- 统计访问日志里请求最多的 10 个 IP:`awk '{print $1}' access.log | sort | uniq -c | sort -rn | head`。
|
||||
- 统计 5xx 的数量:`awk '$9 >= 500' access.log | wc -l`(combined 格式第 9 列是状态码)。
|
||||
- 找出占用 8080 端口的进程:`ss -lntp | grep 8080`。
|
||||
- Go API 部署成 systemd 服务,崩溃 3 秒后自动重启,日志用 `journalctl -u api` 查看。
|
||||
|
||||
## 代码示例
|
||||
|
||||
文本处理组合(可以对 [examples/log-analysis/sample_access.log](examples/log-analysis/sample_access.log) 直接运行):
|
||||
|
||||
```bash
|
||||
# 状态码分布
|
||||
awk '{print $9}' sample_access.log | sort | uniq -c | sort -rn
|
||||
# 请求最多的 IP
|
||||
awk '{print $1}' sample_access.log | sort | uniq -c | sort -rn | head -n 3
|
||||
# 耗时超过 1 秒的请求(最后一列是 $request_time)
|
||||
awk '$NF > 1 {print $NF, $7}' sample_access.log | sort -rn
|
||||
# 某个时间段的错误
|
||||
grep '23/Oct/2026:10:00' sample_access.log | grep -E '" 5[0-9]{2} '
|
||||
```
|
||||
|
||||
同样的分析用 Python 写成脚本:[examples/log-analysis/analyze.py](examples/log-analysis/analyze.py)。
|
||||
|
||||
Go 服务的 systemd 单元文件 `/etc/systemd/system/api.service`:
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=Order API
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
User=app
|
||||
Group=app
|
||||
WorkingDirectory=/srv/api
|
||||
EnvironmentFile=/srv/api/.env
|
||||
ExecStart=/srv/api/bin/api
|
||||
Restart=on-failure
|
||||
RestartSec=3
|
||||
# 停止时先发 SIGTERM,等 15 秒再强杀
|
||||
TimeoutStopSec=15
|
||||
LimitNOFILE=65535
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
```bash
|
||||
sudo systemctl daemon-reload # 修改单元文件后必须执行
|
||||
sudo systemctl enable --now api # 开机自启并立即启动
|
||||
systemctl status api
|
||||
journalctl -u api -f --since "10 min ago"
|
||||
```
|
||||
|
||||
## 易错点
|
||||
|
||||
- **`kill -9` 当默认**:进程来不及清理(关闭连接、写完文件)。先 `kill`(SIGTERM),等不及再 `-9`。
|
||||
- **`rm -rf $DIR/` 变量为空**:变成 `rm -rf /`。脚本里加 `set -u`,或写 `"${DIR:?}"/`。
|
||||
- **删除正在被写的大日志文件**:空间不释放(进程还持有文件句柄)。用 `: > app.log` 清空,或者重启进程;配置 logrotate。
|
||||
- **`chmod 777` 解决权限问题**:安全隐患。找出正确的用户和组。
|
||||
- **修改 systemd 单元文件后忘了 `daemon-reload`**:改动不生效。
|
||||
- **在服务器上直接 `vim` 改线上配置不留记录**:配置纳入 Git,通过部署流程发布。
|
||||
|
||||
## 面试常问
|
||||
|
||||
见 [Linux 面试题](interview.md):查端口占用、查日志、统计、软链接和硬链接、进程信号。
|
||||
@@ -0,0 +1,99 @@
|
||||
---
|
||||
id: devops.linux.troubleshooting
|
||||
title: 线上排查:CPU、内存、磁盘、网络、日志的定位思路
|
||||
type: lesson
|
||||
domain: devops
|
||||
stack: [linux]
|
||||
tags: [排查, top, 负载, OOM, 磁盘, TIME_WAIT, pprof]
|
||||
level: intermediate
|
||||
prerequisites: [devops.linux.commands]
|
||||
est_hours: 3
|
||||
related: [devops.linux.scenario.incident-troubleshooting, devops.linux.interview]
|
||||
week: [14]
|
||||
order: 2
|
||||
---
|
||||
|
||||
# 线上排查:CPU、内存、磁盘、网络、日志的定位思路
|
||||
|
||||
## 解释说明
|
||||
|
||||
**排查的总顺序**:先止血(回滚、扩容、限流、重启),再定位,最后复盘。定位时**从现象到资源再到代码**:
|
||||
|
||||
| 资源 | 看什么 | 命令 |
|
||||
| --- | --- | --- |
|
||||
| 负载 | load average 和 CPU 核数的比较;高负载可能是 CPU 忙,也可能是 IO 等待 | `uptime`、`top`(看 `us` / `sy` / `wa`) |
|
||||
| CPU | 哪个进程、哪个线程占用高 | `top -H -p PID`;Go 用 pprof、Python 用 py-spy |
|
||||
| 内存 | 剩余可用内存(看 `available`)、是否用到 swap、是否有 OOM | `free -h`、`journalctl -k | grep -i oom` |
|
||||
| 磁盘 | 空间和 inode 是否用满、IO 是否繁忙 | `df -h`、`df -i`、`du -sh`、`iostat -x 1` |
|
||||
| 网络 | 端口监听、连接状态分布、连接数是否打满 | `ss -lntp`、`ss -ant` 统计状态、`curl -w` 看耗时 |
|
||||
| 日志 | 错误日志、慢请求、时间点 | `journalctl -u 服务`、`grep`、`awk` |
|
||||
|
||||
**几个关键概念**
|
||||
|
||||
- **load average**:正在运行和等待运行(包括等待 IO)的进程数的平均值。4 核机器 load 为 4 左右算满载。
|
||||
- **`wa`(iowait)高**:CPU 在等磁盘,问题在 IO(慢查询扫盘、日志写太多),而不是计算。
|
||||
- **OOM Killer**:内存耗尽时内核按评分杀掉进程,应用日志里往往什么都没有,只能在内核日志里看到 `Killed process`。
|
||||
- **TIME_WAIT 很多**:主动关闭连接的一方会进入 TIME_WAIT。大量短连接(每次请求都新建连接)导致,解决办法是复用连接(HTTP keep-alive、连接池)。
|
||||
- **CLOSE_WAIT 很多**:对方已关闭,本方没有关闭——通常是代码里忘了关闭连接(如 Go 的 `resp.Body.Close()`)。
|
||||
|
||||
**语言级工具**:Go 服务开启 `net/http/pprof`(只在内网端口暴露),`go tool pprof http://127.0.0.1:6060/debug/pprof/profile?seconds=30` 采集 CPU 热点,`/debug/pprof/heap` 看内存,`/debug/pprof/goroutine` 看 goroutine 泄漏。Python 用 `py-spy top --pid PID`。
|
||||
|
||||
## 案例
|
||||
|
||||
- 接口变慢:`top` 看到 `wa` 30%,`iostat` 显示磁盘利用率 100%;进一步发现是同一台机器上的数据库在做全表扫描的报表查询。把报表改到从库。
|
||||
- 服务每天半夜重启:应用日志没有错误,`journalctl -k` 里有 `Out of memory: Killed process ... (api)`。用 pprof 看 heap,发现一个按天累积的本地缓存没有上限。
|
||||
- `ss -ant` 统计出几千个 CLOSE_WAIT:代码里调用第三方接口后没有关闭响应体。
|
||||
|
||||
## 代码示例
|
||||
|
||||
一分钟快照脚本 [examples/log-analysis/diag.sh](examples/log-analysis/diag.sh)(只读命令)的核心部分:
|
||||
|
||||
```bash
|
||||
uptime
|
||||
ps -eo pid,user,%cpu,%mem,etime,cmd --sort=-%cpu | head -n 10
|
||||
free -h
|
||||
df -h -x tmpfs -x devtmpfs
|
||||
ss -ant | awk 'NR > 1 {count[$1]++} END {for (s in count) print s, count[s]}'
|
||||
```
|
||||
|
||||
Go 服务暴露 pprof(只监听本机端口,不要暴露到公网):
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
_ "net/http/pprof" // 注册 /debug/pprof/* 到 http.DefaultServeMux
|
||||
)
|
||||
|
||||
func main() {
|
||||
go func() {
|
||||
// 单独的端口,只监听 127.0.0.1;业务服务用自己的 ServeMux,不暴露 pprof
|
||||
log.Println(http.ListenAndServe("127.0.0.1:6060", nil))
|
||||
}()
|
||||
biz := http.NewServeMux()
|
||||
biz.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("ok")) })
|
||||
log.Fatal(http.ListenAndServe(":8080", biz))
|
||||
}
|
||||
```
|
||||
|
||||
```bash
|
||||
# 在服务器上采集 30 秒 CPU 数据,然后看最耗 CPU 的函数
|
||||
go tool pprof -top http://127.0.0.1:6060/debug/pprof/profile?seconds=30
|
||||
# goroutine 数量是否持续增长(泄漏)
|
||||
curl -s http://127.0.0.1:6060/debug/pprof/goroutine?debug=1 | head -n 1
|
||||
```
|
||||
|
||||
## 易错点
|
||||
|
||||
- **一上来就重启**:现场没了,问题下次还会出现。重启前至少保存快照(diag.sh 的输出、pprof、日志片段)。
|
||||
- **只看 CPU 不看 iowait**:负载高不一定是计算问题。
|
||||
- **`free` 看 `free` 列认为内存不够**:Linux 会把空闲内存用作缓存,要看 `available`。
|
||||
- **在业务端口暴露 pprof**:泄露内部信息,还能被用来消耗资源。
|
||||
- **删了大文件磁盘空间没释放**:`lsof +L1` 找到仍持有已删除文件的进程。
|
||||
- **排查结论没有数据支撑**:每一步都记录命令和输出,便于复盘。
|
||||
|
||||
## 面试常问
|
||||
|
||||
见 [Linux 面试题](interview.md):CPU 飙高怎么排查、内存泄漏怎么查、磁盘满了怎么办、TIME_WAIT 和 CLOSE_WAIT。
|
||||
@@ -0,0 +1,65 @@
|
||||
"""分析 Nginx 访问日志:状态码分布、请求最多的 IP、最慢的接口、5xx 明细。
|
||||
|
||||
日志格式:Nginx combined 格式末尾追加 $request_time(秒),对应配置:
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent '
|
||||
'"$http_referer" "$http_user_agent" $request_time';
|
||||
运行:python analyze.py [日志文件],默认分析同目录的 sample_access.log
|
||||
等价的命令行写法见 Linux 专题课程(awk / sort / uniq)。
|
||||
"""
|
||||
import re
|
||||
import sys
|
||||
from collections import Counter, defaultdict
|
||||
from pathlib import Path
|
||||
|
||||
LINE = re.compile(
|
||||
r'(?P<ip>\S+) \S+ \S+ \[(?P<time>[^\]]+)\] "(?P<method>\S+) (?P<path>\S+) [^"]*" '
|
||||
r'(?P<status>\d{3}) (?P<bytes>\d+) "[^"]*" "[^"]*" (?P<rt>[\d.]+)'
|
||||
)
|
||||
|
||||
|
||||
def parse(path: Path):
|
||||
for n, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
|
||||
m = LINE.match(line)
|
||||
if not m:
|
||||
print(f"第 {n} 行格式不匹配,跳过", file=sys.stderr)
|
||||
continue
|
||||
d = m.groupdict()
|
||||
d["path"] = d["path"].split("?")[0] # 去掉查询参数,按接口聚合
|
||||
d["rt"] = float(d["rt"])
|
||||
yield d
|
||||
|
||||
|
||||
def analyze(rows: list[dict]) -> dict:
|
||||
by_api: dict[str, list[float]] = defaultdict(list)
|
||||
for r in rows:
|
||||
by_api[f'{r["method"]} {r["path"]}'].append(r["rt"])
|
||||
slowest = sorted(((max(v), k) for k, v in by_api.items()), reverse=True)[:3]
|
||||
return {
|
||||
"status": Counter(r["status"] for r in rows),
|
||||
"top_ips": Counter(r["ip"] for r in rows).most_common(3),
|
||||
"slowest": [(k, t) for t, k in slowest],
|
||||
"errors_5xx": [(r["time"], r["status"], r["path"], r["rt"]) for r in rows if r["status"].startswith("5")],
|
||||
}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
path = Path(sys.argv[1]) if len(sys.argv) > 1 else Path(__file__).with_name("sample_access.log")
|
||||
rows = list(parse(path))
|
||||
res = analyze(rows)
|
||||
print(f"共 {len(rows)} 条请求")
|
||||
print("状态码分布:", dict(sorted(res["status"].items())))
|
||||
print("请求最多的 IP:", res["top_ips"])
|
||||
print("最慢的接口(最大耗时):", res["slowest"])
|
||||
print("5xx 明细:")
|
||||
for e in res["errors_5xx"]:
|
||||
print(" ", *e)
|
||||
|
||||
if path.name == "sample_access.log": # 用示例日志自检
|
||||
assert res["status"]["401"] == 3 and res["status"]["429"] == 1
|
||||
assert res["top_ips"][0] == ("198.51.100.7", 4)
|
||||
assert res["slowest"][0] == ("GET /api/reports/monthly", 30.001)
|
||||
assert [e[1] for e in res["errors_5xx"]] == ["504", "502"]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env bash
|
||||
# 不自动运行:线上问题的“第一分钟”快照,只读命令,不修改任何东西
|
||||
# 用法:bash diag.sh [进程名关键字] > diag-$(date +%F-%H%M).txt
|
||||
set -uo pipefail
|
||||
|
||||
keyword="${1:-}"
|
||||
section() { printf '\n===== %s =====\n' "$1"; }
|
||||
|
||||
section "时间和负载"
|
||||
date
|
||||
uptime
|
||||
|
||||
section "CPU 占用最高的进程"
|
||||
ps -eo pid,ppid,user,%cpu,%mem,etime,cmd --sort=-%cpu | head -n 10
|
||||
|
||||
section "内存"
|
||||
free -h
|
||||
|
||||
section "磁盘空间和 inode"
|
||||
df -h -x tmpfs -x devtmpfs
|
||||
df -i -x tmpfs -x devtmpfs | awk 'NR == 1 || $5+0 > 80'
|
||||
|
||||
section "监听端口"
|
||||
ss -lntp 2>/dev/null | head -n 30
|
||||
|
||||
section "TCP 连接状态统计"
|
||||
ss -ant | awk 'NR > 1 {count[$1]++} END {for (s in count) print s, count[s]}'
|
||||
|
||||
if [[ -n "$keyword" ]]; then
|
||||
section "进程:$keyword"
|
||||
pgrep -af -- "$keyword" || echo "没有找到进程"
|
||||
fi
|
||||
|
||||
section "最近的内核日志(OOM 等)"
|
||||
if kernel_log="$(journalctl -k --since "1 hour ago" --no-pager 2>/dev/null)" || kernel_log="$(dmesg 2>/dev/null)"; then
|
||||
grep -iE 'oom|killed process' <<<"$kernel_log" | tail -n 20 || echo "最近没有 OOM 记录"
|
||||
else
|
||||
echo "无权限读取内核日志"
|
||||
fi
|
||||
@@ -0,0 +1,10 @@
|
||||
203.0.113.5 - - [23/Oct/2026:10:00:01 +0800] "GET /api/orders?page=1 HTTP/1.1" 200 512 "-" "Mozilla/5.0" 0.023
|
||||
203.0.113.5 - - [23/Oct/2026:10:00:02 +0800] "GET /api/orders?page=2 HTTP/1.1" 200 498 "-" "Mozilla/5.0" 0.031
|
||||
198.51.100.7 - - [23/Oct/2026:10:00:02 +0800] "POST /api/login HTTP/1.1" 401 64 "-" "curl/8.5.0" 0.004
|
||||
198.51.100.7 - - [23/Oct/2026:10:00:02 +0800] "POST /api/login HTTP/1.1" 401 64 "-" "curl/8.5.0" 0.004
|
||||
198.51.100.7 - - [23/Oct/2026:10:00:03 +0800] "POST /api/login HTTP/1.1" 401 64 "-" "curl/8.5.0" 0.005
|
||||
198.51.100.7 - - [23/Oct/2026:10:00:03 +0800] "POST /api/login HTTP/1.1" 429 31 "-" "curl/8.5.0" 0.001
|
||||
192.0.2.10 - - [23/Oct/2026:10:00:04 +0800] "GET /api/reports/monthly HTTP/1.1" 504 167 "-" "Mozilla/5.0" 30.001
|
||||
192.0.2.10 - - [23/Oct/2026:10:00:40 +0800] "GET /api/reports/monthly HTTP/1.1" 200 20480 "-" "Mozilla/5.0" 12.874
|
||||
203.0.113.9 - - [23/Oct/2026:10:00:41 +0800] "GET /api/orders/42 HTTP/1.1" 502 157 "-" "Mozilla/5.0" 0.002
|
||||
203.0.113.9 - - [23/Oct/2026:10:00:42 +0800] "GET /api/orders/42 HTTP/1.1" 200 233 "-" "Mozilla/5.0" 0.018
|
||||
@@ -4,12 +4,38 @@ title: Linux 运维基础
|
||||
type: index
|
||||
domain: devops
|
||||
stack: [linux]
|
||||
tags: []
|
||||
tags: [Linux, 命令行, 运维, 线上排查]
|
||||
week: [14]
|
||||
---
|
||||
|
||||
# Linux 运维基础
|
||||
|
||||
常用命令、进程与端口、日志、磁盘、权限、线上排查。
|
||||
后端岗位默认你能独立登录服务器:看日志、查进程和端口、处理磁盘满、配置 systemd 服务、排查“接口变慢 / 服务挂了”。这个专题只讲开发者最常用的部分,不追求系统管理员的深度。
|
||||
|
||||
> 本专题建设中:课程、企业场景、面试题和示例代码会陆续补充。
|
||||
## 学习路线
|
||||
|
||||
| 顺序 | 课程 | 级别 | 预计 |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | [常用命令:文件、文本处理、进程、端口、权限、systemd](01-commands.md) | 基础 | 3 小时 |
|
||||
| 2 | [线上排查:CPU、内存、磁盘、网络、日志的定位思路](02-troubleshooting.md) | 进阶 | 3 小时 |
|
||||
|
||||
## 企业场景
|
||||
|
||||
- [线上故障排查:接口变慢、磁盘写满、进程被 OOM 杀掉](scenarios/incident-troubleshooting.md)
|
||||
|
||||
## 面试题
|
||||
|
||||
- [Linux 面试题](interview.md)
|
||||
|
||||
## 示例代码
|
||||
|
||||
- [examples/log-analysis](examples/log-analysis/):`analyze.py` 分析 Nginx 访问日志(状态码、高频 IP、慢接口、5xx);`diag.sh` 一键采集系统快照(只读命令)
|
||||
|
||||
## 在 16 周计划中
|
||||
|
||||
- [第 14 周:后端扩展与部署](../../../plan/week14-backend-extend-deploy.md)
|
||||
|
||||
## 官方资料
|
||||
|
||||
- systemd 服务单元:https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html
|
||||
- 各命令的手册:`man 命令名`,或 https://man7.org/linux/man-pages/
|
||||
@@ -0,0 +1,59 @@
|
||||
---
|
||||
id: devops.linux.interview
|
||||
title: Linux 面试题
|
||||
type: interview
|
||||
domain: devops
|
||||
stack: [linux]
|
||||
tags: [Linux, 命令, 排查, 面试]
|
||||
level: basic
|
||||
related: [devops.linux.commands, devops.linux.troubleshooting]
|
||||
week: [14, 16]
|
||||
---
|
||||
|
||||
# Linux 面试题
|
||||
|
||||
## 基础
|
||||
|
||||
### 【基础】怎么查看某个端口被哪个进程占用?
|
||||
|
||||
**参考答案**:`ss -lntp | grep 8080`(或旧命令 `netstat -lntp`、`lsof -i :8080`),输出里有进程名和 PID。没有 root 权限时可能看不到其他用户进程的信息。
|
||||
|
||||
### 【基础】怎么实时查看日志?怎么查找包含某个关键字的行?
|
||||
|
||||
**参考答案**:`tail -f app.log` 实时跟踪;`grep -n "ERROR" app.log` 查找并显示行号,`grep -C 5` 显示上下文,`grep -r` 递归目录;systemd 服务用 `journalctl -u 服务名 -f`。
|
||||
|
||||
### 【基础】`kill`、`kill -9`、`kill -15` 有什么区别?
|
||||
|
||||
**参考答案**:`kill` 默认发送 SIGTERM(15),进程可以捕获它做清理后退出;`kill -9` 发送 SIGKILL,不能被捕获,进程立即被杀死,来不及释放资源。应该先 SIGTERM,等待超时后再 SIGKILL。
|
||||
|
||||
### 【基础】软链接和硬链接的区别?
|
||||
|
||||
**参考答案**:硬链接是同一个 inode 的另一个文件名,删除原文件后硬链接仍能访问,不能跨文件系统、不能链接目录;软链接是一个单独的文件,内容是目标路径,原文件删除后软链接失效,可以跨文件系统、可以链接目录。部署中常用软链接切换版本(`current -> releases/v2`)。
|
||||
|
||||
## 进阶
|
||||
|
||||
### 【进阶】服务器负载很高,你怎么排查?
|
||||
|
||||
**参考答案**:`uptime` 看 load 和核数对比;`top` 看 CPU 的 `us`(用户态计算)、`sy`(内核态)、`wa`(IO 等待);`wa` 高查磁盘(`iostat -x`、`iotop`),`us` 高找占用最高的进程和线程(`top -H -p`),再用语言工具(Go pprof、py-spy)定位到函数;同时看内存是否在用 swap。
|
||||
|
||||
### 【进阶】TIME_WAIT 和 CLOSE_WAIT 过多分别说明什么?
|
||||
|
||||
**参考答案**:TIME_WAIT 出现在主动关闭连接的一方,大量出现通常是频繁创建短连接,应该复用连接(keep-alive、连接池)。CLOSE_WAIT 表示对方已关闭、本方还没关闭,大量出现基本是程序没有关闭连接(例如 Go 没有 `resp.Body.Close()`),属于代码 bug。
|
||||
|
||||
### 【进阶】磁盘满了怎么处理?
|
||||
|
||||
**参考答案**:`df -h` 确认哪个分区满,`df -i` 检查 inode 是否用尽(大量小文件);`du -sh /* | sort -h` 逐层找大目录;常见是日志、临时文件、Docker 镜像和容器日志。删除正在被进程写的文件不会释放空间,用 `lsof +L1` 查找,清空文件内容或重启进程。长期方案:logrotate、Docker 日志大小限制、磁盘使用率告警。
|
||||
|
||||
## 场景题
|
||||
|
||||
### 【场景题】Go 服务内存持续上涨,最后被系统杀掉,怎么定位?
|
||||
|
||||
**参考答案**:先在内核日志确认是 OOM(`journalctl -k | grep -i oom`);服务开启 pprof,在内存上涨过程中多次采集 heap,对比哪些对象在增长(`go tool pprof -inuse_space`、`-base` 对比);同时看 goroutine 数量是否持续增长(goroutine 泄漏也会占内存)。常见原因:无上限的缓存或 map、goroutine 阻塞泄漏、读取大文件 / 大响应到内存。修复后加内存告警和 `MemoryMax` 限制。
|
||||
|
||||
### 【场景题】用户反馈接口偶尔很慢,但你登录服务器时一切正常,怎么办?
|
||||
|
||||
**参考答案**:需要事后可查的数据:Nginx 日志记录 `$request_time` 和 `$upstream_response_time`,用 awk 找出慢请求的时间点和接口;应用日志带请求 ID 和每一步耗时;数据库开慢查询日志;看云监控在对应时间点的 CPU、IO、网络曲线。根据时间点关联定时任务、备份、其他服务的批处理。长期方案是接入指标监控和链路追踪。
|
||||
|
||||
### 【场景题】怎么把一个 Go 程序部署成开机自启、崩溃自动重启的服务?
|
||||
|
||||
**参考答案**:编写 systemd 单元文件:指定运行用户、工作目录、环境变量文件、`ExecStart`、`Restart=on-failure`、`RestartSec`、停止超时;`systemctl daemon-reload`、`enable --now`;日志用 `journalctl -u` 查看。程序本身要处理 SIGTERM 优雅退出。也可以用 Docker 的 `restart: unless-stopped` 实现类似效果。
|
||||
@@ -0,0 +1,118 @@
|
||||
---
|
||||
id: devops.linux.scenario.incident-troubleshooting
|
||||
title: 线上故障排查:接口变慢、磁盘写满、进程被 OOM 杀掉
|
||||
type: scenario
|
||||
domain: devops
|
||||
stack: [linux, go, nginx]
|
||||
tags: [故障排查, OOM, 磁盘满, pprof, 复盘]
|
||||
level: intermediate
|
||||
prerequisites: [devops.linux.troubleshooting]
|
||||
est_hours: 2
|
||||
related: [devops.linux.interview, devops.nginx.interview]
|
||||
week: [14]
|
||||
---
|
||||
|
||||
# 线上故障排查:接口变慢、磁盘写满、进程被 OOM 杀掉
|
||||
|
||||
## 业务背景与问题
|
||||
|
||||
一个小团队维护的 SaaS 系统,部署在两台云服务器上(Nginx + Go API + PostgreSQL),没有专职运维。某天上午:用户反馈页面很慢,部分请求 502;中午服务自己恢复了;下午又出现。你被要求“查清楚原因并保证不再发生”。
|
||||
|
||||
## 约束
|
||||
|
||||
- 业务优先:排查不能让故障时间更长。
|
||||
- 只有 SSH 权限和基本的云监控(CPU、内存、磁盘曲线),没有完整的监控平台。
|
||||
- 需要给出书面复盘。
|
||||
|
||||
## 方案设计
|
||||
|
||||
按“止血 → 采集现场 → 定位 → 修复 → 预防”推进:
|
||||
|
||||
1. **止血**:确认影响范围(Nginx 日志里 5xx 比例),如果最近有发布先回滚;必要时重启服务恢复,但**重启前先采集现场**。
|
||||
2. **采集现场**:运行 `diag.sh` 保存系统快照;保存最近的 Nginx 错误日志、应用日志;对 Go 服务抓 pprof(heap、goroutine)。
|
||||
3. **定位**(本例的实际线索):
|
||||
- Nginx 错误日志:`connect() failed (111: Connection refused) while connecting to upstream` → 502 是因为 API 进程不在了;
|
||||
- `journalctl -k`:`Out of memory: Killed process 2345 (api)` → API 被 OOM 杀掉,systemd 自动拉起,所以“自己恢复了”;
|
||||
- 内存曲线:API 内存从启动开始持续上涨;
|
||||
- pprof heap:大部分内存在一个“导出任务结果”的 map 里——导出结果放在内存里等用户下载,从不清理。
|
||||
- 同时发现 `df -h` 磁盘使用 92%:应用日志没有切割。
|
||||
4. **修复**:导出结果改为写入对象存储 / 临时文件并设置过期清理;本地缓存加容量上限;配置 logrotate;systemd 设置 `MemoryMax` 让问题早暴露、不拖垮整机。
|
||||
5. **预防**:增加内存、磁盘、5xx 比例的告警;发布后观察内存曲线;复盘文档记录时间线和改进项。
|
||||
|
||||
## 关键代码
|
||||
|
||||
现场采集(只读):
|
||||
|
||||
```bash
|
||||
bash diag.sh api > /tmp/diag-$(date +%F-%H%M).txt
|
||||
journalctl -k --since "today" | grep -i -E "oom|killed process"
|
||||
tail -n 200 /var/log/nginx/error.log | grep -E "upstream|connect\(\)"
|
||||
curl -s -o /tmp/heap.pb.gz http://127.0.0.1:6060/debug/pprof/heap
|
||||
go tool pprof -top -inuse_space /tmp/heap.pb.gz | head -n 15
|
||||
```
|
||||
|
||||
logrotate 配置 `/etc/logrotate.d/api`:
|
||||
|
||||
```text
|
||||
/srv/api/logs/*.log {
|
||||
daily
|
||||
rotate 14
|
||||
compress
|
||||
missingok
|
||||
notifempty
|
||||
copytruncate
|
||||
}
|
||||
```
|
||||
|
||||
带上限的本地缓存(修复思路,替代“无限增长的 map”):
|
||||
|
||||
```go
|
||||
// 最多保留 max 个条目,超过时淘汰最早写入的(简单的 FIFO,按需换成 LRU)
|
||||
type BoundedCache[K comparable, V any] struct {
|
||||
mu sync.Mutex
|
||||
max int
|
||||
order []K
|
||||
data map[K]V
|
||||
}
|
||||
|
||||
func NewBoundedCache[K comparable, V any](max int) *BoundedCache[K, V] {
|
||||
return &BoundedCache[K, V]{max: max, data: make(map[K]V)}
|
||||
}
|
||||
|
||||
func (c *BoundedCache[K, V]) Put(k K, v V) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if _, ok := c.data[k]; !ok {
|
||||
c.order = append(c.order, k)
|
||||
if len(c.order) > c.max {
|
||||
delete(c.data, c.order[0])
|
||||
c.order = c.order[1:]
|
||||
}
|
||||
}
|
||||
c.data[k] = v
|
||||
}
|
||||
```
|
||||
|
||||
系统快照脚本和日志分析脚本见 [examples/log-analysis](../examples/log-analysis/)。
|
||||
|
||||
## 取舍
|
||||
|
||||
| 做法 | 优点 | 缺点 |
|
||||
| --- | --- | --- |
|
||||
| 直接重启 | 恢复最快 | 丢失现场,问题会再次发生 |
|
||||
| **先采集现场再重启(本方案)** | 能定位根因 | 多花一两分钟 |
|
||||
| 加内存 | 立即缓解 | 掩盖泄漏,只是延后 |
|
||||
| systemd `MemoryMax` 限制 | 防止拖垮整机、问题早暴露 | 进程会更早被杀,需要配合告警 |
|
||||
|
||||
## 易错点
|
||||
|
||||
- 只看应用日志,没看内核日志:OOM 在应用日志里没有任何痕迹。
|
||||
- 把“自己恢复了”当成没问题:其实是 systemd 自动重启掩盖了崩溃。
|
||||
- 删除正在写的日志文件释放空间:空间不会释放,用 `copytruncate` 或让程序重新打开文件。
|
||||
- 复盘只写“已修复”:要写时间线、根因、为什么没有提前发现、改进项和负责人。
|
||||
|
||||
## 面试怎么讲
|
||||
|
||||
> “有一次服务间歇性 502,自己又恢复。我先看 Nginx 错误日志,是连接后端被拒绝,说明进程挂了;再查内核日志发现是 OOM 被杀,systemd 自动重启所以看起来自己恢复了。用 pprof 看 heap,发现导出结果一直放在内存 map 里没清理。修复后又加了缓存上限、日志切割、内存和 5xx 告警,并写了复盘。”
|
||||
|
||||
如果你在前端岗位做过类似的线上排查(比如内存泄漏导致页面卡死、静态资源 404),也可以用同样的结构讲:现象 → 证据 → 根因 → 修复 → 预防。
|
||||
Reference in new issue
Block a user