go-backend-handbook 1.1.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/00-linux.md +389 -83
  2. package/01-go-basics.md +603 -174
  3. package/02-service-architecture.md +447 -101
  4. package/03-concurrency.md +542 -151
  5. package/04-mysql.md +1009 -228
  6. package/05-database-selection.md +608 -112
  7. package/06-redis.md +617 -140
  8. package/07-message-queue.md +493 -111
  9. package/08-api-and-rpc.md +448 -120
  10. package/09-observability.md +450 -110
  11. package/10-resilience.md +666 -112
  12. package/11-monitoring.md +975 -127
  13. package/12-testing.md +514 -102
  14. package/13-performance-debug.md +450 -100
  15. package/14-security.md +430 -108
  16. package/15-development-process.md +618 -115
  17. package/16-api-docs.md +729 -196
  18. package/17-deploy-ops.md +848 -162
  19. package/18-ai-assisted.md +456 -85
  20. package/19-appendix.md +2 -0
  21. package/20-production-patterns.md +117 -31
  22. package/21-complete-project.md +484 -89
  23. package/22-database-deep-dive.md +557 -42
  24. package/23-debugging-and-performance.md +379 -97
  25. package/24-api-contract-and-testing.md +566 -98
  26. package/25-production-operations.md +739 -235
  27. package/CHANGELOG.md +18 -0
  28. package/README.md +7 -3
  29. package/code/mini-coin/README.md +22 -14
  30. package/code/mini-coin/api/openapi.yaml +192 -0
  31. package/code/mini-coin/docs/changes/CHG-001-recharge/spec.md +3 -1
  32. package/code/mini-coin/internal/httpapi/handler.go +145 -25
  33. package/code/mini-coin/internal/httpapi/handler_test.go +181 -0
  34. package/code/mini-coin/internal/model/account.go +31 -6
  35. package/code/mini-coin/internal/repository/memory.go +41 -19
  36. package/code/mini-coin/internal/security/sign_test.go +2 -2
  37. package/code/mini-coin/internal/service/recharge.go +47 -38
  38. package/code/mini-coin/internal/service/recharge_test.go +142 -7
  39. package/code/mini-coin/migrations/002_create_recharge_order.sql +1 -1
  40. package/code/mini-coin/migrations/003_add_channel.sql +1 -1
  41. package/code/mini-coin/scripts/e2e.sh +9 -4
  42. package/code/mini-coin/scripts/loadtest.js +8 -4
  43. package/package.json +1 -1
package/00-linux.md CHANGED
@@ -1,43 +1,41 @@
1
1
  # 第 0 章 开发机与 Linux 入门
2
2
 
3
- > 目标:能连上远程开发机、看懂机器状态、出问题能自己排查第一层。
4
-
5
- > **JS 类比速查**(帮助建立直觉,不表示语义等价;差异列必须看)
3
+ > 目标:能连上远程开发机,看懂机器状态,磁盘/内存/CPU 出问题时知道先做什么,不把“机器卡了”当成玄学。
6
4
  >
7
- > | 后端概念 | JS / 前端里的近似物 | 关键差异 |
8
- > |---|---|---|
9
- > | 远程 Linux shell | 本机终端,只是换了台机器 | 代码、进程、依赖、环境变量全在远端 |
10
- > | 进程 | `node app.js` 启动的进程 | 后端进程长期驻留,要管 PID、信号、日志轮转 |
11
- > | systemd | `pm2` 的自动重启 | 系统级守护,开机自启、崩溃拉起 |
12
- > | top / htop | Chrome 任务管理器 | 看整机 CPU/内存/磁盘 |
13
- > | 磁盘满 | `node_modules` 占满硬盘 | 后端会导致日志写失败、DB 异常 |
14
- > | 没有对应物 | — | nginx / Docker / k8s 是纯后端概念,不用硬套 |
5
+ > 本章用 `devbox.example.com`、`demo` 这类占位符。真实地址、账号、密钥不要写进公开文档。
15
6
 
7
+ ---
16
8
 
17
- ## 0.1 为什么后端要有一台"开发机"
9
+ ## 0.1 为什么后端要有一台开发机
18
10
 
19
- 前端本地跑个 dev server 就够了;后端不行,因为依赖太多:
11
+ 前端本地起一个 dev server 就能开发,因为依赖主要是 Node 和浏览器。后端通常不行:
20
12
 
