提交 1e8018f7 authored 作者: 陈泽健's avatar 陈泽健

docs(自动化部署): ujava2 脚本死锁问题处理与 fastdfs 临时目录分析报告

- _PRD_ujava2脚本死锁导致日志停止_问题处理及计划执行
- fastdfs_start 问题分析报告
- fastdfs 重启问题业主说明(两版)
上级 8a91d8c5
# _PRD_ujava2脚本死锁导致日志停止_问题处理
> 脚本来源:
- `自动化部署脚本/x86架构/新统一平台/定时脚本/ujava2-startup.sh`
- 服务器部署路径:`/data/services/scripts/ujava2-startup.sh`(192.168.5.70)
## 1. 背景与目标
### 1.1 背景
服务器 192.168.5.70(新统一平台)上,定时脚本 `ujava2-startup.sh` 通过 crontab 每 3 分钟执行一次(`*/3 * * * *`)。
用户发现 `ujava2-service-manager.log` 日志一直不更新,怀疑脚本异常停止。
### 1.2 目标
1. 定位脚本日志停止的真正根因
2. 从源头修复,确保脚本每次 cron 触发都能正常完整执行
3. 产出问题处理文档与计划执行文档,同步仓库脚本与部署脚本
---
## 2. 问题报错信息
### 2.1 现象一:服务日志停止更新
```
# /data/logs/ujava2-service-manager.log 最后修改时间停留在 13:26,之后不再更新
-rw-r--r-- 1 root root 1795 9月 1 13:26 /data/logs/ujava2-service-manager.log
```
### 2.2 现象二:cron 日志每 3 分钟打印"另一个实例正在运行"
```
# /var/log/ujava2-cron.log
2026-09-01 13:26:00 [WARNING] 6060端口未监听,将启动malan服务
2026-09-01 13:26:24 [SUCCESS] malan 启动成功,6060端口已监听
===== 2026-09-01 13:26:24 操作完成 =====
2026-09-01 13:27:02 [WARNING] 另一个实例正在运行,退出 ← 从此刻起约 2 小时全部如此
2026-09-01 13:30:01 [WARNING] 另一个实例正在运行,退出
...(持续到 15:15 诊断时仍在重复)
```
### 2.3 现象三:cron 日志混入 `tee` 报错(部署旧版本的附加缺陷)
```
tee: '': 没有那个文件或目录
2026-09-01 14:18:01 [WARNING] 另一个实例正在运行,退出
```
### 2.4 现象四:flock 锁被 malan 进程持有
```
# fuser -v /tmp/ujava2-startup.lock
/tmp/ujava2-startup.lock:
root 70515 F.... malan ← PID 70515 为 ./malan 守护进程
# malan 进程 13:26:00 被脚本拉起,与锁释放的时间点完全吻合
ps -o pid,lstart,cmd -p 70515
PID STARTED CMD
70515 Tue Sep 1 13:26:00 2026 ./malan
# malan 进程 fd 200 确实持有锁
ls -la /proc/70515/fd | grep 200
l-wx------ 1 root root 64 9月 1 15:16 200 -> /tmp/ujava2-startup.lock
```
---
## 3. 问题解决分析
### 3.1 问题根因
**直接根因:后台子进程继承 flock 文件锁,导致死锁永久占锁。**
1. 脚本开头用 `exec 200>"$LOCK_FILE"` + `flock -n 200` 实现并发控制
2. 13:26 脚本检测到 6060 端口未监听,调用 `start_malan_service`
3. 该函数执行 `bash -c "source /etc/profile && ./malan" &` **后台启动 malan**
4. 后台子进程(`bash -c``./malan`**完整继承父脚本的 fd 200 锁**
5. malan 是长期驻留的守护进程 `./malan`(PID 70515,从 13:26 一直运行到现在),**永不退出**
6. 父脚本执行完后自身退出,但 malan 依然持有 fd 200 → **锁被永久占用**
7. 此后每 3 分钟 cron 触发脚本,`flock -n` 失败,打印"另一个实例正在运行,退出"后 exit 1
8. 脚本主流程(API 检查、服务检查、重启、6060 监测)**全部不再执行** → 服务日志停止更新
**附加缺陷:部署版脚本 `LOG_FILE` 定义在 flock 之后。**
- 服务器部署版本中 `LOG_FILE="/data/logs/ujava2-service-manager.log"` 在 flock 代码块之后
- 若 flock 分支先执行,`$LOG_FILE` 为空 → `tee -a ""``tee: '': 没有那个文件或目录`
- 本地仓库脚本已修复(LOG_FILE 提到 flock 之前 + mkdir),部署版是旧版本
**版本差异确认:**
- 本地仓库脚本(source of truth)已含全部修复:`start_malan_service`/`restart_host_extapi_service` 后台启动前均有 `exec 200>&-` 关闭锁 fd,`LOG_FILE` 在 flock 前定义
- 服务器部署的是旧版本脚本,缺失以上修复(diff 确认)
- 其余差异(`RETRY_INTERVAL` 20 vs 30、`stop_service` kill 逻辑)为既有配置差异,与本次问题无关,未改动
### 3.2 涉及文件
| 文件 | 说明 |
|------|------|
| 服务器 `/data/services/scripts/ujava2-startup.sh` | 部署脚本(旧版,缺三处修复) |
| 服务器 `/data/services/scripts/ujava2-startup.sh.bak.20260901` | 修复前备份 |
| 本地 `自动化部署脚本/x86架构/新统一平台/定时脚本/ujava2-startup.sh` | 仓库脚本(已含修复,无需改动) |
| 服务器 `/tmp/ujava2-startup.lock` | flock 锁文件(被 malan 持有) |
| 服务器 `/data/logs/ujava2-service-manager.log` | 服务日志 |
| 服务器 `/var/log/ujava2-cron.log` | cron 输出日志 |
### 3.3 代码验证
**修复前(服务器部署版,缺关闭 fd):**
```bash
start_malan_service() {
...
# 加载环境变量,然后启动malan
bash -c "source /etc/profile && ./malan" &
local start_pid=$!
...
}
```
`./malan` 继承 fd 200 锁 → 永久占锁。
**修复后(与仓库脚本一致):**
```bash
start_malan_service() {
...
# 关闭锁文件描述符,防止子进程继承导致锁无法释放
exec 200>&-
# 加载环境变量,然后启动malan
bash -c "source /etc/profile && ./malan" &
...
}
```
`restart_host_extapi_service` 同样在 `bash -c "... ./run.sh" &` 前加 `exec 200>&-`
`LOG_FILE` 定义移至 flock 之前并加 `mkdir -p "$(dirname "$LOG_FILE")"`
**验证结果(2026-09-01 15:46-15:54):**
- 手动执行修复后脚本:`EXIT_CODE=0`,完整跑通 API 检查、服务自愈、6060 监测
- malan 被重启为新进程 PID 128794,**不再持有 /tmp/ujava2-startup.lock**
- 下一轮 cron(15:51、15:54)日志继续正常完整执行,锁在执行完毕后自动释放
- `fuser /tmp/ujava2-startup.lock` 在 cron 运行间隙无输出(无持锁进程)
### 3.4 解决方案
| 步骤 | 操作 | 状态 |
|------|------|------|
| 1 | 备份服务器脚本 `cp ujava2-startup.sh ujava2-startup.sh.bak.20260901` | ✅ 完成 |
| 2 | 服务器脚本 `start_malan_service` 后台启动前加 `exec 200>&-` | ✅ 完成(与仓库版一致) |
| 3 | 服务器脚本 `restart_host_extapi_service` 后台启动前加 `exec 200>&-` | ✅ 完成(与仓库版一致) |
| 4 | `LOG_FILE` 定义移至 flock 之前 + `mkdir -p` | ✅ 完成(与仓库版一致) |
| 5 | `bash -n` 语法校验 | ✅ SYNTAX_OK |
| 6 | 释放死锁:kill 持锁 malan(70515),脚本自动拉起新 malan | ✅ 完成 |
| 7 | 手动执行一次验证 + 等待两轮 cron 确认自动恢复 | ✅ 完成 |
### 3.5 预防措施
1. **凡是脚本内用 `flock` 加锁后再 `&` 后台启动的守护进程,启动前必须先 `exec 200>&-` 关闭锁 fd**——否则锁被子进程继承后永久占用
2. 新增定时脚本统一模板:`LOG_FILE` 在 flock 之前定义 + `mkdir -p`,避免 flock 分支日志写空路径
3. 服务器脚本事后需与仓库脚本 diff 对齐:本次回填前发现服务器部署版落后于仓库版(缺三处修复),后续部署流程应校验差异
4. 排查"脚本不打印日志"问题时,优先检查两类信号:
- 锁文件持有者:`fuser -v /tmp/ujava2-startup.lock`(死锁从 13:27 即被锁,日志已明确提示"另一个实例正在运行",是脚本自身的保护机制在如实工作)
- cron 日志 `/var/log/ujava2-cron.log` 最后几行,比服务日志更能反映脚本真实状态
### 3.6 问题状态
- [x] 定位根因
- [x] 问题文档输出
- [x] 计划执行文档输出
- [x] 服务器脚本修复并验证(cron 自动恢复)
- [x] 死锁释放
- [ ] 仓库脚本与服务器脚本完全对齐(含 `RETRY_INTERVAL`/`stop_service` 差异确认,非本次问题范围)
### 规范文档
- 代码规范: `Docs/PRD/01规范文档/_PRD_规范文档_代码规范.md`
- 问题总结: `Docs/PRD/01规范文档/_PRD_问题总结_记录文档.md`
- 方法总结: `Docs/PRD/01规范文档/_PRD_方法总结_记录文档.md`
- 文档规范: `Docs/PRD/01规范文档/_PRD_规范文档_文档规范.md`
- 测试规范: `Docs/PRD/01规范文档/_PRD_规范文档_测试规范.md`
- 历史同类问题: `Docs/PRD/自动化部署脚本/新统一平台/问题处理/java监测脚本执行异常_问题处理.md`
---
*文档结束*
\ No newline at end of file
# ujava2监测脚本flock死锁导致日志停止 - 问题处理计划执行文档
> PRD来源: `Docs/PRD/自动化部署脚本/新统一平台/问题处理/_PRD_ujava2脚本死锁导致日志停止_问题处理.md`
> 脚本: `自动化部署脚本/x86架构/新统一平台/定时脚本/ujava2-startup.sh`
> 服务器: 192.168.5.70 `/data/services/scripts/ujava2-startup.sh`
---
## 1. 问题分析
### 1.1 问题描述
crontab 每 3 分钟执行的 `ujava2-startup.sh`,服务日志 `/data/logs/ujava2-service-manager.log` 自 13:26 后停止更新。
### 1.2 问题日志片段
```
2026-09-01 13:26:00 [WARNING] 6060端口未监听,将启动malan服务
2026-09-01 13:26:24 [SUCCESS] malan 启动成功,6060端口已监听
===== 2026-09-01 13:26:24 操作完成 =====
2026-09-01 13:27:02 [WARNING] 另一个实例正在运行,退出
tee: '': 没有那个文件或目录
2026-09-01 13:30:01 [WARNING] 另一个实例正在运行,退出
...(持续重复至 15:15 诊断时)
```
### 1.3 根因分析
| 问题点 | 说明 |
|--------|------|
| 死锁根因 | `start_malan_service` 执行 `bash -c "source /etc/profile && ./malan" &` 后台启动 malan,子进程**继承 fd 200(flock 锁)**。malan 为常驻守护进程永不退出 → 锁被永久占用 |
| 触发时机 | 13:26 脚本检测到 6060 端口未监听 → 调用 `start_malan_service` → malan(PID 70515)持锁,与用户确认的"13:26 自动化部署完毕"时间完全吻合 |
| 连锁反应 | 此后每 3 分钟 cron 触发 `flock -n 200` 失败 → 打印 WARNING 后 exit 1 → 主流程(API 检查/服务检查/自愈/6060 监测)全部不执行 |
| 附加缺陷 | 部署旧版脚本 `LOG_FILE` 定义在 flock 之后 → `tee -a "$LOG_FILE"` 写空路径报 `tee: '': 没有那个文件或目录` |
| 证据 | `fuser -v /tmp/ujava2-startup.lock` → PID 70515 (malan);`ls -la /proc/70515/fd | grep 200``200 -> /tmp/ujava2-startup.lock`;malan 启动时间 13:26:00 |
---
## 2. 修复方案
### 2.1 方案设计(已采纳:修复脚本 + 释放锁)
**原则:从源头修复,与本地仓库脚本(source of truth)对齐,而非仅临时释放锁。**
| 修改点 | 内容 | 目的 |
|--------|------|------|
| 修改点1 | `start_malan_service``./malan &` 前加 `exec 200>&-` | 关闭锁 fd,防止 malan 继承锁 |
| 修改点2 | `restart_host_extapi_service``./run.sh &` 前加 `exec 200>&-` | 同上(host extapi 路径同样有此隐患) |
| 修改点3 | `LOG_FILE` 定义移至 flock 之前 + `mkdir -p` | 修复 `tee: ''` 报错 |
| 释放锁 | kill 持锁的旧 malan(PID 70515) | 解除当前死锁,脚本自身会重新拉起 malan |
### 2.2 代码修改点
#### 变更1: start_malan_service 关闭锁 fd
```bash
# 修改前
# 加载环境变量,然后启动malan
bash -c "source /etc/profile && ./malan" &
# 修改后
# 关闭锁文件描述符,防止子进程继承导致锁无法释放
exec 200>&-
# 加载环境变量,然后启动malan
bash -c "source /etc/profile && ./malan" &
```
#### 变更2: restart_host_extapi_service 关闭锁 fd
```bash
# 修改前
bash -c "source /etc/profile && ./run.sh" &
# 修改后
# 关闭锁文件描述符,防止子进程继承导致锁无法释放
exec 200>&-
bash -c "source /etc/profile && ./run.sh" &
```
#### 变更3: LOG_FILE 定义前置
```bash
# 修改前(flock 在前,LOG_FILE 在后)
LOCK_FILE="/tmp/ujava2-startup.lock"
exec 200>"$LOCK_FILE"
flock -n 200 || { ... tee -a "$LOG_FILE" ... }
LOG_FILE="/data/logs/ujava2-service-manager.log"
# 修改后(与仓库版一致)
LOCK_FILE="/tmp/ujava2-startup.lock"
LOG_FILE="/data/logs/ujava2-service-manager.log"
# 确保日志目录存在
mkdir -p "$(dirname "$LOG_FILE")" 2>/dev/null
exec 200>"$LOCK_FILE"
flock -n 200 || {
echo "$(date '+%Y-%m-%d %H:%M:%S') [WARNING] 另一个实例正在运行,退出" | tee -a "$LOG_FILE"
exit 1
}
```
---
## 3. 执行计划
### 3.1 修改清单
- [x] 步骤1: 备份服务器脚本为 `ujava2-startup.sh.bak.20260901`
- [x] 步骤2: 变更1 — `start_malan_service``exec 200>&-`
- [x] 步骤3: 变更2 — `restart_host_extapi_service``exec 200>&-`
- [x] 步骤4: 变更3 — `LOG_FILE` 前置 + `mkdir -p`
- [x] 步骤5: `bash -n` 语法校验(SYNTAX_OK)
- [x] 步骤6: kill 持锁 malan(PID 70515)释放死锁
- [x] 步骤7: 手动执行脚本验证完整流程
- [x] 步骤8: 等待 cron 自动触发验证恢复
- [x] 步骤9: 问题处理文档 + 本计划执行文档输出
### 3.2 验证结果(2026-09-01 15:46 ~ 15:54)
| 验证项 | 结果 |
|--------|------|
| 手动执行脚本 | ✅ EXIT_CODE=0,完整跑通 API 检查、服务自愈、6060 监测 |
| 新 malan 进程 | ✅ PID 128794 正常运行,**不再持有 /tmp/ujava2-startup.lock** |
| cron 自动触发 | ✅ 15:51、15:54 两轮日志正常完整执行(`6060端口正在监听`),无 WARNING 退出 |
| 锁释放 | ✅ cron 运行间隙 `fuser /tmp/ujava2-startup.lock` 无输出 |
| tee 报错 | ✅ 修复后 cron 日志无 `tee: '': 没有那个文件或目录` |
### 3.3 回滚方案
```bash
# 若修复后异常,恢复备份
cp /data/services/scripts/ujava2-startup.sh.bak.20260901 /data/services/scripts/ujava2-startup.sh
```
---
## 4. 优化功能回填
| 优化项 | 状态 | 说明 |
|--------|------|------|
| start_malan_service 关闭锁 fd | 已实施 | `exec 200>&-` 防止 malan 继承 flock 锁 |
| restart_host_extapi_service 关闭锁 fd | 已实施 | 同上,消除 host extapi 路径同类隐患 |
| LOG_FILE 定义前置 | 已实施 | 修复 flock 分支 `tee -a ""` 报错 |
| 死锁释放与验证 | 已实施 | kill 旧 malan + 手动执行 + 两轮 cron 确认自动恢复 |
| 预防规范 | 已沉淀 | flock 脚本后台启动守护进程前必须关闭锁 fd(详见问题处理文档 3.5 预防措施) |
| 仓库/服务器脚本对齐 | 待实施 | 服务器剩余差异(RETRY_INTERVAL 20/30、stop_service kill 逻辑)为旧版配置差异,非本次问题范围,待后续部署流程统一 |
---
*文档生成时间: 2026-09-01*
# fastdfs_start.sh 问题分析报告
> 分析对象:`自动化部署脚本/arm架构/临时目录/fastdfs_start.sh`
> 分析日期:2026-07-13
> 结论:**存在容器重启后 storage / tracker 不启动的隐患**,且不止一处会触发该故障。
---
## 一、结论先行
脚本最大的风险点是 `fdfs_trackerd ... restart` / `fdfs_storaged ... restart`**重启语义在容器环境(PID1、无 init 系统)下不可靠**。在镜像首次启动、容器重启、pid 残留等多种场景下,`restart``stop` 阶段行为不确定,会导致 tracker / storage 进程没起来,而脚本又没有错误检查与守护机制,最终表现为:
- 容器进程在跑(`tail -f /dev/null` 保活),但 FastDFS 服务全挂;
- 或脚本中途退出,容器直接 `Exited`,配合 `--restart=always` 陷入重启死循环。
---
## 二、问题清单(按严重程度分级)
### 🔴 P0 严重 —— 直接导致重启后服务起不来
#### 问题 1:`restart` 语义不可靠(最可能的根因)
**位置:** 第 35、38 行
```bash
/usr/bin/fdfs_trackerd /etc/fdfs/tracker.conf restart
/usr/bin/fdfs_storaged /etc/fdfs/storage.conf restart
```
`fdfs_trackerd` / `fdfs_storaged` 实际是 wrapper 脚本,`restart` = `stop` + `start`。问题在于:
| 场景 | stop 阶段行为 | 后果 |
|------|--------------|------|
| **镜像首次启动**(进程从未运行) | 无 pid 文件,很多版本返回非零退出 | `start` 不一定被执行 → tracker 没起来 |
| **容器重启后**(PID 命名空间全新,进程已死,pid 文件可能残留) | 尝试 kill 不存在的 pid | 部分版本静默继续,部分版本直接退出 |
| **配置持久化(volume)** | 残留的旧 pid 指向已死进程 | kill 失败,行为不确定 |
**修复方向:**`start` 替代 `restart`,并做幂等兜底:
```bash
fdfs_trackerd /etc/fdfs/tracker.conf stop 2>/dev/null || true
fdfs_trackerd /etc/fdfs/tracker.conf start
fdfs_storaged /etc/fdfs/storage.conf stop 2>/dev/null || true
fdfs_storaged /etc/fdfs/storage.conf start
```
---
#### 问题 2:脚本中途任一步失败会直接退出,导致容器 Exited
**位置:** 第 35、38、41、45 行
```bash
/usr/local/nginx/sbin/nginx # 第41行:若启动失败(端口占用/配置错),脚本退出
tail -f /dev/null # 第45行:容器靠它保活,前面挂了到不了这里
```
`tail -f /dev/null` 是容器内**唯一的 PID1 守护进程**。tracker / storage / nginx 任一启动失败退出,脚本立即终止 → 容器 `Exited`。配合 `--restart=always` 会反复重启失败,尤其 nginx 端口冲突会陷入死循环。
---
### 🟠 P1 高 —— 健壮性与幂等性
#### 问题 3:全程无 `set -e`,无单步错误检查
脚本缺少 `set -euo pipefail`,每个关键命令(sed / fdfs_trackerd / nginx)执行后都没判断 `$?`
- 第 28-32 行的 sed 替换若失败(配置文件路径在某版本镜像中不同),tracker / storage 会带旧 IP 启动;
- storage 连不上 tracker,但脚本继续往下跑,最后仍打印 `fastdfs start success` —— **假性成功**,排查时极易误判。
---
#### 问题 4:sed 替换"一次成型",不可重入(容器二次启动出问题)
**位置:** 第 6、28-30 行
```bash
old_fastdfs_ipaddr="192.168.0.165"
sed -i "s/$old_fastdfs_ipaddr/$new_fastdfs_ipaddr/g" /etc/fdfs/storage.conf
```
| 启动阶段 | 配置文件内容 | sed 行为 |
|---------|-------------|---------|
| 第一次启动 | `192.168.0.165` | 替换成 `$FASTDFS_IPADDR` ✅ |
| 重启后再跑(配置未重置) | `$FASTDFS_IPADDR`(旧 IP 已被替换) | `192.168.0.165` 不存在 → **啥也不替换** ❌ |
**更严重的副作用:** storage.conf 的 `tracker_server` 一旦被替换成具体 IP,storage 会把当前 IP 当成 tracker 地址。若 `FASTDFS_IPADDR` 传的是 storage 自己的 IP(常见用法),storage 会找不到 tracker,进程起来但持续报:
```
tracker server can't be reached
```
最终 storage 不可用。
**修复方向:** 替换整行配置(用占位符 / 行匹配),而非替换"上一次的 IP":
```bash
sed -i "s/^tracker_server=.*/tracker_server=$FASTDFS_IPADDR:22122/" /etc/fdfs/storage.conf
sed -i "s/^tracker_server=.*/tracker_server=$FASTDFS_IPADDR:22122/" /etc/fdfs/mod_fastdfs.conf
```
---
#### 问题 5:nginx 启动无幂等
**位置:** 第 41 行
```bash
/usr/local/nginx/sbin/nginx
```
- 容器重启场景(PID1 重置、nginx 不在):此条正常;
- 但脚本被手动 `docker exec` 再跑一次:nginx 已在运行 → 端口占用退出。
**修复方向:** 加幂等判断:
```bash
if pgrep -x nginx > /dev/null; then
nginx -s reload
else
nginx
fi
```
---
### 🟡 P2 中 —— 规范性问题
#### 问题 6:`exit` 无状态码
**位置:** 第 17 行
```bash
exit # 没指定退出码,依赖上一条 echo 的返回值(0)
```
参数校验失败应明确 `exit 1`,让 dockerd 能识别失败、触发重启策略。
---
#### 问题 7:`WEB_PORT` 校验形同虚设
**位置:** 第 22-26 行 + 第 32 行
第 22-26 行只在 `WEB_PORT` 为空时打印 WARN,但第 32 行 sed 仍会用**空串**替换:
```bash
sed -i "s/$old_web_port/$new_web_port/g" /usr/local/nginx/conf/nginx.conf
# new_web_port 为空时,会把 nginx.conf 里所有 8888 删成空 → 监听配置损坏
```
**修复方向:** 为空时用默认值:
```bash
new_web_port="${WEB_PORT:-8888}"
```
---
#### 问题 8:`mkdir -p /home/fastdfs` 无意义
**位置:** 第 3 行
创建的目录后续没被使用。FastDFS 数据目录在 storage.conf 的 `store_path0`(通常是 `/fastdfs/storage`)。要么删除该行,要么应与 storage.conf 的 store_path 保持一致。
---
## 三、针对核心问题的直接回答
> **有没有可能存在容器重启后出现 storage 和 tracker 服务没启动的问题?**
**有可能,且概率不低。** 有两条主要重现路径:
### 路径 A:restart 语义路径(最常见)
```
容器重启
→ 脚本重跑
→ fdfs_trackerd ... restart 的 stop 阶段找不到活动进程/pid,行为异常
→ tracker 没起来
→ storage 因连不上 tracker 退出
→ nginx 可能起来但后端无 storage
→ 现象:容器在跑,文件上传全失败(假性运行)
```
### 路径 B:sed 幂等路径(配置持久化场景)
```
容器重启(配置目录持久化)
→ 脚本重跑
→ sed 替换不到占位符(已被上次替换成实 IP)
→ 配置错位 / tracker_server 指向错误 IP
→ storage 起不来
```
---
## 四、重启后排查步骤
```bash
# 1. 看进程在不在
docker exec -it <容器名> ps -ef | grep fdfs
# 2. 看 tracker_server 配置是否正确
docker exec -it <容器名> grep tracker_server /etc/fdfs/storage.conf
# 3. 看启动日志有没有关键报错
docker logs <容器名> | grep -E "tracker|storage|can't be reached|pid"
```
**典型故障日志:**
- `tracker server can't be reached` → storage 连不上 tracker(路径 A/B)
- `file pid ... not exist` 或 stop 报错 → restart 语义问题(路径 A)
- 容器状态 `Exited (1)` + 反复重启 → 脚本中途退出(问题 2)
---
## 五、建议修复版脚本
```bash
#!/bin/bash
set -euo pipefail
# ===== 参数处理(带默认值)=====
new_fastdfs_ipaddr="${FASTDFS_IPADDR:?[ERROR] FASTDFS_IPADDR is blank, please pass via -e FASTDFS_IPADDR=x.x.x.x}"
new_web_port="${WEB_PORT:-8888}"
echo "[INFO] FASTDFS_IPADDR=${new_fastdfs_ipaddr}"
echo "[INFO] WEB_PORT=${new_web_port}"
mkdir -p /home/fastdfs
# ===== 配置替换(用行匹配,幂等可重入)=====
sed -i "s/^tracker_server=.*/tracker_server=${new_fastdfs_ipaddr}:22122/" /etc/fdfs/client.conf
sed -i "s/^tracker_server=.*/tracker_server=${new_fastdfs_ipaddr}:22122/" /etc/fdfs/storage.conf
sed -i "s/^tracker_server=.*/tracker_server=${new_fastdfs_ipaddr}:22122/" /etc/fdfs/mod_fastdfs.conf
# WEB_PORT 用占位符替换更安全;此处保留原 8888 行匹配
sed -i "s/^.*listen .*8888.*;/ listen ${new_web_port};/" /usr/local/nginx/conf/nginx.conf
# ===== 启动 FastDFS(stop 兜底 + start,避免 restart 语义问题)=====
echo "start trackerd"
fdfs_trackerd /etc/fdfs/tracker.conf stop 2>/dev/null || true
fdfs_trackerd /etc/fdfs/tracker.conf start
echo "start storage"
fdfs_storaged /etc/fdfs/storage.conf stop 2>/dev/null || true
fdfs_storaged /etc/fdfs/storage.conf start
# ===== 启动 nginx(幂等)=====
echo "start nginx"
if pgrep -x nginx > /dev/null 2>&1; then
nginx -s reload
else
nginx
fi
echo 'fastdfs start success........'
# ===== 容器保活 =====
tail -f /dev/null
```
---
## 六、问题汇总表
| 编号 | 等级 | 问题 | 位置 | 影响 |
|------|------|------|------|------|
| 1 | 🔴 P0 | `restart` 语义不可靠 | 35、38 行 | 重启后 tracker/storage 不启动(假性运行) |
| 2 | 🔴 P0 | 脚本中途失败会退出,容器 Exited | 35-45 行 | 任一服务失败 → 容器退出 / 死循环重启 |
| 3 | 🟠 P1 | 无 `set -e`,无错误检查 | 全局 | 假性成功,难以排查 |
| 4 | 🟠 P1 | sed 替换不可重入 | 6、28-30 行 | 重启后配置错位 / tracker_server 指向错 |
| 5 | 🟠 P1 | nginx 启动无幂等 | 41 行 | 手动重跑脚本失败 |
| 6 | 🟡 P2 | `exit` 无状态码 | 17 行 | dockerd 无法识别失败 |
| 7 | 🟡 P2 | `WEB_PORT` 校验形同虚设 | 22-32 行 | 不传参时 nginx.conf 监听损坏 |
| 8 | 🟡 P2 | `mkdir /home/fastdfs` 无意义 | 3 行 | 死代码 / 易混淆 |
# FastDFS 容器重启后服务不启动 —— 业主说明(两版)
> 场景:业主反馈 FastDFS 文件存储服务在容器/宿主机重启后出现不可用,需要给出原因解释。
> 用途:根据沟通对象选择对应版本。
---
## 版本一:专业版(面向技术负责人 / 运维 / 验收方)
### 1. 问题概述
FastDFS 部署采用 Docker 容器方式,容器内启动脚本 `fastdfs_start.sh` 在启动 tracker、storage、nginx 三个核心进程时使用了 `restart` 参数。在容器重启场景下,该参数的行为不可靠,存在导致 tracker / storage 进程未正常拉起的风险,最终表现为文件上传/下载功能不可用。
### 2. 根因分析
#### 2.1 `restart` 语义在容器环境下不可靠
FastDFS 的 `fdfs_trackerd` / `fdfs_storaged` 实际是 wrapper 脚本,`restart` 指令等价于 `stop` + `start` 两步操作。在容器重启场景下:
- 容器重启后,容器内进程命名空间是全新的,tracker / storage 进程均不在运行;
-`/etc/fdfs/` 下的 pid 文件可能仍然残留(指向已不存在的进程);
- `stop` 阶段尝试停止一个不存在的 pid,在部分 FastDFS 版本中会返回非零退出码;
- wrapper 脚本对 `stop` 失败的处理不一致,可能导致后续 `start` 步骤未执行;
- 结果:tracker 未启动 → storage 无法连接 tracker → storage 退出 → FastDFS 服务整体不可用。
#### 2.2 启动脚本缺少错误处理与进程守护
- 脚本未启用 `set -e`,各启动命令执行后未做返回值检查,无法在单步失败时中止并告警;
- 容器依赖 `tail -f /dev/null` 作为 PID1 保活进程,一旦 tracker / storage / nginx 任一启动失败导致脚本退出,容器会进入 `Exited` 状态;
- 在配置 `--restart=always` 策略下,若失败由端口冲突等持续性原因导致,容器会陷入反复重启的死循环。
#### 2.3 配置替换逻辑不可重入
脚本通过 `sed` 将配置文件中硬编码的旧 IP 替换为 `FASTDFS_IPADDR` 环境变量值。在配置目录持久化(volume 挂载)的场景下,容器二次启动时旧 IP 已被替换,sed 无可替换内容,可能导致 `tracker_server` 配置错位,进一步导致 storage 无法连接 tracker。
### 3. 影响范围
| 影响项 | 说明 |
|--------|------|
| 受影响功能 | 文件上传、下载、访问(nginx 代理) |
| 触发条件 | 容器重启 / 宿主机重启后容器自启 |
| 故障特征 | 容器状态可能为 Running(假性运行)或 Exited |
| 数据影响 | 不影响已存储文件数据本身,仅影响服务可用性 |
### 4. 修复方案
1.`restart` 改为 `stop`(容错)+ `start`,避免 stop 失败阻断 start;
2. 启用 `set -euo pipefail`,单步失败立即退出并返回非零状态码,便于 dockerd 识别与重启;
3. 配置替换改用"行匹配替换"而非"IP 替换",保证脚本幂等可重入;
4. nginx 启动增加进程存在性判断,避免重复启动报错;
5. 参数校验失败时明确返回 `exit 1`
### 5. 验证方式
- 容器重启后执行 `docker exec <容器> ps -ef | grep fdfs`,确认 tracker / storage 进程存在;
- 检查 `docker logs` 是否出现 `tracker server can't be reached` 或 pid 相关报错;
- 通过实际上传文件验证端到端可用性。
---
## 版本二:概要通俗易懂版(面向项目业主 / 业务方 / 非技术决策者)
### 问题说明
文件存储服务(FastDFS)在服务器重启或容器重启后,偶尔会出现文件上传下载暂时用不了的情况。这个问题我们已经找到了原因,修复方案也明确了。
### 请放心
**所有已存储的文件都是安全的,不会丢失。**
只是重启后服务需要重新启动一下,目前这个"自动启动"的流程有一处小瑕疵,导致偶尔没启动成功。修复后就能稳定恢复。
### 问题原因(简单理解)
可以把它理解成"开机自启的手势顺序有点问题"。
容器启动时会依次启动三个程序,之前用的是"**重启**"指令。重启后程序本来都没在运行,但脚本先去执行了"停止"这一步,因为找不到运行中的程序就卡住了,后面的"启动"步骤就没执行到。
打个比方:就像一个人去按电梯,他先按了"关门"再按"开门",但电梯本来就是关着的,关门键按了没反应,他就走了,没按开门键 —— 门一直关着进不去。
### 范围说明
- **受影响的功能**:只涉及文件上传和下载,其他功能不受影响;
- **触发条件**:仅发生在服务器重启或容器重启后,正常运行时不受影响;
- **数据安全****已存储的所有文件完好无损**,问题修复后即可正常访问;
- **不是硬件故障**:不是服务器坏了,也不是硬盘坏了,只是启动脚本的一处逻辑需要调整。
### 修复方案
把启动脚本里的"重启"指令换成"直接启动",并加上简单的容错判断。改动很小,修复后重启就不会再出现这个问题了。
### 验证方式
修复后重启一次容器,上传一个测试文件,确认能正常上传和下载,就说明修复完成。
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论