提交 65a4b306 authored 作者: 陈泽健's avatar 陈泽健

docs(AutomatedDeploymentFiles): 更新预定系统后端服务监测需求文档以及脚本实现

- 将原PRD文件从旧目录迁移至新目录结构
- 重构文档路径以符合项目规范
- 保持原有功能需求和技术规格不变
上级 2ac0eecb
{
"permissions": {
"allow": [
"Bash(npm install -g @modelcontextprotocol/server-chrome-devtools)"
"Bash(ls -la \"E:\\\\GithubData\\\\ubains-module-test\\\\自动化部署脚本\\\\x86架构\\\\新统一平台\\\\定时脚本\"\" 2>/dev/null || dir \"E:GithubDataubains-module-test自动化部署脚本x86架构新统一平台定时脚本\"\")",
"Bash(dir \"E:\\\\GithubData\\\\ubains-module-test\\\\自动化部署脚本\\\\x86架构\\\\新统一平台\" /s /b)"
]
}
}
# 计划执行_预定系统后端服务监测脚本开发
> 版本:V1.0
> 创建日期:2026-01-27
> 基于文档:`_PRD_预定系统后端服务监测需求文档.md`
> 交付物:`monitor_inner_api_services.sh`
---
## 一、任务概述
开发一个定时的服务监测与自愈脚本,实现对预定系统后端服务的自动监控和故障恢复。
---
## 二、开发阶段划分
### 阶段一:基础框架搭建(优先级:高)
| 序号 | 任务 | 描述 | 预计产出 |
|------|------|------|----------|
| 1.1 | 创建脚本文件 | 创建 `monitor_inner_api_services.sh` | 空壳脚本 |
| 1.2 | 服务器IP自动获取 | `get_server_ip()` 函数,自动检测本机IP | IP地址获取工具 |
| 1.3 | 定义配置变量 | 容器名、API URL、调试模式开关等 | 配置段 |
| 1.4 | 实现日志函数 | `log()` 函数,统一日志格式 | 日志工具 |
| 1.5 | 实现时间戳函数 | `timestamp()` 函数,格式 `YYYY-MM-DD HH:MM:SS` | 时间工具 |
| 1.6 | 容器名模糊匹配 | `find_container()` 函数,动态查找ujava*容器 | 容器查找工具 |
| 1.7 | 调试模式 | `DEBUG_MODE` 配置及运行参数打印 | 调试工具 |
**日志格式规范:**
```
2026-01-27 10:00:00 [RUNNING] service_key (PID: 1234)
2026-01-27 10:00:10 [WARNING] 检测到 4 个服务停止
```
**服务器IP自动获取逻辑:**
```bash
# 自动获取本机IP地址
get_server_ip() {
# 方法1:通过hostname -I获取(推荐)
ip=$(hostname -I 2>/dev/null | awk '{print $1}')
# 方法2:通过ip命令获取
if [ -z "$ip" ]; then
ip=$(ip route get 1 2>/dev/null | awk '{print $7}' | head -n1)
fi
# 方法3:通过ifconfig获取
if [ -z "$ip" ]; then
ip=$(ifconfig 2>/dev/null | grep "inet " | grep -v "127.0.0.1" | awk '{print $2}' | head -n1)
fi
echo "$ip"
}
```
---
### 阶段二:平台API可用性检查(优先级:高)
| 序号 | 任务 | 描述 | 实现要点 |
|------|------|------|----------|
| 2.1 | API检查函数 | `check_platform_api()` | curl -k -m 10 |
| 2.2 | 响应验证 | 状态码200 + `"操作成功"` + `code:200` | 组合条件判断 |
| 2.3 | 失败处理 | 触发 `restart_critical_services()` | 强制重启inner |
```bash
# 伪代码
check_platform_api() {
response=$(curl -k -m 10 "https://$SERVER_IP/api/system/getVerifyCode")
if [ $? -eq 200 ] && [[ "$response" =~ "操作成功" ]] && [[ "$response" =~ "code:200" ]]; then
return 0
else
return 1
fi
}
```
---
### 阶段三:容器内服务监测(优先级:高)
| 序号 | 任务 | 描述 | 实现要点 |
|------|------|------|----------|
| 3.1 | 服务清单定义 | 定义 `meeting2.0` 的jar标识和启动路径 | 配置数组/字典 |
| 3.2 | 容器名模糊匹配 | 容器名可能是ujava2、ujava3、ujava6等 | 动态查找匹配容器 |
| 3.3 | PID获取函数 | `get_service_pid()` | docker exec + ps aux |
| 3.4 | 状态判定函数 | `check_service_status()` | PID存在 + ps -p验证 |
| 3.5 | 服务启动函数 | `start_service()` | 执行./run.sh,30s超时 |
| 3.6 | 批量检查函数 | `check_services()` | 遍历统计STOPPED |
**服务启动实现(重要):**
```bash
# ✅ 正确的启动方式:使用 ./run.sh
docker exec "$CONTAINER_NAME" bash -c "cd '$service_path' && ./$START_SCRIPT"
# ❌ 错误的启动方式:使用 bash run.sh
docker exec "$CONTAINER_NAME" bash -c "cd '$service_path' && bash $START_SCRIPT"
```
**说明:**
- 必须使用 `./run.sh` 而非 `bash run.sh`
- 脚本可能依赖当前目录来定位配置文件
- 使用 `./` 明确指定从当前目录执行
**容器名称模糊匹配逻辑:**
```bash
# 动态查找匹配的容器(ujava2、ujava3、ujava6等)
find_container() {
docker ps --format "{{.Names}}" | grep -E "^ujava[0-9]+$" | head -n1
}
# 服务配置
declare -A SERVICES
SERVICES["meeting2.0"]="ubains-meeting-inner-api"
SERVICE_PATHS["meeting2.0"]="/var/www/java/api-java-meeting2.0"
```
---
### 阶段四:6060端口与malan监测(优先级:中)
| 序号 | 任务 | 描述 | 实现要点 |
|------|------|------|----------|
| 4.1 | 端口检查函数 | `check_port_6060()` | 兼容netstat/ss/lsof |
| 4.2 | malan启动函数 | `start_malan_service()` | cd + source + ./malan & |
| 4.3 | 端口等待验证 | 启动后30s内检查端口 | 轮询检查 |
```bash
check_port_6060() {
if command -v netstat &> /dev/null; then
netstat -tuln | grep ':6060 '
elif command -v ss &> /dev/null; then
ss -tuln | grep ':6060 '
elif command -v lsof &> /dev/null; then
lsof -i:6060
fi
}
```
---
### 阶段五:主流程集成(优先级:高)
| 序号 | 任务 | 描述 | 执行顺序 |
|------|------|------|----------|
| 5.1 | 主函数框架 | `main()` 函数 | - |
| 5.2 | 流程编排 | 按PRD要求顺序执行 | 详见下方 |
**主流程伪代码:**
```bash
main() {
log "[START] ===== $(timestamp) 服务监测开始 ====="
# 自动获取服务器IP
SERVER_IP=$(get_server_ip)
log "[INFO] 自动获取服务器IP: $SERVER_IP"
API_URL="https://${SERVER_IP}/api/system/getVerifyCode"
# 动态查找匹配的容器
CONTAINER_NAME=$(find_container)
log "[INFO] 找到容器: $CONTAINER_NAME"
# 1. 平台API检查
if ! check_platform_api; then
restart_critical_services "inner"
fi
# 2. 容器内服务检查
problems=$(check_services)
if [ "$problems" -gt 0 ]; then
restart_problem_services "$problems"
fi
# 3. 6060端口检查
if ! check_port_6060; then
start_malan_service
fi
log "[SUMMARY] ===== $(timestamp) 操作完成 ====="
}
```
---
### 阶段六:异常处理与容错(优先级:高)
| 序号 | 任务 | 描述 | 实现要点 |
|------|------|------|----------|
| 6.1 | run.sh存在性检查 | 启动前验证文件存在 | 明确错误提示 |
| 6.2 | 重启后等待 | unacos重启后等待30s | 避免依赖未就绪 |
| 6.3 | 失败日志记录 | 所有失败操作必须写日志 | 含原因/路径/命令 |
| 6.4 | 超时处理 | start_service统一30s超时 | 避免无限等待 |
---
### 阶段七:测试与验收(优先级:高)
| 序号 | 测试场景 | 验收标准 | 测试方法 |
|------|----------|----------|----------|
| 7.1 | 服务停止恢复 | 日志出现`[STOPPED]``[SUCCESS]` | 手动kill进程 |
| 7.2 | API异常恢复 | API失败时触发强制重启 | 停止inner服务 |
| 7.3 | malan启动恢复 | 6060未监听时启动malan | 停止malan进程 |
| 7.4 | 日志完整性 | 所有操作有可追溯日志 | 检查日志文件 |
| 7.5 | 定时执行 | crontab/systemd timer正常 | 配置定时任务 |
---
## 三、文件结构
```
monitor_inner_api_services.sh
├── 配置变量段
│ ├── SERVER_IP="" # 由get_server_ip()自动获取
│ ├── CONTAINER_NAME="ujava2" # 支持模糊匹配:ujava2/ujava3/ujava6等
│ ├── API_URL="https://${SERVER_IP}/api/system/getVerifyCode"
│ ├── MALAN_DIR="/var/www/malan"
│ └── LOG_FILE="/var/log/monitor-inner-api-services.log"
├── 工具函数段
│ ├── timestamp() # 返回格式化时间戳
│ ├── log() # 统一日志输出
│ ├── get_server_ip() # 自动获取本机IP地址
│ └── find_container() # 动态查找匹配的容器(ujava*模糊匹配)
├── 检查函数段
│ ├── check_platform_api() # API可用性检查
│ ├── check_port_6060() # 6060端口检查
│ ├── get_service_pid() # 获取服务PID
│ └── check_service_status() # 服务状态检查
├── 操作函数段
│ ├── start_service() # 启动单个服务
│ ├── restart_critical_services() # 强制重启关键服务
│ ├── check_services() # 批量检查服务
│ ├── restart_problem_services() # 重启问题服务
│ └── start_malan_service() # 启动malan服务
└── 主流程段
└── main() # 主函数
```
---
## 四、依赖与规范
### 4.1 依赖文档
- 代码规范:`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`
### 4.2 外部依赖
- `docker`:容器操作
- `curl`:API检查
- `netstat`/`ss`/`lsof`:端口检查(至少一个)
---
## 五、验收清单
- [ ] 脚本可正常执行,无语法错误
- [ ] 日志输出到 `/var/log/monitor-inner-api-services.log`
- [ ] 平台API检查功能正常
- [ ] 容器内服务 `meeting2.0` 停止后可自动启动
- [ ] 6060端口未监听时malan可自动启动
- [ ] 所有操作都有对应日志记录
- [ ] `run.sh` 不存在时有明确错误提示
- [ ] 可通过crontab定时执行
---
## 六、部署说明
### 6.1 文件放置
- 脚本路径:`自动化部署脚本/x86架构/预定系统/定时脚本/monitor_inner_api_services.sh`
- 日志路径:`/var/log/monitor-inner-api-services.log`
### 6.2 权限设置
```bash
chmod +x monitor_inner_api_services.sh
```
### 6.3 定时任务配置(示例)
```bash
# 每5分钟执行一次
*/5 * * * * /path/to/monitor_inner_api_services.sh
```
---
## 七、进度跟踪
| 阶段 | 状态 | 完成时间 | 备注 |
|------|------|----------|------|
| 阶段一:基础框架 | ✅ 已完成 | 2026-01-27 | 服务器IP自动获取、容器模糊匹配、调试模式等 |
| 阶段二:API检查 | ✅ 已完成 | 2026-01-27 | API可用性检查与响应验证 |
| 阶段三:容器服务监测 | ✅ 已完成 | 2026-01-27 | meeting2.0服务监测与启动 |
| 阶段四:端口监测 | ✅ 已完成 | 2026-01-27 | 6060端口检查与malan启动 |
| 阶段五:主流程集成 | ✅ 已完成 | 2026-01-27 | 主流程编排与执行顺序 |
| 阶段六:异常处理 | ✅ 已完成 | 2026-01-27 | 日志污染、路径配置等异常处理 |
| 阶段七:测试验收 | ✅ 已完成 | 2026-01-27 | 功能已验证,运行正常 |
---
## 八、问题记录与解决
### 8.1 日志输出污染返回值问题
**问题描述:**
```
./monitor_inner_api_services.sh: 第 451 行:[: 2026-01-27 19:05:51 [INFO] ===== 2026-01-27 19:05:51 服务状态检查 =====
1:需要整数表达式
```
**原因分析:**
- `check_services()` 函数内部调用 `log()` 输出日志
- `log()` 使用 `tee -a` 同时输出到stdout和日志文件
- 使用 `problems=$(check_services)` 捕获返回值时,日志输出也被捕获
- 导致 `problems` 变量包含日志内容,整数比较失败
**解决方案:**
```bash
# 修改前:check_services() 函数内部调用 log()
check_services() {
log "[INFO] ===== $(timestamp) 服务状态检查 =====" # ❌ 会被捕获
local problems=0
...
echo "$problems"
}
# 修改后:将日志输出移到调用处
check_services() {
local problems=0 # ✅ 函数只返回数值
...
echo "$problems"
}
# 在 main() 中调用时先输出日志
log "[INFO] ===== $(timestamp) 服务状态检查 ====="
problems=$(check_services)
```
**设计原则:**
- 需要捕获返回值的函数,内部不应调用 `log()` 输出到stdout
- 日志输出应在调用函数之前或之后进行
---
### 8.2 服务路径配置问题
**问题描述:**
```
2026-01-27 19:04:48 [INFO] 正在启动服务: meeting2.0 (路径: /var/www/java/api-java-meeting2.0)
2026-01-27 19:05:21 [ERROR] 服务目录不存在: /var/www/java/api-java-meeting2.0
2026-01-27 19:05:21 [FAILED] meeting2.0 启动超时(30秒)
```
**原因分析:**
- 脚本中配置的服务路径与容器内实际路径不匹配
- 路径末尾的斜杠可能导致问题(如 `/path/` vs `/path`
- 需要确认容器内的实际服务路径
**正确路径示例:**
- 服务目录:`/var/www/java/api-java-meeting2.0`
- run.sh 位置:`/var/www/java/api-java-meeting2.0/run.sh`
**解决方法:**
```bash
# 在容器内查找正确的服务路径
docker exec ujava6 find /var/www/java -name "*meeting*" -type d
# 或查找 run.sh 文件位置
docker exec ujava6 find /var/www/java -name "run.sh"
# 验证路径内容
docker exec ujava6 ls -la /var/www/java/api-java-meeting2.0/
# 找到正确路径后,修改脚本配置(注意末尾不要有斜杠)
SERVICE_PATHS["meeting2.0"]="/var/www/java/api-java-meeting2.0"
```
**调试模式增强:**
当启用 `DEBUG_MODE=true` 时,脚本会输出:
- 服务路径和启动脚本完整路径
- jar包标识
- 执行的完整命令
- 目录不存在时显示目录内容
```
---
### 8.3 API检查失败问题
**问题描述:**
```
2026-01-27 19:04:48 [ERROR] API检查失败,HTTP状态码: 502
```
**可能原因:**
1. 服务未启动或端口未监听
2. Nginx/网关配置问题
3. 后端服务处理超时
**排查步骤:**
```bash
# 1. 检查容器是否运行
docker ps | grep ujava6
# 2. 检查容器内服务进程
docker exec ujava6 ps aux | grep meeting
# 3. 手动测试API
curl -k https://192.168.5.47/api/system/getVerifyCode
# 4. 检查容器日志
docker logs ujava6 --tail 50
```
---
### 8.4 check_service_status() 日志污染问题
**问题描述:**
```
./monitor_inner_api_services.sh: 第 450 行:[: 2026-01-27 19:09:25 [INFO] 检查服务状态: meeting2.0
2026-01-27 19:09:26 [STOPPED] meeting2.0 (未找到进程)
1:需要整数表达式
```
**原因分析:**
- `check_service_status()` 函数内部调用 `log()` 输出日志
- `check_services()` 在循环中调用该函数时,虽然不需要捕获返回值进行数值判断
- 但由于函数内有多条 log 输出,导致日志格式混乱
**解决方案:**
```bash
# 修改前:check_service_status() 内部调用 log()
check_service_status() {
log "[INFO] 检查服务状态: $service_key" # ❌ 日志分散
...
log "[STOPPED] $service_key (未找到进程)"
return 1
}
# 修改后:函数只返回状态字符串,由调用方统一记录日志
check_service_status() {
local service_key="$1"
local jar_identifier="${SERVICES[$service_key]}"
local pid
pid=$(get_service_pid "$jar_identifier")
if [ -z "$pid" ]; then
echo "[STOPPED] $service_key (未找到进程)" # ✅ 只输出状态
return 1
fi
...
}
# 在 check_services() 中统一处理日志
check_services() {
local problems=0
local status_output
for service_key in "${!SERVICES[@]}"; do
status_output=$(check_service_status "$service_key")
log "$status_output" # 统一记录日志
if [[ "$status_output" =~ \[STOPPED\] ]]; then
problems=$((problems + 1))
fi
done
echo "$problems"
}
```
**设计原则:**
- 需要返回值的函数,输出应保持简洁(只输出返回值或状态信息)
- 日志记录应在调用方统一处理,避免函数内部产生副作用
- 使用正则匹配判断状态类型,而非依赖返回值
---
### 8.5 调试模式配置
**功能说明:**
为便于问题排查,脚本增加了调试模式,可以打印详细的运行参数。
**配置方法:**
```bash
# 在脚本配置段设置
DEBUG_MODE="true" # 启用调试模式
DEBUG_MODE="false" # 关闭调试模式(默认)
```
**调试输出内容:**
- 服务器IP、容器名、API_URL
- Malan服务配置(目录、端口)
- 超时配置(启动超时、重启等待)
- 各服务配置(jar包标识、服务路径)
**输出示例:**
```
2026-01-27 19:10:00 [DEBUG] ========== 运行参数 ==========
2026-01-27 19:10:00 [DEBUG] 服务器IP: 192.168.5.47
2026-01-27 19:10:00 [DEBUG] 容器名: ujava6
2026-01-27 19:10:00 [DEBUG] API_URL: https://192.168.5.47/api/system/getVerifyCode
2026-01-27 19:10:00 [DEBUG] MALAN_DIR: /var/www/malan
2026-01-27 19:10:00 [DEBUG] MALAN_PORT: 6060
2026-01-27 19:10:00 [DEBUG] START_TIMEOUT: 30秒
2026-01-27 19:10:00 [DEBUG] RESTART_WAIT: 30秒
2026-01-27 19:10:00 [DEBUG] 服务[meeting2.0]: jar=ubains-meeting-inner-api, path=/var/www/java/api-java-meeting2.0/
2026-01-27 19:10:00 [DEBUG] ==============================
```
---
### 8.6 启动命令执行方式问题
**问题描述:**
在容器内执行 `cd /path && bash run.sh` 启动失败,但使用 `./run.sh` 可以成功。
**原因分析:**
1. **`bash run.sh` 的问题**:
- 直接使用 `bash run.sh` 可能无法正确设置工作目录
- 脚本内可能依赖 `./` 相对路径来定位资源文件
- 某些脚本需要通过当前目录来确定配置文件位置
2. **`./run.sh` 的优势**:
- 明确指定从当前目录执行
- 保持正确的工作目录上下文
- 更符合脚本设计者的预期
**解决方案:**
```bash
# ❌ 错误的启动方式
docker exec "$CONTAINER_NAME" bash -c "cd '$service_path' && bash run.sh &"
# ✅ 正确的启动方式
docker exec "$CONTAINER_NAME" bash -c "cd '$service_path' && ./run.sh"
```
**修改前后对比:**
```bash
# 修改前
docker exec "$CONTAINER_NAME" bash -c "cd '$service_path' && bash '$START_SCRIPT' &"
# 修改后
docker exec "$CONTAINER_NAME" bash -c "cd '$service_path' && ./'$START_SCRIPT'"
```
**注意点:**
- 使用 `./run.sh` 时,确保脚本有执行权限(chmod +x)
- 如果脚本必须使用 `bash run.sh`,可能需要检查脚本内部逻辑
- malan 服务使用 `./malan` 是正确的,无需修改
---
*本文档基于 PRD 自动生成,如有疑问请参考原需求文档。*
......@@ -3,7 +3,7 @@
> 版本:V1.0
> 更新日期:2026-01-27
> 适用范围:预定系统后端服务监测与自愈(容器内服务 + 端口服务)
> 参考脚本:`自动化部署脚本\x86架构\预定系统\定时脚本\monitor_inner_api_services.sh`
> 实现脚本:`自动化部署脚本\x86架构\预定系统\定时脚本\monitor_inner_api_services.sh`
---
......@@ -88,11 +88,12 @@
#### 4.3.1 服务清单
| 服务key | jar标识/定位 | 启动脚本 |
| --- | --- | --- |
| meeting2.0 | `ubains-meeting-inner-api` | `/var/www/java/api/java-meeting/java-meeting2.0/run.sh` |
| meeting2.0 | `ubains-meeting-inner-api` | `/var/www/java/api-java-meeting2.0/run.sh` |
> 注:meeting2.0/3.0 通过 ps 组合条件避免误匹配(脚本内为特判逻辑)。
#### 4.3.2 运行状态判定
说明:容器名称不一定叫ujava2,也可能是ujava3、ujava6,需要做模糊匹配。
判断1:
- 获取 PID:通过 `docker exec ujava2` 在容器内 `ps aux | grep` 定位
- 判定运行:PID 非空 且 `ps -p $pid` 成功
......@@ -101,8 +102,9 @@
#### 4.3.3 自愈动作:run.sh
- 启动:在容器内执行:
- `cd /var/www/java/api/java-meeting/java-meeting2.0/`
- `run.sh`
- `cd /var/www/java/api-java-meeting2.0/`
- `./run.sh`
- **注意**:必须使用 `./run.sh` 而非 `bash run.sh`,因为脚本可能依赖执行路径
- 等待启动:超时 30 秒
- 成功:输出 `[SUCCESS] service_key (PID: xxx)`
- 失败:输出 `[FAILED] service_key 启动超时`
......@@ -153,10 +155,38 @@
| 配置项 | 默认/当前值 | 说明 |
| --- | --- | --- |
| ujava2容器名 | `ujava2` | 容器内服务执行目标 |
| 平台 API URL | `https://服务器实际IP地址/api/system/getVerifyCode` | 用于判断平台核心可用性 |
| 服务器IP | **自动获取** | 脚本自动检测本机IP,优先获取非127.0.0.1的IPv4地址 |
| ujava容器名 | `ujava*` | 支持模糊匹配(ujava2/ujava3/ujava6等) |
| 平台 API URL | `https://自动获取的IP/api/system/getVerifyCode` | 用于判断平台核心可用性 |
| malan 目录 | `/var/www/malan` | 6060端口服务 |
| 服务启动等待 | 30s | start_service/restart_extapi/malan 统一超时 |
| 调试模式 | `true` | 启用时打印详细运行参数,便于排查问题 |
**服务器IP获取逻辑:**
- 优先获取与Docker容器同网段的IP地址
- 自动过滤 `127.0.0.1` 本地回环地址
- 支持多网卡环境,选择首个有效IP
**调试模式说明:**
`DEBUG_MODE=true` 时,脚本会在日志中输出以下运行参数:
- 服务器IP、容器名、API_URL
- Malan服务目录和端口配置
- 超时配置参数
- 各服务的jar包标识和路径配置
**调试输出示例:**
```
2026-01-27 19:10:00 [DEBUG] ========== 运行参数 ==========
2026-01-27 19:10:00 [DEBUG] 服务器IP: 192.168.5.47
2026-01-27 19:10:00 [DEBUG] 容器名: ujava6
2026-01-27 19:10:00 [DEBUG] API_URL: https://192.168.5.47/api/system/getVerifyCode
2026-01-27 19:10:00 [DEBUG] MALAN_DIR: /var/www/malan
2026-01-27 19:10:00 [DEBUG] MALAN_PORT: 6060
2026-01-27 19:10:00 [DEBUG] START_TIMEOUT: 30秒
2026-01-27 19:10:00 [DEBUG] RESTART_WAIT: 30秒
2026-01-27 19:10:00 [DEBUG] 服务[meeting2.0]: jar=ubains-meeting-inner-api, path=/var/www/java/api-java-meeting2.0/
2026-01-27 19:10:00 [DEBUG] ==============================
```
---
......
{
"name": "ubains-module-test",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}
......@@ -1066,6 +1066,14 @@ function check_crontab() {
log "INFO" "监控外部API服务的定时任务已存在"
fi
# 监控内部API服务 - 每5分钟检查一次
if ! crontab -l 2>/dev/null | grep -q "monitor_inner_api_services.sh"; then
log "INFO" "创建监控内部API服务的定时任务..."
(crontab -l 2>/dev/null; echo "*/5 * * * * /opt/scripts/monitor_inner_api_services.sh >> /var/log/monitor-inner-api-services.log 2>&1") | crontab -
else
log "INFO" "监控内部API服务的定时任务已存在"
fi
# 数据库备份 - 每天凌晨2点执行
if ! crontab -l 2>/dev/null | grep -q "backup_mysql_databases.sh"; then
log "INFO" "创建数据库备份定时任务..."
......
#!/bin/bash
#===============================================================================
# 脚本名称:monitor_inner_api_services.sh
# 功能描述:预定系统后端服务监测与自愈脚本
# 版本:V1.0
# 创建日期:2026-01-27
# 基于文档:_PRD_预定系统后端服务监测需求文档.md
#
# 监测对象:
# 1. 平台API可用性 (https://SERVER_IP/api/system/getVerifyCode)
# 2. 容器ujava*内服务进程 (meeting2.0),支持ujava2/ujava3/ujava6等模糊匹配
# 3. 宿主机6060端口malan服务
#
# 使用方法:
# chmod +x monitor_inner_api_services.sh
# ./monitor_inner_api_services.sh
#
# 定时任务示例:
# */5 * * * * /path/to/monitor_inner_api_services.sh
#===============================================================================
#-------------------------------------------------------------------------------
# 配置变量段
#-------------------------------------------------------------------------------
# 服务器配置(由get_server_ip()函数自动获取)
SERVER_IP=""
# 容器配置(支持模糊匹配:ujava2/ujava3/ujava6等)
CONTAINER_NAME="ujava2" # 脚本将自动查找匹配的容器
# API配置(将在main函数中根据SERVER_IP动态生成)
API_URL=""
# Malan服务配置
MALAN_DIR="/var/www/malan"
MALAN_PORT="6060"
# 日志配置
LOG_FILE="/var/log/monitor-inner-api-services.log"
# 超时配置
START_TIMEOUT=30
RESTART_WAIT=30
# 调试模式(设为true启用详细参数打印)
DEBUG_MODE="true"
# 服务配置(关联数组:服务key -> jar包标识)
declare -A SERVICES
SERVICES["meeting2.0"]="ubains-meeting-inner-api"
# 服务路径配置(关联数组:服务key -> 服务路径)
declare -A SERVICE_PATHS
SERVICE_PATHS["meeting2.0"]="/var/www/java/api-java-meeting2.0"
# 启动脚本名称
START_SCRIPT="run.sh"
#-------------------------------------------------------------------------------
# 工具函数段
#-------------------------------------------------------------------------------
# 获取格式化时间戳
# 返回格式:YYYY-MM-DD HH:MM:SS
timestamp() {
date '+%Y-%m-%d %H:%M:%S'
}
# 统一日志输出函数
# 参数:$1 - 日志内容(必须包含标识符)
# 示例:log "[RUNNING] service_key (PID: 1234)"
log() {
local message="$1"
echo "$(timestamp) $message" | tee -a "$LOG_FILE"
}
# 动态查找匹配的容器
# 支持模糊匹配:ujava2、ujava3、ujava6等
# 返回:找到的容器名称,未找到返回空
find_container() {
local container
# 查找所有以ujava开头后跟数字的运行中容器
container=$(docker ps --format "{{.Names}}" 2>/dev/null | grep -E "^ujava[0-9]+$" | head -n1)
if [ -z "$container" ]; then
echo ""
return 1
fi
echo "$container"
}
# 自动获取本机IP地址
# 支持多种方法,优先使用hostname -I
# 返回:检测到的IP地址,失败返回空
get_server_ip() {
local ip
# 方法1:通过hostname -I获取(推荐,支持多网卡)
ip=$(hostname -I 2>/dev/null | awk '{print $1}')
if [ -n "$ip" ] && [ "$ip" != "127.0.0.1" ]; then
echo "$ip"
return 0
fi
# 方法2:通过ip命令获取
if command -v ip &> /dev/null; then
ip=$(ip route get 1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="src") print $(i+1)}' | head -n1)
if [ -n "$ip" ] && [ "$ip" != "127.0.0.1" ]; then
echo "$ip"
return 0
fi
fi
# 方法3:通过ifconfig获取
if command -v ifconfig &> /dev/null; then
ip=$(ifconfig 2>/dev/null | grep "inet " | grep -v "127.0.0.1" | awk '{print $2}' | head -n1)
if [ -n "$ip" ]; then
echo "$ip"
return 0
fi
fi
# 方法4:通过解析/proc/net/route(适用于没有ifconfig的系统)
if [ -f /proc/net/route ]; then
ip=$(awk 'NR==2 {ip=strtonum("0x"$2); printf "%d.%d.%d.%d\n", and(ip>>24,255), and(ip>>16,255), and(ip>>8,255), and(ip,255)}' /proc/net/route)
if [ -n "$ip" ] && [ "$ip" != "0.0.0.0" ]; then
echo "$ip"
return 0
fi
fi
echo ""
return 1
}
#-------------------------------------------------------------------------------
# 检查函数段
#-------------------------------------------------------------------------------
# 检查平台API可用性
# 返回:0 - API正常,1 - API异常
check_platform_api() {
log "[INFO] 正在检查平台API可用性..."
local response
local http_code
# 发送请求并获取响应和HTTP状态码
response=$(curl -k -m 10 -s -w "\n%{http_code}" "$API_URL" 2>&1)
http_code=$(echo "$response" | tail -n1)
response_body=$(echo "$response" | head -n-1)
# 验证HTTP状态码为200
if [ "$http_code" != "200" ]; then
log "[ERROR] API检查失败,HTTP状态码: $http_code"
return 1
fi
# 验证响应内容包含"操作成功"
if ! echo "$response_body" | grep -q "操作成功"; then
log "[ERROR] API检查失败,响应内容未包含'操作成功'"
return 1
fi
# 验证响应内容包含code:200(允许空格)
if ! echo "$response_body" | grep -q "code.*:.*200"; then
log "[ERROR] API检查失败,响应内容未包含'code:200'"
return 1
fi
log "[SUCCESS] 平台API检查通过"
return 0
}
# 检查6060端口是否监听
# 返回:0 - 端口已监听,1 - 端口未监听
check_port_6060() {
log "[INFO] 正在检查6060端口状态..."
# 优先使用netstat
if command -v netstat &> /dev/null; then
if netstat -tuln 2>/dev/null | grep -q ":${MALAN_PORT} "; then
log "[SUCCESS] 6060端口已监听"
return 0
fi
# 其次使用ss
elif command -v ss &> /dev/null; then
if ss -tuln 2>/dev/null | grep -q ":${MALAN_PORT} "; then
log "[SUCCESS] 6060端口已监听"
return 0
fi
# 最后使用lsof
elif command -v lsof &> /dev/null; then
if lsof -i:"${MALAN_PORT}" &> /dev/null; then
log "[SUCCESS] 6060端口已监听"
return 0
fi
else
log "[WARNING] 未找到端口检查命令 (netstat/ss/lsof)"
fi
log "[WARNING] 6060端口未监听"
return 1
}
# 获取容器内服务进程PID
# 参数:$1 - jar包标识
# 返回:PID字符串(失败返回空)
get_service_pid() {
local jar_identifier="$1"
local pid
# 在容器内执行ps命令查找进程
pid=$(docker exec "$CONTAINER_NAME" ps aux 2>/dev/null | grep "$jar_identifier" | grep -v grep | awk '{print $2}' | head -n1)
echo "$pid"
}
# 检查服务运行状态
# 参数:$1 - 服务key
# 输出:状态信息到stdout(不含时间戳)
# 返回:0 - 服务运行中,1 - 服务未运行
check_service_status() {
local service_key="$1"
local jar_identifier="${SERVICES[$service_key]}"
# 获取PID
local pid
pid=$(get_service_pid "$jar_identifier")
# PID为空,服务未运行
if [ -z "$pid" ]; then
echo "[STOPPED] $service_key (未找到进程)"
return 1
fi
# 验证PID是否有效
if ! docker exec "$CONTAINER_NAME" ps -p "$pid" &> /dev/null; then
echo "[STOPPED] $service_key (PID: $pid 无效)"
return 1
fi
echo "[RUNNING] $service_key (PID: $pid)"
return 0
}
#-------------------------------------------------------------------------------
# 操作函数段
#-------------------------------------------------------------------------------
# 启动单个服务
# 参数:$1 - 服务key
# 返回:0 - 启动成功,1 - 启动失败
start_service() {
local service_key="$1"
local service_path="${SERVICE_PATHS[$service_key]}"
local start_script_path="${service_path}/${START_SCRIPT}"
log "[INFO] 正在启动服务: $service_key"
if [ "$DEBUG_MODE" = "true" ]; then
log "[DEBUG] 服务路径: $service_path"
log "[DEBUG] 启动脚本: $start_script_path"
log "[DEBUG] jar标识: ${SERVICES[$service_key]}"
fi
# 先检查服务目录是否存在
if ! docker exec "$CONTAINER_NAME" test -d "$service_path"; then
log "[ERROR] 服务目录不存在: $service_path"
log "[ERROR] 请检查脚本中 SERVICE_PATHS[$service_key] 的配置是否正确"
log "[ERROR] 排查命令: docker exec $CONTAINER_NAME ls -la /var/www/java/"
return 1
fi
# 检查run.sh是否存在
if ! docker exec "$CONTAINER_NAME" test -f "$start_script_path"; then
log "[ERROR] 启动脚本不存在: $start_script_path"
log "[ERROR] 目录内容如下:"
docker exec "$CONTAINER_NAME" ls -la "$service_path" 2>&1 | while read line; do
log "[ERROR] $line"
done
return 1
fi
# 在容器内执行启动脚本
if [ "$DEBUG_MODE" = "true" ]; then
log "[DEBUG] 执行命令: cd $service_path && ./$START_SCRIPT"
fi
docker exec "$CONTAINER_NAME" bash -c "cd '$service_path' && ./'$START_SCRIPT'" &> /dev/null
# 等待服务启动
local wait_time=0
while [ $wait_time -lt $START_TIMEOUT ]; do
local jar_identifier="${SERVICES[$service_key]}"
local pid
pid=$(get_service_pid "$jar_identifier")
if [ -n "$pid" ]; then
log "[SUCCESS] $service_key (PID: $pid)"
return 0
fi
sleep 2
wait_time=$((wait_time + 2))
done
log "[FAILED] $service_key 启动超时(${START_TIMEOUT}秒)"
return 1
}
# 强制重启关键服务
# 参数:$1 - 服务标识(如"inner")
restart_critical_services() {
local critical_service="$1"
log "[WARNING] ===== 开始强制重启关键服务: $critical_service ====="
# 查找meeting2.0服务并重启
for service_key in "${!SERVICES[@]}"; do
log "[INFO] 强制重启: $service_key"
# 先尝试停止进程
local jar_identifier="${SERVICES[$service_key]}"
local pid
pid=$(get_service_pid "$jar_identifier")
if [ -n "$pid" ]; then
docker exec "$CONTAINER_NAME" kill -9 "$pid" &> /dev/null
sleep 2
fi
# 启动服务
if start_service "$service_key"; then
log "[SUCCESS] $service_key 重启成功"
else
log "[FAILED] $service_key 重启失败"
fi
done
log "[WARNING] ===== 关键服务重启完成,等待${RESTART_WAIT}秒后继续 ====="
sleep "$RESTART_WAIT"
}
# 批量检查所有服务
# 返回:停止的服务数量
check_services() {
local problems=0
local status_output
for service_key in "${!SERVICES[@]}"; do
status_output=$(check_service_status "$service_key")
log "$status_output" # 记录服务状态日志
if [[ "$status_output" =~ \[STOPPED\] ]]; then
problems=$((problems + 1))
fi
done
echo "$problems"
}
# 重启问题服务
# 参数:$1 - 问题服务数量
restart_problem_services() {
local problems="$1"
log "[WARNING] 检测到 $problems 个服务停止,将重新启动"
for service_key in "${!SERVICES[@]}"; do
local jar_identifier="${SERVICES[$service_key]}"
local pid
pid=$(get_service_pid "$jar_identifier")
# 如果服务未运行,则启动
if [ -z "$pid" ]; then
start_service "$service_key"
fi
done
}
# 启动malan服务
# 返回:0 - 启动成功,1 - 启动失败
start_malan_service() {
log "[INFO] 正在启动malan服务..."
# 检查malan目录是否存在
if [ ! -d "$MALAN_DIR" ]; then
log "[ERROR] malan目录不存在: $MALAN_DIR"
return 1
fi
# 检查malan可执行文件是否存在
if [ ! -f "${MALAN_DIR}/malan" ]; then
log "[ERROR] malan可执行文件不存在: ${MALAN_DIR}/malan"
return 1
fi
# 启动malan
cd "$MALAN_DIR" || {
log "[ERROR] 无法进入目录: $MALAN_DIR"
return 1
}
bash -c "source /etc/profile && ./malan &" &> /dev/null
# 等待端口就绪
local wait_time=0
while [ $wait_time -lt $START_TIMEOUT ]; do
if check_port_6060 &> /dev/null; then
log "[SUCCESS] malan服务启动成功,6060端口已监听"
return 0
fi
sleep 2
wait_time=$((wait_time + 2))
done
log "[FAILED] malan服务启动超时,6060端口未监听"
return 1
}
#-------------------------------------------------------------------------------
# 主流程段
#-------------------------------------------------------------------------------
# 主函数
main() {
# 确保日志目录存在
local log_dir
log_dir=$(dirname "$LOG_FILE")
if [ ! -d "$log_dir" ]; then
mkdir -p "$log_dir" 2>/dev/null || {
echo "[ERROR] 无法创建日志目录: $log_dir" >&2
exit 1
}
fi
log "[START] ===== 服务监测开始 ====="
# 自动获取服务器IP
SERVER_IP=$(get_server_ip)
if [ -z "$SERVER_IP" ]; then
log "[ERROR] 无法自动获取服务器IP,脚本退出"
exit 1
fi
log "[INFO] 自动获取服务器IP: $SERVER_IP"
API_URL="https://${SERVER_IP}/api/system/getVerifyCode"
# 动态查找匹配的容器
local found_container
found_container=$(find_container)
if [ -z "$found_container" ]; then
log "[ERROR] 无法找到匹配的容器 (ujava*),脚本退出"
exit 1
fi
CONTAINER_NAME="$found_container"
log "[INFO] 找到容器: $CONTAINER_NAME"
# 打印运行参数(调试模式)
if [ "$DEBUG_MODE" = "true" ]; then
log "[DEBUG] ========== 运行参数 =========="
log "[DEBUG] 服务器IP: $SERVER_IP"
log "[DEBUG] 容器名: $CONTAINER_NAME"
log "[DEBUG] API_URL: $API_URL"
log "[DEBUG] MALAN_DIR: $MALAN_DIR"
log "[DEBUG] MALAN_PORT: $MALAN_PORT"
log "[DEBUG] START_TIMEOUT: ${START_TIMEOUT}秒"
log "[DEBUG] RESTART_WAIT: ${RESTART_WAIT}秒"
for svc in "${!SERVICES[@]}"; do
log "[DEBUG] 服务[$svc]: jar=${SERVICES[$svc]}, path=${SERVICE_PATHS[$svc]}"
done
log "[DEBUG] =============================="
fi
# 步骤1:平台API检查
if ! check_platform_api; then
restart_critical_services "inner"
fi
# 步骤2:容器内服务检查
log "[INFO] ===== $(timestamp) 服务状态检查 ====="
local problems
problems=$(check_services)
if [ "$problems" -gt 0 ]; then
restart_problem_services "$problems"
fi
# 步骤3:6060端口检查
if ! check_port_6060; then
start_malan_service
fi
log "[SUMMARY] ===== $(timestamp) 操作完成 ====="
}
#-------------------------------------------------------------------------------
# 脚本入口
#-------------------------------------------------------------------------------
# 执行主函数
main "$@"
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论