21
- - MySQL / Redis / Kafka 通常跑在机房或容器平台;
13
+ - MySQL/Redis/Kafka 在机房或容器平台,本地连不上;
22
14
  - 服务要访问测试环境、注册中心、配置中心;
23
- - 编译产物、日志、抓包、压测都需要一台长期在线的 Linux。
15
+ - 编译、压测、抓包、看日志需要一台长期在线的 Linux;
16
+ - 本地 Mac/Windows 和线上 Linux 的环境差异会制造 bug。
17
+
18
+ 所以团队一般给每人一台远程 Linux 开发机,代码、编译、联调都在上面做。
19
+
20
+ **“本地写代码、开发机编译”** 的意思是:
21
+
22
+ ```
23
+ 你的电脑:编辑器(VS Code / GoLand)通过 SSH/Remote 编辑远程文件
24
+ 开发机:真正执行 go build / go test / 启动服务 / 连数据库
25
+ ```
26
+
27
+ 不是把代码拷来拷去,而是编辑器直接操作远程工作区。
24
28
 
25
- 所以团队一般给每人一台**远程 Linux 开发机**,代码、编译、联调都在上面做。
29
+ ---
26
30
 
27
- ## 0.2 怎么连
31
+ ## 0.2 怎么连:SSH 四层排查
28
32
 
29
33
  ```bash
30
- # 1. 看本机 SSH 配置里有没有目标主机
31
34
  cat ~/.ssh/config
32
-
33
- # 2. 连接(主机别名来自 ssh config)
34
35
  ssh devbox
35
-
36
- # 3. 首次连接会问 yes/no,输入 yes
37
- # 4. 免密登录靠公钥:把 ~/.ssh/id_ed25519.pub 内容加到目标机 ~/.ssh/authorized_keys
38
36
  ```
39
37
 
40
- `~/.ssh/config` 示例(**用占位符,不要照抄真实内网地址**):
38
+ `~/.ssh/config` 示例:
41
39
 
42
40
  ```
43
41
  Host devbox
@@ -47,31 +45,50 @@ Host devbox
47
45
  IdentityFile ~/.ssh/id_ed25519_demo
48
46
  IdentitiesOnly yes
49
47
  ServerAliveInterval 30
48
+ ServerAliveCountMax 3
50
49
  ```
51
50
 
52
- `ServerAliveInterval 30` 能防止长时间不操作被防火墙断开。
51
+ `ServerAliveInterval 30` 每 30 秒发心跳,防止长时间不操作被防火墙断开。
52
+
53
+ ### 连不上,按四层排查
53
54
 
54
- ### 连不上,按这个顺序排查
55
+ 排查 SSH 不能靠反复重试。先判断失败发生在哪一层:DNS 是否能解析、目标端口是否可达、服务端是否接受认证、登录后 shell 是否正常。每层只保留一个能给出确定结论的命令,并把报错原文记录下来;否则很容易把“代理没开”误判成“密钥坏了”。下面四条命令分别验证网络、端口、认证和登录后环境,执行顺序不要跳。
55
56
 
56
- | 现象 | 排查命令 | 常见原因 |
57
+ ```
58
+ 网络通不通 → 端口开不开 → 认证过不过 → 登录后环境对不对
59
+ ```
60
+
61
+ | 现象 | 命令 | 常见原因 |
57
62
  |---|---|---|
58
- | `Connection timed out` | `ping devbox.example.com`、`nc -vz host 22` | 不在内网/VPN、防火墙、安全组 |
63
+ | `Connection timed out` | `ping host`、`nc -vz host 22` | 不在内网/VPN、防火墙、安全组 |
59
64
  | `Connection refused` | `nc -vz host 22` | sshd 没启动、端口错 |
60
- | `Permission denied (publickey)` | `ssh -vvv devbox` | 公钥没放对、权限不是 600 |
61
- | `Host key verification failed` | `ssh-keygen -R host` | 机器重装,主机指纹变了 |
62
- | 连上就断 | `ssh -vvv`,看服务端 `/var/log/auth.log` | 账号被锁、磁盘满、sshd 限制 |
63
- | 卡在 `Connecting` | `curl -v telnet://host:22` | 代理/跳板机问题 |
65
+ | `Permission denied (publickey)` | `ssh -vvv devbox` | 公钥没放对、权限不对 |
66
+ | `Host key verification failed` | `ssh-keygen -R host` | 机器重装,指纹变了 |
67
+ | 连上就断 | `ssh -vvv`,服务端 `/var/log/auth.log` | 账号被锁、磁盘满、sshd 限制 |
68
+ | 卡在 Connecting | `curl -v telnet://host:22` | 代理/跳板机问题 |
64
69
 
