# HANDOFF — 服务监测模块：定时任务修复 + Docker 容器化

> 最后更新：2026-07-27 21:00 ｜ 分支：troubleshoot-ai-assistant ｜ 负责人：czj

---

## 1. 我们在做什么

服务监测模块（service_monitor）是运行维护平台的子模块，提供目标服务器巡检、报告管理、定时任务、通知推送等功能。

本轮会话完成了两个主要任务，启动了容器化部署：

1. **定时任务并发执行失败修复**：5.44 服务器定时任务（工作日 8:40）不执行，5.202 的 9:00 正常。根因是 APScheduler 的 `misfire_grace_time` 默认仅 1 秒，调度线程稍有延迟就跳过任务；加上 `_execute_scheduled_job` 在 APScheduler 线程中同步阻塞执行，加剧了 misfire。同时发现 `schedules.json` 并发写入无锁保护，多任务同时执行时可能丢失状态更新。

2. **Docker 容器化（阶段一）**：将服务从裸机部署（systemd/nohup + python3 直跑）迁移到 Docker 单容器部署。阶段一只做容器化，JSON 文件存储不变，通过 volume 持久化。SQLite 迁移（阶段二）留到下一轮。

---

## 2. 已经完成了什么

### 2.1 定时任务并发执行失败修复 ✅

**问题文档**：`Docs/需求文档/服务监测/PRD_问题处理_定时任务并发执行失败.md`
**计划文档**：`Docs/需求文档/服务监测/PRD_计划执行_定时任务并发执行失败修复.md`

**根因**：
- `_add_job()` 未设置 `misfire_grace_time`，APScheduler 默认 1 秒容错，调度线程稍有延迟就跳过任务
- `_execute_scheduled_job` 在 APScheduler 线程中同步阻塞执行巡检（可能数分钟），阻塞调度线程
- `schedules.json` 并发写入无锁保护，多任务同时执行时可能丢失状态更新

**修复内容**（5 处，均在 `schedule_service.py`）：

| # | 修复 | 位置 |
|---|------|------|
| 1 | 新增 `_schedules_lock` 模块级锁 | 第 30-31 行 |
| 2 | `_save_schedules()` 加锁 | 第 140 行 |
| 3 | `update_run_status()` 读-改-写整体加锁 | 第 363 行 |
| 4 | `_add_job()` 添加 `misfire_grace_time=3600`、`coalesce=True`、`max_instances=1` | 第 462-464 行 |
| 5 | `_execute_scheduled_job` 改为 `threading.Thread` 后台执行 + `current_status` 并发检查 | 第 520-536 行 |

**验证**：218 个单元测试全绿 ✅

### 2.2 Docker 容器化（阶段一） ✅

**核心文件**：

| 文件 | 说明 |
|------|------|
| `Dockerfile` | 基于 python:3.11-slim，单进程启动（不用 gunicorn，避免 APScheduler 多 worker 冲突），自动修复 .sh CRLF |
| `.dockerignore` | 排除测试/运行时数据/文档 |
| `docker-compose.yml` | 单容器 + volume 挂载 + 资源限制（1G/1CPU）+ 健康检查 |
| `deploy/docker_deploy.sh` | 5.60 部署脚本（前置检查→准备数据目录→构建镜像→启动容器→健康检查） |

**关键设计决策**：
- **不用 gunicorn 多 worker**：APScheduler 的 BackgroundScheduler 在多 worker 模式下会创建多个调度器实例，定时任务重复执行
- **不挂载 docker.sock**：5.60 上有其他服务容器，挂载 sock 会暴露宿主机所有容器控制权
- **资源限制**：`--memory=1g --cpus=1.0`，防止抢占其他容器资源
- **容器名 `troubleshoot`**：明确前缀，不与其他容器冲突

**Volume 挂载**：

| 容器路径 | 宿主机路径 | 说明 |
|---------|-----------|------|
| `/app/web/service_monitor/data` | `/opt/troubleshoot/monitor-data` | targets/schedules/reports |
| `/app/web/users.json` | `/opt/troubleshoot/data/users.json` | 用户数据 |
| `/app/data/搜索索引.json` | `/opt/troubleshoot/data/搜索索引.json` | 知识库索引（只读） |
| `/app/data/搜索向量.json` | `/opt/troubleshoot/data/搜索向量.json` | 向量索引（只读） |
| `/app/web/logs` | `/opt/troubleshoot/logs` | 日志 |

### 2.3 搜索引擎路径适配 ✅

**改动**：
- `utils/paths.py` 新增 `SEARCH_INDEX_DIR` 常量，支持 `SEARCH_INDEX_DIR` 环境变量覆盖，自动检测容器/裸机路径
- `search_engine.py` 改用 `utils/paths.py` 的 `SEARCH_INDEX_DIR` 替代硬编码路径列表

---

## 3. 当前卡在哪

**无代码卡点**。所有修复和容器化文件已完成，测试通过。

**待执行**：
- 5.60 上尚未部署容器化版本（需先确认 Docker 已安装）
- 5.44 定时任务 `end_date` 已过期（2026-07-22），即使修复部署后也不会触发，需通过 API 更新

---

## 4. 下一步计划

