提交 04219d8d authored 作者: 陈泽健's avatar 陈泽健

feat(scripts): 新增多个定时任务并重构入口函数

- 新增 MySQL 备份定时任务,每周五凌晨执行
- 新增 Redis 服务监控定时任务,每 5 分钟执行
- 新增 EMQX 服务监控定时任务,每 5 分钟执行
- 新增 Nginx 日志备份定时任务,每天凌晨 3 点执行
- 新增 MySQL 日志备份定时任务,每天凌晨 2 点执行
- 新增自动清理已删除 ubains 定时任务,每天凌晨 4 点执行
- 将原 add_crontab_job 重命名为 _add_single_cron_job 作为内部函数
- 创建新的 add_crontab_job 作为入口函数,支持参数解析和批量配置
- 更新参数控制设计,支持 --enable-xxx / --disable-xxx 参数组合
- 修改 ujava2-startup 任务名为 ujava2,统一命名规范
- 添加函数式调用方式,避免 main 函数命名冲突
- 更新测试用例覆盖函数和脚本两种调用方式
- 同步更新 EMQX 服务监控需求文档,包含完整功能说明
上级 2747889d
......@@ -16,23 +16,53 @@
- 定时周期:每天凌晨01:00执行
- 定时任务执行脚本:`/data/services/scripts/check_server_health.sh`
- 需要先判断该路径是否存在脚本,若不存在则不进行配置,打印相关日志记录。
- 新增定时任务:
- 定时周期:每周五凌晨1点执行
- 定时任务执行脚本:`/data/services/scripts/UbainsmysqlBakUp`
- 需要先判断该路径是否存在脚本,若不存在则不进行配置,打印相关日志记录。
- 新增定时任务:
- 定时周期:*/5 * * * *
- 定时任务执行脚本:`/data/services/scripts/monitor_redis_service.sh`
- 需要先判断该路径是否存在脚本,若不存在则不进行配置,打印相关日志记录。
- 新增定时任务:
- 定时周期:*/5 * * * *
- 定时任务执行脚本:`/data/services/scripts/monitor_emqx_service.sh`
- 新增定时任务:
- 定时周期:0 3 * * *
- 定时任务执行脚本:`/data/services/scripts/backup_nginx_logs.sh`
- 需要先判断该路径是否存在脚本,若不存在则不进行配置,打印相关日志记录。
- 新增定时任务:
- 定时周期:0 2 * * *
- 定时任务执行脚本:`/data/services/scripts/backup_mysql_logs.sh`
- 需要先判断该路径是否存在脚本,若不存在则不进行配置,打印相关日志记录。
- 新增定时任务:
- 定时周期:0 4 * * *
- 定时任务执行脚本:`/data/services/scripts/auto_clean_deleted_ubains_v3.sh`
- 需要先判断该路径是否存在脚本,若不存在则不进行配置,打印相关日志记录。
#### 参数控制设计
- **参数方式:** 使用 `--enable-xxx` / `--disable-xxx` 开关式参数
- **默认行为:** 不传参数时默认全部启用(向后兼容)
- **调用方式:**
- 方式1:直接调用脚本 `./auto_crontab_settings.sh [参数]`
- 方式2:在主脚本中调用函数 `add_crontab_job [参数]`(避免 main 函数命名冲突)
- **参数示例:**
```bash
# 默认全部启用
./auto_crontab_settings.sh
add_crontab_job
# 只启用 check_server_health
./auto_crontab_settings.sh --disable-ujava2
add_crontab_job --disable-ujava2
# 禁用 check_server_health
./auto_crontab_settings.sh --disable-check-health
add_crontab_job --disable-check-health
# 显式启用指定任务
./auto_crontab_settings.sh --enable-ujava2 --enable-check-health
add_crontab_job --enable-ujava2 --enable-check-health
```
## 疑问与解答记录
......@@ -89,6 +119,14 @@
- **问题:** 如果调用时不传任何参数,默认应该启用哪些任务?
- **解答:** 默认全部启用(向后兼容现有行为)。
### 疑问15:主脚本调用方式?
- **问题:** 主脚本 `new_auto.sh` 如何调用 `auto_crontab_settings.sh`?直接调用脚本还是调用函数?
- **解答:** 使用 `add_crontab_job` 函数作为入口,避免 `main` 函数命名冲突。该函数支持参数传递,功能等同于直接调用脚本。
### 疑问16:函数命名设计?
- **问题:** 原有的 `add_crontab_job` 是添加单个任务的函数,现在要改成入口函数,如何处理?
- **解答:** 将原 `add_crontab_job` 重命名为 `_add_single_cron_job`(内部函数),新的 `add_crontab_job` 作为入口函数,负责参数解析和批量任务配置。
### 疑问14:参数冲突如何处理?
- **问题:** 如果同时传入矛盾的参数(如 `--enable-xxx``--disable-xxx`),如何处理?
- **解答:** 如果重复传参(同时启用和禁用同一任务),则跳过该任务配置并打印警告日志。
......
......@@ -29,7 +29,7 @@
| 任务 | Cron 表达式 | 脚本路径 | 日志路径 |
|------|-------------|----------|----------|
| ujava2-startup | `*/3 * * * *` | `/data/services/scripts/ujava2-startup.sh` | `/var/log/ujava2-cron.log` |
| ujava2 | `*/3 * * * *` | `/data/services/scripts/ujava2-startup.sh` | `/var/log/ujava2-cron.log` |
---
......@@ -37,22 +37,49 @@
| 任务 | Cron 表达式 | 脚本路径 | 日志 | 备注 |
|------|-------------|----------|------|------|
| check_server_health | `0 1 * * *` | `/data/services/scripts/check_server_health.sh` | 脚本自身管理 | 每天凌晨 01:00 执行 |
| check_health | `0 1 * * *` | `/data/services/scripts/check_server_health.sh` | 脚本自身管理 | 每天凌晨 01:00 执行,服务健康检查 |
| mysql_backup | `0 0 * * 5` | `/data/services/scripts/UbainsmysqlBakUp.sh` | 脚本自身管理 | 每周五凌晨 00:00 执行,MySQL数据库备份 |
| monitor_redis | `*/5 * * * *` | `/data/services/scripts/monitor_redis_service.sh` | 脚本自身管理 | 每5分钟执行,Redis服务监控 |
| monitor_emqx | `*/5 * * * *` | `/data/services/scripts/monitor_emqx_service.sh` | 脚本自身管理 | 每5分钟执行,EMQX服务监控 |
| nginx_backup | `0 3 * * *` | `/data/services/scripts/backup_nginx_logs.sh` | 脚本自身管理 | 每天凌晨 03:00 执行,Nginx日志备份 |
| mysql_logs_backup | `0 2 * * *` | `/data/services/scripts/backup_mysql_logs.sh` | 脚本自身管理 | 每天凌晨 02:00 执行,MySQL日志备份 |
| auto_clean | `0 4 * * *` | `/data/services/scripts/auto_clean_deleted_ubains_v3.sh` | 脚本自身管理 | 每天凌晨 04:00 执行,自动清理已删除ubains |
---
## 5. 重构设计
### 5.1 通用函数设计
### 5.1 入口函数设计
将现有 `add_crontab_job()` 重构为通用函数,接受以下参数:
#### 5.1.1 主入口函数:`add_crontab_job`
作为主入口函数,支持参数控制,用于在主脚本中调用:
```bash
add_crontab_job \
# 默认全部启用
add_crontab_job
# 带参数调用
add_crontab_job --disable-ujava2
add_crontab_job --enable-check-health --enable-mysql-backup
```
**函数职责:**
- 解析命令行参数(`--enable-xxx` / `--disable-xxx`
- 执行清理无效任务
- 根据任务状态批量配置定时任务
- 显示当前任务列表
#### 5.1.2 内部函数:`_add_single_cron_job`
添加单个定时任务的内部函数,接受以下参数:
```bash
_add_single_cron_job \
"<cron_expression>" \
"<script_path>" \
"<log_path>" \
"<comment>"
"<task_name>"
```
**参数说明:**
......@@ -61,7 +88,7 @@ add_crontab_job \
| `cron_expression` | Cron 时间表达式 | `"0 1 * * *"` |
| `script_path` | 脚本绝对路径 | `/data/services/scripts/check_server_health.sh` |
| `log_path` | 日志文件路径(可选) | `/var/log/xxx.log` 或空字符串 |
| `comment` | 注释标记 | `"自动配置 - check_server_health"` |
| `task_name` | 任务名称(用于注释标记) | `"check_health"` |
### 5.2 函数处理逻辑
......@@ -107,6 +134,20 @@ add_crontab_job \
| `--disable-ujava2` | 禁用 ujava2-startup 定时任务 |
| `--enable-check-health` | 启用 check_server_health 定时任务 |
| `--disable-check-health` | 禁用 check_server_health 定时任务 |
| `--enable-mysql-backup` | 启用 mysql_backup 定时任务 |
| `--disable-mysql-backup` | 禁用 mysql_backup 定时任务 |
| `--enable-mysql` | 启用 mysql_backup 定时任务(别名) |
| `--disable-mysql` | 禁用 mysql_backup 定时任务(别名) |
| `--enable-monitor-redis` | 启用 monitor_redis 定时任务 |
| `--disable-monitor-redis` | 禁用 monitor_redis 定时任务 |
| `--enable-monitor-emqx` | 启用 monitor_emqx 定时任务 |
| `--disable-monitor-emqx` | 禁用 monitor_emqx 定时任务 |
| `--enable-nginx-backup` | 启用 nginx_backup 定时任务 |
| `--disable-nginx-backup` | 禁用 nginx_backup 定时任务 |
| `--enable-mysql-logs-backup` | 启用 mysql_logs_backup 定时任务 |
| `--disable-mysql-logs-backup` | 禁用 mysql_logs_backup 定时任务 |
| `--enable-auto-clean` | 启用 auto_clean 定时任务 |
| `--disable-auto-clean` | 禁用 auto_clean 定时任务 |
| `-h, --help` | 显示帮助信息 |
#### 5.4.2 参数解析逻辑
......@@ -164,7 +205,7 @@ declare -A TASK_CRON
declare -A TASK_SCRIPT
declare -A TASK_LOG
# ujava2-startup 任务定义
# ujava2 任务定义
TASK_CRON[ujava2]="*/3 * * * *"
TASK_SCRIPT[ujava2]="/data/services/scripts/ujava2-startup.sh"
TASK_LOG[ujava2]="/var/log/ujava2-cron.log"
......@@ -173,10 +214,41 @@ TASK_LOG[ujava2]="/var/log/ujava2-cron.log"
TASK_CRON[check_health]="0 1 * * *"
TASK_SCRIPT[check_health]="/data/services/scripts/check_server_health.sh"
TASK_LOG[check_health]=""
# mysql_backup 任务定义
TASK_CRON[mysql_backup]="0 0 * * 5"
TASK_SCRIPT[mysql_backup]="/data/services/scripts/UbainsmysqlBakUp.sh"
TASK_LOG[mysql_backup]=""
# monitor_redis 任务定义
TASK_CRON[monitor_redis]="*/5 * * * *"
TASK_SCRIPT[monitor_redis]="/data/services/scripts/monitor_redis_service.sh"
TASK_LOG[monitor_redis]=""
# monitor_emqx 任务定义
TASK_CRON[monitor_emqx]="*/5 * * * *"
TASK_SCRIPT[monitor_emqx]="/data/services/scripts/monitor_emqx_service.sh"
TASK_LOG[monitor_emqx]=""
# nginx_backup 任务定义
TASK_CRON[nginx_backup]="0 3 * * *"
TASK_SCRIPT[nginx_backup]="/data/services/scripts/backup_nginx_logs.sh"
TASK_LOG[nginx_backup]=""
# mysql_logs_backup 任务定义
TASK_CRON[mysql_logs_backup]="0 2 * * *"
TASK_SCRIPT[mysql_logs_backup]="/data/services/scripts/backup_mysql_logs.sh"
TASK_LOG[mysql_logs_backup]=""
# auto_clean 任务定义
TASK_CRON[auto_clean]="0 4 * * *"
TASK_SCRIPT[auto_clean]="/data/services/scripts/auto_clean_deleted_ubains_v3.sh"
TASK_LOG[auto_clean]=""
```
#### 5.4.4 调用示例
**方式1:直接调用脚本**
```bash
# 默认全部启用
./auto_crontab_settings.sh
......@@ -194,6 +266,22 @@ TASK_LOG[check_health]=""
./auto_crontab_settings.sh --enable-ujava2 --disable-ujava2
```
**方式2:主脚本中调用函数(推荐)**
```bash
# 在 new_auto.sh 中调用
source "$SCRIPT_DIR/auto_crontab_settings.sh"
# 默认全部启用
add_crontab_job
# 带参数调用
add_crontab_job --disable-ujava2
add_crontab_job --enable-check-health --enable-mysql-backup
# 组合使用
add_crontab_job --disable-ujava2 --enable-check-health
```
---
## 6. 实现步骤
......@@ -253,20 +341,69 @@ TASK_LOG[check_health]=""
## 7. 关键代码片段设计
### 7.1 通用函数框架
### 7.1 入口函数框架
```bash
# ========================================
# ✅ 函数:安全添加定时任务(通用版本
# ✅ 函数:配置定时任务(支持参数控制
# ========================================
function add_crontab_job() {
# 1. 解析传入的参数
parse_arguments "$@"
log "INFO" "=========================================="
log "INFO" "🚀 开始配置定时任务"
log "INFO" "=========================================="
# 2. 执行清理
cleanup_crontab_jobs
# 3. 根据任务状态配置任务
for task_name in "${!TASK_STATUS[@]}"; do
local status="${TASK_STATUS[$task_name]}"
# 跳过冲突任务
if [[ "$status" == "conflict" ]]; then
log "WARN" "⏭️ 任务 $task_name 参数冲突,已跳过"
continue
fi
# 跳过禁用任务
if [[ "$status" == "false" ]]; then
log "INFO" "⏭️ 任务 $task_name 已被禁用,跳过配置"
continue
fi
# 配置启用的任务(调用内部函数)
_add_single_cron_job \
"${TASK_CRON[$task_name]}" \
"${TASK_SCRIPT[$task_name]}" \
"${TASK_LOG[$task_name]}" \
"$task_name"
done
# 4. 显示当前任务列表
show_current_tasks
log "INFO" "=========================================="
log "INFO" "✅ 定时任务配置完成"
log "INFO" "=========================================="
}
```
### 7.2 内部函数框架
```bash
# ========================================
# ✅ 函数:添加单个定时任务(内部函数)
# ========================================
function _add_single_cron_job() {
local cron_expression="$1"
local script_path="$2"
local log_path="$3"
local comment="$4"
local task_name="$5"
local task_name="$4"
# ... 检查逻辑 ...
# ... 检查逻辑和服务启动逻辑 ...
# 生成任务条目
if [[ -n "$log_path" ]]; then
......@@ -378,18 +515,30 @@ EOF
| 用例编号 | 测试场景 | 测试命令 | 预期结果 |
|----------|----------|----------|----------|
| TC-01 | 默认全部启用 | `./auto_crontab_settings.sh` | 所有任务成功添加到 crontab |
| TC-02 | 正常添加单个任务 | `./auto_crontab_settings.sh --disable-check-health` | 只添加 ujava2-startup 任务 |
| TC-03 | 禁用指定任务 | `./auto_crontab_settings.sh --disable-ujava2` | 只添加 check_server_health 任务 |
| TC-04 | 显式启用任务 | `./auto_crontab_settings.sh --enable-ujava2 --enable-check-health` | 所有任务成功添加 |
| TC-05 | 参数冲突处理 | `./auto_crontab_settings.sh --enable-ujava2 --disable-ujava2` | 打印冲突警告,跳过 ujava2 任务 |
| TC-06 | 脚本不存在 | (脚本不存在时运行) | 记录错误日志,跳过配置 |
| TC-07 | 脚本无执行权限 | (脚本无权限时运行) | 自动修复权限后成功配置 |
| TC-08 | 重复添加同一任务 | (任务已存在时运行) | 检测到已存在,跳过添加 |
| TC-09 | 脚本被删除后重新运行 | (脚本被删除后运行) | 自动移除无效任务条目 |
| TC-10 | cron 服务未运行 | (cron 未运行时执行) | 自动启动 cron 服务后配置任务 |
| TC-11 | 未知参数 | `./auto_crontab_settings.sh --unknown-param` | 显示错误信息并退出 |
| TC-12 | 显示帮助 | `./auto_crontab_settings.sh --help` | 显示帮助信息并退出 |
| TC-01 | 默认全部启用(脚本) | `./auto_crontab_settings.sh` | 所有任务成功添加到 crontab |
| TC-02 | 默认全部启用(函数) | `add_crontab_job` | 所有任务成功添加到 crontab |
| TC-03 | 正常添加单个任务(脚本) | `./auto_crontab_settings.sh --disable-check-health` | 只添加 ujava2 任务 |
| TC-04 | 正常添加单个任务(函数) | `add_crontab_job --disable-check-health` | 只添加 ujava2 任务 |
| TC-05 | 禁用指定任务(脚本) | `./auto_crontab_settings.sh --disable-ujava2` | 添加除 ujava2 外的所有任务 |
| TC-06 | 禁用指定任务(函数) | `add_crontab_job --disable-ujava2` | 添加除 ujava2 外的所有任务 |
| TC-07 | 显式启用任务(脚本) | `./auto_crontab_settings.sh --enable-ujava2 --enable-check-health` | 指定任务成功添加 |
| TC-08 | 显式启用任务(函数) | `add_crontab_job --enable-ujava2 --enable-check-health` | 指定任务成功添加 |
| TC-09 | 参数冲突处理(脚本) | `./auto_crontab_settings.sh --enable-ujava2 --disable-ujava2` | 打印冲突警告,跳过 ujava2 任务 |
| TC-10 | 参数冲突处理(函数) | `add_crontab_job --enable-ujava2 --disable-ujava2` | 打印冲突警告,跳过 ujava2 任务 |
| TC-11 | 脚本不存在 | (脚本不存在时运行) | 记录警告日志,跳过配置 |
| TC-12 | 脚本无执行权限 | (脚本无权限时运行) | 自动修复权限后成功配置 |
| TC-13 | 重复添加同一任务 | (任务已存在时运行) | 检测到已存在,跳过添加 |
| TC-14 | 脚本被删除后重新运行 | (脚本被删除后运行) | 自动移除无效任务条目 |
| TC-15 | cron 服务未运行 | (cron 未运行时执行) | 自动启动 cron 服务后配置任务 |
| TC-16 | 未知参数(脚本) | `./auto_crontab_settings.sh --unknown-param` | 显示错误信息并退出 |
| TC-17 | 未知参数(函数) | `add_crontab_job --unknown-param` | 显示错误信息并退出 |
| TC-18 | 显示帮助(脚本) | `./auto_crontab_settings.sh --help` | 显示帮助信息并退出 |
| TC-19 | 显示帮助(函数) | `add_crontab_job --help` | 显示帮助信息并退出 |
| TC-20 | 禁用多个任务(脚本) | `./auto_crontab_settings.sh --disable-monitor-redis --disable-monitor-emqx` | 除监控任务外其他任务添加 |
| TC-21 | 禁用多个任务(函数) | `add_crontab_job --disable-monitor-redis --disable-monitor-emqx` | 除监控任务外其他任务添加 |
| TC-22 | 只启用备份任务(脚本) | `./auto_crontab_settings.sh --enable-mysql-backup --enable-nginx-backup --enable-mysql-logs-backup` | 只添加备份相关任务 |
| TC-23 | 只启用备份任务(函数) | `add_crontab_job --enable-mysql-backup --enable-nginx-backup --enable-mysql-logs-backup` | 只添加备份相关任务 |
| TC-24 | 监控任务测试 | (monitor 脚本不存在时) | 跳过监控任务,添加其他任务 |
---
......@@ -413,12 +562,16 @@ EOF
| 2 | 日志格式优化 | ✅ 已实现 | 统一日志格式,包含时间戳、级别标识和 emoji 图标,提升可读性 |
| 3 | 临时文件命名优化 | ✅ 已实现 | 使用 `$RANDOM` 为临时文件添加随机后缀,避免并发冲突 |
| 4 | 清理函数逻辑优化 | ✅ 已实现 | 清理时使用临时文件而非直接修改,确保原子性操作 |
| 5 | 入口函数设计优化 | ✅ 已实现 | 将 `add_crontab_job` 改造为入口函数,支持参数传递,避免 main 函数命名冲突 |
| 6 | 内部函数分离 | ✅ 已实现 | 原添加任务逻辑移至 `_add_single_cron_job` 内部函数,职责更清晰 |
---
**文档版本:** v1.2
**文档版本:** v1.4
**创建日期:** 2026-04-10
**最后更新:** 2026-04-10
**更新内容:**
- v1.1:新增参数化控制设计(疑问11-14)
- v1.2:完成代码实现,新增优化功能回填记录
- v1.3:新增入口函数设计(疑问15-16),支持 `add_crontab_job` 函数式调用
- v1.4:新增多个定时任务(monitor_redis, monitor_emqx, nginx_backup, mysql_logs_backup, auto_clean),更新参数列表和测试用例
# _PRD_新统一平台EMQX服务监控需求文档.md
> 版本:V2.2
> 更新日期:2026-03-30
> 适用范围:新统一平台EMQX消息队列服务监测与自愈
> 实现脚本:`自动化部署脚本/x86架构/新统一平台/定时脚本/monitor_emqx_service.sh`
---
## 版本变更记录
| 版本 | 日期 | 变更内容 | 变更人 |
|------|------|----------|--------|
| V2.2 | 2026-03-30 | 新增日志自动轮转机制(5MB轮转、保留30天);新增PID文件锁机制防止并发执行;路径同步更新为新统一平台路径 | Claude |
| V2.1 | 2026-03-30 | 统一文档结构,与其他监控脚本保持一致;完善失败计数机制说明 | Claude |
| V2.0 | 2026-03-30 | 重大升级:增加连续失败计数机制、失败重试间隔、重启冷却时间、多维度健康检查、状态持久化 | AI Assistant |
| V1.0 | 2026-01-27 | 初始版本,基础检测 + 重启功能 | - |
---
## 1. 背景与目标
### 1.1 背景
EMQX作为新统一平台的MQTT消息中间件,承担着设备通信、状态同步等核心功能。为确保服务高可用性,需要建立智能化的监控与自愈机制。
### 1.2 现状问题
- 误重启问题:原脚本单次检测失败即触发重启,导致网络波动或临时阻塞时不必要的服务中断
- 缺乏缓冲机制:未考虑服务启动延迟、瞬时故障等场景
- 重启过于频繁:短时间内可能多次重启,影响业务稳定性
### 1.3 改进目标
实现EMQX服务的智能化定时监控功能,具备:
- 智能防抖动:通过连续失败计数机制,避免因单次检测失败就重启服务
- 多维度验证:结合进程、端口、CLI命令的综合健康检查
- 冷却期保护:设置重启冷却时间,防止频繁重启
- 连续失败阈值判断机制,避免单次临时堵塞触发重启影响业务
- 失败次数记录与重置机制
- 详细的日志追踪
---
## 2. 总体范围
### 2.1 纳入监控对象
| 对象 | 说明 | 检测方法 |
|------|------|----------|
| **容器名称** | `uemqx` | EMQX消息队列容器 |
| **EMQX进程** | 容器内的EMQX主进程 | `pgrep -f "emqx"` |
| **核心端口** | MQTT/MQTTS/WebSocket等监听端口 | `netstat -tuln`检查端口监听 |
| **服务状态** | EMQX服务整体健康度 | `emqx_ctl status`命令响应 |
### 2.2 监测端口清单
- **1883**: MQTT默认端口(必须)
- **8883**: MQTTS SSL端口
- **8083**: WebSocket端口
- **8084**: WebSocket SSL端口
- **18083**: Dashboard管理端口
**判定标准**:至少2个核心端口正常监听,且1883端口必须可用。
### 2.3 不在本期范围
- EMQX集群模式监控
- 消息吞吐量监控
- 客户端连接数监控
### 2.4 核心机制说明
**连续失败重启机制**:为避免因网络抖动、瞬时卡顿等临时故障导致的不必要容器重启,采用失败计数器机制:
- 单次检查失败时,仅记录失败次数并等待5秒后重试,不立即重启
- 连续失败达到阈值(默认3次)时,才触发容器重启
- 检查成功后,自动重置失败计数器
- 通过状态文件持久化保存失败次数和重启时间,避免计数器因进程重启丢失
- 设置重启冷却时间(默认30分钟),防止频繁重启影响业务稳定性
---
## 3. 术语说明
- **连续失败计数**:记录连续检测失败的次数,达到阈值才触发重启
- **重启冷却时间**:两次重启之间必须等待的最小时间间隔
- **状态持久化**:将失败计数和重启时间保存到文件,进程重启不丢失
- **多维度健康检查**:综合容器、进程、端口、CLI命令的多重验证
---
## 4. 功能需求
### 4.1 监控流程
#### 4.1.1 状态检查机制
#### 4.1.2 连续失败计数机制
- **阈值设置**: 连续失败 3 次 (`MAX_FAILURES=3`) 才触发重启
- **重置机制**: 任意一次检测成功立即清零计数器
- **失败间隔**: 每次检测间隔 5 秒 (`FAILURE_INTERVAL=5`),排除瞬时故障
#### 4.1.3 多维度健康检查
每次检测执行以下检查项:
```
┌─────────────────────────────────────┐
│ 检查 1: 容器是否运行 │
│ docker ps | grep uemqx │
└──────────────┬──────────────────────┘
┌─────────────────────────────────────┐
│ 检查 2: EMQX 进程是否存在 │
│ docker exec uemqx pgrep -f emqx │
└──────────────┬──────────────────────┘
┌─────────────────────────────────────┐
│ 检查 3: 核心端口是否监听 │
│ netstat -tuln | grep :1883 │
└──────────────┬──────────────────────┘
┌─────────────────────────────────────┐
│ 检查 4: emqx_ctl 命令响应 │
│ emqx_ctl status → "is started" │
└─────────────────────────────────────┘
```
**通过标准**: 所有检查项全部通过视为健康。
#### 4.1.4 失败处理机制(核心)
采用**连续失败阈值判断**机制,避免单次临时故障触发重启:
1. **单次失败处理**
- 任一检查环节失败时,失败计数器+1
- 将最新失败次数写入状态文件持久化保存
- 等待5秒后再次检测
- 记录警告日志,说明当前失败次数和阈值
- **未达到阈值时不触发重启**
2. **连续失败达到阈值**
- 当失败次数 ≥ MAX_FAILURES(默认3次)时
- 检查重启冷却时间是否已过
- 冷却期内:记录警告,跳过重启
- 冷却期外:清零计数器,触发容器重启
- 记录错误日志说明已连续失败N次
3. **检查成功处理**
- 所有检查项通过时
- 将失败计数器清零并更新状态文件
- 记录成功日志
- 正常退出本次检查
### 4.2 异常处理流程
当检测到异常且达到失败阈值时执行:
1. **容器重启**
- 停止现有容器
- 等待2秒
- 启动容器
- 等待30秒让EMQX服务完全启动
2. **状态验证循环**
- 验证容器运行状态
- 验证EMQX进程存在
- 验证端口监听正常
- 等待10秒后验证服务状态
3. **最终确认**
- 所有检查项都通过才算恢复成功
- 恢复成功后更新重启时间戳,清零失败计数器
- 任一环节失败都记录为恢复失败
### 4.3 重启冷却时间
- **冷却时长**: 30 分钟 (`COOLDOWN_PERIOD=1800`秒)
- **状态持久化**: 记录到 `/data/logs/.emqx_monitor_state`
- **冷却期行为**:
- ✅ 继续执行检测
- ✅ 记录失败次数
- ❌ 不执行重启动作 (除非超过冷却期)
### 4.2 自动恢复策略
#### 4.2.1 渐进式恢复流程
```
检测失败 (第 1 次)
↓ 等待 5 秒
检测失败 (第 2 次)
↓ 等待 5 秒
检测失败 (第 3 次) → 达到阈值
执行重启流程:
1. 停止现有容器
2. 等待 2 秒
3. 启动容器
4. 等待 30 秒 (START_WAIT_TIME)
5. 验证容器运行
6. 验证进程存在
7. 验证端口监听
8. 等待 10 秒 (SERVICE_CHECK_WAIT)
9. 验证服务状态
重启成功 → 更新状态,清零计数器
重启失败 → 记录日志,等待人工介入
```
#### 4.2.2 重启验证步骤
重启后依次验证:
1. **容器运行状态** (`docker ps`)
2. **EMQX 进程** (`pgrep -f emqx`)
3. **端口监听** (`netstat -tuln`)
4. **服务状态** (`emqx_ctl status`)
任何一步失败则判定重启失败。
### 4.4 日志与审计
#### 4.4.1 日志文件
- **主日志**: `/data/logs/monitor_emqx_service.log`
- **状态文件**: `/data/logs/.emqx_monitor_state`
- **PID锁文件**: `/data/logs/.emqx_monitor.pid`
#### 4.4.2 日志自动轮转机制
为避免日志文件无限增长占用磁盘空间,实现自动轮转机制:
1. **轮转触发条件**
- 当前日志文件大小达到 5MB (`MAX_LOG_SIZE=$((5*1024*1024))`)
- 每次脚本执行时自动检查
2. **轮转执行动作**
- 将当前日志文件重命名为带时间戳的旧日志:`monitor_emqx_service.log.YYYYMMDDHHMMSS`
- 创建新的空日志文件继续写入
- 记录轮转操作日志:`日志文件超过 5MB,已自动轮转。`
3. **旧日志清理**
- 自动删除超过保留天数的旧日志文件(默认30天)
- 使用 `find` 命令清理:`find ... -name "*.log.*" -mtime +30 -exec rm -f {} \;`
4. **轮转日志示例**
```
[2026-03-30 10:00:00] - 日志文件超过 5MB,已自动轮转。
```
#### 4.4.3 并发控制机制(PID锁)
为防止定时任务重叠执行导致的问题,实现PID文件锁机制:
1. **锁文件检查**
- 脚本开始执行时检查PID文件是否存在
- 如果PID文件存在,读取其中的PID值
2. **进程存活判断**
- 使用 `kill -0 $PID` 检查该进程是否仍在运行
- 如果进程存在,记录警告日志并退出本次执行
- 如果进程不存在,清理旧PID文件并继续执行
3. **锁文件创建**
- 将当前进程PID写入PID文件:`echo $$ > $PID_FILE`
- 确保PID文件目录存在,不存在则自动创建
4. **锁文件释放**
- 使用 `trap` 命令设置退出时自动清理
- 捕获信号:EXIT INT TERM HUP
- 脚本正常或异常退出时自动删除PID文件
5. **并发执行日志示例**
```
[2026-03-30 10:05:01] - 警告: 上一次脚本执行仍在运行 (PID: 12345),本次跳过执行
```
#### 4.4.2 日志格式
每行必须包含:
- 时间戳:`[YYYY-MM-DD HH:MM:SS]`
- 级别标识:信息/警告/错误/成功
- 具体操作和结果状态
- **失败计数信息**(失败时显示当前失败次数)
示例:
```
[2026-03-30 10:00:00] ==========================================
[2026-03-30 10:00:00] EMQX 服务健康检查开始
[2026-03-30 10:00:00] ==========================================
[2026-03-30 10:00:00] 配置参数:
[2026-03-30 10:00:00] - 最大失败次数:3
[2026-03-30 10:00:00] - 失败检测间隔:5 秒
[2026-03-30 10:00:00] - 重启冷却时间:1800 秒 (30 分钟)
[2026-03-30 10:00:00] ==========================================
[2026-03-30 10:00:00] 读取状态文件: 当前失败次数 = 0, 距上次重启 = 3600 秒
[2026-03-30 10:00:01] ✓ 容器运行状态正常
[2026-03-30 10:00:03] ✓ EMQX 进程已启动
[2026-03-30 10:00:03] ✓ EMQX 端口监听正常
[2026-03-30 10:00:05] ✓ EMQX 服务状态验证通过
[2026-03-30 10:00:05] 检查成功,重置失败计数器为 0
---
[2026-03-30 10:15:23] ==========================================
[2026-03-30 10:15:23] EMQX 服务健康检查开始
[2026-03-30 10:15:23] ==========================================
[2026-03-30 10:15:23] 读取状态文件: 当前失败次数 = 0, 距上次重启 = 3600 秒
[2026-03-30 10:15:24] 警告: EMQX 服务状态异常:emqx is not running
[2026-03-30 10:15:24] 失败计数器更新: 0 -> 1 (阈值: 3)
[2026-03-30 10:15:29] 警告: EMQX 服务状态异常:emqx is not running
[2026-03-30 10:15:29] 失败计数器更新: 1 -> 2 (阈值: 3)
[2026-03-30 10:15:34] 警告: EMQX 服务状态异常:emqx is not running
[2026-03-30 10:15:34] 失败计数器更新: 2 -> 3 (阈值: 3)
[2026-03-30 10:15:34] 错误: 连续失败达到阈值(3次),触发容器重启
[2026-03-30 10:15:34] 重置失败计数器为 0
[2026-03-30 10:15:34] ==========================================
[2026-03-30 10:15:34] 开始执行 EMQX 容器重启流程
[2026-03-30 10:15:34] ==========================================
[2026-03-30 10:15:34] 距上次重启时间:3600 秒 (冷却期已过)
[2026-03-30 10:15:36] 容器启动命令执行成功
[2026-03-30 10:15:36] 等待 30 秒让 EMQX 服务完全启动...
[2026-03-30 10:16:06] 验证服务启动结果...
[2026-03-30 10:16:06] ✓ 容器运行状态正常
[2026-03-30 10:16:11] ✓ EMQX 进程已启动
[2026-03-30 10:16:11] ✓ EMQX 端口监听正常
[2026-03-30 10:16:21] ✓ EMQX 服务状态验证通过
[2026-03-30 10:16:21] ==========================================
[2026-03-30 10:16:21] EMQX 容器重启成功,更新重启时间戳
[2026-03-30 10:16:21] ==========================================
```
---
## 5. 配置参数
### 5.1 核心配置参数
| 参数名 | 变量名 | 默认值 | 说明 | 调整建议 |
|--------|--------|--------|------|----------|
| **容器名称** | `CONTAINER_NAME` | uemqx | EMQX容器名称 | 根据实际部署调整 |
| **日志文件** | `LOG_FILE` | /data/logs/monitor_emqx_service.log | 日志文件路径 | 根据实际环境调整 |
| **状态文件** | `STATE_FILE` | /data/logs/.emqx_monitor_state | 失败计数器状态文件 | 固定路径 |
| **PID文件** | `PID_FILE` | /data/logs/.emqx_monitor.pid | 进程锁文件,防止并发执行 | 固定路径 |
| **日志大小限制** | `MAX_LOG_SIZE` | 5MB | 日志文件大小限制,超过后自动轮转 | 可根据需要调整 |
| **日志保留天数** | `LOG_RETENTION_DAYS` | 30天 | 轮转后的日志文件保留天数 | 可根据需要调整 |
| **最大失败次数** | `MAX_FAILURES` | 3 | 连续失败达此阈值触发重启 | 网络不稳定环境可增至 4-5 次 |
| **失败检测间隔** | `FAILURE_INTERVAL` | 5 秒 | 每次检测之间的等待时间 | 可调整为 3-10 秒 |
| **重启冷却时间** | `COOLDOWN_PERIOD` | 1800 秒 (30 分钟) | 两次重启之间的最小间隔 | 关键业务可延长至 60 分钟 |
| **启动等待时间** | `START_WAIT_TIME` | 30 秒 | 容器启动后的等待时长 | 根据实际启动速度调整 |
| **服务检查等待** | `SERVICE_CHECK_WAIT` | 10 秒 | 验证服务状态前的等待 | 可调整为 5-15 秒 |
### 5.2 状态文件字段
状态文件:`/data/logs/.emqx_monitor_state`
```bash
FAILURE_COUNT=0 # 当前连续失败次数
LAST_RESTART_TIME=0 # 上次重启时间戳 (Unix timestamp)
LAST_SUCCESS_TIME=0 # 上次成功检测时间戳
```
---
## 6. 执行要求
### 6.1 执行时机
```cron
*/5 * * * * /data/services/scripts/monitor_emqx_service.sh
```
**执行频率**:每5分钟执行一次
### 6.2 执行环境
- **操作系统**:Linux (CentOS/Ubuntu/Debian等主流发行版)
- **必需工具**:Docker、bash、netstat(或ss)
- **权限要求**:root或sudo权限(用于访问Docker和写入日志)
- **磁盘空间**:至少100MB(日志文件增长需监控)
- **内存**:最低512MB可用内存
### 6.3 资源要求
为避免同时执行造成资源竞争,建议错开执行时间:
| 脚本 | 执行时间 | 偏移量 |
|------|----------|--------|
| `monitor_emqx_service.sh` | */5 * * * * | +0 分钟 |
| `monitor_mysql_service.sh` | */5 * * * * | +1 分钟 |
| `monitor_redis_service.sh` | */5 * * * * | +2 分钟 |
| `monitor_external_api_services_v2.sh` | */5 * * * * | +3 分钟 |
| `monitor_inner_api_services.sh` | */5 * * * * | +4 分钟 |
**crontab 配置示例**:
```cron
*/5 * * * * /data/services/scripts/monitor_emqx_service.sh
*/5 * * * * sleep 60 && /data/services/scripts/monitor_mysql_service.sh
*/5 * * * * sleep 120 && /data/services/scripts/monitor_redis_service.sh
*/5 * * * * sleep 180 && /data/services/scripts/monitor_external_api_services_v2.sh
*/5 * * * * sleep 240 && /data/services/scripts/monitor_inner_api_services.sh
```
---
## 7. 错误处理
### 7.1 错误处理流程
| 级别 | 标识 | 使用场景 |
|------|------|----------|
| **信息** | 无标记 | 正常检测通过、配置说明 |
| **警告** | `警告:` | 冷却期内检测、非致命异常 |
| **错误** | `错误:` | 重启失败、容器不存在等严重问题 |
### 7.2 错误处理流程
```
检测失败
记录失败次数 → 判断是否达阈值
是 → 检查冷却期
冷却期内 → 记录日志,跳过重启
冷却期外 → 执行重启
重启成功 → 更新状态,返回成功
重启失败 → 记录详细错误,提示人工介入
```
### 7.3 异常情况处理
| 异常场景 | 处理策略 | 日志输出 |
|----------|----------|----------|
| **容器不存在** | 放弃重启,提示手动创建 | `错误:容器 'uemqx' 不存在!` |
| **启动后立即停止** | 记录失败,查看容器日志 | `错误:容器启动后立即停止` |
| **冷却期内故障** | 只记录不重启 | `警告:容器在冷却期内` |
| **端口检查工具缺失** | 降级检查 (跳过端口验证) | 记录警告信息 |
| **状态文件损坏** | 重建状态文件,从零开始 | 自动初始化 |
---
## 8. 验收标准
### 8.1 监测准确性
- ✅ 能正确识别容器运行状态
- ✅ 能准确检测EMQX进程状态
- ✅ 能正确验证端口监听状态
- ✅ 能正确执行CLI命令检查
### 8.2 失败计数机制
- ✅ 单次失败时正确累加计数器
- ✅ 失败计数能持久化保存,进程重启不丢失
- ✅ 检查成功时正确重置计数器为0
- ✅ 未达阈值时不会触发重启
- ✅ 达到阈值时正确触发重启并清零计数器
- ✅ 冷却期内不会重复触发重启
### 8.3 恢复有效性
- ✅ 连续失败达到阈值后能正确触发容器重启
- ✅ 重启操作能成功执行
- ✅ 重启后EMQX服务确实正常运行
- ✅ 冷却时间机制有效防止频繁重启
### 8.4 日志完整性
- ✅ 每次检查都有完整的开始和结束记录
- ✅ 日志中包含当前失败次数和阈值信息
- ✅ 异常情况有详细的诊断信息
- ✅ 恢复过程有清晰的步骤记录
- ✅ 不会静默失败
### 8.5 功能验收
#### 8.5.1 正常场景
- ✅ 服务正常时,检测通过且不触发重启
- ✅ 单次检测失败后,等待 5 秒再次检测
- ✅ 连续 3 次失败后才执行重启
- ✅ 重启后服务恢复,计数器清零
#### 8.5.2 异常场景
- ✅ 网络波动导致单次失败,不触发重启
- ✅ 冷却期内检测到故障,不执行重启
- ✅ 容器不存在时,输出明确错误提示
- ✅ 重启失败后,提供详细的诊断建议
### 8.6 性能验收
- ✅ 单次检测耗时 < 3 秒
- ✅ 重启流程总耗时 < 2 分钟
- ✅ 日志文件大小控制在合理范围 (支持轮转)
### 8.7 稳定性验收
- ✅ 连续运行 30 天无异常
- ✅ 重启次数符合预期 (非频繁重启)
- ✅ 状态文件数据准确,无丢失损坏
---
## 9. 故障排查
### 9.1 常见问题
```
问题:EMQX 服务不可用
步骤 1: 查看最近日志
tail -100 /data/logs/monitor_emqx_service.log
步骤 2: 检查容器状态
docker ps -a | grep uemqx
步骤 3: 查看容器日志
docker logs uemqx --tail 50
步骤 4: 检查状态文件
cat /data/logs/.emqx_monitor_state
步骤 5: 手动执行检测脚本
bash -x /data/services/scripts/monitor_emqx_service.sh
```
### 9.2 常见问题处理
#### 问题 1: 服务检查失败(未达阈值)
```
[警告] EMQX 服务状态异常:emqx is not running
[警告] 失败计数器更新: 0 -> 1 (阈值: 3)
```
说明:这是正常现象,系统在等待下次重试确认是否为持续故障
#### 问题 2: 服务检查失败(达到阈值)
```
[错误] 连续失败达到阈值(3次),触发容器重启
```
说明:已连续3次检查失败,系统正在执行自动恢复
#### 问题 3: 冷却期内故障
```
[警告] 容器在冷却期内,距上次重启仅 300 秒
```
说明:这是设计行为,防止频繁重启影响业务
#### 问题 4: 频繁重启
**现象**: 日志中频繁出现重启记录
**原因**:
- 服务本身存在问题,无法正常启动
- 配置参数过于敏感
**解决**:
```bash
# 1. 查看重启频率
grep "重启" /data/logs/monitor_emqx_service.log | tail -20
# 2. 调整冷却时间 (延长至 60 分钟)
编辑脚本,修改 COOLDOWN_PERIOD=3600
# 3. 查看容器日志定位根本原因
docker logs uemqx --tail 100
```
#### 问题 5: 状态文件异常
**现象**: 计数器不准确或重启时间错误
**解决**:
```bash
# 1. 备份并删除状态文件
cp /data/logs/.emqx_monitor_state /data/logs/.emqx_monitor_state.bak
rm /data/logs/.emqx_monitor_state
# 2. 重启脚本会自动重建状态文件
/data/services/scripts/monitor_emqx_service.sh
```
### 9.3 诊断命令速查表
| 目的 | 命令 | 说明 |
|------|------|------|
| **查看实时日志** | `tail -f /data/logs/monitor_emqx_service.log` | 实时监控检测过程 |
| **统计异常发生频率** | `grep "警告\|错误" /data/logs/monitor_emqx_service.log \| wc -l` | 统计异常次数 |
| **查看失败计数变化趋势** | `grep "失败计数器更新" /data/logs/monitor_emqx_service.log` | 分析计数变化 |
| **统计触发重启的次数** | `grep "连续失败达到阈值" /data/logs/monitor_emqx_service.log \| wc -l` | 统计重启次数 |
| **查看最近重启记录** | `grep "重启" /data/logs/monitor_emqx_service.log \| tail -10` | 分析重启频率 |
| **检查容器状态** | `docker ps -a \| grep uemqx` | 查看容器运行状态 |
| **查看容器日志** | `docker logs uemqx --tail 50` | 排查容器内部问题 |
| **查看状态文件** | `cat /data/logs/.emqx_monitor_state` | 查看计数器和时间戳 |
| **手动重置失败计数器** | `echo "FAILURE_COUNT=0\nLAST_RESTART_TIME=0\nLAST_SUCCESS_TIME=0" > /data/logs/.emqx_monitor_state` | 恢复后无需手动操作 |
| **手动执行检测** | `bash -x /data/services/scripts/monitor_emqx_service.sh` | 调试模式执行脚本 |
| **检查端口监听** | `docker exec uemqx netstat -tuln` | 验证端口是否正常 |
| **检查进程状态** | `docker exec uemqx pgrep -f emqx` | 验证进程是否存在 |
| **检查服务状态** | `docker exec uemqx emqx_ctl status` | 验证服务是否健康 |
---
## 10. 维护建议
### 10.1 定期检查
- **每日**: 查看日志是否有异常重启记录
- **每周**: 检查状态文件大小和内容
- **每月**: 评估重启频率是否合理
### 10.2 日志管理
- **日志轮转**: 建议配置 logrotate 定期压缩旧日志
- **清理策略**: 保留最近 30 天的日志
- **大小监控**: 当日志文件>100MB 时进行轮转
### 10.3 参数调优
根据实际运行情况调整参数:
- **网络不稳定环境**: 增加 `MAX_FAILURES` 至 4-5 次
- **启动缓慢容器**: 增加 `START_WAIT_TIME` 至 60 秒
- **关键业务场景**: 延长 `COOLDOWN_PERIOD` 至 60 分钟
### 10.4 备份策略
- **脚本备份**: `/data/services/scripts/monitor_emqx_service.sh` 纳入版本控制
- **配置备份**: 定期备份 `/data/logs/.emqx_monitor_state`
- **日志备份**: 重要时期的日志单独归档
---
## 11. 附录
### 11.1 相关文档
- `_PRD_新统一平台定时脚本需求文档概览.md` - 总体设计规范
- `monitor_emqx_service.sh` - 实际执行脚本
- `/data/logs/monitor_emqx_service.log` - 运行日志
- 《自动化脚本、代码生成与文档标准化综合规范》 - 脚本开发规范
- 《定时任务配置同步规范》 - 调度配置规范
### 11.2 缩略语说明
| 缩写 | 全称 | 说明 |
|------|------|------|
| EMQX | ElasticMQ Telemetry X | 开源 MQTT 消息代理 |
| MQTT | Message Queuing Telemetry Transport | 消息队列遥测传输协议 |
| CLI | Command Line Interface | 命令行界面 |
| API | Application Programming Interface | 应用程序编程接口 |
| SSL | Secure Sockets Layer | 安全套接字层 |
| WebSocket | WebSocket Protocol | 全双工通信协议 |
### 11.3 参考资料
- EMQX 官方文档:https://www.emqx.io/docs/
- Docker 官方文档:https://docs.docker.com/
- Crontab 配置指南:https://man7.org/linux/man-pages/man5/crontab.5.html
---
## 需求规范
- 代码规范: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
\ No newline at end of file
# 新统一平台EMQX服务监控 - 计划执行文档
> 版本:V2.2
> 创建日期:2026-04-10
> 需求文档:`_PRD_新统一平台_EMQX 服务监控需求文档.md`
> 代码路径:`自动化部署脚本/x86架构/新统一平台/定时脚本/monitor_emqx_service.sh`
---
## 1. 执行概述
### 1.1 项目背景
EMQX作为新统一平台的MQTT消息中间件,承担设备通信、状态同步等核心功能。为确保服务高可用性,需要建立智能化的监控与自愈机制。
### 1.2 执行目标
实现EMQX服务的智能化监控与自愈脚本,具备:
- 多维度健康检查(容器 + 进程 + 端口 + CLI命令)
- 连续失败阈值判断机制(3次阈值,避免误触发)
- 重启冷却时间保护(30分钟,防止频繁重启)
- 状态持久化机制(进程重启不丢失计数)
- 日志自动轮转(5MB/30天)
- 并发执行控制(PID文件锁)
### 1.3 执行范围
- [x] 脚本基础框架搭建
- [x] 配置参数定义
- [x] 多维度健康检查功能
- [x] 连续失败阈值机制
- [x] 重启冷却时间保护
- [x] 状态持久化机制
- [x] 自动重启恢复功能
- [x] 日志自动轮转功能
- [x] PID文件锁机制
- [x] 需求文档同步更新
---
## 2. 任务分解与实施计划
### 2.1 任务分解
| 任务编号 | 任务名称 | 优先级 | 预计工作量 | 状态 |
| --- | --- | --- | --- | --- |
| T1 | 脚本基础框架搭建 | 高 | 30分钟 | [x] |
| T2 | 配置参数定义 | 高 | 20分钟 | [x] |
| T3 | PID文件锁机制实现 | 高 | 25分钟 | [x] |
| T4 | 日志自动轮转功能实现 | 中 | 20分钟 | [x] |
| T5 | 状态持久化机制实现 | 高 | 30分钟 | [x] |
| T6 | 多维度健康检查实现 | 高 | 45分钟 | [x] |
| T7 | 连续失败阈值机制实现 | 高 | 40分钟 | [x] |
| T8 | 重启冷却时间保护实现 | 高 | 35分钟 | [x] |
| T9 | 自动重启恢复功能实现 | 高 | 50分钟 | [x] |
| T10 | 需求文档同步更新 | 高 | 40分钟 | [x] |
### 2.2 实施计划
#### T1: 脚本基础框架搭建
**文件位置**`自动化部署脚本/x86架构/新统一平台/定时脚本/monitor_emqx_service.sh`
**实现内容**
```bash
#!/bin/bash
#===============================================================================
# 脚本名称:monitor_emqx_service.sh
# 功能描述:EMQX 服务监测与自愈脚本 (增强版)
# 版本:V2.2
# 创建日期:2026-01-27
# 修改日期:2026-03-30
#===============================================================================
```
#### T2: 配置参数定义
**实现内容**
```bash
# 配置参数
LOG_FILE="/data/logs/monitor_emqx_service.log"
STATE_FILE="/data/logs/.emqx_monitor_state"
PID_FILE="/data/logs/.emqx_monitor.pid"
MAX_LOG_SIZE=$((5*1024*1024)) # 5MB 日志大小限制
LOG_RETENTION_DAYS=30 # 日志保留天数
MAX_FAILURES=3 # 最大连续失败次数
COOLDOWN_PERIOD=1800 # 重启冷却时间 (秒,30 分钟)
START_WAIT_TIME=30 # 启动后等待时间 (秒)
SERVICE_CHECK_WAIT=10 # 服务状态检查等待时间 (秒)
```
#### T3: PID文件锁机制实现
**函数名**`acquire_lock``release_lock`
**实现要点**
- 检查PID文件是否存在
- 检查进程是否仍在运行
- 清理旧的PID文件
- 写入当前进程PID
- 设置trap自动清理
**核心代码**
```bash
acquire_lock() {
# 检查 PID 文件是否存在
if [ -f "$PID_FILE" ]; then
local old_pid=$(cat "$PID_FILE" 2>/dev/null)
# 检查该进程是否还在运行
if [ -n "$old_pid" ] && kill -0 "$old_pid" 2>/dev/null; then
log "警告: 上一次脚本执行仍在运行 (PID: $old_pid),本次跳过执行"
exit 1
else
# 进程不存在,清理旧的 PID 文件
rm -f "$PID_FILE"
fi
fi
# 确保 PID 文件目录存在
mkdir -p "$(dirname "$PID_FILE")" 2>/dev/null || true
# 写入当前进程 PID
echo $$ > "$PID_FILE"
# 设置退出时清理 PID 文件
trap 'rm -f "$PID_FILE"; exit' EXIT INT TERM HUP
}
```
#### T4: 日志自动轮转功能实现
**函数名**`rotate_logs`
**实现要点**
- 检查日志文件大小
- 超过5MB时自动轮转
- 清理30天前的旧日志
**核心代码**
```bash
rotate_logs() {
if [ -f "$LOG_FILE" ]; then
FILE_SIZE=$(stat -c%s "$LOG_FILE" 2>/dev/null || echo 0)
if [ "$FILE_SIZE" -ge "$MAX_LOG_SIZE" ]; then
mv "$LOG_FILE" "$LOG_FILE.$(date '+%Y%m%d%H%M%S')"
touch "$LOG_FILE"
log "日志文件超过 5MB,已自动轮转。"
fi
fi
# 清理超过保留天数的旧日志文件
find "$(dirname "$LOG_FILE")" -name "monitor_emqx_service.log.*" -mtime +$LOG_RETENTION_DAYS -exec rm -f {} \; 2>/dev/null
}
```
#### T5: 状态持久化机制实现
**函数名**`read_state_file``write_state_file``get_last_restart_time`
**实现要点**
- 读取失败计数器
- 写入失败计数器和重启时间
- 获取上次重启时间
**核心代码**
```bash
# 读取状态文件
read_state_file() {
if [ -f "$STATE_FILE" ]; then
source "$STATE_FILE"
echo "${FAILURE_COUNT:-0}"
else
echo "0"
fi
}
# 写入状态文件
write_state_file() {
local count=$1
local restart_time=$2
cat > "$STATE_FILE" << EOF
FAILURE_COUNT=$count
LAST_RESTART_TIME=$restart_time
LAST_CHECK_TIME=$(date +%s)
EOF
}
# 获取上次重启时间
get_last_restart_time() {
if [ -f "$STATE_FILE" ]; then
source "$STATE_FILE"
echo "${LAST_RESTART_TIME:-0}"
else
echo "0"
fi
}
```
#### T6: 多维度健康检查实现
**函数名**`check_emqx_ports``comprehensive_health_check`
**实现要点**
- 检查容器是否运行
- 检查EMQX进程是否存在
- 检查核心端口是否监听(至少2个端口,必须包含1883)
- 检查emqx_ctl命令响应
**核心代码**
```bash
# 检查 EMQX 端口是否监听
check_emqx_ports() {
local ports=(1883 8883 8083 8084 18083)
local listening_count=0
for port in "${ports[@]}"; do
if docker exec uemqx netstat -tuln 2>/dev/null | grep -q ":${port} "; then
((listening_count++))
fi
done
# 至少有 2 个核心端口监听认为正常 (1883 必须)
if [ $listening_count -ge 2 ] && docker exec uemqx netstat -tuln 2>/dev/null | grep -q ":1883 "; then
return 0
else
return 1
fi
}
# 综合健康检查
comprehensive_health_check() {
# 检查 1: 容器是否运行
if ! docker ps --format '{{.Names}}' | grep -Fxq "uemqx"; then
return 1
fi
# 检查 2: EMQX 进程是否存在
if ! docker exec uemqx pgrep -f "emqx" >/dev/null 2>&1; then
return 1
fi
# 检查 3: 核心端口是否监听
if ! check_emqx_ports; then
return 1
fi
# 检查 4: emqx_ctl 命令响应
local status_output=$(docker exec uemqx emqx_ctl status 2>&1)
if ! echo "$status_output" | grep -q "is started"; then
return 1
fi
return 0
}
```
#### T7: 连续失败阈值机制实现
**实现要点**
- 单次失败时累加计数器
- 达到阈值(3次)时触发重启
- 检查成功时清零计数器
- 持久化保存状态
**核心逻辑**
```bash
# 读取当前失败次数
failure_count=$(read_state_file)
# 执行健康检查
if comprehensive_health_check; then
# 检查成功,清零计数器
if [ "$failure_count" -gt 0 ]; then
write_state_file 0 "$last_restart_time"
log "检查成功,重置失败计数器为 0"
fi
log "EMQX 服务状态正常"
else
# 检查失败,累加计数器
failure_count=$((failure_count + 1))
write_state_file "$failure_count" "$last_restart_time"
log "警告: EMQX 服务状态异常"
log "失败计数器更新: $((failure_count - 1)) -> $failure_count (阈值: $MAX_FAILURES)"
# 判断是否达到阈值
if [ "$failure_count" -ge "$MAX_FAILURES" ]; then
log "错误: 连续失败达到阈值($MAX_FAILURES次),触发容器重启"
write_state_file 0 "$last_restart_time"
restart_emqx_container
fi
fi
```
#### T8: 重启冷却时间保护实现
**实现要点**
- 记录上次重启时间戳
- 计算距上次重启的时间间隔
- 冷却期内跳过重启
- 冷却期外正常执行重启
**核心逻辑**
```bash
# 检查冷却期
local current_time=$(date +%s)
local time_since_restart=$((current_time - last_restart_time))
if [ $last_restart_time -gt 0 ] && [ $time_since_restart -lt $COOLDOWN_PERIOD ]; then
log "警告: 距上次重启仅 ${time_since_restart} 秒,冷却期未过(${COOLDOWN_PERIOD}秒),跳过本次重启"
log "失败计数器保持为 $failure_count,等待冷却期过后重新检查"
else
# 执行重启
restart_emqx_container
fi
```
#### T9: 自动重启恢复功能实现
**函数名**`restart_emqx_container`
**实现流程**
1. 检查容器是否存在
2. 停止现有容器
3. 启动容器
4. 等待30秒让EMQX服务启动
5. 验证容器运行状态
6. 验证EMQX进程存在
7. 验证端口监听正常
8. 等待10秒后验证服务状态
**核心代码**
```bash
restart_emqx_container() {
local current_time=$(date +%s)
local last_restart_time=$(get_last_restart_time)
local time_since_restart=$((current_time - last_restart_time))
log "开始执行 EMQX 容器重启流程"
log "距上次重启时间:${time_since_restart}秒"
# 检查容器是否存在
if ! docker ps -a --format '{{.Names}}' | grep -Fxq "uemqx"; then
log "错误: 容器 'uemqx' 不存在!"
return 1
fi
# 停止容器
docker stop uemqx >/dev/null 2>&1 || true
sleep 2
# 启动容器
if docker start uemqx; then
log "容器启动命令执行成功"
# 等待服务启动
log "等待 ${START_WAIT_TIME} 秒让 EMQX 服务完全启动..."
for i in $(seq 1 $START_WAIT_TIME); do
echo -n "." >> "$LOG_FILE"
sleep 1
done
echo "" >> "$LOG_FILE"
# 验证启动结果
# ... (省略验证步骤)
return 0
else
log "错误: 无法启动容器 'uemqx'"
return 1
fi
}
```
#### T10: 需求文档同步更新
**更新内容**
1. 文档标题:预定系统 → 新统一平台
2. 版本号:V2.1 → V2.2
3. 所有路径更新:
- `/var/log/scripts/``/data/logs/`
- `/opt/scripts/``/data/services/scripts/`
4. 版本变更记录:添加V2.2版本说明
5. 诊断命令:更新所有路径
---
## 3. 关键函数设计
### 3.1 comprehensive_health_check 函数
**功能**:综合健康检查,多维度验证EMQX服务状态
**返回值**
- 0 = 服务健康
- 1 = 服务异常
**检查流程**
```
1. 容器运行检查 (docker ps)
2. 进程存在检查 (pgrep -f emqx)
3. 端口监听检查 (netstat -tuln)
- 1883 (必须)
- 至少2个端口正常
4. CLI命令检查 (emqx_ctl status)
全部通过 → 返回成功
任一失败 → 返回失败
```
### 3.2 restart_emqx_container 函数
**功能**:重启EMQX容器并验证服务恢复
**返回值**
- 0 = 重启成功且服务正常
- 1 = 重启失败
**实现流程**
```
1. 检查容器是否存在
↓ 不存在
返回失败
↓ 存在
2. 停止容器
3. 等待2秒
4. 启动容器
↓ 启动失败
返回失败
↓ 启动成功
5. 等待30秒
6. 验证容器运行状态
7. 验证进程状态
8. 验证端口监听
9. 等待10秒
10. 验证服务状态
↓ 全部通过
返回成功,更新重启时间戳
↓ 任一失败
返回失败
```
### 3.3 acquire_lock 函数
**功能**:获取PID文件锁,防止并发执行
**实现流程**
```
1. 检查PID文件是否存在
↓ 不存在
跳到步骤4
↓ 存在
2. 读取PID值
3. 检查进程是否运行
↓ 运行中
记录警告并退出
↓ 不运行
清理旧PID文件
4. 创建PID文件目录(如不存在)
5. 写入当前进程PID
6. 设置trap退出时清理
```
---
## 4. 配置参数说明
| 参数名 | 默认值 | 说明 |
| --- | --- | --- |
| LOG_FILE | /data/logs/monitor_emqx_service.log | 脚本日志文件路径 |
| STATE_FILE | /data/logs/.emqx_monitor_state | 状态文件路径 |
| PID_FILE | /data/logs/.emqx_monitor.pid | PID锁文件路径 |
| MAX_LOG_SIZE | 5242880 (5MB) | 日志文件大小限制 |
| LOG_RETENTION_DAYS | 30 | 日志保留天数 |
| MAX_FAILURES | 3 | 触发重启的最大连续失败次数 |
| COOLDOWN_PERIOD | 1800 (30分钟) | 重启冷却时间(秒) |
| START_WAIT_TIME | 30 | 启动后等待时间(秒) |
| SERVICE_CHECK_WAIT | 10 | 服务状态检查等待时间(秒) |
---
## 5. 监测端口清单
| 端口 | 协议 | 说明 | 必需性 |
| --- | --- | --- | --- |
| 1883 | MQTT | MQTT默认端口 | 必须 |
| 8883 | MQTTS | MQTT SSL端口 | 可选 |
| 8083 | WebSocket | WebSocket端口 | 可选 |
| 8084 | WebSocket SSL | WebSocket SSL端口 | 可选 |
| 18083 | Dashboard | 管理界面端口 | 可选 |
**判定标准**:至少2个核心端口正常监听,且1883端口必须可用。
---
## 6. 主流程设计
```
脚本开始
获取锁(防止并发)
↓ 失败
记录警告并退出
↓ 成功
执行日志轮转
读取当前状态(失败次数、上次重启时间)
执行综合健康检查
↓ 成功
清零失败计数器(如需要)
记录成功日志
退出
↓ 失败
失败计数器+1
记录警告日志
判断是否达到阈值
↓ 未达到
退出
↓ 达到
清零失败计数器
检查冷却期
↓ 冷却期内
记录警告,跳过重启
退出
↓ 冷却期外
执行重启流程
↓ 成功
更新重启时间戳
记录成功日志
↓ 失败
记录错误日志
脚本结束
```
---
## 7. 验收标准
### 7.1 功能验收
- [x] 能正确识别容器运行状态
- [x] 能准确检测EMQX进程状态
- [x] 能正确验证端口监听状态(至少2个端口,含1883)
- [x] 能正确执行CLI命令检查
- [x] 单次失败时正确累加计数器,不触发重启
- [x] 连续失败达到阈值(3次)时正确触发重启
- [x] 检查成功时正确重置计数器为0
- [x] 冷却期内不会重复触发重启
- [x] 重启后能正确验证服务状态
- [x] 日志文件达到5MB时自动轮转
- [x] 轮转后的日志保留30天后自动清理
- [x] PID文件锁能防止并发执行
### 7.2 代码规范验收
- [x] 所有函数有中文注释说明
- [x] 配置参数有中文说明
- [x] 关键逻辑步骤有中文注释
- [x] 日志格式统一
- [x] 变量使用双引号保护
### 7.3 安全性验收
- [x] PID文件正确释放
- [x] 异常退出时清理资源
- [x] 冷却期保护机制有效
---
## 8. 测试计划
### 8.1 单元测试场景
| 测试场景 | 预期结果 |
| --- | --- |
| EMQX服务正常(所有检查通过) | 检查成功,计数器清零 |
| EMQX容器未运行 | 检查失败,计数器+1 |
| EMQX进程不存在 | 检查失败,计数器+1 |
| EMQX端口未监听 | 检查失败,计数器+1 |
| emqx_ctl命令失败 | 检查失败,计数器+1 |
| 第1次检查失败 | 计数器=1,不触发重启 |
| 第2次检查失败 | 计数器=2,不触发重启 |
| 第3次检查失败 | 计数器=3,触发重启 |
| 重启后服务恢复 | 检查成功,计数器清零 |
| 冷却期内再次失败 | 不触发重启 |
| 日志文件超过5MB | 自动轮转日志文件 |
| 并发执行脚本 | 第二个实例直接退出 |
### 8.2 集成测试场景
| 测试场景 | 预期结果 |
| --- | --- |
| EMQX容器不存在 | 记录错误并建议手动创建 |
| 网络临时抖动 | 未达阈值不触发重启 |
| 服务启动缓慢 | 等待30秒后验证 |
| 冷却期内连续失败 | 记录警告但不重启 |
| 定时任务频繁执行 | PID锁防止并发 |
---
## 9. 实施记录
### 9.1 实施进度
| 日期 | 完成任务 | 负责人 | 备注 |
| --- | --- | --- | --- |
| 2026-01-27 | 初始版本开发(V1.0) | - | 基础监控功能 |
| 2026-03-30 | 重大升级(V2.0) | AI Assistant | 连续失败阈值、冷却时间、多维度检查 |
| 2026-03-30 | 文档完善(V2.1) | Claude | 统一文档结构 |
| 2026-03-30 | 功能完善(V2.2) | Claude | 日志轮转、PID锁、路径更新 |
### 9.2 变更记录
| 日期 | 变更内容 | 变更原因 |
| --- | --- | --- |
| 2026-03-30 | 新增日志自动轮转机制(5MB/30天) | 防止日志文件过大 |
| 2026-03-30 | 新增PID文件锁机制 | 防止并发执行冲突 |
| 2026-03-30 | 更新所有路径为新统一平台路径 | 与实际部署环境同步 |
---
## 10. 风险评估
| 风险项 | 风险等级 | 应对措施 |
| --- | --- | --- |
| 频繁重启影响业务 | 中 | 30分钟冷却时间保护 |
| 误判导致不必要重启 | 低 | 连续失败3次阈值,多维度验证 |
| 日志文件过大 | 低 | 5MB自动轮转,30天自动清理 |
| 并发执行冲突 | 低 | PID文件锁机制 |
| 状态文件丢失 | 低 | 自动初始化,从零开始 |
---
## 11. 后续工作
- [ ] 监控脚本运行情况
- [ ] 根据实际运行情况调整失败阈值
- [ ] 考虑增加钉钉/邮件告警通知
- [ ] 增加EMQX性能指标监控
- [ ] 增加客户端连接数监控
---
## 12. 附录
### 12.1 相关文档
- 需求文档:`_PRD_新统一平台_EMQX 服务监控需求文档.md`
- 代码规范:`_PRD_规范文档_代码规范.md`
- 测试规范:`_PRD_规范文档_测试规范.md`
### 12.2 部署信息
| 项目 | 值 |
| --- | --- |
| 脚本路径 | `/data/services/scripts/monitor_emqx_service.sh` |
| 日志路径 | `/data/logs/monitor_emqx_service.log` |
| 状态文件 | `/data/logs/.emqx_monitor_state` |
| PID文件 | `/data/logs/.emqx_monitor.pid` |
| 定时任务 | `*/5 * * * * /data/services/scripts/monitor_emqx_service.sh` |
| 容器名称 | `uemqx` |
### 12.3 监测端口
| 端口 | 协议 | 说明 |
| --- | --- | --- |
| 1883 | MQTT | MQTT默认端口(必须) |
| 8883 | MQTTS | MQTT SSL端口 |
| 8083 | WebSocket | WebSocket端口 |
| 8084 | WebSocket SSL | WebSocket SSL端口 |
| 18083 | Dashboard | 管理界面端口 |
### 12.4 快速诊断命令
```bash
# 查看实时日志
tail -f /data/logs/monitor_emqx_service.log
# 查看失败计数器
cat /data/logs/.emqx_monitor_state
# 检查容器状态
docker ps -a | grep uemqx
# 查看容器日志
docker logs uemqx --tail 50
# 检查端口监听
docker exec uemqx netstat -tuln
# 检查进程状态
docker exec uemqx pgrep -f emqx
# 检查服务状态
docker exec uemqx emqx_ctl status
```
---
**文档状态**:已完成
**最后更新**:2026-04-10
# _PRD_新统一平台清理垃圾进程释放空间需求文档.md
> 版本:V3.1
> 更新日期:2026-03-30
> 适用范围:新统一平台垃圾文件清理与空间释放
> 实现脚本:`自动化部署脚本/x86架构/新统一平台/定时脚本/auto_clean_deleted_ubains_v3.sh`
---
## 版本变更记录
| 版本 | 日期 | 变更内容 | 变更人 |
|------|------|----------|--------|
| V3.1 | 2026-03-30 | 新增日志自动轮转机制(5MB轮转、保留7天);新增特定应用启动支持 | Claude |
| V3.0 | 2026-01-27 | 初始版本,实现已删除大文件扫描和自动清理功能 | - |
---
## 1. 背景与目标
### 1.1 背景
新统一平台运行过程中,部分应用程序(如Java服务)可能会产生大量的日志文件。当这些日志文件被删除后,如果进程仍然占用文件句柄,磁盘空间不会被立即释放,形成"已删除但未释放"的垃圾文件,持续占用磁盘空间。
### 1.2 问题场景
- 应用程序产生大量日志文件(ubains-INFO-AND-ERROR.log)
- 运维人员删除日志文件释放空间
- 进程仍占用文件句柄,磁盘空间未实际释放
- 累积大量已删除大文件,导致磁盘空间不足
### 1.3 目标
实现自动化垃圾文件清理机制,具备:
- 自动扫描已删除但仍被进程占用的大文件
- 按关键字和文件大小智能匹配
- 强制终止占用进程释放磁盘空间
- 自动重启关联容器恢复服务
- 特定应用自动启动支持
- 日志自动轮转功能
- 完整的操作审计记录
---
## 2. 总体范围
### 2.1 纳入清理对象
| 对象 | 说明 | 检测方法 |
|------|------|----------|
| **已删除文件** | 进程fd目录中标记为deleted的文件 | 读取/proc/[pid]/fd/*符号链接 |
| **文件关键字** | ubains-INFO-AND-ERROR | 文件名包含指定关键字 |
| **文件大小阈值** | 大于1GB | 文件大小≥1,073,741,824字节 |
### 2.2 清理策略
| 阶段 | 操作 | 说明 |
|------|------|------|
| **扫描** | 遍历/proc/[0-9]*/fd/* | 查找所有进程的文件描述符 |
| **匹配** | 关键字+deleted标记 | 精准定位目标文件 |
| **过滤** | 文件大小≥1GB | 避免误杀小文件占用进程 |
| **清理** | kill -9强制终止 | 立即释放文件句柄 |
| **恢复** | 重启容器+启动应用 | 确保服务正常运行 |
### 2.3 关联容器与应用
- **容器名称**:ujava2
- **特定应用路径**:/data/services/api/java-meeting/java-meeting-extapi
- **启动脚本**:/data/services/api/java-meeting/java-meeting-extapi/run.sh
### 2.4 不在本期范围
- 未删除文件的清理
- 小于1GB文件的清理
- 非关键字文件的清理
- 磁盘空间预警功能
---
## 3. 术语说明
- **fd目录**:文件描述符目录,/proc/[pid]/fd/包含进程打开的所有文件
- **deleted标记**:文件已被删除但进程仍占用文件句柄
- **符号链接**:fd目录中的文件是指向实际文件的符号链接
- **工作目录(cwd)**:进程当前工作目录,用于判断进程归属
---
## 4. 功能需求
### 4.1 扫描机制
#### 4.1.1 进程fd扫描
遍历所有进程的文件描述符目录:
```bash
for fd_path in /proc/[0-9]*/fd/*; do
# 检查和处理
done
```
#### 4.1.2 文件匹配规则
同时满足以下条件:
1. 文件为符号链接
2. 符号链接目标包含"(deleted)"标记
3. 文件名包含关键字"ubains-INFO-AND-ERROR"
#### 4.1.3 文件大小检测
使用stat命令获取链接指向的实际文件大小:
```bash
stat -L -c %s "$fd_path"
```
### 4.2 清理策略
#### 4.2.1 触发条件
- 文件大小 ≥ 1GB(1,073,741,824字节)
#### 4.2.2 清理操作
1. 记录文件详细信息(进程名、PID、FD、文件路径、大小)
2. 强制终止进程:`kill -9 $pid`
3. 等待1秒确保进程完全退出
4. 设置容器重启标志
#### 4.2.3 特定应用识别
通过进程工作目录判断是否为特定应用:
```bash
proc_cwd=$(readlink /proc/$pid/cwd)
if [ "$proc_cwd" == "$APP_PATH" ]; then
NEED_APP_START=1
fi
```
### 4.3 恢复机制
#### 4.3.1 容器重启
当检测到大文件进程被清理时:
1. 检查docker命令是否可用
2. 执行容器重启:`docker restart ujava2`
3. 记录重启结果
#### 4.3.2 特定应用启动
当被清理进程属于特定应用时:
1. 容器重启完成后检查启动脚本是否存在
2. 执行启动脚本:`bash $APP_START_SCRIPT`
3. 记录启动结果
### 4.4 日志与审计
#### 4.4.1 日志文件
- **主日志**`/data/logs/auto_clean_deleted_ubains.log`
- **轮转大小**:5MB
- **保留天数**:7天
#### 4.4.2 日志内容格式
每条日志包含:
- 时间戳:`[YYYY-MM-DD HH:MM:SS]`
- 操作类型:扫描发现/处理/跳过
- 文件信息:进程名、PID、FD、文件路径、大小
- 操作结果:成功/失败
**日志示例**
```
[2026-03-30 04:00:00] ===============================================
[2026-03-30 04:00:00] 开始扫描:检测 deleted 大文件并自动清理 (内核直读版)
[2026-03-30 04:00:00] 匹配关键字: ubains-INFO-AND-ERROR
[2026-03-30 04:00:00] 大于尺寸: 1GB
[2026-03-30 04:00:00] ===============================================
[2026-03-30 04:00:05] -----------------------------------------------
[2026-03-30 04:00:05] 发现匹配文件:
[2026-03-30 04:00:05] 进程: java
[2026-03-30 04:00:05] PID: 12345
[2026-03-30 04:00:05] FD: 123
[2026-03-30 04:00:05] 文件: /data/services/api/java-meeting/java-meeting-extapi/logs/ubains-INFO-AND-ERROR.log (deleted)
[2026-03-30 04:00:05] 大小: 2048 MB
[2026-03-30 04:00:05] ⚠ 文件超过1GB,执行自动处理。
[2026-03-30 04:00:05] ➡ 杀死进程 PID: 12345
[2026-03-30 04:00:05] 检测到被杀死的进程属于特定应用:/data/services/api/java-meeting/java-meeting-extapi,将在容器重启后尝试启动。
[2026-03-30 04:00:10] ➡ 检测到大文件进程已处理,统一重启 docker 容器:ujava2
[2026-03-30 04:00:30] ✔ Docker 容器重启完成。
[2026-03-30 04:00:30] ➡ 启动特定应用脚本:/data/services/api/java-meeting/java-meeting-extapi/run.sh
[2026-03-30 04:00:45] ✔ 特定应用启动脚本执行完成。
[2026-03-30 04:00:45] 🎉 执行结束。
[2026-03-30 04:00:45] ===============================================
```
#### 4.4.3 日志自动轮转
为避免日志文件无限增长:
1. **轮转触发条件**
- 当前日志文件大小达到5MB
- 每次脚本执行时自动检查
2. **轮转执行动作**
- 将当前日志重命名为带时间戳的旧日志
- 创建新的空日志文件继续写入
- 记录轮转操作日志
3. **旧日志清理**
- 自动删除超过保留天数的旧日志文件(默认7天)
### 4.5 错误处理
#### 4.5.1 进程终止失败
- 记录错误日志,继续执行
- 不影响其他进程的处理
#### 4.5.2 Docker命令不可用
- 记录错误日志
- 提示手动重启容器
#### 4.5.3 启动脚本不存在
- 记录错误日志
- 说明具体缺失的脚本路径
---
## 5. 配置参数
| 配置项 | 默认值 | 说明 |
|--------|--------|------|
| TARGET_KEY | ubains-INFO-AND-ERROR | 文件名匹配关键字 |
| MIN_SIZE | 1073741824 (1GB) | 文件大小阈值(字节) |
| LOG_FILE | /data/logs/auto_clean_deleted_ubains.log | 日志文件路径 |
| MAX_LOG_SIZE | 5242880 (5MB) | 日志文件大小限制 |
| LOG_RETENTION_DAYS | 7 | 日志保留天数 |
| CONTAINER_NAME | ujava2 | Docker容器名称 |
| APP_PATH | /data/services/api/java-meeting/java-meeting-extapi | 特定应用路径 |
| APP_START_SCRIPT | /data/services/api/java-meeting/java-meeting-extapi/run.sh | 特定应用启动脚本 |
---
## 6. 执行要求
### 6.1 执行时机
- **建议**:每日凌晨4点执行
- **cron配置**`0 4 * * * /data/services/scripts/auto_clean_deleted_ubains_v3.sh`
### 6.2 执行环境
- **操作系统**:Linux(支持/proc文件系统)
- **权限要求**:root权限(读取/proc、终止进程、重启容器)
- **必需工具**:docker、bash
### 6.3 资源要求
- **CPU**:扫描过程轻量级
- **内存**:最小占用
- **磁盘**:日志文件需监控
---
## 7. 验收标准
### 7.1 功能验收
- [x] 能正确扫描所有进程的fd目录
- [x] 能准确识别已删除且匹配关键字的文件
- [x] 能正确获取文件大小
- [x] 能正确终止占用大文件的进程
- [x] 能正确重启Docker容器
- [x] 能正确启动特定应用
- [x] 日志文件达到5MB时自动轮转
- [x] 轮转后的日志保留7天后自动清理
### 7.2 安全性验收
- [x] 只处理已删除文件
- [x] 只处理匹配关键字的文件
- [x] 只处理超过1GB的文件
- [x] 不会误杀正常进程
- [x] 操作日志完整可追溯
### 7.3 稳定性验收
- [x] 脚本异常退出时不会影响系统
- [x] Docker不可用时能正确记录错误
- [x] 启动脚本不存在时能正确记录错误
---
## 8. 故障排查
### 8.1 常见问题
#### 问题1:未发现匹配文件
```
[2026-03-30 04:00:00] 未发现匹配的 deleted 文件。
```
说明:系统中没有符合条件的垃圾文件,属于正常情况。
#### 问题2:Docker命令不存在
```
❌ 错误: 未找到 docker 命令,请手动重启容器。
```
解决:安装docker或手动重启ujava2容器。
#### 问题3:启动脚本不存在
```
❌ 错误: 未找到特定应用启动脚本:/data/services/api/java-meeting/java-meeting-extapi/run.sh
```
解决:检查应用路径是否正确,或手动启动应用。
### 8.2 诊断命令
```bash
# 查看实时日志
tail -f /data/logs/auto_clean_deleted_ubains.log
# 手动扫描已删除文件
for fd_path in /proc/[0-9]*/fd/*; do
if [ -L "$fd_path" ]; then
target_file=$(readlink "$fd_path" 2>/dev/null)
if [[ "$target_file" == *"(deleted)"* ]]; then
pid=$(echo "$fd_path" | cut -d'/' -f3)
size=$(stat -L -c %s "$fd_path" 2>/dev/null || echo 0)
echo "PID: $pid, Size: $((size/1024/1024))MB, File: $target_file"
fi
fi
done
# 检查容器状态
docker ps | grep ujava2
# 检查特定应用进程
docker exec ujava2 ps aux | grep java-meeting-extapi
```
---
## 9. 维护建议
### 9.1 定期检查
- **每周**:查看日志确认脚本正常运行
- **每月**:评估清理效果,调整参数
### 9.2 参数调优
根据实际情况调整:
- **文件大小阈值**:垃圾文件较多时降低阈值
- **日志保留天数**:磁盘空间不足时减少天数
### 9.3 监控建议
- 监控磁盘空间使用率
- 监控已删除文件数量和大小
- 监控容器重启频率
---
## 10. 附录
### 10.1 相关文档
- 代码规范:`_PRD_规范文档_代码规范.md`
- 问题总结:`_PRD_问题总结_记录文档.md`
- 方法总结:`_PRD_方法总结_记录文档.md`
- 文档规范:`_PRD_规范文档_文档规范.md`
- 测试规范:`_PRD_规范文档_测试规范.md`
### 10.2 部署信息
| 项目 | 值 |
|---|---|
| 脚本路径 | /data/services/scripts/auto_clean_deleted_ubains_v3.sh |
| 日志路径 | /data/logs/auto_clean_deleted_ubains.log |
| 容器名称 | ujava2 |
| 特定应用 | java-meeting-extapi |
| 定时任务 | 0 4 * * * /data/services/scripts/auto_clean_deleted_ubains_v3.sh |
---
**文档状态**:已完成
**最后更新**:2026-03-30
# 新统一平台清理垃圾进程释放空间 - 计划执行文档
> 版本:V3.1
> 创建日期:2026-04-10
> 需求文档:`_PRD_新统一平台清理垃圾进程释放空间_需求文档.md`
> 代码路径:`自动化部署脚本/x86架构/新统一平台/定时脚本/auto_clean_deleted_ubains_v3.sh`
---
## 1. 执行概述
### 1.1 项目背景
新统一平台运行过程中,Java应用产生的日志文件被删除后,进程仍占用文件句柄导致磁盘空间无法释放。需要建立自动化清理机制,定期扫描并清理这些垃圾文件。
### 1.2 执行目标
实现已删除大文件的自动清理与恢复机制,具备:
- 自动扫描进程fd目录中的已删除文件
- 按关键字和文件大小智能匹配
- 强制终止占用进程释放磁盘空间
- 自动重启关联容器恢复服务
- 特定应用自动启动支持
- 日志自动轮转功能(5MB/7天)
### 1.3 执行范围
- [x] 脚本基础框架搭建
- [x] 配置参数定义
- [x] 进程fd扫描功能
- [x] 文件匹配与过滤逻辑
- [x] 进程终止清理功能
- [x] 容器重启恢复功能
- [x] 特定应用启动功能
- [x] 日志自动轮转功能
- [x] 操作审计记录
---
## 2. 任务分解与实施计划
### 2.1 任务分解
| 任务编号 | 任务名称 | 优先级 | 预计工作量 | 状态 |
| --- | --- | --- | --- | --- |
| T1 | 脚本基础框架搭建 | 高 | 20分钟 | [x] |
| T2 | 配置参数定义 | 高 | 15分钟 | [x] |
| T3 | 日志系统实现 | 中 | 20分钟 | [x] |
| T4 | 进程fd扫描功能实现 | 高 | 30分钟 | [x] |
| T5 | 文件匹配与过滤实现 | 高 | 35分钟 | [x] |
| T6 | 进程终止清理功能实现 | 高 | 25分钟 | [x] |
| T7 | 容器重启恢复功能实现 | 高 | 20分钟 | [x] |
| T8 | 特定应用启动功能实现 | 中 | 25分钟 | [x] |
| T9 | 日志自动轮转功能实现 | 中 | 15分钟 | [x] |
| T10 | 需求文档生成 | 高 | 40分钟 | [x] |
### 2.2 实施计划
#### T1: 脚本基础框架搭建
**文件位置**`自动化部署脚本/x86架构/新统一平台/定时脚本/auto_clean_deleted_ubains_v3.sh`
**实现内容**
```bash
#!/bin/bash
#===============================================================================
# 脚本名称:auto_clean_deleted_ubains_v3.sh
# 功能描述:已删除大文件自动清理与容器重启脚本
# 版本:V3.1
# 创建日期:2026-01-27
# 更新日期:2026-03-30
#===============================================================================
```
#### T2: 配置参数定义
**实现内容**
```bash
# ================= 配置区域 =================
TARGET_KEY="ubains-INFO-AND-ERROR"
MIN_SIZE=$((1024*1024*1024)) # 1GB (单位:字节)
LOG_FILE="/data/logs/auto_clean_deleted_ubains.log"
MAX_LOG_SIZE=$((5*1024*1024)) # 5MB 日志大小限制
LOG_RETENTION_DAYS=7 # 日志保留天数
CONTAINER_NAME="ujava2" # 容器名称
APP_PATH="/data/services/api/java-meeting/java-meeting-extapi"
APP_START_SCRIPT="${APP_PATH}/run.sh"
# ===========================================
```
#### T3: 日志系统实现
**函数名**`log`
**实现要点**
```bash
# 日志函数
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"
}
```
#### T4: 进程fd扫描功能实现
**实现要点**
- 遍历/proc/[0-9]*/fd/*目录
- 检查文件是否为符号链接
- 读取符号链接目标文件
**核心代码**
```bash
for fd_path in /proc/[0-9]*/fd/*; do
# 检查是否为符号链接
if [ -L "$fd_path" ]; then
target_file=$(readlink "$fd_path" 2>/dev/null)
# 后续处理
fi
done
```
#### T5: 文件匹配与过滤实现
**实现要点**
- 检查目标文件是否包含"(deleted)"标记
- 检查目标文件是否包含关键字
- 获取文件大小并与阈值比较
**核心代码**
```bash
if [[ "$target_file" == *"(deleted)"* ]] && [[ "$target_file" == *"$TARGET_KEY"* ]]; then
# 提取 PID 和 FD
pid=$(echo "$fd_path" | cut -d'/' -f3)
fd=$(echo "$fd_path" | cut -d'/' -f5)
# 获取进程名
proc_name=$(cat "/proc/$pid/comm" 2>/dev/null)
# 获取文件大小
size_bytes=$(stat -L -c %s "$fd_path" 2>/dev/null || echo 0)
size_mb=$((size_bytes / 1024 / 1024))
# 判断是否超过阈值
if [ "$size_bytes" -ge "$MIN_SIZE" ]; then
# 执行清理
fi
fi
```
#### T6: 进程终止清理功能实现
**实现要点**
- 记录文件详细信息
- 强制终止进程
- 设置重启标志
**核心代码**
```bash
if [ "$size_bytes" -ge "$MIN_SIZE" ]; then
log "⚠ 文件超过1GB,执行自动处理。"
log "➡ 杀死进程 PID: $pid"
kill -9 "$pid" 2>/dev/null
sleep 1
NEED_RESTART=1
# 检查是否为特定应用
proc_cwd=$(readlink /proc/$pid/cwd 2>/dev/null)
if [ "$proc_cwd" == "$APP_PATH" ]; then
log "检测到被杀死的进程属于特定应用,将在容器重启后尝试启动。"
NEED_APP_START=1
fi
fi
```
#### T7: 容器重启恢复功能实现
**实现要点**
- 检查docker命令是否可用
- 执行容器重启
- 记录重启结果
**核心代码**
```bash
if [ "$NEED_RESTART" -eq 1 ]; then
log "➡ 检测到大文件进程已处理,统一重启 docker 容器:$CONTAINER_NAME"
if command -v docker >/dev/null 2>&1; then
docker restart "$CONTAINER_NAME" >> "$LOG_FILE" 2>&1
log "✔ Docker 容器重启完成。"
else
log "❌ 错误: 未找到 docker 命令,请手动重启容器。"
fi
fi
```
#### T8: 特定应用启动功能实现
**实现要点**
- 检查启动脚本是否存在
- 执行启动脚本
- 记录启动结果
**核心代码**
```bash
if [ "$NEED_APP_START" -eq 1 ]; then
log "➡ 启动特定应用脚本:$APP_START_SCRIPT"
if [ -f "$APP_START_SCRIPT" ]; then
bash "$APP_START_SCRIPT" >> "$LOG_FILE" 2>&1
log "✔ 特定应用启动脚本执行完成。"
else
log "❌ 错误: 未找到特定应用启动脚本:$APP_START_SCRIPT。"
fi
fi
```
#### T9: 日志自动轮转功能实现
**函数名**`rotate_logs`
**实现要点**
- 检查日志文件大小
- 超过5MB时自动轮转
- 清理7天前的旧日志
**核心代码**
```bash
rotate_logs() {
if [ -f "$LOG_FILE" ]; then
FILE_SIZE=$(stat -c%s "$LOG_FILE")
if [ "$FILE_SIZE" -ge "$MAX_LOG_SIZE" ]; then
mv "$LOG_FILE" "$LOG_FILE.$(date '+%Y%m%d%H%M%S')"
touch "$LOG_FILE"
log "日志文件超过 5MB,已自动轮转。"
fi
fi
find "$(dirname "$0")" -name "auto_clean_deleted_ubains.log.*" -mtime +$LOG_RETENTION_DAYS -exec rm -f {} \;
}
```
#### T10: 需求文档生成
**实现内容**
- 创建需求文档
- 文档路径:`Docs/PRD/自动化部署脚本/通用脚本/需求文档/_PRD_新统一平台清理垃圾进程释放空间_需求文档.md`
- 包含完整的功能描述、配置参数、验收标准等
---
## 3. 关键函数设计
### 3.1 log 函数
**功能**:记录日志并输出到控制台和日志文件
**参数**:$*(所有参数作为日志内容)
**实现**
```bash
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"
}
```
### 3.2 rotate_logs 函数
**功能**:日志文件自动轮转
**实现流程**
```
检查日志文件是否存在
↓ 不存在
退出
↓ 存在
获取文件大小
↓ 小于5MB
退出
↓ 大于等于5MB
重命名日志文件(添加时间戳)
创建新的空日志文件
记录轮转日志
清理超过保留天数的旧日志
```
---
## 4. 配置参数说明
| 参数名 | 默认值 | 说明 | 调整建议 |
| --- | --- | --- | --- |
| TARGET_KEY | ubains-INFO-AND-ERROR | 文件名匹配关键字 | 根据实际日志文件名调整 |
| MIN_SIZE | 1073741824 (1GB) | 文件大小阈值(字节) | 磁盘空间紧张时可降低 |
| LOG_FILE | /data/logs/auto_clean_deleted_ubains.log | 日志文件路径 | 根据实际环境调整 |
| MAX_LOG_SIZE | 5242880 (5MB) | 日志文件大小限制 | 可根据需要调整 |
| LOG_RETENTION_DAYS | 7 | 日志保留天数 | 磁盘空间不足时减少 |
| CONTAINER_NAME | ujava2 | Docker容器名称 | 根据实际部署调整 |
| APP_PATH | /data/services/api/java-meeting/java-meeting-extapi | 特定应用路径 | 根据实际部署调整 |
| APP_START_SCRIPT | /data/services/api/java-meeting/java-meeting-extapi/run.sh | 特定应用启动脚本 | 根据实际部署调整 |
---
## 5. 主流程设计
```
脚本开始
执行日志轮转
输出扫描开始信息
初始化标志(FOUND=0, NEED_RESTART=0, NEED_APP_START=0)
遍历所有进程的fd目录
检查是否为符号链接
↓ 否
继续下一个
↓ 是
读取符号链接目标
检查是否包含deleted标记和关键字
↓ 不匹配
继续下一个
↓ 匹配
提取PID、FD、进程名
获取文件大小
输出文件信息
检查文件大小是否≥1GB
↓ 小于1GB
记录跳过,继续下一个
↓ 大于等于1GB
强制终止进程(kill -9)
设置NEED_RESTART=1
检查进程工作目录
↓ 匹配特定应用路径
设置NEED_APP_START=1
继续遍历
遍历完成
判断FOUND标志
↓ 为0(未发现)
输出未发现信息
↓ 为1(发现)
判断NEED_RESTART标志
↓ 为1
检查docker命令
↓ 存在
重启容器
输出完成信息
↓ 不存在
输出错误信息
判断NEED_APP_START标志
↓ 为1
检查启动脚本
↓ 存在
执行启动脚本
输出完成信息
↓ 不存在
输出错误信息
输出执行结束信息
脚本结束
```
---
## 6. 文件匹配规则
### 6.1 匹配条件
文件必须同时满足以下条件才会被处理:
| 条件 | 说明 | 检测方法 |
| --- | --- | --- |
| 符号链接 | fd目录中的文件必须为符号链接 | `[ -L "$fd_path" ]` |
| 已删除标记 | 目标文件包含"(deleted)" | `[[ "$target_file" == *"(deleted)"* ]]` |
| 关键字匹配 | 文件名包含指定关键字 | `[[ "$target_file" == *"$TARGET_KEY"* ]]` |
| 大小阈值 | 文件大小≥1GB | `[ "$size_bytes" -ge "$MIN_SIZE" ]` |
### 6.2 处理流程
```
发现匹配文件
输出文件详细信息
文件大小≥1GB?
↓ 否
跳过处理
↓ 是
强制终止进程
检查是否为特定应用
重启容器
启动特定应用(如需要)
```
---
## 7. 验收标准
### 7.1 功能验收
- [x] 能正确扫描所有进程的fd目录
- [x] 能准确识别符号链接
- [x] 能正确检测deleted标记
- [x] 能正确匹配文件关键字
- [x] 能正确获取文件大小
- [x] 能正确过滤小于1GB的文件
- [x] 能正确终止占用进程
- [x] 能正确识别特定应用
- [x] 能正确重启容器
- [x] 能正确启动特定应用
- [x] 日志文件达到5MB时自动轮转
- [x] 旧日志保留7天后自动清理
### 7.2 安全性验收
- [x] 只处理已删除文件
- [x] 只处理匹配关键字的文件
- [x] 只处理超过1GB的文件
- [x] 不会误杀正常进程
- [x] 操作日志完整可追溯
- [x] 异常情况下有错误提示
### 7.3 稳定性验收
- [x] 脚本异常退出时不会影响系统
- [x] Docker不可用时能正确记录错误
- [x] 启动脚本不存在时能正确记录错误
- [x] fd目录不存在时能正确处理
---
## 8. 测试计划
### 8.1 单元测试场景
| 测试场景 | 预期结果 |
| --- | --- |
| 系统无已删除文件 | 输出"未发现匹配的 deleted 文件" |
| 有已删除文件但小于1GB | 输出文件信息,跳过处理 |
| 有已删除文件大于1GB | 终止进程,重启容器 |
| 进程属于特定应用 | 终止进程,重启容器,启动应用 |
| Docker命令不存在 | 记录错误,提示手动重启 |
| 启动脚本不存在 | 记录错误,说明脚本路径 |
| 日志文件超过5MB | 自动轮转日志文件 |
### 8.2 集成测试场景
| 测试场景 | 预期结果 |
| --- | --- |
| 多个大文件进程同时存在 | 依次处理,统一重启容器 |
| 容器重启后特定应用自动启动 | 应用正常运行 |
| 日志轮转后继续记录 | 新日志正常写入 |
---
## 9. 实施记录
### 9.1 实施进度
| 日期 | 完成任务 | 负责人 | 备注 |
| --- | --- | --- | --- |
| 2026-01-27 | 初始版本开发(V3.0) | - | 基础清理功能 |
| 2026-03-30 | 功能完善(V3.1) | Claude | 新增日志轮转和特定应用启动 |
| 2026-04-10 | 文档生成 | Claude | 生成需求文档和计划执行文档 |
### 9.2 变更记录
| 日期 | 变更内容 | 变更原因 |
| --- | --- | --- |
| 2026-03-30 | 新增日志自动轮转机制(5MB/7天) | 防止日志文件过大 |
| 2026-03-30 | 新增特定应用启动支持 | 自动恢复extapi服务 |
| 2026-03-30 | 优化日志输出格式 | 提高可读性 |
---
## 10. 风险评估
| 风险项 | 风险等级 | 应对措施 |
| --- | --- | --- |
| 误杀正常进程 | 低 | 多重过滤条件(deleted+关键字+大小) |
| 容器重启影响业务 | 中 | 定时在凌晨4点执行,影响最小 |
| 特定应用启动失败 | 低 | 记录错误,可手动启动 |
| 日志文件过大 | 低 | 5MB自动轮转,7天自动清理 |
| Docker命令不可用 | 中 | 记录错误,提示手动重启 |
---
## 11. 后续工作
- [ ] 监控脚本运行情况
- [ ] 根据实际运行情况调整文件大小阈值
- [ ] 考虑增加磁盘空间预警功能
- [ ] 考虑增加邮件告警通知
- [ ] 考虑支持多个关键字匹配
---
## 12. 附录
### 12.1 相关文档
- 需求文档:`_PRD_新统一平台清理垃圾进程释放空间_需求文档.md`
- 代码规范:`_PRD_规范文档_代码规范.md`
- 测试规范:`_PRD_规范文档_测试规范.md`
### 12.2 部署信息
| 项目 | 值 |
| --- | --- |
| 脚本路径 | /data/services/scripts/auto_clean_deleted_ubains_v3.sh |
| 日志路径 | /data/logs/auto_clean_deleted_ubains.log |
| 容器名称 | ujava2 |
| 特定应用 | java-meeting-extapi |
| 定时任务 | 0 4 * * * /data/services/scripts/auto_clean_deleted_ubains_v3.sh |
### 12.3 快速诊断命令
```bash
# 查看实时日志
tail -f /data/logs/auto_clean_deleted_ubains.log
# 手动扫描已删除文件
for fd_path in /proc/[0-9]*/fd/*; do
if [ -L "$fd_path" ]; then
target_file=$(readlink "$fd_path" 2>/dev/null)
if [[ "$target_file" == *"(deleted)"* ]]; then
pid=$(echo "$fd_path" | cut -d'/' -f3)
size=$(stat -L -c %s "$fd_path" 2>/dev/null || echo 0)
echo "PID: $pid, Size: $((size/1024/1024))MB, File: $target_file"
fi
fi
done
# 检查容器状态
docker ps | grep ujava2
# 检查特定应用进程
docker exec ujava2 ps aux | grep java-meeting-extapi
# 手动执行脚本(测试)
/data/services/scripts/auto_clean_deleted_ubains_v3.sh
```
### 12.4 配置文件示例
```bash
# 如果需要调整文件大小阈值(例如改为500MB)
# 修改脚本中的 MIN_SIZE 参数:
MIN_SIZE=$((512*1024*1024)) # 500MB
# 如果需要调整日志保留天数(例如改为30天)
# 修改脚本中的 LOG_RETENTION_DAYS 参数:
LOG_RETENTION_DAYS=30 # 日志保留天数
# 如果需要调整执行时间(例如改为凌晨2点)
# 修改crontab:
0 2 * * * /data/services/scripts/auto_clean_deleted_ubains_v3.sh
```
---
**文档状态**:已完成
**最后更新**:2026-04-10
......@@ -32,6 +32,36 @@ TASK_SCRIPT[mysql_backup]="/data/services/scripts/UbainsmysqlBakUp.sh"
TASK_LOG[mysql_backup]=""
TASK_STATUS[mysql_backup]=true # 默认启用
# monitor_redis 任务定义
TASK_CRON[monitor_redis]="*/5 * * * *"
TASK_SCRIPT[monitor_redis]="/data/services/scripts/monitor_redis_service.sh"
TASK_LOG[monitor_redis]=""
TASK_STATUS[monitor_redis]=true # 默认启用
# monitor_emqx 任务定义
TASK_CRON[monitor_emqx]="*/5 * * * *"
TASK_SCRIPT[monitor_emqx]="/data/services/scripts/monitor_emqx_service.sh"
TASK_LOG[monitor_emqx]=""
TASK_STATUS[monitor_emqx]=true # 默认启用
# nginx_backup 任务定义
TASK_CRON[nginx_backup]="0 3 * * *"
TASK_SCRIPT[nginx_backup]="/data/services/scripts/backup_nginx_logs.sh"
TASK_LOG[nginx_backup]=""
TASK_STATUS[nginx_backup]=true # 默认启用
# mysql_logs_backup 任务定义
TASK_CRON[mysql_logs_backup]="0 2 * * *"
TASK_SCRIPT[mysql_logs_backup]="/data/services/scripts/backup_mysql_logs.sh"
TASK_LOG[mysql_logs_backup]=""
TASK_STATUS[mysql_logs_backup]=true # 默认启用
# auto_clean 任务定义
TASK_CRON[auto_clean]="0 4 * * *"
TASK_SCRIPT[auto_clean]="/data/services/scripts/auto_clean_deleted_ubains_v3.sh"
TASK_LOG[auto_clean]=""
TASK_STATUS[auto_clean]=true # 默认启用
# 初始化 sudo 相关变量
if [[ $(id -u) -ne 0 ]]; then
SUDO="sudo"
......@@ -58,7 +88,12 @@ function show_help() {
cat << EOF
定时任务配置脚本
用法: $0 [选项]
用法(直接调用脚本):
$0 [选项]
用法(主脚本中调用函数):
source $0
add_crontab_job [选项]
选项:
--enable-ujava2 启用 ujava2-startup 定时任务
......@@ -69,17 +104,35 @@ function show_help() {
--disable-mysql-backup 禁用 mysql_backup 定时任务
--enable-mysql 启用 mysql_backup 定时任务(别名)
--disable-mysql 禁用 mysql_backup 定时任务(别名)
--enable-monitor-redis 启用 monitor_redis 定时任务
--disable-monitor-redis 禁用 monitor_redis 定时任务
--enable-monitor-emqx 启用 monitor_emqx 定时任务
--disable-monitor-emqx 禁用 monitor_emqx 定时任务
--enable-nginx-backup 启用 nginx_backup 定时任务
--disable-nginx-backup 禁用 nginx_backup 定时任务
--enable-mysql-logs-backup 启用 mysql_logs_backup 定时任务
--disable-mysql-logs-backup 禁用 mysql_logs_backup 定时任务
--enable-auto-clean 启用 auto_clean 定时任务
--disable-auto-clean 禁用 auto_clean 定时任务
-h, --help 显示此帮助信息
默认行为:
不传参数时,所有定时任务默认启用
示例:
示例(脚本方式):
$0 # 默认启用所有任务
$0 --disable-ujava2 # 禁用 ujava2-startup
$0 --enable-mysql-backup # 启用数据库备份
$0 --disable-monitor-redis --disable-monitor-emqx # 禁用监控任务
$0 --enable-ujava2 --enable-check-health --enable-mysql-backup # 显式启用所有任务
示例(函数方式):
source $0 # 导入脚本
add_crontab_job # 默认启用所有任务
add_crontab_job --disable-ujava2 # 禁用 ujava2-startup
add_crontab_job --enable-mysql-backup # 启用数据库备份
add_crontab_job --disable-monitor-redis --disable-monitor-emqx # 禁用监控任务
EOF
}
......@@ -123,6 +176,61 @@ function parse_arguments() {
TASK_STATUS[mysql_backup]=false
fi
;;
--enable-monitor-redis)
TASK_STATUS[monitor_redis]=true
;;
--disable-monitor-redis)
if [[ "${TASK_STATUS[monitor_redis]}" == "false" ]]; then
log "WARN" "⚠️ 参数冲突:monitor_redis 同时被启用和禁用,跳过该任务"
TASK_STATUS[monitor_redis]=conflict
else
TASK_STATUS[monitor_redis]=false
fi
;;
--enable-monitor-emqx)
TASK_STATUS[monitor_emqx]=true
;;
--disable-monitor-emqx)
if [[ "${TASK_STATUS[monitor_emqx]}" == "false" ]]; then
log "WARN" "⚠️ 参数冲突:monitor_emqx 同时被启用和禁用,跳过该任务"
TASK_STATUS[monitor_emqx]=conflict
else
TASK_STATUS[monitor_emqx]=false
fi
;;
--enable-nginx-backup)
TASK_STATUS[nginx_backup]=true
;;
--disable-nginx-backup)
if [[ "${TASK_STATUS[nginx_backup]}" == "false" ]]; then
log "WARN" "⚠️ 参数冲突:nginx_backup 同时被启用和禁用,跳过该任务"
TASK_STATUS[nginx_backup]=conflict
else
TASK_STATUS[nginx_backup]=false
fi
;;
--enable-mysql-logs-backup)
TASK_STATUS[mysql_logs_backup]=true
;;
--disable-mysql-logs-backup)
if [[ "${TASK_STATUS[mysql_logs_backup]}" == "false" ]]; then
log "WARN" "⚠️ 参数冲突:mysql_logs_backup 同时被启用和禁用,跳过该任务"
TASK_STATUS[mysql_logs_backup]=conflict
else
TASK_STATUS[mysql_logs_backup]=false
fi
;;
--enable-auto-clean)
TASK_STATUS[auto_clean]=true
;;
--disable-auto-clean)
if [[ "${TASK_STATUS[auto_clean]}" == "false" ]]; then
log "WARN" "⚠️ 参数冲突:auto_clean 同时被启用和禁用,跳过该任务"
TASK_STATUS[auto_clean]=conflict
else
TASK_STATUS[auto_clean]=false
fi
;;
-h|--help)
show_help
exit 0
......@@ -180,10 +288,58 @@ function cleanup_crontab_jobs() {
}
# ========================================
# ✅ 函数:安全添加定时任务(通用版本)
# ✅ 函数:配置定时任务(支持参数控制)
# 用途:作为主入口函数,支持 --enable-xxx/--disable-xxx 参数
# ========================================
function add_crontab_job() {
# 1. 解析传入的参数
parse_arguments "$@"
log "INFO" "=========================================="
log "INFO" "🚀 开始配置定时任务"
log "INFO" "=========================================="
# 2. 执行清理
cleanup_crontab_jobs
# 3. 根据任务状态配置任务
for task_name in "${!TASK_STATUS[@]}"; do
local status="${TASK_STATUS[$task_name]}"
# 跳过冲突任务
if [[ "$status" == "conflict" ]]; then
log "WARN" "⏭️ 任务 $task_name 参数冲突,已跳过"
continue
fi
# 跳过禁用任务
if [[ "$status" == "false" ]]; then
log "INFO" "⏭️ 任务 $task_name 已被禁用,跳过配置"
continue
fi
# 配置启用的任务(调用内部函数)
_add_single_cron_job \
"${TASK_CRON[$task_name]}" \
"${TASK_SCRIPT[$task_name]}" \
"${TASK_LOG[$task_name]}" \
"$task_name"
done
# 4. 显示当前任务列表
show_current_tasks
log "INFO" "=========================================="
log "INFO" "✅ 定时任务配置完成"
log "INFO" "=========================================="
}
# ========================================
# ✅ 函数:添加单个定时任务(内部函数)
# ========================================
function _add_single_cron_job() {
local cron_expression="$1"
local script_path="$2"
local log_path="$3"
......@@ -297,53 +453,19 @@ function show_current_tasks() {
# ========================================
# ✅ 函数:主函数
# 用途:供直接调用脚本时使用,内部调用 add_crontab_job 入口函数
# ========================================
function main() {
# 1. 解析命令行参数
parse_arguments "$@"
log "INFO" "=========================================="
log "INFO" "🚀 开始配置定时任务"
log "INFO" "=========================================="
# 2. 执行清理
cleanup_crontab_jobs
# 3. 根据任务状态配置任务
for task_name in "${!TASK_STATUS[@]}"; do
local status="${TASK_STATUS[$task_name]}"
# 跳过冲突任务
if [[ "$status" == "conflict" ]]; then
log "WARN" "⏭️ 任务 $task_name 参数冲突,已跳过"
continue
fi
# 跳过禁用任务
if [[ "$status" == "false" ]]; then
log "INFO" "⏭️ 任务 $task_name 已被禁用,跳过配置"
continue
fi
# 配置启用的任务
add_crontab_job \
"${TASK_CRON[$task_name]}" \
"${TASK_SCRIPT[$task_name]}" \
"${TASK_LOG[$task_name]}" \
"$task_name"
done
# 4. 显示当前任务列表
show_current_tasks
log "INFO" "=========================================="
log "INFO" "✅ 定时任务配置完成"
log "INFO" "=========================================="
# 调用入口函数,传递所有参数
add_crontab_job "$@"
}
# ========================================
# 主函数入口
# 说明:只在直接执行脚本时运行 main,被 source 时不自动运行
# ========================================
main "$@"
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi
#!/bin/bash
#------------------------------工具类模块---------------------------------------------------
# 日志打印函数
LOG_FILE="/data/log/new_auto_script.log"
mkdir -p "$(dirname "$LOG_FILE")" 2>/dev/null
# 用户输入的东西也同步记录到日志信息里面
function log() {
# 获取当前时间
local timestamp=$(date "+%Y-%m-%d %H:%M:%S")
# 日志级别对应的颜色代码
local INFO_COLOR="\033[32m" # 绿色
local WARN_COLOR="\033[33m" # 黄色
local ERROR_COLOR="\033[31m" # 红色
local DEBUG_COLOR="\033[36m" # 青色
local RESET_COLOR="\033[0m" # 重置颜色
# 参数判断
if [ $# -lt 2 ]; then
echo "Usage: log <level> <message>"
return 1
fi
local level=$1
local message=$2
# 根据日志级别选择颜色
case $level in
"INFO")
color=$INFO_COLOR
;;
"WARN")
color=$WARN_COLOR
;;
"ERROR")
color=$ERROR_COLOR
;;
"DEBUG")
color=$DEBUG_COLOR
;;
*)
echo "Invalid log level: $level"
return 1
;;
esac
# 输出带颜色的日志到屏幕
echo -e "${color}[$timestamp] [$level] $message${RESET_COLOR}"
# 追加纯文本日志到文件
echo "[$timestamp] [$level] $message" >> "$LOG_FILE"
}
#------------------------------导入中间件部署模块---------------------------------------------------
# 获取脚本所在目录
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# 导入中间件部署脚本
if [[ -f "$SCRIPT_DIR/auto_middleware_install.sh" ]]; then
source "$SCRIPT_DIR/auto_middleware_install.sh"
else
log "ERROR" "中间件部署脚本不存在: $SCRIPT_DIR/auto_middleware_install.sh"
exit 1
fi
# 导入文件检查脚本
if [[ -f "$SCRIPT_DIR/auto_file_upload_check.sh" ]]; then
source "$SCRIPT_DIR/auto_file_upload_check.sh"
else
log "ERROR" "文件检查脚本不存在: $SCRIPT_DIR/auto_file_upload_check.sh"
exit 1
fi
# 导入定时任务设置脚本
if [[ -f "$SCRIPT_DIR/auto_crontab_settings.sh" ]]; then
source "$SCRIPT_DIR/auto_crontab_settings.sh"
else
log "ERROR" "定时任务设置脚本不存在: $SCRIPT_DIR/auto_crontab_settings.sh"
exit 1
fi
# 导入主服务部署脚本
if [[ -f "$SCRIPT_DIR/auto_deploy_services.sh" ]]; then
source "$SCRIPT_DIR/auto_deploy_services.sh"
else
log "ERROR" "主服务部署脚本不存在: $SCRIPT_DIR/auto_deploy_services.sh"
exit 1
fi
# 导入防火墙设置脚本
if [[ -f "$SCRIPT_DIR/auto_firewall_settings.sh" ]]; then
source "$SCRIPT_DIR/auto_firewall_settings.sh"
else
log "ERROR" "防火墙设置脚本不存在: $SCRIPT_DIR/auto_firewall_settings.sh"
exit 1
fi
#------------------------------检测模块-----------------------------------------------------
#------------------------------服务器检测-start---------------------------------------------
function detectNetType() {
log "INFO" "准备获取所有网口信息,排除 loopback 和虚拟接口"
INTERFACES=$(ls /sys/class/net | grep -vE 'lo|docker|br-|veth|virbr|vmnet')
# 遍历每个网口,检查是否配置了 IP 地址,并输出网口信息
log "INFO" "检测到的网口信息如下:"
for iface in $INTERFACES; do
log "INFO" "-----------------------------"
log "INFO" "接口: $iface"
log "INFO" " 检查接口 $iface 是否配置了 IP 地址"
IP_ADDR=$(ip addr show "$iface" | grep "inet " | awk '{print $2}')
if [[ -n "$IP_ADDR" ]]; then
log "INFO" " 配置的 IP 地址: $IP_ADDR"
else
log "INFO" " 没有配置 IP 地址"
fi
# 获取接口的连接状态(carrier)
log "INFO" " 检查接口 $iface 的连接状态"
if [[ -f /sys/class/net/$iface/carrier ]]; then
CARRIER=$(cat /sys/class/net/$iface/carrier)
[[ "$CARRIER" == "1" ]] && LINK="connected" || LINK="disconnected"
else
LINK="unknown"
fi
log "INFO" " 状态: $LINK"
# 使用 ethtool 获取接口的类型(光口、电口等)
log "INFO" " 获取接口 $iface 的类型"
ETHTOOL_OUTPUT=$(ethtool "$iface" 2>/dev/null)
TYPE=$(echo "$ETHTOOL_OUTPUT" | grep -i "Port:" | awk '{print tolower($2)}')
[[ -z "$TYPE" ]] && TYPE="unknown" && TYPE="other"
# 获取速率和驱动信息
log "INFO" " 获取接口 $iface 的速率和驱动信息"
SPEED=$(echo "$ETHTOOL_OUTPUT" | grep -i "Speed:" | awk '{print $2}')
[[ -z "$SPEED" ]] && SPEED="unknown"
DRIVER=$(ethtool -i "$iface" 2>/dev/null | grep "driver:" | awk '{print $2}')
[[ -z "$DRIVER" ]] && DRIVER="unknown"
# 获取 MAC 地址
log "INFO" " 获取接口 $iface 的 MAC 地址"
MAC=$(cat /sys/class/net/$iface/address)
# 转换 TYPE 为中文描述
case "$TYPE" in
"copper")
TYPE="电口"
;;
"fiber")
TYPE="光口"
;;
"other")
TYPE="其他类型"
;;
*)
TYPE="未知类型"
;;
esac
# 输出接口信息
log "INFO" " 光电口类型: ${TYPE:-unknown}"
log "INFO" " 速率: $SPEED"
log "INFO" " MAC 地址: $MAC"
log "INFO" " 驱动: $DRIVER"
done
}
#------------------------------服务器空间检测-start---------------------------------------------
# 功能说明:检查服务器空间是否满足部署要求
# 调用方式:checkServerSpace
# 返回值:无,直接exit或继续执行
function checkServerSpace() {
log "INFO" "开始检查服务器空间分配"
# 判断当前服务器类型
if [ -d "/data" ]; then
system_type="new_type"
log "INFO" "检测到/data目录,系统类型为:新统一平台"
else
system_type="old_type"
log "INFO" "未检测到/data目录,系统类型为:传统平台"
fi
# 获取脚本所在目录
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# 调用空间检查脚本
log "INFO" "执行空间检查脚本..."
system_type=$system_type "$SCRIPT_DIR/auto_check_space.sh"
return_code=$?
# 根据返回值处理
case $return_code in
0)
log "INFO" "服务器空间检查通过,继续执行部署"
;;
1)
log "ERROR" "服务器空间不足,停止部署"
exit 1
;;
2)
log "ERROR" "服务器未正确分区,停止部署"
exit 1
;;
*)
log "ERROR" "空间检查返回异常值: $return_code,停止部署"
exit 1
;;
esac
log "INFO" "服务器空间检查完成"
}
#------------------------------服务器空间检测-end-----------------------------------------------
function checkOS() {
log "INFO" "开始检测服务器环境"
log "INFO" "**************《请检查当前系统是否适配》**************************************************"
# 检测当前的操作系统是否为x86架构
log "INFO" "开始检查当前操作系统架构"
if [ "$(uname -m)" != "x86_64" ]; then
log "ERROR" "当前操作系统不是x86架构,请使用x86架构的操作系统"
exit 1
else
log "INFO" "当前操作系统为x86架构,继续执行脚本"
fi
server_available_mem=`free -g |awk 'NR==2' | awk '{print$7}'`
server_max_mem=`free -g|awk 'NR==2' |awk '{print$2}'`
log "INFO" "当前系统最大内存为:${server_max_mem}GB 可用内存为:${server_available_mem}GB"
#判断服务器的最大内存值小于约定的内存大小min_memory_value=15,就返回。否则成功。
log "INFO" "开始检查服务器最大内存"
if [ $server_max_mem -lt $min_memory_value ];then
log "WARN" "当前系统最大内存为:${server_max_mem}G,建议将内存至少添加为: ${min_memory_value}G"
# 需要用户确认信息,支持默认空格和y或Y,并日志记录用户输入情况
read -p "是否继续执行脚本?(y/n): " yn
if [[ -z "$yn" || "$yn" =~ ^[yY]$ ]]; then
# 记录用户如情况
log "INFO" "用户选择继续执行脚本,输入为${yn} "
else
log "ERROR" "脚本执行终止,请调整服务器内存后重新运行脚本"
exit 1
fi
else
log "INFO" "检查当前系统最大内存为:${server_max_mem}G 满足系统运行!"
fi
#todo:考虑把交换关闭掉
#判断服务器的内存值小于约定的内存大小min_memory_available=10,就返回。否则成功。
log "INFO" "开始检查服务器可用内存"
if [ $server_available_mem -lt $min_memory_available ];then
log "WARN" "当前系统docker可用内存为:${server_available_mem}G,建议将内存至少添加为: ${min_memory_available}G !"
# 需要用户确认信息,支持默认空格和y或Y,并日志记录用户输入情况
read -p "是否继续执行脚本?(y/n): " yn
if [[ -z "$yn" || "$yn" =~ ^[yY]$ ]]; then
# 记录用户如情况
log "INFO" "用户选择继续执行脚本,输入为${yn} "
else
log "ERROR" "脚本执行终止,请调整服务器内存后重新运行脚本"
exit 1
fi
else
log "INFO" "检查当前系统docker可用内存为:${server_available_mem}G 满足系统运行!"
fi
#判断服务的根分区的硬盘大小是否超过预设的硬盘资源。成功继续,否则退出。
log "INFO" "开始检查服务器硬盘空间"
server_disk_available=`df -B G /|awk '/\//{print$4}' | awk '{sub(/.{1}$/,"")}1'`
if [ $server_disk_available -lt $min_disk_available ];then
log "WARN" "当前系统的硬盘空间为:${server_disk_available}G,建议将硬盘空间至少添加为: ${min_disk_available}G!"
# 需要用户确认信息,支持默认空格和y或Y,并日志记录用户输入情况
read -p "是否继续执行脚本?(y/n): " yn
if [[ -z "$yn" || "$yn" =~ ^[yY]$ ]]; then
# 记录用户如情况
log "INFO" "用户选择继续执行脚本,输入为${yn} "
else
log "ERROR" "脚本执行终止,请调整服务器磁盘后重新运行脚本"
exit 1
fi
else
log "INFO" "检查当前系统硬盘空间为:${server_disk_available}G,满足系统运行!"
fi
# 检测当前用户是否有root权限
log "INFO" "开始检查当前用户权限"
if [ "$(id -u)" -ne 0 ]; then
log "ERROR" "当前用户没有root权限,请使用root用户执行脚本,获者使用sudo命令执行脚本"
exit 1
else
log "INFO" "当前用户有root权限,继续执行脚本"
fi
log "INFO" "当前用户权限检查完成"
# 检测当前服务器是否存在多张网卡并且ipv4不唯一的需要列出来
log "INFO" "开始检查服务器网卡信息"
network_interfaces=$(ip -o -4 addr show | awk '{print $2}' | sort -u)
if [ -z "$network_interfaces" ]; then
log "ERROR" "当前服务器没有检测到任何网卡,请检查网络配置"
exit 1
else
log "INFO" "当前服务器检测到的网卡信息如下:$network_interfaces"
# 获取对应的IP信息
for interface in $network_interfaces; do
ip_address=$(ip -o -4 addr show "$interface" | awk '{print $4}' | cut -d/ -f1)
if [ -n "$ip_address" ]; then
log "INFO" "网卡 $interface 的 IP 地址为: $ip_address"
IP=$ip_address
else
log "WARN" "网卡 $interface 没有配置 IPv4 地址"
fi
done
fi
log "INFO" "服务器网卡信息检查完成"
log "INFO" "开始检测服务器网口类型"
detectNetType
log "INFO" "服务器网口类型检测完成"
# 服务器空间检查
checkServerSpace
# 用户确认以后继续往下
read -p "请确认网口信息是否正确,特别注意需要仔细观察你的项目是要光口还是电口 ,输入 y 或 Y 确认,输入 n 或 N 退出脚本: " yn
if [[ -z "$yn" || "$yn" =~ ^[yY]$ ]]; then
log "INFO" "用户确认网口信息正确,继续执行脚本"
else
log "ERROR" "用户确认网口信息不正确,脚本执行终止"
exit 1
fi
echo -e "\033[36m
*******************************************************
* 可适配的操作系统(x86) *
* *
* cento7.5 *
* cento7.6 *
* cento7.9 *
* 统信系统 *
* 鲲鹏系统 *
* Ubuntu *
* redhat7 *
* 银河麒麟V10 *
* openEuler 24.03 *
* *
*******************************************************
\033[0m"
echo -e "\033[36m\n检测到当前操作系统如下,请确认是否适配:\n \033[0m"
# 优先获取PRETTY_NAME,没有则拼接NAME和VERSION
log "INFO" "开始获取当前操作系统版本信息"
if grep -q '^PRETTY_NAME=' /etc/os-release; then
os_version=$(grep '^PRETTY_NAME=' /etc/os-release | cut -d= -f2 | tr -d '"')
else
name=$(grep '^NAME=' /etc/os-release | cut -d= -f2 | tr -d '"')
version=$(grep '^VERSION=' /etc/os-release | cut -d= -f2 | tr -d '"')
os_version="$name $version"
fi
log "INFO" "当前操作系统版本为: $os_version"
case "$os_version" in
*"CentOS Linux 7"*|*"UOS"*|*"Kylin V10"*|*"Ubuntu"*|*"Red Hat Enterprise Linux Server 7"*|*"openEuler 24.03"*)
log "INFO" "当前操作系统版本适配,继续执行脚本" ;;
*)
log "ERROR" "当前操作系统版本不适配,请使用CentOS 7、UOS、麒麟V10、Ubuntu或Red Hat Enterprise Linux Server 7 openEuler 24.03"
# 需要用户确认信息,支持默认空格和y或Y,并日志记录用户输入情况
read -p "是否继续执行脚本?(y/n): " yn
if [[ -z "$yn" || "$yn" =~ ^[yY]$ ]]; then
# 记录用户如情况
log "INFO" "用户选择继续执行脚本,输入为${yn} "
else
log "ERROR" "脚本执行终止,请调整服务器操作系统后重新运行脚本"
exit 1
fi;;
esac
log "INFO" "当前操作系统版本适配,继续执行脚本"
#检查服务器时间和时区
log "INFO" "开始检查服务器时间和时区"
log "INFO" "**************《请检查当前时间,时区是否正确》*****************************************************************"
log "INFO" "当前服务器时区、时间为: $(date)"
read -p "确认无误请按 y 或 Y 或直接回车,不正确请按 n 修改时间: " yn
if [[ -z "$yn" || "$yn" =~ ^[yY]$ ]]; then
log "INFO" "下面进入系统部署。。。"
sleep 3
else
read -p "输入当前日期,格式如2021/02/06——" da
echo "="
date -s $da
read -p "输入当前时间,格式如18:00:00——" sa
echo "="
date -s $sa
timedatectl set-timezone Asia/Shanghai
clock -w
echo "==================================================="
echo "查看修改后的时间"
date
fi
}
log "INFO" "服务器检测完成"
#创建用户(预定安装需要:该用户与系统启动用户有关)
function add_user()
{
log "INFO" "创建用户user"
user=user
group=user
log "INFO" "判断是否存在用户$user"
egrep "^$user" /etc/passwd >& /dev/null
if [ $? -ne 0 ]; then
log "INFO" "用户$user不存在,开始创建用户"
username="user"
password="Ubains@135"
useradd -m "$username"
echo "$username:$password" | chpasswd
log "INFO" "用户$user创建成功"
else
log "INFO" "用户user已创建! "
fi
}
#x86架构ntp安装
function ntp_x86()
{
# 判断是否为centos7 否则跳出函数
if [ ! -f /etc/redhat-release ]; then
log "ERROR" "当前系统不是CentOS 7,无法安装NTP服务"
return 1
fi
log "INFO" "检查ntp服务......"
$sudoset systemctl status ntpd |grep running
#ps -aux | grep ntp
if [ $? -eq 0 ]; then
log "INFO" "检查到ntp已安装!"
else
log "WARN" "检查到ntp未安装,正在安装..."
$sudoset cd $auto_java/server_bag/ntp
$sudoset rpm -Uvh *.rpm --nodeps --force
$sudoset systemctl enable ntpd
$sudoset systemctl enable ntpdate
$sudoset systemctl start ntpd
$sudoset systemctl status ntpd
$sudoset mv /etc/ntp.conf /etc/ntp.confbak
$sudoset cp $auto_java/server_bag/ntp/ntp.conf /etc/
$sudoset systemctl restart ntpd
$sudoset systemctl disable chronyd
log "INFO" "完成ntp服务的安装"
fi
}
#x86架构统信、麒麟系统的ntp安装
function ntp_uos() {
# 判断如果是centos7就退出安装
if [ -f /etc/redhat-release ]; then
log "ERROR" "当前系统是CentOS 7,已经安装NTP服务,无需安装chrony服务"
return 1
fi
# 判断文件是否存在ntp1.aliyun.com地址
if grep -q "ntp1.aliyun.com" /etc/chrony.conf; then
log "INFO" "NTP配置文件中已存在ntp1.aliyun.com地址,无需重复添加"
return 0
fi
local config_dir="/data/temp/ntp"
local backup="/etc/chrony.confbak"
local target="/etc/chrony.conf"
# 切换目录(不需要 sudo)
cd "$config_dir" || { echo "无法进入目录: $config_dir"; return 1; }
# 备份原有配置
if [ -f "$target" ]; then
log "INFO" "正在备份旧配置文件到 $backup"
sudo cp "$target" "$backup"
fi
# 检查是否可以通外网,可以则配置外网的ntp服务器地址,不可以则询问是否有企业ntp服务器地址
if ! ping -c 1 ntp1.aliyun.com &> /dev/null; then
log "ERROR" "无法连接到外网NTP服务器 ntp1.aliyun.com,请检查网络连接"
read -p "是否有企业NTP服务器地址?(y/n): " yn
if [[ -z "$yn" || "$yn" =~ ^[yY]$ ]]; then
read -p "请输入企业NTP服务器地址: " custom_ntp
echo "server $custom_ntp iburst" | sudo tee -a "$target"
echo "allow all" | sudo tee -a "$target"
else
log "ERROR" "没有可用的NTP服务器地址,配置默认地址,请联系管理员"
echo "server ntp1.aliyun.com iburst" | sudo tee -a "$target"
echo "allow all" | sudo tee -a "$target"
fi
else
log "INFO" "可以连接到外网NTP服务器 ntp1.aliyun.com,继续配置"
# 如果可以连接到外网NTP服务器,则继续添加默认的NTP服务器地址
echo "server ntp1.aliyun.com iburst" | sudo tee -a "$target"
echo "server ntp2.aliyun.com iburst" | sudo tee -a "$target"
echo "server ntp3.aliyun.com iburst" | sudo tee -a "$target"
echo "allow all" | sudo tee -a "$target"
fi
# 输出同步地址到日志
log "INFO" "正在写入同步地址到 $target"
# 重启服务
log "INFO" "正在重启 chronyd 服务..."
sudo systemctl daemon-reload
sudo systemctl restart chronyd
sudo systemctl enable chronyd
sudo systemctl status chronyd
log "INFO" "chronyd 服务已重启并设置为开机自启"
}
# ========================================
# 🔧 函数:malan
# 描述:配置 malan 服务自启动(写入 /etc/rc.local)
# 功能:
# - 检查 /etc/rc.local 文件是否存在
# - 检查是否已存在 malan 启动命令(避免重复添加)
# - 将启动命令写入 /etc/rc.local
# - 确保文件有执行权限
# ========================================
function malan() {
local rc_local="/etc/rc.local"
local service_dir="/data/middleware/monitor"
local startup_cmd="cd /data/middleware/monitor/ && nohup ./malan &"
log "INFO" "=================================================="
log "INFO" "🔧 正在配置 malan 服务自启动"
log "INFO" " 服务目录: $service_dir"
log "INFO" "=================================================="
# 1. 检查服务目录是否存在
if [ ! -d "$service_dir" ]; then
log "WARN" "⚠️ 服务目录不存在: $service_dir"
log "WARN" " 将尝试创建目录..."
mkdir -p "$service_dir" 2>/dev/null || {
log "ERROR" "❌ 无法创建目录: $service_dir"
return 1
}
log "INFO" "✅ 目录已创建: $service_dir"
fi
# 2. 检查 malan 可执行文件是否存在
if [ ! -f "$service_dir/malan" ]; then
log "WARN" "⚠️ malan 可执行文件不存在: $service_dir/malan"
log "WARN" " 将继续配置自启动,但请确保文件已部署"
else
# 确保文件有执行权限
if [ ! -x "$service_dir/malan" ]; then
log "INFO" "🔧 正在为 malan 添加执行权限..."
chmod +x "$service_dir/malan" 2>/dev/null || {
log "WARN" "⚠️ 无法添加执行权限,可能需要 root 权限"
}
fi
log "INFO" "✅ malan 文件检查通过"
fi
# 3. 检查 /etc/rc.local 文件是否存在
if [ ! -f "$rc_local" ]; then
log "INFO" "📁 rc.local 文件不存在,正在创建..."
touch "$rc_local" 2>/dev/null || {
log "ERROR" "❌ 无法创建 $rc_local,请检查权限"
return 1
}
# 添加 shebang 行(如果文件为空)
echo "#!/bin/bash" > "$rc_local"
log "INFO" "✅ rc.local 文件已创建"
fi
# 确保 rc.local 有执行权限
if [ ! -x "$rc_local" ]; then
log "INFO" "🔧 正在为 rc.local 添加执行权限..."
chmod +x "$rc_local" 2>/dev/null || {
log "WARN" "⚠️ 无法添加执行权限,可能需要 root 权限"
}
fi
# 4. 检查是否已经存在相同的启动命令
if grep -Fq "cd /data/middleware/monitor/ && nohup ./malan &" "$rc_local" 2>/dev/null; then
log "INFO" "✅ malan 自启动配置已存在,跳过添加"
return 0
fi
# 5. 备份 rc.local
if [ -f "$rc_local" ]; then
local backup_file="${rc_local}.backup.$(date +%Y%m%d_%H%M%S)"
cp "$rc_local" "$backup_file" 2>/dev/null
if [ $? -eq 0 ]; then
log "INFO" "✅ 已备份 rc.local 文件: $backup_file"
else
log "WARN" "⚠️ 备份 rc.local 失败,继续执行"
fi
fi
# 6. 添加启动命令到 rc.local
log "INFO" "📝 正在添加 malan 启动命令到 $rc_local"
# 确保文件末尾有换行符
if [ -s "$rc_local" ] && [ "$(tail -c 1 "$rc_local")" != "" ]; then
echo "" >> "$rc_local"
fi
# 添加注释和启动命令
echo "# Auto-start malan service - $(date '+%Y-%m-%d %H:%M:%S')" >> "$rc_local"
echo "$startup_cmd" >> "$rc_local"
if [ $? -eq 0 ]; then
log "INFO" "✅ malan 自启动配置已添加到 $rc_local"
log "INFO" " 启动命令: $startup_cmd"
else
log "ERROR" "❌ 写入 $rc_local 失败,请检查权限"
return 1
fi
# 7. 验证写入的内容
if grep -Fq "cd /data/middleware/monitor/ && nohup ./malan &" "$rc_local" 2>/dev/null; then
log "INFO" "✅ 配置验证成功"
else
log "WARN" "⚠️ 配置验证失败,请手动检查 $rc_local"
fi
log "INFO" "=================================================="
log "INFO" "🎉 malan 自启动配置完成!"
log "INFO" " 配置文件: $rc_local"
log "INFO" " 提示: 系统重启后 malan 服务将自动启动"
log "INFO" "=================================================="
return 0
}
# ========================================
# 🚀 函数:start_java_meeting_service
# 描述:启动对外服务并将服务写入服务器自启动
# 功能:
# - 检查Java环境是否部署成功
# - 启动java-meeting-extapi服务
# - 检查服务进程是否启动成功
# - 将启动命令写入/etc/rc.d/rc.local实现自启动
# ========================================
function start_java_meeting_service() {
local service_dir="/data/services/api/java-meeting/java-meeting-extapi"
local run_script="$service_dir/run.sh"
local log_file="$service_dir/logs/ubains-INFO-AND-ERROR.log"
local rc_local="/etc/rc.d/rc.local"
local service_name="java-meeting-extapi"
log "INFO" "=================================================="
log "INFO" "🚀 正在启动对外服务: $service_name"
log "INFO" " 服务目录: $service_dir"
log "INFO" "=================================================="
# 1. 检查Java环境是否部署成功
log "INFO" "🔍 正在检查Java环境..."
source /etc/profile 2>/dev/null || . /etc/profile 2>/dev/null
source /etc/profile
if ! java -version >/dev/null 2>&1; then
log "ERROR" "❌ Java环境未部署或配置失败"
log "ERROR" " 请先执行 deploy_jdk_host 函数部署JDK"
log "ERROR" " 或手动执行: source /etc/profile"
return 1
fi
local java_version=$(java -version 2>&1 | head -n1)
log "INFO" "✅ Java环境检查通过: $java_version"
# 2. 检查服务目录和脚本是否存在
if [ ! -d "$service_dir" ]; then
log "ERROR" "❌ 服务目录不存在: $service_dir"
return 1
fi
if [ ! -f "$run_script" ]; then
log "ERROR" "❌ 启动脚本不存在: $run_script"
return 1
fi
# 确保脚本有执行权限
if [ ! -x "$run_script" ]; then
log "INFO" "🔧 正在为启动脚本添加执行权限..."
chmod +x "$run_script"
fi
log "INFO" "✅ 服务目录和脚本检查通过"
# 3. 检查服务是否已经在运行
cd "$service_dir" || {
log "ERROR" "❌ 无法进入服务目录: $service_dir"
return 1
}
# 尝试通过进程名或日志判断服务是否已运行
local process_count=$(pgrep -f "java-meeting-extapi\|run.sh" 2>/dev/null | wc -l)
if [ "$process_count" -gt 0 ]; then
log "WARN" "⚠️ 检测到服务可能已在运行(进程数: $process_count)"
log "INFO" " 将尝试重新启动服务"
fi
# 4. 启动服务
log "INFO" "▶️ 正在启动服务..."
# 确保日志目录存在
mkdir -p "$(dirname "$log_file")"
# 执行启动命令:./run.sh
log "INFO" "📝 执行启动命令: ./run.sh"
./run.sh > "$service_dir/nohup.out" 2>&1 &
local start_pid=$!
# 等待几秒让服务启动
log "INFO" "⏳ 等待服务启动(5秒)..."
sleep 5
# 执行tail -f查看日志(后台运行,避免阻塞)
log "INFO" "📋 正在查看服务日志: tail -f $log_file"
if [ -f "$log_file" ]; then
# 显示最后20行日志
log "INFO" "📄 最新日志内容(最后20行):"
tail -n 20 "$log_file" 2>/dev/null | while read line; do
log "INFO" " $line"
done
# 后台运行tail -f,输出到nohup
nohup tail -f "$log_file" >> "$service_dir/tail.log" 2>&1 &
log "INFO" "✅ 日志监控已启动(后台运行),实时日志输出到: $service_dir/tail.log"
else
log "WARN" "⚠️ 日志文件尚未创建: $log_file"
log "INFO" " 服务可能正在启动中,请稍后查看日志"
fi
# 5. 检查服务进程是否启动成功
log "INFO" "🔍 正在检查服务进程..."
# 检查run.sh进程
local run_pid=$(pgrep -f "run.sh" | head -n1)
if [ -n "$run_pid" ]; then
log "INFO" "✅ 启动脚本进程已运行(PID: $run_pid)"
else
log "WARN" "⚠️ 未检测到启动脚本进程,服务可能启动失败"
fi
# 检查Java进程(通过服务目录或进程名)
local java_pids=$(pgrep -f "java.*$service_dir\|java-meeting-extapi" 2>/dev/null)
if [ -n "$java_pids" ]; then
log "INFO" "✅ Java服务进程已启动"
echo "$java_pids" | while read pid; do
log "INFO" " PID: $pid"
done
else
log "WARN" "⚠️ 未检测到Java服务进程,请检查日志: $log_file"
log "WARN" " 或查看: $service_dir/nohup.out"
fi
# 检查日志文件是否存在(服务启动后会创建日志文件)
if [ -f "$log_file" ]; then
log "INFO" "✅ 日志文件已创建: $log_file"
# 显示最后几行日志
log "INFO" "📋 最新日志内容:"
tail -n 10 "$log_file" 2>/dev/null | while read line; do
log "INFO" " $line"
done
else
log "WARN" "⚠️ 日志文件尚未创建,服务可能正在启动中"
fi
# 6. 将启动命令写入/etc/rc.d/rc.local实现自启动
log "INFO" "📝 正在配置服务自启动..."
# 检查rc.local文件是否存在
if [ ! -f "$rc_local" ]; then
log "INFO" "📁 rc.local文件不存在,正在创建..."
sudo touch "$rc_local"
sudo chmod +x "$rc_local"
fi
# 构建启动命令(按照用户要求:cd目录,执行run.sh,tail -f日志)
# 注意:在rc.local中,tail -f会阻塞,所以使用后台运行
local startup_cmd="cd $service_dir && ./run.sh > $service_dir/nohup.out 2>&1 &"
local tail_cmd="tail -f $log_file >> $service_dir/tail.log 2>&1 &"
# 检查是否已经存在相同的启动命令
if grep -q "$service_dir.*run.sh" "$rc_local" 2>/dev/null; then
log "INFO" "✅ 自启动配置已存在,跳过添加"
else
# 备份rc.local
if [ -f "$rc_local" ]; then
sudo cp "$rc_local" "${rc_local}.backup.$(date +%Y%m%d_%H%M%S)" 2>/dev/null
log "INFO" "✅ 已备份rc.local文件"
fi
# 添加启动命令到rc.local
log "INFO" "📝 正在添加启动命令到 $rc_local"
echo "" | sudo tee -a "$rc_local" > /dev/null
echo "# Auto-start java-meeting-extapi service - $(date '+%Y-%m-%d %H:%M:%S')" | sudo tee -a "$rc_local" > /dev/null
echo "cd $service_dir && ./run.sh > $service_dir/nohup.out 2>&1 &" | sudo tee -a "$rc_local" > /dev/null
echo "tail -f $log_file >> $service_dir/tail.log 2>&1 &" | sudo tee -a "$rc_local" > /dev/null
# 确保rc.local有执行权限
sudo chmod +x "$rc_local" 2>/dev/null
log "INFO" "✅ 自启动配置已添加到 $rc_local"
log "INFO" " 启动命令: cd $service_dir && ./run.sh"
log "INFO" " 日志监控: tail -f $log_file"
fi
log "INFO" "=================================================="
log "INFO" "🎉 服务启动流程完成!"
log "INFO" " 服务目录: $service_dir"
log "INFO" " 日志文件: $log_file"
log "INFO" " 自启动配置: $rc_local"
log "INFO" " 提示: 可以使用 'tail -f $log_file' 查看实时日志"
log "INFO" "=================================================="
return 0
}
# ========================================
# ☕ 函数:deploy_jdk_host
# 描述:在宿主机上部署JDK(tar.gz格式)
# 功能:
# - 检查JDK安装包是否存在
# - 解压JDK到指定目录
# - 配置Java环境变量(JAVA_HOME, CLASSPATH, PATH)
# - 验证JDK安装
# 参数:
# $1: JDK安装包路径(tar.gz文件)
# $2: JDK安装目录(可选,默认为/opt/java)
# ========================================
function deploy_jdk_host() {
local jdk_tar_path="/data/temp/jdk-8u472-linux-x64.tar.gz"
local install_base_dir="${2:-/opt/java}"
log "INFO" "=================================================="
log "INFO" "☕ 正在部署宿主机JDK"
log "INFO" " JDK安装包: $jdk_tar_path"
log "INFO" " 安装目录: $install_base_dir"
log "INFO" "=================================================="
# 1. 检查JDK安装包是否存在
if [ -z "$jdk_tar_path" ]; then
log "ERROR" "❌ 未指定JDK安装包路径"
log "INFO" "用法: deploy_jdk_host <jdk_tar.gz路径> [安装目录]"
return 1
fi
if [ ! -f "$jdk_tar_path" ]; then
log "ERROR" "❌ JDK安装包不存在: $jdk_tar_path"
return 1
fi
# 检查是否为tar.gz格式
if [[ ! "$jdk_tar_path" =~ \.tar\.gz$ ]]; then
log "ERROR" "❌ 文件格式错误,需要tar.gz格式: $jdk_tar_path"
return 1
fi
log "INFO" "✅ JDK安装包检查通过"
# 2. 检查是否已安装JDK,并进行备份
local old_java_home=""
local old_java_version=""
local backup_dir="/opt/java_backup_$(date +%Y%m%d_%H%M%S)"
if command -v java &> /dev/null; then
old_java_version=$(java -version 2>&1 | head -n1)
log "INFO" "📋 检测到系统已安装Java: $old_java_version"
# 获取旧的JAVA_HOME
if [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ]; then
old_java_home="$JAVA_HOME"
else
# 尝试从java命令路径推断JAVA_HOME
local java_path=$(which java 2>/dev/null)
if [ -n "$java_path" ]; then
java_path=$(readlink -f "$java_path" 2>/dev/null || echo "$java_path")
# 移除/bin/java部分
old_java_home=$(dirname "$(dirname "$java_path")" 2>/dev/null)
fi
fi
# 备份旧的JDK目录
if [ -n "$old_java_home" ] && [ -d "$old_java_home" ] && [ "$old_java_home" != "$install_base_dir" ]; then
log "INFO" "💾 正在备份旧JDK目录: $old_java_home"
mkdir -p "$backup_dir"
if [ $? -eq 0 ]; then
cp -r "$old_java_home" "$backup_dir/old_jdk_$(basename "$old_java_home")" 2>/dev/null
if [ $? -eq 0 ]; then
log "INFO" "✅ 旧JDK目录已备份到: $backup_dir"
else
log "WARN" "⚠️ 旧JDK目录备份失败,继续安装"
fi
fi
fi
# 备份/etc/profile中的Java配置
log "INFO" "💾 正在备份/etc/profile中的Java配置..."
mkdir -p "$backup_dir"
if [ -f /etc/profile ]; then
cp /etc/profile "$backup_dir/profile.backup" 2>/dev/null
if [ $? -eq 0 ]; then
log "INFO" "✅ /etc/profile已备份到: $backup_dir/profile.backup"
fi
fi
# 记录旧Java信息到备份目录
if [ -d "$backup_dir" ]; then
{
echo "备份时间: $(date '+%Y-%m-%d %H:%M:%S')"
echo "旧Java版本: $old_java_version"
echo "旧JAVA_HOME: $old_java_home"
} > "$backup_dir/old_java_info.txt" 2>/dev/null
fi
log "INFO" "🔄 将自动更新Java环境..."
fi
# 3. 创建安装目录
mkdir -p "$install_base_dir"
if [ $? -ne 0 ]; then
log "ERROR" "❌ 无法创建安装目录: $install_base_dir"
return 1
fi
log "INFO" "✅ 创建安装目录: $install_base_dir"
# 4. 解压JDK
log "INFO" "🗜️ 正在解压JDK安装包..."
cd "$install_base_dir" || {
log "ERROR" "❌ 无法进入目录: $install_base_dir"
return 1
}
# 解压到当前目录
tar -xzf "$jdk_tar_path" -C "$install_base_dir"
if [ $? -ne 0 ]; then
log "ERROR" "❌ JDK解压失败,请检查压缩包完整性"
return 1
fi
log "INFO" "✅ JDK解压完成"
# 5. 查找解压后的JDK目录(通常是jdk8u472-b08或类似格式)
local jdk_dir=$(find "$install_base_dir" -maxdepth 1 -type d -name "jdk*" | head -n1)
if [ -z "$jdk_dir" ]; then
log "ERROR" "❌ 未找到JDK目录,解压后的目录结构可能不正确"
log "INFO" "请检查解压后的目录结构"
return 1
fi
local jdk_home="$jdk_dir"
local jdk_name=$(basename "$jdk_dir")
log "INFO" "✅ 找到JDK目录: $jdk_name"
log "INFO" " JDK_HOME: $jdk_home"
# 6. 检查JDK目录结构是否完整
if [ ! -d "$jdk_home/bin" ] || [ ! -f "$jdk_home/bin/java" ]; then
log "ERROR" "❌ JDK目录结构不完整,缺少bin/java文件"
return 1
fi
log "INFO" "✅ JDK目录结构检查通过"
# 7. 配置Java环境变量
log "INFO" "🛠️ 正在配置Java环境变量..."
# 检查/etc/profile中是否已存在JAVA_HOME配置,并备份后注释掉
if grep -q "JAVA_HOME\|#set java environment" /etc/profile; then
log "INFO" "📋 检测到/etc/profile中已存在JAVA_HOME配置,将注释旧配置并添加新配置"
# 备份旧的配置(如果之前没有备份)
if [ ! -f "$backup_dir/profile.backup" ]; then
mkdir -p "$backup_dir"
cp /etc/profile "$backup_dir/profile.backup" 2>/dev/null
log "INFO" "✅ /etc/profile已备份到: $backup_dir/profile.backup"
fi
# 使用awk来注释掉Java配置块(更可靠,处理多行块)
awk '
BEGIN { in_java_block = 0 }
/#set java environment/ {
in_java_block = 1
# 如果这行还没被注释,就注释掉
if ($0 !~ /^[[:space:]]*#/) {
print "#" $0
} else {
print $0
}
next
}
in_java_block {
# 如果是Java相关的配置行(JAVA_HOME, CLASSPATH, PATH, export等)
if ($0 ~ /JAVA_HOME|CLASSPATH|PATH.*JAVA_HOME|export.*JAVA_HOME|export.*CLASSPATH|export.*PATH/) {
# 如果这行还没被注释,就注释掉
if ($0 !~ /^[[:space:]]*#/ && $0 !~ /^[[:space:]]*$/) {
print "#" $0
} else {
print $0
}
next
}
# 如果是空行,保持块状态,继续
if ($0 ~ /^[[:space:]]*$/) {
print $0
next
}
# 其他非空行,结束Java块
in_java_block = 0
print $0
next
}
{ print $0 }
' /etc/profile > /etc/profile.tmp && mv /etc/profile.tmp /etc/profile
if [ $? -eq 0 ]; then
log "INFO" "✅ 旧Java配置已注释"
else
log "WARN" "⚠️ 注释旧配置时出现问题,继续安装"
fi
fi
# 添加新的Java环境变量配置
cat >> /etc/profile << EOF
#set java environment
JAVA_HOME=$jdk_home
CLASSPATH=.:\$JAVA_HOME/lib/tools.jar
PATH=\$JAVA_HOME/bin:\$PATH
export JAVA_HOME CLASSPATH PATH
EOF
log "INFO" "✅ Java环境变量已写入 /etc/profile"
# 8. 立即在当前会话中生效
log "INFO" "🔄 正在重新加载 /etc/profile 使环境变量生效..."
source /etc/profile 2>/dev/null || . /etc/profile 2>/dev/null
source /etc/profile
# 如果source失败,则手动设置环境变量
if [ $? -ne 0 ]; then
log "WARN" "⚠️ source /etc/profile 失败,使用export设置环境变量"
export JAVA_HOME="$jdk_home"
export CLASSPATH=".:$JAVA_HOME/lib/tools.jar"
export PATH="$JAVA_HOME/bin:$PATH"
fi
log "INFO" "✅ 环境变量已在当前会话中生效"
# 9. 验证Java安装
log "INFO" "🔍 正在验证Java安装..."
if java -version >/dev/null 2>&1; then
local java_version=$(java -version 2>&1 | head -n1)
local java_path=$(which java)
log "INFO" "✅ Java环境配置成功!"
log "INFO" " Java版本: $java_version"
log "INFO" " Java路径: $java_path"
log "INFO" " JAVA_HOME: $JAVA_HOME"
else
log "ERROR" "❌ Java环境配置失败,请检查路径或权限"
log "INFO" " 请手动执行: source /etc/profile"
return 1
fi
log "INFO" "=================================================="
log "INFO" "🎉 JDK部署成功!"
log "INFO" " 安装路径: $jdk_home"
log "INFO" " 环境变量已配置到 /etc/profile"
log "INFO" " 提示: 新开终端或执行 'source /etc/profile' 使环境变量生效"
log "INFO" "=================================================="
return 0
}
#------------------------------服务安装-end------------------------------------------------------------------------------------------------------------------------
#------------------------------全局配置-start--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
###服务器检测配置项
#mem-check
#最小内存大小G
min_memory_value=8
#最小有效内存大小G
min_memory_available=8
#disk-check
#最小硬盘资源大小G
#min_disk_value=200
#最小有效硬盘大小G
min_disk_available=50
#默认为空,如果为非root用户执行,则需要配置为sudo
sudoset=""
#------------------------------全局配置-end--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
#-------------------------------脚本执行-start--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
sys1="ubains.txt"
sys2="devops.txt"
sys3="ubains_devops.txt"
checkOS
if [ -e "$PWD/$sys1" ]; then
txt="检测该脚本为【预定系统 部署】"
version="预定系统版本"
elif [ -e "$PWD/$sys2" ]; then
txt="检测该脚本为【运维系统 部署】"
version="运维系统版本"
elif [ -e "$PWD/$sys3" ]; then
txt="检测该脚本为【预定系统+运维系统 部署】"
version="预定系统+运维系统版本"
fi
#-------------------------------获取服务器ip-start--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
#文件默认ip
ip='192.168.9.84'
IP=$(hostname -I | awk '{print $1}')
function server_ip()
{
# 弹出弹框,并根据用户选择执行相应操作
choice=$(whiptail --title "请选择操作" \
--menu "请确认服务器ip是否正确:$IP" 15 50 4 \
1 "是" \
2 "否" \
3 "退出" \
3>&1 1>&2 2>&3)
# 根据用户选择执行相应操作
case $choice in
1)
# 在这里编写操作1的代码逻辑
;;
2)
# 设置对话框的标题和提示信息
dialog_title="请输入服务器IP:"
dialog_prompt="请输入服务器IP:"
# 使用whiptail显示输入框,并将用户输入的值保存到变量input_value
IP=$(whiptail --title "$dialog_title" --inputbox "$dialog_prompt" 10 50 3>&1 1>&2 2>&3)
# 在这里编写操作2的代码逻辑
;;
3)
exit
# 在这里编写操作3的代码逻辑
;;
*)
echo "无效的选择"
;;
esac
}
#-------------------------------获取服务器的ip-end-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
function getLatestVersion() {
log "INFO" "检查中间件最新版本信息"
#判断是否能够访问外网,不能就跳过联网检查
if ! curl -s --head "https://www.baidu.com" | grep "200 OK" > /dev/null; then
log "WARN" "无法访问外网,跳过最新版本检查"
return
fi
#todo:如果访问不到,就跳过,后面改成用自己的接口服务
log "INFO" "准备获取最新版本信息"
latest_nginx_version=$(curl -s "https://nginx.org/en/download.html" | grep -oP 'nginx-\K[0-9]+\.[0-9]+\.[0-9]+' | head -n 1)
latest_redis_version=$(curl -s "https://api.github.com/repos/redis/redis/releases/latest" | \
grep -m 1 '"tag_name"' | \
grep -oP '[0-9]+\.[0-9]+\.[0-9]+' )
latest_mysql_version=$(curl -s https://repo.mysql.com/yum/mysql-8.0-community/el/7/x86_64/ \
| grep -oP 'mysql-community-server-[0-9]+\.[0-9]+\.[0-9]+' \
| sort -V \
| tail -n 1 \
| grep -oP '[0-9]+\.[0-9]+\.[0-9]+'
)
log "INFO" "最新版本信息获取成功"
log "INFO" "准备获取本地版本信息"
current_nginx_version=$(docker exec ujava2 sh -c "/usr/local/nginx/sbin/nginx -v " 2>&1 | grep -oP 'nginx/\K[0-9\.]+')
current_redis_version=$(docker exec uredis sh -c "redis-server --version " 2>&1 | grep -oP 'Redis server v=\K[0-9\.]+')
current_mysql_version=$(docker exec umysql sh -c "mysql --version " 2>&1 | grep -oP 'mysql Ver \K[0-9\.]+')
log "INFO" "本地版本信息获取成功, 准备进行版本比较"
#比较不同的版本情况,不一样的给出告警提示
if [ "$latest_nginx_version" != "$current_nginx_version" ]; then
log "WARN" "Nginx版本不一致,最新版本: $latest_nginx_version, 当前版本: $current_nginx_version"
else
log "INFO" "Nginx版本一致,当前版本: $current_nginx_version"
fi
if [ "$latest_redis_version" != "$current_redis_version" ]; then
log "WARN" "Redis版本不一致,最新版本: $latest_redis_version, 当前版本: $current_redis_version"
else
log "INFO" "Redis版本一致,当前版本: $current_redis_version"
fi
if [ "$latest_mysql_version" != "$current_mysql_version" ]; then
log "WARN" "MySQL版本不一致,最新版本: $latest_mysql_version, 当前版本: $current_mysql_version"
else
log "INFO" "MySQL版本一致,当前版本: $current_mysql_version"
fi
log "INFO" "版本检查完成"
}
#-------------------------------部署系统-------------------------------
log "INFO" "\033[36m
---------------------------------------------开始部署系统---------------------------------------------------
\033[0m"
server_ip
server_ip="$IP"
#自动化脚本路径
auto_java=$PWD
add_user
# 文件检查与部署
check_files_upload
#开放防火墙
firewalldjava
# 部署中间件服务(mysql, redis, emqx, fdfs, ngrok, nacos, nginx)
middleware_type="mysql redis emqx fdfs nacos nginx" install_middleware
ntp_uos
deploy_jdk_host
start_java_meeting_service
malan
deploy_services
add_crontab_job
#检查中间件版本
getLatestVersion
#___________________________________________________________________________________
log "INFO" "正在重启docker服务,请耐心等待。"
$sudoset systemctl restart docker
sleep 30
log "INFO" "查看服务是否启动成功——————docker ps"
log "INFO" "系统服务:umysql,uredis,ujava2,uemqx,ustorage,utracker "
$sudoset docker ps
\ No newline at end of file
#!/bin/bash
#------------------------------工具类模块---------------------------------------------------
# 日志打印函数
LOG_FILE="/data/log/new_auto_script.log"
mkdir -p "$(dirname "$LOG_FILE")" 2>/dev/null
# 用户输入的东西也同步记录到日志信息里面
function log() {
# 获取当前时间
local timestamp=$(date "+%Y-%m-%d %H:%M:%S")
# 日志级别对应的颜色代码
local INFO_COLOR="\033[32m" # 绿色
local WARN_COLOR="\033[33m" # 黄色
local ERROR_COLOR="\033[31m" # 红色
local DEBUG_COLOR="\033[36m" # 青色
local RESET_COLOR="\033[0m" # 重置颜色
# 参数判断
if [ $# -lt 2 ]; then
echo "Usage: log <level> <message>"
return 1
fi
local level=$1
local message=$2
# 根据日志级别选择颜色
case $level in
"INFO")
color=$INFO_COLOR
;;
"WARN")
color=$WARN_COLOR
;;
"ERROR")
color=$ERROR_COLOR
;;
"DEBUG")
color=$DEBUG_COLOR
;;
*)
echo "Invalid log level: $level"
return 1
;;
esac
# 输出带颜色的日志到屏幕
echo -e "${color}[$timestamp] [$level] $message${RESET_COLOR}"
# 追加纯文本日志到文件
echo "[$timestamp] [$level] $message" >> "$LOG_FILE"
}
#------------------------------导入中间件部署模块---------------------------------------------------
# 获取脚本所在目录
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# 导入中间件部署脚本
if [[ -f "$SCRIPT_DIR/auto_middleware_install.sh" ]]; then
source "$SCRIPT_DIR/auto_middleware_install.sh"
else
log "ERROR" "中间件部署脚本不存在: $SCRIPT_DIR/auto_middleware_install.sh"
exit 1
fi
# 导入文件检查脚本
if [[ -f "$SCRIPT_DIR/auto_file_upload_check.sh" ]]; then
source "$SCRIPT_DIR/auto_file_upload_check.sh"
else
log "ERROR" "文件检查脚本不存在: $SCRIPT_DIR/auto_file_upload_check.sh"
exit 1
fi
# 导入定时任务设置脚本
if [[ -f "$SCRIPT_DIR/auto_crontab_settings.sh" ]]; then
source "$SCRIPT_DIR/auto_crontab_settings.sh"
else
log "ERROR" "定时任务设置脚本不存在: $SCRIPT_DIR/auto_crontab_settings.sh"
exit 1
fi
# 导入主服务部署脚本
if [[ -f "$SCRIPT_DIR/auto_deploy_services.sh" ]]; then
source "$SCRIPT_DIR/auto_deploy_services.sh"
else
log "ERROR" "主服务部署脚本不存在: $SCRIPT_DIR/auto_deploy_services.sh"
exit 1
fi
# 导入防火墙设置脚本
if [[ -f "$SCRIPT_DIR/auto_firewall_settings.sh" ]]; then
source "$SCRIPT_DIR/auto_firewall_settings.sh"
else
log "ERROR" "防火墙设置脚本不存在: $SCRIPT_DIR/auto_firewall_settings.sh"
exit 1
fi
#------------------------------检测模块-----------------------------------------------------
#------------------------------服务器检测-start---------------------------------------------
function detectNetType() {
log "INFO" "准备获取所有网口信息,排除 loopback 和虚拟接口"
INTERFACES=$(ls /sys/class/net | grep -vE 'lo|docker|br-|veth|virbr|vmnet')
# 遍历每个网口,检查是否配置了 IP 地址,并输出网口信息
log "INFO" "检测到的网口信息如下:"
for iface in $INTERFACES; do
log "INFO" "-----------------------------"
log "INFO" "接口: $iface"
log "INFO" " 检查接口 $iface 是否配置了 IP 地址"
IP_ADDR=$(ip addr show "$iface" | grep "inet " | awk '{print $2}')
if [[ -n "$IP_ADDR" ]]; then
log "INFO" " 配置的 IP 地址: $IP_ADDR"
else
log "INFO" " 没有配置 IP 地址"
fi
# 获取接口的连接状态(carrier)
log "INFO" " 检查接口 $iface 的连接状态"
if [[ -f /sys/class/net/$iface/carrier ]]; then
CARRIER=$(cat /sys/class/net/$iface/carrier)
[[ "$CARRIER" == "1" ]] && LINK="connected" || LINK="disconnected"
else
LINK="unknown"
fi
log "INFO" " 状态: $LINK"
# 使用 ethtool 获取接口的类型(光口、电口等)
log "INFO" " 获取接口 $iface 的类型"
ETHTOOL_OUTPUT=$(ethtool "$iface" 2>/dev/null)
TYPE=$(echo "$ETHTOOL_OUTPUT" | grep -i "Port:" | awk '{print tolower($2)}')
[[ -z "$TYPE" ]] && TYPE="unknown" && TYPE="other"
# 获取速率和驱动信息
log "INFO" " 获取接口 $iface 的速率和驱动信息"
SPEED=$(echo "$ETHTOOL_OUTPUT" | grep -i "Speed:" | awk '{print $2}')
[[ -z "$SPEED" ]] && SPEED="unknown"
DRIVER=$(ethtool -i "$iface" 2>/dev/null | grep "driver:" | awk '{print $2}')
[[ -z "$DRIVER" ]] && DRIVER="unknown"
# 获取 MAC 地址
log "INFO" " 获取接口 $iface 的 MAC 地址"
MAC=$(cat /sys/class/net/$iface/address)
# 转换 TYPE 为中文描述
case "$TYPE" in
"copper")
TYPE="电口"
;;
"fiber")
TYPE="光口"
;;
"other")
TYPE="其他类型"
;;
*)
TYPE="未知类型"
;;
esac
# 输出接口信息
log "INFO" " 光电口类型: ${TYPE:-unknown}"
log "INFO" " 速率: $SPEED"
log "INFO" " MAC 地址: $MAC"
log "INFO" " 驱动: $DRIVER"
done
}
#------------------------------服务器空间检测-start---------------------------------------------
# 功能说明:检查服务器空间是否满足部署要求
# 调用方式:checkServerSpace
# 返回值:无,直接exit或继续执行
function checkServerSpace() {
log "INFO" "开始检查服务器空间分配"
# 判断当前服务器类型
if [ -d "/data" ]; then
system_type="new_type"
log "INFO" "检测到/data目录,系统类型为:新统一平台"
else
system_type="old_type"
log "INFO" "未检测到/data目录,系统类型为:传统平台"
fi
# 获取脚本所在目录
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# 调用空间检查脚本
log "INFO" "执行空间检查脚本..."
system_type=$system_type "$SCRIPT_DIR/auto_check_space.sh"
return_code=$?
# 根据返回值处理
case $return_code in
0)
log "INFO" "服务器空间检查通过,继续执行部署"
;;
1)
log "ERROR" "服务器空间不足,停止部署"
exit 1
;;
2)
log "ERROR" "服务器未正确分区,停止部署"
exit 1
;;
*)
log "ERROR" "空间检查返回异常值: $return_code,停止部署"
exit 1
;;
esac
log "INFO" "服务器空间检查完成"
}
#------------------------------服务器空间检测-end-----------------------------------------------
function checkOS() {
log "INFO" "开始检测服务器环境"
log "INFO" "**************《请检查当前系统是否适配》**************************************************"
# 检测当前的操作系统是否为x86架构
log "INFO" "开始检查当前操作系统架构"
if [ "$(uname -m)" != "x86_64" ]; then
log "ERROR" "当前操作系统不是x86架构,请使用x86架构的操作系统"
exit 1
else
log "INFO" "当前操作系统为x86架构,继续执行脚本"
fi
server_available_mem=`free -g |awk 'NR==2' | awk '{print$7}'`
server_max_mem=`free -g|awk 'NR==2' |awk '{print$2}'`
log "INFO" "当前系统最大内存为:${server_max_mem}GB 可用内存为:${server_available_mem}GB"
#判断服务器的最大内存值小于约定的内存大小min_memory_value=15,就返回。否则成功。
log "INFO" "开始检查服务器最大内存"
if [ $server_max_mem -lt $min_memory_value ];then
log "WARN" "当前系统最大内存为:${server_max_mem}G,建议将内存至少添加为: ${min_memory_value}G"
# 需要用户确认信息,支持默认空格和y或Y,并日志记录用户输入情况
read -p "是否继续执行脚本?(y/n): " yn
if [[ -z "$yn" || "$yn" =~ ^[yY]$ ]]; then
# 记录用户如情况
log "INFO" "用户选择继续执行脚本,输入为${yn} "
else
log "ERROR" "脚本执行终止,请调整服务器内存后重新运行脚本"
exit 1
fi
else
log "INFO" "检查当前系统最大内存为:${server_max_mem}G 满足系统运行!"
fi
#todo:考虑把交换关闭掉
#判断服务器的内存值小于约定的内存大小min_memory_available=10,就返回。否则成功。
log "INFO" "开始检查服务器可用内存"
if [ $server_available_mem -lt $min_memory_available ];then
log "WARN" "当前系统docker可用内存为:${server_available_mem}G,建议将内存至少添加为: ${min_memory_available}G !"
# 需要用户确认信息,支持默认空格和y或Y,并日志记录用户输入情况
read -p "是否继续执行脚本?(y/n): " yn
if [[ -z "$yn" || "$yn" =~ ^[yY]$ ]]; then
# 记录用户如情况
log "INFO" "用户选择继续执行脚本,输入为${yn} "
else
log "ERROR" "脚本执行终止,请调整服务器内存后重新运行脚本"
exit 1
fi
else
log "INFO" "检查当前系统docker可用内存为:${server_available_mem}G 满足系统运行!"
fi
#判断服务的根分区的硬盘大小是否超过预设的硬盘资源。成功继续,否则退出。
log "INFO" "开始检查服务器硬盘空间"
server_disk_available=`df -B G /|awk '/\//{print$4}' | awk '{sub(/.{1}$/,"")}1'`
if [ $server_disk_available -lt $min_disk_available ];then
log "WARN" "当前系统的硬盘空间为:${server_disk_available}G,建议将硬盘空间至少添加为: ${min_disk_available}G!"
# 需要用户确认信息,支持默认空格和y或Y,并日志记录用户输入情况
read -p "是否继续执行脚本?(y/n): " yn
if [[ -z "$yn" || "$yn" =~ ^[yY]$ ]]; then
# 记录用户如情况
log "INFO" "用户选择继续执行脚本,输入为${yn} "
else
log "ERROR" "脚本执行终止,请调整服务器磁盘后重新运行脚本"
exit 1
fi
else
log "INFO" "检查当前系统硬盘空间为:${server_disk_available}G,满足系统运行!"
fi
# 检测当前用户是否有root权限
log "INFO" "开始检查当前用户权限"
if [ "$(id -u)" -ne 0 ]; then
log "ERROR" "当前用户没有root权限,请使用root用户执行脚本,获者使用sudo命令执行脚本"
exit 1
else
log "INFO" "当前用户有root权限,继续执行脚本"
fi
log "INFO" "当前用户权限检查完成"
# 检测当前服务器是否存在多张网卡并且ipv4不唯一的需要列出来
log "INFO" "开始检查服务器网卡信息"
network_interfaces=$(ip -o -4 addr show | awk '{print $2}' | sort -u)
if [ -z "$network_interfaces" ]; then
log "ERROR" "当前服务器没有检测到任何网卡,请检查网络配置"
exit 1
else
log "INFO" "当前服务器检测到的网卡信息如下:$network_interfaces"
# 获取对应的IP信息
for interface in $network_interfaces; do
ip_address=$(ip -o -4 addr show "$interface" | awk '{print $4}' | cut -d/ -f1)
if [ -n "$ip_address" ]; then
log "INFO" "网卡 $interface 的 IP 地址为: $ip_address"
IP=$ip_address
else
log "WARN" "网卡 $interface 没有配置 IPv4 地址"
fi
done
fi
log "INFO" "服务器网卡信息检查完成"
log "INFO" "开始检测服务器网口类型"
detectNetType
log "INFO" "服务器网口类型检测完成"
# 服务器空间检查
checkServerSpace
# 用户确认以后继续往下
read -p "请确认网口信息是否正确,特别注意需要仔细观察你的项目是要光口还是电口 ,输入 y 或 Y 确认,输入 n 或 N 退出脚本: " yn
if [[ -z "$yn" || "$yn" =~ ^[yY]$ ]]; then
log "INFO" "用户确认网口信息正确,继续执行脚本"
else
log "ERROR" "用户确认网口信息不正确,脚本执行终止"
exit 1
fi
echo -e "\033[36m
*******************************************************
* 可适配的操作系统(x86) *
* *
* cento7.5 *
* cento7.6 *
* cento7.9 *
* 统信系统 *
* 鲲鹏系统 *
* Ubuntu *
* redhat7 *
* 银河麒麟V10 *
* openEuler 24.03 *
* *
*******************************************************
\033[0m"
echo -e "\033[36m\n检测到当前操作系统如下,请确认是否适配:\n \033[0m"
# 优先获取PRETTY_NAME,没有则拼接NAME和VERSION
log "INFO" "开始获取当前操作系统版本信息"
if grep -q '^PRETTY_NAME=' /etc/os-release; then
os_version=$(grep '^PRETTY_NAME=' /etc/os-release | cut -d= -f2 | tr -d '"')
else
name=$(grep '^NAME=' /etc/os-release | cut -d= -f2 | tr -d '"')
version=$(grep '^VERSION=' /etc/os-release | cut -d= -f2 | tr -d '"')
os_version="$name $version"
fi
log "INFO" "当前操作系统版本为: $os_version"
case "$os_version" in
*"CentOS Linux 7"*|*"UOS"*|*"Kylin V10"*|*"Ubuntu"*|*"Red Hat Enterprise Linux Server 7"*|*"openEuler 24.03"*)
log "INFO" "当前操作系统版本适配,继续执行脚本" ;;
*)
log "ERROR" "当前操作系统版本不适配,请使用CentOS 7、UOS、麒麟V10、Ubuntu或Red Hat Enterprise Linux Server 7 openEuler 24.03"
# 需要用户确认信息,支持默认空格和y或Y,并日志记录用户输入情况
read -p "是否继续执行脚本?(y/n): " yn
if [[ -z "$yn" || "$yn" =~ ^[yY]$ ]]; then
# 记录用户如情况
log "INFO" "用户选择继续执行脚本,输入为${yn} "
else
log "ERROR" "脚本执行终止,请调整服务器操作系统后重新运行脚本"
exit 1
fi;;
esac
log "INFO" "当前操作系统版本适配,继续执行脚本"
#检查服务器时间和时区
log "INFO" "开始检查服务器时间和时区"
log "INFO" "**************《请检查当前时间,时区是否正确》*****************************************************************"
log "INFO" "当前服务器时区、时间为: $(date)"
read -p "确认无误请按 y 或 Y 或直接回车,不正确请按 n 修改时间: " yn
if [[ -z "$yn" || "$yn" =~ ^[yY]$ ]]; then
log "INFO" "下面进入系统部署。。。"
sleep 3
else
read -p "输入当前日期,格式如2021/02/06——" da
echo "="
date -s $da
read -p "输入当前时间,格式如18:00:00——" sa
echo "="
date -s $sa
timedatectl set-timezone Asia/Shanghai
clock -w
echo "==================================================="
echo "查看修改后的时间"
date
fi
}
log "INFO" "服务器检测完成"
#创建用户(预定安装需要:该用户与系统启动用户有关)
function add_user()
{
log "INFO" "创建用户user"
user=user
group=user
log "INFO" "判断是否存在用户$user"
egrep "^$user" /etc/passwd >& /dev/null
if [ $? -ne 0 ]; then
log "INFO" "用户$user不存在,开始创建用户"
username="user"
password="Ubains@135"
useradd -m "$username"
echo "$username:$password" | chpasswd
log "INFO" "用户$user创建成功"
else
log "INFO" "用户user已创建! "
fi
}
#x86架构ntp安装
function ntp_x86()
{
# 判断是否为centos7 否则跳出函数
if [ ! -f /etc/redhat-release ]; then
log "ERROR" "当前系统不是CentOS 7,无法安装NTP服务"
return 1
fi
log "INFO" "检查ntp服务......"
$sudoset systemctl status ntpd |grep running
#ps -aux | grep ntp
if [ $? -eq 0 ]; then
log "INFO" "检查到ntp已安装!"
else
log "WARN" "检查到ntp未安装,正在安装..."
$sudoset cd $auto_java/server_bag/ntp
$sudoset rpm -Uvh *.rpm --nodeps --force
$sudoset systemctl enable ntpd
$sudoset systemctl enable ntpdate
$sudoset systemctl start ntpd
$sudoset systemctl status ntpd
$sudoset mv /etc/ntp.conf /etc/ntp.confbak
$sudoset cp $auto_java/server_bag/ntp/ntp.conf /etc/
$sudoset systemctl restart ntpd
$sudoset systemctl disable chronyd
log "INFO" "完成ntp服务的安装"
fi
}
#x86架构统信、麒麟系统的ntp安装
function ntp_uos() {
# 判断如果是centos7就退出安装
if [ -f /etc/redhat-release ]; then
log "ERROR" "当前系统是CentOS 7,已经安装NTP服务,无需安装chrony服务"
return 1
fi
# 判断文件是否存在ntp1.aliyun.com地址
if grep -q "ntp1.aliyun.com" /etc/chrony.conf; then
log "INFO" "NTP配置文件中已存在ntp1.aliyun.com地址,无需重复添加"
return 0
fi
local config_dir="/data/temp/ntp"
local backup="/etc/chrony.confbak"
local target="/etc/chrony.conf"
# 切换目录(不需要 sudo)
cd "$config_dir" || { echo "无法进入目录: $config_dir"; return 1; }
# 备份原有配置
if [ -f "$target" ]; then
log "INFO" "正在备份旧配置文件到 $backup"
sudo cp "$target" "$backup"
fi
# 检查是否可以通外网,可以则配置外网的ntp服务器地址,不可以则询问是否有企业ntp服务器地址
if ! ping -c 1 ntp1.aliyun.com &> /dev/null; then
log "ERROR" "无法连接到外网NTP服务器 ntp1.aliyun.com,请检查网络连接"
read -p "是否有企业NTP服务器地址?(y/n): " yn
if [[ -z "$yn" || "$yn" =~ ^[yY]$ ]]; then
read -p "请输入企业NTP服务器地址: " custom_ntp
echo "server $custom_ntp iburst" | sudo tee -a "$target"
echo "allow all" | sudo tee -a "$target"
else
log "ERROR" "没有可用的NTP服务器地址,配置默认地址,请联系管理员"
echo "server ntp1.aliyun.com iburst" | sudo tee -a "$target"
echo "allow all" | sudo tee -a "$target"
fi
else
log "INFO" "可以连接到外网NTP服务器 ntp1.aliyun.com,继续配置"
# 如果可以连接到外网NTP服务器,则继续添加默认的NTP服务器地址
echo "server ntp1.aliyun.com iburst" | sudo tee -a "$target"
echo "server ntp2.aliyun.com iburst" | sudo tee -a "$target"
echo "server ntp3.aliyun.com iburst" | sudo tee -a "$target"
echo "allow all" | sudo tee -a "$target"
fi
# 输出同步地址到日志
log "INFO" "正在写入同步地址到 $target"
# 重启服务
log "INFO" "正在重启 chronyd 服务..."
sudo systemctl daemon-reload
sudo systemctl restart chronyd
sudo systemctl enable chronyd
sudo systemctl status chronyd
log "INFO" "chronyd 服务已重启并设置为开机自启"
}
# ========================================
# 🔧 函数:malan
# 描述:配置 malan 服务自启动(写入 /etc/rc.local)
# 功能:
# - 检查 /etc/rc.local 文件是否存在
# - 检查是否已存在 malan 启动命令(避免重复添加)
# - 将启动命令写入 /etc/rc.local
# - 确保文件有执行权限
# ========================================
function malan() {
local rc_local="/etc/rc.local"
local service_dir="/data/middleware/monitor"
local startup_cmd="cd /data/middleware/monitor/ && nohup ./malan &"
log "INFO" "=================================================="
log "INFO" "🔧 正在配置 malan 服务自启动"
log "INFO" " 服务目录: $service_dir"
log "INFO" "=================================================="
# 1. 检查服务目录是否存在
if [ ! -d "$service_dir" ]; then
log "WARN" "⚠️ 服务目录不存在: $service_dir"
log "WARN" " 将尝试创建目录..."
mkdir -p "$service_dir" 2>/dev/null || {
log "ERROR" "❌ 无法创建目录: $service_dir"
return 1
}
log "INFO" "✅ 目录已创建: $service_dir"
fi
# 2. 检查 malan 可执行文件是否存在
if [ ! -f "$service_dir/malan" ]; then
log "WARN" "⚠️ malan 可执行文件不存在: $service_dir/malan"
log "WARN" " 将继续配置自启动,但请确保文件已部署"
else
# 确保文件有执行权限
if [ ! -x "$service_dir/malan" ]; then
log "INFO" "🔧 正在为 malan 添加执行权限..."
chmod +x "$service_dir/malan" 2>/dev/null || {
log "WARN" "⚠️ 无法添加执行权限,可能需要 root 权限"
}
fi
log "INFO" "✅ malan 文件检查通过"
fi
# 3. 检查 /etc/rc.local 文件是否存在
if [ ! -f "$rc_local" ]; then
log "INFO" "📁 rc.local 文件不存在,正在创建..."
touch "$rc_local" 2>/dev/null || {
log "ERROR" "❌ 无法创建 $rc_local,请检查权限"
return 1
}
# 添加 shebang 行(如果文件为空)
echo "#!/bin/bash" > "$rc_local"
log "INFO" "✅ rc.local 文件已创建"
fi
# 确保 rc.local 有执行权限
if [ ! -x "$rc_local" ]; then
log "INFO" "🔧 正在为 rc.local 添加执行权限..."
chmod +x "$rc_local" 2>/dev/null || {
log "WARN" "⚠️ 无法添加执行权限,可能需要 root 权限"
}
fi
# 4. 检查是否已经存在相同的启动命令
if grep -Fq "cd /data/middleware/monitor/ && nohup ./malan &" "$rc_local" 2>/dev/null; then
log "INFO" "✅ malan 自启动配置已存在,跳过添加"
return 0
fi
# 5. 备份 rc.local
if [ -f "$rc_local" ]; then
local backup_file="${rc_local}.backup.$(date +%Y%m%d_%H%M%S)"
cp "$rc_local" "$backup_file" 2>/dev/null
if [ $? -eq 0 ]; then
log "INFO" "✅ 已备份 rc.local 文件: $backup_file"
else
log "WARN" "⚠️ 备份 rc.local 失败,继续执行"
fi
fi
# 6. 添加启动命令到 rc.local
log "INFO" "📝 正在添加 malan 启动命令到 $rc_local"
# 确保文件末尾有换行符
if [ -s "$rc_local" ] && [ "$(tail -c 1 "$rc_local")" != "" ]; then
echo "" >> "$rc_local"
fi
# 添加注释和启动命令
echo "# Auto-start malan service - $(date '+%Y-%m-%d %H:%M:%S')" >> "$rc_local"
echo "$startup_cmd" >> "$rc_local"
if [ $? -eq 0 ]; then
log "INFO" "✅ malan 自启动配置已添加到 $rc_local"
log "INFO" " 启动命令: $startup_cmd"
else
log "ERROR" "❌ 写入 $rc_local 失败,请检查权限"
return 1
fi
# 7. 验证写入的内容
if grep -Fq "cd /data/middleware/monitor/ && nohup ./malan &" "$rc_local" 2>/dev/null; then
log "INFO" "✅ 配置验证成功"
else
log "WARN" "⚠️ 配置验证失败,请手动检查 $rc_local"
fi
log "INFO" "=================================================="
log "INFO" "🎉 malan 自启动配置完成!"
log "INFO" " 配置文件: $rc_local"
log "INFO" " 提示: 系统重启后 malan 服务将自动启动"
log "INFO" "=================================================="
return 0
}
# ========================================
# 🚀 函数:start_java_meeting_service
# 描述:启动对外服务并将服务写入服务器自启动
# 功能:
# - 检查Java环境是否部署成功
# - 启动java-meeting-extapi服务
# - 检查服务进程是否启动成功
# - 将启动命令写入/etc/rc.d/rc.local实现自启动
# ========================================
function start_java_meeting_service() {
local service_dir="/data/services/api/java-meeting/java-meeting-extapi"
local run_script="$service_dir/run.sh"
local log_file="$service_dir/logs/ubains-INFO-AND-ERROR.log"
local rc_local="/etc/rc.d/rc.local"
local service_name="java-meeting-extapi"
log "INFO" "=================================================="
log "INFO" "🚀 正在启动对外服务: $service_name"
log "INFO" " 服务目录: $service_dir"
log "INFO" "=================================================="
# 1. 检查Java环境是否部署成功
log "INFO" "🔍 正在检查Java环境..."
source /etc/profile 2>/dev/null || . /etc/profile 2>/dev/null
source /etc/profile
if ! java -version >/dev/null 2>&1; then
log "ERROR" "❌ Java环境未部署或配置失败"
log "ERROR" " 请先执行 deploy_jdk_host 函数部署JDK"
log "ERROR" " 或手动执行: source /etc/profile"
return 1
fi
local java_version=$(java -version 2>&1 | head -n1)
log "INFO" "✅ Java环境检查通过: $java_version"
# 2. 检查服务目录和脚本是否存在
if [ ! -d "$service_dir" ]; then
log "ERROR" "❌ 服务目录不存在: $service_dir"
return 1
fi
if [ ! -f "$run_script" ]; then
log "ERROR" "❌ 启动脚本不存在: $run_script"
return 1
fi
# 确保脚本有执行权限
if [ ! -x "$run_script" ]; then
log "INFO" "🔧 正在为启动脚本添加执行权限..."
chmod +x "$run_script"
fi
log "INFO" "✅ 服务目录和脚本检查通过"
# 3. 检查服务是否已经在运行
cd "$service_dir" || {
log "ERROR" "❌ 无法进入服务目录: $service_dir"
return 1
}
# 尝试通过进程名或日志判断服务是否已运行
local process_count=$(pgrep -f "java-meeting-extapi\|run.sh" 2>/dev/null | wc -l)
if [ "$process_count" -gt 0 ]; then
log "WARN" "⚠️ 检测到服务可能已在运行(进程数: $process_count)"
log "INFO" " 将尝试重新启动服务"
fi
# 4. 启动服务
log "INFO" "▶️ 正在启动服务..."
# 确保日志目录存在
mkdir -p "$(dirname "$log_file")"
# 执行启动命令:./run.sh
log "INFO" "📝 执行启动命令: ./run.sh"
./run.sh > "$service_dir/nohup.out" 2>&1 &
local start_pid=$!
# 等待几秒让服务启动
log "INFO" "⏳ 等待服务启动(5秒)..."
sleep 5
# 执行tail -f查看日志(后台运行,避免阻塞)
log "INFO" "📋 正在查看服务日志: tail -f $log_file"
if [ -f "$log_file" ]; then
# 显示最后20行日志
log "INFO" "📄 最新日志内容(最后20行):"
tail -n 20 "$log_file" 2>/dev/null | while read line; do
log "INFO" " $line"
done
# 后台运行tail -f,输出到nohup
nohup tail -f "$log_file" >> "$service_dir/tail.log" 2>&1 &
log "INFO" "✅ 日志监控已启动(后台运行),实时日志输出到: $service_dir/tail.log"
else
log "WARN" "⚠️ 日志文件尚未创建: $log_file"
log "INFO" " 服务可能正在启动中,请稍后查看日志"
fi
# 5. 检查服务进程是否启动成功
log "INFO" "🔍 正在检查服务进程..."
# 检查run.sh进程
local run_pid=$(pgrep -f "run.sh" | head -n1)
if [ -n "$run_pid" ]; then
log "INFO" "✅ 启动脚本进程已运行(PID: $run_pid)"
else
log "WARN" "⚠️ 未检测到启动脚本进程,服务可能启动失败"
fi
# 检查Java进程(通过服务目录或进程名)
local java_pids=$(pgrep -f "java.*$service_dir\|java-meeting-extapi" 2>/dev/null)
if [ -n "$java_pids" ]; then
log "INFO" "✅ Java服务进程已启动"
echo "$java_pids" | while read pid; do
log "INFO" " PID: $pid"
done
else
log "WARN" "⚠️ 未检测到Java服务进程,请检查日志: $log_file"
log "WARN" " 或查看: $service_dir/nohup.out"
fi
# 检查日志文件是否存在(服务启动后会创建日志文件)
if [ -f "$log_file" ]; then
log "INFO" "✅ 日志文件已创建: $log_file"
# 显示最后几行日志
log "INFO" "📋 最新日志内容:"
tail -n 10 "$log_file" 2>/dev/null | while read line; do
log "INFO" " $line"
done
else
log "WARN" "⚠️ 日志文件尚未创建,服务可能正在启动中"
fi
# 6. 将启动命令写入/etc/rc.d/rc.local实现自启动
log "INFO" "📝 正在配置服务自启动..."
# 检查rc.local文件是否存在
if [ ! -f "$rc_local" ]; then
log "INFO" "📁 rc.local文件不存在,正在创建..."
sudo touch "$rc_local"
sudo chmod +x "$rc_local"
fi
# 构建启动命令(按照用户要求:cd目录,执行run.sh,tail -f日志)
# 注意:在rc.local中,tail -f会阻塞,所以使用后台运行
local startup_cmd="cd $service_dir && ./run.sh > $service_dir/nohup.out 2>&1 &"
local tail_cmd="tail -f $log_file >> $service_dir/tail.log 2>&1 &"
# 检查是否已经存在相同的启动命令
if grep -q "$service_dir.*run.sh" "$rc_local" 2>/dev/null; then
log "INFO" "✅ 自启动配置已存在,跳过添加"
else
# 备份rc.local
if [ -f "$rc_local" ]; then
sudo cp "$rc_local" "${rc_local}.backup.$(date +%Y%m%d_%H%M%S)" 2>/dev/null
log "INFO" "✅ 已备份rc.local文件"
fi
# 添加启动命令到rc.local
log "INFO" "📝 正在添加启动命令到 $rc_local"
echo "" | sudo tee -a "$rc_local" > /dev/null
echo "# Auto-start java-meeting-extapi service - $(date '+%Y-%m-%d %H:%M:%S')" | sudo tee -a "$rc_local" > /dev/null
echo "cd $service_dir && ./run.sh > $service_dir/nohup.out 2>&1 &" | sudo tee -a "$rc_local" > /dev/null
echo "tail -f $log_file >> $service_dir/tail.log 2>&1 &" | sudo tee -a "$rc_local" > /dev/null
# 确保rc.local有执行权限
sudo chmod +x "$rc_local" 2>/dev/null
log "INFO" "✅ 自启动配置已添加到 $rc_local"
log "INFO" " 启动命令: cd $service_dir && ./run.sh"
log "INFO" " 日志监控: tail -f $log_file"
fi
log "INFO" "=================================================="
log "INFO" "🎉 服务启动流程完成!"
log "INFO" " 服务目录: $service_dir"
log "INFO" " 日志文件: $log_file"
log "INFO" " 自启动配置: $rc_local"
log "INFO" " 提示: 可以使用 'tail -f $log_file' 查看实时日志"
log "INFO" "=================================================="
return 0
}
# ========================================
# ☕ 函数:deploy_jdk_host
# 描述:在宿主机上部署JDK(tar.gz格式)
# 功能:
# - 检查JDK安装包是否存在
# - 解压JDK到指定目录
# - 配置Java环境变量(JAVA_HOME, CLASSPATH, PATH)
# - 验证JDK安装
# 参数:
# $1: JDK安装包路径(tar.gz文件)
# $2: JDK安装目录(可选,默认为/opt/java)
# ========================================
function deploy_jdk_host() {
local jdk_tar_path="/data/temp/jdk-8u472-linux-x64.tar.gz"
local install_base_dir="${2:-/opt/java}"
log "INFO" "=================================================="
log "INFO" "☕ 正在部署宿主机JDK"
log "INFO" " JDK安装包: $jdk_tar_path"
log "INFO" " 安装目录: $install_base_dir"
log "INFO" "=================================================="
# 1. 检查JDK安装包是否存在
if [ -z "$jdk_tar_path" ]; then
log "ERROR" "❌ 未指定JDK安装包路径"
log "INFO" "用法: deploy_jdk_host <jdk_tar.gz路径> [安装目录]"
return 1
fi
if [ ! -f "$jdk_tar_path" ]; then
log "ERROR" "❌ JDK安装包不存在: $jdk_tar_path"
return 1
fi
# 检查是否为tar.gz格式
if [[ ! "$jdk_tar_path" =~ \.tar\.gz$ ]]; then
log "ERROR" "❌ 文件格式错误,需要tar.gz格式: $jdk_tar_path"
return 1
fi
log "INFO" "✅ JDK安装包检查通过"
# 2. 检查是否已安装JDK,并进行备份
local old_java_home=""
local old_java_version=""
local backup_dir="/opt/java_backup_$(date +%Y%m%d_%H%M%S)"
if command -v java &> /dev/null; then
old_java_version=$(java -version 2>&1 | head -n1)
log "INFO" "📋 检测到系统已安装Java: $old_java_version"
# 获取旧的JAVA_HOME
if [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ]; then
old_java_home="$JAVA_HOME"
else
# 尝试从java命令路径推断JAVA_HOME
local java_path=$(which java 2>/dev/null)
if [ -n "$java_path" ]; then
java_path=$(readlink -f "$java_path" 2>/dev/null || echo "$java_path")
# 移除/bin/java部分
old_java_home=$(dirname "$(dirname "$java_path")" 2>/dev/null)
fi
fi
# 备份旧的JDK目录
if [ -n "$old_java_home" ] && [ -d "$old_java_home" ] && [ "$old_java_home" != "$install_base_dir" ]; then
log "INFO" "💾 正在备份旧JDK目录: $old_java_home"
mkdir -p "$backup_dir"
if [ $? -eq 0 ]; then
cp -r "$old_java_home" "$backup_dir/old_jdk_$(basename "$old_java_home")" 2>/dev/null
if [ $? -eq 0 ]; then
log "INFO" "✅ 旧JDK目录已备份到: $backup_dir"
else
log "WARN" "⚠️ 旧JDK目录备份失败,继续安装"
fi
fi
fi
# 备份/etc/profile中的Java配置
log "INFO" "💾 正在备份/etc/profile中的Java配置..."
mkdir -p "$backup_dir"
if [ -f /etc/profile ]; then
cp /etc/profile "$backup_dir/profile.backup" 2>/dev/null
if [ $? -eq 0 ]; then
log "INFO" "✅ /etc/profile已备份到: $backup_dir/profile.backup"
fi
fi
# 记录旧Java信息到备份目录
if [ -d "$backup_dir" ]; then
{
echo "备份时间: $(date '+%Y-%m-%d %H:%M:%S')"
echo "旧Java版本: $old_java_version"
echo "旧JAVA_HOME: $old_java_home"
} > "$backup_dir/old_java_info.txt" 2>/dev/null
fi
log "INFO" "🔄 将自动更新Java环境..."
fi
# 3. 创建安装目录
mkdir -p "$install_base_dir"
if [ $? -ne 0 ]; then
log "ERROR" "❌ 无法创建安装目录: $install_base_dir"
return 1
fi
log "INFO" "✅ 创建安装目录: $install_base_dir"
# 4. 解压JDK
log "INFO" "🗜️ 正在解压JDK安装包..."
cd "$install_base_dir" || {
log "ERROR" "❌ 无法进入目录: $install_base_dir"
return 1
}
# 解压到当前目录
tar -xzf "$jdk_tar_path" -C "$install_base_dir"
if [ $? -ne 0 ]; then
log "ERROR" "❌ JDK解压失败,请检查压缩包完整性"
return 1
fi
log "INFO" "✅ JDK解压完成"
# 5. 查找解压后的JDK目录(通常是jdk8u472-b08或类似格式)
local jdk_dir=$(find "$install_base_dir" -maxdepth 1 -type d -name "jdk*" | head -n1)
if [ -z "$jdk_dir" ]; then
log "ERROR" "❌ 未找到JDK目录,解压后的目录结构可能不正确"
log "INFO" "请检查解压后的目录结构"
return 1
fi
local jdk_home="$jdk_dir"
local jdk_name=$(basename "$jdk_dir")
log "INFO" "✅ 找到JDK目录: $jdk_name"
log "INFO" " JDK_HOME: $jdk_home"
# 6. 检查JDK目录结构是否完整
if [ ! -d "$jdk_home/bin" ] || [ ! -f "$jdk_home/bin/java" ]; then
log "ERROR" "❌ JDK目录结构不完整,缺少bin/java文件"
return 1
fi
log "INFO" "✅ JDK目录结构检查通过"
# 7. 配置Java环境变量
log "INFO" "🛠️ 正在配置Java环境变量..."
# 检查/etc/profile中是否已存在JAVA_HOME配置,并备份后注释掉
if grep -q "JAVA_HOME\|#set java environment" /etc/profile; then
log "INFO" "📋 检测到/etc/profile中已存在JAVA_HOME配置,将注释旧配置并添加新配置"
# 备份旧的配置(如果之前没有备份)
if [ ! -f "$backup_dir/profile.backup" ]; then
mkdir -p "$backup_dir"
cp /etc/profile "$backup_dir/profile.backup" 2>/dev/null
log "INFO" "✅ /etc/profile已备份到: $backup_dir/profile.backup"
fi
# 使用awk来注释掉Java配置块(更可靠,处理多行块)
awk '
BEGIN { in_java_block = 0 }
/#set java environment/ {
in_java_block = 1
# 如果这行还没被注释,就注释掉
if ($0 !~ /^[[:space:]]*#/) {
print "#" $0
} else {
print $0
}
next
}
in_java_block {
# 如果是Java相关的配置行(JAVA_HOME, CLASSPATH, PATH, export等)
if ($0 ~ /JAVA_HOME|CLASSPATH|PATH.*JAVA_HOME|export.*JAVA_HOME|export.*CLASSPATH|export.*PATH/) {
# 如果这行还没被注释,就注释掉
if ($0 !~ /^[[:space:]]*#/ && $0 !~ /^[[:space:]]*$/) {
print "#" $0
} else {
print $0
}
next
}
# 如果是空行,保持块状态,继续
if ($0 ~ /^[[:space:]]*$/) {
print $0
next
}
# 其他非空行,结束Java块
in_java_block = 0
print $0
next
}
{ print $0 }
' /etc/profile > /etc/profile.tmp && mv /etc/profile.tmp /etc/profile
if [ $? -eq 0 ]; then
log "INFO" "✅ 旧Java配置已注释"
else
log "WARN" "⚠️ 注释旧配置时出现问题,继续安装"
fi
fi
# 添加新的Java环境变量配置
cat >> /etc/profile << EOF
#set java environment
JAVA_HOME=$jdk_home
CLASSPATH=.:\$JAVA_HOME/lib/tools.jar
PATH=\$JAVA_HOME/bin:\$PATH
export JAVA_HOME CLASSPATH PATH
EOF
log "INFO" "✅ Java环境变量已写入 /etc/profile"
# 8. 立即在当前会话中生效
log "INFO" "🔄 正在重新加载 /etc/profile 使环境变量生效..."
source /etc/profile 2>/dev/null || . /etc/profile 2>/dev/null
source /etc/profile
# 如果source失败,则手动设置环境变量
if [ $? -ne 0 ]; then
log "WARN" "⚠️ source /etc/profile 失败,使用export设置环境变量"
export JAVA_HOME="$jdk_home"
export CLASSPATH=".:$JAVA_HOME/lib/tools.jar"
export PATH="$JAVA_HOME/bin:$PATH"
fi
log "INFO" "✅ 环境变量已在当前会话中生效"
# 9. 验证Java安装
log "INFO" "🔍 正在验证Java安装..."
if java -version >/dev/null 2>&1; then
local java_version=$(java -version 2>&1 | head -n1)
local java_path=$(which java)
log "INFO" "✅ Java环境配置成功!"
log "INFO" " Java版本: $java_version"
log "INFO" " Java路径: $java_path"
log "INFO" " JAVA_HOME: $JAVA_HOME"
else
log "ERROR" "❌ Java环境配置失败,请检查路径或权限"
log "INFO" " 请手动执行: source /etc/profile"
return 1
fi
log "INFO" "=================================================="
log "INFO" "🎉 JDK部署成功!"
log "INFO" " 安装路径: $jdk_home"
log "INFO" " 环境变量已配置到 /etc/profile"
log "INFO" " 提示: 新开终端或执行 'source /etc/profile' 使环境变量生效"
log "INFO" "=================================================="
return 0
}
#------------------------------服务安装-end------------------------------------------------------------------------------------------------------------------------
#------------------------------全局配置-start--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
###服务器检测配置项
#mem-check
#最小内存大小G
min_memory_value=8
#最小有效内存大小G
min_memory_available=8
#disk-check
#最小硬盘资源大小G
#min_disk_value=200
#最小有效硬盘大小G
min_disk_available=50
#默认为空,如果为非root用户执行,则需要配置为sudo
sudoset=""
#------------------------------全局配置-end--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
#-------------------------------脚本执行-start--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
sys1="ubains.txt"
sys2="devops.txt"
sys3="ubains_devops.txt"
checkOS
if [ -e "$PWD/$sys1" ]; then
txt="检测该脚本为【预定系统 部署】"
version="预定系统版本"
elif [ -e "$PWD/$sys2" ]; then
txt="检测该脚本为【运维系统 部署】"
version="运维系统版本"
elif [ -e "$PWD/$sys3" ]; then
txt="检测该脚本为【预定系统+运维系统 部署】"
version="预定系统+运维系统版本"
fi
#-------------------------------获取服务器ip-start--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
#文件默认ip
ip='192.168.9.84'
IP=$(hostname -I | awk '{print $1}')
function server_ip()
{
# 弹出弹框,并根据用户选择执行相应操作
choice=$(whiptail --title "请选择操作" \
--menu "请确认服务器ip是否正确:$IP" 15 50 4 \
1 "是" \
2 "否" \
3 "退出" \
3>&1 1>&2 2>&3)
# 根据用户选择执行相应操作
case $choice in
1)
# 在这里编写操作1的代码逻辑
;;
2)
# 设置对话框的标题和提示信息
dialog_title="请输入服务器IP:"
dialog_prompt="请输入服务器IP:"
# 使用whiptail显示输入框,并将用户输入的值保存到变量input_value
IP=$(whiptail --title "$dialog_title" --inputbox "$dialog_prompt" 10 50 3>&1 1>&2 2>&3)
# 在这里编写操作2的代码逻辑
;;
3)
exit
# 在这里编写操作3的代码逻辑
;;
*)
echo "无效的选择"
;;
esac
}
#-------------------------------获取服务器的ip-end-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
function getLatestVersion() {
log "INFO" "检查中间件最新版本信息"
#判断是否能够访问外网,不能就跳过联网检查
if ! curl -s --head "https://www.baidu.com" | grep "200 OK" > /dev/null; then
log "WARN" "无法访问外网,跳过最新版本检查"
return
fi
#todo:如果访问不到,就跳过,后面改成用自己的接口服务
log "INFO" "准备获取最新版本信息"
latest_nginx_version=$(curl -s "https://nginx.org/en/download.html" | grep -oP 'nginx-\K[0-9]+\.[0-9]+\.[0-9]+' | head -n 1)
latest_redis_version=$(curl -s "https://api.github.com/repos/redis/redis/releases/latest" | \
grep -m 1 '"tag_name"' | \
grep -oP '[0-9]+\.[0-9]+\.[0-9]+' )
latest_mysql_version=$(curl -s https://repo.mysql.com/yum/mysql-8.0-community/el/7/x86_64/ \
| grep -oP 'mysql-community-server-[0-9]+\.[0-9]+\.[0-9]+' \
| sort -V \
| tail -n 1 \
| grep -oP '[0-9]+\.[0-9]+\.[0-9]+'
)
log "INFO" "最新版本信息获取成功"
log "INFO" "准备获取本地版本信息"
current_nginx_version=$(docker exec ujava2 sh -c "/usr/local/nginx/sbin/nginx -v " 2>&1 | grep -oP 'nginx/\K[0-9\.]+')
current_redis_version=$(docker exec uredis sh -c "redis-server --version " 2>&1 | grep -oP 'Redis server v=\K[0-9\.]+')
current_mysql_version=$(docker exec umysql sh -c "mysql --version " 2>&1 | grep -oP 'mysql Ver \K[0-9\.]+')
log "INFO" "本地版本信息获取成功, 准备进行版本比较"
#比较不同的版本情况,不一样的给出告警提示
if [ "$latest_nginx_version" != "$current_nginx_version" ]; then
log "WARN" "Nginx版本不一致,最新版本: $latest_nginx_version, 当前版本: $current_nginx_version"
else
log "INFO" "Nginx版本一致,当前版本: $current_nginx_version"
fi
if [ "$latest_redis_version" != "$current_redis_version" ]; then
log "WARN" "Redis版本不一致,最新版本: $latest_redis_version, 当前版本: $current_redis_version"
else
log "INFO" "Redis版本一致,当前版本: $current_redis_version"
fi
if [ "$latest_mysql_version" != "$current_mysql_version" ]; then
log "WARN" "MySQL版本不一致,最新版本: $latest_mysql_version, 当前版本: $current_mysql_version"
else
log "INFO" "MySQL版本一致,当前版本: $current_mysql_version"
fi
log "INFO" "版本检查完成"
}
#-------------------------------部署系统-------------------------------
log "INFO" "\033[36m
---------------------------------------------开始部署系统---------------------------------------------------
\033[0m"
server_ip
server_ip="$IP"
#自动化脚本路径
auto_java=$PWD
add_user
# 文件检查与部署
check_files_upload
#开放防火墙
firewalldjava
# 部署中间件服务(mysql, redis, emqx, fdfs, ngrok, nacos, nginx)
middleware_type="mysql redis emqx fdfs ngrok nginx" install_middleware
ntp_uos
deploy_jdk_host
start_java_meeting_service
malan
deploy_services
add_crontab_job
#检查中间件版本
getLatestVersion
#___________________________________________________________________________________
log "INFO" "正在重启docker服务,请耐心等待。"
$sudoset systemctl restart docker
sleep 30
log "INFO" "查看服务是否启动成功——————docker ps"
log "INFO" "系统服务:umysql,uredis,ujava2,uemqx,ustorage,utracker "
$sudoset docker ps
\ No newline at end of file
#!/bin/bash
#------------------------------工具类模块---------------------------------------------------
# 日志打印函数
LOG_FILE="/data/log/new_auto_script.log"
mkdir -p "$(dirname "$LOG_FILE")" 2>/dev/null
# 用户输入的东西也同步记录到日志信息里面
function log() {
# 获取当前时间
local timestamp=$(date "+%Y-%m-%d %H:%M:%S")
# 日志级别对应的颜色代码
local INFO_COLOR="\033[32m" # 绿色
local WARN_COLOR="\033[33m" # 黄色
local ERROR_COLOR="\033[31m" # 红色
local DEBUG_COLOR="\033[36m" # 青色
local RESET_COLOR="\033[0m" # 重置颜色
# 参数判断
if [ $# -lt 2 ]; then
echo "Usage: log <level> <message>"
return 1
fi
local level=$1
local message=$2
# 根据日志级别选择颜色
case $level in
"INFO")
color=$INFO_COLOR
;;
"WARN")
color=$WARN_COLOR
;;
"ERROR")
color=$ERROR_COLOR
;;
"DEBUG")
color=$DEBUG_COLOR
;;
*)
echo "Invalid log level: $level"
return 1
;;
esac
# 输出带颜色的日志到屏幕
echo -e "${color}[$timestamp] [$level] $message${RESET_COLOR}"
# 追加纯文本日志到文件
echo "[$timestamp] [$level] $message" >> "$LOG_FILE"
}
#------------------------------导入中间件部署模块---------------------------------------------------
# 获取脚本所在目录
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# 导入中间件部署脚本
if [[ -f "$SCRIPT_DIR/auto_middleware_install.sh" ]]; then
source "$SCRIPT_DIR/auto_middleware_install.sh"
else
log "ERROR" "中间件部署脚本不存在: $SCRIPT_DIR/auto_middleware_install.sh"
exit 1
fi
# 导入文件检查脚本
if [[ -f "$SCRIPT_DIR/auto_file_upload_check.sh" ]]; then
source "$SCRIPT_DIR/auto_file_upload_check.sh"
else
log "ERROR" "文件检查脚本不存在: $SCRIPT_DIR/auto_file_upload_check.sh"
exit 1
fi
# 导入定时任务设置脚本
if [[ -f "$SCRIPT_DIR/auto_crontab_settings.sh" ]]; then
source "$SCRIPT_DIR/auto_crontab_settings.sh"
else
log "ERROR" "定时任务设置脚本不存在: $SCRIPT_DIR/auto_crontab_settings.sh"
exit 1
fi
# 导入主服务部署脚本
if [[ -f "$SCRIPT_DIR/auto_deploy_services.sh" ]]; then
source "$SCRIPT_DIR/auto_deploy_services.sh"
else
log "ERROR" "主服务部署脚本不存在: $SCRIPT_DIR/auto_deploy_services.sh"
exit 1
fi
# 导入防火墙设置脚本
if [[ -f "$SCRIPT_DIR/auto_firewall_settings.sh" ]]; then
source "$SCRIPT_DIR/auto_firewall_settings.sh"
else
log "ERROR" "防火墙设置脚本不存在: $SCRIPT_DIR/auto_firewall_settings.sh"
exit 1
fi
#------------------------------检测模块-----------------------------------------------------
#------------------------------服务器检测-start---------------------------------------------
function detectNetType() {
log "INFO" "准备获取所有网口信息,排除 loopback 和虚拟接口"
INTERFACES=$(ls /sys/class/net | grep -vE 'lo|docker|br-|veth|virbr|vmnet')
# 遍历每个网口,检查是否配置了 IP 地址,并输出网口信息
log "INFO" "检测到的网口信息如下:"
for iface in $INTERFACES; do
log "INFO" "-----------------------------"
log "INFO" "接口: $iface"
log "INFO" " 检查接口 $iface 是否配置了 IP 地址"
IP_ADDR=$(ip addr show "$iface" | grep "inet " | awk '{print $2}')
if [[ -n "$IP_ADDR" ]]; then
log "INFO" " 配置的 IP 地址: $IP_ADDR"
else
log "INFO" " 没有配置 IP 地址"
fi
# 获取接口的连接状态(carrier)
log "INFO" " 检查接口 $iface 的连接状态"
if [[ -f /sys/class/net/$iface/carrier ]]; then
CARRIER=$(cat /sys/class/net/$iface/carrier)
[[ "$CARRIER" == "1" ]] && LINK="connected" || LINK="disconnected"
else
LINK="unknown"
fi
log "INFO" " 状态: $LINK"
# 使用 ethtool 获取接口的类型(光口、电口等)
log "INFO" " 获取接口 $iface 的类型"
ETHTOOL_OUTPUT=$(ethtool "$iface" 2>/dev/null)
TYPE=$(echo "$ETHTOOL_OUTPUT" | grep -i "Port:" | awk '{print tolower($2)}')
[[ -z "$TYPE" ]] && TYPE="unknown" && TYPE="other"
# 获取速率和驱动信息
log "INFO" " 获取接口 $iface 的速率和驱动信息"
SPEED=$(echo "$ETHTOOL_OUTPUT" | grep -i "Speed:" | awk '{print $2}')
[[ -z "$SPEED" ]] && SPEED="unknown"
DRIVER=$(ethtool -i "$iface" 2>/dev/null | grep "driver:" | awk '{print $2}')
[[ -z "$DRIVER" ]] && DRIVER="unknown"
# 获取 MAC 地址
log "INFO" " 获取接口 $iface 的 MAC 地址"
MAC=$(cat /sys/class/net/$iface/address)
# 转换 TYPE 为中文描述
case "$TYPE" in
"copper")
TYPE="电口"
;;
"fiber")
TYPE="光口"
;;
"other")
TYPE="其他类型"
;;
*)
TYPE="未知类型"
;;
esac
# 输出接口信息
log "INFO" " 光电口类型: ${TYPE:-unknown}"
log "INFO" " 速率: $SPEED"
log "INFO" " MAC 地址: $MAC"
log "INFO" " 驱动: $DRIVER"
done
}
#------------------------------服务器空间检测-start---------------------------------------------
# 功能说明:检查服务器空间是否满足部署要求
# 调用方式:checkServerSpace
# 返回值:无,直接exit或继续执行
function checkServerSpace() {
log "INFO" "开始检查服务器空间分配"
# 判断当前服务器类型
if [ -d "/data" ]; then
system_type="new_type"
log "INFO" "检测到/data目录,系统类型为:新统一平台"
else
system_type="old_type"
log "INFO" "未检测到/data目录,系统类型为:传统平台"
fi
# 获取脚本所在目录
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# 调用空间检查脚本
log "INFO" "执行空间检查脚本..."
system_type=$system_type "$SCRIPT_DIR/auto_check_space.sh"
return_code=$?
# 根据返回值处理
case $return_code in
0)
log "INFO" "服务器空间检查通过,继续执行部署"
;;
1)
log "ERROR" "服务器空间不足,停止部署"
exit 1
;;
2)
log "ERROR" "服务器未正确分区,停止部署"
exit 1
;;
*)
log "ERROR" "空间检查返回异常值: $return_code,停止部署"
exit 1
;;
esac
log "INFO" "服务器空间检查完成"
}
#------------------------------服务器空间检测-end-----------------------------------------------
function checkOS() {
log "INFO" "开始检测服务器环境"
log "INFO" "**************《请检查当前系统是否适配》**************************************************"
# 检测当前的操作系统是否为x86架构
log "INFO" "开始检查当前操作系统架构"
if [ "$(uname -m)" != "x86_64" ]; then
log "ERROR" "当前操作系统不是x86架构,请使用x86架构的操作系统"
exit 1
else
log "INFO" "当前操作系统为x86架构,继续执行脚本"
fi
server_available_mem=`free -g |awk 'NR==2' | awk '{print$7}'`
server_max_mem=`free -g|awk 'NR==2' |awk '{print$2}'`
log "INFO" "当前系统最大内存为:${server_max_mem}GB 可用内存为:${server_available_mem}GB"
#判断服务器的最大内存值小于约定的内存大小min_memory_value=15,就返回。否则成功。
log "INFO" "开始检查服务器最大内存"
if [ $server_max_mem -lt $min_memory_value ];then
log "WARN" "当前系统最大内存为:${server_max_mem}G,建议将内存至少添加为: ${min_memory_value}G"
# 需要用户确认信息,支持默认空格和y或Y,并日志记录用户输入情况
read -p "是否继续执行脚本?(y/n): " yn
if [[ -z "$yn" || "$yn" =~ ^[yY]$ ]]; then
# 记录用户如情况
log "INFO" "用户选择继续执行脚本,输入为${yn} "
else
log "ERROR" "脚本执行终止,请调整服务器内存后重新运行脚本"
exit 1
fi
else
log "INFO" "检查当前系统最大内存为:${server_max_mem}G 满足系统运行!"
fi
#todo:考虑把交换关闭掉
#判断服务器的内存值小于约定的内存大小min_memory_available=10,就返回。否则成功。
log "INFO" "开始检查服务器可用内存"
if [ $server_available_mem -lt $min_memory_available ];then
log "WARN" "当前系统docker可用内存为:${server_available_mem}G,建议将内存至少添加为: ${min_memory_available}G !"
# 需要用户确认信息,支持默认空格和y或Y,并日志记录用户输入情况
read -p "是否继续执行脚本?(y/n): " yn
if [[ -z "$yn" || "$yn" =~ ^[yY]$ ]]; then
# 记录用户如情况
log "INFO" "用户选择继续执行脚本,输入为${yn} "
else
log "ERROR" "脚本执行终止,请调整服务器内存后重新运行脚本"
exit 1
fi
else
log "INFO" "检查当前系统docker可用内存为:${server_available_mem}G 满足系统运行!"
fi
#判断服务的根分区的硬盘大小是否超过预设的硬盘资源。成功继续,否则退出。
log "INFO" "开始检查服务器硬盘空间"
server_disk_available=`df -B G /|awk '/\//{print$4}' | awk '{sub(/.{1}$/,"")}1'`
if [ $server_disk_available -lt $min_disk_available ];then
log "WARN" "当前系统的硬盘空间为:${server_disk_available}G,建议将硬盘空间至少添加为: ${min_disk_available}G!"
# 需要用户确认信息,支持默认空格和y或Y,并日志记录用户输入情况
read -p "是否继续执行脚本?(y/n): " yn
if [[ -z "$yn" || "$yn" =~ ^[yY]$ ]]; then
# 记录用户如情况
log "INFO" "用户选择继续执行脚本,输入为${yn} "
else
log "ERROR" "脚本执行终止,请调整服务器磁盘后重新运行脚本"
exit 1
fi
else
log "INFO" "检查当前系统硬盘空间为:${server_disk_available}G,满足系统运行!"
fi
# 检测当前用户是否有root权限
log "INFO" "开始检查当前用户权限"
if [ "$(id -u)" -ne 0 ]; then
log "ERROR" "当前用户没有root权限,请使用root用户执行脚本,获者使用sudo命令执行脚本"
exit 1
else
log "INFO" "当前用户有root权限,继续执行脚本"
fi
log "INFO" "当前用户权限检查完成"
# 检测当前服务器是否存在多张网卡并且ipv4不唯一的需要列出来
log "INFO" "开始检查服务器网卡信息"
network_interfaces=$(ip -o -4 addr show | awk '{print $2}' | sort -u)
if [ -z "$network_interfaces" ]; then
log "ERROR" "当前服务器没有检测到任何网卡,请检查网络配置"
exit 1
else
log "INFO" "当前服务器检测到的网卡信息如下:$network_interfaces"
# 获取对应的IP信息
for interface in $network_interfaces; do
ip_address=$(ip -o -4 addr show "$interface" | awk '{print $4}' | cut -d/ -f1)
if [ -n "$ip_address" ]; then
log "INFO" "网卡 $interface 的 IP 地址为: $ip_address"
IP=$ip_address
else
log "WARN" "网卡 $interface 没有配置 IPv4 地址"
fi
done
fi
log "INFO" "服务器网卡信息检查完成"
log "INFO" "开始检测服务器网口类型"
detectNetType
log "INFO" "服务器网口类型检测完成"
# 服务器空间检查
checkServerSpace
# 用户确认以后继续往下
read -p "请确认网口信息是否正确,特别注意需要仔细观察你的项目是要光口还是电口 ,输入 y 或 Y 确认,输入 n 或 N 退出脚本: " yn
if [[ -z "$yn" || "$yn" =~ ^[yY]$ ]]; then
log "INFO" "用户确认网口信息正确,继续执行脚本"
else
log "ERROR" "用户确认网口信息不正确,脚本执行终止"
exit 1
fi
echo -e "\033[36m
*******************************************************
* 可适配的操作系统(x86) *
* *
* cento7.5 *
* cento7.6 *
* cento7.9 *
* 统信系统 *
* 鲲鹏系统 *
* Ubuntu *
* redhat7 *
* 银河麒麟V10 *
* openEuler 24.03 *
* *
*******************************************************
\033[0m"
echo -e "\033[36m\n检测到当前操作系统如下,请确认是否适配:\n \033[0m"
# 优先获取PRETTY_NAME,没有则拼接NAME和VERSION
log "INFO" "开始获取当前操作系统版本信息"
if grep -q '^PRETTY_NAME=' /etc/os-release; then
os_version=$(grep '^PRETTY_NAME=' /etc/os-release | cut -d= -f2 | tr -d '"')
else
name=$(grep '^NAME=' /etc/os-release | cut -d= -f2 | tr -d '"')
version=$(grep '^VERSION=' /etc/os-release | cut -d= -f2 | tr -d '"')
os_version="$name $version"
fi
log "INFO" "当前操作系统版本为: $os_version"
case "$os_version" in
*"CentOS Linux 7"*|*"UOS"*|*"Kylin V10"*|*"Ubuntu"*|*"Red Hat Enterprise Linux Server 7"*|*"openEuler 24.03"*)
log "INFO" "当前操作系统版本适配,继续执行脚本" ;;
*)
log "ERROR" "当前操作系统版本不适配,请使用CentOS 7、UOS、麒麟V10、Ubuntu或Red Hat Enterprise Linux Server 7 openEuler 24.03"
# 需要用户确认信息,支持默认空格和y或Y,并日志记录用户输入情况
read -p "是否继续执行脚本?(y/n): " yn
if [[ -z "$yn" || "$yn" =~ ^[yY]$ ]]; then
# 记录用户如情况
log "INFO" "用户选择继续执行脚本,输入为${yn} "
else
log "ERROR" "脚本执行终止,请调整服务器操作系统后重新运行脚本"
exit 1
fi;;
esac
log "INFO" "当前操作系统版本适配,继续执行脚本"
#检查服务器时间和时区
log "INFO" "开始检查服务器时间和时区"
log "INFO" "**************《请检查当前时间,时区是否正确》*****************************************************************"
log "INFO" "当前服务器时区、时间为: $(date)"
read -p "确认无误请按 y 或 Y 或直接回车,不正确请按 n 修改时间: " yn
if [[ -z "$yn" || "$yn" =~ ^[yY]$ ]]; then
log "INFO" "下面进入系统部署。。。"
sleep 3
else
read -p "输入当前日期,格式如2021/02/06——" da
echo "="
date -s $da
read -p "输入当前时间,格式如18:00:00——" sa
echo "="
date -s $sa
timedatectl set-timezone Asia/Shanghai
clock -w
echo "==================================================="
echo "查看修改后的时间"
date
fi
}
log "INFO" "服务器检测完成"
#创建用户(预定安装需要:该用户与系统启动用户有关)
function add_user()
{
log "INFO" "创建用户user"
user=user
group=user
log "INFO" "判断是否存在用户$user"
egrep "^$user" /etc/passwd >& /dev/null
if [ $? -ne 0 ]; then
log "INFO" "用户$user不存在,开始创建用户"
username="user"
password="Ubains@135"
useradd -m "$username"
echo "$username:$password" | chpasswd
log "INFO" "用户$user创建成功"
else
log "INFO" "用户user已创建! "
fi
}
#x86架构ntp安装
function ntp_x86()
{
# 判断是否为centos7 否则跳出函数
if [ ! -f /etc/redhat-release ]; then
log "ERROR" "当前系统不是CentOS 7,无法安装NTP服务"
return 1
fi
log "INFO" "检查ntp服务......"
$sudoset systemctl status ntpd |grep running
#ps -aux | grep ntp
if [ $? -eq 0 ]; then
log "INFO" "检查到ntp已安装!"
else
log "WARN" "检查到ntp未安装,正在安装..."
$sudoset cd $auto_java/server_bag/ntp
$sudoset rpm -Uvh *.rpm --nodeps --force
$sudoset systemctl enable ntpd
$sudoset systemctl enable ntpdate
$sudoset systemctl start ntpd
$sudoset systemctl status ntpd
$sudoset mv /etc/ntp.conf /etc/ntp.confbak
$sudoset cp $auto_java/server_bag/ntp/ntp.conf /etc/
$sudoset systemctl restart ntpd
$sudoset systemctl disable chronyd
log "INFO" "完成ntp服务的安装"
fi
}
#x86架构统信、麒麟系统的ntp安装
function ntp_uos() {
# 判断如果是centos7就退出安装
if [ -f /etc/redhat-release ]; then
log "ERROR" "当前系统是CentOS 7,已经安装NTP服务,无需安装chrony服务"
return 1
fi
# 判断文件是否存在ntp1.aliyun.com地址
if grep -q "ntp1.aliyun.com" /etc/chrony.conf; then
log "INFO" "NTP配置文件中已存在ntp1.aliyun.com地址,无需重复添加"
return 0
fi
local config_dir="/data/temp/ntp"
local backup="/etc/chrony.confbak"
local target="/etc/chrony.conf"
# 切换目录(不需要 sudo)
cd "$config_dir" || { echo "无法进入目录: $config_dir"; return 1; }
# 备份原有配置
if [ -f "$target" ]; then
log "INFO" "正在备份旧配置文件到 $backup"
sudo cp "$target" "$backup"
fi
# 检查是否可以通外网,可以则配置外网的ntp服务器地址,不可以则询问是否有企业ntp服务器地址
if ! ping -c 1 ntp1.aliyun.com &> /dev/null; then
log "ERROR" "无法连接到外网NTP服务器 ntp1.aliyun.com,请检查网络连接"
read -p "是否有企业NTP服务器地址?(y/n): " yn
if [[ -z "$yn" || "$yn" =~ ^[yY]$ ]]; then
read -p "请输入企业NTP服务器地址: " custom_ntp
echo "server $custom_ntp iburst" | sudo tee -a "$target"
echo "allow all" | sudo tee -a "$target"
else
log "ERROR" "没有可用的NTP服务器地址,配置默认地址,请联系管理员"
echo "server ntp1.aliyun.com iburst" | sudo tee -a "$target"
echo "allow all" | sudo tee -a "$target"
fi
else
log "INFO" "可以连接到外网NTP服务器 ntp1.aliyun.com,继续配置"
# 如果可以连接到外网NTP服务器,则继续添加默认的NTP服务器地址
echo "server ntp1.aliyun.com iburst" | sudo tee -a "$target"
echo "server ntp2.aliyun.com iburst" | sudo tee -a "$target"
echo "server ntp3.aliyun.com iburst" | sudo tee -a "$target"
echo "allow all" | sudo tee -a "$target"
fi
# 输出同步地址到日志
log "INFO" "正在写入同步地址到 $target"
# 重启服务
log "INFO" "正在重启 chronyd 服务..."
sudo systemctl daemon-reload
sudo systemctl restart chronyd
sudo systemctl enable chronyd
sudo systemctl status chronyd
log "INFO" "chronyd 服务已重启并设置为开机自启"
}
# ========================================
# 🔧 函数:malan
# 描述:配置 malan 服务自启动(写入 /etc/rc.local)
# 功能:
# - 检查 /etc/rc.local 文件是否存在
# - 检查是否已存在 malan 启动命令(避免重复添加)
# - 将启动命令写入 /etc/rc.local
# - 确保文件有执行权限
# ========================================
function malan() {
local rc_local="/etc/rc.local"
local service_dir="/data/middleware/monitor"
local startup_cmd="cd /data/middleware/monitor/ && nohup ./malan &"
log "INFO" "=================================================="
log "INFO" "🔧 正在配置 malan 服务自启动"
log "INFO" " 服务目录: $service_dir"
log "INFO" "=================================================="
# 1. 检查服务目录是否存在
if [ ! -d "$service_dir" ]; then
log "WARN" "⚠️ 服务目录不存在: $service_dir"
log "WARN" " 将尝试创建目录..."
mkdir -p "$service_dir" 2>/dev/null || {
log "ERROR" "❌ 无法创建目录: $service_dir"
return 1
}
log "INFO" "✅ 目录已创建: $service_dir"
fi
# 2. 检查 malan 可执行文件是否存在
if [ ! -f "$service_dir/malan" ]; then
log "WARN" "⚠️ malan 可执行文件不存在: $service_dir/malan"
log "WARN" " 将继续配置自启动,但请确保文件已部署"
else
# 确保文件有执行权限
if [ ! -x "$service_dir/malan" ]; then
log "INFO" "🔧 正在为 malan 添加执行权限..."
chmod +x "$service_dir/malan" 2>/dev/null || {
log "WARN" "⚠️ 无法添加执行权限,可能需要 root 权限"
}
fi
log "INFO" "✅ malan 文件检查通过"
fi
# 3. 检查 /etc/rc.local 文件是否存在
if [ ! -f "$rc_local" ]; then
log "INFO" "📁 rc.local 文件不存在,正在创建..."
touch "$rc_local" 2>/dev/null || {
log "ERROR" "❌ 无法创建 $rc_local,请检查权限"
return 1
}
# 添加 shebang 行(如果文件为空)
echo "#!/bin/bash" > "$rc_local"
log "INFO" "✅ rc.local 文件已创建"
fi
# 确保 rc.local 有执行权限
if [ ! -x "$rc_local" ]; then
log "INFO" "🔧 正在为 rc.local 添加执行权限..."
chmod +x "$rc_local" 2>/dev/null || {
log "WARN" "⚠️ 无法添加执行权限,可能需要 root 权限"
}
fi
# 4. 检查是否已经存在相同的启动命令
if grep -Fq "cd /data/middleware/monitor/ && nohup ./malan &" "$rc_local" 2>/dev/null; then
log "INFO" "✅ malan 自启动配置已存在,跳过添加"
return 0
fi
# 5. 备份 rc.local
if [ -f "$rc_local" ]; then
local backup_file="${rc_local}.backup.$(date +%Y%m%d_%H%M%S)"
cp "$rc_local" "$backup_file" 2>/dev/null
if [ $? -eq 0 ]; then
log "INFO" "✅ 已备份 rc.local 文件: $backup_file"
else
log "WARN" "⚠️ 备份 rc.local 失败,继续执行"
fi
fi
# 6. 添加启动命令到 rc.local
log "INFO" "📝 正在添加 malan 启动命令到 $rc_local"
# 确保文件末尾有换行符
if [ -s "$rc_local" ] && [ "$(tail -c 1 "$rc_local")" != "" ]; then
echo "" >> "$rc_local"
fi
# 添加注释和启动命令
echo "# Auto-start malan service - $(date '+%Y-%m-%d %H:%M:%S')" >> "$rc_local"
echo "$startup_cmd" >> "$rc_local"
if [ $? -eq 0 ]; then
log "INFO" "✅ malan 自启动配置已添加到 $rc_local"
log "INFO" " 启动命令: $startup_cmd"
else
log "ERROR" "❌ 写入 $rc_local 失败,请检查权限"
return 1
fi
# 7. 验证写入的内容
if grep -Fq "cd /data/middleware/monitor/ && nohup ./malan &" "$rc_local" 2>/dev/null; then
log "INFO" "✅ 配置验证成功"
else
log "WARN" "⚠️ 配置验证失败,请手动检查 $rc_local"
fi
log "INFO" "=================================================="
log "INFO" "🎉 malan 自启动配置完成!"
log "INFO" " 配置文件: $rc_local"
log "INFO" " 提示: 系统重启后 malan 服务将自动启动"
log "INFO" "=================================================="
return 0
}
# ========================================
# 🚀 函数:start_java_meeting_service
# 描述:启动对外服务并将服务写入服务器自启动
# 功能:
# - 检查Java环境是否部署成功
# - 启动java-meeting-extapi服务
# - 检查服务进程是否启动成功
# - 将启动命令写入/etc/rc.d/rc.local实现自启动
# ========================================
function start_java_meeting_service() {
local service_dir="/data/services/api/java-meeting/java-meeting-extapi"
local run_script="$service_dir/run.sh"
local log_file="$service_dir/logs/ubains-INFO-AND-ERROR.log"
local rc_local="/etc/rc.d/rc.local"
local service_name="java-meeting-extapi"
log "INFO" "=================================================="
log "INFO" "🚀 正在启动对外服务: $service_name"
log "INFO" " 服务目录: $service_dir"
log "INFO" "=================================================="
# 1. 检查Java环境是否部署成功
log "INFO" "🔍 正在检查Java环境..."
source /etc/profile 2>/dev/null || . /etc/profile 2>/dev/null
source /etc/profile
if ! java -version >/dev/null 2>&1; then
log "ERROR" "❌ Java环境未部署或配置失败"
log "ERROR" " 请先执行 deploy_jdk_host 函数部署JDK"
log "ERROR" " 或手动执行: source /etc/profile"
return 1
fi
local java_version=$(java -version 2>&1 | head -n1)
log "INFO" "✅ Java环境检查通过: $java_version"
# 2. 检查服务目录和脚本是否存在
if [ ! -d "$service_dir" ]; then
log "ERROR" "❌ 服务目录不存在: $service_dir"
return 1
fi
if [ ! -f "$run_script" ]; then
log "ERROR" "❌ 启动脚本不存在: $run_script"
return 1
fi
# 确保脚本有执行权限
if [ ! -x "$run_script" ]; then
log "INFO" "🔧 正在为启动脚本添加执行权限..."
chmod +x "$run_script"
fi
log "INFO" "✅ 服务目录和脚本检查通过"
# 3. 检查服务是否已经在运行
cd "$service_dir" || {
log "ERROR" "❌ 无法进入服务目录: $service_dir"
return 1
}
# 尝试通过进程名或日志判断服务是否已运行
local process_count=$(pgrep -f "java-meeting-extapi\|run.sh" 2>/dev/null | wc -l)
if [ "$process_count" -gt 0 ]; then
log "WARN" "⚠️ 检测到服务可能已在运行(进程数: $process_count)"
log "INFO" " 将尝试重新启动服务"
fi
# 4. 启动服务
log "INFO" "▶️ 正在启动服务..."
# 确保日志目录存在
mkdir -p "$(dirname "$log_file")"
# 执行启动命令:./run.sh
log "INFO" "📝 执行启动命令: ./run.sh"
./run.sh > "$service_dir/nohup.out" 2>&1 &
local start_pid=$!
# 等待几秒让服务启动
log "INFO" "⏳ 等待服务启动(5秒)..."
sleep 5
# 执行tail -f查看日志(后台运行,避免阻塞)
log "INFO" "📋 正在查看服务日志: tail -f $log_file"
if [ -f "$log_file" ]; then
# 显示最后20行日志
log "INFO" "📄 最新日志内容(最后20行):"
tail -n 20 "$log_file" 2>/dev/null | while read line; do
log "INFO" " $line"
done
# 后台运行tail -f,输出到nohup
nohup tail -f "$log_file" >> "$service_dir/tail.log" 2>&1 &
log "INFO" "✅ 日志监控已启动(后台运行),实时日志输出到: $service_dir/tail.log"
else
log "WARN" "⚠️ 日志文件尚未创建: $log_file"
log "INFO" " 服务可能正在启动中,请稍后查看日志"
fi
# 5. 检查服务进程是否启动成功
log "INFO" "🔍 正在检查服务进程..."
# 检查run.sh进程
local run_pid=$(pgrep -f "run.sh" | head -n1)
if [ -n "$run_pid" ]; then
log "INFO" "✅ 启动脚本进程已运行(PID: $run_pid)"
else
log "WARN" "⚠️ 未检测到启动脚本进程,服务可能启动失败"
fi
# 检查Java进程(通过服务目录或进程名)
local java_pids=$(pgrep -f "java.*$service_dir\|java-meeting-extapi" 2>/dev/null)
if [ -n "$java_pids" ]; then
log "INFO" "✅ Java服务进程已启动"
echo "$java_pids" | while read pid; do
log "INFO" " PID: $pid"
done
else
log "WARN" "⚠️ 未检测到Java服务进程,请检查日志: $log_file"
log "WARN" " 或查看: $service_dir/nohup.out"
fi
# 检查日志文件是否存在(服务启动后会创建日志文件)
if [ -f "$log_file" ]; then
log "INFO" "✅ 日志文件已创建: $log_file"
# 显示最后几行日志
log "INFO" "📋 最新日志内容:"
tail -n 10 "$log_file" 2>/dev/null | while read line; do
log "INFO" " $line"
done
else
log "WARN" "⚠️ 日志文件尚未创建,服务可能正在启动中"
fi
# 6. 将启动命令写入/etc/rc.d/rc.local实现自启动
log "INFO" "📝 正在配置服务自启动..."
# 检查rc.local文件是否存在
if [ ! -f "$rc_local" ]; then
log "INFO" "📁 rc.local文件不存在,正在创建..."
sudo touch "$rc_local"
sudo chmod +x "$rc_local"
fi
# 构建启动命令(按照用户要求:cd目录,执行run.sh,tail -f日志)
# 注意:在rc.local中,tail -f会阻塞,所以使用后台运行
local startup_cmd="cd $service_dir && ./run.sh > $service_dir/nohup.out 2>&1 &"
local tail_cmd="tail -f $log_file >> $service_dir/tail.log 2>&1 &"
# 检查是否已经存在相同的启动命令
if grep -q "$service_dir.*run.sh" "$rc_local" 2>/dev/null; then
log "INFO" "✅ 自启动配置已存在,跳过添加"
else
# 备份rc.local
if [ -f "$rc_local" ]; then
sudo cp "$rc_local" "${rc_local}.backup.$(date +%Y%m%d_%H%M%S)" 2>/dev/null
log "INFO" "✅ 已备份rc.local文件"
fi
# 添加启动命令到rc.local
log "INFO" "📝 正在添加启动命令到 $rc_local"
echo "" | sudo tee -a "$rc_local" > /dev/null
echo "# Auto-start java-meeting-extapi service - $(date '+%Y-%m-%d %H:%M:%S')" | sudo tee -a "$rc_local" > /dev/null
echo "cd $service_dir && ./run.sh > $service_dir/nohup.out 2>&1 &" | sudo tee -a "$rc_local" > /dev/null
echo "tail -f $log_file >> $service_dir/tail.log 2>&1 &" | sudo tee -a "$rc_local" > /dev/null
# 确保rc.local有执行权限
sudo chmod +x "$rc_local" 2>/dev/null
log "INFO" "✅ 自启动配置已添加到 $rc_local"
log "INFO" " 启动命令: cd $service_dir && ./run.sh"
log "INFO" " 日志监控: tail -f $log_file"
fi
log "INFO" "=================================================="
log "INFO" "🎉 服务启动流程完成!"
log "INFO" " 服务目录: $service_dir"
log "INFO" " 日志文件: $log_file"
log "INFO" " 自启动配置: $rc_local"
log "INFO" " 提示: 可以使用 'tail -f $log_file' 查看实时日志"
log "INFO" "=================================================="
return 0
}
# ========================================
# ☕ 函数:deploy_jdk_host
# 描述:在宿主机上部署JDK(tar.gz格式)
# 功能:
# - 检查JDK安装包是否存在
# - 解压JDK到指定目录
# - 配置Java环境变量(JAVA_HOME, CLASSPATH, PATH)
# - 验证JDK安装
# 参数:
# $1: JDK安装包路径(tar.gz文件)
# $2: JDK安装目录(可选,默认为/opt/java)
# ========================================
function deploy_jdk_host() {
local jdk_tar_path="/data/temp/jdk-8u472-linux-x64.tar.gz"
local install_base_dir="${2:-/opt/java}"
log "INFO" "=================================================="
log "INFO" "☕ 正在部署宿主机JDK"
log "INFO" " JDK安装包: $jdk_tar_path"
log "INFO" " 安装目录: $install_base_dir"
log "INFO" "=================================================="
# 1. 检查JDK安装包是否存在
if [ -z "$jdk_tar_path" ]; then
log "ERROR" "❌ 未指定JDK安装包路径"
log "INFO" "用法: deploy_jdk_host <jdk_tar.gz路径> [安装目录]"
return 1
fi
if [ ! -f "$jdk_tar_path" ]; then
log "ERROR" "❌ JDK安装包不存在: $jdk_tar_path"
return 1
fi
# 检查是否为tar.gz格式
if [[ ! "$jdk_tar_path" =~ \.tar\.gz$ ]]; then
log "ERROR" "❌ 文件格式错误,需要tar.gz格式: $jdk_tar_path"
return 1
fi
log "INFO" "✅ JDK安装包检查通过"
# 2. 检查是否已安装JDK,并进行备份
local old_java_home=""
local old_java_version=""
local backup_dir="/opt/java_backup_$(date +%Y%m%d_%H%M%S)"
if command -v java &> /dev/null; then
old_java_version=$(java -version 2>&1 | head -n1)
log "INFO" "📋 检测到系统已安装Java: $old_java_version"
# 获取旧的JAVA_HOME
if [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ]; then
old_java_home="$JAVA_HOME"
else
# 尝试从java命令路径推断JAVA_HOME
local java_path=$(which java 2>/dev/null)
if [ -n "$java_path" ]; then
java_path=$(readlink -f "$java_path" 2>/dev/null || echo "$java_path")
# 移除/bin/java部分
old_java_home=$(dirname "$(dirname "$java_path")" 2>/dev/null)
fi
fi
# 备份旧的JDK目录
if [ -n "$old_java_home" ] && [ -d "$old_java_home" ] && [ "$old_java_home" != "$install_base_dir" ]; then
log "INFO" "💾 正在备份旧JDK目录: $old_java_home"
mkdir -p "$backup_dir"
if [ $? -eq 0 ]; then
cp -r "$old_java_home" "$backup_dir/old_jdk_$(basename "$old_java_home")" 2>/dev/null
if [ $? -eq 0 ]; then
log "INFO" "✅ 旧JDK目录已备份到: $backup_dir"
else
log "WARN" "⚠️ 旧JDK目录备份失败,继续安装"
fi
fi
fi
# 备份/etc/profile中的Java配置
log "INFO" "💾 正在备份/etc/profile中的Java配置..."
mkdir -p "$backup_dir"
if [ -f /etc/profile ]; then
cp /etc/profile "$backup_dir/profile.backup" 2>/dev/null
if [ $? -eq 0 ]; then
log "INFO" "✅ /etc/profile已备份到: $backup_dir/profile.backup"
fi
fi
# 记录旧Java信息到备份目录
if [ -d "$backup_dir" ]; then
{
echo "备份时间: $(date '+%Y-%m-%d %H:%M:%S')"
echo "旧Java版本: $old_java_version"
echo "旧JAVA_HOME: $old_java_home"
} > "$backup_dir/old_java_info.txt" 2>/dev/null
fi
log "INFO" "🔄 将自动更新Java环境..."
fi
# 3. 创建安装目录
mkdir -p "$install_base_dir"
if [ $? -ne 0 ]; then
log "ERROR" "❌ 无法创建安装目录: $install_base_dir"
return 1
fi
log "INFO" "✅ 创建安装目录: $install_base_dir"
# 4. 解压JDK
log "INFO" "🗜️ 正在解压JDK安装包..."
cd "$install_base_dir" || {
log "ERROR" "❌ 无法进入目录: $install_base_dir"
return 1
}
# 解压到当前目录
tar -xzf "$jdk_tar_path" -C "$install_base_dir"
if [ $? -ne 0 ]; then
log "ERROR" "❌ JDK解压失败,请检查压缩包完整性"
return 1
fi
log "INFO" "✅ JDK解压完成"
# 5. 查找解压后的JDK目录(通常是jdk8u472-b08或类似格式)
local jdk_dir=$(find "$install_base_dir" -maxdepth 1 -type d -name "jdk*" | head -n1)
if [ -z "$jdk_dir" ]; then
log "ERROR" "❌ 未找到JDK目录,解压后的目录结构可能不正确"
log "INFO" "请检查解压后的目录结构"
return 1
fi
local jdk_home="$jdk_dir"
local jdk_name=$(basename "$jdk_dir")
log "INFO" "✅ 找到JDK目录: $jdk_name"
log "INFO" " JDK_HOME: $jdk_home"
# 6. 检查JDK目录结构是否完整
if [ ! -d "$jdk_home/bin" ] || [ ! -f "$jdk_home/bin/java" ]; then
log "ERROR" "❌ JDK目录结构不完整,缺少bin/java文件"
return 1
fi
log "INFO" "✅ JDK目录结构检查通过"
# 7. 配置Java环境变量
log "INFO" "🛠️ 正在配置Java环境变量..."
# 检查/etc/profile中是否已存在JAVA_HOME配置,并备份后注释掉
if grep -q "JAVA_HOME\|#set java environment" /etc/profile; then
log "INFO" "📋 检测到/etc/profile中已存在JAVA_HOME配置,将注释旧配置并添加新配置"
# 备份旧的配置(如果之前没有备份)
if [ ! -f "$backup_dir/profile.backup" ]; then
mkdir -p "$backup_dir"
cp /etc/profile "$backup_dir/profile.backup" 2>/dev/null
log "INFO" "✅ /etc/profile已备份到: $backup_dir/profile.backup"
fi
# 使用awk来注释掉Java配置块(更可靠,处理多行块)
awk '
BEGIN { in_java_block = 0 }
/#set java environment/ {
in_java_block = 1
# 如果这行还没被注释,就注释掉
if ($0 !~ /^[[:space:]]*#/) {
print "#" $0
} else {
print $0
}
next
}
in_java_block {
# 如果是Java相关的配置行(JAVA_HOME, CLASSPATH, PATH, export等)
if ($0 ~ /JAVA_HOME|CLASSPATH|PATH.*JAVA_HOME|export.*JAVA_HOME|export.*CLASSPATH|export.*PATH/) {
# 如果这行还没被注释,就注释掉
if ($0 !~ /^[[:space:]]*#/ && $0 !~ /^[[:space:]]*$/) {
print "#" $0
} else {
print $0
}
next
}
# 如果是空行,保持块状态,继续
if ($0 ~ /^[[:space:]]*$/) {
print $0
next
}
# 其他非空行,结束Java块
in_java_block = 0
print $0
next
}
{ print $0 }
' /etc/profile > /etc/profile.tmp && mv /etc/profile.tmp /etc/profile
if [ $? -eq 0 ]; then
log "INFO" "✅ 旧Java配置已注释"
else
log "WARN" "⚠️ 注释旧配置时出现问题,继续安装"
fi
fi
# 添加新的Java环境变量配置
cat >> /etc/profile << EOF
#set java environment
JAVA_HOME=$jdk_home
CLASSPATH=.:\$JAVA_HOME/lib/tools.jar
PATH=\$JAVA_HOME/bin:\$PATH
export JAVA_HOME CLASSPATH PATH
EOF
log "INFO" "✅ Java环境变量已写入 /etc/profile"
# 8. 立即在当前会话中生效
log "INFO" "🔄 正在重新加载 /etc/profile 使环境变量生效..."
source /etc/profile 2>/dev/null || . /etc/profile 2>/dev/null
source /etc/profile
# 如果source失败,则手动设置环境变量
if [ $? -ne 0 ]; then
log "WARN" "⚠️ source /etc/profile 失败,使用export设置环境变量"
export JAVA_HOME="$jdk_home"
export CLASSPATH=".:$JAVA_HOME/lib/tools.jar"
export PATH="$JAVA_HOME/bin:$PATH"
fi
log "INFO" "✅ 环境变量已在当前会话中生效"
# 9. 验证Java安装
log "INFO" "🔍 正在验证Java安装..."
if java -version >/dev/null 2>&1; then
local java_version=$(java -version 2>&1 | head -n1)
local java_path=$(which java)
log "INFO" "✅ Java环境配置成功!"
log "INFO" " Java版本: $java_version"
log "INFO" " Java路径: $java_path"
log "INFO" " JAVA_HOME: $JAVA_HOME"
else
log "ERROR" "❌ Java环境配置失败,请检查路径或权限"
log "INFO" " 请手动执行: source /etc/profile"
return 1
fi
log "INFO" "=================================================="
log "INFO" "🎉 JDK部署成功!"
log "INFO" " 安装路径: $jdk_home"
log "INFO" " 环境变量已配置到 /etc/profile"
log "INFO" " 提示: 新开终端或执行 'source /etc/profile' 使环境变量生效"
log "INFO" "=================================================="
return 0
}
#------------------------------服务安装-end------------------------------------------------------------------------------------------------------------------------
#------------------------------全局配置-start--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
###服务器检测配置项
#mem-check
#最小内存大小G
min_memory_value=8
#最小有效内存大小G
min_memory_available=8
#disk-check
#最小硬盘资源大小G
#min_disk_value=200
#最小有效硬盘大小G
min_disk_available=50
#默认为空,如果为非root用户执行,则需要配置为sudo
sudoset=""
#------------------------------全局配置-end--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
#-------------------------------脚本执行-start--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
sys1="ubains.txt"
sys2="devops.txt"
sys3="ubains_devops.txt"
checkOS
if [ -e "$PWD/$sys1" ]; then
txt="检测该脚本为【预定系统 部署】"
version="预定系统版本"
elif [ -e "$PWD/$sys2" ]; then
txt="检测该脚本为【运维系统 部署】"
version="运维系统版本"
elif [ -e "$PWD/$sys3" ]; then
txt="检测该脚本为【预定系统+运维系统 部署】"
version="预定系统+运维系统版本"
fi
#-------------------------------获取服务器ip-start--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
#文件默认ip
ip='192.168.9.84'
IP=$(hostname -I | awk '{print $1}')
function server_ip()
{
# 弹出弹框,并根据用户选择执行相应操作
choice=$(whiptail --title "请选择操作" \
--menu "请确认服务器ip是否正确:$IP" 15 50 4 \
1 "是" \
2 "否" \
3 "退出" \
3>&1 1>&2 2>&3)
# 根据用户选择执行相应操作
case $choice in
1)
# 在这里编写操作1的代码逻辑
;;
2)
# 设置对话框的标题和提示信息
dialog_title="请输入服务器IP:"
dialog_prompt="请输入服务器IP:"
# 使用whiptail显示输入框,并将用户输入的值保存到变量input_value
IP=$(whiptail --title "$dialog_title" --inputbox "$dialog_prompt" 10 50 3>&1 1>&2 2>&3)
# 在这里编写操作2的代码逻辑
;;
3)
exit
# 在这里编写操作3的代码逻辑
;;
*)
echo "无效的选择"
;;
esac
}
#-------------------------------获取服务器的ip-end-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
function getLatestVersion() {
log "INFO" "检查中间件最新版本信息"
#判断是否能够访问外网,不能就跳过联网检查
if ! curl -s --head "https://www.baidu.com" | grep "200 OK" > /dev/null; then
log "WARN" "无法访问外网,跳过最新版本检查"
return
fi
#todo:如果访问不到,就跳过,后面改成用自己的接口服务
log "INFO" "准备获取最新版本信息"
latest_nginx_version=$(curl -s "https://nginx.org/en/download.html" | grep -oP 'nginx-\K[0-9]+\.[0-9]+\.[0-9]+' | head -n 1)
latest_redis_version=$(curl -s "https://api.github.com/repos/redis/redis/releases/latest" | \
grep -m 1 '"tag_name"' | \
grep -oP '[0-9]+\.[0-9]+\.[0-9]+' )
latest_mysql_version=$(curl -s https://repo.mysql.com/yum/mysql-8.0-community/el/7/x86_64/ \
| grep -oP 'mysql-community-server-[0-9]+\.[0-9]+\.[0-9]+' \
| sort -V \
| tail -n 1 \
| grep -oP '[0-9]+\.[0-9]+\.[0-9]+'
)
log "INFO" "最新版本信息获取成功"
log "INFO" "准备获取本地版本信息"
current_nginx_version=$(docker exec ujava2 sh -c "/usr/local/nginx/sbin/nginx -v " 2>&1 | grep -oP 'nginx/\K[0-9\.]+')
current_redis_version=$(docker exec uredis sh -c "redis-server --version " 2>&1 | grep -oP 'Redis server v=\K[0-9\.]+')
current_mysql_version=$(docker exec umysql sh -c "mysql --version " 2>&1 | grep -oP 'mysql Ver \K[0-9\.]+')
log "INFO" "本地版本信息获取成功, 准备进行版本比较"
#比较不同的版本情况,不一样的给出告警提示
if [ "$latest_nginx_version" != "$current_nginx_version" ]; then
log "WARN" "Nginx版本不一致,最新版本: $latest_nginx_version, 当前版本: $current_nginx_version"
else
log "INFO" "Nginx版本一致,当前版本: $current_nginx_version"
fi
if [ "$latest_redis_version" != "$current_redis_version" ]; then
log "WARN" "Redis版本不一致,最新版本: $latest_redis_version, 当前版本: $current_redis_version"
else
log "INFO" "Redis版本一致,当前版本: $current_redis_version"
fi
if [ "$latest_mysql_version" != "$current_mysql_version" ]; then
log "WARN" "MySQL版本不一致,最新版本: $latest_mysql_version, 当前版本: $current_mysql_version"
else
log "INFO" "MySQL版本一致,当前版本: $current_mysql_version"
fi
log "INFO" "版本检查完成"
}
#-------------------------------部署系统-------------------------------
log "INFO" "\033[36m
---------------------------------------------开始部署系统---------------------------------------------------
\033[0m"
server_ip
server_ip="$IP"
#自动化脚本路径
auto_java=$PWD
add_user
# 文件检查与部署
check_files_upload
#开放防火墙
firewalldjava
# 部署中间件服务(mysql, redis, emqx, fdfs, ngrok, nacos, nginx)
middleware_type="mysql redis emqx fdfs nginx" install_middleware
ntp_uos
deploy_jdk_host
start_java_meeting_service
malan
deploy_services
add_crontab_job
#检查中间件版本
getLatestVersion
#___________________________________________________________________________________
log "INFO" "正在重启docker服务,请耐心等待。"
$sudoset systemctl restart docker
sleep 30
log "INFO" "查看服务是否启动成功——————docker ps"
log "INFO" "系统服务:umysql,uredis,ujava2,uemqx,ustorage,utracker "
$sudoset docker ps
\ No newline at end of file
......@@ -25,7 +25,7 @@
# ./auto_clean_deleted_ubains_v3.sh
#
# 定时任务示例:
# 0 4 * * * /opt/scripts/auto_clean_deleted_ubains_v3.sh
# 0 4 * * * /data/services/scripts/auto_clean_deleted_ubains_v3.sh
#===============================================================================
# ================= 配置区域 =================
......
#!/bin/bash
#===============================================================================
# 脚本名称:monitor_emqx_service.sh
# 功能描述:EMQX 服务监测与自愈脚本 (增强版)
# 版本:V2.2
# 创建日期:2026-01-27
# 修改日期:2026-03-30
# 基于文档:_PRD_预定系统_EMQX 服务监控需求文档.md V2.1
# 变更历史:
# V1.0 (2026-01-27): 初始版本,实现基础检测和重启功能
# V2.0 (2026-03-30): 重大升级
# - 增加连续失败计数机制 (MAX_FAILURES=3)
# - 增加失败重试间隔 (FAILURE_INTERVAL=5 秒)
# - 增加重启冷却时间 (COOLDOWN_PERIOD=1800 秒/30 分钟)
# - 增加多维度健康检查 (进程 + 端口+CLI 命令)
# - 增加状态持久化机制
# - 优化日志输出详细度
# - 增加冷却期保护逻辑
# - 完善错误处理和诊断信息
# V2.1 (2026-03-30): 统一日志格式,完善失败计数机制说明
#
# 监测对象:
# 1. uemqx 容器运行状态
# 2. 容器内 EMQX 进程状态
# 3. EMQX 服务可用性 (使用 emqx_ctl status 命令)
# 4. 自动重启异常容器
#
# 智能恢复策略:
# 1. 连续失败检测:连续 3 次检测失败才触发重启
# 2. 多重验证机制:进程检查 + 端口检查 + 命令响应
# 3. 重启冷却时间:30 分钟内不重复重启同一容器
#
# 使用方法:
# chmod +x monitor_emqx_service.sh
# ./monitor_emqx_service.sh
#
# 定时任务示例:
# */5 * * * * /data/services/scripts/monitor_emqx_service.sh
#===============================================================================
# 配置参数
LOG_FILE="/data/logs/monitor_emqx_service.log"
STATE_FILE="/data/logs/.emqx_monitor_state"
PID_FILE="/data/logs/.emqx_monitor.pid"
MAX_LOG_SIZE=$((5*1024*1024)) # 5MB 日志大小限制
LOG_RETENTION_DAYS=30 # 日志保留天数
MAX_FAILURES=3 # 最大连续失败次数
COOLDOWN_PERIOD=1800 # 重启冷却时间 (秒,30 分钟)
START_WAIT_TIME=30 # 启动后等待时间 (秒)
SERVICE_CHECK_WAIT=10 # 服务状态检查等待时间 (秒)
#===============================================================================
# 函数定义
#===============================================================================
# 获取锁函数(防止并发执行)
acquire_lock() {
# 检查 PID 文件是否存在
if [ -f "$PID_FILE" ]; then
local old_pid
old_pid=$(cat "$PID_FILE" 2>/dev/null)
# 检查该进程是否还在运行
if [ -n "$old_pid" ] && kill -0 "$old_pid" 2>/dev/null; then
echo "$(date '+%Y-%m-%d %H:%M:%S') - 警告: 上一次脚本执行仍在运行 (PID: $old_pid),本次跳过执行" | tee -a "$LOG_FILE"
exit 1
else
# 进程不存在,清理旧的 PID 文件
rm -f "$PID_FILE"
fi
fi
# 确保 PID 文件目录存在
local pid_dir
pid_dir=$(dirname "$PID_FILE")
if [ ! -d "$pid_dir" ]; then
mkdir -p "$pid_dir" 2>/dev/null || true
fi
# 写入当前进程 PID
echo $$ > "$PID_FILE"
# 设置退出时清理 PID 文件
trap 'rm -f "$PID_FILE"; exit' EXIT INT TERM HUP
}
# 释放锁函数(可选,trap 会自动处理)
release_lock() {
rm -f "$PID_FILE"
}
# 日志轮转函数
rotate_logs() {
if [ -f "$LOG_FILE" ]; then
FILE_SIZE=$(stat -c%s "$LOG_FILE" 2>/dev/null || echo 0)
if [ "$FILE_SIZE" -ge "$MAX_LOG_SIZE" ]; then
mv "$LOG_FILE" "$LOG_FILE.$(date '+%Y%m%d%H%M%S')"
touch "$LOG_FILE"
echo "$(date '+%Y-%m-%d %H:%M:%S') - 日志文件超过 5MB,已自动轮转。" | tee -a "$LOG_FILE"
fi
fi
# 清理超过保留天数的旧日志文件
find "$(dirname "$LOG_FILE")" -name "monitor_emqx_service.log.*" -mtime +$LOG_RETENTION_DAYS -exec rm -f {} \; 2>/dev/null
}
# 日志记录函数
log() {
local message
message="$(date '+%Y-%m-%d %H:%M:%S') - $1"
echo "$message"
# 同时写入日志文件
echo "$message" >> "$LOG_FILE"
}
# 读取状态文件
read_state_file() {
if [ -f "$STATE_FILE" ]; then
source "$STATE_FILE"
echo "${FAILURE_COUNT:-0}"
else
echo "0"
fi
}
# 写入状态文件
write_state_file() {
local count=$1
local restart_time=$2
cat > "$STATE_FILE" << EOF
FAILURE_COUNT=$count
LAST_RESTART_TIME=$restart_time
LAST_CHECK_TIME=$(date +%s)
EOF
}
# 获取上次重启时间
get_last_restart_time() {
if [ -f "$STATE_FILE" ]; then
source "$STATE_FILE"
echo "${LAST_RESTART_TIME:-0}"
else
echo "0"
fi
}
# 检查 EMQX 端口是否监听
check_emqx_ports() {
local ports=(1883 8883 8083 8084 18083)
local listening_count=0
for port in "${ports[@]}"; do
if docker exec uemqx netstat -tuln 2>/dev/null | grep -q ":${port} "; then
((listening_count++))
fi
done
# 至少有 2 个核心端口监听认为正常 (1883 必须)
if [ $listening_count -ge 2 ] && docker exec uemqx netstat -tuln 2>/dev/null | grep -q ":1883 "; then
return 0
else
return 1
fi
}
# 综合健康检查
comprehensive_health_check() {
# 检查 1: 容器是否运行
if ! docker ps --format '{{.Names}}' | grep -Fxq "uemqx"; then
return 1
fi
# 检查 2: EMQX 进程是否存在
if ! docker exec uemqx pgrep -f "emqx" >/dev/null 2>&1; then
return 1
fi
# 检查 3: 核心端口是否监听
if ! check_emqx_ports; then
return 1
fi
# 检查 4: emqx_ctl 命令响应
local status_output
status_output=$(docker exec uemqx emqx_ctl status 2>&1)
if ! echo "$status_output" | grep -q "is started"; then
return 1
fi
return 0
}
# 重启EMQX容器
restart_emqx_container() {
local current_time
current_time=$(date +%s)
local last_restart_time
last_restart_time=$(get_last_restart_time)
local time_since_restart=$((current_time - last_restart_time))
log "开始执行 EMQX 容器重启流程"
log "距上次重启时间:${time_since_restart}秒"
# 检查容器是否存在
if ! docker ps -a --format '{{.Names}}' | grep -Fxq "uemqx"; then
log "错误: 容器 'uemqx' 不存在!"
return 1
fi
# 停止容器
docker stop uemqx >/dev/null 2>&1 || true
sleep 2
# 启动容器
if docker start uemqx; then
log "容器启动命令执行成功"
# 等待服务启动
log "等待 ${START_WAIT_TIME} 秒让 EMQX 服务完全启动..."
for i in $(seq 1 $START_WAIT_TIME); do
echo -n "." >> "$LOG_FILE"
sleep 1
done
echo "" >> "$LOG_FILE"
# 验证启动结果
# 检查容器运行状态
if ! docker ps --format '{{.Names}}' | grep -Fxq "uemqx"; then
log "错误: 容器启动后立即停止"
write_state_file 0 "$current_time"
return 1
fi
log "容器运行状态正常"
# 检查进程状态
sleep 5
if ! docker exec uemqx pgrep -f "emqx" >/dev/null 2>&1; then
log "错误: EMQX 进程未启动"
write_state_file 0 "$current_time"
return 1
fi
log "EMQX 进程已启动"
# 检查端口监听
if ! check_emqx_ports; then
log "错误: EMQX 端口未正常监听"
write_state_file 0 "$current_time"
return 1
fi
log "EMQX 端口监听正常"
# 检查服务状态
sleep $SERVICE_CHECK_WAIT
local status_output
status_output=$(docker exec uemqx emqx_ctl status 2>&1)
if echo "$status_output" | grep -q "is started"; then
log "EMQX 服务状态验证通过"
write_state_file 0 "$current_time"
return 0
else
log "错误: EMQX 服务状态异常"
write_state_file 0 "$current_time"
return 1
fi
else
log "错误: 无法启动容器 'uemqx'"
return 1
fi
}
#===============================================================================
# 主逻辑
#===============================================================================
# 主函数
main() {
# 获取锁(防止并发执行)
acquire_lock
# 执行日志轮转
rotate_logs
log "==========================================="
log "EMQX 服务健康检查开始"
log "==========================================="
log "配置参数:"
log " - 最大失败次数:${MAX_FAILURES}"
log " - 重启冷却时间:${COOLDOWN_PERIOD}秒 ($(($COOLDOWN_PERIOD / 60))分钟)"
log "==========================================="
# 确保日志目录存在
local log_dir
log_dir=$(dirname "$LOG_FILE")
if [ ! -d "$log_dir" ]; then
mkdir -p "$log_dir" 2>/dev/null || true
fi
# 读取当前失败次数
local failure_count
failure_count=$(read_state_file)
local last_restart_time
last_restart_time=$(get_last_restart_time)
log "读取状态文件: 当前失败次数 = $failure_count, 距上次重启 = $((($(date +%s) - last_restart_time)))秒"
# 执行健康检查
if comprehensive_health_check; then
# 检查成功,清零计数器
if [ "$failure_count" -gt 0 ]; then
write_state_file 0 "$last_restart_time"
log "检查成功,重置失败计数器为 0"
fi
log "EMQX 服务状态正常"
else
# 检查失败,累加计数器
failure_count=$((failure_count + 1))
write_state_file "$failure_count" "$last_restart_time"
log "警告: EMQX 服务状态异常:emqx is not running"
log "失败计数器更新: $((failure_count - 1)) -> $failure_count (阈值: $MAX_FAILURES)"
# 判断是否达到阈值
if [ "$failure_count" -ge "$MAX_FAILURES" ]; then
log "错误: 连续失败达到阈值($MAX_FAILURES次),触发容器重启"
write_state_file 0 "$last_restart_time"
# 检查冷却期
local current_time
current_time=$(date +%s)
local time_since_restart=$((current_time - last_restart_time))
if [ $last_restart_time -gt 0 ] && [ $time_since_restart -lt $COOLDOWN_PERIOD ]; then
log "警告: 距上次重启仅 ${time_since_restart} 秒,冷却期未过(${COOLDOWN_PERIOD}秒),跳过本次重启"
log "失败计数器保持为 $failure_count,等待冷却期过后重新检查"
else
# 执行重启
if restart_emqx_container; then
log "EMQX 容器重启成功"
else
log "错误: EMQX 容器重启失败"
fi
fi
else
log "警告: 未达到重启阈值,等待下次检查确认"
fi
fi
log "==========================================="
log "EMQX 服务健康检查完成"
log "==========================================="
}
# 执行主逻辑
main
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论