65
- 固定排查套路:**网络通不通 → 端口开不开 → 认证过不过 → 登录后环境对不对**。
70
+ 一次性确认:
66
71
 
67
72
  ```bash
68
- # 一次性确认四层
69
73
  ping -c 2 devbox.example.com
70
74
  nc -vz devbox.example.com 22
71
75
  ssh -o BatchMode=yes devbox 'echo ok'
72
76
  ssh devbox 'hostname; whoami; uptime'
73
77
  ```
74
78
 
79
+ ### 公钥权限
80
+
81
+ ```bash
82
+ chmod 700 ~/.ssh
83
+ chmod 600 ~/.ssh/id_ed25519_demo
84
+ chmod 644 ~/.ssh/id_ed25519_demo.pub
85
+ chmod 600 ~/.ssh/authorized_keys # 在开发机上
86
+ ```
87
+
88
+ 权限太宽,sshd 会拒绝使用密钥。
89
+
90
+ ---
91
+
75
92
  ## 0.3 登录后先看什么
76
93
 
77
94
  ```bash
@@ -84,34 +101,86 @@ uptime # 负载
84
101
  top -b -n1 | head -20
85
102
  ```
86
103
 
87
- **负载(load average)怎么读**:三个数分别是 1/5/15 分钟平均。经验值:`load > CPU 核数` 说明有排队,`load > 2 × 核数` 说明明显过载。但 IO 密集时 load 高不一定 CPU 忙,要结合 `%wa` 看。
104
+ ### 负载怎么读
88
105
 
89
- ## 0.4 磁盘和内存满了怎么办
106
+ `uptime` 输出:
90
107
 
91
- ### 磁盘
108
+ ```
109
+ load average: 1.20, 0.80, 0.50
110
+ ```
111
+
112
+ 三个数分别是 1/5/15 分钟平均。经验:
113
+
114
+ - `load < 核数`:通常正常;
115
+ - `load > 核数`:有排队;
116
+ - `load > 2 × 核数`:明显过载。
117
+
118
+ 但 load 包含不可中断 IO 等待,磁盘慢时 load 也会高。结合 `top` 的 `%wa` 和 `iostat` 看。
119
+
120
+ ### CPU 怎么读
121
+
122
+ `%CPU` 是某个进程消耗的 CPU 时间比例,单核跑满大约是 100%,多核进程可能超过 100%;`load average` 表示处于可运行和不可中断等待状态的任务数,不等于 CPU 使用率。判断时要把两者和三段时间窗口一起看:1 分钟高、15 分钟低通常只是短突发;三个值一起高且 `nproc` 只有 2 时,机器已经持续过载;CPU 不高但 load 很高,常见原因是大量 IO 等待。
123
+
124
+ ```bash
125
+ top
126
+ # 看 %us(用户态)、%sy(内核态)、%wa(IO 等待)、%id(空闲)
127
+ ```
128
+
129
+ - `%us` 高:业务计算多,查 CPU profile;
130
+ - `%sy` 高:系统调用/上下文切换多;
131
+ - `%wa` 高:磁盘 IO 慢;
132
+ - `%id` 低但 load 高:可能在等 IO。
133
+
134
+ ---
135
+
136
+ ## 0.4 磁盘满了怎么办
137
+
138
+ ### 先定位
139
+
140
+ 定位磁盘问题要同时看“文件系统使用率”和“文件归属”。`df -h` 只告诉你哪个挂载点满了,`du` 找出目录占用,`lsof +L1` 处理“文件已删除但进程仍持有句柄”的特殊情况。生产机不要立刻递归扫描整个 `/`,先限定到日志、容器、临时目录和 core dump,控制命令本身的 IO。
92
141
 
93
142
  ```bash
94
143
  df -h # 哪个分区满
95
144
  du -sh /var/* 2>/dev/null | sort -h # 一级目录谁大
96
145
  du -sh /var/log/* | sort -h | tail # 日志大户
97
- lsof +L1 # 已删除但被进程占用的文件(空间不释放的元凶)
146
+ lsof +L1 # 已删除但被进程占用的文件
98
147
  ```