| 优先级 | 任务 | 说明 |
|--------|------|------|
| **P0** | 部署容器化版本到 5.60 | 1. SSH 确认 Docker 已安装；2. 准备 `/opt/troubleshoot/.env`；3. 执行 `docker_deploy.sh` |
| **P0** | 更新 5.44 定时任务 end_date | 通过 API 或前端更新，当前 end_date=2026-07-22 已过期 |
| P1 | 修复 APScheduler day_of_week 约定 bug | `CronTrigger.from_crontab('1-5')` 实际是周二到周六而非周一到周五（APScheduler 3.x 约定与标准 cron 不同），需改为直接构造 `CronTrigger(day_of_week='0-4')` |
| P2 | SQLite 迁移（阶段二） | 替换 JSON 文件存储，需：1. 新增 db.py 连接管理；2. 逐模块替换 target/schedule/report/notification service；3. 更新 55 个测试 fixture；4. 迁移脚本 |
| P2 | 迁移 auth.py 用户数据到 SQLite | UserManager 的 JSON 读写替换为 SQLite |
| P2 | 数据迁移脚本 | JSON → SQLite，保留 .bak 备份 |

---

## 5. 踩过的坑（绝对不要再踩）

### 坑 1：APScheduler misfire_grace_time 默认 1 秒

**坑**：`_add_job()` 不设置 `misfire_grace_time`，APScheduler 默认仅 1 秒容错。调度线程稍有延迟（GIL 竞争、垃圾回收等），任务就被标记为 misfire 跳过不执行。

**避免方法**：`_add_job()` 必须显式设置 `misfire_grace_time=3600`（1 小时容错）。

### 坑 2：`_execute_scheduled_job` 同步阻塞 APScheduler 线程

**坑**：定时触发直接在 APScheduler 线程中同步调用 `_run_job_body`，巡检可能耗时数分钟，阻塞调度线程导致后续任务 misfire。手动触发 `run_now()` 用了 `threading.Thread`，但定时触发没有。

**避免方法**：定时触发也用 `threading.Thread` 后台执行，与 `run_now()` 保持一致。

### 坑 3：JSON 文件并发写入无锁保护

**坑**：`_update_current_status()` 和 `update_run_status()` 采用"读-改-写"模式操作 `schedules.json`，但无锁保护。两个定时任务同时执行时，一个线程的写入可能被另一个线程覆盖。

**避免方法**：用 `threading.Lock()` 保护读-改-写操作。SQLite 迁移后 WAL 模式自带并发安全，可删除此锁。

### 坑 4：Docker 多 worker 与 APScheduler 不兼容

**坑**：旧 Dockerfile 用 `gunicorn -w 4`，APScheduler 的 BackgroundScheduler 在每个 worker 中各创建一个实例，导致定时任务重复执行 4 次。

**避免方法**：容器化用单进程 `python3 server.py` 启动，不用 gunicorn。

### 坑 5：5.60 上有其他服务容器

**坑**：5.60 上运行着其他服务的 Docker 容器，必须严格隔离。挂载 docker.sock 会暴露宿主机所有容器控制权，资源不限制会抢占其他容器。

**避免方法**：不挂载 docker.sock、设置 `--memory=1g --cpus=1.0`、volume 路径用 `/opt/troubleshoot/` 独占前缀、容器名 `troubleshoot` 明确区分。

### 坑 6：APScheduler 3.x day_of_week 约定与标准 cron 不同

**坑**：`CronTrigger.from_crontab('40 8 * * 1-5')` 中 `1-5` 按 APScheduler 约定是周二到周六，而非标准 cron 的周一到周五。导致"工作日"定时任务实际在周二到周六触发，周一不执行。

**避免方法**：不用 `from_crontab()`，改为直接构造 `CronTrigger(day_of_week='0-4')`（APScheduler 约定 0=周一，4=周五）。**待修复**。

---

## 6. 关键文件与命令速查

### 本次修改文件

| 文件 | 改动 |
|------|------|
| `skill/code/web/service_monitor/services/schedule_service.py` | 5 处并发修复：锁 + misfire_grace_time + 后台线程 |
| `skill/code/web/search_engine.py` | 搜索索引路径改用 `utils/paths.py` 的 `SEARCH_INDEX_DIR` |
| `skill/code/web/utils/paths.py` | 新增 `SEARCH_INDEX_DIR` 常量 |
| `Dockerfile` | 重写：python:3.11-slim + 单进程 + CRLF 修复 |
| `docker-compose.yml` | 重写：单容器 + volume + 资源限制 + 健康检查 |
| `.dockerignore` | 更新：补充 service_monitor/tests 等排除 |

### 本次新增文件

| 文件 | 说明 |
|------|------|
| `deploy/docker_deploy.sh` | 5.60 Docker 部署脚本 |
| `Docs/需求文档/服务监测/PRD_问题处理_定时任务并发执行失败.md` | 问题处理文档 |
| `Docs/需求文档/服务监测/PRD_计划执行_定时任务并发执行失败修复.md` | 计划执行文档 |

### 未提交文件

- `skill/code/web/users.json`（仅登录统计更新）
- `deploy/` 下多个测试/检查脚本（非正式部署文件）

### 常用命令

```bash
# 单元测试（218 用例，应全绿）
cd skill/code && python -m pytest -v

# 本地 Docker 构建
docker build -t troubleshoot:latest .

# 本地 Docker 启动
docker run -d --name troubleshoot -p 8088:8088 troubleshoot:latest

# 5.60 部署（在服务器上执行）
cd /path/to/project && bash deploy/docker_deploy.sh

# 健康检查
curl -s http://192.168.5.60:8088/api/health

# 查看容器日志
docker logs -f troubleshoot

# 查看容器状态
docker ps -f name=troubleshoot
```
