提交 584cfe65 authored 作者: 陈泽健's avatar 陈泽健

feat(skills): 新增X86/ARM集群监控、Cockpit-KVM部署三大skill,补充集群部署文档与脚本

新增skill:
- X86-Cluster-Monitor: 5网段四台麒麟V10服务器三机热备集群夜间监控,22:00-09:00每2h执行
- ARM-CLUSTER-MONITOR: 9网段四台ARM架构服务器集群监控
- Cockpit-KVM-Deploy: Cockpit平台KVM虚拟机自动化部署

补充文档与脚本:
- EMQX/FastDFS/Nginx负载均衡/Nacos集群部署指南
- KVM虚拟机创建操作指南
- 达梦数据库导入脚本、EMQX集群部署/监控脚本
- 远程部署辅助脚本
Co-Authored-By: 's avatarClaude <noreply@anthropic.com>
上级 b151dba1
---
name: ARM-CLUSTER-MONITOR
description: ARM集群夜间监控 - 监测9网段四台ARM架构服务器集群状态,定时执行(22:00-09:00每2小时),输出分析报告,以日期时间命名
---
监测 9 网段四台 ARM 架构服务器集群的服务健康状态,执行定时监控(夜间22:00至次日09:00,每2小时一次),输出详细分析报告并以日期时间命名。
**核心特性**: 四台集群监控 → 定时执行(22:00-09:00每2h) → 集群横向对比 → 输出MD报告(日期时间命名)
## 工作规范
### 语言规范
- **所有回复、代码注释、Git提交信息默认使用中文(简体)描述**
- 报告内容、日志输出、异常信息均使用中文;
- 配置文件中的 `name``description` 等可读字段使用中文。
### SSH连接规范
- **连接方式**:默认使用 Python `paramiko` 库进行SSH连接;
- **免密检查**:每次连接前优先检索本地是否存在对应服务器的SSH免密配置文件;
- 免密文件夹命名规则:`.ssh/免密_{服务器IP}/`,例如 `.ssh/免密_192.168.9.89/`
- 检查项:是否存在私钥文件(id_rsa)、公钥文件(id_rsa.pub)、known_hosts
- **密码处理**:如果本地不存在免密配置,则提示用户提供SSH密码;
- **免密配置**:每次成功SSH连接后,自动为客户机配置SSH免密登录:
1. 在本地生成RSA密钥对(如不存在)
2. 通过paramiko将公钥分发到远程服务器的 `~/.ssh/authorized_keys`
3. 将免密凭据保存到本地 `.ssh/免密_{服务器IP}/` 目录
4. 后续连接优先使用该目录下的免密配置
### 上下文管理规范
- **自动压缩上下文**:当前模型上下文为200k tokens,需主动管理避免超限;
- **压缩策略**
1. 四次服务器的监控数据量大时,采用**逐台收集→汇总写入**的方式,避免同时持有四台原始数据;
2. 单台服务器日志分析超过100行时,使用统计摘要代替原文输出;
3. 报告文件生成后立即写入磁盘,不在对话上下文中保留完整报告内容;
4. 定时任务每次执行后,对话上下文需精简,只保留上一次的关键摘要;
5. 使用Agent工具并行监控四台服务器时,各Agent独立执行并返回摘要,主流程仅汇总摘要数据。
### 计划文档规范
- **每次理解需求文档后**,必须输出 **《计划执行》** 文档;
- 计划执行文档需包含以下内容:
- 需求理解摘要
- 实施步骤(分步编号)
- 涉及的文件清单
- 风险评估
- 预期产出
- **必须等待用户确认计划后方可开始执行**,不得跳过确认步骤直接操作;
- 计划执行文档保存到 `.claude/skills/ARM-CLUSTER-MONITOR/计划执行_{YYYYMMDD_HHMMSS}.md`
## Usage
```
/ARM-CLUSTER-MONITOR # 立即执行一次集群监控
/ARM-CLUSTER-MONITOR 定时 # 启动定时监控(22:00-09:00每2小时)
/ARM-CLUSTER-MONITOR 定时状态 # 查看定时任务状态
/ARM-CLUSTER-MONITOR 定时停止 # 停止定时监控任务
/ARM-CLUSTER-MONITOR 配置免密 # 为四台服务器配置SSH免密登录
```
## 核心位置
- **Skill目录**`.claude/skills/ARM-CLUSTER-MONITOR/`
- **配置文件**`.claude/skills/ARM-CLUSTER-MONITOR/config.json`(四台服务器配置)
- **集群信息表**`.claude/skills/ARM-CLUSTER-MONITOR/集群信息配置表.md`
- **SSH免密目录**`.ssh/免密_{服务器IP}/`(每个节点独立目录)
- **报告输出**`AuxiliaryTool/ScriptTool/ServiceMonitor/reports/arm_cluster/`
- **计划文档**`.claude/skills/ARM-CLUSTER-MONITOR/计划执行_{YYYYMMDD_HHMMSS}.md`
## SSH连接与免密配置流程
### 连接前检查
```
1. 读取 集群信息配置表.md 获取四台服务器IP
2. 对每台服务器执行免密检查:
├─ 检查本地目录 .ssh/免密_{IP}/ 是否存在
│ ├─ id_rsa(私钥文件)
│ ├─ id_rsa.pub(公钥文件)
│ └─ known_hosts
├─ 存在 → 使用免密连接
└─ 不存在 → 提示用户提供密码
3. 使用paramiko建立SSH连接:
├─ 免密模式:paramiko.RSAKey.from_private_key_file()
├─ 密码模式:提示用户输入密码
└─ 连接失败 → 重试3次 → 标记失败节点
```
### 免密配置流程
```
1. 在本地生成RSA密钥对(如 ~/.ssh/id_rsa 不存在)
$ ssh-keygen -t rsa -b 4096 -f ~/.ssh/id_rsa -N ""
2. 通过paramiko连接远程服务器(首次使用密码)
3. 在远程服务器上配置免密:
├─ sftp 上传 ~/.ssh/id_rsa.pub → /tmp/temp_pub_key
├─ ssh 执行: mkdir -p ~/.ssh && chmod 700 ~/.ssh
├─ ssh 执行: cat /tmp/temp_pub_key >> ~/.ssh/authorized_keys
├─ ssh 执行: chmod 600 ~/.ssh/authorized_keys
└─ ssh 执行: rm /tmp/temp_pub_key
4. 保存免密凭据到本地目录:
├─ 创建目录: .ssh/免密_{服务器IP}/
├─ 复制 id_rsa → .ssh/免密_{服务器IP}/id_rsa
├─ 复制 id_rsa.pub → .ssh/免密_{服务器IP}/id_rsa.pub
└─ 保存主机指纹 → .ssh/免密_{服务器IP}/known_hosts
5. 验证免密连接成功 → 输出配置完成提示
```
### paramiko连接示例代码
```python
import paramiko
import os
def connect_to_server(server_ip, username, password=None, key_dir=None):
"""使用paramiko连接远程服务器,优先使用免密配置"""
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
# 优先检查免密配置
key_path = f".ssh/免密_{server_ip}/id_rsa"
if os.path.exists(key_path):
# 免密模式连接
private_key = paramiko.RSAKey.from_private_key_file(key_path)
client.connect(
hostname=server_ip,
port=22,
username=username,
pkey=private_key,
timeout=10
)
elif password:
# 密码模式连接
client.connect(
hostname=server_ip,
port=22,
username=username,
password=password,
timeout=10
)
else:
raise Exception(f"服务器 {server_ip} 无免密配置且未提供密码")
return client
```
## 四台ARM集群服务器配置
### 服务器列表(config.json)
```json
{
"servers": [
{
"id": 1,
"name": "ARM集群节点1",
"ip": "192.168.9.76",
"port": 22,
"username": "admin",
"password": "Ubains@123",
"use_sudo": true,
"description": "ARM Ubuntu集群节点1",
"environment": "cluster",
"role": "master",
"tags": ["ARM", "Ubuntu", "集群主节点"]
},
{
"id": 2,
"name": "ARM集群节点2",
"ip": "192.168.9.77",
"port": 22,
"username": "admin",
"password": "Ubains@123",
"use_sudo": true,
"description": "ARM Ubuntu集群节点2",
"environment": "cluster",
"role": "worker",
"tags": ["ARM", "Ubuntu", "集群工作节点"]
},
{
"id": 3,
"name": "ARM集群节点3",
"ip": "192.168.9.78",
"port": 22,
"username": "admin",
"password": "Ubains@123",
"use_sudo": true,
"description": "ARM Ubuntu集群节点3",
"environment": "cluster",
"role": "worker",
"tags": ["ARM", "Ubuntu", "集群工作节点"]
},
{
"id": 4,
"name": "ARM集群节点4",
"ip": "192.168.9.79",
"port": 22,
"username": "admin",
"password": "Ubains@123",
"use_sudo": true,
"description": "ARM Ubuntu集群节点4",
"environment": "cluster",
"role": "worker",
"tags": ["ARM", "Ubuntu", "集群工作节点"]
}
],
"monitor": {
"schedule": {
"start_hour": 22,
"end_hour": 9,
"interval_hours": 2
},
"items": {
"containers": true,
"middleware": true,
"logs": true,
"resources": true,
"cluster_status": true
}
}
}
```
> **注意**:实际服务器IP、凭据需根据真实环境调整,以上仅为示例配置结构。
## 监控范围
### 1. 集群整体状态
- **集群连通性**:四台服务器SSH可达性检查(优先使用免密)
- **主节点状态**:master节点的服务健康度
- **工作节点状态**:worker节点的资源负载与服务可用性
- **集群一致性**:核心服务在各节点版本一致性检查
### 2. 服务容器状态(各节点)
| 容器名称 | 类型 | 说明 |
|---------|------|------|
| ujava2 | Java合集容器 | auth+gateway+system+meeting+mqtt+quartz+dubbo |
| unginx | Nginx反向代理 | 统一入口HTTPS 443 |
| umysql | MySQL数据库 | 集群数据库节点 |
| uredis | Redis缓存 | 集群缓存节点 |
| uemqx | MQTT消息队列 | 集群MQTT服务 |
| unacos | Nacos配置中心 | 集群配置中心 |
| utracker | FastDFS Tracker | 文件追踪 |
| ustorage | FastDFS Storage | 文件存储 |
| upython | 运维集控CMDB | Python Django服务 |
### 3. 中间件状态(三层验证)
- **L1 容器层**:docker inspect 存活状态
- **L2 网络层**:端口监听检查(ss -tln)
- **L3 功能层**
- umysql:数据库连接 + 集群库查询
- uredis:Redis读写测试
- uemqx:MQTT连接测试
- unacos:配置读取测试
### 4. 系统资源监控
| 监控项 | 阈值 | 告警级别 |
|-------|------|---------|
| CPU使用率 | >80% | WARNING |
| CPU使用率 | >95% | CRITICAL |
| 内存使用率 | >85% | WARNING |
| 内存使用率 | >95% | CRITICAL |
| 磁盘使用率 | >90% | WARNING |
| 磁盘使用率 | >95% | CRITICAL |
### 5. 日志分析
- ERROR/WARN日志统计(最近N行)
- 异常堆栈提取
- 关键错误模式匹配
- 日志文件大小异常检测
## 定时监控机制
### 定时执行时间表
| 时间点 | 说明 |
|-------|------|
| 22:00 | 夜间第一次监控 |
| 00:00 | 零点监控 |
| 02:00 | 凌晨监控 |
| 04:00 | 深夜监控 |
| 06:00 | 清晨监控 |
| 08:00 | 早间监控 |
> **覆盖时间段**:22:00 → 09:00,每夜共执行 **6次** 监控
### 定时任务实现
使用 Claude Code 的 **CronCreate** 工具创建定时任务:
```javascript
// 定时任务配置(使用CronCreate)
{
cron: "0 22,0,2,4,6,8 * * *", // 每天22:00、00:00、02:00、04:00、06:00、08:00执行
prompt: "/ARM-CLUSTER-MONITOR",
recurring: true,
durable: true // 持久化到.claude/scheduled_tasks.json
}
```
### 定时任务管理
| 命令 | 说明 |
|------|------|
| `/ARM-CLUSTER-MONITOR 定时` | 启动定时监控任务 |
| `/ARM-CLUSTER-MONITOR 定时状态` | 查看当前定时任务状态(CronList) |
| `/ARM-CLUSTER-MONITOR 定时停止` | 停止定时任务(CronDelete) |
## 监控流程
### 单次监控执行流程
```
1. 读取配置文件(集群信息配置表.md 或 config.json)
2. SSH免密检查与连接(按SSH连接规范执行)
├─ 对每台服务器检查 .ssh/免密_{IP}/ 目录
├─ 存在免密 → 使用paramiko免密连接
├─ 不存在免密 → 提示用户提供密码 → 连接后自动配置免密
└─ 连接失败 → 重试3次 → 标记失败节点
3. 使用Agent工具并行监控四台服务器
├─ Agent-1: 监控节点1(独立执行,返回摘要)
├─ Agent-2: 监控节点2(独立执行,返回摘要)
├─ Agent-3: 监控节点3(独立执行,返回摘要)
└─ Agent-4: 监控节点4(独立执行,返回摘要)
└─ 各Agent监控项:
├─ 系统资源检查
├─ 容器状态检查
├─ 中间件功能检查
├─ 日志分析
└─ 集群状态检查
4. 上下文压缩处理(按上下文管理规范执行)
├─ 各Agent仅返回摘要数据(非完整原始日志)
├─ 主流程汇总时不在上下文中保留完整报告
└─ 报告写入磁盘后释放上下文
5. 生成集群汇总报告
├─ 四台服务器横向对比
├─ 集群整体健康评分
├─ 异常节点高亮标记
└─ 集群一致性分析
6. 输出报告(日期时间命名)
├─ 控制台摘要:简要汇总(中文)
└─ MD报告文件:保存到 reports/arm_cluster/
└─ 命名格式:arm_cluster_monitor_YYYYMMDD_HHMMSS.md
```
### 定时监控循环流程
```
启动定时任务(22:00触发)
执行集群监控 → 生成报告 → 保存文件 → 压缩上下文
等待下一个触发点(每2小时)
重复执行直至09:00后停止
到达09:00 → 自动暂停定时任务
次日22:00 → 自动恢复执行
```
### 上下文压缩流程
```
监控完成后:
1. 各子Agent返回的数据为摘要格式(非原始数据)
- 系统资源:只返回使用率百分比和告警项
- 容器状态:只返回异常容器列表(正常容器记为"✅ 全部正常")
- 中间件功能:只返回三层中存在异常的项
- 日志分析:只返回ERROR/WARN统计数量和关键错误类型
2. 主流程汇总时:
- 将完整报告立即写入MD文件
- 对话中只输出控制台摘要(不超过200行)
- 释放上下文中的中间数据
3. 定时任务间:
- 每次执行后只保留上一次的摘要结果
- 不保留历史报告的完整内容在上下文中
```
## 报告输出
### 报告命名规范
**以日期时间命名**`arm_cluster_monitor_YYYYMMDD_HHMMSS.md`
**示例**
- `arm_cluster_monitor_20260709_220000.md`(22:00监控)
- `arm_cluster_monitor_20260710_000000.md`(00:00监控)
- `arm_cluster_monitor_20260710_080000.md`(08:00监控)
### 报告存储位置
`AuxiliaryTool/ScriptTool/ServiceMonitor/reports/arm_cluster/`
### 报告内容结构(集群特色)
```markdown
# ARM集群服务监控报告
**监控时间**: YYYY-MM-DD HH:MM:SS
**集群节点数**: 4台
**集群状态**: ✅ 正常 / ⚠️ 警告 / ❌ 异常
**监控轮次**: 夜间第N次监控
## 一、集群节点状态总览
| # | 节点名称 | IP | 角色 | 系统资源 | 容器 | 中间件 | 日志 | 综合状态 |
|---|---------|-----|------|---------|------|--------|------|---------|
| 1 | ARM集群节点1 | 192.168.9.89 | master | ✅ | ✅ | ✅ | ✅ | ✅ 正常 |
| 2 | ARM集群节点2 | 192.168.9.90 | worker | ✅ | ✅ | ✅ | ✅ | ✅ 正常 |
| 3 | ARM集群节点3 | 192.168.9.91 | worker | ⚠️ 磁盘92% | ✅ | ✅ | ✅ | ⚠️ 警告 |
| 4 | ARM集群节点4 | 192.168.9.92 | worker | ✅ | ✅ | ✅ | ✅ | ✅ 正常 |
## 二、集群横向对比 - 系统资源
| 节点 | CPU | 内存 | 磁盘 | 负载 | 网络连接数 |
|------|-----|------|------|------|-----------|
| 节点1 | 45% | 72% | 65% | 2.3 | 150 |
| 节点2 | 38% | 68% | 58% | 1.8 | 120 |
| 节点3 | 42% | 75% | 🔴92% | 2.0 | 130 |
| 节点4 | 35% | 70% | 60% | 1.5 | 110 |
## 三、集群横向对比 - 容器状态
| 节点 | ujava2 | unginx | umysql | uredis | uemqx | unacos | utracker | ustorage |
|------|--------|--------|--------|--------|-------|--------|----------|----------|
| 节点1 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| 节点2 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| 节点3 | ✅ | ✅ | ✅ | ✅ | ✅ | ⚠️ | ✅ | ✅ |
| 节点4 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
## 四、集群横向对比 - 中间件功能
| 节点 | MySQL L3 | Redis L3 | EMQX L3 | Nacos L3 | FastDFS |
|------|----------|----------|---------|----------|---------|
| 节点1 | ✅ | ✅ | ✅ | ✅ | ✅ |
| 节点2 | ✅ | ✅ | ✅ | ✅ | ✅ |
| 节点3 | ✅ | ✅ | ✅ | ⚠️ 配置读取慢 | ✅ |
| 节点4 | ✅ | ✅ | ✅ | ✅ | ✅ |
## 五、集群一致性检查
| 服务项 | 节点1版本 | 节点2版本 | 节点3版本 | 节点4版本 | 一致性 |
|-------|----------|----------|----------|----------|--------|
| ujava2镜像 | ujava:v6 | ujava:v6 | ujava:v6 | ujava:v6 | ✅ 一致 |
| umysql版本 | 8.0.32 | 8.0.32 | 8.0.32 | 8.0.32 | ✅ 一致 |
| Redis配置 | cluster模式 | cluster模式 | cluster模式 | cluster模式 | ✅ 一致 |
## 六、各节点详细监控数据
### 节点1 (192.168.9.89) - master
#### 系统资源
- CPU使用率: 45%
- 内存使用率: 72% (10GB/14GB)
- 磁盘使用率: 65% (50GB/80GB)
#### 容器状态
(详细容器列表)
#### 中间件功能验证
(三层验证详情)
#### 日志分析
(ERROR/WARN统计)
### 节点2 (192.168.9.90) - worker
(同节点1结构)
### 节点3 (192.168.9.91) - worker
(同节点1结构)
### 节点4 (192.168.9.92) - worker
(同节点1结构)
## 七、集群告警汇总
| 时间 | 节点 | 级别 | 异常项 | 详情 | 处置建议 |
|------|------|------|--------|------|---------|
| 22:05 | 节点3 | WARNING | 磁盘使用率 | 92%,接近阈值 | 清理日志/临时文件 |
## 八、集群健康评分
- **节点1 (master)**: 100/100 🟢
- **节点2 (worker)**: 100/100 🟢
- **节点3 (worker)**: 85/100 🟡(磁盘告警)
- **节点4 (worker)**: 100/100 🟢
- **集群整体**: 95/100 🟢
## 九、处置建议
| 优先级 | 节点 | 建议操作 | 风险等级 |
|-------|------|---------|---------|
| P1 | 节点3 | 清理磁盘空间(当前92%) | HIGH |
## 十、监控覆盖率
| 监控维度 | 覆盖节点数 | 覆盖率 |
|---------|-----------|--------|
| 系统资源 | 4/4 | 100% |
| 容器状态 | 4/4 | 100% |
| 中间件功能 | 4/4 | 100% |
| 日志分析 | 4/4 | 100% |
| 集群一致性 | 4/4 | 100% |
```
## 执行方式
### 方式一:通过Skill调用(推荐)
```
/ARM-CLUSTER-MONITOR # 立即执行一次集群监控
/ARM-CLUSTER-MONITOR 定时 # 启动定时监控(22:00-09:00每2h)
/ARM-CLUSTER-MONITOR 定时状态 # 查看定时任务状态
/ARM-CLUSTER-MONITOR 定时停止 # 停止定时任务
/ARM-CLUSTER-MONITOR 配置免密 # 为四台服务器配置SSH免密登录
```
### 方式二:手动定时管理
通过 Claude Code 内置的定时任务工具:
```
/cron # 查看所有定时任务
/cron add "0 22,0,2,4,6,8 * * *" "/ARM-CLUSTER-MONITOR" # 手动添加定时任务
/cron delete <job_id> # 删除定时任务
```
## 计划执行流程
当用户提出需求变更或新功能需求时:
```
1. 理解需求
└─ 分析用户需求,确认理解无误
2. 输出《计划执行》文档
├─ 需求理解摘要
├─ 实施步骤(分步编号)
├─ 涉及的文件清单
├─ 风险评估
└─ 预期产出
3. 保存计划文档
└─ .claude/skills/ARM-CLUSTER-MONITOR/计划执行_{YYYYMMDD_HHMMSS}.md
4. 等待用户确认
├─ 用户确认 → 按计划执行
└─ 用户提出修改 → 修改计划后重新确认
5. 执行完成后
└─ 在计划文档中标记完成状态
```
### 计划执行文档模板
```markdown
# 计划执行 — YYYY-MM-DD HH:MM:SS
## 一、需求理解摘要
(用1-2句话描述用户需求的核心目标)
## 二、实施步骤
### 步骤1:(步骤名称)
- 操作内容:(具体要做什么)
- 涉及文件:(修改/新增的文件路径)
- 预期结果:(完成后应该达到的效果)
### 步骤2:(步骤名称)
...
## 三、涉及文件清单
| 文件路径 | 操作类型 | 说明 |
|---------|---------|------|
| .claude/skills/ARM-CLUSTER-MONITOR/SKILL.md | 修改 | 更新xxx |
| ... | ... | ... |
## 四、风险评估
| 风险项 | 可能性 | 影响 | 缓解措施 |
|-------|--------|------|---------|
| ... | 低/中/高 | 低/中/高 | ... |
## 五、预期产出
- 产出1:(描述)
- 产出2:(描述)
---
**状态**: □ 待确认 □ 已确认 □ 执行中 □ 已完成
**确认人**: [待填写]
**确认时间**: [待填写]
```
## 注意事项
1. **集群并行检查**:四台服务器使用Agent工具并行连接,提升监控效率
2. **定时任务持久化**:使用 `durable: true` 确保定时任务跨session保持
3. **报告时间命名**:严格按照 `YYYYMMDD_HHMMSS` 格式命名报告文件
4. **夜间时段限制**:定时监控仅在22:00-09:00执行,白天不自动触发
5. **sudo环境**:部分ARM服务器root被禁用,需配置 `use_sudo: true`
6. **集群一致性**:重点检查核心服务在各节点的版本、配置一致性
7. **告警抑制**:连续相同告警有冷却期,避免告警风暴
8. **历史报告**:报告文件持久保存,可追溯历史监控数据
9. **免密优先**:每次连接前检查免密配置,不存在时提示密码并自动配置
10. **上下文管理**:200k tokens上限,并行监控时各Agent返回摘要,主流程仅汇总
11. **中文输出**:所有回复、代码注释、Git提交信息使用简体中文
12. **计划先行**:需求变更必须输出计划执行文档,确认后方可执行
13. **paramiko连接**:默认使用Python paramiko库,不依赖系统ssh命令
## 关联文档
- `XTY-MONITOR` skill - 单台服务器监控参考
- `Troubleshoot` skill - 问题排查流程
- `arm_nacos_cron_race_condition` - ARM Nacos崩溃排查经验
- `server_976_ssh_admin` - 9.76用admin+sudo
- `.claude/skills/ARM-CLUSTER-MONITOR/集群信息配置表.md` - 集群信息配置表
## 扩展功能(待实现)
- [ ] 集群拓扑可视化(自动绘制集群架构图)
- [ ] 集群故障自动恢复(主节点故障自动切换)
- [ ] 集群负载均衡分析(节点间负载分布)
- [ ] 集群历史趋势图表(资源使用率趋势)
- [ ] 集群告警钉钉通知(自动推送告警)
- [ ] 集群容量规划建议(基于历史数据预测扩容需求)
- [ ] 免密配置自动检测与批量配置工具
\ No newline at end of file
# ARM集群信息配置表
> 本文件用于配置9网段四台ARM架构服务器集群的详细信息,供 `ARM-CLUSTER-MONITOR` skill 使用。
## 集群概述
- **集群名称**:新统一平台ARM集群
- **集群架构**:ARM架构
- **操作系统**:麒麟V10
- **节点数量**:4台
- **集群用途**:测试环境
- **集群模式**:三机热备
- **Redis集群服务说明**:采用1主2从3哨兵模式
- **FastDFS集群服务说明**:采用3个节点部署FastDFS集群
- **Nacos集群服务说明**:采用3节点部署Nacos集群
- **Emqx集群服务说明**:采用3个节点部署Emqx集群
## 节点配置信息
### 节点1(主节点 - master)
| 配置项 | 值 |
|-------|---------------------------------------------------------------------------------|
| **节点名称** | ARM集群节点1 |
| **IP地址** | 192.168.9.89 |
| **SSH端口** | 22 |
| **用户名** | openkylin |
| **密码** | Ubains@123 |
| **是否需要sudo** | 是 |
| **节点角色** | master(主节点) |
| **CPU核心数** | 8核 |
| **内存容量** | 16GB |
| **磁盘容量** | 80GB |
| **部署的服务** | ujava2, utengine, umysql, redis-master, redis-sentinel, uemqx, unacos, utracker, paperless |
| **特殊说明** | 主节点,承担核心业务 |
### 节点2(工作节点 - worker)
| 配置项 | 值 |
|-------|-----------------------------------------------------------------------------------------------|
| **节点名称** | ARM集群节点2 |
| **IP地址** | 192.168.9.90 |
| **SSH端口** | 22 |
| **用户名** | openkylin |
| **密码** | Ubains@123 |
| **是否需要sudo** | 是 |
| **节点角色** | slave1(从节点1) |
| **CPU核心数** | 8核 |
| **内存容量** | 16GB |
| **磁盘容量** | 80GB |
| **部署的服务** | ujava2, utengine, umysql, redis-slave-1, redis-sentinel-2, uemqx, unacos, utracker, paperless |
| **特殊说明** | 从节点1 |
### 节点3(工作节点 - worker)
| 配置项 | 值 |
|-------|-----------------------------------------------------------------------------------------------|
| **节点名称** | ARM集群节点3 |
| **IP地址** | 192.168.9.91 |
| **SSH端口** | 22 |
| **用户名** | openkylin |
| **密码** | Ubains@123 |
| **是否需要sudo** | 是 |
| **节点角色** | slave2(从节点2) |
| **CPU核心数** | 8核 |
| **内存容量** | 16GB |
| **磁盘容量** | 80GB |
| **部署的服务** | ujava2, utengine, umysql, redis-slave-2, redis-sentinel-3, uemqx, unacos, utracker, paperless |
| **特殊说明** | 从节点2 |
### 节点4(工作节点 - worker)
| 配置项 | 值 |
|-------|----------------------|
| **节点名称** | ARM集群节点4 |
| **IP地址** | 192.168.9.92 |
| **SSH端口** | 22 |
| **用户名** | openkylin |
| **密码** | Ubains@123 |
| **是否需要sudo** | 是 |
| **节点角色** | slave3(从节点3) |
| **CPU核心数** | 8核 |
| **内存容量** | 8GB |
| **磁盘容量** | 60GB |
| **部署的服务** | dm8-server, utengine |
| **特殊说明** | 达梦数据库存储使用 |
## 集群服务拓扑
### 服务分布矩阵
| 服务名称 | 节点1 | 节点2 | 节点3 | 节点4 | 说明 |
|---------|-------|-------|-------|-------|------|
| ujava2 | ✅ | ✅ | ✅ | ✅ | Java合集容器 |
| unginx | ✅ | ✅ | ✅ | ✅ | Nginx反向代理 |
| umysql | ✅ | ❌ | ❌ | ❌ | MySQL数据库(主节点独占) |
| uredis | ✅ | ✅ | ❌ | ❌ | Redis缓存 |
| uemqx | ✅ | ✅ | ✅ | ❌ | MQTT消息队列 |
| unacos | ✅ | ❌ | ❌ | ❌ | Nacos配置中心(主节点独占) |
| utracker | ❌ | ❌ | ❌ | ✅ | FastDFS Tracker |
| ustorage | ❌ | ❌ | ❌ | ✅ | FastDFS Storage |
| upython | ✅ | ❌ | ❌ | ❌ | 运维集控CMDB |
> **说明**:✅表示该节点部署了该服务,❌表示未部署
### 服务端口映射
| 服务 | 容器端口 | 宿主机端口 | 说明 |
|------|---------|-----------|------|
| ujava2 | 8085, 30880-84, 9903-20 | 同容器端口 | Java合集服务 |
| unginx | 443 | 443 | HTTPS入口 |
| umysql | 3306 | 8306 | MySQL数据库 |
| uredis | 6379 | 6379 | Redis缓存 |
| uemqx | 1883, 8883, 8083, 18083 | 同容器端口 | MQTT服务+Dashboard |
| unacos | 8848, 9848 | 同容器端口 | Nacos控制台 |
| utracker | 22122 | 22122 | FastDFS Tracker |
| ustorage | 23000 | 23000 | FastDFS Storage |
| upython | 8000, 8002 | 同容器端口 | CMDB服务 |
## 集群中间件配置
### MySQL集群配置
| 配置项 | 值 |
|-------|-----|
| **主库节点** | 节点1(192.168.9.76) |
| **从库节点** | [如:无 / 节点X] |
| **数据库端口** | 8306 |
| **数据库名称** | [如:nacos_mysql, devops] |
| **用户名** | [如:root] |
| **密码** | [如:Ubains@123] |
| **主从同步状态** | [如:无主从 / 已配置主从] |
### Redis集群配置
| 配置项 | 值 |
|-------|-----|
| **部署节点** | 节点1, 节点2 |
| **运行模式** | [如:单机 / 主从 / 集群] |
| **端口** | 6379 |
| **密码** | [如:Ubains@123] |
| **持久化方式** | [如:RDB / AOF] |
### EMQX集群配置
| 配置项 | 值 |
|-------|-----|
| **部署节点** | 节点1, 节点2, 节点3 |
| **集群名称** | [如:emqx_cluster] |
| **MQTT端口** | 1883 |
| **MQTT SSL端口** | 8883 |
| **WebSocket端口** | 8083 |
| **Dashboard端口** | 18083 |
| **Dashboard用户名** | [如:admin] |
| **Dashboard密码** | [如:public] |
### Nacos配置
| 配置项 | 值 |
|-------|-----|
| **部署节点** | 节点1(主节点独占) |
| **控制台端口** | 8848 |
| **控制台用户名** | [如:nacos] |
| **控制台密码** | [如:nacos] |
| **数据库** | MySQL(nacos_mysql库) |
| **已知问题** | [如:控制台登录失败user not found] |
## 集群监控配置
### 系统资源告警阈值
| 监控项 | WARNING阈值 | CRITICAL阈值 |
|-------|-----------|-------------|
| CPU使用率 | 80% | 95% |
| 内存使用率 | 85% | 95% |
| 磁盘使用率 | 90% | 95% |
| 磁盘inode使用率 | 90% | 95% |
| 网络连接数 | >5000 | >8000 |
### 定时监控配置
| 配置项 | 值 |
|-------|-----|
| **监控时段** | 22:00 - 09:00(夜间) |
| **执行间隔** | 每2小时 |
| **执行时间点** | 22:00, 00:00, 02:00, 04:00, 06:00, 08:00 |
| **报告输出目录** | `AuxiliaryTool/ScriptTool/ServiceMonitor/reports/arm_cluster/` |
| **报告命名格式** | `arm_cluster_monitor_YYYYMMDD_HHMMSS.md` |
### 监控项目配置
| 监控项 | 是否启用 | 说明 |
|-------|---------|------|
| 系统资源监控 | ✅ | CPU/内存/磁盘/网络 |
| 容器状态监控 | ✅ | Docker容器运行状态 |
| 中间件功能验证 | ✅ | MySQL/Redis/EMQX/Nacos三层验证 |
| 日志分析 | ✅ | ERROR/WARN统计 |
| 集群一致性检查 | ✅ | 服务版本/配置一致性 |
| 集群连通性检查 | ✅ | 节点间网络连通性 |
## 集群已知问题
### 节点级已知问题
| 节点 | 服务 | 问题描述 | 原因 | 处置建议 |
|------|------|---------|------|---------|
| 节点1 | unacos | 控制台登录失败user not found | 部署改密破坏认证链 | 勿restart,业务面正常 |
| 节点1 | ujava2 | 启动后预定返回500 | Java启动约12分钟 | 等待就绪,勿反复restart |
| [待补充] | [待补充] | [待补充] | [待补充] | [待补充] |
### 集群级已知问题
| 问题类型 | 问题描述 | 影响范围 | 处置状态 |
|---------|---------|---------|---------|
| [待补充] | [待补充] | [待补充] | [待补充] |
## 集群网络配置
### 节点间网络拓扑
```
节点1 (192.168.9.89) - master
├── 节点2 (192.168.9.90) - worker
├── 节点3 (192.168.9.91) - worker
└── 节点4 (192.168.9.92) - worker
```
### 防火墙配置
| 端口 | 协议 | 用途 | 开放范围 |
|------|------|------|---------|
| 22 | TCP | SSH | 全节点互通 |
| 443 | TCP | HTTPS | 对外开放 |
| 8306 | TCP | MySQL | 集群内部 |
| 6379 | TCP | Redis | 集群内部 |
| 1883 | TCP | MQTT | 对外开放 |
| 8848 | TCP | Nacos | 集群内部 |
| 22122/23000 | TCP | FastDFS | 集群内部 |
## 集群维护记录
| 日期 | 维护内容 | 影响节点 | 维护人员 | 备注 |
|------|---------|---------|---------|------|
| [待补充] | [待补充] | [待补充] | [待补充] | [待补充] |
---
## 填写说明
1. **必填项**:节点IP、用户名、密码、节点角色
2. **选填项**:CPU核心数、内存容量、磁盘容量(用于资源监控)
3. **服务拓扑**:根据实际部署情况标注 ✅ 或 ❌
4. **已知问题**:记录已发现的非阻塞性问题,避免重复排查
5. **网络配置**:记录节点间网络拓扑和防火墙规则
6. **维护记录**:记录重大变更和故障处理过程
---
**配置文件更新后**,skill将自动读取最新的集群配置执行监控任务。
\ No newline at end of file
---
name: Cockpit-KVM-Deploy
description: CentOS 7服务器Cockpit虚拟化管理平台部署 - 修复yum源、安装Cockpit+插件、配置桥接网络、存储池,使服务器具备Web VM管理能力
---
CentOS 7 服务器上部署 Cockpit Web 虚拟化管理平台(功能对标 Proxmox VE),包括修复 yum 源、安装 Cockpit + cockpit-machines、配置桥接网络、配置存储池和防火墙。
## 使用场景
1. **新服务器**:在已有 CentOS 7 的物理服务器上快速搭建 Web 虚拟化管理平台
2. **迁移部署**:其他服务器需要相同能力时复用此 skill
3. **故障恢复**:服务异常后快速重建
## Usage
```
/Cockpit-KVM-Deploy <服务器IP> [root密码]
```
不传参数则交互确认服务器信息。默认参数:
- SSH 用户:`root`
- 密码:`Ubains@123`
- 服务器 IP:`192.168.5.3`
## 前置条件
| 条件 | 要求 |
|------|------|
| 操作系统 | CentOS 7 x86_64 |
| CPU | 支持 Intel VT-x / AMD-V(`vmx``svm` 标志) |
| 内存 | ≥ 4G(建议 ≥ 32G 以支持 VM) |
| 磁盘 | ≥ 20G 空闲 |
| 网络 | 至少一个物理网口,有静态 IP |
| KVM | `kvm``kvm_intel`/`kvm_amd` 模块已加载 |
## 部署架构
```
Cockpit Web 面板 (端口 9090)
├── cockpit-machines → VM 创建/管理/控制台
├── cockpit-storaged → 磁盘/存储池管理
└── cockpit-networkmanager → 网络配置
底层: libvirt + QEMU/KVM
网络: br0 桥接 (VM 直通物理网络)
存储: /var/lib/libvirt/images/
```
## 执行步骤
### Step 1:修复 yum 源
CentOS 7 EOL 后 mirrorlist 下线,必须改为 vault.centos.org 归档源。
```python
# 备份原 repo
cp /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.bak
# 写入 vault 源
cat > /etc/yum.repos.d/CentOS-Base.repo << 'REPOEOF'
[base]
name=CentOS-7 - Base
baseurl=http://vault.centos.org/centos/7/os/$basearch/
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7
[updates]
name=CentOS-7 - Updates
baseurl=http://vault.centos.org/centos/7/updates/$basearch/
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7
[extras]
name=CentOS-7 - Extras
baseurl=http://vault.centos.org/centos/7/extras/$basearch/
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7
REPOEOF
# 清理缓存
yum clean all && yum makecache
```
安装 EPEL(也需改 archives 源):
```bash
yum install -y epel-release
sed -i "s|^metalink=|#metalink=|g" /etc/yum.repos.d/epel*.repo
sed -i "s|^#baseurl=http://download.fedoraproject.org/pub/epel|baseurl=https://archives.fedoraproject.org/pub/archive/epel|g" /etc/yum.repos.d/epel.repo
```
### Step 2:安装 Cockpit + 插件
```bash
yum install -y cockpit cockpit-machines cockpit-storaged
systemctl enable --now cockpit.socket
```
Cockpit 是 socket-activated 的,`systemctl status cockpit` 显示 `inactive (dead)` 是正常的(有人访问才启动 woker)。
### Step 3:安装 virt-install(关键,缺了 Create VM 按钮灰色)
```bash
yum install -y virt-install
```
**这是最容易漏的步骤。** Cockpit 的 Create VM 功能依赖 `virt-install` 命令行工具,缺了按钮灰显、hover 提示 "virt-install not available"。
### Step 4:配置桥接网络
> ⚠️ **安全铁律**:只使用空闲网卡创建桥接,绝不动管理网卡!
1. 检查网卡状态:`nmcli connection show` + `ip link show`
2. 找空闲网卡(`state DOWN`、无 IP 的),将其物理接入交换机
3. 创建桥接:
```bash
# 创建桥接 br0(不配 IP,纯二层)
nmcli connection add type bridge autoconnect yes con-name br0 ifname br0 bridge.stp no
# 把空闲网卡加入桥接
nmcli connection modify <空闲网卡> connection.slave-type bridge master br0
# 启用
nmcli connection up br0
nmcli connection up <空闲网卡>
```
4. 验证:`brctl show br0` 应有对应 interface
### Step 5:配置防火墙和存储池
```bash
# 防火墙开放 Cockpit
firewall-cmd --permanent --add-service=cockpit 2>/dev/null
firewall-cmd --reload 2>/dev/null
# 配置 libvirt 默认存储池
virsh pool-define-as default dir --target /var/lib/libvirt/images
virsh pool-start default
virsh pool-autostart default
```
## 验证方法
```bash
# 1. Cockpit 服务
systemctl is-active cockpit.socket
# 2. 端口监听
ss -tlnp | grep 9090
# 3. Web 可达
curl -sk https://localhost:9090/ | head -3
# 4. bridge 正常
brctl show br0
# 5. libvirt 存储池
virsh pool-list --all
# 6. virt-install 可用
virt-install --version
```
浏览器打开 `https://<服务器IP>:9090`,用 root 登录后进入「虚拟机」页面,**Create VM 按钮应为可点击状态(非灰色)**
## 🔧 实战教训
### 教训1:eno1 被搞掉导致失联
**现象**:在 eno1(管理网卡)上创建桥接,`nmcli connection down eno1` 后 SSH 断开,br0 没 up,服务器彻底失联。
**根因**:动了正在使用的管理网卡。br0 上配静态 IP 和宿主网卡桥接需要精确的配置顺序,远程操作风险极高。
**对策**
- **永不碰管理网卡**:用空闲网卡做桥接
- **bro 不配 IP**:纯二层桥接,不设 IP/Gateway/DNS,降低配置复杂度
- **若只有一个网口**:先用 NAT(virbr0),后面再找机会加物理网口
### 教训2:virt-install 未安装 → Create VM 按钮灰色
**现象**:Cockpit 虚拟机页面 Create VM 按钮灰显,hover 提示 "virt-install not available"。
**根因**:CentOS 7 最小化安装不包含 `virt-install` 包,cockpit-machines 依赖它执行实际的 VM 创建。
**对策**`yum install -y virt-install`
### 教训3:EPEL 源也需改为 archives
**现象**`yum install epel-release``yum makecache` 报 "Cannot find a valid baseurl for repo: epel"。
**根因**:EPEL 7 的 mirrorlist 也随 CentOS 7 EOL 停止服务。
**对策**:EPEL 也需改为 `https://archives.fedoraproject.org/pub/archive/epel/7/$basearch/`
## 关联
- 操作指南文档:`Docs/虚拟机创建操作指南.md`
- 服务器参考:192.168.5.3(联想 ThinkServer TD340,125G/1.8T/8核)
- 网盘上传模块:`UploadSelfCheck`(可用于传递 ISO)
## 参考服务器
| 服务器 | IP | SSH | 用途 |
|--------|-----|-----|------|
| 默认目标 | 192.168.5.3 | root/Ubains@123 | 本次已部署 |
## 命令速查(paramiko Python 连接模板)
```python
import paramiko
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
client.connect('<服务器IP>', port=22, username='root', password='<密码>', timeout=10)
def run(cmd):
stdin, stdout, stderr = client.exec_command(cmd)
out = stdout.read().decode('utf-8', errors='replace')
err = stderr.read().decode('utf-8', errors='replace')
return out.strip(), err.strip()
```
---
name: X86-Cluster-Monitor
description: X86集群夜间监控 - 监测5网段四台X86架构服务器集群状态,定时执行(22:00-09:00每2小时),输出分析报告,以日期时间命名
tags: [监控, 集群, X86, 定时任务]
trigger: /X86-Cluster-Monitor
scheduled: true
schedule_window: "22:00-09:00"
schedule_interval: "2h"
---
# X86-Cluster-Monitor Skill
监测5网段X86架构四台服务器的集群服务状态,定时输出分析报告。
---
## 0. 执行规范(强制要求)
> ⚠️ 以下规范为强制要求,必须在每次执行时严格遵守。
### 0.1 语言规范
- **所有回复、代码注释、Git提交信息默认使用中文(简体)描述**
- 报告内容、日志输出、错误信息均使用中文
- 代码变量名、函数名可使用英文,但注释必须使用中文
### 0.2 SSH连接规范
连接服务器时必须遵守以下流程:
```
┌─────────────────────────────────────────────────────────────┐
│ SSH连接流程 │
├─────────────────────────────────────────────────────────────┤
│ 1. 检查本地免密配置 │
│ └─ 路径: ~/.ssh/keys/{服务器IP}/ │
│ │
│ 2. 如果存在免密配置 │
│ └─ 直接使用免密连接 │
│ │
│ 3. 如果不存在免密配置 │
│ ├─ 提示用户提供密码 │
│ └─ 连接成功后自动配置SSH免密 │
│ │
│ 4. 免密文件夹命名规则 │
│ └─ ~/.ssh/keys/192.168.5.41/ │
│ └─ ~/.ssh/keys/192.168.5.42/ │
│ └─ ~/.ssh/keys/192.168.5.43/ │
│ └─ ~/.ssh/keys/192.168.5.40/ │
│ │
│ 5. 使用Python paramiko库进行SSH连接 │
│ └─ 连接成功后生成免密公钥并上传到目标服务器 │
└─────────────────────────────────────────────────────────────┘
```
**免密配置要点**:
- 密钥路径: `~/.ssh/keys/{服务器IP}/id_rsa`
- 公钥路径: `~/.ssh/keys/{服务器IP}/id_rsa.pub`
- 连接成功后自动生成密钥对并上传公钥到目标服务器
### 0.3 上下文管理规范
- **自动压缩对话上下文**,当前模型上下文限制为200k tokens
- 当上下文接近限制时,自动触发压缩机制
- 压缩策略:保留关键检查结果、告警信息、报告摘要,删除中间执行日志
**上下文压缩触发条件**:
- 上下文使用超过 150k tokens 时触发压缩
- 每完成一次完整监控周期后自动压缩非必要信息
### 0.4 需求文档处理规范
- **每次理解需求文档,都需要输出《计划执行》文档**
- 计划执行文档需包含:执行目标、执行步骤、预期结果、风险评估
- 用户确认后方可执行
**计划执行文档模板**:
```markdown
# 计划执行文档
## 执行目标
[描述本次执行的目标]
## 执行步骤
1. [步骤1]
2. [步骤2]
...
## 预期结果
- [预期结果1]
- [预期结果2]
## 风险评估
| 风险项 | 影响 | 应对措施 |
|--------|------|---------|
| [风险1] | [高/中/低] | [应对方法] |
## 确认信息
请确认是否执行?(是/否)
```
---
## 1. Skill 概述
| 属性 | 值 |
|------|-----|
| **名称** | X86-Cluster-Monitor |
| **功能** | 监测5网段X86架构四台服务器集群状态 |
| **执行方式** | 定时执行(夜间) |
| **执行时间** | 22:00 - 09:00,每2小时执行一次 |
| **输出** | 分析报告(MD格式,以日期时间命名) |
---
## 2. 执行流程
```
┌─────────────────────────────────────────────────────────────┐
│ X86集群监控流程 │
├─────────────────────────────────────────────────────────────┤
│ 1. 读取集群配置 │
│ └─ 从 集群信息配置.md 读取四台服务器信息 │
│ │
│ 2. 并行连接四台服务器 │
│ ├─ 服务器1 SSH连接 → 执行检查 │
│ ├─ 服务器2 SSH连接 → 执行检查 │
│ ├─ 服务器3 SSH连接 → 执行检查 │
│ └─ 服务器4 SSH连接 → 执行检查 │
│ │
│ 3. 每台服务器执行检查项 │
│ ├─ 系统资源: CPU/内存/磁盘/网络 │
│ ├─ 容器状态: 运行状态/重启次数/资源使用 │
│ ├─ 中间件: MySQL/Redis/EMQX/Nacos/FastDFS │
│ ├─ Java服务: 进程存活/日志ERROR │
│ └─ 业务接口: HTTP可访问性 │
│ │
│ 4. 集群状态分析 │
│ ├─ 汇总四台服务器状态 │
│ ├─ 分析集群整体健康度 │
│ ├─ 识别异常和告警 │
│ └─ 生成趋势分析(如有历史数据) │
│ │
│ 5. 生成报告 │
│ ├─ 控制台输出摘要 │
│ └─ 保存MD报告到指定目录 │
└─────────────────────────────────────────────────────────────┘
```
---
## 3. 检查项详情
### 3.1 系统资源检查
```bash
# CPU使用率
top -bn1 | grep "Cpu(s)" | awk '{print $2}' | cut -d'%' -f1
# 内存使用率
free -m | awk 'NR==2{printf "%.2f%%", $3*100/$2}'
# 磁盘使用率
df -h | grep -E '^/dev' | awk '{print $1, $5, $6}'
# 系统负载
uptime | awk -F'load average:' '{print $2}'
# 网络连接数
netstat -an | grep ESTABLISHED | wc -l
```
### 3.2 Docker容器检查
```bash
# 获取所有容器状态
docker ps -a --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
# 容器资源使用
docker stats --no-stream --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}"
# 检查特定容器
for container in ujava2 unginx umysql uredis uemqx unacos; do
docker inspect --format='{{.State.Status}}' $container 2>/dev/null || echo "$container: NOT_FOUND"
done
```
### 3.3 中间件功能检查
**MySQL**:
```bash
# 连接测试
docker exec umysql mysql -uroot -p'密码' -e "SELECT 1;"
# 数据库大小
docker exec umysql mysql -uroot -p'密码' -e "SELECT table_schema, ROUND(SUM(data_length+index_length)/1024/1024, 2) AS 'Size_MB' FROM information_schema.tables GROUP BY table_schema;"
```
**Redis**:
```bash
# ping测试
docker exec uredis redis-cli -a '密码' ping
# 信息获取
docker exec uredis redis-cli -a '密码' info | grep -E 'used_memory_human|connected_clients|db\d+:keys'
```
**EMQX**:
```bash
# 状态检查
docker exec uemqx emqx_ctl status
# 连接数
docker exec uemqx emqx_ctl listeners
# 订阅数
curl -s -u admin:public http://localhost:18083/api/v5/subscriptions | jq '.data | length'
```
**Nacos**:
```bash
# 服务状态
curl -s http://localhost:8848/nacos/v1/console/server/state
# 注册服务数
curl -s http://localhost:8848/nacos/v1/ns/service/list | jq '.count'
```
### 3.4 Java进程检查
```bash
# 检查Java进程
ps aux | grep java | grep -v grep
# 检查关键jar进程
for jar in "ubains-auth" "ubains-gateway" "ubains-meeting"; do
count=$(ps aux | grep "$jar" | grep -v grep | wc -l)
echo "$jar: $count 进程"
done
# 日志ERROR扫描
find /data/services/api -name "*.log" -mmin -120 -exec grep -l "ERROR\|Exception" {} \; | head -10
```
### 3.5 业务接口检查
```bash
# 前台页面
curl -k -s -o /dev/null -w "%{http_code}" https://localhost/
# 后台管理
curl -k -s -o /dev/null -w "%{http_code}" https://localhost/pc-vue2-backstage/
# 业务接口
curl -k -s https://localhost/meetingV3/api/systemConfiguration/globalConfig
```
---
## 4. 报告格式
### 4.1 文件命名
```
cluster_monitor_20260709_220000.md
cluster_monitor_20260710_000000.md
cluster_monitor_20260710_020000.md
...
```
### 4.2 报告结构
```markdown
# X86集群监控报告
**生成时间**: 2026-07-09 22:00:00
**监控范围**: 5网段X86架构四台服务器集群
**执行周期**: 夜间22:00-09:00,每2小时
---
## 执行摘要
| 指标 | 值 |
|------|-----|
| 总体状态 | 🟢 正常 / 🟡 警告 / 🔴 异常 |
| 监控服务器数 | 4 |
| 容器总数 | XX |
| 异常容器数 | XX |
| 告警项数 | XX |
---
## 服务器状态汇总
| 服务器 | IP | 状态 | CPU | 内存 | 磁盘 | 容器异常 | 告警 |
|--------|-----|------|-----|------|------|---------|------|
| 服务器1 | 192.168.5.xx | 🟢 | 15% | 45% | 60% | 0 | 0 |
| 服务器2 | 192.168.5.xx | 🟡 | 25% | 78% | 85% | 1 | 2 |
| 服务器3 | 192.168.5.xx | 🟢 | 12% | 52% | 55% | 0 | 0 |
| 服务器4 | 192.168.5.xx | 🟢 | 18% | 48% | 62% | 0 | 0 |
---
## 详细检查结果
### 服务器1 (192.168.5.xx)
#### 系统资源
- CPU: 15% (正常)
- 内存: 45% (正常)
- 磁盘: / 60%, /data 55% (正常)
#### 容器状态
| 容器 | 状态 | CPU | 内存 | 重启次数 |
|------|------|-----|------|---------|
| ujava2 | running | 8% | 2.1GB/4GB | 0 |
| unginx | running | 0.5% | 50MB | 0 |
| umysql | running | 12% | 1.5GB | 0 |
| ... | ... | ... | ... | ... |
#### 中间件检查
- MySQL: ✅ 连接正常,数据库总大小 XX MB
- Redis: ✅ ping正常,key数 XXX
- EMQX: ✅ 运行正常,连接数 XX
- Nacos: ✅ 注册服务数 XX
#### Java服务检查
- ubains-auth: ✅ 运行中
- ubains-gateway: ✅ 运行中
- ubains-meeting: ✅ 运行中
#### 日志分析
- ERROR日志: 最近2小时 XX 条
- 关键异常: 无
---
### 服务器2 (192.168.5.xx)
(同上格式)
---
### 服务器3 (192.168.5.xx)
(同上格式)
---
### 服务器4 (192.168.5.xx)
(同上格式)
---
## 集群整体分析
### 集群健康度评分
- 服务器可用性: 100% (4/4)
- 容器健康度: 98% (XX/XX)
- 中间件状态: 100%
- 业务接口: 100%
### 发现的问题
#### 🟡 警告
1. 服务器2 /data 磁盘使用率 85%,建议清理
2. 服务器3 uemqx 近期重启过,需关注
#### 🔴 异常
(无)
### 趋势分析
(如有历史数据,展示趋势对比)
---
## 建议
1. [建议项1]
2. [建议项2]
---
## 附录
### 检查时间
- 开始时间: 2026-07-09 22:00:00
- 结束时间: 2026-07-09 22:02:35
- 执行耗时: 2分35秒
### 报告路径
`AuxiliaryTool/ScriptTool/ClusterMonitor/reports/cluster_monitor_20260709_220000.md`
```
---
## 5. 定时任务配置
### 5.1 Cron表达式
夜间22:00-09:00,每2小时执行一次:
```cron
0 22,0,2,4,6,8 * * *
```
**执行时间点**:
- 22:00
- 00:00
- 02:00
- 04:00
- 06:00
- 08:00
### 5.2 Claude Code内置定时
使用Claude Code的CronCreate工具创建定时任务:
```json
{
"cron": "0 22,0,2,4,6,8 * * *",
"prompt": "/X86-Cluster-Monitor",
"recurring": true,
"durable": true
}
```
---
## 6. 配置文件
集群配置从同级目录的 `集群信息配置.md` 读取,请先填写该配置文档。
---
## 7. 使用方法
### 手动执行
```
/X86-Cluster-Monitor
```
### 定时执行
skill已配置定时任务,将在指定时间自动执行。
### 查看报告
报告保存在:
```
AuxiliaryTool/ScriptTool/ClusterMonitor/reports/
```
---
## 8. 前置条件
1. **配置文件**: 必须先填写 `集群信息配置.md`
2. **SSH访问**: 确保能SSH连接到四台服务器
3. **凭据正确**: MySQL/Redis/EMQX等中间件凭据必须正确
4. **网络连通**: 执行机器需能访问5网段
---
## 9. 异常处理
| 异常情况 | 处理方式 |
|---------|---------|
| SSH连接失败 | 记录异常,跳过该服务器,继续检查其他服务器 |
| 容器不存在 | 记录为异常,检查该服务器配置是否正确 |
| 中间件凭据错误 | 记录警告,跳过该中间件功能检查 |
| 日志目录不存在 | 记录警告,跳过日志检查 |
---
## 10. 注意事项
1. **执行时段**: 仅在夜间22:00-09:00执行,避免影响业务高峰期
2. **并行执行**: 四台服务器并行检查,减少总执行时间
3. **资源消耗**: 检查操作轻量,不会对服务器造成明显负载
4. **报告保留**: 默认保留30天,可根据需要调整
---
## 更新记录
| 日期 | 版本 | 更新内容 |
|------|------|---------|
| 2026-07-09 | v1.0 | 初始版本,创建skill骨架 |
\ No newline at end of file
{
"_comment": "=== X86集群监控配置 === 麒麟V10三机热备集群(节点1-3)+ 达梦数据库节点(节点4)",
"_updated_at": "2026-07-09",
"cluster_info": {
"name": "新统一平台X86集群",
"architecture": "X86",
"os": "麒麟V10",
"mode": "三机热备",
"purpose": "测试环境",
"node_count": 4
},
"servers": [
{
"id": 1,
"name": "X86集群节点1",
"ip": "192.168.5.41",
"port": 22,
"username": "openkylin",
"password": "Ubains@123",
"use_sudo": true,
"role": "master",
"cpu_cores": 8,
"memory_gb": 16,
"disk_gb": 80,
"containers": ["ujava2", "utengine", "umysql", "redis-master", "redis-sentinel", "uemqx", "unacos", "utracker", "paperless"],
"services": {
"mysql": {"port": 8306, "type": "master"},
"redis": {"port": 6379, "type": "master"},
"emqx": {"port": 1883, "dashboard_port": 18083},
"nacos": {"port": 8848}
},
"note": "主节点,承担核心业务"
},
{
"id": 2,
"name": "X86集群节点2",
"ip": "192.168.5.42",
"port": 22,
"username": "openkylin",
"password": "Ubains@123",
"use_sudo": true,
"role": "slave1",
"cpu_cores": 8,
"memory_gb": 16,
"disk_gb": 80,
"containers": ["ujava2", "utengine", "umysql", "redis-slave-1", "redis-sentinel-2", "uemqx", "unacos", "utracker", "paperless"],
"services": {
"redis": {"port": 6379, "type": "slave"}
},
"note": "从节点1"
},
{
"id": 3,
"name": "X86集群节点3",
"ip": "192.168.5.43",
"port": 22,
"username": "openkylin",
"password": "Ubains@123",
"use_sudo": true,
"role": "slave2",
"cpu_cores": 8,
"memory_gb": 16,
"disk_gb": 80,
"containers": ["ujava2", "utengine", "umysql", "redis-slave-2", "redis-sentinel-3", "uemqx", "unacos", "utracker", "paperless"],
"services": {
"redis": {"port": 6379, "type": "slave"}
},
"note": "从节点2"
},
{
"id": 4,
"name": "X86集群节点4",
"ip": "192.168.5.40",
"port": 22,
"username": "openkylin",
"password": "Ubains@123",
"use_sudo": true,
"role": "dm8-server",
"cpu_cores": 8,
"memory_gb": 8,
"disk_gb": 60,
"containers": ["dm8-server", "utengine"],
"services": {
"dm8": {"port": 5236}
},
"note": "达梦数据库存储使用"
}
],
"cluster_services": {
"redis_cluster": {
"mode": "1主2从3哨兵",
"master": "192.168.5.41",
"slaves": ["192.168.5.42", "192.168.5.43"],
"sentinels": ["192.168.5.41", "192.168.5.42", "192.168.5.43"],
"password": "Ubains@123"
},
"emqx_cluster": {
"mode": "3节点集群",
"nodes": ["192.168.5.41", "192.168.5.42", "192.168.5.43"],
"dashboard_username": "admin",
"dashboard_password": "public"
},
"nacos_cluster": {
"mode": "3节点集群",
"nodes": ["192.168.5.41", "192.168.5.42", "192.168.5.43"],
"console_username": "nacos",
"console_password": "nacos"
},
"fastdfs_cluster": {
"tracker_nodes": ["192.168.5.41", "192.168.5.42", "192.168.5.43"],
"storage_nodes": ["192.168.5.41", "192.168.5.42", "192.168.5.43"]
}
},
"middleware_credentials": {
"mysql": {
"username": "root",
"password": "Ubains@123",
"port": 8306
},
"redis": {
"password": "Ubains@123",
"port": 6379
},
"emqx": {
"dashboard_username": "admin",
"dashboard_password": "public",
"dashboard_port": 18083
},
"nacos": {
"username": "nacos",
"password": "nacos",
"port": 8848
}
},
"thresholds": {
"cpu_warning": 80,
"cpu_critical": 95,
"memory_warning": 85,
"memory_critical": 95,
"disk_warning": 90,
"disk_critical": 95,
"inode_warning": 90,
"container_restart_warning": 3,
"container_restart_critical": 5,
"log_error_warning": 10,
"log_error_critical": 50
},
"schedule": {
"window": "22:00-09:00",
"interval": "2h",
"cron": "0 22,0,2,4,6,8 * * *",
"execution_times": ["22:00", "00:00", "02:00", "04:00", "06:00", "08:00"]
},
"report": {
"output_dir": "AuxiliaryTool/ScriptTool/ClusterMonitor/reports",
"filename_format": "cluster_monitor_{YYYYMMDD_HHMMSS}.md",
"history_days": 30
}
}
\ No newline at end of file
# X86集群信息配置表
> 本文件用于配置5网段四台X86架构服务器集群的详细信息,供 `X86-CLUSTER-MONITOR` skill 使用。
## 集群概述
- **集群名称**:新统一平台X86集群
- **集群架构**:X86架构
- **操作系统**:麒麟V10
- **节点数量**:4台
- **集群用途**:测试环境
- **集群模式**:三机热备
- **Redis集群服务说明**:采用1主2从3哨兵模式
- **FastDFS集群服务说明**:采用3个节点部署FastDFS集群
- **Nacos集群服务说明**:采用3节点部署Nacos集群
- **Emqx集群服务说明**:采用3个节点部署Emqx集群
## 节点配置信息
### 节点1(主节点 - master)
| 配置项 | 值 |
|-------|---------------------------------------------------------------------------------|
| **节点名称** | X86集群节点1 |
| **IP地址** | 192.168.5.41 |
| **SSH端口** | 22 |
| **用户名** | openkylin |
| **密码** | Ubains@123 |
| **是否需要sudo** | 是 |
| **节点角色** | master(主节点) |
| **CPU核心数** | 8核 |
| **内存容量** | 16GB |
| **磁盘容量** | 80GB |
| **部署的服务** | ujava2, utengine, umysql, redis-master, redis-sentinel, uemqx, unacos, utracker, paperless |
| **特殊说明** | 主节点,承担核心业务 |
### 节点2(工作节点 - worker)
| 配置项 | 值 |
|-------|-----------------------------------------------------------------------------------------------|
| **节点名称** | X86集群节点2 |
| **IP地址** | 192.168.5.42 |
| **SSH端口** | 22 |
| **用户名** | openkylin |
| **密码** | Ubains@123 |
| **是否需要sudo** | 是 |
| **节点角色** | slave1(从节点1) |
| **CPU核心数** | 8核 |
| **内存容量** | 16GB |
| **磁盘容量** | 80GB |
| **部署的服务** | ujava2, utengine, umysql, redis-slave-1, redis-sentinel-2, uemqx, unacos, utracker, paperless |
| **特殊说明** | 从节点1 |
### 节点3(工作节点 - worker)
| 配置项 | 值 |
|-------|-----------------------------------------------------------------------------------------------|
| **节点名称** | X86集群节点3 |
| **IP地址** | 192.168.5.43 |
| **SSH端口** | 22 |
| **用户名** | openkylin |
| **密码** | Ubains@123 |
| **是否需要sudo** | 是 |
| **节点角色** | slave2(从节点2) |
| **CPU核心数** | 8核 |
| **内存容量** | 16GB |
| **磁盘容量** | 80GB |
| **部署的服务** | ujava2, utengine, umysql, redis-slave-2, redis-sentinel-3, uemqx, unacos, utracker, paperless |
| **特殊说明** | 从节点2 |
### 节点4(工作节点 - worker)
| 配置项 | 值 |
|-------|----------------------|
| **节点名称** | X86集群节点4 |
| **IP地址** | 192.168.5.40 |
| **SSH端口** | 22 |
| **用户名** | openkylin |
| **密码** | Ubains@123 |
| **是否需要sudo** | 是 |
| **节点角色** | slave3(从节点3) |
| **CPU核心数** | 8核 |
| **内存容量** | 8GB |
| **磁盘容量** | 60GB |
| **部署的服务** | dm8-server, utengine |
| **特殊说明** | 达梦数据库存储使用 |
## 集群服务拓扑
### 服务分布矩阵
| 服务名称 | 节点1 | 节点2 | 节点3 | 节点4 | 说明 |
|---------|-------|-------|-------|-------|------|
| ujava2 | ✅ | ✅ | ✅ | ✅ | Java合集容器 |
| unginx | ✅ | ✅ | ✅ | ✅ | Nginx反向代理 |
| umysql | ✅ | ❌ | ❌ | ❌ | MySQL数据库(主节点独占) |
| uredis | ✅ | ✅ | ❌ | ❌ | Redis缓存 |
| uemqx | ✅ | ✅ | ✅ | ❌ | MQTT消息队列 |
| unacos | ✅ | ❌ | ❌ | ❌ | Nacos配置中心(主节点独占) |
| utracker | ❌ | ❌ | ❌ | ✅ | FastDFS Tracker |
| ustorage | ❌ | ❌ | ❌ | ✅ | FastDFS Storage |
| upython | ✅ | ❌ | ❌ | ❌ | 运维集控CMDB |
> **说明**:✅表示该节点部署了该服务,❌表示未部署
### 服务端口映射
| 服务 | 容器端口 | 宿主机端口 | 说明 |
|------|---------|-----------|------|
| ujava2 | 8085, 30880-84, 9903-20 | 同容器端口 | Java合集服务 |
| unginx | 443 | 443 | HTTPS入口 |
| umysql | 3306 | 8306 | MySQL数据库 |
| uredis | 6379 | 6379 | Redis缓存 |
| uemqx | 1883, 8883, 8083, 18083 | 同容器端口 | MQTT服务+Dashboard |
| unacos | 8848, 9848 | 同容器端口 | Nacos控制台 |
| utracker | 22122 | 22122 | FastDFS Tracker |
| ustorage | 23000 | 23000 | FastDFS Storage |
| upython | 8000, 8002 | 同容器端口 | CMDB服务 |
## 集群中间件配置
### MySQL集群配置
| 配置项 | 值 |
|-------|-----|
| **主库节点** | 节点1(192.168.9.76) |
| **从库节点** | [如:无 / 节点X] |
| **数据库端口** | 8306 |
| **数据库名称** | [如:nacos_mysql, devops] |
| **用户名** | [如:root] |
| **密码** | [如:Ubains@123] |
| **主从同步状态** | [如:无主从 / 已配置主从] |
### Redis集群配置
| 配置项 | 值 |
|-------|-----|
| **部署节点** | 节点1, 节点2 |
| **运行模式** | [如:单机 / 主从 / 集群] |
| **端口** | 6379 |
| **密码** | [如:Ubains@123] |
| **持久化方式** | [如:RDB / AOF] |
### EMQX集群配置
| 配置项 | 值 |
|-------|-----|
| **部署节点** | 节点1, 节点2, 节点3 |
| **集群名称** | [如:emqx_cluster] |
| **MQTT端口** | 1883 |
| **MQTT SSL端口** | 8883 |
| **WebSocket端口** | 8083 |
| **Dashboard端口** | 18083 |
| **Dashboard用户名** | [如:admin] |
| **Dashboard密码** | [如:public] |
### Nacos配置
| 配置项 | 值 |
|-------|-----|
| **部署节点** | 节点1(主节点独占) |
| **控制台端口** | 8848 |
| **控制台用户名** | [如:nacos] |
| **控制台密码** | [如:nacos] |
| **数据库** | MySQL(nacos_mysql库) |
| **已知问题** | [如:控制台登录失败user not found] |
## 集群监控配置
### 系统资源告警阈值
| 监控项 | WARNING阈值 | CRITICAL阈值 |
|-------|-----------|-------------|
| CPU使用率 | 80% | 95% |
| 内存使用率 | 85% | 95% |
| 磁盘使用率 | 90% | 95% |
| 磁盘inode使用率 | 90% | 95% |
| 网络连接数 | >5000 | >8000 |
### 定时监控配置
| 配置项 | 值 |
|-------|-----|
| **监控时段** | 22:00 - 09:00(夜间) |
| **执行间隔** | 每2小时 |
| **执行时间点** | 22:00, 00:00, 02:00, 04:00, 06:00, 08:00 |
| **报告输出目录** | `AuxiliaryTool/ScriptTool/ServiceMonitor/reports/X86_cluster/` |
| **报告命名格式** | `X86_cluster_monitor_YYYYMMDD_HHMMSS.md` |
### 监控项目配置
| 监控项 | 是否启用 | 说明 |
|-------|---------|------|
| 系统资源监控 | ✅ | CPU/内存/磁盘/网络 |
| 容器状态监控 | ✅ | Docker容器运行状态 |
| 中间件功能验证 | ✅ | MySQL/Redis/EMQX/Nacos三层验证 |
| 日志分析 | ✅ | ERROR/WARN统计 |
| 集群一致性检查 | ✅ | 服务版本/配置一致性 |
| 集群连通性检查 | ✅ | 节点间网络连通性 |
## 集群已知问题
### 节点级已知问题
| 节点 | 服务 | 问题描述 | 原因 | 处置建议 |
|------|------|---------|------|---------|
| 节点1 | unacos | 控制台登录失败user not found | 部署改密破坏认证链 | 勿restart,业务面正常 |
| 节点1 | ujava2 | 启动后预定返回500 | Java启动约12分钟 | 等待就绪,勿反复restart |
| [待补充] | [待补充] | [待补充] | [待补充] | [待补充] |
### 集群级已知问题
| 问题类型 | 问题描述 | 影响范围 | 处置状态 |
|---------|---------|---------|---------|
| [待补充] | [待补充] | [待补充] | [待补充] |
## 集群网络配置
### 节点间网络拓扑
```
节点1 (192.168.9.41) - master
├── 节点2 (192.168.9.42) - worker
├── 节点3 (192.168.9.43) - worker
└── 节点4 (192.168.9.40) - worker
```
### 防火墙配置
| 端口 | 协议 | 用途 | 开放范围 |
|------|------|------|---------|
| 22 | TCP | SSH | 全节点互通 |
| 443 | TCP | HTTPS | 对外开放 |
| 8306 | TCP | MySQL | 集群内部 |
| 6379 | TCP | Redis | 集群内部 |
| 1883 | TCP | MQTT | 对外开放 |
| 8848 | TCP | Nacos | 集群内部 |
| 22122/23000 | TCP | FastDFS | 集群内部 |
## 集群维护记录
| 日期 | 维护内容 | 影响节点 | 维护人员 | 备注 |
|------|---------|---------|---------|------|
| [待补充] | [待补充] | [待补充] | [待补充] | [待补充] |
---
## 填写说明
1. **必填项**:节点IP、用户名、密码、节点角色
2. **选填项**:CPU核心数、内存容量、磁盘容量(用于资源监控)
3. **服务拓扑**:根据实际部署情况标注 ✅ 或 ❌
4. **已知问题**:记录已发现的非阻塞性问题,避免重复排查
5. **网络配置**:记录节点间网络拓扑和防火墙规则
6. **维护记录**:记录重大变更和故障处理过程
---
**配置文件更新后**,skill将自动读取最新的集群配置执行监控任务。
\ No newline at end of file
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
阶段4补充:下载激活文件 - 获取当前服务器机器码
"""
import time, os
from playwright.sync_api import sync_playwright
HOST = "192.168.5.70"
ADMIN_USER = "superadmin"
ADMIN_PASS = "Ubains@1357"
CAPTCHA = "csba"
SCREEN_DIR = r"E:\GithubData\ubains-module-test\AuxiliaryTool\ScriptTool\RemoteDeploy\screenshots"
os.makedirs(SCREEN_DIR, exist_ok=True)
def log(msg):
try:
print(f"[{time.strftime('%H:%M:%S')}] {msg}", flush=True)
except:
print(f"[{time.strftime('%H:%M:%S')}] {msg.encode('gbk','replace').decode('gbk')}", flush=True)
def js_click(page, el, desc=""):
try:
el.evaluate("el => el.click()")
log(f" JS-click: {desc}")
return True
except Exception as e:
log(f" Click-fail: {desc} - {e}")
return False
def ss(page, name):
page.screenshot(path=os.path.join(SCREEN_DIR, f"dl_{name}.png"))
def main():
log("========== Download Activation File ==========")
with sync_playwright() as p:
browser = p.chromium.launch(headless=False, args=['--ignore-certificate-errors'])
context = browser.new_context(ignore_https_errors=True, viewport={'width': 1920, 'height': 1080})
page = context.new_page()
page.set_default_timeout(20000)
try:
# Login
log("Step 1: Login")
page.goto(f"https://{HOST}/#/LoginConfig", timeout=60000, wait_until='domcontentloaded')
page.wait_for_timeout(5000)
if 'backend' not in page.url:
inputs = page.query_selector_all('input')
if len(inputs) >= 1:
inputs[0].fill(ADMIN_USER)
if len(inputs) >= 2:
inputs[1].fill(ADMIN_PASS)
if len(inputs) >= 3:
inputs[2].fill(CAPTCHA)
page.wait_for_timeout(500)
submit = page.query_selector('input[type="submit"]')
if submit:
js_click(page, submit, "Login")
page.wait_for_timeout(12000)
log(f" After login: {page.url}")
ss(page, "01_logged_in")
page.wait_for_timeout(3000)
# Click "下载激活文件"
log("Step 2: Click 'Download Activation File'")
download_btn = page.query_selector('button:has-text("下载激活文件")')
if download_btn:
js_click(page, download_btn, "Download activation file")
page.wait_for_timeout(3000)
ss(page, "02_dialog")
# Fill identity verification dialog
log(" Filling identity verification dialog...")
pwd = page.query_selector('.el-dialog__wrapper:not([style*="display: none"]) input[type="password"]')
if pwd:
pwd.fill(ADMIN_PASS)
log(" Password filled")
for ci in page.query_selector_all('.el-dialog__wrapper:not([style*="display: none"]) input[type="text"]'):
if ci.get_attribute('disabled') is not None:
continue
ph = ci.get_attribute('placeholder') or ''
if '验证' in ph:
ci.fill(CAPTCHA)
log(" Captcha filled")
break
ss(page, "03_filled")
# Set up download listener
log(" Setting up download listener...")
try:
with page.expect_download(timeout=15000) as download_info:
confirm = page.query_selector('.el-dialog__wrapper:not([style*="display: none"]) button:has-text("确定")')
if not confirm:
confirm = page.query_selector('.el-dialog__wrapper:not([style*="display: none"]) .el-button--primary')
if confirm:
js_click(page, confirm, "Confirm download")
else:
page.keyboard.press('Enter')
download = download_info.value
download_path = os.path.join(SCREEN_DIR, "activation_file.zip")
download.save_as(download_path)
log(f" ✅ Activation file downloaded to: {download_path}")
log(f" Filename: {download.suggested_filename}")
except Exception as e:
log(f" ⚠️ Download not triggered: {e}")
page.wait_for_timeout(5000)
ss(page, "04_after_download_attempt")
# Check for any dialog/message
try:
msgs = page.query_selector_all('.el-message, .el-notification')
for m in msgs:
txt = (m.inner_text() or '')[:300]
if txt.strip():
log(f" Message: {txt}")
except:
pass
else:
log(" ❌ 'Download Activation File' button not found!")
ss(page, "99_final")
except Exception as e:
log(f"ERROR: {e}")
ss(page, "99_error")
import traceback
traceback.print_exc()
finally:
page.wait_for_timeout(2000)
browser.close()
if __name__ == '__main__':
main()
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
ARM麒麟V10部署脚本包装器
严格按照需求文档执行部署操作
"""
import paramiko
import time
import sys
def deploy_arm_kylin():
"""执行ARM麒麟V10部署"""
# SSH连接配置
print('=' * 60)
print('ARM麒麟V10自动化部署开始')
print('目标服务器: 192.168.9.70')
print('部署时间: 2026-06-30')
print('=' * 60)
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
try:
print('\n[步骤1] SSH连接服务器...')
ssh.connect('192.168.9.70', username='openkylin', password='Ubains@123', timeout=30)
print('连接成功')
print('\n[步骤2] 切换到root用户...')
channel = ssh.invoke_shell()
time.sleep(1)
# 切换到root
channel.send('sudo su -\n')
time.sleep(2)
channel.send('Ubains@123\n')
time.sleep(2)
# 设置环境变量
print('\n[步骤3] 设置TERM=dumb环境变量...')
channel.send('export TERM=dumb\n')
time.sleep(1)
# 进入部署目录
print('\n[步骤4] 进入部署目录...')
channel.send('cd /data/arm_offline_auto_unifiedPlatform\n')
time.sleep(1)
# 执行部署脚本
print('\n[步骤5] 开始执行部署脚本(预计40分钟)...')
print('自动应答: 7个y + 1个n(无企业NTP服务器)')
# 使用管道输入自动应答
channel.send('printf \'y\\ny\\ny\\ny\\ny\\ny\\ny\\nn\\n\' | ./arm_new_auto.sh --all\n')
# 监控部署进度
start_time = time.time()
last_output_time = start_time
deployment_complete = False
print('\n部署进度监控:')
print('-' * 60)
while True:
if channel.recv_ready():
output = channel.recv(4096).decode('utf-8', errors='ignore')
# 过滤掉一些无关的ANSI控制字符
output_clean = output.replace('[?2004h', '').replace('[?2004l', '')
# 输出关键信息
for line in output_clean.split('\n'):
if line.strip() and (
'执行' in line or
'完成' in line or
'部署' in line or
'成功' in line or
'失败' in line or
'错误' in line or
'source /etc/profile' in line or
'whiptail' in line.lower()
):
print(f'[{time.strftime("%H:%M:%S")}] {line.strip()}')
# 检查是否完成
if 'source /etc/profile' in output or '部署完成' in output:
deployment_complete = True
print('\n' + '=' * 60)
print('部署脚本执行完成!')
print('=' * 60)
break
last_output_time = time.time()
# 每5分钟报告一次进度
current_time = time.time()
elapsed = current_time - start_time
if int(elapsed) % 300 == 0 and elapsed > 0:
minutes = int(elapsed / 60)
print(f'[{time.strftime("%H:%M:%S")}] 已运行 {minutes} 分钟...')
# 检查是否超时(60分钟)
if elapsed > 3600:
print('\n警告: 部署脚本执行超过60分钟,可能存在问题')
break
# 检查是否长时间无输出(超过5分钟)
if current_time - last_output_time > 300:
print(f'\n警告: 超过5分钟无输出,可能脚本卡住')
break
time.sleep(2)
if deployment_complete:
print('\n[步骤6] 执行 source /etc/profile...')
channel.send('source /etc/profile\n')
time.sleep(2)
print('\n[步骤7] 检查容器状态...')
channel.send('docker ps --format "table {{.Names}}\\t{{.Status}}"\n')
time.sleep(3)
output = channel.recv(65535).decode('utf-8', errors='ignore')
print('容器状态:')
print(output)
# 统计容器数量
container_count = len([line for line in output.split('\n') if 'Running' in line])
print(f'\n正在运行的容器数量: {container_count}')
if container_count >= 11:
print('[OK] 容器状态正常(预期11个容器全部Running)')
else:
print(f'[WARN] 容器数量不足(预期11个,实际{container_count}个)')
print('\n部署阶段完成!')
print('总耗时:', f'{int(elapsed / 60)} 分钟')
channel.close()
except Exception as e:
print(f'\n部署过程出错: {e}')
import traceback
traceback.print_exc()
return False
finally:
ssh.close()
return True
if __name__ == '__main__':
success = deploy_arm_kylin()
sys.exit(0 if success else 1)
\ No newline at end of file
# Cockpit 虚拟机创建操作指南
> 服务器:192.168.5.3 | Cockpit 面板:https://192.168.5.3:9090
---
## 一、登录 Cockpit
1. 浏览器打开 **https://192.168.5.3:9090**
2. 点击「高级」→「继续访问」(证书是自签名的,忽略警告即可)
3. 输入用户名和密码:
| 字段 | 值 |
|------|-----|
| 用户名 | `root` |
| 密码 | `Ubains@123` |
4. 勾选「重用我的密码以执行特权任务」→ 点击「登录」
---
## 二、上传 ISO 镜像
> Cockpit Web 界面不支持上传文件,需要用 WinSCP 或 scp 将 ISO 传到服务器。
**目标路径:** `/var/lib/libvirt/images/`
**scp 方式(在你自己电脑的终端执行):**
```bash
scp CentOS-7-x86_64-Minimal-2009.iso root@192.168.5.3:/var/lib/libvirt/images/
```
**WinSCP 方式:**
- 主机:`192.168.5.3`
- 用户名:`root`,密码:`Ubains@123`
- 协议:SCP
- 左侧选本地 ISO 文件,右侧导航到 `/var/lib/libvirt/images/`,拖拽上传
---
## 三、创建虚拟机
### 3.1 进入虚拟机管理页面
登录后,点击左侧导航栏的「**虚拟机**」。
### 3.2 打开创建对话框
点击页面右上角的「**Create VM**」按钮。
### 3.3 填写虚拟机参数
#### Connection(连接方式)
保持默认:**QEMU/KVM System connection**
#### Name(名称)
给 VM 起个名字,如 `centos7-test`
#### Installation Source Type(安装源类型)
选择 **Local Install Media**(本地安装介质)
#### Installation Source(安装源路径)
填写之前上传的 ISO 文件路径,例如:
```
/var/lib/libvirt/images/CentOS-7-x86_64-Minimal-2009.iso
```
> 也可以选 **URL**,直接填网络下载地址
#### Storage(存储)
- 保持 **Create New Volume**(创建新虚拟磁盘)
- **Size**:根据需求设置,一般 Linux 系统 20-40 GiB 即可
- 单位选 **GiB**
#### Memory(内存)
根据需求设置,最小 1 GiB。这台服务器有 125G 内存,够用。
> 建议:测试用 2-4 GiB,生产环境按需分配
#### OS Vendor / Operating System(操作系统)
选择你 ISO 实际对应的系统:
| 你要装的系统 | 选择 |
|-------------|------|
| 麒麟 V10 | Unspecified → Other OS |
| CentOS 7 | CentOS → CentOS 7 |
| Ubuntu | Debian Project → Ubuntu |
| Windows | Microsoft Corporation → 对应版本 |
| 其他 | Unspecified → Other OS |
> 如果列表里没找到你的系统,选 Unspecified 也能正常安装运行。
#### Immediately Start VM
- **勾选**:创建完立刻开机,然后弹出 VNC 控制台让你装系统
- **不勾选**:创建完保持关机状态,需要手动开机
### 3.4 点击「Create」
对话框底部点击「**Create**」按钮,等待 VM 创建完成。
---
## 四、安装操作系统
### 4.1 打开控制台
VM 创建并开机后,VM 列表中会出现一行。点击 VM 名称进入详情页,再点击「**Console**」标签页进入 VNC 控制台。
如果鼠标点不动,按一下 `Ctrl+Alt` 释放光标。
### 4.2 常见操作
| 操作 | 方法 |
|------|------|
| 发送 Ctrl+Alt+Del | 点击页面上方的「Send Key」按钮 |
| 释放鼠标 | 按 `Ctrl+Alt` |
| 强制关机 | 点击「Force Off」 |
| 重启 | 点击「Reboot」 |
---
## 五、系统安装后必须配置(命令行操作)
> ⚠️ Cockpit Web 界面的 VM 详情页功能有限,以下配置必须通过命令行完成。
### 5.1 修复启动顺序(防止重启进安装界面)
VM 创建后默认 `cdrom` 优先启动,系统装完后必须改为硬盘优先:
```bash
# 1. 关机
virsh destroy "VM名称"
# 2. 导出 XML,去掉 cdrom 启动项
virsh dumpxml "VM名称" | perl -ne "print unless /boot dev=.cdrom/" > /tmp/VM名称_fix.xml
# 3. 如果 XML 里有 UUID 且和已有 VM 冲突,去 UUID
perl -ne "print unless /<uuid>/" /tmp/VM名称_fix.xml > /tmp/VM名称_final.xml
# 4. undefine + define + start
virsh undefine "VM名称"
virsh define "/tmp/VM名称_final.xml"
virsh start "VM名称"
# 5. 验证(应只有 boot dev='hd',没有 cdrom)
virsh dumpxml "VM名称" | grep "boot dev"
```
### 5.2 修改网卡为 br0 桥接(接入 192.168.5.x 网段)
VM 创建后默认网卡接在 `virbr0`(NAT 内网),无法从外部访问。需改为 br0 桥接:
```bash
# 1. 确认 br0 存在
brctl show br0
# 2. 如果 br0 不存在,手动创建
ip link add name br0 type bridge
ip link set eno2 master br0
ip link set br0 up
# 3. 关机 VM,修改网卡 XML
virsh destroy "VM名称"
virsh dumpxml "VM名称" | \
perl -pe "s|type='network'|type='bridge'|g; s|<source network='default' bridge='virbr0'/>|<source bridge='br0'/>|g; s|rtl8139|virtio|g" \
> /tmp/VM名称_net.xml
# 4. undefine + define + start
virsh undefine "VM名称"
virsh define "/tmp/VM名称_net.xml"
virsh start "VM名称"
# 5. 验证(应显示 bridge br0 + virtio)
virsh domiflist "VM名称"
```
> **网卡型号选 virtio**:麒麟 V10、CentOS 7 等现代 Linux 内核自带 virtio 驱动,比 rtl8139(老式百兆网卡)性能好且稳定。
### 5.3 调整 CPU 核数
VM 创建后默认只有 1 核,需要改为 8 核:
```bash
virsh destroy "VM名称"
virsh setvcpus "VM名称" 8 --config --maximum
virsh setvcpus "VM名称" 8 --config
virsh start "VM名称"
# 验证
virsh vcpucount "VM名称"
```
### 5.4 创建快照
系统安装配置完成后,创建快照方便后续回滚:
```bash
virsh snapshot-create-as "VM名称" "initial-install" "系统安装完成,8核+br0桥接"
# 查看快照列表
virsh snapshot-list "VM名称"
# 恢复快照
virsh snapshot-revert "VM名称" --snapshotname initial-install
# 删除快照
virsh snapshot-delete "VM名称" --snapshotname initial-install
```
---
## 六、完整部署流程示例(麒麟 V10)
以下为 192.168.5.3 上三台麒麟 V10 VM 的完整部署记录:
| VM | IP | CPU | 网卡 | 快照 |
|-----|-----|-----|------|------|
| 5.41招商局负载1 | 192.168.5.41 | 8 核 | br0 + virtio | ✅ initial-install |
| 5.42招商局负载2 | 192.168.5.42 | 8 核 | br0 + virtio | ✅ initial-install |
| 5.43招商局负载3 | 192.168.5.43 | 8 核 | br0 + virtio | ✅ initial-install |
### 一键配置脚本
将以下脚本保存为 `/tmp/setup_vm.sh`,创建新 VM 后执行:
```bash
#!/bin/bash
# 用法: bash /tmp/setup_vm.sh "VM名称"
VM="$1"
echo "=== 关机 ==="
virsh destroy "$VM"
echo "=== 导出并修改 XML ==="
virsh dumpxml "$VM" | \
perl -ne "print unless /boot dev=.cdrom/" | \
perl -pe "s|type='network'|type='bridge'|g; s|<source network='default' bridge='virbr0'/>|<source bridge='br0'/>|g; s|rtl8139|virtio|g" | \
perl -ne "print unless /<uuid>/" \
> "/tmp/${VM}_setup.xml"
echo "=== 重新定义 ==="
virsh undefine "$VM"
virsh define "/tmp/${VM}_setup.xml"
echo "=== 设置 8 核 ==="
virsh setvcpus "$VM" 8 --config --maximum
virsh setvcpus "$VM" 8 --config
echo "=== 启动 ==="
virsh start "$VM"
sleep 5
echo "=== 创建快照 ==="
virsh snapshot-create-as "$VM" "initial-install" "系统安装完成,8核+br0桥接"
echo "=== 验证 ==="
virsh domiflist "$VM"
virsh vcpucount "$VM"
virsh dumpxml "$VM" | grep "boot dev"
virsh snapshot-list "$VM"
```
---
## 七、常用管理操作
### Cockpit Web 界面
| 操作 | 位置 |
|------|------|
| 开机/关机/重启 | VM 详情页顶部按钮 |
| VNC 控制台 | VM 详情页 → Console 标签 |
| 修改内存 | VM 详情页 → Overview 标签 |
| 删除 VM | VM 列表页 → 勾选 VM → 点击「Delete」 |
### 命令行
| 操作 | 命令 |
|------|------|
| 查看所有 VM | `virsh list --all` |
| 查看 VM 网卡 | `virsh domiflist "VM名称"` |
| 查看 vCPU | `virsh vcpucount "VM名称"` |
| 查看快照列表 | `virsh snapshot-list "VM名称"` |
| 创建快照 | `virsh snapshot-create-as "VM名称" "快照名" "描述"` |
| 恢复快照 | `virsh snapshot-revert "VM名称" --snapshotname "快照名"` |
| 删除快照 | `virsh snapshot-delete "VM名称" --snapshotname "快照名"` |
| 桥接状态 | `brctl show` |
---
## 八、注意事项
1. **ISO 文件用完后不要删除**:重装系统或救援模式时还会用到
2. **内存不要全部分配**:给宿主机留至少 2-4G,服务器总共 125G,单台 VM 建议不超过 64G
3. **桥接网络选 br0**:VM 才能直接接入 192.168.5.x 网段,virbr0 是 NAT 内网(外部无法访问)
4. **网卡型号选 virtio**:现代 Linux 内核自带驱动,性能优于 rtl8139
5. **系统装完后必须改启动顺序**:否则重启会从 ISO 引导进安装界面
6. **安装完成后立即创建快照**:方便后续搞坏了快速回滚
7. **br0 桥接需手动维护**:NetworkManager 可能导致 br0 消失,建议写入 `/etc/rc.local` 开机自动创建
# EMQX 5.8.7 三节点集群部署文档
## 1. 背景
当前项目中 EMQX 部署为单节点模式(`auto_middleware_install.sh` 中的 `emqx_x86()` 函数),无法满足高可用需求。本文档描述如何将现有单节点部署改造为三节点集群。
## 2. 架构概览
```
┌──────────────────────┐ ┌──────────────────────┐ ┌──────────────────────┐
│ 服务器1 (Node1) │ │ 服务器2 (Node2) │ │ 服务器3 (Node3) │
│ 192.168.5.41 │ │ 192.168.5.42 │ │ 192.168.5.43 │
│ │ │ │ │ │
│ ┌────────────────┐ │ │ ┌────────────────┐ │ │ ┌────────────────┐ │
│ │ uemqx 容器 │ │ │ │ uemqx 容器 │ │ │ │ uemqx 容器 │ │
│ │ emqx@.41 │◄─┼────┼─►│ emqx@.42 │◄─┼────┼─►│ emqx@.43 │ │
│ │ emqx/emqx:5.8.7 │ │ │ │ emqx/emqx:5.8.7 │ │ │ │ emqx/emqx:5.8.7 │ │
│ └────────────────┘ │ │ └────────────────┘ │ │ └────────────────┘ │
│ │ │ │ │ │
│ MQTT :1883 │ │ MQTT :1883 │ │ MQTT :1883 │
│ Dashboard :18083 │ │ Dashboard :18083 │ │ Dashboard :18083 │
└──────────────────────┘ └──────────────────────┘ └──────────────────────┘
集群发现策略: static(静态节点列表)
内部通信协议: Erlang 分布式协议 (EPMD:4370, gen_rpc:5370)
```
## 3. 单节点 vs 集群 关键差异
| 配置项 | 单节点部署(现有) | 集群部署(目标) |
|--------|-------------------|-----------------|
| `node.name` | 默认 `emqx@127.0.0.1`(不可集群) | `emqx@<节点真实IP>` |
| `cluster.discovery_strategy` | 默认 `manual` | `static` |
| `cluster.static.seeds` | 无 | 三个节点的 `emqx@IP` 列表 |
| `cluster.name` | 未显式设置 | `emqxcl`(各节点一致) |
| `node.cookie` | 默认随机生成 | 固定值(各节点一致) |
| 端口 4370 | 未发布到宿主机 | 必须发布(EPMD 端口) |
| 端口 5370 | 未发布到宿主机 | 必须发布(gen_rpc 端口) |
| `rpc.tcp_server_port` | 默认 | 显式指定 5370 |
| Dashboard 18083 | 仅在容器内 | 发布到宿主机(便于管理) |
## 4. 前置条件
### 4.1 网络互通(⚠️ 必须提前操作)
三台服务器之间必须网络互通,以下端口需要在**防火墙/安全组中放行**,否则集群无法组起来:
| 端口 | 协议 | 用途 | 是否必须 |
|------|------|------|---------|
| **4370** | TCP | **Erlang 分布式协议 (EPMD),集群节点互相发现** | ⭐ 集群必须 |
| **5370** | TCP | **Erlang 分布式节点间通信 (gen_rpc)** | ⭐ 集群必须 |
| 1883 | TCP | MQTT 客户端连接 | ✅ 业务必须 |
| 8083 | TCP | WebSocket 连接 | ✅ 业务必须 |
| 8883 | TCP | MQTT over SSL | ✅ 业务必须 |
| 18083 | TCP | EMQX Dashboard 管理界面 | ✅ 建议开放 |
**开放端口命令**(在每台服务器上执行,如使用 firewalld):
```bash
# 放行所有端口
firewall-cmd --permanent --add-port={1883,8083,8883,4370,5370,18083}/tcp
firewall-cmd --reload
# 验证放行结果
firewall-cmd --list-ports | grep 4370
```
> 如果使用云服务器安全组,需在控制台添加入站规则,放行上述端口。
### 4.2 各服务器准备
```bash
# 1. 确保 Docker 已安装
docker --version
# 2. 确保镜像包已上传到服务器
ls -la /data/temp/uemqx-5.8.7.tar.gz
# 3. 如果已有单节点容器在运行,先停掉
docker stop uemqx 2>/dev/null && docker rm uemqx 2>/dev/null || true
```
## 5. 部署步骤
假设三台服务器:
- 节点1:`192.168.5.41`
- 节点2:`192.168.5.42`
- 节点3:`192.168.5.43`
> **注意**:以下步骤在**三台服务器上分别执行**,各节点只改 `node.name` 中的 IP。
### 5.1 准备目录和配置文件
在每台服务器上执行:
```bash
# 创建目录结构
mkdir -p /data/middleware/emqx/{config,data,log}
# 设置权限(EMQX 容器内用户 UID=1000)
chown -R 1000:1000 /data/middleware/emqx/{data,log}
```
### 5.2 生成 emqx.conf
**节点1(192.168.5.41)**
```bash
cat > /data/middleware/emqx/config/emqx.conf << 'EOF'
## Place read-only configurations in this file.
## To define configurations that can later be overridden through UI/API/CLI, add them to `etc/base.hocon`.
##
## Config precedence order:
## etc/base.hocon < cluster.hocon < emqx.conf < environment variables
##
## See https://docs.emqx.com/en/enterprise/v5.8/configuration/configuration.html for more information.
## Configuration full example can be found in etc/examples
node {
name = "emqx@192.168.9.90"
cookie = "emqxsecretcookie"
data_dir = "data"
}
cluster {
name = emqxcl
discovery_strategy = static
static {
seeds = [emqx@192.168.9.70, emqx@192.168.9.89, emqx@192.168.9.90]
}
}
dashboard {
listeners {
http.bind = 18083
# https.bind = 18084
https {
ssl_options {
certfile = "${EMQX_ETC_DIR}/certs/cert.pem"
keyfile = "${EMQX_ETC_DIR}/certs/key.pem"
}
}
}
}
log {
file_handlers {
default {
enable = true
level = info
file = "log/emqx.log"
rotation {
enable = true
count = 10
}
max_size = 50MB
formatter = text
}
error {
enable = true
level = error
file = "log/emqx_error.log"
rotation {
enable = true
count = 5
}
max_size = 50MB
formatter = text
}
}
console_handler {
enable = true
level = warning
}
}
EOF
```
**节点2(192.168.5.42)**:仅需修改一行:
```bash
# 在节点2上,修改 node.name 行为:
name = emqx@192.168.5.42
```
**节点3(192.168.5.43)**:同样只改 node.name:
```bash
# 在节点3上,修改 node.name 行为:
name = emqx@192.168.5.43
```
### 5.3 加载镜像并启动容器
在每台服务器上执行(三台可同时执行,EMQX 会自动相互发现并组成集群):
```bash
# 加载镜像(如已加载可跳过)
docker load -i /data/temp/uemqx-5.8.7.tar.gz
# 启动集群容器
docker run -d \
--name uemqx \
--mac-address="02:42:ac:12:00:06" \
--privileged \
--restart=always \
-p 1883:1883 \
-p 8083:8083 \
-p 8883:8883 \
-p 4370:4370 \
-p 5370:5370 \
-p 18083:18083 \
-e EMQX_NODE__NAME="emqx@192.168.9.91" \
-e EMQX_NODE__COOKIE="emqxsecretcookie" \
-e EMQX_CLUSTER__DISCOVERY_STRATEGY="static" \
-e EMQX_CLUSTER__STATIC__SEEDS="[emqx@192.168.9.89,emqx@192.168.9.90,emqx@192.168.9.91]" \
-v /data/middleware/emqx/config/emqx.conf:/opt/emqx/etc/emqx.conf:ro \
-v /data/middleware/emqx/config/auth-built-in-db-bootstrap.csv:/opt/emqx/etc/auth-built-in-db-bootstrap.csv:ro \
-v /data/middleware/emqx/config/acl.conf:/opt/emqx/etc/acl.conf:ro \
-v /data/middleware/emqx/data:/opt/emqx/data \
-v /data/middleware/emqx/log:/opt/emqx/log \
emqx/emqx:5.8.7
# 等待服务就绪(约 30 秒)
sleep 30
```
**节点2(192.168.5.42)**:只改 EMQX_NODE__NAME 中的 IP:
```bash
-e EMQX_NODE__NAME="emqx@192.168.5.42"
```
**节点3(192.168.5.43)**
```bash
-e EMQX_NODE__NAME="emqx@192.168.5.43"
```
> **说明**:环境变量的优先级高于 emqx.conf,EMQX 5.8.7 的 base.hocon 可能会覆盖 emqx.conf 中的配置,因此通过 `-e` 环境变量设置 node.name、cookie、集群配置是最可靠的方式。
### 5.4 验证集群
```bash
# 1. 检查容器状态
docker ps --filter name=uemqx --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
# 2. 检查集群状态
docker exec uemqx emqx ctl cluster status
```
**预期输出示例**
```
Cluster status: #{running_nodes => ['emqx@192.168.5.41','emqx@192.168.5.42','emqx@192.168.5.43']}
```
输出中 `running_nodes` 应包含全部三个节点。
```bash
# 3. 通过 Dashboard 验证
# 浏览器访问 http://192.168.5.41:18083
# 登录: admin / public
# 进入 "集群管理" 页面,应看到 3 个节点的集群拓扑图
```
## 6. 集群管理命令
```bash
# 查看集群状态
docker exec uemqx emqx ctl cluster status
# 查看集群中的节点列表
docker exec uemqx emqx ctl brokers list
# 手动踢出一个节点(在需要运维时使用)
docker exec uemqx emqx ctl cluster leave emqx@192.168.5.43
# 手动加入集群(节点重启后通常自动加入,此命令用于手动恢复)
docker exec uemqx emqx ctl cluster join emqx@192.168.5.41
```
## 7. 与现有系统的适配说明
### 7.1 现有监控脚本 `monitor_emqx_service.sh`
需要做以下适配:
| 项目 | 现有值 | 集群后需调整 |
|------|--------|-------------|
| 端口检测列表 | `1883 8083 8883` | 新增 `4370 5370` 检测 |
| 预期节点 | 无集群检查 | 新增 `EXPECTED_NODES=3` 检查 |
| Dashboard 端口 | 18083 仅在容器内 | 已发布到宿主机,可从宿主机侧检测 |
集群版监控脚本已生成在:`自动化部署脚本/monitor_emqx_cluster.sh`
### 7.2 MiddlewareVerify 配置 `config.yaml`
需要更新 `uemqx` 段的配置:
```yaml
uemqx:
name: uemqx
image_expected: emqx/emqx:5.8.7 # 版本更新
mqtt_port: 1883
dashboard_port: 18083 # 现在已可以访问
dashboard_user: admin
dashboard_password: 'public'
mqtt_user: 'mqtt@cmdb'
mqtt_password: 'mqtt@webpassw0RD'
# 新增:集群验证
cluster:
expected_nodes: 3
check_command: "emqx ctl cluster status"
```
### 7.3 负载均衡
客户端连接集群中的任意节点均可正常收发消息。如需负载均衡,可在前端部署 Nginx 或 HAProxy:
```nginx
# Nginx 配置示例 - MQTT TCP 负载均衡 (/etc/nginx/conf.d/emqx_mqtt.conf)
stream {
upstream emqx_mqtt {
server 192.168.5.41:1883 max_fails=3 fail_timeout=30s;
server 192.168.5.42:1883 max_fails=3 fail_timeout=30s;
server 192.168.5.43:1883 max_fails=3 fail_timeout=30s;
}
server {
listen 1883;
proxy_pass emqx_mqtt;
proxy_connect_timeout 10s;
}
}
```
## 8. 常见问题
### 8.1 节点无法加入集群
**排查步骤**
```bash
# 1. 检查防火墙是否放通了 4370 和 5370 端口
firewall-cmd --list-ports
# 2. 从其他节点 telnet 测试连通性
telnet 192.168.5.42 4370
# 3. 检查 cookie 是否一致(三台服务器的 emqx.conf 中 node.cookie 必须完全相同)
# 4. 检查容器日志
docker logs uemqx | grep -i cluster
```
### 8.2 集群发生脑裂
**现象**`docker exec uemqx emqx ctl cluster status` 显示每个节点只看到自己
**恢复**
```bash
# 在脱离集群的节点上执行
docker restart uemqx
# 重启后会自动通过 static seeds 重新加入集群
# 如果仍无法加入,手动执行
docker exec uemqx emqx ctl cluster join emqx@192.168.5.41
```
### 8.3 单节点故障影响
- 三个节点中一个宕机:剩余两节点继续提供服务
- 三个节点中两个宕机:剩余节点可接收连接但集群不可用(需至少 2 个节点维持集群)
- 建议配置 `monitor_emqx_cluster.sh` 定时任务(每 5 分钟),确保宕机后自动恢复
### 8.4 节点名中的 IP 地址必须正确
`node.name` 必须使用**其他节点能访问到的 IP 地址**。如果使用 `127.0.0.1` 或容器内 IP(如 `172.17.0.x`),其他节点将无法连接。
## 9. 部署脚本
集群部署脚本已生成在:
- `自动化部署脚本/emqx_cluster_deploy.sh` — 集群部署脚本(支持参数化配置)
- `自动化部署脚本/monitor_emqx_cluster.sh` — 集群监控脚本(含脑裂检测)
### 使用示例
```bash
# 服务器1(192.168.5.41)
bash emqx_cluster_deploy.sh --node 1 --ip1 192.168.5.41 --ip2 192.168.5.42 --ip3 192.168.5.43
# 服务器2(192.168.5.42)
bash emqx_cluster_deploy.sh --node 2 --ip1 192.168.5.41 --ip2 192.168.5.42 --ip3 192.168.5.43
# 服务器3(192.168.5.43)
bash emqx_cluster_deploy.sh --node 3 --ip1 192.168.5.41 --ip2 192.168.5.42 --ip3 192.168.5.43
```
## 10. 变更历史
| 版本 | 日期 | 变更内容 |
|------|------|---------|
| V1.0 | 2026-07-06 | 初始版本,基于 `emqx_x86()` 单节点部署改造为三节点集群 |
# FastDFS ARM64 集群部署操作指南
> 服务器:192.168.9.89 / 192.168.9.90 / 192.168.9.91 | 架构:ARM64 | 镜像:ufastdfs:v2
> 部署日期:2026-07-08
---
## 一、架构说明
### 1.1 部署架构
每台服务器上运行一个 `utracker` 容器,同时包含 Tracker 和 Storage 两个角色:
| 服务器 | IP | 角色 | Tracker(22122) | Storage(23000) | HTTP(8888) |
|--------|----|------|---------------|----------------|------------|
| 节点1 | 192.168.9.89 | Tracker + Storage | ✅ | ✅ | ✅ |
| 节点2 | 192.168.9.90 | Tracker + Storage | ✅ | ✅ | ✅ |
| 节点3 | 192.168.9.91 | Tracker + Storage | ✅ | ✅ | ✅ |
### 1.2 文件存储结构
```
容器内: /home/fastdfs/
├── data/00/00/xxx.txt # 实际存储的文件(按256*256子目录分散)
├── data/00/01/xxx.txt
├── logs/ # 运行日志
│ ├── trackerd.log
│ └── storaged.log
├── fdfs_trackerd.pid
├── fdfs_storaged.pid
└── storage_stat.dat # storage 状态元数据
宿主机: /data/storage/storage/ → 映射到容器内 /home/fastdfs/
```
### 1.3 端口说明
| 端口 | 组件 | 用途 | 防火墙必开 |
|------|------|------|-----------|
| 22122 | Tracker | Storage 注册、客户端查询、Tracker 集群同步 | ✅ |
| 23000 | Storage | 文件上传/下载、节点间同步 | ✅ |
| 8888 | Nginx | HTTP 文件下载 | ✅ |
---
## 二、前置条件
1. **Docker 已安装**(版本 ≥ 20.x)
2. **三台服务器之间网络互通**(22122、23000、8888 端口互相可达)
3. **存储目录已创建**`mkdir -p /data/storage/storage`
4. **防火墙开放端口**
```bash
firewall-cmd --permanent --add-port=22122/tcp
firewall-cmd --permanent --add-port=23000/tcp
firewall-cmd --permanent --add-port=8888/tcp
firewall-cmd --reload
```
---
## 三、部署步骤(顺序执行)
### 第一步:修改配置文件(解决 docker 镜像 entrypoint bug)
`ufastdfs:v2` 镜像的 entrypoint 有个已知 bug:启动后 `storage.conf``client.conf` 里的 `tracker_server` 写成了 `192.168.0.165`(docker bridge 地址),需要用以下步骤修复。
### 第二步:逐个服务器部署
#### 1. 在 192.168.9.89 上
```bash
# 启动容器
docker run -d \
--network=host \
--name utracker \
--restart=always \
-e TRACKER_SERVER="192.168.9.89:22122,192.168.9.90:22122,192.168.9.91:22122" \
-e FASTDFS_IPADDR=192.168.9.89 \
-v /data/storage/storage:/home/fastdfs \
-v /etc/localtime:/etc/localtime:ro \
ufastdfs:v2
# 修复配置文件(删掉旧的 0.165 地址,加正确的 3 台 tracker)
docker exec utracker sed -i '/^tracker_server = 192.168.0.165:22122/d' /etc/fdfs/storage.conf
docker exec utracker sed -i '/^tracker_server = 192.168.0.165:22122/d' /etc/fdfs/client.conf
docker exec utracker bash -c 'cat >> /etc/fdfs/storage.conf << EOF
tracker_server = 192.168.9.89:22122
tracker_server = 192.168.9.90:22122
tracker_server = 192.168.9.91:22122
EOF'
docker exec utracker bash -c 'cat >> /etc/fdfs/client.conf << EOF
tracker_server = 192.168.9.89:22122
tracker_server = 192.168.9.90:22122
tracker_server = 192.168.9.91:22122
EOF'
# 清理旧元数据并重启
docker exec utracker bash -c 'cd /home/fastdfs/data && rm -f storage_*.dat fdfs_*.pid'
docker restart utracker
sleep 45
docker exec utracker fdfs_monitor /etc/fdfs/client.conf | grep -E "ip_addr|ACTIVE|OFFLINE"
```
#### 2. 在 192.168.9.90 上
```bash
# 启动容器(注意 FASTDFS_IPADDR 改成 90)
docker run -d \
--network=host \
--name utracker \
--restart=always \
-e TRACKER_SERVER="192.168.9.89:22122,192.168.9.90:22122,192.168.9.91:22122" \
-e FASTDFS_IPADDR=192.168.9.90 \
-v /data/storage/storage:/home/fastdfs \
-v /etc/localtime:/etc/localtime:ro \
ufastdfs:v2
# 同样的修复步骤
docker exec utracker sed -i '/^tracker_server = 192.168.0.165:22122/d' /etc/fdfs/storage.conf
docker exec utracker sed -i '/^tracker_server = 192.168.0.165:22122/d' /etc/fdfs/client.conf
docker exec utracker bash -c 'cat >> /etc/fdfs/storage.conf << EOF
tracker_server = 192.168.9.89:22122
tracker_server = 192.168.9.90:22122
tracker_server = 192.168.9.91:22122
EOF'
docker exec utracker bash -c 'cat >> /etc/fdfs/client.conf << EOF
tracker_server = 192.168.9.89:22122
tracker_server = 192.168.9.90:22122
tracker_server = 192.168.9.91:22122
EOF'
docker exec utracker bash -c 'cd /home/fastdfs/data && rm -f storage_*.dat fdfs_*.pid'
docker restart utracker
sleep 45
docker exec utracker fdfs_monitor /etc/fdfs/client.conf | grep -E "ip_addr|ACTIVE|OFFLINE"
```
#### 3. 在 192.168.9.91 上
```bash
# 同 90,FASTDFS_IPADDR 改成 91
# ...(同上)
docker run -d \
--network=host \
--name utracker \
--restart=always \
-e TRACKER_SERVER="192.168.9.89:22122,192.168.9.90:22122,192.168.9.91:22122" \
-e FASTDFS_IPADDR=192.168.9.91 \
# ...(其余同上)
```
### 第三步:验证集群
在任意一台服务器上执行:
```bash
docker exec utracker fdfs_monitor /etc/fdfs/client.conf | grep -E "ip_addr|ACTIVE|OFFLINE"
```
预期输出三台都是 ACTIVE:
```
ip_addr = 192.168.9.89 ACTIVE
ip_addr = 192.168.9.90 ACTIVE
ip_addr = 192.168.9.91 ACTIVE
```
---
## 四、文件上传与访问测试
### 4.1 上传测试
```bash
echo "hello fastdfs" > /tmp/test.txt
docker exec utracker fdfs_upload_file /etc/fdfs/client.conf /tmp/test.txt
# 返回: group1/M00/00/00/xxxxxxxxxxxx.txt
```
### 4.2 HTTP 访问
```bash
# 直接访问(需端口开放)
curl http://192.168.9.89:8888/group1/M00/00/00/xxxxxxxxxxxx.txt
# 通过 nginx 反向代理访问(HTTPS)
# 前提:nginx 容器配置了 /group1 的 proxy_pass
curl -k https://192.168.9.89/group1/M00/00/00/xxxxxxxxxxxx.txt
```
### 4.3 文件下载验证
```bash
# 下载到本地
docker exec utracker fdfs_download_file /etc/fdfs/client.conf group1/M00/00/00/xxxxxxxxxxxx.txt /tmp/downloaded.txt
cat /tmp/downloaded.txt
```
---
## 五、文件路径对照
FastDFS 返回的路径和磁盘实际路径:
```
group1/M00/00/00/xxxxxxxxxxxx.txt
↓ 容器内:
/home/fastdfs/data/00/00/xxxxxxxxxxxx.txt
↓ 宿主机:
/data/storage/storage/data/00/00/xxxxxxxxxxxx.txt
```
| FastDFS 路径段 | 含义 |
|---------------|------|
| `group1` | 存储组名 |
| `M00` | store_path0 的挂载标识 |
| `00/00` | 子目录(256×256 个,分散文件) |
| `xxx.txt` | 实际文件名(base64 编码) |
---
## 六、运维命令
### 6.1 查看集群状态
```bash
docker exec utracker fdfs_monitor /etc/fdfs/client.conf
```
### 6.2 查看日志
```bash
docker exec utracker cat /home/fastdfs/logs/trackerd.log | tail -30
docker exec utracker cat /home/fastdfs/logs/storaged.log | tail -30
docker exec utracker tail -30 /usr/local/nginx/logs/error.log
```
### 6.3 删除 OFFLINE storage 记录
```bash
docker exec utracker fdfs_monitor /etc/fdfs/client.conf delete group1 192.168.9.xx
```
### 6.4 清理元数据(IP 变更后或恢复)
```bash
docker exec utracker bash -c 'cd /home/fastdfs/data && rm -f storage_*.dat fdfs_*.pid'
docker restart utracker
```
---
## 七、踩坑记录
### 坑1:tracker_server 写成了 docker bridge 地址
**现象**`storage.conf``tracker_server = 192.168.0.165:22122`,storage 连不上 tracker
**根因**`ufastdfs:v2` 镜像 entrypoint 没有正确解析环境变量 `TRACKER_SERVER`,写入了 docker 内网地址
**对策**:部署后必须用 `sed` 修复配置文件(见部署步骤)
### 坑2:ngx_fastdfs_module 段错误(signal 11)
**现象**:nginx worker 进程不断崩溃,8888 端口无法访问
**根因**:启动时没有传 `-e FASTDFS_IPADDR=xxx` 参数,entrypoint 没有生成正确的 `mod_fastdfs.conf`,nginx 模块找不到配置导致段错误
**对策**`docker run` 时必须传 `-e FASTDFS_IPADDR=<本机IP>`
### 坑3:tracker leader 脑裂(split-brain)
**现象**:多台 tracker 同时认为自己是 leader,storage 注册失败
**根因**:tracker 集群需要多数派,新启动的 tracker 与已有 leader 冲突
**对策**
1. 如果单台跑稳定了再加新节点,新节点启动前先清理元数据再启动
2. 配置完全一致(所有节点的 tracker_server 列表相同)
### 坑4:私服 Docker 镜像缺少 `curl`
**现象**:容器内没有 `curl``netstat` 等工具,排查困难
**对策**:在宿主机上通过 `docker exec utracker curl` 测试,会比较慢(走 TCP),但能验证
---
## 八、常见问题
**Q: storage 注册失败,日志报 `response status 14`?**
A: 检查磁盘空间(`df -h`),错误码 14 = ENOSPC。但也可能是 old metadata 冲突,清理 `storage_*.dat` 重启。
**Q: storage 注册失败,日志报 `response status 11`?**
A: 错误码 11 = EAGAIN(资源暂时不可用)。通常发生在集群 tracker 选主期间,等待 30-60 秒自动恢复。
**Q: 节点部署完后其他节点显示 OFFLINE?**
A: 检查新节点的防火墙端口是否开放;检查新节点的 `storage.conf``tracker_server` 列表是否完整。
**Q: 如何卸载重装?**
A:
```bash
docker stop utracker && docker rm utracker
# 数据还在 /data/storage/storage/,不会丢失
# 如果不需要旧数据:
# rm -rf /data/storage/storage/*
```
\ No newline at end of file
# Nginx 负载均衡(模拟 VIP 入口)部署文档
## 1. 背景与目标
### 1.1 需求
三台业务服务器(`192.168.5.41` / `192.168.5.42` / `192.168.5.43`)已各自部署完整的中间件集群:
- **EMQX 集群**(MQTT 消息)
- **Nacos 集群**(配置中心 / 注册中心)
- **FastDFS 集群**(文件存储)
- **Redis 集群**(缓存)
- **业务后端 + 前端**(通过各自 unginx 的 443 暴露)
用户希望:**只访问一个地址 `192.168.5.40`,任意一台后端宕机,服务仍能正常使用。**
### 1.2 方案选型
| 方案 | 是否真高可用 | 复杂度 | 本文档 |
|------|-------------|--------|--------|
| 硬件 VIP / F5 | ✅ | 高 | 无设备,不可用 |
| Keepalived + VIP(多机漂移) | ✅ | 中 | 见第 8 章可选升级 |
| **单台 Nginx 模拟 VIP(5.40)** | ⚠️ 后端高可用,入口单点 | 低 | **✅ 本文档采用** |
> **说明**:由于当前环境没有 Keepalived 所需的 VIP 漂移条件(多播/单播、VRRP 协议支持),本方案用 **5.40 单台 Nginx 做四层(TCP)负载均衡**,模拟统一入口。它能满足"后端任意一台宕机不影响服务"的需求;后续如需"入口本身也高可用",可按第 8 章升级为 Keepalived 双机热备。
### 1.3 工作原理
```
客户端(浏览器 / MQTT 设备)
│ 只需访问 192.168.5.40
┌─────────────────────────────────────┐
│ 192.168.5.40 (Nginx 负载均衡) │
│ │
│ stream 模块(四层 TCP 转发) │
│ ├─ :443 ── 透传 ──┐ │
│ └─ :1883 ── 透传 ──┤ │
└──────────────────────┼──────────────┘
┌──────────────┼──────────────┐
▼ ▼ ▼
192.168.5.41 192.168.5.42 192.168.5.43
┌──────────┐ ┌──────────┐ ┌──────────┐
│ unginx443│ │ unginx443│ │ unginx443│ ← TLS 卸载 + 业务转发
│ EMQX1883 │ │ EMQX1883 │ │ EMQX1883 │
│ Nacos │ │ Nacos │ │ Nacos │
│ FastDFS │ │ FastDFS │ │ FastDFS │
│ Redis │ │ Redis │ │ Redis │
└──────────┘ └──────────┘ └──────────┘
健康检查:某台宕机 → Nginx 自动剔除 → 流量转到剩余健康节点
```
**核心要点**
- 5.40 的 Nginx 只做 **TCP 透传**(四层),不解析 HTTPS、不需要 SSL 证书
- TLS 卸载由后端 41/42/43 各自的 unginx 完成(即现有的 `unified443.conf` 逻辑不变)
- Nginx 通过**健康检查**自动剔除宕机节点,客户端无感知
---
## 2. 端口规划
| 端口 | 协议 | 5.40 Nginx | 后端 41/42/43 | 说明 |
|------|------|-----------|--------------|------|
| **443** | TCP 透传 | 监听 | 监听 | HTTPS 入口,透传到后端 unginx |
| **1883** | TCP 透传 | 监听 | 监听 | MQTT 原生协议,透传到后端 EMQX |
| 8848 | - | 不监听 | 各自监听 | Nacos,**已包含在 443 的 /nacos/ 路径里**,无需单独转发 |
| 22122/23000 | - | 不监听 | 各自监听 | FastDFS,**已包含在 443 的 /group1、/image 路径里** |
| 8083 | - | 不监听 | 各自监听 | MQTT WebSocket,**已包含在 443 的 /mqtt 路径里** |
> **重要结论**:由于后端 `unified443.conf` 已经把 Nacos、FastDFS、业务后端、MQTT-WS 全部整合在 443 端口下,**5.40 只需透传 443 和 1883 两个端口**,即可覆盖所有业务。
---
## 3. 前置条件
### 3.1 5.40 服务器准备
```bash
# 1. Docker 已安装
docker --version
# 2. 镜像就绪(Tengine 或标准 Nginx 二选一)
ls -la /data/temp/tengine-*.tar.gz # 如使用 Tengine
# 或
ls -la /data/temp/nginx-1.30.2.tar.gz # 如使用标准 Nginx
# 3. 创建目录
mkdir -p /data/middleware/nginx/{config,log}
```
### 3.2 防火墙放行(5.40)
```bash
firewall-cmd --permanent --add-port={443,1883}/tcp
firewall-cmd --reload
firewall-cmd --list-ports
```
### 3.3 后端服务器准备(41/42/43)
后端各台的 unginx 配置(`unified443.conf`)需要做**一处统一调整**
```nginx
# 原配置(各台不同):
server_name 192.168.5.41; # ← 41 上是这个
server_name 192.168.5.42; # ← 42 上是这个
# 改为(三台统一,匹配透传过来的 Host):
server_name _; # _ 表示匹配所有 Host
```
> **原因**:客户端访问 `https://192.168.5.40`,TLS 透传后 SNI = `192.168.5.40`,后端 unginx 的 `server_name` 若写死 `192.168.5.41` 会不匹配。改为 `_` 后三台行为一致,负载均衡才能正确轮询。
---
## 4. 5.40 Nginx 部署
### 4.1 启动 Nginx 容器
> 推荐使用 **host 网络模式**,避免容器端口映射与后端转发的冲突。
```bash
# 使用 Tengine(与现有项目一致)
docker load -i /data/temp/tengine-3.x.x.tar.gz
docker run -d \
--name utengine \
--restart=always \
--network host \
-v /data/middleware/nginx/config:/etc/tengine/conf.d \
-v /data/middleware/nginx/log:/var/log/tengine \
tengine/tengine:latest
```
> 如果只有标准 Nginx 镜像,把路径换成 `/etc/nginx/conf.d` 和 `/var/log/nginx` 即可,配置语法完全通用。
### 4.2 配置文件 1:`stream_tcp.conf`(核心 TCP 透传)
```bash
cat > /data/middleware/nginx/config/stream_tcp.conf << 'EOF'
# ==============================================================================
# 5.40 Nginx 四层 TCP 负载均衡(模拟 VIP 入口)
# 仅做透传,TLS 卸载交由后端 41/42/43 各自的 unginx 完成
# ==============================================================================
stream {
# ------------------------------ 日志格式 -------------------------------------
log_format tcp_log '$remote_addr [$time_local] '
'$protocol $status $bytes_sent $bytes_received '
'$session_time -> $upstream_addr';
access_log /var/log/tengine/stream_access.log tcp_log;
# ------------------------------ 共享健康检查状态 -----------------------------
# 用于多 worker 间共享后端健康状态(Tengine/Nginx 通用)
upstream emqx_mqtt {
server 192.168.5.41:1883 max_fails=3 fail_timeout=30s;
server 192.168.5.42:1883 max_fails=3 fail_timeout=30s;
server 192.168.5.43:1883 max_fails=3 fail_timeout=30s;
}
upstream backend_443 {
server 192.168.5.41:443 max_fails=3 fail_timeout=30s;
server 192.168.5.42:443 max_fails=3 fail_timeout=30s;
server 192.168.5.43:443 max_fails=3 fail_timeout=30s;
}
# ------------------------------ MQTT 1883 透传 -------------------------------
server {
listen 1883;
proxy_pass emqx_mqtt;
proxy_connect_timeout 5s;
proxy_timeout 1h; # MQTT 长连接,超时设长
}
# ------------------------------ HTTPS 443 透传 -------------------------------
server {
listen 443;
proxy_pass backend_443;
proxy_connect_timeout 5s;
proxy_timeout 1h;
# 透传时保留客户端真实 IP(需后端配合,HTTPS 透传场景一般用 Proxy Protocol)
# proxy_protocol on;
}
}
EOF
```
### 4.3 配置文件 2:`default_http.conf`(健康检查页 / 兜底,可选)
```bash
cat > /data/middleware/nginx/config/default_http.conf << 'EOF'
# ==============================================================================
# 5.40 本地 HTTP(仅用于健康检查页面,不参与业务转发)
# ==============================================================================
server {
listen 80;
server_name _;
# 健康检查接口,返回 200
location /health {
access_log off;
return 200 "nginx-lb-ok\n";
add_header Content-Type text/plain;
}
# 根路径提示(避免直接访问 80 端口时报 404 困惑)
location / {
return 200 "This is 5.40 Nginx Load Balancer. Use https://192.168.5.40\n";
add_header Content-Type text/plain;
}
}
EOF
```
### 4.4 修改主配置加载 stream 模块
进入容器,在主配置文件中添加 `stream` 的 include:
```bash
# 查看主配置文件
docker exec utengine cat /etc/tengine/tengine.conf
# 备份
docker exec utengine cp /etc/tengine/tengine.conf /etc/tengine/tengine.conf.bak
# 在文件末尾追加 stream 块(Tengine 主配置默认只有 http {} 块)
docker exec utengine bash -c "cat >> /etc/tengine/tengine.conf << 'APPEND'
stream {
include /etc/tengine/conf.d/stream_tcp.conf;
}
APPEND"
# 确认结果
docker exec utengine tail -5 /etc/tengine/tengine.conf
```
### 4.5 验证并加载
```bash
# 测试配置语法
docker exec utengine nginx -t
# 重新加载
docker exec utengine nginx -s reload
# 查看监听端口
docker exec utengine netstat -tlnp | grep -E "443|1883|80"
```
---
## 5. 健康检查机制说明
本方案采用 **Nginx 被动健康检查**`max_fails` + `fail_timeout`):
| 参数 | 含义 | 本方案值 |
|------|------|---------|
| `max_fails` | 在 `fail_timeout` 时间内失败次数达到该值,标记节点不可用 | 3 次 |
| `fail_timeout` | 节点被标记不可用后,多久后重试 | 30 秒 |
**故障剔除流程**
```
1. 5.41 宕机
2. 5.40 Nginx 转发到 5.41:443,连接失败(连续 3 次)
3. Nginx 标记 5.41 为 "down",后续请求只转发到 5.42、5.43
4. 30 秒后 Nginx 重试 5.41,若仍失败继续保持 down
5. 5.41 恢复后,下次重试成功,自动重新加入负载均衡
```
> **客户端无感知**:整个过程由 Nginx 自动处理,客户端始终只连 `192.168.5.40`。
> **如需主动健康检查**:Tengine 支持 `check interval=5000 rise=2 fall=3 timeout=3000 type=tcp;` 指令(见现有 `unified443.conf` 第 15 行的 FastDFS upstream),可按需在 upstream 块内追加,比被动检查更灵敏。
---
## 6. 验证清单
### 6.1 基础连通性
```bash
# 在任意客户端执行
# 1. HTTPS 透传
curl -k https://192.168.5.40/
# 2. MQTT 1883(用 mosquitto_sub 测试,如已安装)
mosquitto_sub -h 192.168.5.40 -p 1883 -t test/topic -u mqtt@cmdb -P 'mqtt@webpassw0RD'
# 3. Nacos(走 443 路径)
curl -k https://192.168.5.40/nacos/
# 4. 健康检查页
curl http://192.168.5.40/health
```
### 6.2 故障切换验证(关键)
```bash
# 1. 模拟 5.41 宕机(在 5.41 上执行)
docker stop uemqx && systemctl stop nginx # 或 docker stop utengine
# 2. 在客户端持续访问,确认服务不中断
while true; do curl -k -o /dev/null -s -w "%{http_code} " https://192.168.5.40/; sleep 1; done
# 3. 观察 5.40 Nginx 日志,确认流量只转到 5.42、5.43
docker exec utengine tail -f /var/log/tengine/stream_access.log
# 4. 恢复 5.41
docker start uemqx && systemctl start nginx
# 5. 30 秒后确认 5.41 重新承接流量
```
### 6.3 负载均衡分布
```bash
# 多次访问,观察 upstream_addr 是否在三台间轮询
for i in $(seq 1 10); do curl -k -s https://192.168.5.40/health; done
docker exec utengine tail -20 /var/log/tengine/stream_access.log
```
---
## 7. 故障场景对照表
| 故障场景 | 服务是否可用 | 原因 |
|---------|-------------|------|
| **5.41 宕机** | ✅ | Nginx 剔除 5.41,流量转 5.42/5.43 |
| **5.42 宕机** | ✅ | Nginx 剔除 5.42,流量转 5.41/5.43 |
| **5.43 宕机** | ✅ | Nginx 剔除 5.43,流量转 5.41/5.42 |
| **5.41、5.42 同时宕机** | ✅(仅 5.43 承压) | 流量全转 5.43,注意单点性能 |
| **5.40 宕机** | ❌ | 入口单点故障,**需第 8 章方案解决** |
| 后端某中间件单点挂(如 5.41 EMQX) | ✅ | EMQX/Nacos/Redis 自身是集群,已有冗余 |
---
## 8. 可选升级:Keepalived 双机热备(真正高可用)
> 当前 5.40 是单点。如需"入口本身也高可用",可引入第二台 Nginx + Keepalived 实现 VIP 漂移。
### 8.1 架构
```
VIP: 192.168.5.40(虚拟 IP,由 Keepalived 管理)
┌────┴────┐
▼ ▼
5.40 5.44 ← 两台 Nginx,互为主备
(MASTER) (BACKUP)
│ │
└────┬────┘
41 / 42 / 43 业务集群
```
### 8.2 适用前提
- 网络环境支持 VRRP 协议(多数局域网支持)
- 两个 Nginx 节点间能通信(建议同网段)
- 有一个**未占用的 IP** 作为 VIP(如 192.168.5.40 当前未绑定到任何机器)
### 8.3 关键配置(5.40 MASTER 为例)
```bash
# 安装 keepalived
yum install -y keepalived
# /etc/keepalived/keepalived.conf
cat > /etc/keepalived/keepalived.conf << 'EOF'
vrrp_script check_nginx {
script "/etc/keepalived/check_nginx.sh"
interval 2
fall 3
rise 2
}
vrrp_instance VI_1 {
state MASTER # 5.44 上改为 BACKUP
interface eth0 # 实际网卡名
virtual_router_id 51
priority 100 # 5.44 上改为 90
advert_int 1
authentication {
auth_type PASS
auth_pass Ubains@123
}
virtual_ipaddress {
192.168.5.40 # VIP
}
track_script {
check_nginx
}
}
EOF
# 健康检查脚本
cat > /etc/keepalived/check_nginx.sh << 'EOF'
#!/bin/bash
if ! killall -0 nginx; then
docker restart utengine
sleep 2
killall -0 nginx || exit 1
fi
EOF
chmod +x /etc/keepalived/check_nginx.sh
systemctl enable --now keepalived
```
> 此方案下,`192.168.5.40` 是真正可漂移的 VIP,5.40 宕机自动切到 5.44,实现入口零中断。**如需实施请单独评估网络环境。**
---
## 9. 配置文件清单
5.40 上的 Nginx 配置目录最终结构:
```
/data/middleware/nginx/config/
├── stream_tcp.conf # ① TCP 透传(443 + 1883),核心文件
└── default_http.conf # ② 健康检查页 / 兜底,可选
```
> 仅 **2 个文件**即可完成全部负载均衡配置。后端 41/42/43 各自的 `unified443.conf` 保持不变(仅需 `server_name` 改 `_`)。
---
## 10. 客户端连接地址汇总
部署完成后,所有客户端统一指向 `192.168.5.40`
| 服务 | 地址 |
|------|------|
| 前端门户 | `https://192.168.5.40` |
| Nacos 控制台 | `https://192.168.5.40/nacos/` |
| FastDFS 文件访问 | `https://192.168.5.40/group1/...` |
| MQTT(设备直连) | `192.168.5.40:1883` |
| MQTT(WebSocket) | `wss://192.168.5.40/mqtt` |
| 健康检查 | `http://192.168.5.40/health` |
---
## 11. 常见问题
### 11.1 访问 https 报 SSL 错误
**原因**:443 是 TCP 透传,SSL 证书在后端 41/42/43 的 unginx 上。若证书的 CN/SAN 不含 `192.168.5.40`,浏览器会告警。
**解决**
- 重新签发证书,SAN 包含 `192.168.5.40``5.41/5.42/5.43`;或
- 客户端配置信任(测试环境用 `-k` 忽略)。
### 11.2 透传后后端 502 / 404
**原因**:后端 unginx 的 `server_name` 与透传的 Host 不匹配。
**解决**:按第 3.3 节,将三台 `server_name` 统一改为 `_`
### 11.3 负载不均
**原因**:HTTPS 长连接复用,导致流量集中在某台。
**解决**:在 upstream 中加 `least_conn;`(最少连接数算法)替代默认轮询:
```nginx
upstream backend_443 {
least_conn;
server 192.168.5.41:443 ...;
...
}
```
### 11.4 MQTT 设备断连重连频繁
**原因**`proxy_timeout` 过短导致空闲连接被断开。
**解决**:已设为 `proxy_timeout 1h`,MQTT 心跳保活即可。
---
## 12. 变更历史
| 版本 | 日期 | 变更内容 |
|------|------|---------|
| V1.0 | 2026-07-07 | 初始版本,5.40 单点 Nginx 模拟 VIP,443+1883 TCP 透传 |
# X86 架构 Nacos 容器三节点集群部署指南
## 一、背景与目标
当前项目 `auto_middleware_install.sh` 中的 `nacos_x86()` 函数部署的是**单机模式**`MODE=standalone`)。本指南描述如何将三台 X86 服务器的 Nacos 容器组成集群,实现高可用。
### 架构总览
```
┌──────────────────────────────────────────────────────────────┐
│ Tengine (unginx) │
│ upstream nacos_cluster │
│ server 192.168.5.70:8848 │
│ server 192.168.9.76:8848 │
│ server 192.168.9.77:8848 │
│ check interval=5000 rise=2 fall=3 type=tcp │
└────────┬──────────────────┬──────────────────┬────────────────┘
│ │ │
┌────▼────┐ ┌────▼────┐ ┌────▼────┐
│ Server1 │ │ Server2 │ │ Server3 │
│ unacos │◄──────►│ unacos │◄──────►│ unacos │
│ :8848 │ Raft │ :8848 │ Raft │ :8848 │
│ :7848 │ │ :7848 │ │ :7848 │
│ :9848 │ │ :9848 │ │ :9848 │
│ :9849 │ │ :9849 │ │ :9849 │
└────┬────┘ └────┬────┘ └────┬────┘
│ │ │
└──────────────────┼──────────────────┘
┌──────▼──────┐
│ MySQL │
│ (umysql) │
│ :8306 │
│ nacos_mysql │
└─────────────┘
```
### 端口说明
| 端口 | 协议 | 用途 |
|------|------|------|
| 8848 | HTTP | Nacos Web 控制台 & API |
| 9848 | gRPC | 客户端 gRPC 请求 |
| 9849 | gRPC | 服务端 gRPC 通信 |
| 7848 | TCP | Raft 一致性协议(集群内部通信) |
---
## 二、前置条件
### 2.1 MySQL 数据库
集群模式**必须使用外部 MySQL**(不能使用内置 Derby 数据库),且三个节点连接**同一个 MySQL 实例或同一数据库**
当前项目已部署 umysql 容器(端口 8306),已创建 `nacos_mysql` 数据库和 `nacos` 用户:
- 数据库:`nacos_mysql`
- 用户:`nacos`
- 密码:`nacos2025`
- 来源:`auto_middleware_install.sh:228-230`
### 2.2 服务器信息(示例)
| 角色 | IP | 说明 |
|------|-----|------|
| Server1 | 192.168.5.70 | Nacos 节点 1 |
| Server2 | 192.168.9.76 | Nacos 节点 2 |
| Server3 | 192.168.9.77 | Nacos 节点 3 |
| MySQL | 192.168.5.44:8306 | 共享数据库 |
---
## 三、部署步骤
### 3.1 停止现有单机容器(如有)
```bash
# 三台服务器分别执行
docker stop unacos
docker rm unacos
```
### 3.2 确保数据目录存在
```bash
# 三台服务器分别执行
mkdir -p /data/middleware/nacos/{conf,logs,data}
```
### 3.3 配置 application.properties
**每台服务器**`/data/middleware/nacos/conf/application.properties` 中配置数据库连接(三台内容相同):
```properties
# ========== 数据库配置 ==========
spring.datasource.platform=mysql
db.num=1
db.url.0=jdbc:mysql://192.168.5.44:8306/nacos_mysql?characterEncoding=utf8&connectTimeout=5000&socketTimeout=3000&autoReconnect=true&useSSL=false&allowPublicKeyRetrieval=true
db.user.0=nacos
db.password.0=nacos2025
# ========== 鉴权配置 ==========
nacos.core.auth.enabled=true
nacos.core.auth.plugin.nacos.token.secret.key=VGhpc0lzTXlDdXN0b21TZWNyZXRLZXkwMTIzNDU2Nzg=
nacos.core.auth.plugin.nacos.token.expire.seconds=18000
nacos.core.auth.plugin.nacos.username=nacos
nacos.core.auth.plugin.nacos.password=dNrprU&2S
# ========== 集群配置 ==========
nacos.core.member.meta.site=default
```
> **注意**:`nacos.core.auth.plugin.nacos.token.secret.key` 必须配置为**原始密钥的 Base64 编码**,且长度 ≥ 32 字符。三台服务器必须一致。
### 3.4 启动 Nacos 集群容器
**每台服务器**上执行以下命令,替换 `$server_ip` 为本机实际 IP:
```bash
# 集群节点列表(三台一致)
NACOS_SERVERS="192.168.5.70:8848,192.168.9.76:8848,192.168.9.77:8848"
# 获取本机 IP
server_ip=$(ip addr show | grep -E 'inet\s+(192\.168)' | awk '{print $2}' | cut -d/ -f1 | head -n1)
# 启动集群模式容器
docker run -d \
--name unacos \
--restart=always \
-p 8848:8848 \
-p 9848:9848 \
-p 9849:9849 \
-p 7848:7848 \
-e MODE=cluster \
-e NACOS_SERVERS="192.168.9.89:8848,192.168.9.90:8848,192.168.9.91:8848" \
-e NACOS_SERVER_IP="$server_ip" \
-e NACOS_AUTH_ENABLE=true \
-e NACOS_AUTH_USERNAME="nacos" \
-e NACOS_AUTH_PASSWORD="dNrprU&2S" \
-e NACOS_AUTH_IDENTITY_KEY="nacos" \
-e NACOS_AUTH_IDENTITY_VALUE="nacos" \
-e JVM_XMS=2g \
-e JVM_XMX=2g \
-e JVM_XMN=1g \
-e JVM_MS=128m \
-e JVM_MMS=256m \
-v /data/middleware/nacos:/home/nacos \
-v /etc/localtime:/etc/localtime:ro \
--mac-address="02:42:ac:11:00:10" \
nacos/nacos-server:v2.5.2
```
**关键环境变量说明:**
| 变量 | 值 | 说明 |
|------|-----|------|
| `MODE` | `cluster` | 集群模式(原单机为 `standalone`) |
| `NACOS_SERVERS` | `IP1:8848,IP2:8848,IP3:8848` | 所有集群节点列表 |
| `NACOS_SERVER_IP` | 本机 IP | 本节点在集群中通信使用的 IP |
| `JVM_XMS`/`JVM_XMX` | `2g` | 集群模式建议 ≥ 2G 堆内存 |
### 3.5 防火墙配置
每台服务器开放集群通信端口:
```bash
# 集群内部通信端口
firewall-cmd --permanent --add-port=7848/tcp
firewall-cmd --permanent --add-port=8848/tcp
firewall-cmd --permanent --add-port=9848/tcp
firewall-cmd --permanent --add-port=9849/tcp
firewall-cmd --reload
```
---
## 四、Tengine 负载均衡配置
修改 Tengine 容器的 `unified443.conf`,将 Nacos 代理从单机直连改为 upstream 负载均衡。
### 4.1 配置 upstream
```nginx
# 在 http 块中添加
upstream nacos_cluster {
server 192.168.5.70:8848;
server 192.168.9.76:8848;
server 192.168.9.77:8848;
# Tengine 主动健康检查(TCP 探测)
check interval=5000 rise=2 fall=3 timeout=3000 type=tcp;
# 连接池复用
keepalive 32;
}
```
### 4.2 修改 location 代理
将原来的:
```nginx
location /nacos/ {
proxy_pass http://172.17.0.1:8848/nacos/;
}
```
改为:
```nginx
location /nacos/ {
proxy_pass http://nacos_cluster/nacos/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
```
### 4.3 重载 Tengine
```bash
docker exec unginx nginx -t # 测试语法
docker exec unginx nginx -s reload # 热重载
```
---
## 五、验证集群状态
### 5.1 基础验证
```bash
# 1. 检查容器状态
docker ps --filter "name=unacos"
# 2. 查看启动日志,确认集群模式
docker logs unacos 2>&1 | grep -iE "cluster|cluster mode|NACOS_SERVERS"
# 3. 查看 Raft 角色
docker exec unacos cat /home/nacos/logs/protocol-raft.log 2>/dev/null | grep -iE "LEADER|FOLLOWER|CANDIDATE" | tail -5
```
### 5.2 API 验证
```bash
# 查看集群节点列表
curl -s http://127.0.0.1:8848/nacos/v1/ns/operator/servers
# 预期返回类似:
# {"servers":[{"ip":"192.168.5.70","port":8848,"state":"UP",...},
# {"ip":"192.168.9.76","port":8848,"state":"UP",...},
# {"ip":"192.168.9.77","port":8848,"state":"UP",...}]}
# 健康检查
curl -s http://127.0.0.1:8848/nacos/v1/console/health/readiness
# 预期返回:ok 或 UP
```
### 5.3 Web 控制台验证
浏览器访问任意节点的 `http://<任意节点IP>:8848/nacos/` → 登录后进入「集群管理」→「节点列表」,应看到 3 个节点且状态均为 UP。
### 5.4 高可用测试
```bash
# 停掉其中一台的 Nacos 容器
docker stop unacos
# 在另一台验证集群仍可用(2 台满足 Raft 多数派)
curl -s http://<另一台IP>:8848/nacos/v1/ns/operator/servers
# 恢复后自动重新加入集群
docker start unacos
```
---
## 六、监控脚本适配
### 6.1 现有脚本兼容性
`nacos-service.sh` 中的 `check_cluster_status()` 函数(第 324 行)已支持集群模式检测:
- 自动检测 `/home/nacos/conf/cluster.conf` 是否存在
- 存在则通过 API 查询集群节点数
- 检查 Raft 日志中的 LEADER/FOLLOWER 状态
**无需额外修改**,但建议确认 `CONTAINER_PATTERN="nacos"` 能匹配到 `unacos` 容器。
### 6.2 建议新增集群节点数告警
`check_cluster_status()` 函数中增加节点数阈值判断:
```bash
# 在 node_count 解析后增加
if [ "$node_count" -lt 3 ]; then
log_warn "集群节点数不足: $node_count (预期 3)"
fi
```
---
## 七、与原单机部署的差异对照
| 配置项 | 单机模式(现有) | 集群模式(改造后) |
|--------|-----------------|-------------------|
| `MODE` 环境变量 | `standalone` | `cluster` |
| `NACOS_SERVERS` | 无 | 三台 IP:8848 列表 |
| `NACOS_SERVER_IP` | 无 | 本机 IP |
| 映射端口 | 8848, 9848 | 8848, 9848, 9849, **7848** |
| 数据库 | 可使用内置 Derby | **必须外部 MySQL** |
| JVM 堆内存 | 默认(约 512M) | 建议 ≥ 2G |
| Tengine proxy | 直连单 IP | upstream 负载均衡 |
| 防火墙 | 8848 即可 | 8848 + 9848 + 9849 + 7848 |
---
## 八、常见问题
### Q1: 节点启动后一直报 "The server is not in the cluster"
**原因**:节点未能在 `cluster.conf``NACOS_SERVERS` 中找到自己。
**排查**
```bash
# 确认 NACOS_SERVER_IP 与 NACOS_SERVERS 中的某个 IP 一致
docker inspect unacos | grep -E "NACOS_SERVER_IP|NACOS_SERVERS"
```
### Q2: 集群选举不出 Leader
**原因**:Raft 端口 7848 不通,或节点数不足半数+1。
**排查**
```bash
# 在三台之间互相测试 7848 端口连通性
nc -zv 192.168.5.70 7848
nc -zv 192.168.9.76 7848
nc -zv 192.168.9.77 7848
# 检查防火墙
firewall-cmd --list-ports
```
### Q3: 数据库连接失败
**原因**:MySQL 未授权远程访问或网络不通。
**排查**
```bash
# 从 Nacos 容器内测试 MySQL 连接
docker exec unacos bash -c "timeout 3 bash -c 'cat < /dev/tcp/192.168.5.44/8306' && echo 'OK' || echo 'FAIL'"
```
### Q4: 原 nacos_x86() 函数是否保留
建议保留原函数作为单机回退方案,新增 `nacos_x86_cluster()` 函数用于集群部署,通过 `middleware_type` 参数区分调用。
---
## 九、相关文件索引
| 文件 | 说明 |
|------|------|
| `自动化部署脚本/x86架构/新统一平台/auto_middleware_install.sh` | 中间件部署主脚本,含 `nacos_x86()` 函数 |
| `自动化部署脚本/x86架构/新统一平台/定时脚本/nacos-service.sh` | Nacos 健康检查脚本(含集群状态检查) |
| `自动化部署脚本/x86架构/新统一平台/定时脚本/nacos-service_logic.md` | 健康检查脚本逻辑说明 |
| `自动化部署脚本/x86架构/tengine/tengine负载均衡配置指导.md` | Tengine upstream 负载均衡配置 |
| `AuxiliaryTool/ScriptTool/MiddlewareVerify/checks/nacos.py` | Nacos 验证检查器(Python) |
| `AuxiliaryTool/ScriptTool/MiddlewareVerify/config.yaml` | 中间件验证配置(含 unacos 凭据) |
#!/bin/bash
#==============================================================#
# 脚本名: import_dm8_databases.sh
# 功能: 将 databases/ 目录下的 DDL SQL 文件导入到 DM8 容器
# 说明: 1. 自动创建用户(如果不存在),密码满足达梦复杂度规则
# 2. 用管道方式执行 DDL SQL(避免 disql \` backtick 截断问题)
# 3. 验证导入后的表数量
# 作者: [ubains]
# 注意事项:
# - 达梦密码需满足复杂度:≥9位,含大小写+数字+特殊符号
# - disql 不支持分号分隔多语句,CREATE 和 GRANT 必分开执行
# - 密码含 ! 时需用单引号避免 bash 历史扩展
#==============================================================#
set -e
# ------------------- 可配置参数 -------------------
CONTAINER="${DM_CONTAINER:-dm8-server}"
SYSDBA_PWD="${DM_PWD:-dNrprU&2S!}"
DM_PORT="${DM_PORT:-5236}"
DM_INSTALL_DIR="${DM_INSTALL_DIR:-/usr/local/dm8}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DDL_DIR="$SCRIPT_DIR/databases"
HOST_BACKUP_DIR="/data/middleware/dm8/bak"
# 要导入的用户列表(按依赖顺序)
USERS=(
"DM_NACOS"
"DM_UBAINS"
"DM_UBAINS_SSO"
"DM_DEVOPS"
"DM_DEVOPS_VOICE"
"DM_HUAZHAO2"
"DM_OFFLINE"
)
# 用户默认密码(必须满足达梦复杂度:≥9位,含大小写+数字+特殊符号)
declare -A USER_PASSWORDS=(
["DM_NACOS"]="dNrprU&2S!"
["DM_UBAINS"]="dNrprU&2S!"
["DM_UBAINS_SSO"]="dNrprU&2S!"
["DM_DEVOPS"]="dNrprU&2S!"
["DM_DEVOPS_VOICE"]="dNrprU&2S!"
["DM_HUAZHAO2"]="dNrprU&2S!"
["DM_OFFLINE"]="dNrprU&2S!"
)
# ------------------- 颜色输出 -------------------
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
# ------------------- 检查容器 -------------------
check_container() {
if ! docker ps --format '{{.Names}}' | grep -qw "$CONTAINER"; then
log_error "容器 '$CONTAINER' 未运行,请先启动"
exit 1
fi
log_info "容器 '$CONTAINER' 已就绪"
}
# ------------------- 创建用户(密码满足达梦复杂度) -------------------
create_user() {
local user=$1
local pwd=${USER_PASSWORDS[$user]:-"User@2026Pwd"}
log_info "检查用户 $user 是否存在..."
# 检查用户是否存在(用单引号避免 ! 历史扩展)
local exists=$(docker exec "$CONTAINER" "${DM_INSTALL_DIR}/bin/disql" SYSDBA/'dNrprU&2S!'@localhost:$DM_PORT \
-e "SELECT COUNT(*) FROM DBA_USERS WHERE USERNAME='$user';" 2>/dev/null | grep -E '^[0-9]+$' | head -1)
if [[ "$exists" == "0" ]]; then
log_info "创建用户 $user(密码复杂度达标)..."
# CREATE 和 GRANT 分开执行(disql 不支持分号合并)
docker exec "$CONTAINER" "${DM_INSTALL_DIR}/bin/disql" SYSDBA/'dNrprU&2S!'@localhost:$DM_PORT \
-e "CREATE USER \"$user\" IDENTIFIED BY \"$pwd\";" 2>/dev/null || {
log_warn "创建用户失败,尝试用复杂密码重试..."
docker exec "$CONTAINER" "${DM_INSTALL_DIR}/bin/disql" SYSDBA/'dNrprU&2S!'@localhost:$DM_PORT \
-e "CREATE USER \"$user\" IDENTIFIED BY \"${user}Pwd@2026\";"
}
docker exec "$CONTAINER" "${DM_INSTALL_DIR}/bin/disql" SYSDBA/'dNrprU&2S!'@localhost:$DM_PORT \
-e "GRANT DBA TO \"$user\";" 2>/dev/null
log_info "用户 $user 创建成功,密码: $pwd"
else
log_info "用户 $user 已存在,跳过创建"
fi
}
# ------------------- 导入 DDL SQL(管道方式,避免截断) -------------------
import_ddl() {
local user=$1
local ddl_file=$(ls "$DDL_DIR"/${user}_ddl.sql 2>/dev/null | head -1)
if [[ -z "$ddl_file" ]]; then
log_warn "未找到 $user 的 DDL SQL 文件,跳过"
return 0
fi
local ddl_name=$(basename "$ddl_file")
log_info "导入 $user DDL..."
log_info " 文件: $ddl_name ($(wc -l < "$ddl_file") 行)"
# 清理 DDL 文件:去掉 spool 残留的 SQL> 提示符、SELECT 查询行、空行、多余分号
local cleaned_file="/tmp/${ddl_name}"
cp "$ddl_file" "$cleaned_file"
sed -i '/^SQL>/d' "$cleaned_file" 2>/dev/null || true
sed -i '/^SELECT DBMS_METADATA/d' "$cleaned_file" 2>/dev/null || true
sed -i '/^$/d' "$cleaned_file" 2>/dev/null || true
perl -i -pe 's/;{2,}$/;/g' "$cleaned_file" 2>/dev/null || true
log_info " 清理后: $(wc -l < "$cleaned_file") 行"
# 复制 SQL 到容器
docker cp "$cleaned_file" "$CONTAINER:${HOST_BACKUP_DIR}/$ddl_name"
rm -f "$cleaned_file"
# 复制 SQL 到容器
docker cp "$ddl_file" "$CONTAINER:${HOST_BACKUP_DIR}/$ddl_name"
# 用管道方式执行 disql(用单引号避免 ! 历史扩展)
# 关键: cat | disql 方式不会截断超长 DDL,且能正确执行所有 CREATE TABLE 语句
local result=$(docker exec "$CONTAINER" bash -c \
"export LD_LIBRARY_PATH=${DM_INSTALL_DIR}/bin && \
cat ${HOST_BACKUP_DIR}/$ddl_name | ${DM_INSTALL_DIR}/bin/disql SYSDBA/dNrprU\&2S\!@localhost:$DM_PORT" 2>&1)
# 统计执行成功的语句数
local success_count=$(echo "$result" | grep -c "executed successfully" || true)
log_info " 执行成功: $success_count 条语句"
# 清理容器内 SQL
docker exec "$CONTAINER" rm -f "${HOST_BACKUP_DIR}/$ddl_name"
log_info "导入 $user DDL 完成"
}
# ------------------- 验证表数量(取数字行,避免端口号干扰) -------------------
verify_tables() {
log_info "验证各用户表数量..."
echo ""
printf "%-20s %10s\n" "用户" "表数量"
printf "%-20s %10s\n" "----" "------"
for user in "${USERS[@]}"; do
local cnt=$(docker exec "$CONTAINER" "${DM_INSTALL_DIR}/bin/disql" SYSDBA/'dNrprU&2S!'@localhost:$DM_PORT \
-e "SELECT COUNT(*) FROM DBA_TABLES WHERE OWNER='$user';" 2>/dev/null | \
grep -E '^[0-9]+$' | head -1)
printf "%-20s %10s\n" "$user" "${cnt:-0}"
done
echo ""
}
# ------------------- 主流程 -------------------
main() {
log_info "=============================================="
log_info "DM8 DDL 导入工具(SQL 方式,ARM)"
log_info "=============================================="
log_info "容器: $CONTAINER"
log_info "DDL 目录: $DDL_DIR"
log_info "目标用户: ${USERS[*]}"
echo ""
check_container
mkdir -p "$HOST_BACKUP_DIR"
docker exec "$CONTAINER" mkdir -p "$HOST_BACKUP_DIR" 2>/dev/null || true
for user in "${USERS[@]}"; do
echo ""
log_info "=========================================="
log_info "处理用户: $user"
log_info "=========================================="
create_user "$user"
import_ddl "$user"
done
echo ""
log_info "=============================================="
log_info "导入完成"
log_info "=============================================="
verify_tables
}
main "$@"
\ No newline at end of file
#!/bin/bash
#===============================================================================
# 脚本名称:emqx_cluster_deploy.sh
# 功能描述:EMQX 6.0.0 三节点集群部署脚本
# 版本:V1.0
# 创建日期:2026-07-06
# 基于:auto_middleware_install.sh 中的 emqx_x86() 单机部署函数
#
# 架构说明:
# - 采用 EMQX 静态节点发现(static)策略
# - 三台服务器各自运行一个 uemqx 容器
# - 每个节点通过 EMQX_NODE_NAME 指定唯一节点名
# - 通过 EMQX_CLUSTER__STATIC__SEEDS 指定集群种子节点
# - 所有节点通过 Docker bridge 网络或宿主机网络互相通信
#
# 使用前提:
# 1. 三台服务器已安装 Docker
# 2. 三台服务器之间网络互通(需放开以下端口):
# - 1883 (MQTT)
# - 8083 (WebSocket)
# - 8883 (MQTT over SSL)
# - 4370 (EMQX 集群内部通信 - Erlang 分布式协议端口)
# - 5370 (EMQX 集群内部通信 - Erlang 分布式协议端口 + 1000)
# - 18083 (Dashboard)
# 3. 镜像文件已上传到各服务器:/data/temp/uemqx-6.0.0.tar.gz
# 4. 配置文件已准备好(可选,可使用环境变量配置)
#
# 使用方法:
# 在三台服务器上分别执行本脚本,通过参数指定当前节点编号:
#
# 服务器1(如 192.168.9.76):
# bash emqx_cluster_deploy.sh --node 1 --ip1 192.168.9.76 --ip2 192.168.9.77 --ip3 192.168.9.78
#
# 服务器2(如 192.168.9.77):
# bash emqx_cluster_deploy.sh --node 2 --ip1 192.168.9.76 --ip2 192.168.9.77 --ip3 192.168.9.78
#
# 服务器3(如 192.168.9.78):
# bash emqx_cluster_deploy.sh --node 3 --ip1 192.168.9.76 --ip2 192.168.9.77 --ip3 192.168.9.78
#
# 注意事项:
# 1. 首次部署需按顺序启动:先启动节点1,等其完全就绪后再启动节点2、3
# 2. 也可三台同时部署,EMQX 会自动发现并加入集群
# 3. 如果节点加入集群失败,检查防火墙是否放通 4370 和 5370 端口
# 4. EMQX 6.0 集群节点名格式为 emqx@<ip_address>
# 5. 节点名中的 IP 必须能被其他节点访问到
#===============================================================================
set -e
# ==================== 默认配置 ====================
CONTAINER_NAME="uemqx"
IMAGE_TAR="/data/temp/uemqx-6.0.0.tar.gz"
IMAGE_NAME="emqx/emqx:6.0.0"
HOST_CONFIG_DIR="/data/middleware/emqx/config"
HOST_DATA_DIR="/data/middleware/emqx/data"
HOST_LOG_DIR="/data/middleware/emqx/log"
CLUSTER_NAME="emqxcl"
# EMQX 配置参数(沿用现有项目配置)
EMQX_ALLOW_ANONYMOUS="false"
DASHBOARD_USER="admin"
DASHBOARD_PASSWORD="public"
# ==================== 日志函数 ====================
log() {
local level="$1"
shift
local message="$*"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$level] $message"
}
# ==================== 使用说明 ====================
usage() {
echo "用法: $0 --node <1|2|3> --ip1 <IP> --ip2 <IP> --ip3 <IP> [选项]"
echo ""
echo "必选参数:"
echo " --node <1|2|3> 当前服务器节点编号(1、2 或 3)"
echo " --ip1 <IP> 节点1 的 IP 地址"
echo " --ip2 <IP> 节点2 的 IP 地址"
echo " --ip3 <IP> 节点3 的 IP 地址"
echo ""
echo "可选参数:"
echo " --container-name <名称> 容器名称 (默认: uemqx)"
echo " --image-tar <路径> 离线镜像包路径 (默认: /data/temp/uemqx-6.0.0.tar.gz)"
echo " --cluster-name <名称> 集群名称 (默认: emqxcl)"
echo " --skip-image-load 跳过镜像加载步骤(镜像已存在时使用)"
echo " --force 强制重新创建容器(删除已有容器)"
echo " --help 显示此帮助信息"
echo ""
echo "示例:"
echo " # 在节点1(192.168.9.76)上执行:"
echo " bash $0 --node 1 --ip1 192.168.9.76 --ip2 192.168.9.77 --ip3 192.168.9.78"
echo ""
echo " # 在节点2(192.168.9.77)上执行:"
echo " bash $0 --node 2 --ip1 192.168.9.76 --ip2 192.168.9.77 --ip3 192.168.9.78"
}
# ==================== 参数解析 ====================
NODE_NUM=""
IP1=""
IP2=""
IP3=""
SKIP_IMAGE_LOAD=false
FORCE_RECREATE=false
while [[ $# -gt 0 ]]; do
case "$1" in
--node)
NODE_NUM="$2"
shift 2
;;
--ip1)
IP1="$2"
shift 2
;;
--ip2)
IP2="$2"
shift 2
;;
--ip3)
IP3="$2"
shift 2
;;
--container-name)
CONTAINER_NAME="$2"
shift 2
;;
--image-tar)
IMAGE_TAR="$2"
shift 2
;;
--cluster-name)
CLUSTER_NAME="$2"
shift 2
;;
--skip-image-load)
SKIP_IMAGE_LOAD=true
shift
;;
--force)
FORCE_RECREATE=true
shift
;;
--help)
usage
exit 0
;;
*)
log "ERROR" "未知参数: $1"
usage
exit 1
;;
esac
done
# ==================== 参数校验 ====================
if [[ -z "$NODE_NUM" || -z "$IP1" || -z "$IP2" || -z "$IP3" ]]; then
log "ERROR" "缺少必选参数 --node, --ip1, --ip2, --ip3"
usage
exit 1
fi
if [[ ! "$NODE_NUM" =~ ^[123]$ ]]; then
log "ERROR" "--node 必须是 1、2 或 3,当前值: $NODE_NUM"
exit 1
fi
# 根据节点编号确定当前节点的 IP 和节点名
case "$NODE_NUM" in
1) CURRENT_IP="$IP1" ;;
2) CURRENT_IP="$IP2" ;;
3) CURRENT_IP="$IP3" ;;
esac
# EMQX 节点名(Erlang 格式)
NODE_NAME="emqx@${CURRENT_IP}"
# 种子节点列表(所有三个节点)
SEEDS="emqx@${IP1},emqx@${IP2},emqx@${IP3}"
log "INFO" "================================================================"
log "INFO" "EMQX 6.0.0 集群部署配置"
log "INFO" "================================================================"
log "INFO" "当前节点编号: $NODE_NUM"
log "INFO" "当前节点 IP: $CURRENT_IP"
log "INFO" "节点名: $NODE_NAME"
log "INFO" "集群名: $CLUSTER_NAME"
log "INFO" "种子节点: $SEEDS"
log "INFO" "容器名: $CONTAINER_NAME"
log "INFO" "================================================================"
# ==================== 自动判断 sudo ====================
SUDO=""
if [[ $(id -u) -ne 0 ]]; then
if command -v sudo >/dev/null 2>&1; then
SUDO="sudo"
else
log "ERROR" "当前不是 root 用户,且系统未安装 sudo"
exit 1
fi
fi
# ==================== 步骤1: 检查 Docker ====================
log "INFO" "🔍 检查 Docker 是否已安装..."
if ! command -v docker >/dev/null 2>&1; then
log "ERROR" "Docker 未安装,请先安装 Docker"
exit 1
fi
log "INFO" "✅ Docker 已安装: $(docker --version)"
# ==================== 步骤2: 处理已有容器 ====================
log "INFO" "🔍 检查容器 '$CONTAINER_NAME' 是否存在..."
if $SUDO docker ps -a --format '{{.Names}}' | grep -qw "$CONTAINER_NAME"; then
if $SUDO docker ps --format '{{.Names}}' | grep -qw "$CONTAINER_NAME"; then
log "WARN" "容器 '$CONTAINER_NAME' 正在运行"
else
log "WARN" "容器 '$CONTAINER_NAME' 存在但未运行"
fi
if [[ "$FORCE_RECREATE" == true ]]; then
log "INFO" "🔄 --force 模式:删除已有容器..."
$SUDO docker stop "$CONTAINER_NAME" 2>/dev/null || true
$SUDO docker rm "$CONTAINER_NAME" 2>/dev/null || true
log "INFO" "✅ 已有容器已删除"
else
log "INFO" "容器已存在,先停止并删除..."
$SUDO docker stop "$CONTAINER_NAME" 2>/dev/null || true
$SUDO docker rm "$CONTAINER_NAME" 2>/dev/null || true
fi
fi
# ==================== 步骤3: 加载镜像 ====================
if [[ "$SKIP_IMAGE_LOAD" != true ]]; then
log "INFO" "🔍 检查镜像 $IMAGE_NAME 是否存在..."
if $SUDO docker images --format '{{.Repository}}:{{.Tag}}' | grep -qw "$IMAGE_NAME"; then
log "INFO" "✅ 镜像 $IMAGE_NAME 已存在,跳过加载"
else
log "INFO" "📦 从离线包加载镜像: $IMAGE_TAR"
if [[ ! -f "$IMAGE_TAR" ]]; then
log "ERROR" "镜像文件不存在: $IMAGE_TAR"
exit 1
fi
if $SUDO docker load -i "$IMAGE_TAR"; then
log "INFO" "✅ 镜像加载成功"
else
log "ERROR" "镜像加载失败"
exit 1
fi
fi
else
log "INFO" "⏭️ 跳过镜像加载(--skip-image-load)"
fi
# ==================== 步骤4: 准备目录 ====================
log "INFO" "📁 准备数据目录..."
$SUDO mkdir -p "$HOST_CONFIG_DIR" "$HOST_DATA_DIR" "$HOST_LOG_DIR"
$SUDO chown -R 1000:1000 "$HOST_DATA_DIR" "$HOST_LOG_DIR" 2>/dev/null || true
log "INFO" "✅ 数据目录已就绪"
# ==================== 步骤5: 生成配置文件 ====================
log "INFO" "📝 生成 emqx.conf 集群配置..."
EMQX_CONF="$HOST_CONFIG_DIR/emqx.conf"
# 生成 emqx.conf(核心集群配置)
$SUDO tee "$EMQX_CONF" > /dev/null << EOF
#===============================================================================
# EMQX 6.0.0 集群配置文件
# 节点: $NODE_NAME
# 集群: $CLUSTER_NAME
# 生成时间: $(date '+%Y-%m-%d %H:%M:%S')
#===============================================================================
# ==================== 集群配置 ====================
cluster {
## 集群名称
name = $CLUSTER_NAME
## 集群发现策略:static(静态节点列表)
discovery_strategy = static
## 静态节点种子列表
static {
seeds = [$SEEDS]
}
}
# ==================== 节点配置 ====================
node {
## 节点名(Erlang 格式)
name = $NODE_NAME
## Cookie(各节点必须一致,用于 Erlang 集群认证)
cookie = emqxcluster2026secret
}
# ==================== MQTT 配置 ====================
mqtt {
## 禁止匿名访问
allow_anonymous = $EMQX_ALLOW_ANONYMOUS
}
# ==================== Dashboard 配置 ====================
dashboard {
## 默认用户名密码
default_username = "$DASHBOARD_USER"
default_password = "$DASHBOARD_PASSWORD"
}
# ==================== 监听器配置 ====================
listeners.tcp.default {
bind = "0.0.0.0:1883"
}
listeners.ws.default {
bind = "0.0.0.0:8083"
}
listeners.ssl.default {
bind = "0.0.0.0:8883"
}
# ==================== RPC 端口配置 ====================
## 集群 RPC 端口(用于 Erlang 分布式协议,各节点间需互通)
rpc {
port_discovery = manual
mode = async
tcp_server_port = 5370
}
EOF
log "INFO" "✅ emqx.conf 已生成: $EMQX_CONF"
log "INFO" " 关键配置:节点名=$NODE_NAME, 集群名=$CLUSTER_NAME, 种子=$SEEDS"
# ==================== 步骤6: 启动容器 ====================
log "INFO" "🚀 正在启动 EMQX 集群容器: $CONTAINER_NAME (节点 $NODE_NUM) ..."
$SUDO docker run -d \
--name "$CONTAINER_NAME" \
--privileged \
--restart=always \
-p 1883:1883 \
-p 8083:8083 \
-p 8883:8883 \
-p 4370:4370 \
-p 5370:5370 \
-p 18083:18083 \
-v "$EMQX_CONF:/opt/emqx/etc/emqx.conf:ro" \
-v "$HOST_DATA_DIR:/opt/emqx/data" \
-v "$HOST_LOG_DIR:/opt/emqx/log" \
"$IMAGE_NAME"
if [ $? -ne 0 ]; then
log "ERROR" "容器启动失败"
log "INFO" "查看容器日志: docker logs $CONTAINER_NAME"
exit 1
fi
log "INFO" "✅ 容器启动命令执行成功,等待 EMQX 初始化..."
# ==================== 步骤7: 等待就绪 ====================
log "INFO" "⏳ 等待 EMQX 服务启动(最多 60 秒)..."
READY=false
for i in $(seq 1 60); do
if $SUDO docker ps --format '{{.Names}}' | grep -qw "$CONTAINER_NAME"; then
# 检查 Dashboard 是否可访问
if $SUDO docker exec "$CONTAINER_NAME" curl -s -o /dev/null -w '%{http_code}' \
"http://localhost:18083" 2>/dev/null | grep -q "200\|302"; then
READY=true
log "INFO" "✅ EMQX 服务已就绪(耗时 ${i} 秒)"
break
fi
else
log "ERROR" "容器意外退出"
log "INFO" "查看容器日志: docker logs $CONTAINER_NAME"
exit 1
fi
sleep 1
done
if [[ "$READY" != true ]]; then
log "WARN" "⚠️ 等待超时,EMQX 可能仍在初始化中"
log "INFO" "请稍后通过 Dashboard 或 docker logs 检查状态"
fi
# ==================== 步骤8: 验证容器状态 ====================
log "INFO" "🔍 验证容器状态..."
if $SUDO docker ps --format '{{.Names}}' | grep -qw "$CONTAINER_NAME"; then
log "INFO" "✅ 容器运行正常"
$SUDO docker ps --filter "name=$CONTAINER_NAME" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
else
log "ERROR" "容器启动后未运行"
log "INFO" "查看容器日志: docker logs $CONTAINER_NAME"
exit 1
fi
# ==================== 步骤9: 检查集群状态 ====================
log "INFO" "🔍 检查 EMQX 集群状态..."
sleep 5
# 尝试使用 emqx ctl 命令检查集群
CLUSTER_STATUS=$($SUDO docker exec "$CONTAINER_NAME" emqx ctl cluster status 2>/dev/null || echo "")
if [[ -n "$CLUSTER_STATUS" ]]; then
log "INFO" "集群状态:"
echo "$CLUSTER_STATUS"
else
log "INFO" "集群状态命令暂未返回,节点可能仍在加入集群中"
fi
# ==================== 完成 ====================
log "INFO" "================================================================"
log "INFO" "🎉 EMQX 节点 $NODE_NUM 部署完成!"
log "INFO" "================================================================"
log "INFO" "节点信息:"
log "INFO" " - 节点名: $NODE_NAME"
log "INFO" " - 容器名: $CONTAINER_NAME"
log "INFO" " - Dashboard: http://$CURRENT_IP:18083"
log "INFO" " - MQTT 端口: $CURRENT_IP:1883"
log "INFO" " - WS 端口: $CURRENT_IP:8083"
log "INFO" ""
log "INFO" "后续步骤:"
log "INFO" " 1. 在其他两台服务器上执行相同脚本(修改 --node 参数)"
log "INFO" " 2. 所有节点启动后,通过 Dashboard 查看集群状态"
log "INFO" " 3. Dashboard 默认账号: admin / public"
log "INFO" ""
log "INFO" "验证集群命令:"
log "INFO" " docker exec $CONTAINER_NAME emqx ctl cluster status"
log "INFO" "================================================================"
exit 0
#!/bin/bash
#===============================================================================
# 脚本名称:monitor_emqx_cluster.sh
# 功能描述:EMQX 集群健康监测与自愈脚本
# 版本:V1.0
# 创建日期:2026-07-06
# 基于文档:monitor_emqx_service.sh V2.3
#
# 说明:
# 本脚本在原有单节点监测逻辑基础上,新增集群维度检测:
# 1. 集群节点数是否正常(预期 3 个节点)
# 2. 各集群节点是否在线
# 3. 脑裂检测:是否有节点从集群中脱离
# 4. 节点加入/离开事件告警
#
# 使用前提:
# - EMQX 容器名为 uemqx
# - 集群预期节点数为 3(可通过 EXPECTED_NODES 配置)
#
# 使用方法:
# chmod +x monitor_emqx_cluster.sh
# ./monitor_emqx_cluster.sh
#
# 定时任务:
# */5 * * * * /data/services/scripts/monitor_emqx_cluster.sh
#===============================================================================
# ==================== 配置参数 ====================
LOG_FILE="/data/logs/monitor_emqx_cluster.log"
STATE_FILE="/data/logs/.emqx_cluster_monitor_state"
PID_FILE="/data/logs/.emqx_cluster_monitor.pid"
MAX_LOG_SIZE=$((5*1024*1024))
LOG_RETENTION_DAYS=30
MAX_FAILURES=3
COOLDOWN_PERIOD=1800 # 30 分钟冷却期
START_WAIT_TIME=30
SERVICE_CHECK_WAIT=10
EXPECTED_NODES=3 # 集群预期节点数
CONTAINER_NAME="uemqx"
# ==================== 日志函数 ====================
log() {
local message="$(date '+%Y-%m-%d %H:%M:%S') - $1"
echo "$message"
echo "$message" >> "$LOG_FILE"
}
# ==================== 锁 ====================
acquire_lock() {
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 "WARN: 上一次脚本仍在运行 (PID: $old_pid),跳过"
exit 1
else
rm -f "$PID_FILE"
fi
fi
local pid_dir=$(dirname "$PID_FILE")
[ ! -d "$pid_dir" ] && mkdir -p "$pid_dir" 2>/dev/null
echo $$ > "$PID_FILE"
trap 'rm -f "$PID_FILE"; exit' EXIT INT TERM HUP
}
# ==================== 日志轮转 ====================
rotate_logs() {
if [ -f "$LOG_FILE" ]; then
local 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 "日志文件已轮转"
fi
fi
find "$(dirname "$LOG_FILE")" -name "monitor_emqx_cluster.log.*" -mtime +$LOG_RETENTION_DAYS -exec rm -f {} \; 2>/dev/null
}
# ==================== 状态文件 ====================
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
local cluster_state=$3
cat > "$STATE_FILE" << EOF
FAILURE_COUNT=$count
LAST_RESTART_TIME=$restart_time
LAST_CHECK_TIME=$(date +%s)
PREV_CLUSTER_STATE=$cluster_state
EOF
}
get_last_restart_time() {
if [ -f "$STATE_FILE" ]; then
source "$STATE_FILE"
echo "${LAST_RESTART_TIME:-0}"
else
echo "0"
fi
}
get_prev_cluster_state() {
if [ -f "$STATE_FILE" ]; then
source "$STATE_FILE"
echo "${PREV_CLUSTER_STATE:-}"
else
echo ""
fi
}
# ==================== 端口检测(宿主机侧) ====================
_emqx_port_listening() {
local port="$1"
if ss -tln 2>/dev/null | grep -qE ":${port}([^0-9]|$)"; then
return 0
fi
if netstat -tln 2>/dev/null | grep -qE ":${port}([^0-9]|$)"; then
return 0
fi
if (exec 3<>"/dev/tcp/127.0.0.1/${port}") 2>/dev/null; then
exec 3>&-
return 0
fi
return 1
}
# ==================== 单节点健康检查 ====================
check_node_health() {
# 检查1: 容器是否运行
if ! docker ps --format '{{.Names}}' | grep -Fxq "$CONTAINER_NAME"; then
log "ERROR: 容器 $CONTAINER_NAME 未运行"
return 1
fi
# 检查2: EMQX 进程是否存在
if ! docker exec "$CONTAINER_NAME" pgrep -f "emqx" >/dev/null 2>&1; then
log "ERROR: EMQX 进程未运行"
return 1
fi
# 检查3: 核心端口
local failed_ports=0
for port in 1883 8083 8883; do
if ! _emqx_port_listening "$port"; then
((failed_ports++))
fi
done
if [ "$failed_ports" -ge 2 ] || ! _emqx_port_listening 1883; then
log "ERROR: 端口检测失败 (失败数: $failed_ports)"
return 1
fi
# 检查4: EMQX 服务状态
local status_output
status_output=$(docker exec "$CONTAINER_NAME" emqx ctl status 2>/dev/null || true)
if ! echo "$status_output" | grep -qE "is started|is running"; then
log "ERROR: EMQX 服务状态异常"
return 1
fi
return 0
}
# ==================== 集群状态检查 ====================
check_cluster_health() {
local cluster_output
cluster_output=$(docker exec "$CONTAINER_NAME" emqx ctl cluster status 2>/dev/null || echo "")
if [[ -z "$cluster_output" ]]; then
log "ERROR: 无法获取集群状态"
return 1
fi
# 统计运行中的节点数
local running_count=0
local stopped_count=0
local all_nodes=""
# 解析集群状态输出,EMQX 5.x/6.x 格式
# 示例输出:
# Cluster status: #{running_nodes => ['emqx@10.0.0.1','emqx@10.0.0.2'],
# stopped_nodes => ['emqx@10.0.0.3']}
running_count=$(echo "$cluster_output" | grep -oP "'emqx@[^']*'" | head -10 | wc -l)
# 简化版解析:统计 emqx@ 出现次数
local node_count
node_count=$(echo "$cluster_output" | grep -o "emqx@" | wc -l)
log "INFO: 集群节点总数: $node_count"
if [[ "$node_count" -lt "$EXPECTED_NODES" ]]; then
log "WARN: 集群节点不足!当前: $node_count, 预期: $EXPECTED_NODES"
return 1
fi
# 检测节点变动
local prev_state=$(get_prev_cluster_state)
local curr_state="$node_count"
if [[ -n "$prev_state" && "$prev_state" != "$curr_state" ]]; then
log "WARN: 集群节点数发生变化!$prev_state -> $curr_state"
fi
log "INFO: 集群节点数正常 ($node_count/$EXPECTED_NODES)"
return 0
}
# ==================== 脑裂检测 ====================
check_split_brain() {
# 脑裂检测:如果某个节点的集群视图只有自身,则为脑裂
local cluster_output
cluster_output=$(docker exec "$CONTAINER_NAME" emqx ctl cluster status 2>/dev/null || echo "")
if [[ -z "$cluster_output" ]]; then
return 1
fi
local node_count
node_count=$(echo "$cluster_output" | grep -o "emqx@" | wc -l)
# 如果只看到自己一个节点,且预期有3个节点,可能是脑裂
if [[ "$node_count" -eq 1 && "$EXPECTED_NODES" -gt 1 ]]; then
log "CRITICAL: 疑似脑裂!当前节点仅看到自身"
log "CRITICAL: 集群状态: $cluster_output"
return 1
fi
return 0
}
# ==================== 综合检查 ====================
comprehensive_health_check() {
# 1. 节点健康
if ! check_node_health; then
return 1
fi
# 2. 集群健康
if ! check_cluster_health; then
return 1
fi
# 3. 脑裂检测
if ! check_split_brain; then
return 1
fi
return 0
}
# ==================== 重启 ====================
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 "INFO: 开始执行 EMQX 容器重启"
log "INFO: 距上次重启: ${time_since_restart}秒"
# 冷却期检查
if [ $last_restart_time -gt 0 ] && [ $time_since_restart -lt $COOLDOWN_PERIOD ]; then
log "WARN: 冷却期内(${COOLDOWN_PERIOD}秒),跳过重启"
return 1
fi
# 停止容器
docker stop "$CONTAINER_NAME" >/dev/null 2>&1 || true
sleep 2
# 启动容器
if docker start "$CONTAINER_NAME"; then
log "INFO: 容器启动命令成功"
# 等待启动
log "INFO: 等待 ${START_WAIT_TIME} 秒..."
sleep "$START_WAIT_TIME"
# 验证
if check_node_health; then
log "INFO: EMQX 服务重启验证通过"
# 等待集群重新加入
log "INFO: 等待集群重新加入(${SERVICE_CHECK_WAIT} 秒)..."
sleep "$SERVICE_CHECK_WAIT"
if check_cluster_health; then
log "INFO: 集群重新加入成功"
write_state_file 0 "$current_time" ""
return 0
else
log "WARN: 集群重新加入可能未完成,等待下次检查"
write_state_file 0 "$current_time" ""
return 0
fi
else
log "ERROR: 重启验证失败"
write_state_file 0 "$current_time" ""
return 1
fi
else
log "ERROR: 容器启动失败"
return 1
fi
}
# ==================== 集群恢复 ====================
recover_cluster() {
# 当集群节点不足但节点自身健康时,尝试集群恢复
log "INFO: 尝试集群恢复..."
# 方法1:手动加入集群
local current_ip
current_ip=$(hostname -I | awk '{print $1}')
local my_node="emqx@${current_ip}"
# 尝试通过 emqx ctl cluster join 重新加入
# 这里需要知道其他活跃节点的 IP(可通过配置传入)
# 暂保守处理:只记录日志,超过阈值后重启
log "WARN: 集群恢复需要通过 Dashboard 或手动操作完成"
log "WARN: 登录 Dashboard 检查: http://${current_ip}:18083"
return 1
}
# ==================== 主逻辑 ====================
main() {
acquire_lock
rotate_logs
log "==========================================="
log "EMQX 集群健康检查开始"
log "预期节点数: $EXPECTED_NODES"
log "最大失败次数: $MAX_FAILURES"
log "冷却时间: ${COOLDOWN_PERIOD}秒"
log "==========================================="
# 确保日志目录存在
local log_dir=$(dirname "$LOG_FILE")
[ ! -d "$log_dir" ] && mkdir -p "$log_dir" 2>/dev/null
# 读取状态
local failure_count=$(read_state_file)
local last_restart_time=$(get_last_restart_time)
log "INFO: 当前失败次数=$failure_count, 上次重启=$last_restart_time"
# 执行综合检查
if comprehensive_health_check; then
if [ "$failure_count" -gt 0 ]; then
write_state_file 0 "$last_restart_time" "$EXPECTED_NODES"
log "INFO: 检查通过,重置失败计数器"
fi
log "INFO: EMQX 集群状态正常"
# 更新集群状态快照
local curr_node_count
curr_node_count=$(docker exec "$CONTAINER_NAME" emqx ctl cluster status 2>/dev/null | grep -o "emqx@" | wc -l)
write_state_file 0 "$last_restart_time" "$curr_node_count"
else
failure_count=$((failure_count + 1))
write_state_file "$failure_count" "$last_restart_time" ""
log "WARN: 集群健康检查失败 ($failure_count/$MAX_FAILURES)"
if [ "$failure_count" -ge "$MAX_FAILURES" ]; then
log "ERROR: 连续失败达到阈值,触发恢复"
write_state_file 0 "$last_restart_time" ""
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 "WARN: 冷却期内,跳过操作"
else
# 先尝试集群恢复
if ! recover_cluster; then
# 集群恢复失败,尝试重启容器
restart_emqx_container
fi
fi
else
log "WARN: 未达到重启阈值,等待下次检查"
fi
fi
log "==========================================="
log "EMQX 集群健康检查完成"
log "==========================================="
}
main
#!/bin/bash
#==============================================================#
# 脚本名: import_dm8_databases.sh
# 功能: 将 databases/ 目录下的 DDL SQL 文件导入到 DM8 容器
# 说明: 1. 自动创建用户(如果不存在),密码满足达梦复杂度规则
# 2. 用管道方式执行 DDL SQL(避免 disql \` backtick 截断问题)
# 3. 验证导入后的表数量
# 作者: [ubains]
# 注意事项:
# - 达梦密码需满足复杂度:≥9位,含大小写+数字+特殊符号
# - disql 不支持分号分隔多语句,CREATE 和 GRANT 必分开执行
# - 密码含 ! 时需用单引号避免 bash 历史扩展
#==============================================================#
set -e
# ------------------- 可配置参数 -------------------
CONTAINER="${DM_CONTAINER:-dm8-server}"
SYSDBA_PWD="${DM_PWD:-dNrprU&2S!}"
DM_PORT="${DM_PORT:-5236}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DDL_DIR="$SCRIPT_DIR/databases"
HOST_BACKUP_DIR="/data/middleware/dm/bak"
# 要导入的用户列表(按依赖顺序)
USERS=(
"DM_NACOS"
"DM_UBAINS"
"DM_UBAINS_SSO"
"DM_DEVOPS"
"DM_DEVOPS_VOICE"
"DM_HUAZHAO2"
"DM_OFFLINE"
)
# 用户默认密码(必须满足达梦复杂度:≥9位,含大小写+数字+特殊符号)
declare -A USER_PASSWORDS=(
["DM_NACOS"]="dNrprU&2S!"
["DM_UBAINS"]="dNrprU&2S!"
["DM_UBAINS_SSO"]="dNrprU&2S!"
["DM_DEVOPS"]="dNrprU&2S!"
["DM_DEVOPS_VOICE"]="dNrprU&2S!"
["DM_HUAZHAO2"]="dNrprU&2S!"
["DM_OFFLINE"]="dNrprU&2S!"
)
# ------------------- 颜色输出 -------------------
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
# ------------------- 检查容器 -------------------
check_container() {
if ! docker ps --format '{{.Names}}' | grep -qw "$CONTAINER"; then
log_error "容器 '$CONTAINER' 未运行,请先启动"
exit 1
fi
log_info "容器 '$CONTAINER' 已就绪"
}
# ------------------- 创建用户(密码满足达梦复杂度) -------------------
create_user() {
local user=$1
local pwd=${USER_PASSWORDS[$user]:-"User@2026Pwd"}
log_info "检查用户 $user 是否存在..."
# 检查用户是否存在(用单引号避免 ! 历史扩展)
local exists=$(docker exec "$CONTAINER" /opt/dmdbms/bin/disql SYSDBA/'dNrprU&2S!'@localhost:$DM_PORT \
-e "SELECT COUNT(*) FROM DBA_USERS WHERE USERNAME='$user';" 2>/dev/null | grep -E '^[0-9]+$' | head -1)
if [[ "$exists" == "0" ]]; then
log_info "创建用户 $user(密码复杂度达标)..."
# CREATE 和 GRANT 分开执行(disql 不支持分号合并)
docker exec "$CONTAINER" /opt/dmdbms/bin/disql SYSDBA/'dNrprU&2S!'@localhost:$DM_PORT \
-e "CREATE USER \"$user\" IDENTIFIED BY \"$pwd\";" 2>/dev/null || {
log_warn "创建用户失败,尝试用复杂密码重试..."
docker exec "$CONTAINER" /opt/dmdbms/bin/disql SYSDBA/'dNrprU&2S!'@localhost:$DM_PORT \
-e "CREATE USER \"$user\" IDENTIFIED BY \"${user}Pwd@2026\";"
}
docker exec "$CONTAINER" /opt/dmdbms/bin/disql SYSDBA/'dNrprU&2S!'@localhost:$DM_PORT \
-e "GRANT DBA TO \"$user\";" 2>/dev/null
log_info "用户 $user 创建成功,密码: $pwd"
else
log_info "用户 $user 已存在,跳过创建"
fi
}
# ------------------- 导入 DDL SQL(管道方式,避免截断) -------------------
import_ddl() {
local user=$1
local ddl_file=$(ls "$DDL_DIR"/${user}_ddl.sql 2>/dev/null | head -1)
if [[ -z "$ddl_file" ]]; then
log_warn "未找到 $user 的 DDL SQL 文件,跳过"
return 0
fi
local ddl_name=$(basename "$ddl_file")
log_info "导入 $user DDL..."
log_info " 文件: $ddl_name ($(wc -l < "$ddl_file") 行)"
# 清理 DDL 文件:去掉 spool 残留的 SQL> 提示符、SELECT 查询行、空行、多余分号
local cleaned_file="/tmp/${ddl_name}"
cp "$ddl_file" "$cleaned_file"
sed -i '/^SQL>/d' "$cleaned_file" 2>/dev/null || true
sed -i '/^SELECT DBMS_METADATA/d' "$cleaned_file" 2>/dev/null || true
sed -i '/^$/d' "$cleaned_file" 2>/dev/null || true
perl -i -pe 's/;{2,}$/;/g' "$cleaned_file" 2>/dev/null || true
log_info " 清理后: $(wc -l < "$cleaned_file") 行"
# 复制 SQL 到容器
docker cp "$cleaned_file" "$CONTAINER:/opt/dmdbms/bak/$ddl_name"
rm -f "$cleaned_file"
# 用管道方式执行 disql(用单引号避免 ! 历史扩展)
# 关键: cat | disql 方式不会截断超长 DDL,且能正确执行所有 CREATE TABLE 语句
local result=$(docker exec "$CONTAINER" bash -c \
"export LD_LIBRARY_PATH=/opt/dmdbms/bin && \
cat /opt/dmdbms/bak/$ddl_name | /opt/dmdbms/bin/disql SYSDBA/dNrprU\&2S\!@localhost:$DM_PORT" 2>&1)
# 统计执行成功的语句数
local success_count=$(echo "$result" | grep -c "executed successfully" || true)
log_info " 执行成功: $success_count 条语句"
# 清理容器内 SQL
docker exec "$CONTAINER" rm -f "/opt/dmdbms/bak/$ddl_name"
log_info "导入 $user DDL 完成"
}
# ------------------- 验证表数量(取数字行,避免端口号干扰) -------------------
verify_tables() {
log_info "验证各用户表数量..."
echo ""
printf "%-20s %10s\n" "用户" "表数量"
printf "%-20s %10s\n" "----" "------"
for user in "${USERS[@]}"; do
local cnt=$(docker exec "$CONTAINER" /opt/dmdbms/bin/disql SYSDBA/'dNrprU&2S!'@localhost:$DM_PORT \
-e "SELECT COUNT(*) FROM DBA_TABLES WHERE OWNER='$user';" 2>/dev/null | \
grep -E '^[0-9]+$' | head -1)
printf "%-20s %10s\n" "$user" "${cnt:-0}"
done
echo ""
}
# ------------------- 主流程 -------------------
main() {
log_info "=============================================="
log_info "DM8 DDL 导入工具(SQL 方式,v2)"
log_info "=============================================="
log_info "容器: $CONTAINER"
log_info "DDL 目录: $DDL_DIR"
log_info "目标用户: ${USERS[*]}"
echo ""
check_container
mkdir -p "$HOST_BACKUP_DIR"
docker exec "$CONTAINER" mkdir -p /opt/dmdbms/bak 2>/dev/null || true
for user in "${USERS[@]}"; do
echo ""
log_info "=========================================="
log_info "处理用户: $user"
log_info "=========================================="
create_user "$user"
import_ddl "$user"
done
echo ""
log_info "=============================================="
log_info "导入完成"
log_info "=============================================="
verify_tables
}
main "$@"
\ No newline at end of file
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论