99
148
 
100
- 处理顺序:
149
+ `df` 显示 100% 但 `du` 找不到大文件,通常是**文件被删除但进程还开着**,用 `lsof +L1` 找。
150
+
151
+ ### 处理顺序
101
152
 
102
- 1. **先止血**:清日志前先确认不是审计/事故证据,优先 `truncate -s 0 file` 而不是 `rm`(`rm` 对正在写的文件不释放空间)。
103
- 2. **找根因**:日志没轮转?临时文件没清?core dump?
104
- 3. **加防线**:logrotate、磁盘告警阈值 80%/90%、容器日志大小限制。
153
+ 先止血再找根因。生产上优先确认影响范围、是否有副本可承接、最近是否发布,再决定扩容、摘流量或回滚;不要一上来 `kill -9`,否则线程栈、buffer 和未完成请求都会丢失。下面的顺序可以当作一次最小应急演练。
154
+
155
+ 1. **止血**:确认不是审计/事故证据后,清理日志或临时文件;
156
+ 2. **找根因**:日志没轮转?core dump?临时文件没清?
157
+ 3. **加防线**:logrotate、磁盘告警 80%/90%、容器日志大小限制。
105
158
 
106
159
  ```bash
107
- # 安全清空一个正在被写的日志
160
+ # 安全清空正在写的日志(rm 后空间可能不释放)
108
161
  truncate -s 0 /var/log/app/app.log
109
162
 
110
- # 找 7 天前的临时文件(先看再删)
163
+ # 找 7 天前的临时文件,先看再删
111
164
  find /tmp -type f -mtime +7 -ls | head
112
165
  ```
113
166
 
114
- ### 内存
167
+ **不要直接 `rm -rf /var/log/*`**:可能删掉正在写的关键日志,空间还不释放。
168
+
169
+ ### 磁盘满会导致什么
170
+
171
+ 磁盘满不只影响写日志。Go 进程可能无法创建临时文件、MySQL 无法写 redo/binlog、容器运行时无法拉镜像,甚至 DNS 和日志采集也会异常。因此“接口 500”可能只是磁盘问题,排查时要主动看 `df -i` 和 `df -h`。
172
+
173
+ - 日志写失败;
174
+ - 数据库无法写入/WAL 卡住;
175
+ - 容器被驱逐;
176
+ - SSH 登录失败(无法写 session);
177
+ - 服务启动失败。
178
+
179
+ 磁盘告警必须早于 100%。
180
+
181
+ ---
182
+
183
+ ## 0.5 内存满了怎么办
115
184
 
116
185
  ```bash
117
186
  free -h
@@ -119,69 +188,306 @@ ps aux --sort=-%mem | head
119
188
  cat /proc/meminfo | head
120
189
  ```
121
190
 
122
- Linux 会把空闲内存用作 page cache,`free` 的 `available` 才是真正可用的。如果 `available` 很低且 `swap` 在涨,就是真紧张。处理:限容器内存、查泄漏(pprof)、加 swap 只是缓冲不是方案。
191
+ Linux 会把空闲内存用作 page cache,`free` 的 `available` 才是真正可用。如果 `available` 很低且 swap 在涨,就是真紧张。
192
+
193
+ ### 处理
194
+
195
+ 内存问题先分清 `free`、`available`、缓存和 RSS。Linux 会把空闲内存用于 page cache,`free` 很小不一定危险;`available` 才是新进程大约还能拿到的内存。若 `available` 持续下降、swap 大量换入换出,或内核日志出现 OOM kill,才说明已经影响业务。处理时优先摘流量、回滚、限制并发和保留现场,不要用清理 page cache 伪装恢复。
196
+
197
+ 先找到增长最快的目录和文件,确认它是否能安全清理,再处理进程仍持有的已删除文件。生产清理必须保留审计日志和数据库文件,不能直接 `rm -rf /` 或删除正在写入的 binlog。
198
+
199
+ 1. 找最大进程:`ps aux --sort=-%mem | head`;
200
+ 2. 看是否持续上涨(内存泄漏);
201
+ 3. Go 服务用 pprof heap 分析;
202
+ 4. 容器设置合理内存 limit 和 `GOMEMLIMIT`;
203
+ 5. swap 只是缓冲,不是解决方案。
204
+
205
+ OOM 时内核会杀进程:
206
+
207
+ ```bash
208
+ dmesg -T | grep -i 'killed process'
209
+ ```
210
+
211
+ ---
212
+
213
+ ## 0.6 环境变量与 shell
214
+
215
+ ```bash
216
+ export APP_ENV=dev
217
+ echo $APP_ENV
218
+ env | grep APP_
219
+ ```
220
+
221
+ 临时生效只影响当前 shell。永久生效写到 `~/.bashrc` 或 `~/.zshrc`,但**不要把密钥写进 shell 历史**。
222
+
223
+ 查看命令来源:
224
+
225
+ ```bash
226
+ which go
227
+ type go
228
+ go version
229
+ ```
230
+
231
+ PATH 配置:
232
+
233
+ ```bash
234
+ export PATH=$PATH:/usr/local/go/bin
235
+ ```
236
+
237
+ ### 常用的管道组合
238
+
239
+ 管道把上一条命令的 stdout 交给下一条命令的 stdin,`grep` 过滤、`sort` 排序、`uniq -c` 计数、`awk` 取列。排障时要意识到每一段都可能改变结果:日志格式变化会让列号失效,`tail -f` 后接过滤会缓冲,统计大文件也会争抢磁盘。先在样本文件上验证输出,再对线上大日志执行。
240
+
241
+ ```bash
242
+ # 统计访问日志里各状态码数量
243
+ awk '{print $9}' access.log | sort | uniq -c | sort -nr
244
+
245
+ # 找监听 8080 的进程
246
+ ss -lntp | grep 8080
247
+
248
+ # 实时看错误日志
249
+ tail -f app.log | grep -i error
250
+
251
+ # 查文件里某个字符串
252
+ grep -rn 'panic' /var/log/app/
253
+ ```
254
+
255
+ ---
123
256
 
124
- ## 0.5 必会命令清单
257
+ ## 0.7 必会命令
125
258
 
126
259
  ```bash
127
260
  # 文件
128
- ls -lah; cd; pwd; cp -r; mv; mkdir -p; find . -name '*.go'
261
+ ls -lah
262
+ cd /path
263
+ pwd
264
+ cp -r src dst
265
+ mv old new
266
+ mkdir -p /data/app
267
+ find . -name '*.go'
268
+ du -sh *
269
+
129
270
  # 文本
130
- cat; less; head -n 50; tail -f; grep -rn 'keyword' .; sed -n '10,20p' file
271
+ cat file
272
+ less file # /搜索,q退出
273
+ head -n 50 file
274
+ tail -f file
275
+ grep -rn 'keyword' .
276
+ sed -n '10,20p' file
277
+
131
278
  # 进程
132
- ps aux | grep app; top; kill -15 PID; kill -9 PID # 先 15 再 9
279
+ ps aux | grep app
280
+ top
281
+ kill -15 PID # 优雅退出
282
+ kill -9 PID # 强杀,最后手段
283
+
133
284
  # 端口
134
- ss -lntp | grep 8080; lsof -i:8080
285
+ ss -lntp | grep 8080
286
+ lsof -i:8080
287
+
135
288
  # 网络
136
- curl -v; curl -sS -w '%{http_code} %{time_total}\n' -o /dev/null URL
289
+ curl -v https://example.com
290
+ curl -sS -o /dev/null -w '%{http_code} %{time_total}\n' https://example.com
291
+ nc -vz host 443
292
+
137
293
  # 压缩
138
- tar -czf x.tar.gz dir/; tar -xzf x.tar.gz
139
- # 权限
140
- chmod 600 key; chown user:group file
294
+ tar -czf app.tar.gz app/
295
+ tar -xzf app.tar.gz
141
296
  ```
142
297
 
143
- `kill -15`(SIGTERM)让程序优雅退出,`kill -9` 强杀不给清理机会,生产环境优先 15。
298
+ ### `kill -15` 和 `kill -9`
144
299
 
145
- ## 0.6 环境变量与 shell
300
+ 信号不是“关闭按钮”,而是通知进程采取动作。`SIGTERM` 给服务执行优雅退出的机会:停止接收新请求、等待在途请求、关闭连接池并 flush 日志;`SIGKILL` 由内核立即终止,进程没有清理机会。正确顺序是先发 TERM,观察进程是否在发布窗口内退出;只有确认卡死且已评估影响后才发 KILL。
301
+
302
+ - `SIGTERM(15)`:请求进程优雅退出,服务可以处理完在途请求、关闭连接;
303
+ - `SIGKILL(9)`:内核直接杀,进程没有机会清理。
304
+
305
+ 先 15,等几秒不退再 9。
306
+
307
+ ---
308
+
309
+ ## 0.8 systemd:让进程常驻
310
+
311
+ systemd 负责拉起进程、收集日志和管理开机启动,但它不是业务健康检查器。一个 unit 至少要写清运行用户、工作目录、启动命令、重启策略、环境变量和文件描述符上限;配置错误时先 `systemctl daemon-reload`,再用 `systemctl status` 与 `journalctl -u` 看真实退出原因。`Restart=on-failure` 只能恢复偶发崩溃,如果进程因为配置错误反复退出,自动重启会制造日志风暴。
312
+
313
+ ```ini
314
+ # /etc/systemd/system/minicoin.service
315
+ [Unit]
316
+ Description=minicoin
317
+ After=network.target
318
+
319
+ [Service]
320
+ User=app
321
+ WorkingDirectory=/opt/minicoin
322
+ ExecStart=/opt/minicoin/server
323
+ Restart=on-failure
324
+ RestartSec=3
325
+ Environment=APP_ENV=prod
326
+ LimitNOFILE=65535
327
+
328
+ [Install]
329
+ WantedBy=multi-user.target
330
+ ```
146
331
 
147
332
  ```bash
148
- export APP_ENV=dev
149
- env | grep APP_
150
- echo $PATH
151
- which go
333
+ sudo systemctl daemon-reload
334
+ sudo systemctl start minicoin
335
+ sudo systemctl status minicoin
336
+ sudo systemctl restart minicoin
337
+ journalctl -u minicoin -f
152
338
  ```
153
339
 
154
- `~/.bashrc` 放交互式配置,`~/.bash_profile` 放登录配置。**不要把密码写进 shell 历史**:用 `read -s` 或密钥管理。
340
+ `Restart=on-failure` 让进程崩溃后自动拉起,但**不要用它掩盖反复崩溃**,要看日志找根因。
341
+
342
+ ---
343
+
344
+ ## 0.9 开发机常见问题
345
+
346
+ 这张表用于建立第一反应:先区分网络、认证、DNS、端口和本机环境,不要看到“连不上”就重装客户端。每次排查都记录失败发生在哪一层,后续才能复用命令。
347
+
348
+ | 现象 | 可能原因 | 处理 |
349
+ |---|---|---|
350
+ | 连不上 | VPN/防火墙/sshd | 按四层排查 |
351
+ | 登录后很卡 | CPU/内存/磁盘满 | top/free/df |
352
+ | `go: command not found` | PATH 没配 | `which go`、配 PATH |
353
+ | 拉代码失败 | 凭证/网络 | `ssh -T git@host`、检查代理 |
354
+ | 端口被占 | 旧进程没退 | `ss -lntp`、kill |
355
+ | 磁盘满 | 日志/core dump | df/du/lsof |
356
+ | 服务起不来 | 配置/端口/依赖 | 看日志、`journalctl` |
357
+
358
+ ---
155
359
 
156
360
  ## 习题
157
361
 
158
- 1. 写出连接 `devbox.example.com`、用户 `demo`、密钥 `~/.ssh/id_ed25519_demo` 的 ssh config 片段。
159
- 2. `ssh` 报 `Permission denied (publickey)`,列出至少 4 个可能原因和验证命令。
160
- 3. 磁盘 100%,`du` 显示 `/var/log` 只有 2G,但 `df` 显示少了 20G,怎么查?
161
- 4. `free -h` 显示 free 很低但 available 很高,正常吗?为什么?
162
- 5. load average = 16,机器 8 核,说明什么?还要看哪个指标才能判断是 CPU 还是 IO 问题?
163
- 6. 用一条命令找出当前监听 8080 端口的进程。
164
- 7. 为什么清理正在写入的日志推荐 `truncate` 而不是 `rm`?
165
- 8. 写一个命令,输出某 URL 的 HTTP 状态码和总耗时。
362
+ ### 基础题
363
+
364
+ 1. 为什么后端需要远程开发机?
365
+ 2. SSH 连不上的四层排查顺序是什么?
366
+ 3. `load average` 三个数分别代表什么?load 高一定是 CPU 忙吗?
367
+ 4. `df` 显示磁盘满但 `du` 找不到大文件,可能是什么原因?
368
+ 5. `free` 的 `available` 和 `free` 有什么区别?
369
+ 6. `kill -15` 和 `kill -9` 的区别?
370
+ 7. systemd 的 `Restart=on-failure` 有什么作用和风险?
371
+ 8. 为什么不能把密钥写进 `~/.bashrc` 或命令历史?
372
+
373
+ ### 动手题
374
+
375
+ 9. 写一组命令:查看主机名、CPU 核数、内存、磁盘、负载。
376
+ 10. 写命令找出 `/var/log` 下最大的 10 个文件。
377
+ 11. 写命令找出监听 8080 的进程并优雅停止。
378
+ 12. 写一个 systemd unit,让服务崩溃后 3 秒重启。
379
+
380
+ ### 综合题
381
+
382
+ 13. 开发机突然很卡,SSH 勉强能连上。写出你的前 10 个排查命令和判断逻辑。
383
+
384
+ ---
166
385
 
167
386
  ## 参考答案
168
387
 
169
388
  <details>
170
389
  <summary>展开</summary>
171
390
 
172
- 1.
391
+ 1. 后端依赖数据库、缓存、消息队列和内网服务,还需要一台长期在线、与线上 Linux 接近的机器做编译、联调、压测、抓包和看日志。前端本地 dev server 能替代一部分工作,但不能替代后端依赖环境。
392
+
393
+ 2. 按“DNS → TCP 端口 → SSH 认证 → 登录后 shell”排查。DNS 用 `getent hosts devbox.example.com`;TCP 用 `nc -vz devbox.example.com 22`;认证用 `ssh -vvv devbox`;登录后用 `hostname; whoami; uptime`。每层成功后再进下一层,不要把网络超时误判成密钥错误。
394
+
395
+ 3. 三个数分别是最近 1、5、15 分钟的可运行任务和不可中断等待任务的平均数量。load 高不一定是 CPU 忙:磁盘 IO 阻塞也会把 load 拉高,要同时看 `top` 的 `%wa`、`iostat` 和进程状态。
396
+
397
+ 4. 常见原因是文件已经被删除,但进程仍持有打开句柄,inode 空间没有释放。执行:
398
+
399
+ ```bash
400
+ df -h
401
+ sudo lsof +L1 | head -30
173
402
  ```
174
- Host devbox
175
- HostName devbox.example.com
176
- User demo
177
- IdentityFile ~/.ssh/id_ed25519_demo
178
- IdentitiesOnly yes
403
+
404
+ 确认是哪个进程后,重启或让进程重新打开文件;不要直接删除不认识的文件。
405
+
406
+ 5. `free` 是完全没有被使用的内存;`available` 是估算后可分配给新进程的内存,包含可回收的 page cache。判断是否快 OOM 主要看 `available`、swap 和进程 RSS。
407
+
408
+ 6. `SIGTERM(15)` 请求进程优雅退出,服务可以停止接新请求、等待在途请求并关闭连接池;`SIGKILL(9)` 由内核立即终止。先发 15,等待发布窗口仍不退出并评估影响后才发 9。
409
+
410
+ 7. `Restart=on-failure` 能在进程异常退出后自动拉起,减少短时故障;风险是配置错误、端口冲突或持续 panic 时反复重启,制造日志风暴并掩盖根因。必须同时看 `systemctl status` 和 `journalctl -u`。
411
+
412
+ 8. 写进 `.bashrc`、命令历史或进程命令行会进入文件、shell history、备份、截图和 `ps` 输出,泄露后很难彻底清理。使用密钥管理或平台注入的环境变量,并确保日志脱敏。
413
+
414
+ 9.
415
+
416
+ ```bash
417
+ hostnamectl
418
+ nproc
419
+ free -h
420
+ df -h
421
+ uptime
422
+ ```
423
+
424
+ 预期输出依次包含主机信息、CPU 核数、内存、磁盘和三个 load 值。命令缺失时先查 `command -v`,不要用一个命令的输出推断全部资源状态。
425
+
426
+ 10.
427
+
428
+ ```bash
429
+ sudo du -ah /var/log 2>/dev/null | sort -rh | head -10
430
+ ```
431
+
432
+ 这是一次性排查,不要在生产高峰对整个 `/` 扫。用 `sudo` 是为了读取受限日志;如果 `du` 很慢,先缩小到具体业务日志目录。
433
+
434
+ 11.
435
+
436
+ ```bash
437
+ pid=$(ss -lntp 'sport = :8080' | sed -n 's/.*pid=\([0-9]*\).*/\1/p' | head -1)
438
+ test -n "$pid"
439
+ kill -TERM "$pid"
440
+ ```
441
+
442
+ 先确认 PID 对应的是预期进程,再发 SIGTERM。若进程 10 秒后仍不退出,先用 `ps -o pid,ppid,stat,etime,cmd -p "$pid"` 和日志判断是否卡在关闭流程;只有确认必须强杀时才用 `kill -KILL`。
443
+
444
+ 12. 完整的 unit 文件:
445
+
446
+ ```ini
447
+ [Unit]
448
+ Description=minicoin
449
+ After=network.target
450
+
451
+ [Service]
452
+ User=app
453
+ WorkingDirectory=/opt/minicoin
454
+ ExecStart=/opt/minicoin/server
455
+ Restart=on-failure
456
+ RestartSec=3
457
+ Environment=APP_ENV=prod
458
+ LimitNOFILE=65535
459
+
460
+ [Install]
461
+ WantedBy=multi-user.target
179
462
  ```
180
- 2. 公钥未加入 `authorized_keys`;`~/.ssh` 或公钥权限过宽(应为 700/600);ssh config 指错 IdentityFile;服务端禁用该用户或 `PubkeyAuthentication no`;密钥类型不被服务端支持。验证:`ssh -vvv devbox` 看 `Offering public key` 与 `Authentications that can continue`;`ssh devbox 'ls -ld ~ ~/.ssh ~/.ssh/authorized_keys'`。
181
- 3. `lsof +L1` 找 deleted 但被进程占用的文件;常见是应用日志被 `rm` 后进程仍持有 fd。
182
- 4. 正常。page cache 会占用 free,`available` 估算可回收部分。真紧张看 `available` 和 swap 使用。
183
- 5. 有明显排队/过载。还要看 `top` 的 `%us/%sy/%wa`、`iostat -x 1` 的 `%util` 和 await。
184
- 6. `ss -lntp | grep :8080` 或 `lsof -i:8080`。
185
- 7. `rm` 只删目录项,进程仍持有 fd,空间不释放;`truncate` 直接把文件长度置 0,立即释放。
186
- 8. `curl -sS -o /dev/null -w '%{http_code} %{time_total}\n' https://api.example.com/healthz`
463
+
464
+ 启用并验证:
465
+
466
+ ```bash
467
+ sudo systemctl daemon-reload
468
+ sudo systemctl enable --now minicoin
469
+ sudo systemctl status minicoin
470
+ journalctl -u minicoin -n 50 --no-pager
471
+ ```
472
+
473
+ 预期进程处于 `active (running)`,日志中没有配置或端口错误。配置错时先修 unit 再 reload;端口占用时先查监听者,不要靠反复 restart。
474
+
475
+ 13. 先保留 SSH 会话,按下面顺序取证据:
476
+
477
+ ```bash
478
+ uptime
479
+ nproc
480
+ free -h
481
+ df -h
482
+ top -b -n1 | head -30
483
+ ps -eo pid,ppid,stat,%cpu,%mem,rss,etime,cmd --sort=-%cpu | head -20
484
+ ps -eo pid,ppid,stat,%cpu,%mem,rss,etime,cmd --sort=-%mem | head -20
485
+ iostat -xz 1 3
486
+ ss -s
487
+ dmesg -T | tail -50
488
+ journalctl -n 100 --no-pager
489
+ ```
490
+
491
+ 判断逻辑:`load` 高且 `%us` 高,查 CPU 消耗进程;`%wa` 高且 `iostat` 的 `%util`/await 高,查磁盘 IO;`available` 低且 RSS 高,查内存泄漏或被回收的缓存;`df` 满,查日志和已删除但未释放的文件;`dmesg` 出现 OOM killer,先保留证据再考虑扩容或重启。命令权限不足时记录 `sudo` 限制,不能把“没权限”误写成“机器正常”。
492
+
187
493
  </details>