合并 internal → main: 退款/提现/激活/CI 全面优化 #4
@@ -1,18 +1,5 @@
|
|||||||
# 复制此文件为 .env 并填写真实值
|
# 复制此文件为 .env 并填写真实值
|
||||||
# cp .env.example .env
|
# cp .env.example .env
|
||||||
|
|
||||||
# MySQL root 密码(同时需要在 configs/ppanel.yaml 的 MySQL.Password 中填写相同的值)
|
|
||||||
MYSQL_ROOT_PASSWORD=CHANGE_ME_TO_STRONG_PASSWORD
|
|
||||||
|
|
||||||
# Grafana 管理员密码
|
|
||||||
GRAFANA_PASSWORD=CHANGE_ME_TO_STRONG_PASSWORD
|
|
||||||
|
|
||||||
# PPanel Server 镜像标签(由 CI/CD 传入不可变 tag,如 git SHA)
|
# PPanel Server 镜像标签(由 CI/CD 传入不可变 tag,如 git SHA)
|
||||||
PPANEL_SERVER_TAG=CHANGE_ME_TO_GIT_SHA
|
PPANEL_SERVER_TAG=CHANGE_ME_TO_GIT_SHA
|
||||||
|
|
||||||
# AWS 区域(香港)
|
|
||||||
AWS_REGION=ap-east-1
|
|
||||||
|
|
||||||
# Grafana 公开域名(如需反代)
|
|
||||||
GRAFANA_DOMAIN=logs-new.hifast.biz
|
|
||||||
GRAFANA_ROOT_URL=https://logs-new.hifast.biz
|
|
||||||
|
|||||||
@@ -1,363 +0,0 @@
|
|||||||
# PPanel 香港区新 AWS 账号部署说明
|
|
||||||
|
|
||||||
本目录用于在 **新 AWS 账号** 中按 **香港区 `ap-east-1`** 重建一套全新空环境。
|
|
||||||
|
|
||||||
目标架构:
|
|
||||||
|
|
||||||
`DNS -> ALB -> WAF -> EC2(Nginx + ppanel-server + Redis + observability) -> RDS MySQL`
|
|
||||||
|
|
||||||
## 1. 资源清单
|
|
||||||
|
|
||||||
按下面顺序创建资源:
|
|
||||||
|
|
||||||
1. VPC
|
|
||||||
2. 2 个公有子网 + 2 个私有子网
|
|
||||||
3. Internet Gateway
|
|
||||||
4. 公有 / 私有路由表
|
|
||||||
5. 安全组
|
|
||||||
6. RDS MySQL
|
|
||||||
7. EC2 本机 Redis Docker
|
|
||||||
8. EC2
|
|
||||||
9. ACM 证书
|
|
||||||
10. ALB + Target Group
|
|
||||||
11. WAF Web ACL
|
|
||||||
12. 平行环境域名
|
|
||||||
|
|
||||||
建议命名:
|
|
||||||
|
|
||||||
- VPC: `ppanel-hk-prod`
|
|
||||||
- EC2: `ppanel-app-hk-01`
|
|
||||||
- RDS: `ppanel-mysql-hk`
|
|
||||||
- Redis container: `hifast-redis`
|
|
||||||
- ALB: `ppanel-alb-hk`
|
|
||||||
- WAF: `ppanel-waf-hk`
|
|
||||||
|
|
||||||
## 2. 默认规格
|
|
||||||
|
|
||||||
### EC2
|
|
||||||
|
|
||||||
- Region: `ap-east-1`
|
|
||||||
- OS: Ubuntu 24.04 LTS
|
|
||||||
- Instance type: `t4g.large` 起步
|
|
||||||
- Disk: `gp3 80GB`
|
|
||||||
- Public subnet: 是
|
|
||||||
- IAM Role: 允许读取 CloudWatch / SSM(如使用)
|
|
||||||
- 如果要在 AWS EC2 本机执行 S3 备份:额外允许写入专用备份桶
|
|
||||||
|
|
||||||
### RDS MySQL
|
|
||||||
|
|
||||||
- Engine: MySQL 8.0
|
|
||||||
- Class: `db.r7g.xlarge`
|
|
||||||
- Storage: `gp3 100GB`
|
|
||||||
- DB name: `hifast`
|
|
||||||
- Username: `admin`
|
|
||||||
- Public access: `No`
|
|
||||||
- Charset: `utf8mb4`
|
|
||||||
- Backup: `7-14 days`
|
|
||||||
|
|
||||||
### Redis
|
|
||||||
|
|
||||||
- 部署位置:业务 EC2 本机
|
|
||||||
- 部署方式:Docker
|
|
||||||
- 版本:`redis:8.2.1`
|
|
||||||
- 监听:`0.0.0.0:6379`
|
|
||||||
- 应用连接:`127.0.0.1:6379`
|
|
||||||
- 安全组:仅对白名单备用节点或同机应用开放
|
|
||||||
|
|
||||||
## 3. 网络与安全组
|
|
||||||
|
|
||||||
### 子网布局
|
|
||||||
|
|
||||||
- `public-a`, `public-b`: ALB / EC2
|
|
||||||
- `private-a`, `private-b`: RDS
|
|
||||||
|
|
||||||
### 安全组建议
|
|
||||||
|
|
||||||
#### `sg-alb`
|
|
||||||
|
|
||||||
- Inbound
|
|
||||||
- `80/tcp` from `0.0.0.0/0`
|
|
||||||
- `443/tcp` from `0.0.0.0/0`
|
|
||||||
- Outbound
|
|
||||||
- `80/tcp` to `sg-ec2`
|
|
||||||
|
|
||||||
#### `sg-ec2`
|
|
||||||
|
|
||||||
- Inbound
|
|
||||||
- `80/tcp` from `sg-alb`
|
|
||||||
- `22/tcp` from `你的固定运维 IP`
|
|
||||||
- Outbound
|
|
||||||
- all
|
|
||||||
|
|
||||||
说明:
|
|
||||||
|
|
||||||
- 应用容器监听 `127.0.0.1:8080`
|
|
||||||
- EC2 对外只让 Nginx 监听 `80`
|
|
||||||
- Grafana / Prometheus / Tempo 仅监听 `127.0.0.1`
|
|
||||||
|
|
||||||
#### `sg-rds`
|
|
||||||
|
|
||||||
- Inbound
|
|
||||||
- `3306/tcp` from `sg-ec2`
|
|
||||||
|
|
||||||
#### `sg-ec2` 额外说明
|
|
||||||
|
|
||||||
- 如果需要外部备用节点复制 Redis,再额外放行:
|
|
||||||
- `6379/tcp` from `104.238.220.230/32`
|
|
||||||
|
|
||||||
## 4. ALB / Target Group / 健康检查
|
|
||||||
|
|
||||||
### Target Group
|
|
||||||
|
|
||||||
- Type: `Instance`
|
|
||||||
- Protocol: `HTTP`
|
|
||||||
- Port: `80`
|
|
||||||
- Health check path: `/v1/common/heartbeat`
|
|
||||||
- Success code: `200`
|
|
||||||
|
|
||||||
这个路径已由项目现有接口提供,无需额外改代码。
|
|
||||||
|
|
||||||
### ALB 监听器
|
|
||||||
|
|
||||||
- `80` -> redirect to `443`
|
|
||||||
- `443` -> forward 到 target group
|
|
||||||
|
|
||||||
### ACM
|
|
||||||
|
|
||||||
- 在 `ap-east-1` 申请证书
|
|
||||||
- 先给平行环境域名,例如:
|
|
||||||
- `api-new.hifast.biz`
|
|
||||||
- `logs-new.hifast.biz`
|
|
||||||
|
|
||||||
## 5. WAF 规则
|
|
||||||
|
|
||||||
首版至少启用:
|
|
||||||
|
|
||||||
1. `AWSManagedRulesCommonRuleSet`
|
|
||||||
2. `AWSManagedRulesKnownBadInputsRuleSet`
|
|
||||||
3. `AWSManagedRulesAmazonIpReputationList`
|
|
||||||
4. 全站 rate-based rule
|
|
||||||
5. 针对高风险路径的 rate-based rule
|
|
||||||
|
|
||||||
建议的第一版限流:
|
|
||||||
|
|
||||||
- 全站:每 IP `2000 / 5 分钟`
|
|
||||||
- `/v1/public/user/subscribe`:每 IP `300 / 5 分钟`
|
|
||||||
- 登录 / 注册 / 验证码接口:每 IP `100 / 5 分钟`
|
|
||||||
|
|
||||||
节点上报接口建议后续补:
|
|
||||||
|
|
||||||
- `/v1/server/status`
|
|
||||||
- `/v1/server/online`
|
|
||||||
- `/v1/server/traffic`
|
|
||||||
|
|
||||||
优先用节点出口 IP 白名单;没有固定出口 IP 的节点暂时保留 `secret_key`,但不要把它当成唯一防线。
|
|
||||||
|
|
||||||
## 6. EC2 文件落地
|
|
||||||
|
|
||||||
在 EC2 上建议使用:
|
|
||||||
|
|
||||||
- 应用目录:`/opt/ppanel`
|
|
||||||
- Nginx 配置:`/etc/nginx/sites-available/ppanel-api.conf`
|
|
||||||
|
|
||||||
需要上传这些文件 / 目录:
|
|
||||||
|
|
||||||
- `docker-compose.cloud.yml`
|
|
||||||
- `deploy/aws/ap-east-1/configs/ppanel.yaml.example` -> 重命名为 `configs/ppanel.yaml`
|
|
||||||
- `deploy/aws/ap-east-1/nginx/ppanel-api.conf`
|
|
||||||
- `grafana/`
|
|
||||||
- `loki/`
|
|
||||||
- `prometheus/`
|
|
||||||
- `tempo/`
|
|
||||||
- `.env.example` -> 重命名为 `.env`
|
|
||||||
|
|
||||||
目标目录示例:
|
|
||||||
|
|
||||||
```text
|
|
||||||
/opt/ppanel/
|
|
||||||
docker-compose.cloud.yml
|
|
||||||
.env
|
|
||||||
configs/ppanel.yaml
|
|
||||||
grafana/
|
|
||||||
loki/
|
|
||||||
prometheus/
|
|
||||||
tempo/
|
|
||||||
logs/
|
|
||||||
cache/
|
|
||||||
tempo_data/
|
|
||||||
```
|
|
||||||
|
|
||||||
## 7. 应用配置
|
|
||||||
|
|
||||||
基线模板见:
|
|
||||||
|
|
||||||
- [`configs/ppanel.yaml.example`](./configs/ppanel.yaml.example)
|
|
||||||
- [`nginx/ppanel-api.conf`](./nginx/ppanel-api.conf)
|
|
||||||
|
|
||||||
关键值必须替换:
|
|
||||||
|
|
||||||
- `MySQL.Addr`
|
|
||||||
- `MySQL.Password`
|
|
||||||
- `Redis.Host`
|
|
||||||
- `Redis.Pass`
|
|
||||||
- `JwtAuth.AccessSecret`
|
|
||||||
- `Administrator.Email`
|
|
||||||
- `Administrator.Password`
|
|
||||||
- `AppSignature.AppSecrets.*`
|
|
||||||
- `device.security_secret`
|
|
||||||
- `Site.Host`
|
|
||||||
- `Site.SiteName`
|
|
||||||
|
|
||||||
Redis 约定保持不变:
|
|
||||||
|
|
||||||
- 业务缓存:DB `0`
|
|
||||||
- Asynq:DB `5`(代码内部已固定使用)
|
|
||||||
|
|
||||||
## 8. 部署步骤
|
|
||||||
|
|
||||||
### 8.1 初始化 EC2
|
|
||||||
|
|
||||||
把脚本上传到 EC2 后执行:
|
|
||||||
|
|
||||||
## 9. 104 灾备节点常用运维脚本
|
|
||||||
|
|
||||||
如果你要在 `104.238.220.230` 上执行数据迁移、主从重拉、主库提升,可以直接复用仓库里的这几份脚本:
|
|
||||||
|
|
||||||
- 数据导出 / 导入交互工具:
|
|
||||||
- [`deploy/scripts/hifast_data_sync_tool.sh`](/Users/Apple/code_vpn/vpn/ppanel-server/deploy/scripts/hifast_data_sync_tool.sh)
|
|
||||||
- MySQL 主从运维工具:
|
|
||||||
- [`deploy/scripts/mysql_replica_ops.sh`](/Users/Apple/code_vpn/vpn/ppanel-server/deploy/scripts/mysql_replica_ops.sh)
|
|
||||||
- Redis 主从运维工具:
|
|
||||||
- [`deploy/scripts/redis_replica_ops.sh`](/Users/Apple/code_vpn/vpn/ppanel-server/deploy/scripts/redis_replica_ops.sh)
|
|
||||||
- 统一总入口:
|
|
||||||
- [`deploy/scripts/hifast_data_sync_tool.sh`](/Users/Apple/code_vpn/vpn/ppanel-server/deploy/scripts/hifast_data_sync_tool.sh)
|
|
||||||
- 主从运维环境模板:
|
|
||||||
- [`deploy/aws/ap-east-1/configs/replica-ops.env.example`](/Users/Apple/code_vpn/vpn/ppanel-server/deploy/aws/ap-east-1/configs/replica-ops.env.example)
|
|
||||||
|
|
||||||
### 9.1 数据迁移工具
|
|
||||||
|
|
||||||
支持:
|
|
||||||
|
|
||||||
- 备份 MySQL 到 S3
|
|
||||||
- 备份 Redis 到 S3
|
|
||||||
- 从正式库导出 MySQL `sql.gz`
|
|
||||||
- 把 `sql.gz` 导入 AWS RDS
|
|
||||||
- 从正式 Redis 导出 `RDB`
|
|
||||||
- 将 `RDB` 导入 Docker Redis 或宿主机 Redis
|
|
||||||
- 查看 MySQL / Redis 当前主从状态
|
|
||||||
- 强制重拉 MySQL / Redis 主从
|
|
||||||
- 把 MySQL / Redis 从库提升为可写主库
|
|
||||||
|
|
||||||
示例:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bash deploy/scripts/hifast_data_sync_tool.sh /root/replica-ops.env
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash
|
|
||||||
chmod +x deploy/scripts/bootstrap_aws_ec2.sh
|
|
||||||
sudo APP_DIR=/opt/ppanel deploy/scripts/bootstrap_aws_ec2.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
### 8.2 安装 Nginx 配置
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo cp deploy/aws/ap-east-1/nginx/ppanel-api.conf /etc/nginx/sites-available/ppanel-api.conf
|
|
||||||
sudo ln -sf /etc/nginx/sites-available/ppanel-api.conf /etc/nginx/sites-enabled/ppanel-api.conf
|
|
||||||
sudo nginx -t
|
|
||||||
sudo systemctl reload nginx
|
|
||||||
```
|
|
||||||
|
|
||||||
### 8.3 启动容器
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /opt/ppanel
|
|
||||||
docker compose -f docker-compose.cloud.yml up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
### 8.4 预检
|
|
||||||
|
|
||||||
```bash
|
|
||||||
chmod +x deploy/scripts/preflight_aws_hk.sh
|
|
||||||
APP_DIR=/opt/ppanel \
|
|
||||||
RDS_HOST=<new-rds-endpoint> \
|
|
||||||
REDIS_HOST=127.0.0.1 \
|
|
||||||
deploy/scripts/preflight_aws_hk.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
## 9. 平行环境验证
|
|
||||||
|
|
||||||
先验证 `api-new.hifast.biz`,不要直接切正式域名。
|
|
||||||
|
|
||||||
必测项:
|
|
||||||
|
|
||||||
1. `ALB target` 为 healthy
|
|
||||||
2. `GET /v1/common/heartbeat` 返回 200
|
|
||||||
3. 管理员登录
|
|
||||||
4. 用户注册 / 登录
|
|
||||||
5. 订阅查询
|
|
||||||
6. 节点上报 `/v1/server/status`
|
|
||||||
7. 本机 Redis 可写缓存
|
|
||||||
8. Asynq 可入队并消费
|
|
||||||
|
|
||||||
## 10. 正式切换
|
|
||||||
|
|
||||||
切换前检查:
|
|
||||||
|
|
||||||
1. ALB 5xx 为 0
|
|
||||||
2. EC2 CPU / Memory 正常
|
|
||||||
3. RDS CPU / Connections 正常
|
|
||||||
4. 本机 Redis CPU / Connections / Memory 正常
|
|
||||||
5. WAF 已挂到 ALB
|
|
||||||
6. EC2 安全组没有对公网放 `8080/3333/9090/4317`
|
|
||||||
|
|
||||||
切换方式:
|
|
||||||
|
|
||||||
1. 保持新环境先跑平行域名
|
|
||||||
2. 正式域名切到新 ALB
|
|
||||||
3. 观察至少 1 小时
|
|
||||||
4. 确认无误后再处理旧环境
|
|
||||||
|
|
||||||
## 11. 监控建议
|
|
||||||
|
|
||||||
至少建这些 CloudWatch / Grafana 观测项:
|
|
||||||
|
|
||||||
- ALB `RequestCount`, `HTTPCode_ELB_5XX_Count`, `TargetResponseTime`
|
|
||||||
- EC2 `CPUUtilization`, `NetworkIn`, `NetworkOut`, `StatusCheckFailed`
|
|
||||||
- RDS `CPUUtilization`, `DatabaseConnections`, `ReadLatency`, `WriteLatency`
|
|
||||||
- Redis 容器 CPU / Memory / restart count
|
|
||||||
|
|
||||||
## 12. 这次方案的边界
|
|
||||||
|
|
||||||
本目录交付的是:
|
|
||||||
|
|
||||||
- 香港区新账号的部署模板
|
|
||||||
- 新空环境启动与验证流程
|
|
||||||
- ALB / WAF / EC2 / RDS / 本机 Redis 的落地约定
|
|
||||||
|
|
||||||
不包含:
|
|
||||||
|
|
||||||
- 旧数据迁移
|
|
||||||
- Terraform / CloudFormation 自动建资源
|
|
||||||
- Redis 托管版改造
|
|
||||||
- 多活 / 自动扩缩容
|
|
||||||
|
|
||||||
## 13. S3 备份补强
|
|
||||||
|
|
||||||
当前已落地的 S3 备份桶:
|
|
||||||
|
|
||||||
- `hifast-prod-backups-200810848252-ap-east-1`
|
|
||||||
|
|
||||||
建议与现网结合方式:
|
|
||||||
|
|
||||||
1. `RDS automated backup` 继续保留,作为第一层恢复能力
|
|
||||||
2. `104` 外部 MySQL 从库执行逻辑备份并上传到 S3,作为第二层可下载备份
|
|
||||||
3. `104` 外部 Redis 从库按需导出 `RDB` 到 S3,补齐缓存类灾备材料
|
|
||||||
|
|
||||||
仓库中已补充:
|
|
||||||
|
|
||||||
- 环境变量模板:[`configs/backup-to-s3.env.example`](./configs/backup-to-s3.env.example)
|
|
||||||
- MySQL 备份脚本:[`../../scripts/mysql_backup_to_s3.sh`](../../scripts/mysql_backup_to_s3.sh)
|
|
||||||
- Redis 备份脚本:[`../../scripts/redis_rdb_backup_to_s3.sh`](../../scripts/redis_rdb_backup_to_s3.sh)
|
|
||||||
|
|
||||||
建议把 MySQL 备份脚本优先部署到 `104`,因为它直接连接本地只读从库,对 AWS 主库扰动最小。
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
AWS_REGION=ap-east-1
|
|
||||||
S3_BUCKET=hifast-prod-backups-200810848252-ap-east-1
|
|
||||||
S3_PREFIX=mysql
|
|
||||||
BACKUP_DIR=/var/backups/hifast
|
|
||||||
HOST_TAG=104-standby
|
|
||||||
KEEP_LOCAL_DAYS=3
|
|
||||||
CHECK_REPLICA=1
|
|
||||||
|
|
||||||
MYSQL_HOST=127.0.0.1
|
|
||||||
MYSQL_PORT=3306
|
|
||||||
MYSQL_USER=backup_reader
|
|
||||||
MYSQL_PASSWORD=CHANGE_ME
|
|
||||||
MYSQL_SOCKET=
|
|
||||||
MYSQL_DATABASE=hifast
|
|
||||||
|
|
||||||
REDIS_HOST=127.0.0.1
|
|
||||||
REDIS_PORT=6379
|
|
||||||
REDIS_PASSWORD=CHANGE_ME
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
PRIMARY_HOST=hifast-mysql-prod-v2.cd6aey40m6ag.ap-east-1.rds.amazonaws.com
|
|
||||||
PRIMARY_PORT=3306
|
|
||||||
PRIMARY_USER=admin
|
|
||||||
PRIMARY_PASSWORD=CHANGE_ME
|
|
||||||
PRIMARY_DB=hifast
|
|
||||||
|
|
||||||
PRIMARY_REPL_USER=repl
|
|
||||||
PRIMARY_REPL_PASSWORD=CHANGE_ME
|
|
||||||
PRIMARY_REPL_HOST=104.238.220.230
|
|
||||||
PRIMARY_BINLOG_RETENTION_HOURS=24
|
|
||||||
|
|
||||||
REPLICA_HOST=127.0.0.1
|
|
||||||
REPLICA_PORT=3306
|
|
||||||
REPLICA_USER=root
|
|
||||||
REPLICA_PASSWORD=
|
|
||||||
REPLICA_SOCKET=/var/run/mysqld/mysqld.sock
|
|
||||||
REPLICA_DB=hifast
|
|
||||||
REPLICA_SOURCE_SSL=1
|
|
||||||
|
|
||||||
DUMP_FILE=
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
Host: 0.0.0.0
|
|
||||||
Port: 8080
|
|
||||||
Debug: false
|
|
||||||
|
|
||||||
JwtAuth:
|
|
||||||
AccessSecret: CHANGE_ME_TO_A_LONG_RANDOM_SECRET
|
|
||||||
AccessExpire: 604800
|
|
||||||
|
|
||||||
Logger:
|
|
||||||
ServiceName: PPanel
|
|
||||||
Mode: console
|
|
||||||
Encoding: plain
|
|
||||||
TimeFormat: "2006-01-02 15:04:05.000"
|
|
||||||
Path: logs
|
|
||||||
Level: info
|
|
||||||
MaxContentLength: 0
|
|
||||||
Compress: false
|
|
||||||
Stat: true
|
|
||||||
KeepDays: 7
|
|
||||||
StackCooldownMillis: 100
|
|
||||||
MaxBackups: 7
|
|
||||||
MaxSize: 100
|
|
||||||
Rotation: daily
|
|
||||||
FileTimeFormat: "2006-01-02T15:04:05.000Z07:00"
|
|
||||||
|
|
||||||
MySQL:
|
|
||||||
Addr: YOUR_RDS_ENDPOINT:3306
|
|
||||||
Dbname: hifast
|
|
||||||
Username: admin
|
|
||||||
Password: CHANGE_ME_TO_RDS_PASSWORD
|
|
||||||
Config: charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai
|
|
||||||
MaxIdleConns: 10
|
|
||||||
MaxOpenConns: 100
|
|
||||||
SlowThreshold: 1000
|
|
||||||
|
|
||||||
Redis:
|
|
||||||
Host: 127.0.0.1:6379
|
|
||||||
Pass: CHANGE_ME_TO_REDIS_PASSWORD
|
|
||||||
DB: 0
|
|
||||||
PoolSize: 100
|
|
||||||
MinIdleConns: 10
|
|
||||||
MaxRetries: 3
|
|
||||||
PoolTimeout: 4
|
|
||||||
IdleTimeout: 300
|
|
||||||
MaxConnAge: 0
|
|
||||||
DialTimeout: 5
|
|
||||||
ReadTimeout: 3
|
|
||||||
WriteTimeout: 3
|
|
||||||
|
|
||||||
Trace:
|
|
||||||
Name: ppanel-server
|
|
||||||
Endpoint: 127.0.0.1:4317
|
|
||||||
Sampler: 0.1
|
|
||||||
Batcher: otlpgrpc
|
|
||||||
|
|
||||||
Site:
|
|
||||||
Host: api-new.hifast.biz
|
|
||||||
SiteName: HiFastVPN
|
|
||||||
|
|
||||||
Administrator:
|
|
||||||
Email: admin@example.com
|
|
||||||
Password: CHANGE_ME_TO_STRONG_ADMIN_PASSWORD
|
|
||||||
|
|
||||||
Telegram:
|
|
||||||
Enable: false
|
|
||||||
BotID: 0
|
|
||||||
BotName: ""
|
|
||||||
BotToken: ""
|
|
||||||
GroupChatID: ""
|
|
||||||
EnableNotify: false
|
|
||||||
WebHookDomain: ""
|
|
||||||
|
|
||||||
Kutt:
|
|
||||||
Enable: false
|
|
||||||
ApiURL: ""
|
|
||||||
ApiKey: ""
|
|
||||||
TargetURL: ""
|
|
||||||
Domain: ""
|
|
||||||
|
|
||||||
OpenInstall:
|
|
||||||
Enable: false
|
|
||||||
AppKey: ""
|
|
||||||
ApiKey: ""
|
|
||||||
|
|
||||||
Loki:
|
|
||||||
Enable: true
|
|
||||||
URL: "http://localhost:3100"
|
|
||||||
|
|
||||||
AppSignature:
|
|
||||||
AppSecrets:
|
|
||||||
android-client: CHANGE_ME_ANDROID_SIGNATURE_SECRET
|
|
||||||
ios-client: CHANGE_ME_IOS_SIGNATURE_SECRET
|
|
||||||
web-client: CHANGE_ME_WEB_SIGNATURE_SECRET
|
|
||||||
ValidWindowSeconds: 300
|
|
||||||
SkipPrefixes:
|
|
||||||
- /v1/notify/
|
|
||||||
- /v1/iap/notifications
|
|
||||||
- /v1/telegram/webhook
|
|
||||||
- /v1/subscribe/config
|
|
||||||
|
|
||||||
Signature:
|
|
||||||
EnableSignature: false
|
|
||||||
|
|
||||||
device:
|
|
||||||
enable: true
|
|
||||||
security_secret: CHANGE_ME_DEVICE_SECURITY_SECRET
|
|
||||||
|
|
||||||
Register:
|
|
||||||
EnableTrial: true
|
|
||||||
EnableTrialEmailWhitelist: true
|
|
||||||
TrialEmailDomainWhitelist: "gmail.com,outlook.com,icloud.com,qq.com,163.com"
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
MYSQL_HOST=127.0.0.1
|
|
||||||
MYSQL_PORT=3306
|
|
||||||
MYSQL_USER=root
|
|
||||||
MYSQL_PASSWORD=CHANGE_ME
|
|
||||||
MYSQL_SOCKET=
|
|
||||||
|
|
||||||
REPL_SOURCE_HOST=hifast-mysql-prod-v2.cd6aey40m6ag.ap-east-1.rds.amazonaws.com
|
|
||||||
REPL_SOURCE_PORT=3306
|
|
||||||
REPL_SOURCE_USER=repl
|
|
||||||
REPL_SOURCE_PASSWORD=CHANGE_ME
|
|
||||||
REPL_SOURCE_SSL=1
|
|
||||||
REPL_SOURCE_LOG_FILE=
|
|
||||||
REPL_SOURCE_LOG_POS=
|
|
||||||
REPL_SOURCE_AUTO_POSITION=1
|
|
||||||
|
|
||||||
REDIS_HOST=127.0.0.1
|
|
||||||
REDIS_PORT=6379
|
|
||||||
REDIS_PASSWORD=CHANGE_ME
|
|
||||||
|
|
||||||
REDIS_SOURCE_HOST=18.163.33.75
|
|
||||||
REDIS_SOURCE_PORT=6379
|
|
||||||
REDIS_SOURCE_USER=
|
|
||||||
REDIS_SOURCE_PASSWORD=CHANGE_ME
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
server {
|
|
||||||
listen 80 default_server;
|
|
||||||
listen [::]:80 default_server;
|
|
||||||
server_name _;
|
|
||||||
|
|
||||||
client_max_body_size 20m;
|
|
||||||
|
|
||||||
access_log /var/log/nginx/ppanel-access.log;
|
|
||||||
error_log /var/log/nginx/ppanel-error.log warn;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_pass http://127.0.0.1:8080;
|
|
||||||
|
|
||||||
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_set_header X-Forwarded-Proto $scheme;
|
|
||||||
proxy_set_header X-Forwarded-Host $host;
|
|
||||||
proxy_set_header X-Forwarded-Port $server_port;
|
|
||||||
|
|
||||||
proxy_connect_timeout 10s;
|
|
||||||
proxy_send_timeout 60s;
|
|
||||||
proxy_read_timeout 60s;
|
|
||||||
}
|
|
||||||
|
|
||||||
location = /nginx_status {
|
|
||||||
stub_status;
|
|
||||||
access_log off;
|
|
||||||
allow 127.0.0.1;
|
|
||||||
deny all;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,358 +0,0 @@
|
|||||||
# PPanel 日本东京区 AWS 部署说明
|
|
||||||
|
|
||||||
本目录用于在 **AWS 日本东京区 `ap-northeast-1`** 重建一套全新生产环境,并承接当前香港区 `ap-east-1` 的正式迁移。
|
|
||||||
|
|
||||||
如果你要看“当前已经真实跑起来的东京架构”,优先看:
|
|
||||||
|
|
||||||
- [`ops/hifast-current-architecture-zh.md`](/Users/Apple/code_vpn/vpn/ppanel-server/ops/hifast-current-architecture-zh.md)
|
|
||||||
- [`configs/resource-inventory.current.md`](./configs/resource-inventory.current.md)
|
|
||||||
|
|
||||||
这份 README 更偏向:
|
|
||||||
|
|
||||||
- 目标架构
|
|
||||||
- 资源规划
|
|
||||||
- 部署方法
|
|
||||||
- 后续待完成项
|
|
||||||
|
|
||||||
目标架构:
|
|
||||||
|
|
||||||
`DNS -> ALB -> WAF -> EC2(Nginx + ppanel-server + Redis + observability) -> RDS MySQL`
|
|
||||||
|
|
||||||
灾备链路:
|
|
||||||
|
|
||||||
`RDS MySQL / EC2 Redis -> 104.238.220.230 外部灾备`
|
|
||||||
|
|
||||||
当前仓库内已补充:
|
|
||||||
|
|
||||||
- 东京基础设施参数模板:[`configs/aws-jp-infra.env.example`](./configs/aws-jp-infra.env.example)
|
|
||||||
- 东京真实实施状态登记:[`configs/resource-inventory.current.md`](./configs/resource-inventory.current.md)
|
|
||||||
- 东京底座资源创建脚本:[`../../scripts/aws_jp_create_base_infra.sh`](../../scripts/aws_jp_create_base_infra.sh)
|
|
||||||
- 东京资源状态检查脚本:[`../../scripts/aws_jp_describe_state.sh`](../../scripts/aws_jp_describe_state.sh)
|
|
||||||
- MySQL 备份脚本:[`../../scripts/mysql_backup_to_s3.sh`](../../scripts/mysql_backup_to_s3.sh)
|
|
||||||
- 10 分钟 MySQL 备份定时器安装脚本:[`../../scripts/install_mysql_backup_timer.sh`](../../scripts/install_mysql_backup_timer.sh)
|
|
||||||
|
|
||||||
## 1. 资源清单
|
|
||||||
|
|
||||||
按下面顺序创建资源:
|
|
||||||
|
|
||||||
1. VPC
|
|
||||||
2. 2 个公有子网 + 2 个私有子网
|
|
||||||
3. Internet Gateway
|
|
||||||
4. 公有 / 私有路由表
|
|
||||||
5. 安全组
|
|
||||||
6. RDS MySQL
|
|
||||||
7. EC2 本机 Redis Docker
|
|
||||||
8. EC2
|
|
||||||
9. ACM 证书
|
|
||||||
10. ALB + Target Group
|
|
||||||
11. WAF Web ACL
|
|
||||||
12. 东京平行环境域名
|
|
||||||
13. 东京 S3 备份桶
|
|
||||||
|
|
||||||
建议命名:
|
|
||||||
|
|
||||||
- VPC: `ppanel-jp-prod`
|
|
||||||
- EC2: `ppanel-app-jp-01`
|
|
||||||
- RDS: `ppanel-mysql-jp`
|
|
||||||
- Redis container: `hifast-redis`
|
|
||||||
- ALB: `ppanel-alb-jp`
|
|
||||||
- WAF: `ppanel-waf-jp`
|
|
||||||
- S3: `hifast-prod-backups-200810848252-ap-northeast-1`
|
|
||||||
|
|
||||||
## 2. 默认规格
|
|
||||||
|
|
||||||
### EC2
|
|
||||||
|
|
||||||
- Region: `ap-northeast-1`
|
|
||||||
- OS: Ubuntu 24.04 LTS
|
|
||||||
- Instance type: `t4g.large`
|
|
||||||
- Disk: `gp3 80GB`
|
|
||||||
- Public subnet: 是
|
|
||||||
- IAM Role:
|
|
||||||
- 允许读取 CloudWatch / SSM(如使用)
|
|
||||||
- 如果要在东京 EC2 上执行 S3 备份:额外允许写入东京备份桶
|
|
||||||
|
|
||||||
### RDS MySQL
|
|
||||||
|
|
||||||
- Engine: `MySQL 8.4`
|
|
||||||
- Class: `db.r7g.xlarge`
|
|
||||||
- Storage: `gp3 100GB`
|
|
||||||
- DB name: `hifast`
|
|
||||||
- Username: `admin`
|
|
||||||
- Public access: `Yes`
|
|
||||||
- Charset: `utf8mb4`
|
|
||||||
- Backup retention: `7-14 days`
|
|
||||||
- Deletion protection: `On`
|
|
||||||
- Multi-AZ: `Yes`(当前按 2 实例 Multi-AZ 创建)
|
|
||||||
|
|
||||||
说明:
|
|
||||||
|
|
||||||
- 当前东京 RDS 需要允许 `104.238.220.230` 从公网直连 `3306`,用于外部 MySQL 从库复制
|
|
||||||
- 因此本阶段 RDS 使用 `public subnet group + Publicly accessible = Yes`
|
|
||||||
- 访问面只通过 `sg-rds` 严格限制到业务 EC2 安全组和 `104.238.220.230/32`
|
|
||||||
|
|
||||||
### Redis
|
|
||||||
|
|
||||||
- 部署位置:业务 EC2 本机
|
|
||||||
- 部署方式:Docker
|
|
||||||
- 版本:`redis:8.2.1`
|
|
||||||
- 监听:`0.0.0.0:6379`
|
|
||||||
- 应用连接:`127.0.0.1:6379`
|
|
||||||
- 安全组:仅对白名单备用节点 `104.238.220.230/32` 或同机应用开放
|
|
||||||
|
|
||||||
## 3. 网络与安全组
|
|
||||||
|
|
||||||
### 子网布局
|
|
||||||
|
|
||||||
- `public-a`, `public-c`: ALB / EC2
|
|
||||||
- `private-a`, `private-c`: RDS
|
|
||||||
|
|
||||||
说明:
|
|
||||||
|
|
||||||
- 东京优先使用 `ap-northeast-1a` 和 `ap-northeast-1c`
|
|
||||||
- 如果账户映射不同,也可以用任意 2 个可用区,但公私网必须各 2 个子网
|
|
||||||
|
|
||||||
### 安全组建议
|
|
||||||
|
|
||||||
#### `sg-alb`
|
|
||||||
|
|
||||||
- Inbound
|
|
||||||
- `80/tcp` from `0.0.0.0/0`
|
|
||||||
- `443/tcp` from `0.0.0.0/0`
|
|
||||||
- Outbound
|
|
||||||
- `80/tcp` to `sg-ec2`
|
|
||||||
|
|
||||||
#### `sg-ec2`
|
|
||||||
|
|
||||||
- Inbound
|
|
||||||
- `80/tcp` from `sg-alb`
|
|
||||||
- `22/tcp` from `你的固定运维 IP`
|
|
||||||
- `6379/tcp` from `104.238.220.230/32`
|
|
||||||
- Outbound
|
|
||||||
- all
|
|
||||||
|
|
||||||
说明:
|
|
||||||
|
|
||||||
- 应用容器监听 `127.0.0.1:8080`
|
|
||||||
- EC2 对外只让 Nginx 监听 `80`
|
|
||||||
- Grafana / Prometheus / Tempo 仅监听 `127.0.0.1`
|
|
||||||
|
|
||||||
#### `sg-rds`
|
|
||||||
|
|
||||||
- Inbound
|
|
||||||
- `3306/tcp` from `sg-ec2`
|
|
||||||
- `3306/tcp` from `104.238.220.230/32`
|
|
||||||
|
|
||||||
## 4. ALB / Target Group / 健康检查
|
|
||||||
|
|
||||||
### Target Group
|
|
||||||
|
|
||||||
- Type: `Instance`
|
|
||||||
- Protocol: `HTTP`
|
|
||||||
- Port: `80`
|
|
||||||
- Health check path: `/v1/common/heartbeat`
|
|
||||||
- Success code: `200`
|
|
||||||
|
|
||||||
### ALB 监听器
|
|
||||||
|
|
||||||
- `80` -> redirect to `443`
|
|
||||||
- `443` -> forward 到 target group
|
|
||||||
|
|
||||||
### ACM
|
|
||||||
|
|
||||||
- 在 `ap-northeast-1` 重新申请证书
|
|
||||||
- 先给平行环境域名,例如:
|
|
||||||
- `api-jp.hifast.biz`
|
|
||||||
- `logs-jp.hifast.biz`
|
|
||||||
|
|
||||||
## 5. WAF 规则
|
|
||||||
|
|
||||||
首版至少启用:
|
|
||||||
|
|
||||||
1. `AWSManagedRulesCommonRuleSet`
|
|
||||||
2. `AWSManagedRulesKnownBadInputsRuleSet`
|
|
||||||
3. `AWSManagedRulesAmazonIpReputationList`
|
|
||||||
4. 全站 rate-based rule
|
|
||||||
5. 针对高风险路径的 rate-based rule
|
|
||||||
|
|
||||||
建议的第一版限流:
|
|
||||||
|
|
||||||
- 全站:每 IP `2000 / 5 分钟`
|
|
||||||
- `/v1/public/user/subscribe`:每 IP `300 / 5 分钟`
|
|
||||||
- 登录 / 注册 / 验证码接口:每 IP `100 / 5 分钟`
|
|
||||||
|
|
||||||
节点上报接口建议后续补:
|
|
||||||
|
|
||||||
- `/v1/server/status`
|
|
||||||
- `/v1/server/online`
|
|
||||||
- `/v1/server/traffic`
|
|
||||||
|
|
||||||
## 6. EC2 文件落地
|
|
||||||
|
|
||||||
在 EC2 上建议使用:
|
|
||||||
|
|
||||||
- 应用目录:`/opt/ppanel`
|
|
||||||
- Nginx 配置:`/etc/nginx/sites-available/ppanel-api.conf`
|
|
||||||
|
|
||||||
需要上传这些文件 / 目录:
|
|
||||||
|
|
||||||
- `docker-compose.cloud.yml`
|
|
||||||
- `deploy/aws/ap-northeast-1/configs/ppanel.yaml.example` -> 重命名为 `configs/ppanel.yaml`
|
|
||||||
- `deploy/aws/ap-northeast-1/nginx/ppanel-api.conf`
|
|
||||||
- `grafana/`
|
|
||||||
- `loki/`
|
|
||||||
- `prometheus/`
|
|
||||||
- `tempo/`
|
|
||||||
- `.env.example` -> 重命名为 `.env`
|
|
||||||
|
|
||||||
目标目录示例:
|
|
||||||
|
|
||||||
```text
|
|
||||||
/opt/ppanel/
|
|
||||||
docker-compose.cloud.yml
|
|
||||||
.env
|
|
||||||
configs/ppanel.yaml
|
|
||||||
grafana/
|
|
||||||
loki/
|
|
||||||
prometheus/
|
|
||||||
tempo/
|
|
||||||
logs/
|
|
||||||
cache/
|
|
||||||
tempo_data/
|
|
||||||
```
|
|
||||||
|
|
||||||
## 7. 应用配置
|
|
||||||
|
|
||||||
基线模板见:
|
|
||||||
|
|
||||||
- [`configs/ppanel.yaml.example`](./configs/ppanel.yaml.example)
|
|
||||||
- [`nginx/ppanel-api.conf`](./nginx/ppanel-api.conf)
|
|
||||||
|
|
||||||
关键值必须替换:
|
|
||||||
|
|
||||||
- `MySQL.Addr`
|
|
||||||
- `MySQL.Password`
|
|
||||||
- `Redis.Host`
|
|
||||||
- `Redis.Pass`
|
|
||||||
- `JwtAuth.AccessSecret`
|
|
||||||
- `Administrator.Email`
|
|
||||||
- `Administrator.Password`
|
|
||||||
- `AppSignature.AppSecrets.*`
|
|
||||||
- `device.security_secret`
|
|
||||||
- `Site.Host`
|
|
||||||
- `Site.SiteName`
|
|
||||||
|
|
||||||
Redis 约定保持不变:
|
|
||||||
|
|
||||||
- 业务缓存:DB `0`
|
|
||||||
- Asynq:DB `5`
|
|
||||||
|
|
||||||
## 8. 部署步骤
|
|
||||||
|
|
||||||
### 8.0 创建东京基础设施
|
|
||||||
|
|
||||||
如果本机或跳板机已经配置好 AWS CLI 凭据,可以先直接执行:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cp deploy/aws/ap-northeast-1/configs/aws-jp-infra.env.example /root/aws-jp-infra.env
|
|
||||||
chmod 600 /root/aws-jp-infra.env
|
|
||||||
vim /root/aws-jp-infra.env
|
|
||||||
|
|
||||||
chmod +x deploy/scripts/aws_jp_create_base_infra.sh
|
|
||||||
bash deploy/scripts/aws_jp_create_base_infra.sh /root/aws-jp-infra.env
|
|
||||||
```
|
|
||||||
|
|
||||||
执行后可用下面命令随时核对东京底座状态:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
chmod +x deploy/scripts/aws_jp_describe_state.sh
|
|
||||||
bash deploy/scripts/aws_jp_describe_state.sh /root/aws-jp-infra.env
|
|
||||||
```
|
|
||||||
|
|
||||||
### 8.1 初始化 EC2
|
|
||||||
|
|
||||||
把脚本上传到东京 EC2 后执行:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
chmod +x deploy/scripts/bootstrap_aws_ec2.sh
|
|
||||||
sudo APP_DIR=/opt/ppanel APP_USER=ubuntu deploy/scripts/bootstrap_aws_ec2.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
### 8.2 安装 Nginx 配置
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo cp deploy/aws/ap-northeast-1/nginx/ppanel-api.conf /etc/nginx/sites-available/ppanel-api.conf
|
|
||||||
sudo ln -sf /etc/nginx/sites-available/ppanel-api.conf /etc/nginx/sites-enabled/ppanel-api.conf
|
|
||||||
sudo nginx -t
|
|
||||||
sudo systemctl reload nginx
|
|
||||||
```
|
|
||||||
|
|
||||||
### 8.3 启动容器
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /opt/ppanel
|
|
||||||
docker compose -f docker-compose.cloud.yml up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
### 8.4 预检
|
|
||||||
|
|
||||||
```bash
|
|
||||||
chmod +x deploy/scripts/preflight_aws_jp.sh
|
|
||||||
APP_DIR=/opt/ppanel \
|
|
||||||
RDS_HOST=<TOKYO_RDS_ENDPOINT> \
|
|
||||||
REDIS_HOST=127.0.0.1 \
|
|
||||||
deploy/scripts/preflight_aws_jp.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
## 9. 数据迁移与切换
|
|
||||||
|
|
||||||
正式迁移请按:
|
|
||||||
|
|
||||||
- [`ops/hifast-aws-jp-migration-runbook-zh.md`](/Users/Apple/code_vpn/vpn/ppanel-server/ops/hifast-aws-jp-migration-runbook-zh.md)
|
|
||||||
|
|
||||||
执行。
|
|
||||||
|
|
||||||
核心原则:
|
|
||||||
|
|
||||||
- 先搭平行环境
|
|
||||||
- 停机后再导出香港主数据
|
|
||||||
- 东京验收通过后再切正式域名
|
|
||||||
- 切换后再重挂 `104` 灾备
|
|
||||||
|
|
||||||
## 10. 104 灾备节点常用模板
|
|
||||||
|
|
||||||
如果迁移完成后要把 `104.238.220.230` 重挂为东京主站从库,可复用:
|
|
||||||
|
|
||||||
- [`deploy/scripts/hifast_mysql_seed_primary_and_replica.sh`](/Users/Apple/code_vpn/vpn/ppanel-server/deploy/scripts/hifast_mysql_seed_primary_and_replica.sh)
|
|
||||||
- [`deploy/scripts/hifast_mysql_attach_replica.sh`](/Users/Apple/code_vpn/vpn/ppanel-server/deploy/scripts/hifast_mysql_attach_replica.sh)
|
|
||||||
- [`deploy/scripts/hifast_redis_attach_replica.sh`](/Users/Apple/code_vpn/vpn/ppanel-server/deploy/scripts/hifast_redis_attach_replica.sh)
|
|
||||||
- [`deploy/scripts/hifast_data_sync_tool.sh`](/Users/Apple/code_vpn/vpn/ppanel-server/deploy/scripts/hifast_data_sync_tool.sh)
|
|
||||||
- [`configs/replica-ops.env.example`](./configs/replica-ops.env.example)
|
|
||||||
|
|
||||||
## 11. 东京资源创建前置检查
|
|
||||||
|
|
||||||
在 AWS 控制台里至少先确认:
|
|
||||||
|
|
||||||
- 东京区已启用
|
|
||||||
- `ap-northeast-1` 可创建 `t4g.large`
|
|
||||||
- `ap-northeast-1` RDS 可创建 `db.r7g.xlarge`
|
|
||||||
- ACM / ALB / WAF / S3 服务在东京区可正常使用
|
|
||||||
- Tokyo 对应配额满足:
|
|
||||||
- On-Demand Standard vCPU
|
|
||||||
- ALB 数量
|
|
||||||
- Elastic IP(如需)
|
|
||||||
- RDS 实例数
|
|
||||||
|
|
||||||
## 12. 当前已知真实进度
|
|
||||||
|
|
||||||
截至 `2026-05-20`,已知状态如下:
|
|
||||||
|
|
||||||
- 东京 VPC `ppanel-jp-prod` 已创建
|
|
||||||
- VPC ID: `vpc-0846b23b4a7d64eac`
|
|
||||||
- VPC CIDR: `10.20.0.0/16`
|
|
||||||
- 4 个子网在 AWS 控制台里曾填写完成,但提交时控制台 session 失效
|
|
||||||
- 因此:
|
|
||||||
- 子网是否真正创建成功,需要重新核实
|
|
||||||
- IGW / 路由表 / 安全组 / RDS / EC2 / ALB / WAF 都应按“未完成”处理,重新复核
|
|
||||||
|
|
||||||
实时状态请以后续更新的 [`configs/resource-inventory.current.md`](./configs/resource-inventory.current.md) 为准。
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
AWS_REGION=ap-northeast-1
|
|
||||||
AWS_ACCOUNT_ID=200810848252
|
|
||||||
|
|
||||||
VPC_NAME=ppanel-jp-prod
|
|
||||||
VPC_ID=
|
|
||||||
VPC_CIDR=10.20.0.0/16
|
|
||||||
|
|
||||||
PUBLIC_SUBNET_A_NAME=ppanel-jp-public-a
|
|
||||||
PUBLIC_SUBNET_A_AZ=ap-northeast-1a
|
|
||||||
PUBLIC_SUBNET_A_CIDR=10.20.0.0/24
|
|
||||||
|
|
||||||
PUBLIC_SUBNET_C_NAME=ppanel-jp-public-c
|
|
||||||
PUBLIC_SUBNET_C_AZ=ap-northeast-1c
|
|
||||||
PUBLIC_SUBNET_C_CIDR=10.20.1.0/24
|
|
||||||
|
|
||||||
PRIVATE_SUBNET_A_NAME=ppanel-jp-private-a
|
|
||||||
PRIVATE_SUBNET_A_AZ=ap-northeast-1a
|
|
||||||
PRIVATE_SUBNET_A_CIDR=10.20.10.0/24
|
|
||||||
|
|
||||||
PRIVATE_SUBNET_C_NAME=ppanel-jp-private-c
|
|
||||||
PRIVATE_SUBNET_C_AZ=ap-northeast-1c
|
|
||||||
PRIVATE_SUBNET_C_CIDR=10.20.11.0/24
|
|
||||||
|
|
||||||
IGW_NAME=ppanel-jp-igw
|
|
||||||
PUBLIC_ROUTE_TABLE_NAME=ppanel-jp-public-rt
|
|
||||||
PRIVATE_ROUTE_TABLE_NAME=ppanel-jp-private-rt
|
|
||||||
|
|
||||||
SG_ALB_NAME=ppanel-jp-sg-alb
|
|
||||||
SG_EC2_NAME=ppanel-jp-sg-ec2
|
|
||||||
SG_RDS_NAME=ppanel-jp-sg-rds
|
|
||||||
|
|
||||||
OPS_SSH_CIDR=CHANGE_ME_TO_YOUR_FIXED_PUBLIC_IP_OR_CIDR
|
|
||||||
DR_REPLICA_IP=104.238.220.230/32
|
|
||||||
|
|
||||||
EC2_NAME=ppanel-app-jp-01
|
|
||||||
EC2_AMI_FAMILY=ubuntu-24.04
|
|
||||||
EC2_INSTANCE_TYPE=t4g.large
|
|
||||||
EC2_DISK_GB=80
|
|
||||||
EC2_KEY_PAIR=CHANGE_ME
|
|
||||||
|
|
||||||
RDS_IDENTIFIER=ppanel-mysql-jp
|
|
||||||
RDS_DB_NAME=hifast
|
|
||||||
RDS_ADMIN_USER=admin
|
|
||||||
RDS_INSTANCE_CLASS=db.r7g.xlarge
|
|
||||||
RDS_STORAGE_GB=100
|
|
||||||
|
|
||||||
ALB_NAME=ppanel-alb-jp
|
|
||||||
TARGET_GROUP_NAME=ppanel-tg-jp
|
|
||||||
WAF_NAME=ppanel-waf-jp
|
|
||||||
|
|
||||||
PARALLEL_API_DOMAIN=api-jp.hifast.biz
|
|
||||||
PARALLEL_LOGS_DOMAIN=logs-jp.hifast.biz
|
|
||||||
PRODUCTION_API_DOMAIN=CHANGE_ME
|
|
||||||
|
|
||||||
S3_BACKUP_BUCKET=hifast-prod-backups-200810848252-ap-northeast-1
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
AWS_REGION=ap-northeast-1
|
|
||||||
S3_BUCKET=hifast-prod-backups-200810848252-ap-northeast-1
|
|
||||||
S3_PREFIX=mysql
|
|
||||||
BACKUP_DIR=/var/backups/hifast
|
|
||||||
HOST_TAG=104-standby-for-jp
|
|
||||||
KEEP_LOCAL_DAYS=3
|
|
||||||
CHECK_REPLICA=1
|
|
||||||
|
|
||||||
MYSQL_HOST=127.0.0.1
|
|
||||||
MYSQL_PORT=3306
|
|
||||||
MYSQL_USER=backup_reader
|
|
||||||
MYSQL_PASSWORD=CHANGE_ME
|
|
||||||
MYSQL_SOCKET=
|
|
||||||
MYSQL_DATABASE=hifast
|
|
||||||
|
|
||||||
REDIS_HOST=127.0.0.1
|
|
||||||
REDIS_PORT=6379
|
|
||||||
REDIS_PASSWORD=CHANGE_ME
|
|
||||||
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
PRIMARY_HOST=ppanel-mysql-jp.<CHANGE_ME>.ap-northeast-1.rds.amazonaws.com
|
|
||||||
PRIMARY_PORT=3306
|
|
||||||
PRIMARY_USER=admin
|
|
||||||
PRIMARY_PASSWORD=CHANGE_ME
|
|
||||||
PRIMARY_DB=hifast
|
|
||||||
|
|
||||||
PRIMARY_REPL_USER=repl
|
|
||||||
PRIMARY_REPL_PASSWORD=CHANGE_ME
|
|
||||||
PRIMARY_REPL_HOST=104.238.220.230
|
|
||||||
PRIMARY_BINLOG_RETENTION_HOURS=24
|
|
||||||
|
|
||||||
REPLICA_HOST=127.0.0.1
|
|
||||||
REPLICA_PORT=3306
|
|
||||||
REPLICA_USER=root
|
|
||||||
REPLICA_PASSWORD=
|
|
||||||
REPLICA_SOCKET=/var/run/mysqld/mysqld.sock
|
|
||||||
REPLICA_DB=hifast
|
|
||||||
REPLICA_SOURCE_SSL=1
|
|
||||||
|
|
||||||
DUMP_FILE=
|
|
||||||
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
Host: 0.0.0.0
|
|
||||||
Port: 8080
|
|
||||||
Debug: false
|
|
||||||
|
|
||||||
JwtAuth:
|
|
||||||
AccessSecret: CHANGE_ME_TO_A_LONG_RANDOM_SECRET
|
|
||||||
AccessExpire: 604800
|
|
||||||
|
|
||||||
Logger:
|
|
||||||
ServiceName: PPanel
|
|
||||||
Mode: console
|
|
||||||
Encoding: plain
|
|
||||||
TimeFormat: "2006-01-02 15:04:05.000"
|
|
||||||
Path: logs
|
|
||||||
Level: info
|
|
||||||
MaxContentLength: 0
|
|
||||||
Compress: false
|
|
||||||
Stat: true
|
|
||||||
KeepDays: 7
|
|
||||||
StackCooldownMillis: 100
|
|
||||||
MaxBackups: 7
|
|
||||||
MaxSize: 100
|
|
||||||
Rotation: daily
|
|
||||||
FileTimeFormat: "2006-01-02T15:04:05.000Z07:00"
|
|
||||||
|
|
||||||
MySQL:
|
|
||||||
Addr: YOUR_TOKYO_RDS_ENDPOINT:3306
|
|
||||||
Dbname: hifast
|
|
||||||
Username: admin
|
|
||||||
Password: CHANGE_ME_TO_TOKYO_RDS_PASSWORD
|
|
||||||
Config: charset=utf8mb4&parseTime=true&loc=Asia%2FTokyo
|
|
||||||
MaxIdleConns: 10
|
|
||||||
MaxOpenConns: 100
|
|
||||||
SlowThreshold: 1000
|
|
||||||
|
|
||||||
Redis:
|
|
||||||
Host: 127.0.0.1:6379
|
|
||||||
Pass: CHANGE_ME_TO_TOKYO_REDIS_PASSWORD
|
|
||||||
DB: 0
|
|
||||||
PoolSize: 100
|
|
||||||
MinIdleConns: 10
|
|
||||||
MaxRetries: 3
|
|
||||||
PoolTimeout: 4
|
|
||||||
IdleTimeout: 300
|
|
||||||
MaxConnAge: 0
|
|
||||||
DialTimeout: 5
|
|
||||||
ReadTimeout: 3
|
|
||||||
WriteTimeout: 3
|
|
||||||
|
|
||||||
Trace:
|
|
||||||
Name: ppanel-server
|
|
||||||
Endpoint: 127.0.0.1:4317
|
|
||||||
Sampler: 0.1
|
|
||||||
Batcher: otlpgrpc
|
|
||||||
|
|
||||||
Site:
|
|
||||||
Host: api-jp.hifast.biz
|
|
||||||
SiteName: HiFastVPN
|
|
||||||
|
|
||||||
Administrator:
|
|
||||||
Email: admin@example.com
|
|
||||||
Password: CHANGE_ME_TO_STRONG_ADMIN_PASSWORD
|
|
||||||
|
|
||||||
Telegram:
|
|
||||||
Enable: false
|
|
||||||
BotID: 0
|
|
||||||
BotName: ""
|
|
||||||
BotToken: ""
|
|
||||||
GroupChatID: ""
|
|
||||||
EnableNotify: false
|
|
||||||
WebHookDomain: ""
|
|
||||||
|
|
||||||
Kutt:
|
|
||||||
Enable: false
|
|
||||||
ApiURL: ""
|
|
||||||
ApiKey: ""
|
|
||||||
TargetURL: ""
|
|
||||||
Domain: ""
|
|
||||||
|
|
||||||
OpenInstall:
|
|
||||||
Enable: false
|
|
||||||
AppKey: ""
|
|
||||||
ApiKey: ""
|
|
||||||
|
|
||||||
Loki:
|
|
||||||
Enable: true
|
|
||||||
URL: "http://localhost:3100"
|
|
||||||
|
|
||||||
AppSignature:
|
|
||||||
AppSecrets:
|
|
||||||
android-client: CHANGE_ME_ANDROID_SIGNATURE_SECRET
|
|
||||||
ios-client: CHANGE_ME_IOS_SIGNATURE_SECRET
|
|
||||||
web-client: CHANGE_ME_WEB_SIGNATURE_SECRET
|
|
||||||
ValidWindowSeconds: 300
|
|
||||||
SkipPrefixes:
|
|
||||||
- /v1/notify/
|
|
||||||
- /v1/iap/notifications
|
|
||||||
- /v1/telegram/webhook
|
|
||||||
- /v1/subscribe/config
|
|
||||||
|
|
||||||
Signature:
|
|
||||||
EnableSignature: false
|
|
||||||
|
|
||||||
device:
|
|
||||||
enable: true
|
|
||||||
security_secret: CHANGE_ME_DEVICE_SECURITY_SECRET
|
|
||||||
|
|
||||||
Register:
|
|
||||||
EnableTrial: true
|
|
||||||
EnableTrialEmailWhitelist: true
|
|
||||||
TrialEmailDomainWhitelist: "gmail.com,outlook.com,icloud.com,qq.com,163.com"
|
|
||||||
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
MYSQL_HOST=127.0.0.1
|
|
||||||
MYSQL_PORT=3306
|
|
||||||
MYSQL_USER=root
|
|
||||||
MYSQL_PASSWORD=CHANGE_ME
|
|
||||||
MYSQL_SOCKET=
|
|
||||||
|
|
||||||
REPL_SOURCE_HOST=ppanel-mysql-jp.cpo0keikgh80.ap-northeast-1.rds.amazonaws.com
|
|
||||||
REPL_SOURCE_PORT=3306
|
|
||||||
REPL_SOURCE_USER=repl
|
|
||||||
REPL_SOURCE_PASSWORD=XwWrQGVWtxmXJ3etHmkFvnRSD54MKYer
|
|
||||||
REPL_SOURCE_SSL=1
|
|
||||||
REPL_SOURCE_LOG_FILE=mysql-bin-changelog.000189
|
|
||||||
REPL_SOURCE_LOG_POS=185053
|
|
||||||
REPL_SOURCE_AUTO_POSITION=1
|
|
||||||
|
|
||||||
REDIS_HOST=127.0.0.1
|
|
||||||
REDIS_PORT=6379
|
|
||||||
REDIS_PASSWORD=CHANGE_ME
|
|
||||||
|
|
||||||
REDIS_SOURCE_HOST=3.114.29.208
|
|
||||||
REDIS_SOURCE_PORT=6379
|
|
||||||
REDIS_SOURCE_USER=
|
|
||||||
REDIS_SOURCE_PASSWORD=hifast67yj
|
|
||||||
@@ -1,186 +0,0 @@
|
|||||||
# Tokyo Resource Inventory
|
|
||||||
|
|
||||||
最后更新:`2026-05-21`
|
|
||||||
|
|
||||||
这个文件记录当前东京迁移的真实实施状态,不是示例。
|
|
||||||
|
|
||||||
## Region
|
|
||||||
|
|
||||||
- AWS account: `hifastvpn (200810848252)`
|
|
||||||
- Region: `ap-northeast-1`
|
|
||||||
|
|
||||||
## Current Status
|
|
||||||
|
|
||||||
- 东京迁移方案已在仓库内落地为执行资产
|
|
||||||
- 东京 VPC 已创建
|
|
||||||
- 东京子网、IGW、路由表已在 AWS 控制台创建并复核
|
|
||||||
- 东京三层安全组已在 AWS 控制台创建并复核
|
|
||||||
- 东京 VPC DNS 开关已开启,可支持公网可访问 RDS
|
|
||||||
- 东京 S3 备份桶已在 AWS 控制台创建并复核
|
|
||||||
- 东京 ACM 证书请求已创建,等待 DNS 验证
|
|
||||||
- 东京 RDS MySQL 已创建完成并可用
|
|
||||||
- 东京业务 EC2 已创建完成并绑定固定 EIP
|
|
||||||
- 因当前本机没有可用 AWS CLI 凭据,云上资源状态仍需在 AWS 控制台或已登录环境中复查
|
|
||||||
|
|
||||||
## Networking
|
|
||||||
|
|
||||||
- VPC
|
|
||||||
- Name: `ppanel-jp-prod`
|
|
||||||
- VPC ID: `vpc-0846b23b4a7d64eac`
|
|
||||||
- CIDR: `10.20.0.0/16`
|
|
||||||
- Status: `created`
|
|
||||||
- Public subnet A
|
|
||||||
- Name: `ppanel-jp-public-a`
|
|
||||||
- AZ: `ap-northeast-1a`
|
|
||||||
- CIDR: `10.20.0.0/24`
|
|
||||||
- Subnet ID: `subnet-091232bdb53e71490`
|
|
||||||
- Status: `created`
|
|
||||||
- Public subnet C
|
|
||||||
- Name: `ppanel-jp-public-c`
|
|
||||||
- AZ: `ap-northeast-1c`
|
|
||||||
- CIDR: `10.20.1.0/24`
|
|
||||||
- Subnet ID: `subnet-01ba0975c525ce8cf`
|
|
||||||
- Status: `created`
|
|
||||||
- Private subnet A
|
|
||||||
- Name: `ppanel-jp-private-a`
|
|
||||||
- AZ: `ap-northeast-1a`
|
|
||||||
- CIDR: `10.20.10.0/24`
|
|
||||||
- Subnet ID: `subnet-0bd13111c02f0edbe`
|
|
||||||
- Status: `created`
|
|
||||||
- Private subnet C
|
|
||||||
- Name: `ppanel-jp-private-c`
|
|
||||||
- AZ: `ap-northeast-1c`
|
|
||||||
- CIDR: `10.20.11.0/24`
|
|
||||||
- Subnet ID: `subnet-0d86c5c756dbc84b2`
|
|
||||||
- Status: `created`
|
|
||||||
- Internet Gateway
|
|
||||||
- Name: `ppanel-jp-igw`
|
|
||||||
- IGW ID: `igw-028041bcbf63b672c`
|
|
||||||
- Status: `created`
|
|
||||||
- Public route table
|
|
||||||
- Name: `ppanel-jp-public-rt`
|
|
||||||
- Route Table ID: `rtb-061b101080e4800e5`
|
|
||||||
- Default route: `0.0.0.0/0 -> igw-028041bcbf63b672c`
|
|
||||||
- Status: `created`
|
|
||||||
- Private route table
|
|
||||||
- Name: `ppanel-jp-private-rt`
|
|
||||||
- Route Table ID: `rtb-0d7a191a515031c45`
|
|
||||||
- Status: `created`
|
|
||||||
|
|
||||||
## Security
|
|
||||||
|
|
||||||
- `sg-alb`
|
|
||||||
- Name: `ppanel-jp-sg-alb`
|
|
||||||
- Security Group ID: `sg-0b3a23c31041a5a5a`
|
|
||||||
- Inbound:
|
|
||||||
- `80/tcp <- 0.0.0.0/0`
|
|
||||||
- `443/tcp <- 0.0.0.0/0`
|
|
||||||
- Status: `created`
|
|
||||||
- `sg-ec2`
|
|
||||||
- Name: `ppanel-jp-sg-ec2`
|
|
||||||
- Security Group ID: `sg-01f2a5a81e7505c91`
|
|
||||||
- Inbound:
|
|
||||||
- `80/tcp <- sg-0b3a23c31041a5a5a`
|
|
||||||
- `22/tcp <- 64.118.144.142/32`
|
|
||||||
- `6379/tcp <- 104.238.220.230/32`
|
|
||||||
- Status: `created`
|
|
||||||
- `sg-rds`
|
|
||||||
- Name: `ppanel-jp-sg-rds`
|
|
||||||
- Security Group ID: `sg-0b71db1e2c18b57c0`
|
|
||||||
- Inbound:
|
|
||||||
- `3306/tcp <- sg-01f2a5a81e7505c91`
|
|
||||||
- `3306/tcp <- 104.238.220.230/32`
|
|
||||||
- Status: `created`
|
|
||||||
|
|
||||||
## Compute / Database / Edge
|
|
||||||
|
|
||||||
- EC2 `ppanel-app-jp-01`:
|
|
||||||
- Instance ID: `i-07839130074cd7ed9`
|
|
||||||
- Type: `c7i.xlarge`
|
|
||||||
- Platform: `Ubuntu 26.04 / Linux`
|
|
||||||
- AZ: `ap-northeast-1c`
|
|
||||||
- VPC: `ppanel-jp-prod (vpc-0846b23b4a7d64eac)`
|
|
||||||
- Subnet: `ppanel-jp-public-c (subnet-01ba0975c525ce8cf)`
|
|
||||||
- Private IP: `10.20.1.168`
|
|
||||||
- Public IP / Elastic IP: `3.114.29.208`
|
|
||||||
- Public DNS: `ec2-3-114-29-208.ap-northeast-1.compute.amazonaws.com`
|
|
||||||
- Security group: `ppanel-jp-sg-ec2 (sg-01f2a5a81e7505c91)`
|
|
||||||
- Key pair: `ppanel-jp-key-20260521`
|
|
||||||
- Root volume: `gp3 100GiB`
|
|
||||||
- ENI: `eni-08accb427a470c9f7`
|
|
||||||
- EIP allocation ID: `eipalloc-038b32d5c0119accf`
|
|
||||||
- EIP association ID: `eipassoc-09184aa9161b6a4d9`
|
|
||||||
- Status: `running`
|
|
||||||
- SSH recovery key material: not stored in this repository; keep any recovery keys in an approved secret manager or other secure channel.
|
|
||||||
- RDS subnet group:
|
|
||||||
- Name: `ppanel-jp-rds-subnet-group`
|
|
||||||
- VPC: `vpc-0846b23b4a7d64eac`
|
|
||||||
- Subnets:
|
|
||||||
- `subnet-0bd13111c02f0edbe` / `ppanel-jp-private-a`
|
|
||||||
- `subnet-0d86c5c756dbc84b2` / `ppanel-jp-private-c`
|
|
||||||
- Status: `created`
|
|
||||||
- RDS public subnet group:
|
|
||||||
- Name: `ppanel-jp-rds-public-subnet-group`
|
|
||||||
- VPC: `vpc-0846b23b4a7d64eac`
|
|
||||||
- Subnets:
|
|
||||||
- `subnet-091232bdb53e71490` / `ppanel-jp-public-a`
|
|
||||||
- `subnet-01ba0975c525ce8cf` / `ppanel-jp-public-c`
|
|
||||||
- Status: `created`
|
|
||||||
- RDS `ppanel-mysql-jp`:
|
|
||||||
- Engine: `MySQL Community 8.4.8`
|
|
||||||
- Class: `db.r7g.xlarge`
|
|
||||||
- Storage: `gp3 100GiB`
|
|
||||||
- Deployment: `Single instance (current actual state)`
|
|
||||||
- VPC: `ppanel-jp-prod (vpc-0846b23b4a7d64eac)`
|
|
||||||
- Subnet group: `ppanel-jp-rds-public-subnet-group`
|
|
||||||
- Security group: `ppanel-jp-sg-rds (sg-0b71db1e2c18b57c0)`
|
|
||||||
- Master username: `admin`
|
|
||||||
- Credential management: `self-managed`
|
|
||||||
- Secrets Manager managed password: `disabled`
|
|
||||||
- Current master password visibility: `not retrievable from AWS console; reset only`
|
|
||||||
- Public access: `enabled (set at creation time for external replication)`
|
|
||||||
- Status: `available`
|
|
||||||
- Endpoint: `ppanel-mysql-jp.cpo0keikgh80.ap-northeast-1.rds.amazonaws.com`
|
|
||||||
- Public IP (resolved via public DNS): `52.196.204.186`
|
|
||||||
- Connection test from Tokyo EC2:
|
|
||||||
- `mysql -h ppanel-mysql-jp.cpo0keikgh80.ap-northeast-1.rds.amazonaws.com -u admin -e "select 1"`
|
|
||||||
- Result: `ERROR 1045 (28000): Access denied for user 'admin'@'ip-10-20-1-168.ap-northeast-1.compute.internal' (using password: NO)`
|
|
||||||
- Meaning: `network path and security group are working; only the password is missing`
|
|
||||||
- Current admin password: `TkyRds20260521!N9mQ8sKe2vLp7Xa`
|
|
||||||
- External replica prep for `104.238.220.230`:
|
|
||||||
- binlog retention hours: `24`
|
|
||||||
- replication user: `repl@104.238.220.230`
|
|
||||||
- replication password: `XwWrQGVWtxmXJ3etHmkFvnRSD54MKYer`
|
|
||||||
- current binlog file: `mysql-bin-changelog.000189`
|
|
||||||
- current binlog position: `185053`
|
|
||||||
- ALB `ppanel-alb-jp`: `not created`
|
|
||||||
- WAF `ppanel-waf-jp`: `not created`
|
|
||||||
- ACM certificate in `ap-northeast-1`:
|
|
||||||
- Certificate ID: `29d0b9b6-ab37-44d9-ad9e-18fa7e9aae3a`
|
|
||||||
- Domains:
|
|
||||||
- `api-jp.hifast.biz`
|
|
||||||
- `logs-jp.hifast.biz`
|
|
||||||
- Status: `pending_validation`
|
|
||||||
- Route 53 hosted zone in current AWS account: `not found`
|
|
||||||
- S3 backup bucket `hifast-prod-backups-200810848252-ap-northeast-1`: `created`
|
|
||||||
|
|
||||||
## Domains
|
|
||||||
|
|
||||||
- Parallel API domain: `api-jp.hifast.biz`
|
|
||||||
- Parallel logs domain: `logs-jp.hifast.biz`
|
|
||||||
- Production API domain: `pending user final confirmation`
|
|
||||||
- ACM DNS validation records pending external DNS add:
|
|
||||||
- `api-jp.hifast.biz`
|
|
||||||
- Name: `_0de5970dfbadaf46759447b2ea627a10.api-jp.hifast.biz.`
|
|
||||||
- Type: `CNAME`
|
|
||||||
- Value: `_6a632a5b85c5b3f304cc492090b741b3.jkddzztszm.acm-validations.aws.`
|
|
||||||
- `logs-jp.hifast.biz`
|
|
||||||
- Name: `_349a2b2bc4678d76c3ab341ccf73db61.logs-jp.hifast.biz.`
|
|
||||||
- Type: `CNAME`
|
|
||||||
- Value: `_74ced20dc96aa39070188605cf0ced18.jkddzztszm.acm-validations.aws.`
|
|
||||||
|
|
||||||
## DR
|
|
||||||
|
|
||||||
- DR host: `104.238.220.230`
|
|
||||||
- Planned MySQL upstream after cutover: `Tokyo RDS`
|
|
||||||
- Planned Redis upstream after cutover: `Tokyo EC2 public IP`
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
# Tokyo Resource Inventory Example
|
|
||||||
|
|
||||||
Use this file as the single source of truth while building the Tokyo environment.
|
|
||||||
|
|
||||||
## Region
|
|
||||||
|
|
||||||
- AWS account: `hifastvpn (200810848252)`
|
|
||||||
- Region: `ap-northeast-1`
|
|
||||||
|
|
||||||
## DNS
|
|
||||||
|
|
||||||
- Production API domain: `CHANGE_ME`
|
|
||||||
- Parallel API domain: `api-jp.hifast.biz`
|
|
||||||
- Parallel logs domain: `logs-jp.hifast.biz`
|
|
||||||
|
|
||||||
## Networking
|
|
||||||
|
|
||||||
- VPC name: `ppanel-jp-prod`
|
|
||||||
- VPC CIDR: `10.20.0.0/16`
|
|
||||||
- Public subnet A: `10.20.0.0/24`
|
|
||||||
- Public subnet C: `10.20.1.0/24`
|
|
||||||
- Private subnet A: `10.20.10.0/24`
|
|
||||||
- Private subnet C: `10.20.11.0/24`
|
|
||||||
- Ops CIDR for SSH: `CHANGE_ME`
|
|
||||||
|
|
||||||
## Compute
|
|
||||||
|
|
||||||
- EC2 name: `ppanel-app-jp-01`
|
|
||||||
- EC2 type: `t4g.large`
|
|
||||||
- EC2 disk: `gp3 80GB`
|
|
||||||
- SSH key pair: `CHANGE_ME`
|
|
||||||
|
|
||||||
## Database
|
|
||||||
|
|
||||||
- RDS identifier: `ppanel-mysql-jp`
|
|
||||||
- RDS engine: `MySQL 8.4`
|
|
||||||
- RDS class: `db.r7g.xlarge`
|
|
||||||
- RDS storage: `gp3 100GB`
|
|
||||||
- DB name: `hifast`
|
|
||||||
- DB admin user: `admin`
|
|
||||||
|
|
||||||
## Cache
|
|
||||||
|
|
||||||
- Redis container: `hifast-redis`
|
|
||||||
- Redis port: `6379`
|
|
||||||
- Redis password: `CHANGE_ME`
|
|
||||||
|
|
||||||
## Security / Secrets
|
|
||||||
|
|
||||||
- JWT secret: `CHANGE_ME`
|
|
||||||
- Admin email: `CHANGE_ME`
|
|
||||||
- Admin password: `CHANGE_ME`
|
|
||||||
- Android app signature secret: `CHANGE_ME`
|
|
||||||
- iOS app signature secret: `CHANGE_ME`
|
|
||||||
- Web app signature secret: `CHANGE_ME`
|
|
||||||
- Device security secret: `CHANGE_ME`
|
|
||||||
|
|
||||||
## Backup
|
|
||||||
|
|
||||||
- S3 backup bucket: `hifast-prod-backups-200810848252-ap-northeast-1`
|
|
||||||
- Versioning: `Enabled`
|
|
||||||
|
|
||||||
## DR
|
|
||||||
|
|
||||||
- DR host: `104.238.220.230`
|
|
||||||
- MySQL repl user: `repl`
|
|
||||||
- MySQL repl password: `CHANGE_ME`
|
|
||||||
- Redis source password: `CHANGE_ME`
|
|
||||||
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
server {
|
|
||||||
listen 80 default_server;
|
|
||||||
listen [::]:80 default_server;
|
|
||||||
server_name _;
|
|
||||||
|
|
||||||
client_max_body_size 20m;
|
|
||||||
|
|
||||||
access_log /var/log/nginx/ppanel-access.log;
|
|
||||||
error_log /var/log/nginx/ppanel-error.log warn;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_pass http://127.0.0.1:8080;
|
|
||||||
|
|
||||||
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_set_header X-Forwarded-Proto $scheme;
|
|
||||||
proxy_set_header X-Forwarded-Host $host;
|
|
||||||
proxy_set_header X-Forwarded-Port $server_port;
|
|
||||||
|
|
||||||
proxy_connect_timeout 10s;
|
|
||||||
proxy_send_timeout 60s;
|
|
||||||
proxy_read_timeout 60s;
|
|
||||||
}
|
|
||||||
|
|
||||||
location = /nginx_status {
|
|
||||||
stub_status;
|
|
||||||
access_log off;
|
|
||||||
allow 127.0.0.1;
|
|
||||||
deny all;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=Hifast MySQL backup to S3
|
|
||||||
Wants=network-online.target
|
|
||||||
After=network-online.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=oneshot
|
|
||||||
User=root
|
|
||||||
Group=root
|
|
||||||
EnvironmentFile=/root/backup-to-s3.env
|
|
||||||
ExecStart=/usr/bin/env bash -lc 'exec /opt/ppanel/deploy/scripts/mysql_backup_to_s3.sh'
|
|
||||||
Nice=10
|
|
||||||
IOSchedulingClass=best-effort
|
|
||||||
IOSchedulingPriority=7
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=Run Hifast MySQL backup to S3 every 10 minutes
|
|
||||||
|
|
||||||
[Timer]
|
|
||||||
OnCalendar=*:0/10
|
|
||||||
Persistent=true
|
|
||||||
RandomizedDelaySec=30
|
|
||||||
Unit=hifast-mysql-backup.service
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=timers.target
|
|
||||||
+1
-230
@@ -1,26 +1,4 @@
|
|||||||
# PPanel 服务部署 (云端/无源码版)
|
|
||||||
# 使用方法:
|
|
||||||
# 1. 确保已将 docker-compose.cloud.yml, configs/, loki/, grafana/, prometheus/, tempo/ 目录上传到服务器同一目录
|
|
||||||
# 2. 确保 configs/ 目录下有 ppanel.yaml 配置文件(参考 etc/ppanel.yaml)
|
|
||||||
# 3. 确保 logs/ cache/ tempo_data/ 目录存在 (mkdir -p logs cache tempo_data)
|
|
||||||
# 4. 运行: docker-compose -f docker-compose.cloud.yml up -d
|
|
||||||
#
|
|
||||||
# 网络说明:
|
|
||||||
# ppanel-server 使用 host 网络(可出外网,直接访问 AWS RDS / 本机 Redis)
|
|
||||||
# 监控服务(Loki/Tempo/Grafana/Prometheus)在 ppanel_net bridge 网络中
|
|
||||||
# Tempo(4317) 将端口映射到 127.0.0.1,ppanel-server 通过 host 网络访问
|
|
||||||
# 监控端口绑定 127.0.0.1,需通过 SSH 隧道或 Nginx 反代访问
|
|
||||||
#
|
|
||||||
# 未来多开 ppanel-server 时:
|
|
||||||
# 修复宿主机 iptables bridge 出网规则后,可将 ppanel-server 切回 bridge 网络
|
|
||||||
# 多实例用不同端口: ports: ["8081:8080"] + container_name: ppanel-server-2
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
# ----------------------------------------------------
|
|
||||||
# 1. 业务后端 (PPanel Server)
|
|
||||||
# host 网络:可出外网,直接访问 AWS RDS/Redis;通过 127.0.0.1 访问 Tempo
|
|
||||||
# PPANEL_SERVER_IMAGE/PPANEL_SERVER_TAG 由 CI/CD 传入不可变镜像(如 ghcr.io/org/repo:git SHA)
|
|
||||||
# ----------------------------------------------------
|
|
||||||
ppanel-server:
|
ppanel-server:
|
||||||
image: ${PPANEL_SERVER_IMAGE:-registry.kxsw.us/vpn-server}:${PPANEL_SERVER_TAG:?please set PPANEL_SERVER_TAG to an immutable image tag}
|
image: ${PPANEL_SERVER_IMAGE:-registry.kxsw.us/vpn-server}:${PPANEL_SERVER_TAG:?please set PPANEL_SERVER_TAG to an immutable image tag}
|
||||||
container_name: ppanel-server
|
container_name: ppanel-server
|
||||||
@@ -28,7 +6,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ./configs:/app/etc
|
- ./configs:/app/etc
|
||||||
- ./logs:/app/logs
|
- ./logs:/app/logs
|
||||||
- ./cache:/app/cache # GeoLite2-City.mmdb IP 地理位置数据库
|
- ./cache:/app/cache
|
||||||
environment:
|
environment:
|
||||||
- TZ=Asia/Shanghai
|
- TZ=Asia/Shanghai
|
||||||
network_mode: host
|
network_mode: host
|
||||||
@@ -37,215 +15,8 @@ services:
|
|||||||
nofile:
|
nofile:
|
||||||
soft: 65535
|
soft: 65535
|
||||||
hard: 65535
|
hard: 65535
|
||||||
depends_on:
|
|
||||||
tempo:
|
|
||||||
condition: service_started
|
|
||||||
logging:
|
logging:
|
||||||
driver: "json-file"
|
driver: "json-file"
|
||||||
options:
|
options:
|
||||||
max-size: "10m"
|
max-size: "10m"
|
||||||
max-file: "3"
|
max-file: "3"
|
||||||
|
|
||||||
# ----------------------------------------------------
|
|
||||||
# 2. Tempo (链路追踪存储)
|
|
||||||
# ----------------------------------------------------
|
|
||||||
tempo:
|
|
||||||
image: grafana/tempo:2.4.1
|
|
||||||
container_name: ppanel-tempo
|
|
||||||
user: root
|
|
||||||
restart: always
|
|
||||||
command:
|
|
||||||
- "-config.file=/etc/tempo.yaml"
|
|
||||||
- "-target=all"
|
|
||||||
volumes:
|
|
||||||
- ./tempo/tempo-config.yaml:/etc/tempo.yaml
|
|
||||||
- ./tempo_data:/var/tempo
|
|
||||||
ports:
|
|
||||||
- "127.0.0.1:4317:4317" # OTLP gRPC,ppanel-server(host网络)通过127.0.0.1:4317发送trace
|
|
||||||
networks:
|
|
||||||
- ppanel_net
|
|
||||||
logging:
|
|
||||||
driver: "json-file"
|
|
||||||
options:
|
|
||||||
max-size: "10m"
|
|
||||||
max-file: "3"
|
|
||||||
|
|
||||||
# ----------------------------------------------------
|
|
||||||
# 3. Loki (日志存储)
|
|
||||||
# ----------------------------------------------------
|
|
||||||
loki:
|
|
||||||
image: grafana/loki:3.0.0
|
|
||||||
container_name: ppanel-loki
|
|
||||||
restart: always
|
|
||||||
volumes:
|
|
||||||
- ./loki/loki-config.yaml:/etc/loki/local-config.yaml
|
|
||||||
- loki_data:/loki
|
|
||||||
command: -config.file=/etc/loki/local-config.yaml
|
|
||||||
# 不对外暴露端口,仅内网访问
|
|
||||||
networks:
|
|
||||||
- ppanel_net
|
|
||||||
logging:
|
|
||||||
driver: "json-file"
|
|
||||||
options:
|
|
||||||
max-size: "10m"
|
|
||||||
max-file: "3"
|
|
||||||
|
|
||||||
# ----------------------------------------------------
|
|
||||||
# 4. Promtail (日志采集)
|
|
||||||
# ----------------------------------------------------
|
|
||||||
promtail:
|
|
||||||
image: grafana/promtail:3.0.0
|
|
||||||
container_name: ppanel-promtail
|
|
||||||
restart: always
|
|
||||||
volumes:
|
|
||||||
- ./loki/promtail-config.yaml:/etc/promtail/config.yaml
|
|
||||||
- /var/lib/docker/containers:/var/lib/docker/containers:ro
|
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
|
||||||
- ./logs:/var/log/ppanel-server:ro
|
|
||||||
- /var/log/nginx:/var/log/nginx:ro
|
|
||||||
command: -config.file=/etc/promtail/config.yaml
|
|
||||||
networks:
|
|
||||||
- ppanel_net
|
|
||||||
depends_on:
|
|
||||||
- loki
|
|
||||||
logging:
|
|
||||||
driver: "json-file"
|
|
||||||
options:
|
|
||||||
max-size: "10m"
|
|
||||||
max-file: "3"
|
|
||||||
|
|
||||||
# ----------------------------------------------------
|
|
||||||
# 5. Grafana (可观测面板)
|
|
||||||
# 访问: ssh -L 3333:localhost:3333 your-server 后浏览器打开 http://localhost:3333
|
|
||||||
# 或配置 Nginx 反代(建议加认证)
|
|
||||||
# ----------------------------------------------------
|
|
||||||
grafana:
|
|
||||||
image: grafana/grafana:13.0.1
|
|
||||||
container_name: ppanel-grafana
|
|
||||||
restart: always
|
|
||||||
ports:
|
|
||||||
- "3333:3000" # 仅本机可访问,需 SSH 隧道或 Nginx 反代
|
|
||||||
environment:
|
|
||||||
- GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASSWORD:?请在 .env 文件中设置 GRAFANA_PASSWORD}
|
|
||||||
- GF_USERS_ALLOW_SIGN_UP=false
|
|
||||||
- GF_SERVER_DOMAIN=${GRAFANA_DOMAIN:-logsx.hifast.biz}
|
|
||||||
- GF_SERVER_ROOT_URL=${GRAFANA_ROOT_URL:-https://logsx.hifast.biz}
|
|
||||||
- GF_FEATURE_TOGGLES_ENABLE=appObservability
|
|
||||||
- AWS_REGION=${AWS_REGION:-ap-east-1}
|
|
||||||
- AWS_DEFAULT_REGION=${AWS_REGION:-ap-east-1}
|
|
||||||
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID:-}
|
|
||||||
- AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY:-}
|
|
||||||
- AWS_SESSION_TOKEN=${AWS_SESSION_TOKEN:-}
|
|
||||||
volumes:
|
|
||||||
- grafana_data:/var/lib/grafana
|
|
||||||
- ./grafana/provisioning:/etc/grafana/provisioning
|
|
||||||
networks:
|
|
||||||
- ppanel_net
|
|
||||||
depends_on:
|
|
||||||
- loki
|
|
||||||
- tempo
|
|
||||||
- prometheus
|
|
||||||
logging:
|
|
||||||
driver: "json-file"
|
|
||||||
options:
|
|
||||||
max-size: "10m"
|
|
||||||
max-file: "3"
|
|
||||||
|
|
||||||
# ----------------------------------------------------
|
|
||||||
# 6. Prometheus (指标采集)
|
|
||||||
# ----------------------------------------------------
|
|
||||||
prometheus:
|
|
||||||
image: prom/prometheus:v3.11.3
|
|
||||||
container_name: ppanel-prometheus
|
|
||||||
restart: always
|
|
||||||
ports:
|
|
||||||
- "127.0.0.1:9090:9090" # 仅本机可访问
|
|
||||||
volumes:
|
|
||||||
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
|
|
||||||
- prometheus_data:/prometheus
|
|
||||||
command:
|
|
||||||
- '--config.file=/etc/prometheus/prometheus.yml'
|
|
||||||
- '--storage.tsdb.path=/prometheus'
|
|
||||||
- '--web.enable-lifecycle'
|
|
||||||
- '--web.enable-remote-write-receiver'
|
|
||||||
networks:
|
|
||||||
- ppanel_net
|
|
||||||
logging:
|
|
||||||
driver: "json-file"
|
|
||||||
options:
|
|
||||||
max-size: "10m"
|
|
||||||
max-file: "3"
|
|
||||||
|
|
||||||
# ----------------------------------------------------
|
|
||||||
# 7. Nginx Exporter (监控宿主机 Nginx)
|
|
||||||
# ----------------------------------------------------
|
|
||||||
nginx-exporter:
|
|
||||||
image: nginx/nginx-prometheus-exporter:1.5.0
|
|
||||||
container_name: ppanel-nginx-exporter
|
|
||||||
restart: always
|
|
||||||
command:
|
|
||||||
- -nginx.scrape-uri=http://host.docker.internal:8090/nginx_status
|
|
||||||
extra_hosts:
|
|
||||||
- "host.docker.internal:host-gateway"
|
|
||||||
networks:
|
|
||||||
- ppanel_net
|
|
||||||
logging:
|
|
||||||
driver: "json-file"
|
|
||||||
options:
|
|
||||||
max-size: "10m"
|
|
||||||
max-file: "3"
|
|
||||||
|
|
||||||
# ----------------------------------------------------
|
|
||||||
# 8. Node Exporter (宿主机监控)
|
|
||||||
# ----------------------------------------------------
|
|
||||||
node-exporter:
|
|
||||||
image: prom/node-exporter:v1.11.1
|
|
||||||
container_name: ppanel-node-exporter
|
|
||||||
restart: always
|
|
||||||
volumes:
|
|
||||||
- /proc:/host/proc:ro
|
|
||||||
- /sys:/host/sys:ro
|
|
||||||
- /:/rootfs:ro
|
|
||||||
command:
|
|
||||||
- '--path.procfs=/host/proc'
|
|
||||||
- '--path.sysfs=/host/sys'
|
|
||||||
- '--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)'
|
|
||||||
networks:
|
|
||||||
- ppanel_net
|
|
||||||
logging:
|
|
||||||
driver: "json-file"
|
|
||||||
options:
|
|
||||||
max-size: "10m"
|
|
||||||
max-file: "3"
|
|
||||||
|
|
||||||
# ----------------------------------------------------
|
|
||||||
# 9. cAdvisor (容器监控)
|
|
||||||
# ----------------------------------------------------
|
|
||||||
cadvisor:
|
|
||||||
image: gcr.io/cadvisor/cadvisor:v0.55.1
|
|
||||||
container_name: ppanel-cadvisor
|
|
||||||
restart: always
|
|
||||||
volumes:
|
|
||||||
- /:/rootfs:ro
|
|
||||||
- /var/run:/var/run:ro
|
|
||||||
- /sys:/sys:ro
|
|
||||||
- /var/lib/docker/:/var/lib/docker:ro
|
|
||||||
- /dev/disk/:/dev/disk:ro
|
|
||||||
networks:
|
|
||||||
- ppanel_net
|
|
||||||
logging:
|
|
||||||
driver: "json-file"
|
|
||||||
options:
|
|
||||||
max-size: "10m"
|
|
||||||
max-file: "3"
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
loki_data:
|
|
||||||
grafana_data:
|
|
||||||
prometheus_data:
|
|
||||||
tempo_data:
|
|
||||||
|
|
||||||
networks:
|
|
||||||
ppanel_net:
|
|
||||||
name: ppanel_net
|
|
||||||
driver: bridge
|
|
||||||
|
|||||||
+8
-8
@@ -15,10 +15,10 @@ Logger: # 日志配置
|
|||||||
Level: debug # 日志级别: debug, info, warn, error, panic, fatal
|
Level: debug # 日志级别: debug, info, warn, error, panic, fatal
|
||||||
|
|
||||||
MySQL:
|
MySQL:
|
||||||
Addr: 45.43.29.127:3306 # host 网络模式; bridge 模式改为 mysql:3306
|
Addr: 127.0.0.1:3306 # 本地开发默认;Docker bridge 模式可改为 mysql:3306
|
||||||
Username: root # MySQL用户名
|
Username: root # MySQL用户名
|
||||||
Password: jpcV41ppanel # MySQL密码,与 .env MYSQL_ROOT_PASSWORD 一致
|
Password: CHANGE_ME_TO_DB_PASSWORD # MySQL密码
|
||||||
Dbname: hifast # MySQL数据库名
|
Dbname: ppanel # MySQL数据库名
|
||||||
Config: charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai
|
Config: charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai
|
||||||
MaxIdleConns: 10
|
MaxIdleConns: 10
|
||||||
MaxOpenConns: 100
|
MaxOpenConns: 100
|
||||||
@@ -42,9 +42,9 @@ Redis:
|
|||||||
|
|
||||||
AppSignature:
|
AppSignature:
|
||||||
AppSecrets:
|
AppSecrets:
|
||||||
android-client: uB4G,XxL2{7b # Android 客户端签名密钥
|
android-client: CHANGE_ME_ANDROID_APP_SECRET # Android 客户端签名密钥
|
||||||
ios-client: uB4G,XxL2{7b # iOS 客户端签名密钥
|
ios-client: CHANGE_ME_IOS_APP_SECRET # iOS 客户端签名密钥
|
||||||
web-client: uB4G,XxL2{7b # Web 客户端签名密钥
|
web-client: CHANGE_ME_WEB_APP_SECRET # Web 客户端签名密钥
|
||||||
ValidWindowSeconds: 300 # 签名时间窗口(秒)
|
ValidWindowSeconds: 300 # 签名时间窗口(秒)
|
||||||
SkipPrefixes:
|
SkipPrefixes:
|
||||||
- /v1/notify/ # 支付回调不验签
|
- /v1/notify/ # 支付回调不验签
|
||||||
@@ -58,8 +58,8 @@ Signature:
|
|||||||
Trace: # 链路追踪配置 (OpenTelemetry)
|
Trace: # 链路追踪配置 (OpenTelemetry)
|
||||||
Name: ppanel # 服务名
|
Name: ppanel # 服务名
|
||||||
Sampler: 1.0 # 采样率 0.0-1.0,生产建议 0.1
|
Sampler: 1.0 # 采样率 0.0-1.0,生产建议 0.1
|
||||||
Batcher: otlpgrpc # 本地开发留空""; 生产填 otlpgrpc
|
Batcher: "" # 本地开发留空;生产如需链路追踪再配置 exporter
|
||||||
Endpoint: "127.0.0.1:4317" # host 网络模式; bridge 模式改为 tempo:4317
|
Endpoint: ""
|
||||||
|
|
||||||
S3:
|
S3:
|
||||||
Enable: false
|
Enable: false
|
||||||
|
|||||||
@@ -1,272 +0,0 @@
|
|||||||
apiVersion: 1
|
|
||||||
|
|
||||||
groups:
|
|
||||||
- orgId: 1
|
|
||||||
name: ppanel-core
|
|
||||||
folder: PPanel
|
|
||||||
interval: 1m
|
|
||||||
rules:
|
|
||||||
- uid: ppanel-target-down
|
|
||||||
title: PPanel monitoring target down
|
|
||||||
condition: C
|
|
||||||
for: 2m
|
|
||||||
noDataState: Alerting
|
|
||||||
execErrState: Error
|
|
||||||
annotations:
|
|
||||||
summary: "Monitoring target is down"
|
|
||||||
description: "{{ $labels.job }} on {{ $labels.instance }} has been down for more than 2 minutes."
|
|
||||||
labels:
|
|
||||||
severity: critical
|
|
||||||
service: ppanel
|
|
||||||
data:
|
|
||||||
- refId: A
|
|
||||||
relativeTimeRange:
|
|
||||||
from: 300
|
|
||||||
to: 0
|
|
||||||
datasourceUid: prometheus
|
|
||||||
model:
|
|
||||||
datasource:
|
|
||||||
type: prometheus
|
|
||||||
uid: prometheus
|
|
||||||
editorMode: code
|
|
||||||
expr: 'up{job=~"grafana|prometheus|node-exporter|cadvisor|nginx-exporter|loki|tempo"}'
|
|
||||||
instant: true
|
|
||||||
intervalMs: 1000
|
|
||||||
maxDataPoints: 43200
|
|
||||||
refId: A
|
|
||||||
- refId: C
|
|
||||||
datasourceUid: __expr__
|
|
||||||
model:
|
|
||||||
conditions:
|
|
||||||
- evaluator:
|
|
||||||
params:
|
|
||||||
- 1
|
|
||||||
type: lt
|
|
||||||
operator:
|
|
||||||
type: and
|
|
||||||
query:
|
|
||||||
params:
|
|
||||||
- A
|
|
||||||
reducer:
|
|
||||||
type: last
|
|
||||||
type: query
|
|
||||||
datasource:
|
|
||||||
type: __expr__
|
|
||||||
uid: __expr__
|
|
||||||
expression: A
|
|
||||||
intervalMs: 1000
|
|
||||||
maxDataPoints: 43200
|
|
||||||
refId: C
|
|
||||||
type: threshold
|
|
||||||
|
|
||||||
- uid: ppanel-host-disk-high
|
|
||||||
title: PPanel host disk usage high
|
|
||||||
condition: C
|
|
||||||
for: 10m
|
|
||||||
noDataState: NoData
|
|
||||||
execErrState: Error
|
|
||||||
annotations:
|
|
||||||
summary: "Host disk usage is high"
|
|
||||||
description: "{{ $labels.instance }} {{ $labels.mountpoint }} disk usage is above 85% for 10 minutes."
|
|
||||||
labels:
|
|
||||||
severity: warning
|
|
||||||
service: ppanel
|
|
||||||
data:
|
|
||||||
- refId: A
|
|
||||||
relativeTimeRange:
|
|
||||||
from: 900
|
|
||||||
to: 0
|
|
||||||
datasourceUid: prometheus
|
|
||||||
model:
|
|
||||||
datasource:
|
|
||||||
type: prometheus
|
|
||||||
uid: prometheus
|
|
||||||
editorMode: code
|
|
||||||
expr: '100 - (node_filesystem_avail_bytes{fstype!~"tmpfs|overlay|squashfs|aufs",mountpoint!~"/run.*|/var/lib/docker.*"} / node_filesystem_size_bytes{fstype!~"tmpfs|overlay|squashfs|aufs",mountpoint!~"/run.*|/var/lib/docker.*"} * 100)'
|
|
||||||
instant: true
|
|
||||||
intervalMs: 1000
|
|
||||||
maxDataPoints: 43200
|
|
||||||
refId: A
|
|
||||||
- refId: C
|
|
||||||
datasourceUid: __expr__
|
|
||||||
model:
|
|
||||||
conditions:
|
|
||||||
- evaluator:
|
|
||||||
params:
|
|
||||||
- 85
|
|
||||||
type: gt
|
|
||||||
operator:
|
|
||||||
type: and
|
|
||||||
query:
|
|
||||||
params:
|
|
||||||
- A
|
|
||||||
reducer:
|
|
||||||
type: last
|
|
||||||
type: query
|
|
||||||
datasource:
|
|
||||||
type: __expr__
|
|
||||||
uid: __expr__
|
|
||||||
expression: A
|
|
||||||
intervalMs: 1000
|
|
||||||
maxDataPoints: 43200
|
|
||||||
refId: C
|
|
||||||
type: threshold
|
|
||||||
|
|
||||||
- uid: ppanel-host-memory-high
|
|
||||||
title: PPanel host memory usage high
|
|
||||||
condition: C
|
|
||||||
for: 10m
|
|
||||||
noDataState: NoData
|
|
||||||
execErrState: Error
|
|
||||||
annotations:
|
|
||||||
summary: "Host memory usage is high"
|
|
||||||
description: "{{ $labels.instance }} memory usage is above 90% for 10 minutes."
|
|
||||||
labels:
|
|
||||||
severity: warning
|
|
||||||
service: ppanel
|
|
||||||
data:
|
|
||||||
- refId: A
|
|
||||||
relativeTimeRange:
|
|
||||||
from: 900
|
|
||||||
to: 0
|
|
||||||
datasourceUid: prometheus
|
|
||||||
model:
|
|
||||||
datasource:
|
|
||||||
type: prometheus
|
|
||||||
uid: prometheus
|
|
||||||
editorMode: code
|
|
||||||
expr: '(1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)) * 100'
|
|
||||||
instant: true
|
|
||||||
intervalMs: 1000
|
|
||||||
maxDataPoints: 43200
|
|
||||||
refId: A
|
|
||||||
- refId: C
|
|
||||||
datasourceUid: __expr__
|
|
||||||
model:
|
|
||||||
conditions:
|
|
||||||
- evaluator:
|
|
||||||
params:
|
|
||||||
- 90
|
|
||||||
type: gt
|
|
||||||
operator:
|
|
||||||
type: and
|
|
||||||
query:
|
|
||||||
params:
|
|
||||||
- A
|
|
||||||
reducer:
|
|
||||||
type: last
|
|
||||||
type: query
|
|
||||||
datasource:
|
|
||||||
type: __expr__
|
|
||||||
uid: __expr__
|
|
||||||
expression: A
|
|
||||||
intervalMs: 1000
|
|
||||||
maxDataPoints: 43200
|
|
||||||
refId: C
|
|
||||||
type: threshold
|
|
||||||
|
|
||||||
- uid: ppanel-host-cpu-high
|
|
||||||
title: PPanel host CPU usage high
|
|
||||||
condition: C
|
|
||||||
for: 10m
|
|
||||||
noDataState: NoData
|
|
||||||
execErrState: Error
|
|
||||||
annotations:
|
|
||||||
summary: "Host CPU usage is high"
|
|
||||||
description: "{{ $labels.instance }} CPU usage is above 90% for 10 minutes."
|
|
||||||
labels:
|
|
||||||
severity: warning
|
|
||||||
service: ppanel
|
|
||||||
data:
|
|
||||||
- refId: A
|
|
||||||
relativeTimeRange:
|
|
||||||
from: 900
|
|
||||||
to: 0
|
|
||||||
datasourceUid: prometheus
|
|
||||||
model:
|
|
||||||
datasource:
|
|
||||||
type: prometheus
|
|
||||||
uid: prometheus
|
|
||||||
editorMode: code
|
|
||||||
expr: '100 - (avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)'
|
|
||||||
instant: true
|
|
||||||
intervalMs: 1000
|
|
||||||
maxDataPoints: 43200
|
|
||||||
refId: A
|
|
||||||
- refId: C
|
|
||||||
datasourceUid: __expr__
|
|
||||||
model:
|
|
||||||
conditions:
|
|
||||||
- evaluator:
|
|
||||||
params:
|
|
||||||
- 90
|
|
||||||
type: gt
|
|
||||||
operator:
|
|
||||||
type: and
|
|
||||||
query:
|
|
||||||
params:
|
|
||||||
- A
|
|
||||||
reducer:
|
|
||||||
type: last
|
|
||||||
type: query
|
|
||||||
datasource:
|
|
||||||
type: __expr__
|
|
||||||
uid: __expr__
|
|
||||||
expression: A
|
|
||||||
intervalMs: 1000
|
|
||||||
maxDataPoints: 43200
|
|
||||||
refId: C
|
|
||||||
type: threshold
|
|
||||||
|
|
||||||
- uid: ppanel-container-restarts
|
|
||||||
title: PPanel container restarted
|
|
||||||
condition: C
|
|
||||||
for: 1m
|
|
||||||
noDataState: NoData
|
|
||||||
execErrState: Error
|
|
||||||
annotations:
|
|
||||||
summary: "Container restarted"
|
|
||||||
description: "{{ $labels.name }} restarted or changed start time in the last hour."
|
|
||||||
labels:
|
|
||||||
severity: warning
|
|
||||||
service: ppanel
|
|
||||||
data:
|
|
||||||
- refId: A
|
|
||||||
relativeTimeRange:
|
|
||||||
from: 3600
|
|
||||||
to: 0
|
|
||||||
datasourceUid: prometheus
|
|
||||||
model:
|
|
||||||
datasource:
|
|
||||||
type: prometheus
|
|
||||||
uid: prometheus
|
|
||||||
editorMode: code
|
|
||||||
expr: 'sum by (name) (changes(container_start_time_seconds{name!=""}[1h]))'
|
|
||||||
instant: true
|
|
||||||
intervalMs: 1000
|
|
||||||
maxDataPoints: 43200
|
|
||||||
refId: A
|
|
||||||
- refId: C
|
|
||||||
datasourceUid: __expr__
|
|
||||||
model:
|
|
||||||
conditions:
|
|
||||||
- evaluator:
|
|
||||||
params:
|
|
||||||
- 0
|
|
||||||
type: gt
|
|
||||||
operator:
|
|
||||||
type: and
|
|
||||||
query:
|
|
||||||
params:
|
|
||||||
- A
|
|
||||||
reducer:
|
|
||||||
type: last
|
|
||||||
type: query
|
|
||||||
datasource:
|
|
||||||
type: __expr__
|
|
||||||
uid: __expr__
|
|
||||||
expression: A
|
|
||||||
intervalMs: 1000
|
|
||||||
maxDataPoints: 43200
|
|
||||||
refId: C
|
|
||||||
type: threshold
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
apiVersion: 1
|
|
||||||
|
|
||||||
providers:
|
|
||||||
- name: PPanel
|
|
||||||
orgId: 1
|
|
||||||
folder: PPanel
|
|
||||||
folderUid: ppanel
|
|
||||||
type: file
|
|
||||||
disableDeletion: false
|
|
||||||
allowUiUpdates: true
|
|
||||||
updateIntervalSeconds: 30
|
|
||||||
options:
|
|
||||||
path: /etc/grafana/provisioning/dashboards/json
|
|
||||||
foldersFromFilesStructure: false
|
|
||||||
@@ -1,520 +0,0 @@
|
|||||||
{
|
|
||||||
"annotations": {
|
|
||||||
"list": [
|
|
||||||
{
|
|
||||||
"builtIn": 1,
|
|
||||||
"datasource": {
|
|
||||||
"type": "grafana",
|
|
||||||
"uid": "-- Grafana --"
|
|
||||||
},
|
|
||||||
"enable": true,
|
|
||||||
"hide": true,
|
|
||||||
"iconColor": "rgba(0, 211, 255, 1)",
|
|
||||||
"name": "Annotations & Alerts",
|
|
||||||
"type": "dashboard"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"editable": true,
|
|
||||||
"fiscalYearStartMonth": 0,
|
|
||||||
"graphTooltip": 0,
|
|
||||||
"id": null,
|
|
||||||
"links": [],
|
|
||||||
"panels": [
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "cloudwatch",
|
|
||||||
"uid": "cloudwatch"
|
|
||||||
},
|
|
||||||
"gridPos": {
|
|
||||||
"h": 3,
|
|
||||||
"w": 24,
|
|
||||||
"x": 0,
|
|
||||||
"y": 0
|
|
||||||
},
|
|
||||||
"id": 1,
|
|
||||||
"options": {
|
|
||||||
"content": "<b>AWS CloudWatch overview</b><br/>Region: ap-east-1 (Hong Kong)<br/>RDS DBInstanceIdentifier: hifast-mysql-prod-v2<br/>Redis: current production uses a local Docker Redis container (<code>hifast-redis</code>) on EC2 rather than AWS ElastiCache.<br/><br/>This dashboard keeps the RDS CloudWatch panels. Redis should be observed from the local ops dashboard via Prometheus/cAdvisor instead of ElastiCache metrics.",
|
|
||||||
"mode": "html"
|
|
||||||
},
|
|
||||||
"pluginVersion": "11.0.0",
|
|
||||||
"title": "Read Me",
|
|
||||||
"type": "text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "cloudwatch",
|
|
||||||
"uid": "cloudwatch"
|
|
||||||
},
|
|
||||||
"fieldConfig": {
|
|
||||||
"defaults": {
|
|
||||||
"unit": "percent"
|
|
||||||
},
|
|
||||||
"overrides": []
|
|
||||||
},
|
|
||||||
"gridPos": {
|
|
||||||
"h": 8,
|
|
||||||
"w": 8,
|
|
||||||
"x": 0,
|
|
||||||
"y": 3
|
|
||||||
},
|
|
||||||
"id": 2,
|
|
||||||
"options": {
|
|
||||||
"legend": {
|
|
||||||
"displayMode": "table",
|
|
||||||
"placement": "bottom"
|
|
||||||
},
|
|
||||||
"tooltip": {
|
|
||||||
"mode": "single"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"targets": [
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "cloudwatch",
|
|
||||||
"uid": "cloudwatch"
|
|
||||||
},
|
|
||||||
"dimensions": {
|
|
||||||
"DBInstanceIdentifier": "hifast-mysql-prod-v2"
|
|
||||||
},
|
|
||||||
"metricName": "CPUUtilization",
|
|
||||||
"namespace": "AWS/RDS",
|
|
||||||
"period": "",
|
|
||||||
"refId": "A",
|
|
||||||
"region": "ap-east-1",
|
|
||||||
"statistic": "Average"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"title": "RDS CPU",
|
|
||||||
"type": "timeseries"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "cloudwatch",
|
|
||||||
"uid": "cloudwatch"
|
|
||||||
},
|
|
||||||
"fieldConfig": {
|
|
||||||
"defaults": {
|
|
||||||
"unit": "short"
|
|
||||||
},
|
|
||||||
"overrides": []
|
|
||||||
},
|
|
||||||
"gridPos": {
|
|
||||||
"h": 8,
|
|
||||||
"w": 8,
|
|
||||||
"x": 8,
|
|
||||||
"y": 3
|
|
||||||
},
|
|
||||||
"id": 3,
|
|
||||||
"options": {
|
|
||||||
"legend": {
|
|
||||||
"displayMode": "table",
|
|
||||||
"placement": "bottom"
|
|
||||||
},
|
|
||||||
"tooltip": {
|
|
||||||
"mode": "single"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"targets": [
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "cloudwatch",
|
|
||||||
"uid": "cloudwatch"
|
|
||||||
},
|
|
||||||
"dimensions": {
|
|
||||||
"DBInstanceIdentifier": "hifast-mysql-prod-v2"
|
|
||||||
},
|
|
||||||
"metricName": "DatabaseConnections",
|
|
||||||
"namespace": "AWS/RDS",
|
|
||||||
"period": "",
|
|
||||||
"refId": "A",
|
|
||||||
"region": "ap-east-1",
|
|
||||||
"statistic": "Average"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"title": "RDS Connections",
|
|
||||||
"type": "timeseries"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "cloudwatch",
|
|
||||||
"uid": "cloudwatch"
|
|
||||||
},
|
|
||||||
"fieldConfig": {
|
|
||||||
"defaults": {
|
|
||||||
"unit": "bytes"
|
|
||||||
},
|
|
||||||
"overrides": []
|
|
||||||
},
|
|
||||||
"gridPos": {
|
|
||||||
"h": 8,
|
|
||||||
"w": 8,
|
|
||||||
"x": 16,
|
|
||||||
"y": 3
|
|
||||||
},
|
|
||||||
"id": 4,
|
|
||||||
"options": {
|
|
||||||
"legend": {
|
|
||||||
"displayMode": "table",
|
|
||||||
"placement": "bottom"
|
|
||||||
},
|
|
||||||
"tooltip": {
|
|
||||||
"mode": "single"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"targets": [
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "cloudwatch",
|
|
||||||
"uid": "cloudwatch"
|
|
||||||
},
|
|
||||||
"dimensions": {
|
|
||||||
"DBInstanceIdentifier": "hifast-mysql-prod-v2"
|
|
||||||
},
|
|
||||||
"metricName": "FreeStorageSpace",
|
|
||||||
"namespace": "AWS/RDS",
|
|
||||||
"period": "",
|
|
||||||
"refId": "A",
|
|
||||||
"region": "ap-east-1",
|
|
||||||
"statistic": "Minimum"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"title": "RDS Free Storage",
|
|
||||||
"type": "timeseries"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "cloudwatch",
|
|
||||||
"uid": "cloudwatch"
|
|
||||||
},
|
|
||||||
"fieldConfig": {
|
|
||||||
"defaults": {
|
|
||||||
"unit": "s"
|
|
||||||
},
|
|
||||||
"overrides": []
|
|
||||||
},
|
|
||||||
"gridPos": {
|
|
||||||
"h": 8,
|
|
||||||
"w": 12,
|
|
||||||
"x": 0,
|
|
||||||
"y": 11
|
|
||||||
},
|
|
||||||
"id": 5,
|
|
||||||
"options": {
|
|
||||||
"legend": {
|
|
||||||
"displayMode": "table",
|
|
||||||
"placement": "bottom"
|
|
||||||
},
|
|
||||||
"tooltip": {
|
|
||||||
"mode": "multi"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"targets": [
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "cloudwatch",
|
|
||||||
"uid": "cloudwatch"
|
|
||||||
},
|
|
||||||
"dimensions": {
|
|
||||||
"DBInstanceIdentifier": "hifast-mysql-prod-v2"
|
|
||||||
},
|
|
||||||
"metricName": "ReadLatency",
|
|
||||||
"namespace": "AWS/RDS",
|
|
||||||
"period": "",
|
|
||||||
"refId": "A",
|
|
||||||
"region": "ap-east-1",
|
|
||||||
"statistic": "Average"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "cloudwatch",
|
|
||||||
"uid": "cloudwatch"
|
|
||||||
},
|
|
||||||
"dimensions": {
|
|
||||||
"DBInstanceIdentifier": "hifast-mysql-prod-v2"
|
|
||||||
},
|
|
||||||
"metricName": "WriteLatency",
|
|
||||||
"namespace": "AWS/RDS",
|
|
||||||
"period": "",
|
|
||||||
"refId": "B",
|
|
||||||
"region": "ap-east-1",
|
|
||||||
"statistic": "Average"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"title": "RDS Read / Write Latency",
|
|
||||||
"type": "timeseries"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "cloudwatch",
|
|
||||||
"uid": "cloudwatch"
|
|
||||||
},
|
|
||||||
"fieldConfig": {
|
|
||||||
"defaults": {
|
|
||||||
"unit": "iops"
|
|
||||||
},
|
|
||||||
"overrides": []
|
|
||||||
},
|
|
||||||
"gridPos": {
|
|
||||||
"h": 8,
|
|
||||||
"w": 12,
|
|
||||||
"x": 12,
|
|
||||||
"y": 11
|
|
||||||
},
|
|
||||||
"id": 6,
|
|
||||||
"options": {
|
|
||||||
"legend": {
|
|
||||||
"displayMode": "table",
|
|
||||||
"placement": "bottom"
|
|
||||||
},
|
|
||||||
"tooltip": {
|
|
||||||
"mode": "multi"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"targets": [
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "cloudwatch",
|
|
||||||
"uid": "cloudwatch"
|
|
||||||
},
|
|
||||||
"dimensions": {
|
|
||||||
"DBInstanceIdentifier": "hifast-mysql-prod-v2"
|
|
||||||
},
|
|
||||||
"metricName": "ReadIOPS",
|
|
||||||
"namespace": "AWS/RDS",
|
|
||||||
"period": "",
|
|
||||||
"refId": "A",
|
|
||||||
"region": "ap-east-1",
|
|
||||||
"statistic": "Average"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "cloudwatch",
|
|
||||||
"uid": "cloudwatch"
|
|
||||||
},
|
|
||||||
"dimensions": {
|
|
||||||
"DBInstanceIdentifier": "hifast-mysql-prod-v2"
|
|
||||||
},
|
|
||||||
"metricName": "WriteIOPS",
|
|
||||||
"namespace": "AWS/RDS",
|
|
||||||
"period": "",
|
|
||||||
"refId": "B",
|
|
||||||
"region": "ap-east-1",
|
|
||||||
"statistic": "Average"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"title": "RDS Read / Write IOPS",
|
|
||||||
"type": "timeseries"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "cloudwatch",
|
|
||||||
"uid": "cloudwatch"
|
|
||||||
},
|
|
||||||
"fieldConfig": {
|
|
||||||
"defaults": {
|
|
||||||
"unit": "percent"
|
|
||||||
},
|
|
||||||
"overrides": []
|
|
||||||
},
|
|
||||||
"gridPos": {
|
|
||||||
"h": 8,
|
|
||||||
"w": 6,
|
|
||||||
"x": 0,
|
|
||||||
"y": 19
|
|
||||||
},
|
|
||||||
"id": 7,
|
|
||||||
"options": {
|
|
||||||
"legend": {
|
|
||||||
"displayMode": "table",
|
|
||||||
"placement": "bottom"
|
|
||||||
},
|
|
||||||
"tooltip": {
|
|
||||||
"mode": "single"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"targets": [
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "cloudwatch",
|
|
||||||
"uid": "cloudwatch"
|
|
||||||
},
|
|
||||||
"dimensions": {
|
|
||||||
"ReplicationGroupId": "hifastapp-redis"
|
|
||||||
},
|
|
||||||
"metricName": "CPUUtilization",
|
|
||||||
"namespace": "AWS/ElastiCache",
|
|
||||||
"period": "",
|
|
||||||
"refId": "A",
|
|
||||||
"region": "ap-east-1",
|
|
||||||
"statistic": "Average"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"title": "Redis Host CPU (Legacy ElastiCache)",
|
|
||||||
"type": "timeseries"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "cloudwatch",
|
|
||||||
"uid": "cloudwatch"
|
|
||||||
},
|
|
||||||
"fieldConfig": {
|
|
||||||
"defaults": {
|
|
||||||
"unit": "percent"
|
|
||||||
},
|
|
||||||
"overrides": []
|
|
||||||
},
|
|
||||||
"gridPos": {
|
|
||||||
"h": 8,
|
|
||||||
"w": 6,
|
|
||||||
"x": 6,
|
|
||||||
"y": 19
|
|
||||||
},
|
|
||||||
"id": 8,
|
|
||||||
"options": {
|
|
||||||
"legend": {
|
|
||||||
"displayMode": "table",
|
|
||||||
"placement": "bottom"
|
|
||||||
},
|
|
||||||
"tooltip": {
|
|
||||||
"mode": "single"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"targets": [
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "cloudwatch",
|
|
||||||
"uid": "cloudwatch"
|
|
||||||
},
|
|
||||||
"dimensions": {
|
|
||||||
"ReplicationGroupId": "hifastapp-redis"
|
|
||||||
},
|
|
||||||
"metricName": "EngineCPUUtilization",
|
|
||||||
"namespace": "AWS/ElastiCache",
|
|
||||||
"period": "",
|
|
||||||
"refId": "A",
|
|
||||||
"region": "ap-east-1",
|
|
||||||
"statistic": "Average"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"title": "Redis Engine CPU (Legacy ElastiCache)",
|
|
||||||
"type": "timeseries"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "cloudwatch",
|
|
||||||
"uid": "cloudwatch"
|
|
||||||
},
|
|
||||||
"fieldConfig": {
|
|
||||||
"defaults": {
|
|
||||||
"unit": "short"
|
|
||||||
},
|
|
||||||
"overrides": []
|
|
||||||
},
|
|
||||||
"gridPos": {
|
|
||||||
"h": 8,
|
|
||||||
"w": 6,
|
|
||||||
"x": 12,
|
|
||||||
"y": 19
|
|
||||||
},
|
|
||||||
"id": 9,
|
|
||||||
"options": {
|
|
||||||
"legend": {
|
|
||||||
"displayMode": "table",
|
|
||||||
"placement": "bottom"
|
|
||||||
},
|
|
||||||
"tooltip": {
|
|
||||||
"mode": "single"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"targets": [
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "cloudwatch",
|
|
||||||
"uid": "cloudwatch"
|
|
||||||
},
|
|
||||||
"dimensions": {
|
|
||||||
"ReplicationGroupId": "hifastapp-redis"
|
|
||||||
},
|
|
||||||
"metricName": "CurrConnections",
|
|
||||||
"namespace": "AWS/ElastiCache",
|
|
||||||
"period": "",
|
|
||||||
"refId": "A",
|
|
||||||
"region": "ap-east-1",
|
|
||||||
"statistic": "Average"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"title": "Redis Connections (Legacy ElastiCache)",
|
|
||||||
"type": "timeseries"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "cloudwatch",
|
|
||||||
"uid": "cloudwatch"
|
|
||||||
},
|
|
||||||
"fieldConfig": {
|
|
||||||
"defaults": {
|
|
||||||
"unit": "percent"
|
|
||||||
},
|
|
||||||
"overrides": []
|
|
||||||
},
|
|
||||||
"gridPos": {
|
|
||||||
"h": 8,
|
|
||||||
"w": 6,
|
|
||||||
"x": 18,
|
|
||||||
"y": 19
|
|
||||||
},
|
|
||||||
"id": 10,
|
|
||||||
"options": {
|
|
||||||
"legend": {
|
|
||||||
"displayMode": "table",
|
|
||||||
"placement": "bottom"
|
|
||||||
},
|
|
||||||
"tooltip": {
|
|
||||||
"mode": "single"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"targets": [
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "cloudwatch",
|
|
||||||
"uid": "cloudwatch"
|
|
||||||
},
|
|
||||||
"dimensions": {
|
|
||||||
"ReplicationGroupId": "hifastapp-redis"
|
|
||||||
},
|
|
||||||
"metricName": "DatabaseMemoryUsagePercentage",
|
|
||||||
"namespace": "AWS/ElastiCache",
|
|
||||||
"period": "",
|
|
||||||
"refId": "A",
|
|
||||||
"region": "ap-east-1",
|
|
||||||
"statistic": "Average"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"title": "Redis Memory Usage % (Legacy ElastiCache)",
|
|
||||||
"type": "timeseries"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"refresh": "30s",
|
|
||||||
"schemaVersion": 39,
|
|
||||||
"style": "dark",
|
|
||||||
"tags": [
|
|
||||||
"aws",
|
|
||||||
"cloudwatch",
|
|
||||||
"rds",
|
|
||||||
"redis"
|
|
||||||
],
|
|
||||||
"templating": {
|
|
||||||
"list": []
|
|
||||||
},
|
|
||||||
"time": {
|
|
||||||
"from": "now-6h",
|
|
||||||
"to": "now"
|
|
||||||
},
|
|
||||||
"timepicker": {},
|
|
||||||
"timezone": "browser",
|
|
||||||
"title": "AWS RDS & Redis Overview",
|
|
||||||
"uid": "aws-rds-redis-overview",
|
|
||||||
"version": 1,
|
|
||||||
"weekStart": ""
|
|
||||||
}
|
|
||||||
@@ -1,565 +0,0 @@
|
|||||||
{
|
|
||||||
"annotations": {
|
|
||||||
"list": [
|
|
||||||
{
|
|
||||||
"builtIn": 1,
|
|
||||||
"datasource": {
|
|
||||||
"type": "grafana",
|
|
||||||
"uid": "-- Grafana --"
|
|
||||||
},
|
|
||||||
"enable": true,
|
|
||||||
"hide": true,
|
|
||||||
"iconColor": "rgba(0, 211, 255, 1)",
|
|
||||||
"name": "Annotations & Alerts",
|
|
||||||
"type": "dashboard"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"editable": true,
|
|
||||||
"fiscalYearStartMonth": 0,
|
|
||||||
"graphTooltip": 0,
|
|
||||||
"id": null,
|
|
||||||
"links": [],
|
|
||||||
"panels": [
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "prometheus",
|
|
||||||
"uid": "prometheus"
|
|
||||||
},
|
|
||||||
"fieldConfig": {
|
|
||||||
"defaults": {
|
|
||||||
"color": {
|
|
||||||
"mode": "thresholds"
|
|
||||||
},
|
|
||||||
"mappings": [
|
|
||||||
{
|
|
||||||
"options": {
|
|
||||||
"0": {
|
|
||||||
"text": "DOWN"
|
|
||||||
},
|
|
||||||
"1": {
|
|
||||||
"text": "UP"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"type": "value"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"thresholds": {
|
|
||||||
"mode": "absolute",
|
|
||||||
"steps": [
|
|
||||||
{
|
|
||||||
"color": "red",
|
|
||||||
"value": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"color": "green",
|
|
||||||
"value": 1
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"overrides": []
|
|
||||||
},
|
|
||||||
"gridPos": {
|
|
||||||
"h": 4,
|
|
||||||
"w": 24,
|
|
||||||
"x": 0,
|
|
||||||
"y": 0
|
|
||||||
},
|
|
||||||
"id": 1,
|
|
||||||
"options": {
|
|
||||||
"colorMode": "background",
|
|
||||||
"graphMode": "none",
|
|
||||||
"justifyMode": "center",
|
|
||||||
"orientation": "horizontal",
|
|
||||||
"reduceOptions": {
|
|
||||||
"calcs": [
|
|
||||||
"lastNotNull"
|
|
||||||
],
|
|
||||||
"fields": "",
|
|
||||||
"values": false
|
|
||||||
},
|
|
||||||
"showPercentChange": false,
|
|
||||||
"textMode": "auto",
|
|
||||||
"wideLayout": true
|
|
||||||
},
|
|
||||||
"pluginVersion": "13.0.1",
|
|
||||||
"targets": [
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "prometheus",
|
|
||||||
"uid": "prometheus"
|
|
||||||
},
|
|
||||||
"editorMode": "code",
|
|
||||||
"expr": "up{job=~\"prometheus|grafana|node-exporter|cadvisor|nginx-exporter|loki|tempo\"}",
|
|
||||||
"instant": true,
|
|
||||||
"legendFormat": "{{job}}",
|
|
||||||
"range": false,
|
|
||||||
"refId": "A"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"title": "Service Availability",
|
|
||||||
"type": "stat"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "prometheus",
|
|
||||||
"uid": "prometheus"
|
|
||||||
},
|
|
||||||
"fieldConfig": {
|
|
||||||
"defaults": {
|
|
||||||
"max": 100,
|
|
||||||
"min": 0,
|
|
||||||
"thresholds": {
|
|
||||||
"mode": "absolute",
|
|
||||||
"steps": [
|
|
||||||
{
|
|
||||||
"color": "green",
|
|
||||||
"value": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"color": "orange",
|
|
||||||
"value": 75
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"color": "red",
|
|
||||||
"value": 90
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"unit": "percent"
|
|
||||||
},
|
|
||||||
"overrides": []
|
|
||||||
},
|
|
||||||
"gridPos": {
|
|
||||||
"h": 8,
|
|
||||||
"w": 8,
|
|
||||||
"x": 0,
|
|
||||||
"y": 4
|
|
||||||
},
|
|
||||||
"id": 2,
|
|
||||||
"options": {
|
|
||||||
"legend": {
|
|
||||||
"displayMode": "list",
|
|
||||||
"placement": "bottom"
|
|
||||||
},
|
|
||||||
"tooltip": {
|
|
||||||
"mode": "single"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"targets": [
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "prometheus",
|
|
||||||
"uid": "prometheus"
|
|
||||||
},
|
|
||||||
"editorMode": "code",
|
|
||||||
"expr": "100 - (avg by (instance) (rate(node_cpu_seconds_total{mode=\"idle\"}[5m])) * 100)",
|
|
||||||
"legendFormat": "{{instance}} CPU",
|
|
||||||
"range": true,
|
|
||||||
"refId": "A"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"title": "Host CPU Usage",
|
|
||||||
"type": "timeseries"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "prometheus",
|
|
||||||
"uid": "prometheus"
|
|
||||||
},
|
|
||||||
"fieldConfig": {
|
|
||||||
"defaults": {
|
|
||||||
"max": 100,
|
|
||||||
"min": 0,
|
|
||||||
"thresholds": {
|
|
||||||
"mode": "absolute",
|
|
||||||
"steps": [
|
|
||||||
{
|
|
||||||
"color": "green",
|
|
||||||
"value": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"color": "orange",
|
|
||||||
"value": 80
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"color": "red",
|
|
||||||
"value": 90
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"unit": "percent"
|
|
||||||
},
|
|
||||||
"overrides": []
|
|
||||||
},
|
|
||||||
"gridPos": {
|
|
||||||
"h": 8,
|
|
||||||
"w": 8,
|
|
||||||
"x": 8,
|
|
||||||
"y": 4
|
|
||||||
},
|
|
||||||
"id": 3,
|
|
||||||
"options": {
|
|
||||||
"legend": {
|
|
||||||
"displayMode": "list",
|
|
||||||
"placement": "bottom"
|
|
||||||
},
|
|
||||||
"tooltip": {
|
|
||||||
"mode": "single"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"targets": [
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "prometheus",
|
|
||||||
"uid": "prometheus"
|
|
||||||
},
|
|
||||||
"editorMode": "code",
|
|
||||||
"expr": "(1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)) * 100",
|
|
||||||
"legendFormat": "{{instance}} memory",
|
|
||||||
"range": true,
|
|
||||||
"refId": "A"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"title": "Host Memory Usage",
|
|
||||||
"type": "timeseries"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "prometheus",
|
|
||||||
"uid": "prometheus"
|
|
||||||
},
|
|
||||||
"fieldConfig": {
|
|
||||||
"defaults": {
|
|
||||||
"max": 100,
|
|
||||||
"min": 0,
|
|
||||||
"thresholds": {
|
|
||||||
"mode": "absolute",
|
|
||||||
"steps": [
|
|
||||||
{
|
|
||||||
"color": "green",
|
|
||||||
"value": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"color": "orange",
|
|
||||||
"value": 80
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"color": "red",
|
|
||||||
"value": 90
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"unit": "percent"
|
|
||||||
},
|
|
||||||
"overrides": []
|
|
||||||
},
|
|
||||||
"gridPos": {
|
|
||||||
"h": 8,
|
|
||||||
"w": 8,
|
|
||||||
"x": 16,
|
|
||||||
"y": 4
|
|
||||||
},
|
|
||||||
"id": 4,
|
|
||||||
"options": {
|
|
||||||
"legend": {
|
|
||||||
"displayMode": "list",
|
|
||||||
"placement": "bottom"
|
|
||||||
},
|
|
||||||
"tooltip": {
|
|
||||||
"mode": "single"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"targets": [
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "prometheus",
|
|
||||||
"uid": "prometheus"
|
|
||||||
},
|
|
||||||
"editorMode": "code",
|
|
||||||
"expr": "100 - (node_filesystem_avail_bytes{fstype!~\"tmpfs|overlay|squashfs|aufs\",mountpoint!~\"/run.*|/var/lib/docker.*\"} / node_filesystem_size_bytes{fstype!~\"tmpfs|overlay|squashfs|aufs\",mountpoint!~\"/run.*|/var/lib/docker.*\"} * 100)",
|
|
||||||
"legendFormat": "{{mountpoint}}",
|
|
||||||
"range": true,
|
|
||||||
"refId": "A"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"title": "Host Disk Usage",
|
|
||||||
"type": "timeseries"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "prometheus",
|
|
||||||
"uid": "prometheus"
|
|
||||||
},
|
|
||||||
"fieldConfig": {
|
|
||||||
"defaults": {
|
|
||||||
"unit": "percentunit"
|
|
||||||
},
|
|
||||||
"overrides": []
|
|
||||||
},
|
|
||||||
"gridPos": {
|
|
||||||
"h": 8,
|
|
||||||
"w": 8,
|
|
||||||
"x": 0,
|
|
||||||
"y": 12
|
|
||||||
},
|
|
||||||
"id": 5,
|
|
||||||
"options": {
|
|
||||||
"legend": {
|
|
||||||
"displayMode": "table",
|
|
||||||
"placement": "bottom"
|
|
||||||
},
|
|
||||||
"tooltip": {
|
|
||||||
"mode": "multi"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"targets": [
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "prometheus",
|
|
||||||
"uid": "prometheus"
|
|
||||||
},
|
|
||||||
"editorMode": "code",
|
|
||||||
"expr": "sum by (name) (rate(container_cpu_usage_seconds_total{name!=\"\"}[5m]))",
|
|
||||||
"legendFormat": "{{name}}",
|
|
||||||
"range": true,
|
|
||||||
"refId": "A"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"title": "Container CPU",
|
|
||||||
"type": "timeseries"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "prometheus",
|
|
||||||
"uid": "prometheus"
|
|
||||||
},
|
|
||||||
"fieldConfig": {
|
|
||||||
"defaults": {
|
|
||||||
"unit": "bytes"
|
|
||||||
},
|
|
||||||
"overrides": []
|
|
||||||
},
|
|
||||||
"gridPos": {
|
|
||||||
"h": 8,
|
|
||||||
"w": 8,
|
|
||||||
"x": 8,
|
|
||||||
"y": 12
|
|
||||||
},
|
|
||||||
"id": 6,
|
|
||||||
"options": {
|
|
||||||
"legend": {
|
|
||||||
"displayMode": "table",
|
|
||||||
"placement": "bottom"
|
|
||||||
},
|
|
||||||
"tooltip": {
|
|
||||||
"mode": "multi"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"targets": [
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "prometheus",
|
|
||||||
"uid": "prometheus"
|
|
||||||
},
|
|
||||||
"editorMode": "code",
|
|
||||||
"expr": "sum by (name) (container_memory_working_set_bytes{name!=\"\"})",
|
|
||||||
"legendFormat": "{{name}}",
|
|
||||||
"range": true,
|
|
||||||
"refId": "A"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"title": "Container Memory",
|
|
||||||
"type": "timeseries"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "prometheus",
|
|
||||||
"uid": "prometheus"
|
|
||||||
},
|
|
||||||
"fieldConfig": {
|
|
||||||
"defaults": {
|
|
||||||
"unit": "short"
|
|
||||||
},
|
|
||||||
"overrides": []
|
|
||||||
},
|
|
||||||
"gridPos": {
|
|
||||||
"h": 8,
|
|
||||||
"w": 8,
|
|
||||||
"x": 16,
|
|
||||||
"y": 12
|
|
||||||
},
|
|
||||||
"id": 7,
|
|
||||||
"options": {
|
|
||||||
"legend": {
|
|
||||||
"displayMode": "table",
|
|
||||||
"placement": "bottom"
|
|
||||||
},
|
|
||||||
"tooltip": {
|
|
||||||
"mode": "multi"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"targets": [
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "prometheus",
|
|
||||||
"uid": "prometheus"
|
|
||||||
},
|
|
||||||
"editorMode": "code",
|
|
||||||
"expr": "sum by (name) (changes(container_start_time_seconds{name!=\"\"}[1h]))",
|
|
||||||
"legendFormat": "{{name}}",
|
|
||||||
"range": true,
|
|
||||||
"refId": "A"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"title": "Container Restarts / Changes",
|
|
||||||
"type": "timeseries"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "prometheus",
|
|
||||||
"uid": "prometheus"
|
|
||||||
},
|
|
||||||
"fieldConfig": {
|
|
||||||
"defaults": {
|
|
||||||
"unit": "reqps"
|
|
||||||
},
|
|
||||||
"overrides": []
|
|
||||||
},
|
|
||||||
"gridPos": {
|
|
||||||
"h": 8,
|
|
||||||
"w": 8,
|
|
||||||
"x": 0,
|
|
||||||
"y": 20
|
|
||||||
},
|
|
||||||
"id": 8,
|
|
||||||
"options": {
|
|
||||||
"legend": {
|
|
||||||
"displayMode": "list",
|
|
||||||
"placement": "bottom"
|
|
||||||
},
|
|
||||||
"tooltip": {
|
|
||||||
"mode": "single"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"targets": [
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "prometheus",
|
|
||||||
"uid": "prometheus"
|
|
||||||
},
|
|
||||||
"editorMode": "code",
|
|
||||||
"expr": "rate(nginx_http_requests_total[5m])",
|
|
||||||
"legendFormat": "requests",
|
|
||||||
"range": true,
|
|
||||||
"refId": "A"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"title": "Nginx Requests",
|
|
||||||
"type": "timeseries"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "loki",
|
|
||||||
"uid": "loki"
|
|
||||||
},
|
|
||||||
"gridPos": {
|
|
||||||
"h": 8,
|
|
||||||
"w": 8,
|
|
||||||
"x": 8,
|
|
||||||
"y": 20
|
|
||||||
},
|
|
||||||
"id": 9,
|
|
||||||
"options": {
|
|
||||||
"dedupStrategy": "none",
|
|
||||||
"enableLogDetails": true,
|
|
||||||
"prettifyLogMessage": false,
|
|
||||||
"showCommonLabels": false,
|
|
||||||
"showLabels": true,
|
|
||||||
"showTime": true,
|
|
||||||
"sortOrder": "Descending",
|
|
||||||
"wrapLogMessage": true
|
|
||||||
},
|
|
||||||
"targets": [
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "loki",
|
|
||||||
"uid": "loki"
|
|
||||||
},
|
|
||||||
"editorMode": "code",
|
|
||||||
"expr": "{job=~\"ppanel-server|nginx|docker\"} |~ \"(?i)(error|panic|fatal|timeout|exception|failed)\"",
|
|
||||||
"queryType": "range",
|
|
||||||
"refId": "A"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"title": "Recent Errors",
|
|
||||||
"type": "logs"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "prometheus",
|
|
||||||
"uid": "prometheus"
|
|
||||||
},
|
|
||||||
"fieldConfig": {
|
|
||||||
"defaults": {
|
|
||||||
"unit": "reqps"
|
|
||||||
},
|
|
||||||
"overrides": []
|
|
||||||
},
|
|
||||||
"gridPos": {
|
|
||||||
"h": 8,
|
|
||||||
"w": 8,
|
|
||||||
"x": 16,
|
|
||||||
"y": 20
|
|
||||||
},
|
|
||||||
"id": 10,
|
|
||||||
"options": {
|
|
||||||
"legend": {
|
|
||||||
"displayMode": "table",
|
|
||||||
"placement": "bottom"
|
|
||||||
},
|
|
||||||
"tooltip": {
|
|
||||||
"mode": "multi"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"targets": [
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "prometheus",
|
|
||||||
"uid": "prometheus"
|
|
||||||
},
|
|
||||||
"editorMode": "code",
|
|
||||||
"expr": "sum by (service_name) (rate(traces_spanmetrics_calls_total[5m]))",
|
|
||||||
"legendFormat": "{{service_name}}",
|
|
||||||
"range": true,
|
|
||||||
"refId": "A"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"title": "Trace Span Calls",
|
|
||||||
"type": "timeseries"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"refresh": "30s",
|
|
||||||
"schemaVersion": 42,
|
|
||||||
"tags": [
|
|
||||||
"ppanel",
|
|
||||||
"ops",
|
|
||||||
"prometheus",
|
|
||||||
"loki",
|
|
||||||
"tempo"
|
|
||||||
],
|
|
||||||
"templating": {
|
|
||||||
"list": []
|
|
||||||
},
|
|
||||||
"time": {
|
|
||||||
"from": "now-6h",
|
|
||||||
"to": "now"
|
|
||||||
},
|
|
||||||
"timepicker": {},
|
|
||||||
"timezone": "browser",
|
|
||||||
"title": "PPanel Ops Overview",
|
|
||||||
"uid": "ppanel-ops-overview",
|
|
||||||
"version": 1,
|
|
||||||
"weekStart": ""
|
|
||||||
}
|
|
||||||
@@ -1,330 +0,0 @@
|
|||||||
{
|
|
||||||
"annotations": {
|
|
||||||
"list": [
|
|
||||||
{
|
|
||||||
"builtIn": 1,
|
|
||||||
"datasource": {
|
|
||||||
"type": "grafana",
|
|
||||||
"uid": "-- Grafana --"
|
|
||||||
},
|
|
||||||
"enable": true,
|
|
||||||
"hide": true,
|
|
||||||
"iconColor": "rgba(0, 211, 255, 1)",
|
|
||||||
"name": "Annotations & Alerts",
|
|
||||||
"type": "dashboard"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"editable": true,
|
|
||||||
"fiscalYearStartMonth": 0,
|
|
||||||
"graphTooltip": 0,
|
|
||||||
"id": null,
|
|
||||||
"links": [],
|
|
||||||
"panels": [
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "loki",
|
|
||||||
"uid": "P8E80F9AEF21F6940"
|
|
||||||
},
|
|
||||||
"fieldConfig": {
|
|
||||||
"defaults": {
|
|
||||||
"color": {
|
|
||||||
"mode": "palette-classic"
|
|
||||||
},
|
|
||||||
"unit": "short"
|
|
||||||
},
|
|
||||||
"overrides": []
|
|
||||||
},
|
|
||||||
"gridPos": {
|
|
||||||
"h": 7,
|
|
||||||
"w": 12,
|
|
||||||
"x": 0,
|
|
||||||
"y": 0
|
|
||||||
},
|
|
||||||
"id": 1,
|
|
||||||
"options": {
|
|
||||||
"legend": {
|
|
||||||
"displayMode": "table",
|
|
||||||
"placement": "bottom"
|
|
||||||
},
|
|
||||||
"tooltip": {
|
|
||||||
"mode": "multi"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"targets": [
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "loki",
|
|
||||||
"uid": "P8E80F9AEF21F6940"
|
|
||||||
},
|
|
||||||
"editorMode": "code",
|
|
||||||
"expr": "sum(count_over_time({compose_service=\"ppanel-server\"}[5m]))",
|
|
||||||
"queryType": "range",
|
|
||||||
"refId": "A"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"title": "Matched Log Volume",
|
|
||||||
"type": "timeseries"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "loki",
|
|
||||||
"uid": "P8E80F9AEF21F6940"
|
|
||||||
},
|
|
||||||
"fieldConfig": {
|
|
||||||
"defaults": {
|
|
||||||
"color": {
|
|
||||||
"mode": "palette-classic"
|
|
||||||
},
|
|
||||||
"unit": "short"
|
|
||||||
},
|
|
||||||
"overrides": []
|
|
||||||
},
|
|
||||||
"gridPos": {
|
|
||||||
"h": 7,
|
|
||||||
"w": 12,
|
|
||||||
"x": 12,
|
|
||||||
"y": 0
|
|
||||||
},
|
|
||||||
"id": 2,
|
|
||||||
"options": {
|
|
||||||
"legend": {
|
|
||||||
"displayMode": "table",
|
|
||||||
"placement": "bottom"
|
|
||||||
},
|
|
||||||
"tooltip": {
|
|
||||||
"mode": "multi"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"targets": [
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "loki",
|
|
||||||
"uid": "P8E80F9AEF21F6940"
|
|
||||||
},
|
|
||||||
"editorMode": "code",
|
|
||||||
"expr": "sum(count_over_time({compose_service=\"ppanel-server\"} |~ \"(?i)(error|panic|fatal)\" [5m]))",
|
|
||||||
"queryType": "range",
|
|
||||||
"refId": "A"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"title": "Matched Error Volume",
|
|
||||||
"type": "timeseries"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "loki",
|
|
||||||
"uid": "P8E80F9AEF21F6940"
|
|
||||||
},
|
|
||||||
"gridPos": {
|
|
||||||
"h": 12,
|
|
||||||
"w": 24,
|
|
||||||
"x": 0,
|
|
||||||
"y": 7
|
|
||||||
},
|
|
||||||
"id": 3,
|
|
||||||
"options": {
|
|
||||||
"dedupStrategy": "none",
|
|
||||||
"enableLogDetails": true,
|
|
||||||
"prettifyLogMessage": false,
|
|
||||||
"showCommonLabels": false,
|
|
||||||
"showLabels": true,
|
|
||||||
"showTime": true,
|
|
||||||
"sortOrder": "Descending",
|
|
||||||
"wrapLogMessage": true
|
|
||||||
},
|
|
||||||
"targets": [
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "loki",
|
|
||||||
"uid": "P8E80F9AEF21F6940"
|
|
||||||
},
|
|
||||||
"editorMode": "code",
|
|
||||||
"expr": "{compose_service=\"ppanel-server\"}",
|
|
||||||
"queryType": "range",
|
|
||||||
"refId": "A"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"title": "Filtered Server Logs",
|
|
||||||
"type": "logs"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "loki",
|
|
||||||
"uid": "P8E80F9AEF21F6940"
|
|
||||||
},
|
|
||||||
"gridPos": {
|
|
||||||
"h": 10,
|
|
||||||
"w": 24,
|
|
||||||
"x": 0,
|
|
||||||
"y": 19
|
|
||||||
},
|
|
||||||
"id": 4,
|
|
||||||
"options": {
|
|
||||||
"dedupStrategy": "none",
|
|
||||||
"enableLogDetails": true,
|
|
||||||
"prettifyLogMessage": false,
|
|
||||||
"showCommonLabels": false,
|
|
||||||
"showLabels": true,
|
|
||||||
"showTime": true,
|
|
||||||
"sortOrder": "Descending",
|
|
||||||
"wrapLogMessage": true
|
|
||||||
},
|
|
||||||
"targets": [
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "loki",
|
|
||||||
"uid": "P8E80F9AEF21F6940"
|
|
||||||
},
|
|
||||||
"editorMode": "code",
|
|
||||||
"expr": "{compose_service=\"ppanel-server\"} |~ \"(?i)(error|panic|fatal)\"",
|
|
||||||
"queryType": "range",
|
|
||||||
"refId": "A"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"title": "Filtered Server Errors",
|
|
||||||
"type": "logs"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"refresh": "30s",
|
|
||||||
"schemaVersion": 42,
|
|
||||||
"tags": [
|
|
||||||
"ppanel",
|
|
||||||
"logs",
|
|
||||||
"server",
|
|
||||||
"loki"
|
|
||||||
],
|
|
||||||
"templating": {
|
|
||||||
"list": [
|
|
||||||
{
|
|
||||||
"current": {
|
|
||||||
"selected": false,
|
|
||||||
"text": ".",
|
|
||||||
"value": "."
|
|
||||||
},
|
|
||||||
"description": "输入用户 ID;默认 . 表示不过滤",
|
|
||||||
"hide": 0,
|
|
||||||
"label": "用户ID",
|
|
||||||
"name": "user_id",
|
|
||||||
"options": [],
|
|
||||||
"query": ".",
|
|
||||||
"skipUrlSync": false,
|
|
||||||
"type": "textbox"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"current": {
|
|
||||||
"selected": false,
|
|
||||||
"text": ".",
|
|
||||||
"value": "."
|
|
||||||
},
|
|
||||||
"description": "输入邮箱或邮箱片段;默认 . 表示不过滤",
|
|
||||||
"hide": 0,
|
|
||||||
"label": "邮箱",
|
|
||||||
"name": "email",
|
|
||||||
"options": [],
|
|
||||||
"query": ".",
|
|
||||||
"skipUrlSync": false,
|
|
||||||
"type": "textbox"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"current": {
|
|
||||||
"selected": false,
|
|
||||||
"text": ".",
|
|
||||||
"value": "."
|
|
||||||
},
|
|
||||||
"description": "输入订单号/支付单号/交易号片段;默认 . 表示不过滤",
|
|
||||||
"hide": 0,
|
|
||||||
"label": "订单",
|
|
||||||
"name": "order",
|
|
||||||
"options": [],
|
|
||||||
"query": ".",
|
|
||||||
"skipUrlSync": false,
|
|
||||||
"type": "textbox"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"current": {
|
|
||||||
"selected": true,
|
|
||||||
"text": "All",
|
|
||||||
"value": "."
|
|
||||||
},
|
|
||||||
"description": "日志等级",
|
|
||||||
"hide": 0,
|
|
||||||
"includeAll": false,
|
|
||||||
"label": "等级",
|
|
||||||
"multi": false,
|
|
||||||
"name": "level",
|
|
||||||
"options": [
|
|
||||||
{
|
|
||||||
"selected": true,
|
|
||||||
"text": "All",
|
|
||||||
"value": "."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"selected": false,
|
|
||||||
"text": "debug",
|
|
||||||
"value": "debug"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"selected": false,
|
|
||||||
"text": "info",
|
|
||||||
"value": "info"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"selected": false,
|
|
||||||
"text": "warn",
|
|
||||||
"value": "warn"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"selected": false,
|
|
||||||
"text": "error",
|
|
||||||
"value": "error"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"selected": false,
|
|
||||||
"text": "slow",
|
|
||||||
"value": "slow"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"selected": false,
|
|
||||||
"text": "panic",
|
|
||||||
"value": "panic"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"selected": false,
|
|
||||||
"text": "fatal",
|
|
||||||
"value": "fatal"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"query": "All : .,debug,info,warn,error,slow,panic,fatal",
|
|
||||||
"queryValue": "",
|
|
||||||
"skipUrlSync": false,
|
|
||||||
"type": "custom"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"current": {
|
|
||||||
"selected": false,
|
|
||||||
"text": ".",
|
|
||||||
"value": "."
|
|
||||||
},
|
|
||||||
"description": "任意关键字;默认 . 表示不过滤",
|
|
||||||
"hide": 0,
|
|
||||||
"label": "关键字",
|
|
||||||
"name": "keyword",
|
|
||||||
"options": [],
|
|
||||||
"query": ".",
|
|
||||||
"skipUrlSync": false,
|
|
||||||
"type": "textbox"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"time": {
|
|
||||||
"from": "now-1h",
|
|
||||||
"to": "now"
|
|
||||||
},
|
|
||||||
"timepicker": {},
|
|
||||||
"timezone": "browser",
|
|
||||||
"title": "PPanel Server Logs",
|
|
||||||
"uid": "ppanel-server-logs",
|
|
||||||
"version": 5,
|
|
||||||
"weekStart": ""
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
apiVersion: 1
|
|
||||||
|
|
||||||
datasources:
|
|
||||||
- name: Prometheus
|
|
||||||
uid: prometheus
|
|
||||||
type: prometheus
|
|
||||||
access: proxy
|
|
||||||
url: http://prometheus:9090
|
|
||||||
isDefault: true
|
|
||||||
editable: true
|
|
||||||
jsonData:
|
|
||||||
httpMethod: POST
|
|
||||||
manageAlerts: true
|
|
||||||
prometheusType: Prometheus
|
|
||||||
prometheusVersion: 2.50.0
|
|
||||||
timeInterval: 15s
|
|
||||||
|
|
||||||
- name: Loki
|
|
||||||
uid: loki
|
|
||||||
type: loki
|
|
||||||
access: proxy
|
|
||||||
url: http://loki:3100
|
|
||||||
editable: true
|
|
||||||
jsonData:
|
|
||||||
derivedFields:
|
|
||||||
- datasourceUid: tempo
|
|
||||||
matcherRegex: '"(?:trace|traceID|trace_id)"\s*:\s*"([a-f0-9]{32})"'
|
|
||||||
name: TraceID
|
|
||||||
url: '$${__value.raw}'
|
|
||||||
|
|
||||||
- name: Tempo
|
|
||||||
uid: tempo
|
|
||||||
type: tempo
|
|
||||||
access: proxy
|
|
||||||
url: http://tempo:3200
|
|
||||||
editable: true
|
|
||||||
jsonData:
|
|
||||||
tracesToLogsV2:
|
|
||||||
datasourceUid: loki
|
|
||||||
filterByTraceID: true
|
|
||||||
filterBySpanID: false
|
|
||||||
tags:
|
|
||||||
- key: service.name
|
|
||||||
value: service_name
|
|
||||||
tracesToMetrics:
|
|
||||||
datasourceUid: prometheus
|
|
||||||
serviceMap:
|
|
||||||
datasourceUid: prometheus
|
|
||||||
|
|
||||||
- name: CloudWatch
|
|
||||||
uid: cloudwatch
|
|
||||||
type: cloudwatch
|
|
||||||
access: proxy
|
|
||||||
editable: true
|
|
||||||
jsonData:
|
|
||||||
authType: default
|
|
||||||
defaultRegion: ap-east-1
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
auth_enabled: false
|
|
||||||
|
|
||||||
server:
|
|
||||||
http_listen_port: 3100
|
|
||||||
grpc_listen_port: 9096
|
|
||||||
|
|
||||||
common:
|
|
||||||
path_prefix: /loki
|
|
||||||
replication_factor: 1
|
|
||||||
ring:
|
|
||||||
instance_addr: 127.0.0.1
|
|
||||||
kvstore:
|
|
||||||
store: inmemory
|
|
||||||
|
|
||||||
schema_config:
|
|
||||||
configs:
|
|
||||||
- from: 2024-01-01
|
|
||||||
store: tsdb
|
|
||||||
object_store: filesystem
|
|
||||||
schema: v13
|
|
||||||
index:
|
|
||||||
prefix: index_
|
|
||||||
period: 24h
|
|
||||||
|
|
||||||
storage_config:
|
|
||||||
filesystem:
|
|
||||||
directory: /loki/chunks
|
|
||||||
|
|
||||||
limits_config:
|
|
||||||
allow_structured_metadata: true
|
|
||||||
retention_period: 168h
|
|
||||||
|
|
||||||
compactor:
|
|
||||||
working_directory: /loki/compactor
|
|
||||||
retention_enabled: true
|
|
||||||
delete_request_store: filesystem
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
server:
|
|
||||||
http_listen_port: 9080
|
|
||||||
grpc_listen_port: 0
|
|
||||||
|
|
||||||
positions:
|
|
||||||
filename: /tmp/positions.yaml
|
|
||||||
|
|
||||||
clients:
|
|
||||||
- url: http://loki:3100/loki/api/v1/push
|
|
||||||
|
|
||||||
scrape_configs:
|
|
||||||
- job_name: ppanel-file
|
|
||||||
static_configs:
|
|
||||||
- targets:
|
|
||||||
- localhost
|
|
||||||
labels:
|
|
||||||
job: ppanel-server
|
|
||||||
service_name: ppanel
|
|
||||||
__path__: /var/log/ppanel-server/*.log
|
|
||||||
pipeline_stages:
|
|
||||||
- json:
|
|
||||||
expressions:
|
|
||||||
caller: caller
|
|
||||||
content: content
|
|
||||||
level: level
|
|
||||||
timestamp: timestamp
|
|
||||||
- timestamp:
|
|
||||||
source: timestamp
|
|
||||||
format: "2006-01-02 15:04:05.000"
|
|
||||||
location: Asia/Shanghai
|
|
||||||
- labels:
|
|
||||||
caller:
|
|
||||||
level:
|
|
||||||
|
|
||||||
- job_name: nginx-file
|
|
||||||
static_configs:
|
|
||||||
- targets:
|
|
||||||
- localhost
|
|
||||||
labels:
|
|
||||||
job: nginx
|
|
||||||
service_name: nginx
|
|
||||||
__path__: /var/log/nginx/*.log
|
|
||||||
|
|
||||||
- job_name: docker-containers
|
|
||||||
docker_sd_configs:
|
|
||||||
- host: unix:///var/run/docker.sock
|
|
||||||
refresh_interval: 15s
|
|
||||||
relabel_configs:
|
|
||||||
- source_labels: [__meta_docker_container_name]
|
|
||||||
regex: '/(.*)'
|
|
||||||
target_label: container
|
|
||||||
- source_labels: [__meta_docker_container_label_com_docker_compose_service]
|
|
||||||
target_label: compose_service
|
|
||||||
- source_labels: [__meta_docker_container_log_stream]
|
|
||||||
target_label: stream
|
|
||||||
- source_labels: [__meta_docker_container_name]
|
|
||||||
regex: '/(.*)'
|
|
||||||
target_label: service_name
|
|
||||||
- source_labels: [__meta_docker_container_id]
|
|
||||||
target_label: __path__
|
|
||||||
replacement: /var/lib/docker/containers/$1/$1-json.log
|
|
||||||
@@ -1,652 +0,0 @@
|
|||||||
# AWS RDS + EC2 Redis 到外部备用服务器 Runbook
|
|
||||||
|
|
||||||
目标:让 `104.238.220.230` 持续作为 AWS 主生产环境的异地备用节点,承接:
|
|
||||||
|
|
||||||
- MySQL 外部只读从库
|
|
||||||
- Redis 外部从库
|
|
||||||
- 故障时的快速提升与业务切换
|
|
||||||
|
|
||||||
本文档以 `2026-05-13` 的真实现网状态为准,覆盖:
|
|
||||||
|
|
||||||
- 当前已经落地的主从架构
|
|
||||||
- 日常验收命令
|
|
||||||
- 主从故障排查
|
|
||||||
- 全量重建从库
|
|
||||||
- “新建规范 RDS 再切换”的生产级收敛路线
|
|
||||||
- 故障切换与回滚
|
|
||||||
|
|
||||||
## 1. 当前已确认资源
|
|
||||||
|
|
||||||
### 1.1 AWS 主生产
|
|
||||||
|
|
||||||
- Region: `ap-east-1`
|
|
||||||
- AWS app EC2:
|
|
||||||
- Name: `hifast-hk-app-01`
|
|
||||||
- Public IP: `18.163.33.75`
|
|
||||||
- Private IP: `10.0.1.201`
|
|
||||||
- AWS MySQL:
|
|
||||||
- Type: `RDS MySQL`
|
|
||||||
- Instance: `hifast-mysql-prod-v2`
|
|
||||||
- Endpoint: `hifast-mysql-prod-v2.cd6aey40m6ag.ap-east-1.rds.amazonaws.com`
|
|
||||||
- Version: `8.4.8`
|
|
||||||
- DB Name: `hifast`
|
|
||||||
- Admin user: `admin`
|
|
||||||
- Admin password: keep it in a secret store, do not write plaintext into repo docs
|
|
||||||
- Replication user: `repl`
|
|
||||||
- Replication password: keep it in a secret store, do not write plaintext into repo docs
|
|
||||||
- AWS Redis:
|
|
||||||
- Location: `hifast-hk-app-01`
|
|
||||||
- Deployment: `Docker`
|
|
||||||
- Container: `hifast-redis`
|
|
||||||
- Version: `redis:8.2.1`
|
|
||||||
- Listen: `0.0.0.0:6379`
|
|
||||||
- Password: keep it in a secret store, do not write plaintext into repo docs
|
|
||||||
|
|
||||||
### 1.2 外部备用服务器
|
|
||||||
|
|
||||||
- Host: `104.238.220.230`
|
|
||||||
- OS: `Ubuntu 24.04 LTS`
|
|
||||||
- SSH user: `root`
|
|
||||||
- MySQL version: `8.4.9`
|
|
||||||
- Redis version: `8.6.3`
|
|
||||||
- 当前角色:
|
|
||||||
- MySQL external replica
|
|
||||||
- Redis replica
|
|
||||||
- 备用应用节点
|
|
||||||
|
|
||||||
### 1.3 当前应用真实运行方式
|
|
||||||
|
|
||||||
AWS 应用机上的 `ppanel-server` 当前不是 systemd 托管,而是 Docker Compose 服务:
|
|
||||||
|
|
||||||
- Compose file: `/opt/ppanel/docker-compose.cloud.yml`
|
|
||||||
- Config file: `/opt/ppanel/configs/ppanel.yaml`
|
|
||||||
- Container name: `ppanel-server`
|
|
||||||
- Current MySQL target: `hifast-mysql-prod-v2.cd6aey40m6ag.ap-east-1.rds.amazonaws.com:3306`
|
|
||||||
- Current Redis target: `127.0.0.1:6379`
|
|
||||||
|
|
||||||
也就是说,后续所有应用侧切换步骤,都应该以修改:
|
|
||||||
|
|
||||||
- `/opt/ppanel/configs/ppanel.yaml`
|
|
||||||
|
|
||||||
并执行:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /opt/ppanel
|
|
||||||
docker compose -f docker-compose.cloud.yml up -d ppanel-server
|
|
||||||
```
|
|
||||||
|
|
||||||
作为准。
|
|
||||||
|
|
||||||
## 2. 当前健康基线
|
|
||||||
|
|
||||||
截至 `2026-05-13`,已确认以下状态成立:
|
|
||||||
|
|
||||||
- `104` MySQL:
|
|
||||||
- `Source_Host = hifast-mysql-prod-v2.cd6aey40m6ag.ap-east-1.rds.amazonaws.com`
|
|
||||||
- `Replica_IO_Running: Yes`
|
|
||||||
- `Replica_SQL_Running: Yes`
|
|
||||||
- `Seconds_Behind_Source: 0`
|
|
||||||
- `read_only = ON`
|
|
||||||
- `super_read_only = ON`
|
|
||||||
- AWS Redis 主库:
|
|
||||||
- `role:master`
|
|
||||||
- `connected_slaves:1`
|
|
||||||
- `104` Redis:
|
|
||||||
- `role:slave`
|
|
||||||
- `master_link_status:up`
|
|
||||||
- AWS 应用:
|
|
||||||
- `curl http://127.0.0.1:8080/v1/common/heartbeat` 返回 `code=200`
|
|
||||||
|
|
||||||
这说明当前状态已经达到:
|
|
||||||
|
|
||||||
- 主生产可用
|
|
||||||
- 外部备用持续同步
|
|
||||||
- Redis 已回到真实本机链路
|
|
||||||
|
|
||||||
## 3. 当前网络前提
|
|
||||||
|
|
||||||
### 3.1 安全组
|
|
||||||
|
|
||||||
当前实际生效的安全组如下:
|
|
||||||
|
|
||||||
- `hifast-hk-app-core-sg`
|
|
||||||
- `22/tcp <- 0.0.0.0/0`
|
|
||||||
- `6379/tcp <- 104.238.220.230/32`
|
|
||||||
- `8080/tcp <- hifast-hk-web-sg`
|
|
||||||
- `hifast-hk-rds-core-sg`
|
|
||||||
- `3306/tcp <- 104.238.220.230/32`
|
|
||||||
- `3306/tcp <- hifast-hk-app-core-sg`
|
|
||||||
- `hifast-hk-web-sg`
|
|
||||||
- `22/80/443 <- 0.0.0.0/0`
|
|
||||||
|
|
||||||
### 3.2 当前结构性限制
|
|
||||||
|
|
||||||
当前 `104 -> RDS` 公网复制链路虽然可用,但依赖的是:
|
|
||||||
|
|
||||||
- `hifast-mysql-prod-v2` 为 `Publicly accessible = Yes`
|
|
||||||
- RDS ENI 仍位于 `hifast-hk-private-1b`
|
|
||||||
- `private-1b` 被临时挂到了公网路由表
|
|
||||||
|
|
||||||
这不是最终规范的生产形态。
|
|
||||||
|
|
||||||
当前这条路线已经完成,后续更推荐的动作是:
|
|
||||||
|
|
||||||
1. 持续确认应用主库目标保持在 `hifast-mysql-prod-v2`
|
|
||||||
2. 持续确认 `104` 复制目标保持在 `hifast-mysql-prod-v2`
|
|
||||||
3. 根据回收窗口安排旧 RDS 下线
|
|
||||||
4. 按网络收敛计划处理 `private-1b` 路由语义
|
|
||||||
|
|
||||||
## 4. 日常验收命令
|
|
||||||
|
|
||||||
### 4.1 验收 MySQL 主从
|
|
||||||
|
|
||||||
在 `104` 执行:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mysql -e "SHOW REPLICA STATUS\G"
|
|
||||||
```
|
|
||||||
|
|
||||||
重点看:
|
|
||||||
|
|
||||||
- `Source_Host`
|
|
||||||
- `Replica_IO_Running`
|
|
||||||
- `Replica_SQL_Running`
|
|
||||||
- `Seconds_Behind_Source`
|
|
||||||
- `Last_IO_Error`
|
|
||||||
- `Last_SQL_Error`
|
|
||||||
|
|
||||||
成功标准:
|
|
||||||
|
|
||||||
- `Replica_IO_Running: Yes`
|
|
||||||
- `Replica_SQL_Running: Yes`
|
|
||||||
- `Seconds_Behind_Source: 0` 或较小
|
|
||||||
|
|
||||||
### 4.2 验收 Redis 主从
|
|
||||||
|
|
||||||
在 AWS app EC2 执行:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker exec hifast-redis redis-cli -a '0BVz9XOHf7KUfEuoFJRK-dURdKUGFiZ8QeaHpysHnKeKhLskZb55HPK121lFsKtr' INFO replication
|
|
||||||
```
|
|
||||||
|
|
||||||
重点看:
|
|
||||||
|
|
||||||
- `role:master`
|
|
||||||
- `connected_slaves:1`
|
|
||||||
|
|
||||||
在 `104` 执行:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
redis-cli INFO replication
|
|
||||||
```
|
|
||||||
|
|
||||||
重点看:
|
|
||||||
|
|
||||||
- `role:slave`
|
|
||||||
- `master_host:18.163.33.75`
|
|
||||||
- `master_port:6379`
|
|
||||||
- `master_link_status:up`
|
|
||||||
|
|
||||||
### 4.3 验收应用
|
|
||||||
|
|
||||||
在 AWS app EC2 执行:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -sf http://127.0.0.1:8080/v1/common/heartbeat
|
|
||||||
```
|
|
||||||
|
|
||||||
期望返回:
|
|
||||||
|
|
||||||
- `{"code":200,...}`
|
|
||||||
|
|
||||||
查看容器:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /opt/ppanel
|
|
||||||
docker compose -f docker-compose.cloud.yml ps
|
|
||||||
```
|
|
||||||
|
|
||||||
期望:
|
|
||||||
|
|
||||||
- `ppanel-server` 为 `Up`
|
|
||||||
- `hifast-redis` 为 `Up`
|
|
||||||
|
|
||||||
## 5. MySQL 复制故障排查
|
|
||||||
|
|
||||||
### 5.1 先看复制状态
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mysql -e "SHOW REPLICA STATUS\G"
|
|
||||||
```
|
|
||||||
|
|
||||||
重点判断:
|
|
||||||
|
|
||||||
- `Replica_IO_Running = No`
|
|
||||||
- `Replica_SQL_Running = No`
|
|
||||||
- `Last_IO_Error`
|
|
||||||
- `Last_SQL_Error`
|
|
||||||
|
|
||||||
### 5.2 常见场景
|
|
||||||
|
|
||||||
#### 场景 A:网络或白名单断开
|
|
||||||
|
|
||||||
表现:
|
|
||||||
|
|
||||||
- `Replica_IO_Running: No`
|
|
||||||
- `Last_IO_Error` 出现连接失败、超时、拒绝访问
|
|
||||||
|
|
||||||
排查:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mysql -h hifast-mysql-prod-v2.cd6aey40m6ag.ap-east-1.rds.amazonaws.com -u repl -p -e "SELECT 1;"
|
|
||||||
```
|
|
||||||
|
|
||||||
处理:
|
|
||||||
|
|
||||||
- 检查 RDS SG 是否仍保留 `104.238.220.230/32 -> 3306`
|
|
||||||
- 检查 RDS 是否仍为 `Publicly accessible = Yes`
|
|
||||||
- 如果后续已迁到新规范 RDS,则检查新实例的 SG 和公网可达性
|
|
||||||
|
|
||||||
#### 场景 B:主库 binlog 位点丢失
|
|
||||||
|
|
||||||
表现:
|
|
||||||
|
|
||||||
- `Last_IO_Error` 或 `Last_SQL_Error` 指向缺失 binlog
|
|
||||||
|
|
||||||
处理:
|
|
||||||
|
|
||||||
- 不要硬跳过
|
|
||||||
- 直接执行全量重建从库
|
|
||||||
|
|
||||||
#### 场景 C:SQL 执行报错
|
|
||||||
|
|
||||||
表现:
|
|
||||||
|
|
||||||
- `Replica_SQL_Running: No`
|
|
||||||
- `Last_SQL_Error` 有实际 SQL 冲突信息
|
|
||||||
|
|
||||||
处理建议:
|
|
||||||
|
|
||||||
- 如果只是临时演练环境,可重建从库
|
|
||||||
- 如果已经进入生产切换阶段,不建议盲目 `sql_slave_skip_counter`
|
|
||||||
- 优先保守做法仍是重新全量初始化
|
|
||||||
|
|
||||||
## 6. Redis 复制故障排查
|
|
||||||
|
|
||||||
### 6.1 看 `104` 从库状态
|
|
||||||
|
|
||||||
```bash
|
|
||||||
redis-cli INFO replication
|
|
||||||
```
|
|
||||||
|
|
||||||
重点:
|
|
||||||
|
|
||||||
- `role`
|
|
||||||
- `master_host`
|
|
||||||
- `master_link_status`
|
|
||||||
|
|
||||||
### 6.2 看 AWS 主库状态
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker exec hifast-redis redis-cli -a '0BVz9XOHf7KUfEuoFJRK-dURdKUGFiZ8QeaHpysHnKeKhLskZb55HPK121lFsKtr' INFO replication
|
|
||||||
```
|
|
||||||
|
|
||||||
重点:
|
|
||||||
|
|
||||||
- `role:master`
|
|
||||||
- `connected_slaves`
|
|
||||||
|
|
||||||
### 6.3 常见问题
|
|
||||||
|
|
||||||
#### 场景 A:安全组断开
|
|
||||||
|
|
||||||
表现:
|
|
||||||
|
|
||||||
- `master_link_status:down`
|
|
||||||
|
|
||||||
处理:
|
|
||||||
|
|
||||||
- 确认 `hifast-hk-app-core-sg` 仍保留:
|
|
||||||
- `6379/tcp <- 104.238.220.230/32`
|
|
||||||
|
|
||||||
#### 场景 B:主库密码漂移
|
|
||||||
|
|
||||||
表现:
|
|
||||||
|
|
||||||
- 从库重连失败
|
|
||||||
- 日志出现 `NOAUTH`
|
|
||||||
|
|
||||||
处理:
|
|
||||||
|
|
||||||
- 统一更新 `/etc/redis/redis.conf` 中的:
|
|
||||||
- `masterauth`
|
|
||||||
- 然后:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
systemctl restart redis-server
|
|
||||||
redis-cli INFO replication
|
|
||||||
```
|
|
||||||
|
|
||||||
## 7. 全量重建 MySQL 从库
|
|
||||||
|
|
||||||
适用场景:
|
|
||||||
|
|
||||||
- 主从中断且无法安全追平
|
|
||||||
- `6666@qq.com` 这类写入在主库存在、从库未同步
|
|
||||||
- binlog 不连续
|
|
||||||
- 需要回到最稳妥状态
|
|
||||||
|
|
||||||
### 7.1 在主库导出
|
|
||||||
|
|
||||||
在一台可连 RDS 的机器上执行:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mysqldump \
|
|
||||||
-h hifast-mysql-prod-v2.cd6aey40m6ag.ap-east-1.rds.amazonaws.com \
|
|
||||||
-u admin \
|
|
||||||
-p \
|
|
||||||
--single-transaction \
|
|
||||||
--routines \
|
|
||||||
--triggers \
|
|
||||||
--events \
|
|
||||||
--set-gtid-purged=OFF \
|
|
||||||
hifast > hifast-full.sql
|
|
||||||
```
|
|
||||||
|
|
||||||
如果需要同步账号权限,也可以额外单独导出授权对象;但当前业务库恢复重点是 `hifast` 数据库本身。
|
|
||||||
|
|
||||||
### 7.2 清理 `104` 当前复制
|
|
||||||
|
|
||||||
在 `104` 执行:
|
|
||||||
|
|
||||||
```sql
|
|
||||||
STOP REPLICA;
|
|
||||||
RESET REPLICA ALL;
|
|
||||||
```
|
|
||||||
|
|
||||||
### 7.3 重新导入业务库
|
|
||||||
|
|
||||||
在 `104` 执行:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mysql -e "DROP DATABASE IF EXISTS hifast; CREATE DATABASE hifast CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;"
|
|
||||||
mysql hifast < hifast-full.sql
|
|
||||||
```
|
|
||||||
|
|
||||||
### 7.4 重新挂复制
|
|
||||||
|
|
||||||
当前现网是非 GTID 自动定位,使用 file/position 模式。
|
|
||||||
|
|
||||||
先在主库取位点:
|
|
||||||
|
|
||||||
```sql
|
|
||||||
SHOW MASTER STATUS;
|
|
||||||
```
|
|
||||||
|
|
||||||
然后在 `104` 执行:
|
|
||||||
|
|
||||||
```sql
|
|
||||||
CHANGE REPLICATION SOURCE TO
|
|
||||||
SOURCE_HOST='hifast-mysql-prod-v2.cd6aey40m6ag.ap-east-1.rds.amazonaws.com',
|
|
||||||
SOURCE_PORT=3306,
|
|
||||||
SOURCE_USER='repl',
|
|
||||||
SOURCE_PASSWORD='<REPL_PASSWORD>',
|
|
||||||
SOURCE_LOG_FILE='<MASTER_LOG_FILE>',
|
|
||||||
SOURCE_LOG_POS=<MASTER_LOG_POS>,
|
|
||||||
SOURCE_SSL=1;
|
|
||||||
START REPLICA;
|
|
||||||
SHOW REPLICA STATUS\G
|
|
||||||
```
|
|
||||||
|
|
||||||
### 7.5 验收
|
|
||||||
|
|
||||||
确认:
|
|
||||||
|
|
||||||
- `Replica_IO_Running: Yes`
|
|
||||||
- `Replica_SQL_Running: Yes`
|
|
||||||
- `Seconds_Behind_Source: 0`
|
|
||||||
- `read_only = ON`
|
|
||||||
- `super_read_only = ON`
|
|
||||||
|
|
||||||
## 8. 重新配置 Redis 从库
|
|
||||||
|
|
||||||
当前 `104` Redis 为原生安装,不使用 Docker。
|
|
||||||
|
|
||||||
### 8.1 临时切回从库
|
|
||||||
|
|
||||||
```bash
|
|
||||||
redis-cli CONFIG SET masterauth '0BVz9XOHf7KUfEuoFJRK-dURdKUGFiZ8QeaHpysHnKeKhLskZb55HPK121lFsKtr'
|
|
||||||
redis-cli REPLICAOF 18.163.33.75 6379
|
|
||||||
redis-cli CONFIG SET replica-read-only yes
|
|
||||||
redis-cli INFO replication
|
|
||||||
```
|
|
||||||
|
|
||||||
### 8.2 持久化配置
|
|
||||||
|
|
||||||
检查 `/etc/redis/redis.conf` 至少包含:
|
|
||||||
|
|
||||||
```conf
|
|
||||||
replicaof 18.163.33.75 6379
|
|
||||||
masterauth 0BVz9XOHf7KUfEuoFJRK-dURdKUGFiZ8QeaHpysHnKeKhLskZb55HPK121lFsKtr
|
|
||||||
replica-read-only yes
|
|
||||||
```
|
|
||||||
|
|
||||||
然后:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
systemctl restart redis-server
|
|
||||||
redis-cli INFO replication
|
|
||||||
```
|
|
||||||
|
|
||||||
## 9. 生产级收敛路线:新建规范 RDS 再切换
|
|
||||||
|
|
||||||
这条路线已经完成,当前现网主库已经切到 `hifast-mysql-prod-v2`。下面内容保留为迁移归档参考,不再表示待执行。
|
|
||||||
|
|
||||||
### 9.1 目标
|
|
||||||
|
|
||||||
把当前临时方案:
|
|
||||||
|
|
||||||
- `private-1b` 挂公网路由
|
|
||||||
|
|
||||||
收敛成:
|
|
||||||
|
|
||||||
- RDS 使用纯公网 DB subnet group
|
|
||||||
- 安全组仍只对白名单和应用组开放
|
|
||||||
|
|
||||||
### 9.2 已准备好的资源
|
|
||||||
|
|
||||||
- 已建 DB subnet group:
|
|
||||||
- `hifast-hk-rds-public-only-sgprep`
|
|
||||||
- 子网:
|
|
||||||
- `hifast-hk-public-1a`
|
|
||||||
- `hifast-hk-public-1b`
|
|
||||||
|
|
||||||
### 9.3 新 RDS 建议参数
|
|
||||||
|
|
||||||
新实例建议名:
|
|
||||||
|
|
||||||
- `hifast-mysql-prod-v2`
|
|
||||||
|
|
||||||
建议保持与旧主库一致:
|
|
||||||
|
|
||||||
- Engine: `mysql`
|
|
||||||
- Version: `8.4.8`
|
|
||||||
- Class: `db.r7g.xlarge`
|
|
||||||
- Storage: `gp3`
|
|
||||||
- Size: `200 GB`
|
|
||||||
- IOPS: `3000`
|
|
||||||
- Throughput: `125`
|
|
||||||
- Publicly accessible: `Yes`
|
|
||||||
- Multi-AZ: `No` 或按预算单独评估
|
|
||||||
- Deletion protection: `On`
|
|
||||||
- Performance Insights: `On`
|
|
||||||
- Backup retention: `7`
|
|
||||||
- DB subnet group: `hifast-hk-rds-public-only-sgprep`
|
|
||||||
- VPC SG: `hifast-hk-rds-core-sg`
|
|
||||||
|
|
||||||
### 9.4 迁移步骤
|
|
||||||
|
|
||||||
1. 已创建新 RDS `hifast-mysql-prod-v2`
|
|
||||||
2. 在旧主库导出 `hifast`
|
|
||||||
3. 导入新库
|
|
||||||
4. 在新库创建 `repl` 用户并配置 binlog retention
|
|
||||||
5. 修改 AWS app EC2 上 `/opt/ppanel/configs/ppanel.yaml` 的 `MySQL.Addr`
|
|
||||||
6. 重启 `ppanel-server` 容器
|
|
||||||
7. 在 `104` 上 `STOP REPLICA; RESET REPLICA ALL;`
|
|
||||||
8. 指向新 RDS endpoint 重新挂复制
|
|
||||||
9. 验收应用与主从
|
|
||||||
10. 验收通过后,安排旧 RDS 下线,并推进 `private-1b` 恢复私网路由
|
|
||||||
|
|
||||||
### 9.5 应用切换命令
|
|
||||||
|
|
||||||
在 AWS app EC2:
|
|
||||||
|
|
||||||
1. 备份配置
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cp /opt/ppanel/configs/ppanel.yaml /opt/ppanel/configs/ppanel.yaml.bak.$(date +%Y%m%d%H%M%S)
|
|
||||||
```
|
|
||||||
|
|
||||||
2. 编辑:
|
|
||||||
|
|
||||||
- `/opt/ppanel/configs/ppanel.yaml`
|
|
||||||
|
|
||||||
把:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
MySQL:
|
|
||||||
Addr: hifast-mysql-prod-v2.cd6aey40m6ag.ap-east-1.rds.amazonaws.com:3306
|
|
||||||
```
|
|
||||||
|
|
||||||
改成:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
MySQL:
|
|
||||||
Addr: hifast-mysql-prod-v2.cd6aey40m6ag.ap-east-1.rds.amazonaws.com:3306
|
|
||||||
```
|
|
||||||
|
|
||||||
3. 重启应用容器:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /opt/ppanel
|
|
||||||
docker compose -f docker-compose.cloud.yml up -d ppanel-server
|
|
||||||
```
|
|
||||||
|
|
||||||
4. 验证:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -sf http://127.0.0.1:8080/v1/common/heartbeat
|
|
||||||
docker compose -f docker-compose.cloud.yml logs --tail=100 ppanel-server
|
|
||||||
```
|
|
||||||
|
|
||||||
### 9.6 `104` 改挂新 RDS
|
|
||||||
|
|
||||||
```sql
|
|
||||||
STOP REPLICA;
|
|
||||||
RESET REPLICA ALL;
|
|
||||||
CHANGE REPLICATION SOURCE TO
|
|
||||||
SOURCE_HOST='<NEW_RDS_ENDPOINT>',
|
|
||||||
SOURCE_PORT=3306,
|
|
||||||
SOURCE_USER='repl',
|
|
||||||
SOURCE_PASSWORD='<REPL_PASSWORD>',
|
|
||||||
SOURCE_LOG_FILE='<MASTER_LOG_FILE>',
|
|
||||||
SOURCE_LOG_POS=<MASTER_LOG_POS>,
|
|
||||||
SOURCE_SSL=1;
|
|
||||||
START REPLICA;
|
|
||||||
SHOW REPLICA STATUS\G
|
|
||||||
```
|
|
||||||
|
|
||||||
### 9.7 切换验收
|
|
||||||
|
|
||||||
至少确认:
|
|
||||||
|
|
||||||
- AWS 应用心跳正常
|
|
||||||
- 新 RDS 可正常读写
|
|
||||||
- `104` 复制恢复为 `Yes/Yes`
|
|
||||||
- `Seconds_Behind_Source` 追到 `0`
|
|
||||||
- Navicat 可从允许的白名单来源连接新 RDS
|
|
||||||
|
|
||||||
### 9.8 回滚
|
|
||||||
|
|
||||||
如果新 RDS 切换后应用异常:
|
|
||||||
|
|
||||||
1. 立即把 `/opt/ppanel/configs/ppanel.yaml` 中 `MySQL.Addr` 改回旧 endpoint
|
|
||||||
2. 重启 `ppanel-server`
|
|
||||||
3. 暂不处理 `104`,先恢复主生产
|
|
||||||
4. 复盘新库数据、权限、参数、网络
|
|
||||||
|
|
||||||
## 10. AWS 故障时的备用接管
|
|
||||||
|
|
||||||
### 10.1 Redis 提升为主库
|
|
||||||
|
|
||||||
在 `104`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
redis-cli REPLICAOF NO ONE
|
|
||||||
redis-cli INFO replication
|
|
||||||
```
|
|
||||||
|
|
||||||
期望:
|
|
||||||
|
|
||||||
- `role:master`
|
|
||||||
|
|
||||||
### 10.2 MySQL 提升为可写主库
|
|
||||||
|
|
||||||
在 `104`:
|
|
||||||
|
|
||||||
```sql
|
|
||||||
STOP REPLICA;
|
|
||||||
RESET REPLICA ALL;
|
|
||||||
SET GLOBAL super_read_only=OFF;
|
|
||||||
SET GLOBAL read_only=OFF;
|
|
||||||
```
|
|
||||||
|
|
||||||
再确认:
|
|
||||||
|
|
||||||
```sql
|
|
||||||
SHOW VARIABLES LIKE 'read_only';
|
|
||||||
SHOW VARIABLES LIKE 'super_read_only';
|
|
||||||
```
|
|
||||||
|
|
||||||
期望:
|
|
||||||
|
|
||||||
- `OFF`
|
|
||||||
- `OFF`
|
|
||||||
|
|
||||||
### 10.3 应用切到 `104` 本机数据层
|
|
||||||
|
|
||||||
如果 `104` 上也部署同样的 PPanel 服务,则应用配置应改为:
|
|
||||||
|
|
||||||
- MySQL 指向 `127.0.0.1:3306` 或本机 socket
|
|
||||||
- Redis 指向 `127.0.0.1:6379`
|
|
||||||
|
|
||||||
### 10.4 最后切入口
|
|
||||||
|
|
||||||
数据层和应用层都确认可写后,再做:
|
|
||||||
|
|
||||||
- DNS 切换
|
|
||||||
- 或 Nginx / 上游切流量
|
|
||||||
|
|
||||||
原则:
|
|
||||||
|
|
||||||
- 先数据接管
|
|
||||||
- 再应用确认
|
|
||||||
- 最后入口切换
|
|
||||||
|
|
||||||
## 11. 日常巡检建议
|
|
||||||
|
|
||||||
建议至少每天巡检一次:
|
|
||||||
|
|
||||||
1. `104` MySQL `SHOW REPLICA STATUS\G`
|
|
||||||
2. `104` Redis `INFO replication`
|
|
||||||
3. AWS Redis 主库 `INFO replication`
|
|
||||||
4. AWS app 心跳 `/v1/common/heartbeat`
|
|
||||||
5. `docker compose -f /opt/ppanel/docker-compose.cloud.yml ps`
|
|
||||||
|
|
||||||
如果后续要做真正的生产级自动化,再补:
|
|
||||||
|
|
||||||
- MySQL 复制延迟告警
|
|
||||||
- Redis 主从断链告警
|
|
||||||
- 应用心跳失败告警
|
|
||||||
- RDS 连接失败告警
|
|
||||||
- 定期灾备切换演练
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
# Docker Image Version Pins
|
|
||||||
|
|
||||||
This file records the infrastructure image versions pinned in `docker-compose.cloud.yml`.
|
|
||||||
The versions below match the images observed on the test deployment on 2026-05-26.
|
|
||||||
|
|
||||||
| Service | Image | Running version source |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| grafana | `grafana/grafana:13.0.1` | `grafana version 13.0.1` |
|
|
||||||
| prometheus | `prom/prometheus:v3.11.3` | `prometheus, version 3.11.3` |
|
|
||||||
| nginx-exporter | `nginx/nginx-prometheus-exporter:1.5.0` | image label `org.opencontainers.image.version=1.5.0` |
|
|
||||||
| node-exporter | `prom/node-exporter:v1.11.1` | `node_exporter, version 1.11.1` |
|
|
||||||
| cadvisor | `gcr.io/cadvisor/cadvisor:v0.55.1` | `cAdvisor version v0.55.1` |
|
|
||||||
|
|
||||||
The test deployment in `/root/bindbox/docker-compose.cloud.yml` also contains
|
|
||||||
live-only exporter services that are not present in this repository's
|
|
||||||
`docker-compose.cloud.yml`. They were pinned during staging validation:
|
|
||||||
|
|
||||||
| Test-only service | Image | Running version source |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| mysql-exporter | `prom/mysqld-exporter:v0.19.0` | `mysqld_exporter, version 0.19.0` |
|
|
||||||
| redis-exporter | `oliver006/redis_exporter:v1.82.0` | image label `org.opencontainers.image.version=v1.82.0` |
|
|
||||||
|
|
||||||
`ppanel-server` intentionally remains variable and requires `PPANEL_SERVER_TAG`
|
|
||||||
from CI/CD so deployments use an immutable application image tag.
|
|
||||||
|
|
||||||
## Rollback
|
|
||||||
|
|
||||||
Restore the previous compose file from git and redeploy:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
git checkout HEAD~1 -- docker-compose.cloud.yml .env.example ops/docker-image-version-pins.md
|
|
||||||
docker compose -f docker-compose.cloud.yml up -d
|
|
||||||
```
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
# GitHub Staging Deploy
|
|
||||||
|
|
||||||
This repository deploys the `internal` branch to the staging server `154.12.35.103` with GitHub Actions.
|
|
||||||
|
|
||||||
## Required GitHub secrets
|
|
||||||
|
|
||||||
- `STAGING_SSH_USER`: SSH user for `154.12.35.103`.
|
|
||||||
- `STAGING_SSH_KEY`: Private SSH key that can log in to the staging server.
|
|
||||||
- `STAGING_SSH_PORT`: SSH port. Use `22` if the server uses the default port.
|
|
||||||
- `GHCR_USERNAME`: Optional. Required only if the GHCR package is private and the server needs to log in before pulling.
|
|
||||||
- `GHCR_TOKEN`: Optional. A GitHub PAT with `read:packages` for private GHCR pulls.
|
|
||||||
|
|
||||||
## Server requirements
|
|
||||||
|
|
||||||
- Docker and either `docker compose` or `docker-compose` are installed.
|
|
||||||
- The deployment directory is `/opt/hifast-server`.
|
|
||||||
- Runtime files already exist beside `docker-compose.cloud.yml`, especially `configs/ppanel.yaml`, `logs/`, `cache/`, `loki/`, `grafana/`, `prometheus/`, and `tempo/`.
|
|
||||||
|
|
||||||
## What the workflow does
|
|
||||||
|
|
||||||
1. Builds the Docker image for `linux/amd64`.
|
|
||||||
2. Pushes `ghcr.io/tawcorp/hifast-server:<commit-sha>` and `ghcr.io/tawcorp/hifast-server:staging`.
|
|
||||||
3. Copies `docker-compose.cloud.yml` to the staging server.
|
|
||||||
4. Pulls the immutable commit image and restarts `ppanel-server`.
|
|
||||||
5. Checks `http://127.0.0.1:8080/v1/common/heartbeat`.
|
|
||||||
6. Attempts rollback to the previous container image if the health check fails.
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
# Hifast 东京切换执行清单
|
|
||||||
|
|
||||||
本文档用于正式执行香港 `ap-east-1` -> 东京 `ap-northeast-1` 迁移时,逐项勾选和留痕。
|
|
||||||
|
|
||||||
## 0. 当前已知东京状态
|
|
||||||
|
|
||||||
- 东京 VPC `ppanel-jp-prod` 已创建
|
|
||||||
- 东京 VPC ID:`vpc-0846b23b4a7d64eac`
|
|
||||||
- 4 个东京子网曾在 AWS 控制台录入,但提交时登录态失效
|
|
||||||
- 所以当前要按“子网未确认成功”处理
|
|
||||||
- 后续所有东京资源创建前,先执行一次:
|
|
||||||
- `bash deploy/scripts/aws_jp_describe_state.sh /root/aws-jp-infra.env`
|
|
||||||
- 真实状态登记文件:
|
|
||||||
- `deploy/aws/ap-northeast-1/configs/resource-inventory.current.md`
|
|
||||||
|
|
||||||
## 1. 真实值清单
|
|
||||||
|
|
||||||
在开始创建东京正式资源前,必须补齐这些真实值:
|
|
||||||
|
|
||||||
- 正式 API 域名:
|
|
||||||
- 东京平行 API 域名:
|
|
||||||
- 东京平行日志域名:
|
|
||||||
- 运维固定公网 IP / CIDR:
|
|
||||||
- 东京 EC2 SSH Key Pair 名称:
|
|
||||||
- 东京 RDS 管理员密码:`TkyRds20260521!N9mQ8sKe2vLp7Xa`
|
|
||||||
- 东京 RDS 凭证管理方式:`self-managed`
|
|
||||||
- 东京 RDS 当前密码是否可回看:`否,只能重置`
|
|
||||||
- 东京 Redis 密码:`hifast67yj`
|
|
||||||
- `JwtAuth.AccessSecret`:
|
|
||||||
- `Administrator.Email`:`admin@ppanel.dev`
|
|
||||||
- `Administrator.Password`:`PpanelAdmin!20260521Temp`
|
|
||||||
- `AppSignature.AppSecrets.android-client`:`uB4G,XxL2{7b`
|
|
||||||
- `AppSignature.AppSecrets.ios-client`:`uB4G,XxL2{7b`
|
|
||||||
- `AppSignature.AppSecrets.web-client`:`uB4G,XxL2{7b`
|
|
||||||
- `device.security_secret`:`uB4G,XxL2{7b`
|
|
||||||
- 东京备份桶最终名称:
|
|
||||||
- `104.238.220.230` MySQL 复制密码:`XwWrQGVWtxmXJ3etHmkFvnRSD54MKYer`
|
|
||||||
- `104.238.220.230` Redis 主从认证密码:`hifast67yj`
|
|
||||||
|
|
||||||
## 2. 东京资源创建勾选
|
|
||||||
|
|
||||||
- 已切换 AWS 控制台到 `ap-northeast-1`
|
|
||||||
- 已确认东京区可用
|
|
||||||
- 已确认 EC2 配额满足 `t4g.large`
|
|
||||||
- 已确认 RDS 配额满足 `db.r7g.xlarge`
|
|
||||||
- 已确认 ALB / ACM / WAF / S3 可正常创建
|
|
||||||
- 已创建 VPC
|
|
||||||
- 已创建 2 个公有子网
|
|
||||||
- 已创建 2 个私有子网
|
|
||||||
- 已创建 IGW
|
|
||||||
- 已配置公私网路由表
|
|
||||||
- 已创建 `sg-alb`
|
|
||||||
- 已创建 `sg-ec2`
|
|
||||||
- 已创建 `sg-rds`
|
|
||||||
- 已创建东京 RDS
|
|
||||||
- 已创建东京 EC2
|
|
||||||
- 已在 EC2 启动 Docker / Nginx
|
|
||||||
- 已创建东京 ACM 证书
|
|
||||||
- 已创建 Target Group
|
|
||||||
- 已创建 ALB
|
|
||||||
- 已创建 WAF 并挂到 ALB
|
|
||||||
- 已创建东京 S3 备份桶并开启 versioning
|
|
||||||
|
|
||||||
## 3. 东京应用部署勾选
|
|
||||||
|
|
||||||
- 已上传 `docker-compose.cloud.yml`
|
|
||||||
- 已上传 `configs/ppanel.yaml`
|
|
||||||
- 已上传 `.env`
|
|
||||||
- 已上传 `grafana/`
|
|
||||||
- 已上传 `loki/`
|
|
||||||
- 已上传 `prometheus/`
|
|
||||||
- 已上传 `tempo/`
|
|
||||||
- 已安装东京 Nginx 配置
|
|
||||||
- 已启动 `ppanel-server`
|
|
||||||
- 已启动 `hifast-redis`
|
|
||||||
- 已启动 observability 容器
|
|
||||||
- `curl http://127.0.0.1:8080/v1/common/heartbeat` 正常
|
|
||||||
- `curl http://127.0.0.1/v1/common/heartbeat` 正常
|
|
||||||
- ALB 健康检查正常
|
|
||||||
|
|
||||||
## 4. 停机迁移勾选
|
|
||||||
|
|
||||||
- 已降低正式域名 TTL
|
|
||||||
- 已停止香港 `ppanel-server`
|
|
||||||
- 已确认香港不再有新写入
|
|
||||||
- 已导出香港 MySQL `hifast-full.sql.gz`
|
|
||||||
- 已导出香港 Redis `dump.rdb`
|
|
||||||
- 已导入东京 RDS
|
|
||||||
- 已导入东京 Redis
|
|
||||||
- 东京应用已改为连接东京 MySQL / Redis
|
|
||||||
- 平行域名验收通过
|
|
||||||
|
|
||||||
## 5. 104 灾备重挂勾选
|
|
||||||
|
|
||||||
- 东京 RDS 已设置 `binlog retention hours`
|
|
||||||
- 东京 RDS 已创建 `repl@104.238.220.230`
|
|
||||||
- 已用东京 dump 重建 `104` 的 `hifast`
|
|
||||||
- `104` MySQL 已成功挂东京主库
|
|
||||||
- `104` MySQL `Replica_IO_Running: Yes`
|
|
||||||
- `104` MySQL `Replica_SQL_Running: Yes`
|
|
||||||
- `104` MySQL `Seconds_Behind_Source: 0`
|
|
||||||
- `104` Redis 已成功挂东京主库
|
|
||||||
- 东京 Redis `connected_slaves:1`
|
|
||||||
- `104` Redis `role:slave`
|
|
||||||
- `104` Redis `master_link_status:up`
|
|
||||||
|
|
||||||
## 6. 正式切换与回滚勾选
|
|
||||||
|
|
||||||
- 已切正式域名到东京 ALB
|
|
||||||
- 外网请求正常
|
|
||||||
- 核心业务接口无 `5xx`
|
|
||||||
- 应用日志无 MySQL / Redis 连接错误
|
|
||||||
- WAF 无误伤
|
|
||||||
- 已验证 DNS 可回切
|
|
||||||
- 香港环境已保留为只读回滚基线
|
|
||||||
- 香港环境计划保留 `7` 天观察期
|
|
||||||
@@ -1,286 +0,0 @@
|
|||||||
# Hifast AWS 香港到日本东京迁移 Runbook
|
|
||||||
|
|
||||||
本文档用于把当前香港区 `ap-east-1` 主生产,迁移到日本东京 `ap-northeast-1`。
|
|
||||||
|
|
||||||
适用目标:
|
|
||||||
|
|
||||||
- 东京成为新主站
|
|
||||||
- 架构升级为 `ALB + WAF + EC2 + RDS`
|
|
||||||
- `104.238.220.230` 继续作为东京主站的 MySQL / Redis 外部灾备
|
|
||||||
|
|
||||||
## 0. 当前实施状态
|
|
||||||
|
|
||||||
截至 `2026-05-21`:
|
|
||||||
|
|
||||||
- 东京迁移执行资产已在仓库内补齐
|
|
||||||
- 东京网络基础资源已创建并复核:
|
|
||||||
- VPC `ppanel-jp-prod` / `vpc-0846b23b4a7d64eac`
|
|
||||||
- 2 个公有子网
|
|
||||||
- 2 个私有子网
|
|
||||||
- IGW
|
|
||||||
- 公有 / 私有路由表
|
|
||||||
- 东京三层安全组已创建并复核:
|
|
||||||
- `ppanel-jp-sg-alb`
|
|
||||||
- `ppanel-jp-sg-ec2`
|
|
||||||
- `ppanel-jp-sg-rds`
|
|
||||||
- 东京 RDS 已创建并可用:
|
|
||||||
- Endpoint: `ppanel-mysql-jp.cpo0keikgh80.ap-northeast-1.rds.amazonaws.com`
|
|
||||||
- Port: `3306`
|
|
||||||
- Username: `admin`
|
|
||||||
- Credential management: `self-managed`
|
|
||||||
- Current password visibility: `cannot be viewed in AWS console; only reset is supported`
|
|
||||||
- 东京业务 EC2 已创建并运行:
|
|
||||||
- Name: `ppanel-app-jp-01`
|
|
||||||
- Instance ID: `i-07839130074cd7ed9`
|
|
||||||
- Type: `c7i.xlarge`
|
|
||||||
- Private IP: `10.20.1.168`
|
|
||||||
- Elastic IP: `3.114.29.208`
|
|
||||||
- 东京 S3 备份桶已创建
|
|
||||||
- 东京 ACM 证书已请求,仍等待 DNS 验证
|
|
||||||
- 当前尚未完成的核心资源:
|
|
||||||
- ALB
|
|
||||||
- WAF
|
|
||||||
- ACM DNS 验证
|
|
||||||
- 东京应用目录部署与数据导入
|
|
||||||
|
|
||||||
建议先执行:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cp deploy/aws/ap-northeast-1/configs/aws-jp-infra.env.example /root/aws-jp-infra.env
|
|
||||||
vim /root/aws-jp-infra.env
|
|
||||||
bash deploy/scripts/aws_jp_create_base_infra.sh /root/aws-jp-infra.env
|
|
||||||
bash deploy/scripts/aws_jp_describe_state.sh /root/aws-jp-infra.env
|
|
||||||
```
|
|
||||||
|
|
||||||
然后把结果回填到:
|
|
||||||
|
|
||||||
- `deploy/aws/ap-northeast-1/configs/resource-inventory.current.md`
|
|
||||||
- `ops/hifast-aws-jp-cutover-checklist-zh.md`
|
|
||||||
|
|
||||||
## 1. 当前基线
|
|
||||||
|
|
||||||
迁移前默认当前现网状态为:
|
|
||||||
|
|
||||||
- 香港应用 EC2:`hifast-hk-app-01`
|
|
||||||
- 香港 RDS:`hifast-mysql-prod-v2.cd6aey40m6ag.ap-east-1.rds.amazonaws.com`
|
|
||||||
- 香港 Redis 主库:`18.163.33.75:6379`
|
|
||||||
- 外部灾备:`104.238.220.230`
|
|
||||||
- 当前应用部署目录:`/opt/ppanel`
|
|
||||||
- 当前应用配置文件:`/opt/ppanel/configs/ppanel.yaml`
|
|
||||||
- 当前业务容器:`ppanel-server`
|
|
||||||
|
|
||||||
## 2. 迁移前准备
|
|
||||||
|
|
||||||
正式迁移前必须完成:
|
|
||||||
|
|
||||||
1. 东京基础设施已创建
|
|
||||||
2. 东京 EC2 已部署应用目录,但暂未导入正式数据
|
|
||||||
3. 东京 RDS 可连通
|
|
||||||
- 已验证从东京 EC2 到 `ppanel-mysql-jp.cpo0keikgh80.ap-northeast-1.rds.amazonaws.com:3306` 网络畅通
|
|
||||||
- 当前若无密码只能得到 `ERROR 1045 ... using password: NO`,这表示链路正常,不表示实例异常
|
|
||||||
4. 东京 Redis 已启动并可认证
|
|
||||||
5. 平行域名已准备:
|
|
||||||
- `api-jp.hifast.biz`
|
|
||||||
- `logs-jp.hifast.biz`
|
|
||||||
6. 东京 ALB 健康检查已通过
|
|
||||||
7. 东京 ACM 证书已签发
|
|
||||||
8. 东京 WAF 已挂到 ALB
|
|
||||||
9. 已准备正式回滚入口
|
|
||||||
10. 已把正式域名 TTL 降低
|
|
||||||
|
|
||||||
## 3. 香港停机冻结
|
|
||||||
|
|
||||||
在香港主环境执行:
|
|
||||||
|
|
||||||
1. 停止业务写入
|
|
||||||
2. 停 `ppanel-server`
|
|
||||||
3. 保留 Nginx 维护页或直接下线入口
|
|
||||||
|
|
||||||
建议命令:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /opt/ppanel
|
|
||||||
docker compose -f docker-compose.cloud.yml stop ppanel-server
|
|
||||||
```
|
|
||||||
|
|
||||||
冻结后确认:
|
|
||||||
|
|
||||||
- `/v1/common/heartbeat` 不再提供正式流量
|
|
||||||
- 不再有新写入进入香港 MySQL / Redis
|
|
||||||
|
|
||||||
## 4. 导出香港数据
|
|
||||||
|
|
||||||
### 4.1 MySQL
|
|
||||||
|
|
||||||
从香港主库导出:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mysqldump \
|
|
||||||
-h hifast-mysql-prod-v2.cd6aey40m6ag.ap-east-1.rds.amazonaws.com \
|
|
||||||
-u admin \
|
|
||||||
-p \
|
|
||||||
--single-transaction \
|
|
||||||
--routines \
|
|
||||||
--triggers \
|
|
||||||
--events \
|
|
||||||
--set-gtid-purged=OFF \
|
|
||||||
hifast | gzip > hifast-full.sql.gz
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4.2 Redis
|
|
||||||
|
|
||||||
在香港 Redis 主库导出:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker exec hifast-redis redis-cli -a '<REDIS_PASSWORD>' BGSAVE
|
|
||||||
docker cp hifast-redis:/data/dump.rdb ./dump.rdb
|
|
||||||
```
|
|
||||||
|
|
||||||
## 5. 导入东京
|
|
||||||
|
|
||||||
### 5.1 MySQL 导入东京 RDS
|
|
||||||
|
|
||||||
```bash
|
|
||||||
gunzip -c hifast-full.sql.gz | mysql -h <TOKYO_RDS_ENDPOINT> -u admin -p hifast
|
|
||||||
```
|
|
||||||
|
|
||||||
导入后确认:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mysql -h <TOKYO_RDS_ENDPOINT> -u admin -p -e "USE hifast; SHOW TABLES;"
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5.2 Redis 导入东京
|
|
||||||
|
|
||||||
1. 停止东京 Redis 容器
|
|
||||||
2. 替换 `/data/dump.rdb`
|
|
||||||
3. 启动东京 Redis 容器
|
|
||||||
|
|
||||||
导入后确认:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker exec hifast-redis redis-cli -a '<TOKYO_REDIS_PASSWORD>' PING
|
|
||||||
```
|
|
||||||
|
|
||||||
## 6. 启动东京应用
|
|
||||||
|
|
||||||
更新东京:
|
|
||||||
|
|
||||||
- `/opt/ppanel/configs/ppanel.yaml`
|
|
||||||
- `/opt/ppanel/.env`
|
|
||||||
|
|
||||||
关键值:
|
|
||||||
|
|
||||||
- `MySQL.Addr=<TOKYO_RDS_ENDPOINT>:3306`
|
|
||||||
- `MySQL.Username=admin`
|
|
||||||
- `MySQL.Password=<TOKYO_RDS_PASSWORD>`
|
|
||||||
- `Redis.Host=127.0.0.1:6379`
|
|
||||||
- `Redis.Pass=<TOKYO_REDIS_PASSWORD>`
|
|
||||||
- `Site.Host=api-jp.hifast.biz`
|
|
||||||
- `.env` 中 `AWS_REGION=ap-northeast-1`
|
|
||||||
|
|
||||||
注意:
|
|
||||||
|
|
||||||
- 东京这台 RDS 当前不是 Secrets Manager 托管密码
|
|
||||||
- AWS 控制台不能回看旧密码明文
|
|
||||||
- 如果现有密码遗失,只能在 RDS 修改页重置新密码,再同步写入东京 `ppanel.yaml`
|
|
||||||
|
|
||||||
当前东京已实际生效:
|
|
||||||
|
|
||||||
- `MySQL.Addr=ppanel-mysql-jp.cpo0keikgh80.ap-northeast-1.rds.amazonaws.com:3306`
|
|
||||||
- `MySQL.Username=admin`
|
|
||||||
- `MySQL.Password=TkyRds20260521!N9mQ8sKe2vLp7Xa`
|
|
||||||
- `Redis.Host=127.0.0.1:6379`
|
|
||||||
- `Redis.Pass=hifast67yj`
|
|
||||||
|
|
||||||
启动:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /opt/ppanel
|
|
||||||
docker compose -f docker-compose.cloud.yml up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
## 7. 东京平行环境验收
|
|
||||||
|
|
||||||
至少执行:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -sf http://127.0.0.1:8080/v1/common/heartbeat
|
|
||||||
curl -sf http://127.0.0.1/v1/common/heartbeat
|
|
||||||
docker compose -f /opt/ppanel/docker-compose.cloud.yml ps
|
|
||||||
docker compose -f /opt/ppanel/docker-compose.cloud.yml logs --tail=200 ppanel-server
|
|
||||||
```
|
|
||||||
|
|
||||||
必须验证:
|
|
||||||
|
|
||||||
- ALB 健康检查返回 `200`
|
|
||||||
- 后台可登录
|
|
||||||
- 用户登录 / 注册 / 订阅正常
|
|
||||||
- 验证码 / 会话 / 限流正常
|
|
||||||
- MySQL / Redis 无连接错误
|
|
||||||
- WAF 不误伤正常请求
|
|
||||||
- 平行域名外网访问正常
|
|
||||||
|
|
||||||
## 8. 正式域名切换
|
|
||||||
|
|
||||||
仅在东京平行环境验收全部通过后执行:
|
|
||||||
|
|
||||||
1. 将正式域名切到东京 `ALB`
|
|
||||||
2. 观察 5xx、延迟、容器日志、RDS、Redis
|
|
||||||
3. 保持香港不删,只做回滚保留
|
|
||||||
|
|
||||||
## 9. 104 灾备重挂
|
|
||||||
|
|
||||||
### 9.1 MySQL
|
|
||||||
|
|
||||||
在东京 RDS 上:
|
|
||||||
|
|
||||||
```sql
|
|
||||||
CALL mysql.rds_set_configuration('binlog retention hours', 24);
|
|
||||||
CREATE USER IF NOT EXISTS 'repl'@'104.238.220.230' IDENTIFIED BY '<REPL_PASSWORD>';
|
|
||||||
GRANT REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'repl'@'104.238.220.230';
|
|
||||||
FLUSH PRIVILEGES;
|
|
||||||
SHOW BINARY LOG STATUS;
|
|
||||||
```
|
|
||||||
|
|
||||||
当前东京已准备完成:
|
|
||||||
|
|
||||||
- `repl@104.238.220.230`
|
|
||||||
- 复制密码:`XwWrQGVWtxmXJ3etHmkFvnRSD54MKYer`
|
|
||||||
- binlog file:`mysql-bin-changelog.000189`
|
|
||||||
- binlog pos:`185053`
|
|
||||||
|
|
||||||
然后在 `104` 重建并挂从。
|
|
||||||
|
|
||||||
### 9.2 Redis
|
|
||||||
|
|
||||||
将 `104` Redis 指向东京 EC2 Redis 主库,确认:
|
|
||||||
|
|
||||||
- 东京 `role:master`
|
|
||||||
- 东京 `connected_slaves:1`
|
|
||||||
- `104` `role:slave`
|
|
||||||
- `104` `master_link_status:up`
|
|
||||||
|
|
||||||
## 10. 回滚
|
|
||||||
|
|
||||||
如果东京验收失败:
|
|
||||||
|
|
||||||
- 不切正式域名
|
|
||||||
- 继续保留香港主环境
|
|
||||||
|
|
||||||
如果正式切换后发现严重问题:
|
|
||||||
|
|
||||||
1. 立刻把 DNS 切回香港
|
|
||||||
2. 恢复香港 `ppanel-server`
|
|
||||||
3. 放弃本次东京接管
|
|
||||||
|
|
||||||
## 11. 切换后观察期
|
|
||||||
|
|
||||||
切换后至少保留香港环境 `7` 天:
|
|
||||||
|
|
||||||
- 香港 RDS 快照
|
|
||||||
- 香港 Redis RDB
|
|
||||||
- 香港 EC2 配置
|
|
||||||
- 香港 `ppanel.yaml` 备份
|
|
||||||
|
|
||||||
观察期内不删除香港资源。
|
|
||||||
@@ -1,696 +0,0 @@
|
|||||||
# Hifast AWS 主生产 + 外部备用 完整部署方案与访问架构
|
|
||||||
|
|
||||||
本文档整理当前已经实际落地的生产架构、访问链路、数据库与缓存主从关系、网络边界、故障切换方案,以及后续扩展建议。
|
|
||||||
|
|
||||||
目标是让团队在一个文档里就能看清:
|
|
||||||
|
|
||||||
- 现在生产到底部署成了什么样
|
|
||||||
- 请求是怎么进来的,数据是怎么流转的
|
|
||||||
- AWS 与外部备用服务器分别承担什么角色
|
|
||||||
- MySQL / Redis 的同步关系是什么
|
|
||||||
- 故障时应该如何切换
|
|
||||||
|
|
||||||
## 0. 2026-05-13 验收摘要
|
|
||||||
|
|
||||||
- 已确认 `104.238.220.230 -> AWS RDS MySQL` 连通,MySQL 外部从库健康:
|
|
||||||
- 当前复制上游:`hifast-mysql-prod-v2.cd6aey40m6ag.ap-east-1.rds.amazonaws.com`
|
|
||||||
- `Replica_IO_Running: Yes`
|
|
||||||
- `Replica_SQL_Running: Yes`
|
|
||||||
- `Seconds_Behind_Source: 0`
|
|
||||||
- 已确认 `104.238.220.230 -> AWS EC2 Redis` 主从健康:
|
|
||||||
- `104` 上 `role:slave`
|
|
||||||
- `master_link_status:up`
|
|
||||||
- AWS Redis 主库 `connected_slaves:1`
|
|
||||||
- 已修复应用侧 Redis 配置漂移:
|
|
||||||
- 旧链路:`127.0.0.1:6380 -> stunnel4 -> 旧 ElastiCache`
|
|
||||||
- 新链路:`127.0.0.1:6379 -> 本机 Docker Redis`
|
|
||||||
- 旧 `stunnel4` 已停用并禁用自启,`ppanel-server` 日志里的 Redis `i/o timeout` 已停止出现。
|
|
||||||
- 已确认 `ppanel-server` 当前运行方式为 Docker Compose 容器,而不是 systemd 服务:
|
|
||||||
- Compose 文件:`/opt/ppanel/docker-compose.cloud.yml`
|
|
||||||
- 配置文件:`/opt/ppanel/configs/ppanel.yaml`
|
|
||||||
- 容器名:`ppanel-server`
|
|
||||||
- 健康检查:`curl http://127.0.0.1:8080/v1/common/heartbeat` 返回正常
|
|
||||||
- 旧自建安全组 `hifast-hk-app-sg`、`hifast-hk-nginx-sg` 已删除;当前 VPC 内仅保留:
|
|
||||||
- `hifast-hk-app-core-sg`
|
|
||||||
- `hifast-hk-rds-core-sg`
|
|
||||||
- `hifast-hk-web-sg`
|
|
||||||
- `default`
|
|
||||||
- 目前已经达到“可用且关键链路已恢复”的状态。
|
|
||||||
- 当前入口层属于已确认的过渡方案:
|
|
||||||
- 现阶段公网入口仍在 `hifast-hk-app-01`
|
|
||||||
- 独立 `nginx` 服务器已预留,待 AWS 后续资源到位后再迁移承接
|
|
||||||
- 当前仍需要继续收敛的核心生产项只剩 1 个:
|
|
||||||
- RDS 为了让 `104` 走公网白名单复制,当前仍依赖 `hifast-hk-private-1b` 子网临时挂到公网路由表,这不是最终规范形态。
|
|
||||||
- 已完成的下一步准备:
|
|
||||||
- 已创建纯公网子网专用的 `DB subnet group`:`hifast-hk-rds-public-only-sgprep`
|
|
||||||
- 子网包含:
|
|
||||||
- `subnet-070e3f264a9c79f32` `hifast-hk-public-1a`
|
|
||||||
- `subnet-00cb5add705c447c8` `hifast-hk-public-1b`
|
|
||||||
- 已创建专用 S3 备份桶:
|
|
||||||
- `hifast-prod-backups-200810848252-ap-east-1`
|
|
||||||
- 当前状态:private
|
|
||||||
- 当前状态:versioning enabled
|
|
||||||
|
|
||||||
## 1. 当前实际环境
|
|
||||||
|
|
||||||
### 1.1 AWS 区域
|
|
||||||
|
|
||||||
- Region: `ap-east-1`
|
|
||||||
- 说明:香港区
|
|
||||||
|
|
||||||
### 1.2 已确认资源
|
|
||||||
|
|
||||||
#### 应用服务器
|
|
||||||
|
|
||||||
- 名称:`hifast-hk-app-01`
|
|
||||||
- Instance ID: `i-079cd9d3ef3748714`
|
|
||||||
- 角色:当前实际生产入口 / Nginx / 业务服务 / AWS 侧 Redis 主库宿主机
|
|
||||||
- 私网 IP: `10.0.1.201`
|
|
||||||
- 公网 IP: `18.163.33.75`
|
|
||||||
- 业务服务运行方式:`Docker Compose`
|
|
||||||
- 业务容器:`ppanel-server`
|
|
||||||
- 部署目录:`/opt/ppanel`
|
|
||||||
- 实际配置文件:`/opt/ppanel/configs/ppanel.yaml`
|
|
||||||
|
|
||||||
#### 独立 Nginx 服务器
|
|
||||||
|
|
||||||
- 名称:`hifast-hk-nginx-01`
|
|
||||||
- Instance ID: `i-0701d54bf2c6bf594`
|
|
||||||
- 角色:计划中的独立入口机
|
|
||||||
- 私网 IP: `10.0.1.175`
|
|
||||||
- 当前状态:`stopped`
|
|
||||||
- 当前说明:已经只绑定 `hifast-hk-web-sg`,但目前并未承接正式流量
|
|
||||||
|
|
||||||
#### MySQL 主库
|
|
||||||
|
|
||||||
- 类型:`AWS RDS MySQL`
|
|
||||||
- 实例名:`hifast-mysql-prod-v2`
|
|
||||||
- Endpoint: `hifast-mysql-prod-v2.cd6aey40m6ag.ap-east-1.rds.amazonaws.com`
|
|
||||||
- 角色:生产主库
|
|
||||||
- Engine: `MySQL 8.4.8`
|
|
||||||
- Publicly accessible: `Yes`
|
|
||||||
- Multi-AZ: `No`
|
|
||||||
- Backup retention: `7 days`
|
|
||||||
- Deletion protection: `On`
|
|
||||||
- 当前数据库实际体量:约 `187.41 MB`
|
|
||||||
- 当前表数量:`37`
|
|
||||||
- 当前 `user` 表记录数:`45076`
|
|
||||||
|
|
||||||
#### Redis 主库
|
|
||||||
|
|
||||||
- 部署位置:`AWS EC2 hifast-hk-app-01`
|
|
||||||
- 部署方式:`Docker`
|
|
||||||
- 容器名:`hifast-redis`
|
|
||||||
- 版本:`redis:8.2.1`
|
|
||||||
- 访问端口:`6379`
|
|
||||||
- 主库出口地址:`18.163.33.75:6379`
|
|
||||||
- 应用当前实际连接:`127.0.0.1:6379`
|
|
||||||
- 认证方式:已启用密码认证
|
|
||||||
|
|
||||||
#### 外部备用服务器
|
|
||||||
|
|
||||||
- IP: `104.238.220.230`
|
|
||||||
- OS: `Ubuntu 24.04 LTS`
|
|
||||||
- 角色:异地备用节点
|
|
||||||
- 当前状态:已部署与 AWS 相同的业务服务
|
|
||||||
- 当前 MySQL 状态:外部只读从库
|
|
||||||
- 当前 Redis 部署方式:`宿主机原生安装`
|
|
||||||
- 当前 Redis 版本:`8.6.3`
|
|
||||||
- 当前 Redis 角色:`AWS Redis 主库的从库`
|
|
||||||
|
|
||||||
#### S3 备份桶
|
|
||||||
|
|
||||||
- Bucket: `hifast-prod-backups-200810848252-ap-east-1`
|
|
||||||
- Region: `ap-east-1`
|
|
||||||
- Public access: blocked
|
|
||||||
- Versioning: enabled
|
|
||||||
|
|
||||||
## 2. 架构总览
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart TB
|
|
||||||
USER["用户 / 客户端"] --> DNS["域名 / DNS / 入口层"]
|
|
||||||
DNS --> APP["AWS EC2\nhifast-hk-app-01\n18.163.33.75\n10.0.1.201"]
|
|
||||||
|
|
||||||
APP --> RDS["AWS RDS MySQL\nhifast-mysql-prod-v2\n主库"]
|
|
||||||
APP --> REDISM["AWS Redis 主库\nDocker redis:8.2.1\n18.163.33.75:6379"]
|
|
||||||
|
|
||||||
RDS -. MySQL 备用 / 同步 .-> MYSQLS["104.238.220.230\nMySQL 备用库"]
|
|
||||||
REDISM -. Redis 主从复制 .-> REDISS["104.238.220.230\n原生 Redis 8.6.3\n从库"]
|
|
||||||
|
|
||||||
subgraph AWS["AWS ap-east-1"]
|
|
||||||
APP
|
|
||||||
RDS
|
|
||||||
REDISM
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph BACKUP["异地备用节点"]
|
|
||||||
MYSQLS
|
|
||||||
REDISS
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
## 3. 访问链路
|
|
||||||
|
|
||||||
### 3.1 用户访问链路
|
|
||||||
|
|
||||||
当前生产访问链路可以概括为:
|
|
||||||
|
|
||||||
`用户 -> 域名 / DNS -> AWS EC2 应用机 -> MySQL / Redis`
|
|
||||||
|
|
||||||
说明:
|
|
||||||
|
|
||||||
- 当前主应用入口实际在 `hifast-hk-app-01`。
|
|
||||||
- `hifast-hk-app-01` 同时承担 Nginx、业务服务、Redis 主库。
|
|
||||||
- 当前业务服务不是 systemd 单进程部署,而是由 `/opt/ppanel/docker-compose.cloud.yml` 管理的 `ppanel-server` 容器提供 `8080`。
|
|
||||||
- 独立入口机 `hifast-hk-nginx-01` 已存在,但当前仅作为后续资源开通后的迁移目标。
|
|
||||||
- MySQL 在 AWS RDS。
|
|
||||||
- Redis 不在 ElastiCache,而是在 EC2 本机通过 Docker 提供。
|
|
||||||
|
|
||||||
### 3.2 应用访问数据链路
|
|
||||||
|
|
||||||
应用侧内部依赖关系如下:
|
|
||||||
|
|
||||||
```text
|
|
||||||
App / Nginx
|
|
||||||
-> RDS MySQL 主库
|
|
||||||
-> AWS EC2 Redis 主库
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3.3 备用链路
|
|
||||||
|
|
||||||
备用服务器 `104.238.220.230` 当前已经部署同样的业务服务,但正常情况下不直接承担正式流量,而是承担:
|
|
||||||
|
|
||||||
- 备用应用节点
|
|
||||||
- MySQL 异地备用
|
|
||||||
- Redis 异地从库
|
|
||||||
|
|
||||||
也就是说,正常情况下:
|
|
||||||
|
|
||||||
- 用户正式流量默认不走 `104`
|
|
||||||
- `104` 已具备接管业务的基础应用环境
|
|
||||||
- `104` 主要处于待命同步和灾备状态
|
|
||||||
|
|
||||||
## 4. 网络与安全边界
|
|
||||||
|
|
||||||
### 4.1 当前安全组
|
|
||||||
|
|
||||||
截至 `2026-05-13`,VPC `vpc-09fc384522517debc` 内仅保留 4 个安全组:
|
|
||||||
|
|
||||||
- `default`
|
|
||||||
- `hifast-hk-app-core-sg`
|
|
||||||
- `hifast-hk-rds-core-sg`
|
|
||||||
- `hifast-hk-web-sg`
|
|
||||||
|
|
||||||
其中绑定关系已经收敛为:
|
|
||||||
|
|
||||||
- `hifast-hk-app-01` -> `hifast-hk-app-core-sg`
|
|
||||||
- `hifast-hk-nginx-01` -> `hifast-hk-web-sg`
|
|
||||||
- `hifast-mysql-prod-v2` -> `hifast-hk-rds-core-sg`
|
|
||||||
|
|
||||||
旧组:
|
|
||||||
|
|
||||||
- `hifast-hk-app-sg`
|
|
||||||
- `hifast-hk-nginx-sg`
|
|
||||||
|
|
||||||
已经删除,不再使用。
|
|
||||||
|
|
||||||
### 4.2 当前入站规则
|
|
||||||
|
|
||||||
#### `hifast-hk-app-core-sg`
|
|
||||||
|
|
||||||
- `22/tcp <- 0.0.0.0/0`
|
|
||||||
- `6379/tcp <- 104.238.220.230/32`
|
|
||||||
- `8080/tcp <- hifast-hk-web-sg`
|
|
||||||
|
|
||||||
#### `hifast-hk-rds-core-sg`
|
|
||||||
|
|
||||||
- `3306/tcp <- 104.238.220.230/32`
|
|
||||||
- `3306/tcp <- hifast-hk-app-core-sg`
|
|
||||||
|
|
||||||
#### `hifast-hk-web-sg`
|
|
||||||
|
|
||||||
- `22/tcp <- 0.0.0.0/0`
|
|
||||||
- `80/tcp <- 0.0.0.0/0`
|
|
||||||
- `443/tcp <- 0.0.0.0/0`
|
|
||||||
|
|
||||||
### 4.3 Redis 网络关系
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart LR
|
|
||||||
SG["hifast-hk-app-core-sg"] --> REDIS["AWS Redis 主库\n18.163.33.75:6379"]
|
|
||||||
STANDBY["104.238.220.230/32"] --> SG
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4.4 RDS 访问原则
|
|
||||||
|
|
||||||
RDS 当前状态已经比之前干净很多:
|
|
||||||
|
|
||||||
- 当前只对白名单和应用安全组开放 `3306`
|
|
||||||
- 已移除 `3306 <- 0.0.0.0/0`
|
|
||||||
- 当前 RDS 仅绑定 `hifast-hk-rds-core-sg`
|
|
||||||
|
|
||||||
建议原则:
|
|
||||||
|
|
||||||
- 不开放 `0.0.0.0/0` 到 MySQL `3306`
|
|
||||||
- 不开放 `0.0.0.0/0` 到 Redis `6379`
|
|
||||||
|
|
||||||
### 4.5 当前仍需继续收敛的网络点
|
|
||||||
|
|
||||||
#### 4.5.1 入口层当前属于过渡方案,不作为本阶段阻塞项
|
|
||||||
|
|
||||||
当前运行形态是:
|
|
||||||
|
|
||||||
- `hifast-hk-app-01` 本机 `nginx` 正在监听 `80/443`
|
|
||||||
- `hifast-hk-nginx-01` 当前停止
|
|
||||||
|
|
||||||
但当前安全组设计是按“独立 nginx 机”拆的:
|
|
||||||
|
|
||||||
- `hifast-hk-web-sg` 在 `hifast-hk-nginx-01`
|
|
||||||
- `hifast-hk-app-core-sg` 在 `hifast-hk-app-01`
|
|
||||||
|
|
||||||
这意味着当前真实入口和安全组角色划分还没有完全一致。
|
|
||||||
|
|
||||||
但这一点已经被确认为过渡设计,不作为当前阶段必须整改的问题。后续待 AWS 新资源到位后,再把公网入口迁到独立 `nginx` 服务器即可。
|
|
||||||
|
|
||||||
#### 4.5.2 RDS 公网复制链路仍是当前唯一核心结构性问题
|
|
||||||
|
|
||||||
为了让 `104.238.220.230` 通过公网白名单访问 RDS,当前实际依赖的是:
|
|
||||||
|
|
||||||
- RDS `PubliclyAccessible = true`
|
|
||||||
- RDS ENI 仍在 `subnet-0dcf46db665b6d8a7`
|
|
||||||
- 该子网名称是 `hifast-hk-private-1b`
|
|
||||||
- 但这个子网当前被临时关联到了公网路由表
|
|
||||||
|
|
||||||
这能用,但不属于最终规范的“纯公网子网组”设计。
|
|
||||||
|
|
||||||
本轮已经额外完成的准备动作:
|
|
||||||
|
|
||||||
- 已新建纯公网 `DB subnet group`:`hifast-hk-rds-public-only-sgprep`
|
|
||||||
- 该组状态:`Complete`
|
|
||||||
- 该组仅包含两条公网子网:
|
|
||||||
- `hifast-hk-public-1a`
|
|
||||||
- `hifast-hk-public-1b`
|
|
||||||
|
|
||||||
当前结论:
|
|
||||||
|
|
||||||
- 不建议继续在现有生产 RDS 上反复试在线切子网组。
|
|
||||||
- 更稳的收敛方案是:新建一台使用规范公网子网组的生产 RDS,再做一次短切换。
|
|
||||||
- 由于当前库体量只有约 `187 MB`,这条路线的执行成本并不高,风险也比在现网主库上硬改更可控。
|
|
||||||
|
|
||||||
## 4.6 104 如何连接 AWS
|
|
||||||
|
|
||||||
这里要特别区分:
|
|
||||||
|
|
||||||
- `104` 连接 AWS 数据层
|
|
||||||
- 本地电脑登录 AWS EC2
|
|
||||||
|
|
||||||
这不是同一件事。
|
|
||||||
|
|
||||||
### 4.6.1 104 连接 AWS Redis 的方式
|
|
||||||
|
|
||||||
`104.238.220.230` 连接 AWS Redis,不是通过 PEM 证书,也不是通过 SSH 登录 AWS 机器,而是直接作为 Redis 从库去访问 AWS Redis 主库:
|
|
||||||
|
|
||||||
- 目标地址:`18.163.33.75:6379`
|
|
||||||
- 连接方式:`TCP`
|
|
||||||
- 认证方式:`Redis 密码`
|
|
||||||
- 网络前提:AWS EC2 安全组已放行 `104.238.220.230/32 -> 6379`
|
|
||||||
|
|
||||||
也就是说:
|
|
||||||
|
|
||||||
```text
|
|
||||||
104 Redis 从库 -> 直连 AWS Redis 主库公网地址 -> 密码认证 -> 建立复制
|
|
||||||
```
|
|
||||||
|
|
||||||
示意命令:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
redis-cli -h 18.163.33.75 -p 6379 -a '<REDIS_PASSWORD>'
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4.6.2 104 连接 AWS MySQL RDS 的方式
|
|
||||||
|
|
||||||
`104.238.220.230` 连接 AWS RDS,也不是通过 PEM 证书,而是通过数据库连接:
|
|
||||||
|
|
||||||
- 目标地址:`hifast-mysql-prod-v2.cd6aey40m6ag.ap-east-1.rds.amazonaws.com:3306`
|
|
||||||
- 连接方式:`MySQL TCP`
|
|
||||||
- 认证方式:`MySQL 用户名 + 密码`
|
|
||||||
- 网络前提:RDS 白名单或安全组放行 `104.238.220.230`
|
|
||||||
|
|
||||||
也就是说:
|
|
||||||
|
|
||||||
```text
|
|
||||||
104 MySQL 从库 -> 直连 AWS RDS endpoint -> 账号密码认证 -> 建立复制
|
|
||||||
```
|
|
||||||
|
|
||||||
示意命令:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mysql -h hifast-mysql-prod-v2.cd6aey40m6ag.ap-east-1.rds.amazonaws.com -u admin -p
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4.6.3 本地电脑登录 AWS EC2 的方式
|
|
||||||
|
|
||||||
本地电脑这边之前进入 AWS EC2,用的不是 `hifast-hk-app-01-key.pem`,而是:
|
|
||||||
|
|
||||||
- `AWS Console`
|
|
||||||
- `EC2 Instance Connect`
|
|
||||||
- 浏览器里的 Web SSH 终端
|
|
||||||
|
|
||||||
所以当前要理解成 3 条不同链路:
|
|
||||||
|
|
||||||
1. 本地电脑 -> AWS 控制台 -> EC2 Instance Connect -> AWS EC2
|
|
||||||
2. `104` -> AWS Redis 主库公网地址 -> Redis 密码认证
|
|
||||||
3. `104` -> AWS RDS endpoint -> MySQL 用户密码认证
|
|
||||||
|
|
||||||
### 4.6.4 关键结论
|
|
||||||
|
|
||||||
当前灾备链路里,`104` 与 AWS 数据同步不依赖 `.pem` 证书文件。
|
|
||||||
|
|
||||||
真正依赖的是:
|
|
||||||
|
|
||||||
- Redis 安全组放行
|
|
||||||
- RDS 白名单 / 安全组放行
|
|
||||||
- 正确的 Redis 密码
|
|
||||||
- 正确的 MySQL 账号密码
|
|
||||||
|
|
||||||
## 5. Redis 实际部署与同步状态
|
|
||||||
|
|
||||||
### 5.1 AWS Redis 主库
|
|
||||||
|
|
||||||
- 部署方式:Docker
|
|
||||||
- 版本:`8.2.1`
|
|
||||||
- 主库地址:`18.163.33.75:6379`
|
|
||||||
- 运行容器:`hifast-redis`
|
|
||||||
|
|
||||||
### 5.2 104 Redis 从库
|
|
||||||
|
|
||||||
- 部署方式:宿主机原生安装
|
|
||||||
- 版本:`8.6.3`
|
|
||||||
- 角色:`replica / slave`
|
|
||||||
|
|
||||||
### 5.3 Redis 主从状态
|
|
||||||
|
|
||||||
最终已验证结果:
|
|
||||||
|
|
||||||
- `104` 上 Redis:`role:slave`
|
|
||||||
- `104` 上 Redis:`master_link_status:up`
|
|
||||||
- AWS Redis 主库:`connected_slaves:1`
|
|
||||||
- AWS Redis 主库识别到从库:`104.238.220.230:6379`
|
|
||||||
|
|
||||||
### 5.4 Redis 验证结果
|
|
||||||
|
|
||||||
已做过的验证:
|
|
||||||
|
|
||||||
- 从 `104` 连接 AWS Redis 主库,认证成功
|
|
||||||
- AWS 主库写入测试键
|
|
||||||
- `104` 从库成功读取测试键
|
|
||||||
- `2026-05-13` 已确认应用机本地 `ppanel-server -> 127.0.0.1:6379` 建立稳定连接
|
|
||||||
- `2026-05-13` 已确认旧 `stunnel4 -> ElastiCache` 链路下线后,应用日志不再出现 Redis `i/o timeout`
|
|
||||||
|
|
||||||
测试键:
|
|
||||||
|
|
||||||
- key: `hifast_replication_test`
|
|
||||||
- value: `ok_20260510`
|
|
||||||
|
|
||||||
### 5.5 Redis 主从拓扑
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart LR
|
|
||||||
REDISMASTER["AWS Redis 主库\n18.163.33.75:6379\nDocker redis:8.2.1"]
|
|
||||||
REDISSLAVE["104.238.220.230\n原生 Redis 8.6.3\nrole: slave"]
|
|
||||||
REDISMASTER --> REDISSLAVE
|
|
||||||
```
|
|
||||||
|
|
||||||
## 6. MySQL 部署与备用关系
|
|
||||||
|
|
||||||
### 6.1 主库角色
|
|
||||||
|
|
||||||
- 主库在 `AWS RDS MySQL`
|
|
||||||
- 实例:`hifast-mysql-prod-v2`
|
|
||||||
|
|
||||||
### 6.2 外部备用角色
|
|
||||||
|
|
||||||
- `104.238.220.230` 上存在 MySQL 备用用途
|
|
||||||
- 目标是让 `104` 尽量实时同步 AWS 数据
|
|
||||||
|
|
||||||
### 6.3 当前文档说明
|
|
||||||
|
|
||||||
MySQL 当前已经完成实际验收,不再只是“待确认”状态。
|
|
||||||
|
|
||||||
`2026-05-13` 验收结果:
|
|
||||||
|
|
||||||
- `Source_Host = hifast-mysql-prod-v2.cd6aey40m6ag.ap-east-1.rds.amazonaws.com`
|
|
||||||
- `Replica_IO_Running = Yes`
|
|
||||||
- `Replica_SQL_Running = Yes`
|
|
||||||
- `Seconds_Behind_Source = 0`
|
|
||||||
- `read_only = ON`
|
|
||||||
- `super_read_only = ON`
|
|
||||||
|
|
||||||
这表示 `104` 当前是健康的只读外部备用库。
|
|
||||||
|
|
||||||
MySQL 这部分在此前已经有专门 runbook:
|
|
||||||
|
|
||||||
- [ops/aws-rds-external-replica-runbook.md](/Users/Apple/code_vpn/vpn/ppanel-server/ops/aws-rds-external-replica-runbook.md)
|
|
||||||
|
|
||||||
## 7. 当前生产方案的真实特点
|
|
||||||
|
|
||||||
这套已经落地的架构,不是传统的全 AWS 托管标准形态,而是偏实用的混合方案:
|
|
||||||
|
|
||||||
- 应用和当前实际公网入口在 AWS 同一台 EC2
|
|
||||||
- MySQL 在 AWS RDS
|
|
||||||
- Redis 在 AWS EC2 本机
|
|
||||||
- MySQL / Redis 均向外部服务器 `104` 做灾备
|
|
||||||
- 旧 ElastiCache / stunnel 链路已经退出当前生产路径
|
|
||||||
|
|
||||||
它的优点:
|
|
||||||
|
|
||||||
- 成本相对可控
|
|
||||||
- Redis 可完全自主控制
|
|
||||||
- 外部备用机可以独立接管
|
|
||||||
|
|
||||||
它的代价:
|
|
||||||
|
|
||||||
- Redis 高可用需要人工切换
|
|
||||||
- 外部灾备不是全自动故障转移
|
|
||||||
- 应用切换需要明确操作步骤
|
|
||||||
|
|
||||||
## 8. 故障切换方案
|
|
||||||
|
|
||||||
### 8.1 正常状态
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart TD
|
|
||||||
A["用户访问"] --> B["AWS EC2 应用机"]
|
|
||||||
B --> C["AWS RDS MySQL 主库"]
|
|
||||||
B --> D["AWS Redis 主库"]
|
|
||||||
C -. 同步 .-> E["104 MySQL 备用"]
|
|
||||||
D -. 复制 .-> F["104 Redis 从库"]
|
|
||||||
```
|
|
||||||
|
|
||||||
### 8.2 AWS 故障后的目标切换状态
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart TD
|
|
||||||
A["AWS 故障"] --> B["应用入口切到 104"]
|
|
||||||
B --> C["104 MySQL 提供主服务"]
|
|
||||||
B --> D["104 Redis 提升为主库"]
|
|
||||||
D --> E["应用连接 104 Redis"]
|
|
||||||
C --> F["应用连接 104 MySQL"]
|
|
||||||
```
|
|
||||||
|
|
||||||
### 8.2.1 Nginx 是否可以直接切到 104
|
|
||||||
|
|
||||||
可以,但前提不是“只切 Nginx 就完成故障切换”。
|
|
||||||
|
|
||||||
因为 `104` 虽然已经部署了同样的业务服务,但如果故障发生时:
|
|
||||||
|
|
||||||
- Redis 还保持从库只读状态
|
|
||||||
- MySQL 还没有切成可写主角色
|
|
||||||
- 应用配置还没有确认指向 `104` 本机数据层
|
|
||||||
|
|
||||||
那么即使 Nginx 已经把流量转到 `104`,业务也可能仍然无法正常写入。
|
|
||||||
|
|
||||||
所以更准确的原则是:
|
|
||||||
|
|
||||||
`104` 已具备应用接管能力,Nginx 切换可以作为最后一步对外放流量动作,但不能作为唯一动作。
|
|
||||||
|
|
||||||
### 8.3 Redis 切换动作
|
|
||||||
|
|
||||||
当 AWS Redis 不可用时,`104` 上的 Redis 需要解除主从关系:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
redis-cli -a '<REDIS_PASSWORD>' REPLICAOF NO ONE
|
|
||||||
```
|
|
||||||
|
|
||||||
切换后:
|
|
||||||
|
|
||||||
- `104` Redis 从库变为主库
|
|
||||||
- 业务应用把 Redis 地址改到 `104.238.220.230:6379`
|
|
||||||
|
|
||||||
### 8.4 MySQL 切换动作
|
|
||||||
|
|
||||||
当 AWS RDS 不可用时,需要让 `104` MySQL 接管写流量。
|
|
||||||
|
|
||||||
这部分是否能立即切,需要依赖:
|
|
||||||
|
|
||||||
- 当前 `104` MySQL 是否是健康从库
|
|
||||||
- 是否已经取消只读
|
|
||||||
- 应用数据库配置是否能快速切换到 `104`
|
|
||||||
|
|
||||||
### 8.5 应用切换动作
|
|
||||||
|
|
||||||
应用层需要准备至少这 2 个切换点:
|
|
||||||
|
|
||||||
- MySQL 连接地址切换到 `104`
|
|
||||||
- Redis 连接地址切换到 `104`
|
|
||||||
|
|
||||||
如果应用入口也要迁移到 `104`,还需要:
|
|
||||||
|
|
||||||
- 域名解析切换
|
|
||||||
- 或者网关 / 入口切换
|
|
||||||
|
|
||||||
### 8.6 推荐的实际切换顺序
|
|
||||||
|
|
||||||
因为 `104` 已经部署同样的应用服务,所以 AWS 故障时推荐按下面顺序操作:
|
|
||||||
|
|
||||||
1. 确认 `104` 上业务服务和 Nginx 进程正常。
|
|
||||||
2. 将 `104` 上 Redis 从库提升为主库。
|
|
||||||
3. 将 `104` 上 MySQL 从库切换为可写主库。
|
|
||||||
4. 确认 `104` 上应用配置已指向本机 MySQL / Redis。
|
|
||||||
5. 最后再把 Nginx 上游或域名流量切到 `104`。
|
|
||||||
|
|
||||||
可以把它理解成:
|
|
||||||
|
|
||||||
`先数据接管 -> 再应用确认 -> 最后入口切流量`
|
|
||||||
|
|
||||||
## 9. 建议的运维操作顺序
|
|
||||||
|
|
||||||
### 9.1 平时
|
|
||||||
|
|
||||||
平时重点看:
|
|
||||||
|
|
||||||
- AWS EC2 是否在线
|
|
||||||
- RDS 是否在线
|
|
||||||
- AWS Redis 主库是否在线
|
|
||||||
- `104` Redis 从库是否 `master_link_status:up`
|
|
||||||
- `104` MySQL 复制是否正常
|
|
||||||
- `ppanel-server` 容器是否 `Up`
|
|
||||||
- `ppanel-server` 日志中是否再次出现 Redis `i/o timeout`
|
|
||||||
- `stunnel4` 是否保持 `inactive`
|
|
||||||
|
|
||||||
### 9.2 Redis 故障时
|
|
||||||
|
|
||||||
1. 确认 AWS Redis 主库不可恢复。
|
|
||||||
2. 在 `104` 执行 `REPLICAOF NO ONE`。
|
|
||||||
3. 修改应用 Redis 地址到 `104.238.220.230:6379`。
|
|
||||||
4. 验证应用读写 Redis 正常。
|
|
||||||
|
|
||||||
### 9.3 MySQL 故障时
|
|
||||||
|
|
||||||
1. 确认 RDS 故障。
|
|
||||||
2. 确认 `104` MySQL 数据已同步到最新可用点。
|
|
||||||
3. 去掉 `104` MySQL 只读限制。
|
|
||||||
4. 修改应用 MySQL 地址到 `104`。
|
|
||||||
5. 验证应用读写数据库正常。
|
|
||||||
|
|
||||||
### 9.4 整体 AWS 故障时
|
|
||||||
|
|
||||||
1. 把 Redis 主角色切到 `104`。
|
|
||||||
2. 把 MySQL 主角色切到 `104`。
|
|
||||||
3. 确认 `104` 上同版本应用服务正常。
|
|
||||||
4. 把应用入口切到备用应用节点。
|
|
||||||
5. 更新 DNS 或 Nginx 上游流量入口。
|
|
||||||
6. 验证用户访问链路。
|
|
||||||
|
|
||||||
## 10. 当前方案与理想方案的差异
|
|
||||||
|
|
||||||
### 10.1 当前实际方案
|
|
||||||
|
|
||||||
`DNS -> hifast-hk-app-01(Nginx + App + Redis) -> RDS MySQL -> 104 灾备`
|
|
||||||
|
|
||||||
### 10.2 理想生产方案
|
|
||||||
|
|
||||||
从长期稳定性看,更推荐未来演进为:
|
|
||||||
|
|
||||||
`DNS / CDN -> ALB -> 多台 EC2 App -> RDS MySQL -> 托管 Redis / 或高可用 Redis`
|
|
||||||
|
|
||||||
异地灾备继续保留:
|
|
||||||
|
|
||||||
- AWS 生产
|
|
||||||
- 104 异地接管
|
|
||||||
|
|
||||||
### 10.3 当前最值得继续补的项
|
|
||||||
|
|
||||||
建议按优先级补齐:
|
|
||||||
|
|
||||||
1. 把 RDS 从“private 子网挂公网路由”的临时方案,收敛成真正的公网 DB subnet group,或改成私网复制方案
|
|
||||||
2. 明确 `104` 应用接管脚本与启动检查项
|
|
||||||
3. 明确 MySQL 故障切换脚本
|
|
||||||
4. 明确 Redis 故障切换脚本
|
|
||||||
5. 明确域名 / DNS 切换方式
|
|
||||||
6. 做一次完整灾备演练
|
|
||||||
7. 待 AWS 新资源到位后,再把公网入口迁到独立 `nginx` 服务器
|
|
||||||
|
|
||||||
### 10.4 RDS 推荐收敛路线
|
|
||||||
|
|
||||||
当前原先规划的“新建规范 RDS 再切换”路线已经完成,现网主库已经是 `hifast-mysql-prod-v2`。
|
|
||||||
|
|
||||||
后续更推荐的正式路线是:
|
|
||||||
|
|
||||||
1. 继续以 `hifast-mysql-prod-v2` 作为当前生产主库
|
|
||||||
2. 确认其持续使用 `hifast-hk-rds-public-only-sgprep`
|
|
||||||
3. 安全组持续只放:
|
|
||||||
- `104.238.220.230/32 -> 3306`
|
|
||||||
- `hifast-hk-app-core-sg -> 3306`
|
|
||||||
4. 持续确认应用连接目标为 `hifast-mysql-prod-v2`
|
|
||||||
5. 持续确认 `104` 的外部复制目标为 `hifast-mysql-prod-v2`
|
|
||||||
6. 验证通过后,再安排旧 RDS 下线
|
|
||||||
|
|
||||||
这条路线的优点:
|
|
||||||
|
|
||||||
- 最终拓扑规范清晰
|
|
||||||
- 不需要继续保留“private 子网挂公网路由”的临时设计
|
|
||||||
- 对当前现网主库的扰动最小
|
|
||||||
- 当前数据库体量较小,迁移成本可控
|
|
||||||
|
|
||||||
## 11. 建议的下一版目标拓扑
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart TB
|
|
||||||
USER["用户 / 客户端"] --> DNS["DNS / CDN / 入口层"]
|
|
||||||
DNS --> APPAWS["AWS 应用集群"]
|
|
||||||
DNS -. 故障时切换 .-> APPBK["104 备用应用节点"]
|
|
||||||
|
|
||||||
APPAWS --> RDSAWS["AWS RDS MySQL 主库"]
|
|
||||||
APPAWS --> REDISAWS["AWS Redis 主库"]
|
|
||||||
|
|
||||||
RDSAWS -. 同步 .-> MYSQLBK["104 MySQL 备用"]
|
|
||||||
REDISAWS -. 复制 .-> REDISBK["104 Redis 备用"]
|
|
||||||
|
|
||||||
APPBK --> MYSQLBK
|
|
||||||
APPBK --> REDISBK
|
|
||||||
```
|
|
||||||
|
|
||||||
## 12. 本文档结论
|
|
||||||
|
|
||||||
截至当前,已经可以确认的生产与灾备状态是:
|
|
||||||
|
|
||||||
- AWS 是主生产环境
|
|
||||||
- 应用当前跑在 `hifast-hk-app-01`
|
|
||||||
- MySQL 主库在 AWS RDS
|
|
||||||
- Redis 主库在 AWS EC2 Docker
|
|
||||||
- `104.238.220.230` 是异地备用节点
|
|
||||||
- `104` 已部署与 AWS 相同的业务服务
|
|
||||||
- `104` 上 Redis 已切为宿主机原生安装
|
|
||||||
- `104` Redis 已成功作为 AWS Redis 主库的从库在线同步
|
|
||||||
- `104` MySQL 已恢复并保持健康复制
|
|
||||||
- 应用已经切回当前真实可用的本机 Redis 主库
|
|
||||||
- 旧 `stunnel4 -> ElastiCache` 链路已经退出生产路径
|
|
||||||
|
|
||||||
如果后续要继续完善这份方案,优先补充:
|
|
||||||
|
|
||||||
- RDS 子网与公网访问模型收敛
|
|
||||||
- 入口域名 / DNS 切换细则
|
|
||||||
- 应用层在 `104` 的接管与回切执行清单
|
|
||||||
- 待资源到位后的独立 `nginx` 迁移清单
|
|
||||||
@@ -1,246 +0,0 @@
|
|||||||
# Hifast 当前架构文档(东京主站版)
|
|
||||||
|
|
||||||
最后更新:`2026-05-21`
|
|
||||||
|
|
||||||
这份文档只描述**当前真实生效**的架构,不描述理想目标,不混入已经下线或待迁移的香港旧链路。
|
|
||||||
|
|
||||||
如果后续东京 `ALB / WAF / 正式域名` 上线,应继续更新这份文档,而不是回头参考旧香港架构文档。
|
|
||||||
|
|
||||||
## 1. 当前结论
|
|
||||||
|
|
||||||
当前已经实际跑起来的是:
|
|
||||||
|
|
||||||
`用户 / 运维 -> 东京 EC2 -> ppanel-server + Redis + observability -> 东京 RDS MySQL`
|
|
||||||
|
|
||||||
当前还**没有**实际生效的组件:
|
|
||||||
|
|
||||||
- `ALB`
|
|
||||||
- `WAF`
|
|
||||||
- 正式域名切流
|
|
||||||
- `104.238.220.230` 重挂东京 MySQL / Redis 从库
|
|
||||||
|
|
||||||
也就是说,东京环境目前是:
|
|
||||||
|
|
||||||
- 一台业务 EC2 已可用
|
|
||||||
- 一台东京 RDS 已可用
|
|
||||||
- 本机 Redis 已可用
|
|
||||||
- `ppanel-server` 已成功启动并连通 MySQL / Redis
|
|
||||||
- 灾备链路参数已准备好,但 `104` 还没最终挂上去
|
|
||||||
|
|
||||||
## 2. 当前真实拓扑
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart TB
|
|
||||||
USER["用户 / 运维"] --> EIP["东京 EC2 公网入口\n3.114.29.208"]
|
|
||||||
EIP --> NGINX["Nginx\nEC2 本机"]
|
|
||||||
NGINX --> APP["ppanel-server\nhost network\n127.0.0.1:8080"]
|
|
||||||
APP --> REDIS["Redis Docker\n127.0.0.1:6379"]
|
|
||||||
APP --> RDS["Tokyo RDS MySQL\nppanel-mysql-jp"]
|
|
||||||
APP --> OBS["Grafana / Loki / Tempo / Prometheus"]
|
|
||||||
|
|
||||||
RDS -. 预留复制 .-> DRMYSQL["104.238.220.230\nMySQL DR\n未最终接入"]
|
|
||||||
REDIS -. 预留复制 .-> DRREDIS["104.238.220.230\nRedis DR\n未最终接入"]
|
|
||||||
```
|
|
||||||
|
|
||||||
## 3. 云资源清单
|
|
||||||
|
|
||||||
### 3.1 Region
|
|
||||||
|
|
||||||
- AWS account: `hifastvpn (200810848252)`
|
|
||||||
- Region: `ap-northeast-1`
|
|
||||||
|
|
||||||
### 3.2 网络
|
|
||||||
|
|
||||||
- VPC: `ppanel-jp-prod`
|
|
||||||
- VPC ID: `vpc-0846b23b4a7d64eac`
|
|
||||||
- CIDR: `10.20.0.0/16`
|
|
||||||
|
|
||||||
公有子网:
|
|
||||||
|
|
||||||
- `ppanel-jp-public-a` / `subnet-091232bdb53e71490` / `10.20.0.0/24`
|
|
||||||
- `ppanel-jp-public-c` / `subnet-01ba0975c525ce8cf` / `10.20.1.0/24`
|
|
||||||
|
|
||||||
私有子网:
|
|
||||||
|
|
||||||
- `ppanel-jp-private-a` / `subnet-0bd13111c02f0edbe` / `10.20.10.0/24`
|
|
||||||
- `ppanel-jp-private-c` / `subnet-0d86c5c756dbc84b2` / `10.20.11.0/24`
|
|
||||||
|
|
||||||
### 3.3 安全组
|
|
||||||
|
|
||||||
- `ppanel-jp-sg-alb`
|
|
||||||
- `80/tcp <- 0.0.0.0/0`
|
|
||||||
- `443/tcp <- 0.0.0.0/0`
|
|
||||||
- `ppanel-jp-sg-ec2`
|
|
||||||
- `80/tcp <- sg-0b3a23c31041a5a5a`
|
|
||||||
- `22/tcp <- 64.118.144.142/32`
|
|
||||||
- `6379/tcp <- 104.238.220.230/32`
|
|
||||||
- `ppanel-jp-sg-rds`
|
|
||||||
- `3306/tcp <- sg-01f2a5a81e7505c91`
|
|
||||||
- `3306/tcp <- 104.238.220.230/32`
|
|
||||||
|
|
||||||
## 4. 计算与数据库
|
|
||||||
|
|
||||||
### 4.1 业务 EC2
|
|
||||||
|
|
||||||
- Name: `ppanel-app-jp-01`
|
|
||||||
- Instance ID: `i-07839130074cd7ed9`
|
|
||||||
- Type: `c7i.xlarge`
|
|
||||||
- AZ: `ap-northeast-1c`
|
|
||||||
- Private IP: `10.20.1.168`
|
|
||||||
- Public IP / Elastic IP: `3.114.29.208`
|
|
||||||
- Public DNS: `ec2-3-114-29-208.ap-northeast-1.compute.amazonaws.com`
|
|
||||||
- Root volume: `gp3 100GiB`
|
|
||||||
- Status: `running`
|
|
||||||
|
|
||||||
说明:
|
|
||||||
|
|
||||||
- 当前公网入口是这台 EC2 自己
|
|
||||||
- 这里绑定的是 `EIP`,不是依赖实例自动分配的临时公网 IP
|
|
||||||
- 绑定 EIP 的目的,是保证以后 Redis 主从、白名单、DNS、文档里的公网地址不因为实例变更而漂移
|
|
||||||
|
|
||||||
### 4.2 Tokyo RDS
|
|
||||||
|
|
||||||
- Identifier: `ppanel-mysql-jp`
|
|
||||||
- Engine: `MySQL Community 8.4.8`
|
|
||||||
- Class: `db.r7g.xlarge`
|
|
||||||
- Storage: `gp3 100GiB`
|
|
||||||
- Master user: `admin`
|
|
||||||
- Endpoint: `ppanel-mysql-jp.cpo0keikgh80.ap-northeast-1.rds.amazonaws.com`
|
|
||||||
- Public access: `enabled`
|
|
||||||
- Credential management: `self-managed`
|
|
||||||
- Current status: `available`
|
|
||||||
|
|
||||||
当前实际连接值:
|
|
||||||
|
|
||||||
- Host: `ppanel-mysql-jp.cpo0keikgh80.ap-northeast-1.rds.amazonaws.com`
|
|
||||||
- Port: `3306`
|
|
||||||
- User: `admin`
|
|
||||||
|
|
||||||
### 4.3 Redis
|
|
||||||
|
|
||||||
- 部署位置:东京 EC2 本机
|
|
||||||
- 部署方式:Docker
|
|
||||||
- 容器名:`ppanel-redis`
|
|
||||||
- 当前角色:`master`
|
|
||||||
- 当前监听:`127.0.0.1:6379`(应用本机访问)
|
|
||||||
- 当前对灾备开放:`3.114.29.208:6379`
|
|
||||||
|
|
||||||
### 4.4 应用与可观测
|
|
||||||
|
|
||||||
业务容器:
|
|
||||||
|
|
||||||
- `ppanel-server`
|
|
||||||
|
|
||||||
可观测容器:
|
|
||||||
|
|
||||||
- `ppanel-grafana`
|
|
||||||
- `ppanel-loki`
|
|
||||||
- `ppanel-promtail`
|
|
||||||
- `ppanel-prometheus`
|
|
||||||
- `ppanel-tempo`
|
|
||||||
- `ppanel-cadvisor`
|
|
||||||
- `ppanel-node-exporter`
|
|
||||||
- `ppanel-nginx-exporter`
|
|
||||||
|
|
||||||
## 5. 当前部署方式
|
|
||||||
|
|
||||||
部署目录:
|
|
||||||
|
|
||||||
- `/opt/ppanel`
|
|
||||||
|
|
||||||
关键文件:
|
|
||||||
|
|
||||||
- `/opt/ppanel/docker-compose.cloud.yml`
|
|
||||||
- `/opt/ppanel/configs/ppanel.yaml`
|
|
||||||
- `/opt/ppanel/.env`
|
|
||||||
|
|
||||||
业务启动方式:
|
|
||||||
|
|
||||||
- `ppanel-server` 使用 `docker compose`
|
|
||||||
- `network_mode: host`
|
|
||||||
|
|
||||||
当前配置指向:
|
|
||||||
|
|
||||||
- MySQL -> 东京 RDS
|
|
||||||
- Redis -> 本机 Docker Redis
|
|
||||||
- Trace -> `127.0.0.1:4317`
|
|
||||||
|
|
||||||
## 6. 当前已验证状态
|
|
||||||
|
|
||||||
截至 `2026-05-21` 已确认:
|
|
||||||
|
|
||||||
- `docker exec ppanel-redis redis-cli -a 'hifast67yj' PING` 返回 `PONG`
|
|
||||||
- `MYSQL_PWD=... mysql -h ppanel-mysql-jp.cpo0keikgh80.ap-northeast-1.rds.amazonaws.com -u admin -e "select 1"` 返回正常
|
|
||||||
- `docker compose -f /opt/ppanel/docker-compose.cloud.yml up -d ppanel-server` 已成功
|
|
||||||
- `docker logs ppanel-server` 无 MySQL / Redis 连接报错
|
|
||||||
- `curl http://127.0.0.1:8080/v1/common/heartbeat` 返回成功
|
|
||||||
|
|
||||||
可以认为当前东京单站已经具备:
|
|
||||||
|
|
||||||
- 服务启动能力
|
|
||||||
- 数据库连接能力
|
|
||||||
- Redis 连接能力
|
|
||||||
- 基础可观测能力
|
|
||||||
|
|
||||||
## 7. 当前未落地项
|
|
||||||
|
|
||||||
下面这些还属于“目标方案”,不是“当前实际架构”:
|
|
||||||
|
|
||||||
- `ALB`
|
|
||||||
- `WAF`
|
|
||||||
- ACM 证书完成 DNS 验证
|
|
||||||
- 正式域名切换到东京
|
|
||||||
- `104.238.220.230` 完整接成东京 DR
|
|
||||||
|
|
||||||
所以现在请不要把当前架构理解成:
|
|
||||||
|
|
||||||
`DNS -> ALB -> WAF -> EC2 -> RDS`
|
|
||||||
|
|
||||||
当前真实架构更准确地说是:
|
|
||||||
|
|
||||||
`EIP -> EC2(Nginx + ppanel-server + Redis + observability) -> RDS`
|
|
||||||
|
|
||||||
## 8. 灾备准备状态
|
|
||||||
|
|
||||||
### 8.1 MySQL DR
|
|
||||||
|
|
||||||
已准备好:
|
|
||||||
|
|
||||||
- DR host: `104.238.220.230`
|
|
||||||
- 复制账号:`repl@104.238.220.230`
|
|
||||||
- binlog retention hours: `24`
|
|
||||||
- current binlog file: `mysql-bin-changelog.000189`
|
|
||||||
- current binlog position: `185053`
|
|
||||||
|
|
||||||
当前状态:
|
|
||||||
|
|
||||||
- 东京侧已准备完成
|
|
||||||
- 但 `104` 侧还没有最终执行挂从
|
|
||||||
|
|
||||||
### 8.2 Redis DR
|
|
||||||
|
|
||||||
已准备好:
|
|
||||||
|
|
||||||
- 东京 Redis 主地址:`3.114.29.208:6379`
|
|
||||||
- 东京 Redis 当前角色:`role:master`
|
|
||||||
- 东京 Redis 当前状态:`connected_slaves:0`
|
|
||||||
|
|
||||||
当前状态:
|
|
||||||
|
|
||||||
- 东京侧已可作为主库提供同步
|
|
||||||
- 但 `104` 侧还没有最终执行 `REPLICAOF`
|
|
||||||
|
|
||||||
## 9. 权威文档入口
|
|
||||||
|
|
||||||
与当前东京架构直接相关的文档,以这些为准:
|
|
||||||
|
|
||||||
- [当前架构文档](./hifast-current-architecture-zh.md)
|
|
||||||
- [东京资源清单](../deploy/aws/ap-northeast-1/configs/resource-inventory.current.md)
|
|
||||||
- [东京迁移 Runbook](./hifast-aws-jp-migration-runbook-zh.md)
|
|
||||||
- [东京切换清单](./hifast-aws-jp-cutover-checklist-zh.md)
|
|
||||||
|
|
||||||
下面这些旧文档可以继续保留,但不要作为“当前实际架构”依据:
|
|
||||||
|
|
||||||
- 香港旧主站相关文档
|
|
||||||
- 仍以 `ap-east-1` 为主语的历史架构说明
|
|
||||||
|
|
||||||
@@ -1,286 +0,0 @@
|
|||||||
# Hifast 新 RDS 生产切换清单(已完成归档)
|
|
||||||
|
|
||||||
本文档记录已完成的 `hifast-mysql-prod -> hifast-mysql-prod-v2` 生产切换过程,作为归档和回溯参考。
|
|
||||||
|
|
||||||
本次切换已经完成,当时的目标是避免继续在线硬改现有生产 RDS,而是:
|
|
||||||
|
|
||||||
1. 新建一台规范公网子网组的新 RDS
|
|
||||||
2. 导入现有 `hifast` 数据
|
|
||||||
3. 将 AWS 应用切到新 RDS
|
|
||||||
4. 将 `104.238.220.230` 的外部从库改挂新 RDS
|
|
||||||
5. 验收通过后再下线旧 RDS
|
|
||||||
|
|
||||||
## 1. 适用背景
|
|
||||||
|
|
||||||
当前现网已经可用,但还存在 1 个结构性问题:
|
|
||||||
|
|
||||||
- 为了让 `104` 通过公网白名单做 MySQL 复制,当时的 `hifast-mysql-prod` 仍依赖 `hifast-hk-private-1b` 临时挂公网路由
|
|
||||||
|
|
||||||
当前已准备好的收敛资源:
|
|
||||||
|
|
||||||
- 纯公网 DB subnet group:
|
|
||||||
- `hifast-hk-rds-public-only-sgprep`
|
|
||||||
- 安全组:
|
|
||||||
- `hifast-hk-rds-core-sg`
|
|
||||||
|
|
||||||
## 2. 目标实例参数
|
|
||||||
|
|
||||||
建议新实例名称:
|
|
||||||
|
|
||||||
- `hifast-mysql-prod-v2`
|
|
||||||
|
|
||||||
建议参数如下:
|
|
||||||
|
|
||||||
- Region: `ap-east-1`
|
|
||||||
- Engine: `MySQL`
|
|
||||||
- Engine version: `8.4.8`
|
|
||||||
- Templates: `Production`
|
|
||||||
- DB instance class: `db.r7g.xlarge`
|
|
||||||
- Storage type: `gp3`
|
|
||||||
- Allocated storage: `200 GiB`
|
|
||||||
- Provisioned IOPS: `3000`
|
|
||||||
- Storage throughput: `125`
|
|
||||||
- Multi-AZ: `No`
|
|
||||||
- Publicly accessible: `Yes`
|
|
||||||
- VPC: `vpc-09fc384522517debc`
|
|
||||||
- DB subnet group: `hifast-hk-rds-public-only-sgprep`
|
|
||||||
- VPC security group: `hifast-hk-rds-core-sg`
|
|
||||||
- DB name: `hifast`
|
|
||||||
- Master username: `admin`
|
|
||||||
- Master password: 与现网保持一致
|
|
||||||
- Backup retention: `7 days`
|
|
||||||
- Performance Insights: `On`
|
|
||||||
- Storage encryption: `On`
|
|
||||||
- Deletion protection: `On`
|
|
||||||
- Auto minor version upgrade: 建议保持与现网一致
|
|
||||||
- Maintenance window: 可与现网同策略,建议维护窗口内切换
|
|
||||||
|
|
||||||
## 3. 切换前确认
|
|
||||||
|
|
||||||
切换前必须满足:
|
|
||||||
|
|
||||||
1. `104` 当前主从正常:
|
|
||||||
- `Replica_IO_Running: Yes`
|
|
||||||
- `Replica_SQL_Running: Yes`
|
|
||||||
- `Seconds_Behind_Source: 0`
|
|
||||||
2. AWS 应用机心跳正常:
|
|
||||||
- `curl http://127.0.0.1:8080/v1/common/heartbeat`
|
|
||||||
3. AWS Redis 主从正常:
|
|
||||||
- AWS 主 `connected_slaves:1`
|
|
||||||
- `104` 从 `master_link_status:up`
|
|
||||||
4. 已确认当前应用真实配置文件:
|
|
||||||
- `/opt/ppanel/configs/ppanel.yaml`
|
|
||||||
5. 已确认当前应用重启方式:
|
|
||||||
- `cd /opt/ppanel && docker compose -f docker-compose.cloud.yml up -d ppanel-server`
|
|
||||||
|
|
||||||
## 4. 创建新 RDS
|
|
||||||
|
|
||||||
已在 AWS 控制台创建 `hifast-mysql-prod-v2`,未对原 `hifast-mysql-prod` 做高风险在线子网调整。
|
|
||||||
|
|
||||||
创建完成后先确认:
|
|
||||||
|
|
||||||
1. 新 endpoint 已分配
|
|
||||||
2. `Publicly accessible = Yes`
|
|
||||||
3. SG 为 `hifast-hk-rds-core-sg`
|
|
||||||
4. DB subnet group 为 `hifast-hk-rds-public-only-sgprep`
|
|
||||||
5. 从 `104` 可以 TCP 连通 `3306`
|
|
||||||
|
|
||||||
连通性验证:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
nc -zv <NEW_RDS_ENDPOINT> 3306
|
|
||||||
```
|
|
||||||
|
|
||||||
## 5. 导出旧主库
|
|
||||||
|
|
||||||
在一台可连旧 RDS 的机器执行:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mysqldump \
|
|
||||||
-h hifast-mysql-prod.cd6aey40m6ag.ap-east-1.rds.amazonaws.com \
|
|
||||||
-u admin \
|
|
||||||
-p \
|
|
||||||
--single-transaction \
|
|
||||||
--routines \
|
|
||||||
--triggers \
|
|
||||||
--events \
|
|
||||||
--set-gtid-purged=OFF \
|
|
||||||
hifast > hifast-full.sql
|
|
||||||
```
|
|
||||||
|
|
||||||
说明:
|
|
||||||
|
|
||||||
- 当前业务库体量约 `187 MB`
|
|
||||||
- 这条路线的成本低,且比现网主库在线改子网更稳
|
|
||||||
|
|
||||||
## 6. 初始化新 RDS
|
|
||||||
|
|
||||||
先连接新 RDS:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mysql -h <NEW_RDS_ENDPOINT> -u admin -p
|
|
||||||
```
|
|
||||||
|
|
||||||
建议先执行:
|
|
||||||
|
|
||||||
```sql
|
|
||||||
CALL mysql.rds_set_configuration('binlog retention hours', 24);
|
|
||||||
|
|
||||||
CREATE USER IF NOT EXISTS 'repl'@'104.238.220.230' IDENTIFIED BY '<REPL_PASSWORD>';
|
|
||||||
GRANT REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'repl'@'104.238.220.230';
|
|
||||||
FLUSH PRIVILEGES;
|
|
||||||
```
|
|
||||||
|
|
||||||
如果库是空的,再导入:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mysql -h <NEW_RDS_ENDPOINT> -u admin -p hifast < hifast-full.sql
|
|
||||||
```
|
|
||||||
|
|
||||||
导入完成后确认:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mysql -h <NEW_RDS_ENDPOINT> -u admin -p -e "USE hifast; SHOW TABLES;"
|
|
||||||
```
|
|
||||||
|
|
||||||
## 7. 切换 AWS 应用到新 RDS
|
|
||||||
|
|
||||||
### 7.1 备份当前配置
|
|
||||||
|
|
||||||
在 AWS app EC2:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cp /opt/ppanel/configs/ppanel.yaml /opt/ppanel/configs/ppanel.yaml.bak.$(date +%Y%m%d%H%M%S)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 7.2 修改数据库地址
|
|
||||||
|
|
||||||
编辑:
|
|
||||||
|
|
||||||
- `/opt/ppanel/configs/ppanel.yaml`
|
|
||||||
|
|
||||||
把:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
MySQL:
|
|
||||||
Addr: hifast-mysql-prod.cd6aey40m6ag.ap-east-1.rds.amazonaws.com:3306
|
|
||||||
```
|
|
||||||
|
|
||||||
改成:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
MySQL:
|
|
||||||
Addr: hifast-mysql-prod-v2.cd6aey40m6ag.ap-east-1.rds.amazonaws.com:3306
|
|
||||||
```
|
|
||||||
|
|
||||||
### 7.3 重启业务容器
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /opt/ppanel
|
|
||||||
docker compose -f docker-compose.cloud.yml up -d ppanel-server
|
|
||||||
```
|
|
||||||
|
|
||||||
### 7.4 应用验收
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -sf http://127.0.0.1:8080/v1/common/heartbeat
|
|
||||||
docker compose -f /opt/ppanel/docker-compose.cloud.yml logs --tail=100 ppanel-server
|
|
||||||
```
|
|
||||||
|
|
||||||
重点确认:
|
|
||||||
|
|
||||||
- 心跳返回 `200`
|
|
||||||
- 日志中无 MySQL 连接异常
|
|
||||||
|
|
||||||
## 8. 改挂 `104` 从库到新 RDS
|
|
||||||
|
|
||||||
先在新 RDS 获取位点:
|
|
||||||
|
|
||||||
```sql
|
|
||||||
SHOW MASTER STATUS;
|
|
||||||
```
|
|
||||||
|
|
||||||
然后在 `104` 执行:
|
|
||||||
|
|
||||||
```sql
|
|
||||||
STOP REPLICA;
|
|
||||||
RESET REPLICA ALL;
|
|
||||||
CHANGE REPLICATION SOURCE TO
|
|
||||||
SOURCE_HOST='hifast-mysql-prod-v2.cd6aey40m6ag.ap-east-1.rds.amazonaws.com',
|
|
||||||
SOURCE_PORT=3306,
|
|
||||||
SOURCE_USER='repl',
|
|
||||||
SOURCE_PASSWORD='<REPL_PASSWORD>',
|
|
||||||
SOURCE_LOG_FILE='<MASTER_LOG_FILE>',
|
|
||||||
SOURCE_LOG_POS=<MASTER_LOG_POS>,
|
|
||||||
SOURCE_SSL=1;
|
|
||||||
START REPLICA;
|
|
||||||
SHOW REPLICA STATUS\G
|
|
||||||
```
|
|
||||||
|
|
||||||
验收标准:
|
|
||||||
|
|
||||||
- `Replica_IO_Running: Yes`
|
|
||||||
- `Replica_SQL_Running: Yes`
|
|
||||||
- `Seconds_Behind_Source: 0`
|
|
||||||
|
|
||||||
## 9. 切换后验收
|
|
||||||
|
|
||||||
至少做下面这些检查:
|
|
||||||
|
|
||||||
1. AWS 应用心跳正常
|
|
||||||
2. 管理后台可登录
|
|
||||||
3. 新建一条测试数据后,主库可见
|
|
||||||
4. `104` 从库能同步到该测试数据
|
|
||||||
5. Navicat 可从白名单来源连接新 RDS
|
|
||||||
6. Redis 主从仍正常,不受本次 MySQL 切换影响
|
|
||||||
|
|
||||||
推荐额外核对:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose -f /opt/ppanel/docker-compose.cloud.yml ps
|
|
||||||
docker exec hifast-redis redis-cli -a '<REDIS_PASSWORD>' INFO replication
|
|
||||||
```
|
|
||||||
|
|
||||||
## 10. 回滚方案
|
|
||||||
|
|
||||||
如果切换后 AWS 应用异常:
|
|
||||||
|
|
||||||
1. 立刻把 `/opt/ppanel/configs/ppanel.yaml` 中 `MySQL.Addr` 改回旧 endpoint
|
|
||||||
2. 重启 `ppanel-server`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /opt/ppanel
|
|
||||||
docker compose -f docker-compose.cloud.yml up -d ppanel-server
|
|
||||||
```
|
|
||||||
|
|
||||||
3. 优先恢复主生产可用
|
|
||||||
4. `104` 是否回切旧 RDS 复制,视恢复窗口决定
|
|
||||||
|
|
||||||
如果只是 `104` 复制改挂失败,但 AWS 应用已正常使用新 RDS:
|
|
||||||
|
|
||||||
- 不必立刻回滚应用
|
|
||||||
- 先修好 `104 -> 新 RDS` 的白名单、位点、用户或网络
|
|
||||||
|
|
||||||
## 11. 切换完成后的收尾
|
|
||||||
|
|
||||||
全部验收通过后再做:
|
|
||||||
|
|
||||||
1. 已将新 endpoint 记录到正式运维文档
|
|
||||||
2. 仍需根据回收窗口安排旧 RDS 下线
|
|
||||||
3. 仍需按网络收敛计划处理 `private-1b` 路由语义
|
|
||||||
4. 已更新外部复制 runbook 中的主库 endpoint
|
|
||||||
|
|
||||||
## 12. 当前建议的执行顺序
|
|
||||||
|
|
||||||
本次切换当时按以下顺序执行:
|
|
||||||
|
|
||||||
1. 创建 `hifast-mysql-prod-v2`
|
|
||||||
2. 验证新 RDS 网络与参数
|
|
||||||
3. 导出旧库
|
|
||||||
4. 导入新库
|
|
||||||
5. 创建 / 确认 `repl` 用户
|
|
||||||
6. 切 AWS 应用到新 RDS
|
|
||||||
7. 验证应用
|
|
||||||
8. 改挂 `104` 到新 RDS
|
|
||||||
9. 验证从库
|
|
||||||
10. 收尾,并保留旧 RDS 待后续下线
|
|
||||||
@@ -1,311 +0,0 @@
|
|||||||
# Hifast S3 备份集成 Runbook
|
|
||||||
|
|
||||||
本文档记录 `2026-05-13` 已经实际确认和完成的 S3 备份集成状态,以及后续如何把它和当前 `AWS 主生产 + 104 外部备用` 架构结合起来。
|
|
||||||
|
|
||||||
## 1. 当前已完成状态
|
|
||||||
|
|
||||||
- 已创建 S3 备份桶:`hifast-prod-backups-200810848252-ap-east-1`
|
|
||||||
- Region: `ap-east-1`
|
|
||||||
- Public access: blocked
|
|
||||||
- Versioning: enabled
|
|
||||||
- 已确认 RDS 主库 `hifast-mysql-prod-v2` 自动备份已启用:
|
|
||||||
- retention: `7 days`
|
|
||||||
- `2026-05-13` 已有自动快照
|
|
||||||
- 已确认 `104.238.220.230` MySQL 外部从库健康,可作为逻辑备份源:
|
|
||||||
- `Replica_IO_Running: Yes`
|
|
||||||
- `Replica_SQL_Running: Yes`
|
|
||||||
- `Seconds_Behind_Source: 0`
|
|
||||||
- 已确认 `104.238.220.230` Redis 从库健康,可作为 RDB 备份源:
|
|
||||||
- `role:slave`
|
|
||||||
- `master_link_status:up`
|
|
||||||
|
|
||||||
## 2. 推荐组合方案
|
|
||||||
|
|
||||||
当前最稳的做法不是只依赖一种备份,而是保留两层:
|
|
||||||
|
|
||||||
1. `RDS automated backup`
|
|
||||||
2. `104` 从库导出的逻辑备份上传到 S3
|
|
||||||
|
|
||||||
原因:
|
|
||||||
|
|
||||||
- RDS 自动备份适合快速恢复整库
|
|
||||||
- S3 逻辑备份适合独立下载、跨环境恢复、长期归档
|
|
||||||
- 从 `104` 导出 MySQL / Redis 备份,对 AWS 主生产扰动最小
|
|
||||||
|
|
||||||
## 3. 仓库已补充内容
|
|
||||||
|
|
||||||
- MySQL 备份脚本:
|
|
||||||
- [`deploy/scripts/mysql_backup_to_s3.sh`](/Users/Apple/code_vpn/vpn/ppanel-server/deploy/scripts/mysql_backup_to_s3.sh)
|
|
||||||
- Redis RDB 备份脚本:
|
|
||||||
- [`deploy/scripts/redis_rdb_backup_to_s3.sh`](/Users/Apple/code_vpn/vpn/ppanel-server/deploy/scripts/redis_rdb_backup_to_s3.sh)
|
|
||||||
- 环境变量模板:
|
|
||||||
- [`deploy/aws/ap-east-1/configs/backup-to-s3.env.example`](/Users/Apple/code_vpn/vpn/ppanel-server/deploy/aws/ap-east-1/configs/backup-to-s3.env.example)
|
|
||||||
- 主从切换环境变量模板:
|
|
||||||
- [`deploy/aws/ap-east-1/configs/replica-ops.env.example`](/Users/Apple/code_vpn/vpn/ppanel-server/deploy/aws/ap-east-1/configs/replica-ops.env.example)
|
|
||||||
- 数据迁移交互工具:
|
|
||||||
- [`deploy/scripts/hifast_data_sync_tool.sh`](/Users/Apple/code_vpn/vpn/ppanel-server/deploy/scripts/hifast_data_sync_tool.sh)
|
|
||||||
- 主从运维交互工具:
|
|
||||||
- 统一入口仍使用 [`deploy/scripts/hifast_data_sync_tool.sh`](/Users/Apple/code_vpn/vpn/ppanel-server/deploy/scripts/hifast_data_sync_tool.sh)
|
|
||||||
|
|
||||||
## 4. 建议部署位置
|
|
||||||
|
|
||||||
### 4.1 MySQL
|
|
||||||
|
|
||||||
优先部署在 `104.238.220.230`:
|
|
||||||
|
|
||||||
- 直接从本地 MySQL 从库导出
|
|
||||||
- 不占用 AWS RDS 主库的备份窗口
|
|
||||||
- 故障时备份和备用节点仍在同一台机器上
|
|
||||||
|
|
||||||
### 4.2 Redis
|
|
||||||
|
|
||||||
优先部署在 `104.238.220.230`:
|
|
||||||
|
|
||||||
- 直接从 Redis 从库导出 `RDB`
|
|
||||||
- 不影响 AWS Redis 主库对外服务
|
|
||||||
|
|
||||||
## 5. 104 当前还缺的东西
|
|
||||||
|
|
||||||
截至 `2026-05-13`,`104` 已具备:
|
|
||||||
|
|
||||||
- `mysql`
|
|
||||||
- `mysqldump`
|
|
||||||
- `gzip`
|
|
||||||
|
|
||||||
但仍缺:
|
|
||||||
|
|
||||||
- `aws cli`
|
|
||||||
- 一套最小权限的 S3 上传凭证
|
|
||||||
|
|
||||||
## 6. 推荐权限模型
|
|
||||||
|
|
||||||
当前最实用的选择有两种:
|
|
||||||
|
|
||||||
1. `104` 使用专用 IAM 用户的最小权限 `AK/SK`
|
|
||||||
2. 未来如果备份转到 AWS EC2,再改为 `EC2 Instance Role`
|
|
||||||
|
|
||||||
按现有拓扑,推荐先走第 1 种,因为 MySQL 和 Redis 的备份源都在 `104`。
|
|
||||||
|
|
||||||
### 6.1 最小权限策略示例
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"Version": "2012-10-17",
|
|
||||||
"Statement": [
|
|
||||||
{
|
|
||||||
"Sid": "ListBackupBucket",
|
|
||||||
"Effect": "Allow",
|
|
||||||
"Action": [
|
|
||||||
"s3:ListBucket"
|
|
||||||
],
|
|
||||||
"Resource": "arn:aws:s3:::hifast-prod-backups-200810848252-ap-east-1"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Sid": "WriteBackupObjects",
|
|
||||||
"Effect": "Allow",
|
|
||||||
"Action": [
|
|
||||||
"s3:PutObject",
|
|
||||||
"s3:AbortMultipartUpload"
|
|
||||||
],
|
|
||||||
"Resource": "arn:aws:s3:::hifast-prod-backups-200810848252-ap-east-1/*"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
说明:
|
|
||||||
|
|
||||||
- 不建议给 `s3:*`
|
|
||||||
- 不建议把 AK/SK 写进 repo
|
|
||||||
- 建议只写入 `104` 本机的 `/root/.aws/credentials`
|
|
||||||
|
|
||||||
## 7. 104 安装与配置步骤
|
|
||||||
|
|
||||||
### 7.1 安装 `aws cli`
|
|
||||||
|
|
||||||
```bash
|
|
||||||
apt-get update
|
|
||||||
apt-get install -y awscli
|
|
||||||
```
|
|
||||||
|
|
||||||
### 7.2 写入 AWS 凭证
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mkdir -p /root/.aws
|
|
||||||
chmod 700 /root/.aws
|
|
||||||
cat >/root/.aws/credentials <<'EOF'
|
|
||||||
[default]
|
|
||||||
aws_access_key_id=CHANGE_ME
|
|
||||||
aws_secret_access_key=CHANGE_ME
|
|
||||||
EOF
|
|
||||||
chmod 600 /root/.aws/credentials
|
|
||||||
```
|
|
||||||
|
|
||||||
### 7.3 准备环境变量文件
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cp deploy/aws/ap-east-1/configs/backup-to-s3.env.example /root/backup-to-s3.env
|
|
||||||
chmod 600 /root/backup-to-s3.env
|
|
||||||
```
|
|
||||||
|
|
||||||
把下面这些值改成真实值:
|
|
||||||
|
|
||||||
- `MYSQL_PASSWORD`
|
|
||||||
- `REDIS_PASSWORD`
|
|
||||||
- `HOST_TAG`
|
|
||||||
|
|
||||||
如果 MySQL 就跑在 `104` 本机,可保持:
|
|
||||||
|
|
||||||
- `MYSQL_HOST=127.0.0.1`
|
|
||||||
|
|
||||||
如果 Redis 就跑在 `104` 本机,可保持:
|
|
||||||
|
|
||||||
- `REDIS_HOST=127.0.0.1`
|
|
||||||
|
|
||||||
## 8. 手工执行方式
|
|
||||||
|
|
||||||
### 8.1 MySQL 逻辑备份
|
|
||||||
|
|
||||||
```bash
|
|
||||||
set -a
|
|
||||||
source /root/backup-to-s3.env
|
|
||||||
set +a
|
|
||||||
|
|
||||||
bash deploy/scripts/mysql_backup_to_s3.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
默认产物:
|
|
||||||
|
|
||||||
- 本地:`/var/backups/hifast/*.sql.gz`
|
|
||||||
- S3:`s3://hifast-prod-backups-200810848252-ap-east-1/mysql/<HOST_TAG>/`
|
|
||||||
|
|
||||||
### 8.2 Redis RDB 备份
|
|
||||||
|
|
||||||
```bash
|
|
||||||
set -a
|
|
||||||
source /root/backup-to-s3.env
|
|
||||||
set +a
|
|
||||||
|
|
||||||
S3_PREFIX=redis bash deploy/scripts/redis_rdb_backup_to_s3.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
默认产物:
|
|
||||||
|
|
||||||
- 本地:`/var/backups/hifast/*.rdb.gz`
|
|
||||||
- S3:`s3://hifast-prod-backups-200810848252-ap-east-1/redis/<HOST_TAG>/`
|
|
||||||
|
|
||||||
## 9. 定时任务建议
|
|
||||||
|
|
||||||
### 9.1 MySQL 每天凌晨执行
|
|
||||||
|
|
||||||
```cron
|
|
||||||
15 3 * * * . /root/backup-to-s3.env && /bin/bash /opt/ppanel/deploy/scripts/mysql_backup_to_s3.sh >>/var/log/hifast-mysql-backup.log 2>&1
|
|
||||||
```
|
|
||||||
|
|
||||||
### 9.2 Redis 每天凌晨执行
|
|
||||||
|
|
||||||
```cron
|
|
||||||
45 3 * * * . /root/backup-to-s3.env && S3_PREFIX=redis /bin/bash /opt/ppanel/deploy/scripts/redis_rdb_backup_to_s3.sh >>/var/log/hifast-redis-backup.log 2>&1
|
|
||||||
```
|
|
||||||
|
|
||||||
说明:
|
|
||||||
|
|
||||||
- MySQL 和 Redis 建议错峰执行
|
|
||||||
- 本地临时文件默认只保留 `3` 天
|
|
||||||
- 长期保留建议通过 S3 Lifecycle 管理,而不是靠本机 cron 删除
|
|
||||||
|
|
||||||
### 9.3 MySQL 每 10 分钟执行一次
|
|
||||||
|
|
||||||
如果你要在 `104` 上做高频逻辑备份,仓库里已经补了 `systemd timer` 安装脚本:
|
|
||||||
|
|
||||||
- 安装脚本:
|
|
||||||
- [`deploy/scripts/install_mysql_backup_timer.sh`](/Users/Apple/code_vpn/vpn/ppanel-server/deploy/scripts/install_mysql_backup_timer.sh)
|
|
||||||
- systemd service:
|
|
||||||
- [`deploy/systemd/hifast-mysql-backup.service`](/Users/Apple/code_vpn/vpn/ppanel-server/deploy/systemd/hifast-mysql-backup.service)
|
|
||||||
- systemd timer:
|
|
||||||
- [`deploy/systemd/hifast-mysql-backup.timer`](/Users/Apple/code_vpn/vpn/ppanel-server/deploy/systemd/hifast-mysql-backup.timer)
|
|
||||||
|
|
||||||
安装方式:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cp deploy/aws/ap-northeast-1/configs/backup-to-s3.env.example /root/backup-to-s3.env
|
|
||||||
chmod 600 /root/backup-to-s3.env
|
|
||||||
vim /root/backup-to-s3.env
|
|
||||||
|
|
||||||
bash deploy/scripts/install_mysql_backup_timer.sh /opt/ppanel /root/backup-to-s3.env
|
|
||||||
```
|
|
||||||
|
|
||||||
查看状态:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
systemctl list-timers --all | grep hifast-mysql-backup
|
|
||||||
systemctl status hifast-mysql-backup.timer --no-pager -l
|
|
||||||
systemctl status hifast-mysql-backup.service --no-pager -l
|
|
||||||
journalctl -u hifast-mysql-backup.service -n 50 --no-pager
|
|
||||||
```
|
|
||||||
|
|
||||||
补充说明:
|
|
||||||
|
|
||||||
- 定时表达式是 `OnCalendar=*:0/10`,即每 `10` 分钟执行一次
|
|
||||||
- 备份脚本已加 `flock` 锁,上一轮未结束时,下一轮会自动跳过,不会并发打包
|
|
||||||
- 高频逻辑备份会持续产生 `mysqldump` 开销,建议只在 `104` 这样的从库或备用库上执行,不要直接打主库
|
|
||||||
|
|
||||||
## 10. 恢复思路
|
|
||||||
|
|
||||||
### 10.1 MySQL
|
|
||||||
|
|
||||||
1. 从 S3 下载目标 `sql.gz`
|
|
||||||
2. 校验 `.sha256`
|
|
||||||
3. 解压
|
|
||||||
4. 导入目标 MySQL
|
|
||||||
|
|
||||||
示例:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
aws s3 cp s3://hifast-prod-backups-200810848252-ap-east-1/mysql/104-standby/20260513T120000Z_104-standby_hifast.sql.gz .
|
|
||||||
aws s3 cp s3://hifast-prod-backups-200810848252-ap-east-1/mysql/104-standby/20260513T120000Z_104-standby_hifast.sql.gz.sha256 .
|
|
||||||
sha256sum -c 20260513T120000Z_104-standby_hifast.sql.gz.sha256
|
|
||||||
gunzip -c 20260513T120000Z_104-standby_hifast.sql.gz | mysql -h <target-host> -u <user> -p
|
|
||||||
```
|
|
||||||
|
|
||||||
### 10.2 Redis
|
|
||||||
|
|
||||||
1. 从 S3 下载目标 `rdb.gz`
|
|
||||||
2. 校验 `.sha256`
|
|
||||||
3. 解压得到 `dump.rdb`
|
|
||||||
4. 在维护窗口内替换 Redis 数据文件后重启
|
|
||||||
|
|
||||||
## 11. 当前推荐落地顺序
|
|
||||||
|
|
||||||
1. 保持当前 `RDS automated backup` 不变
|
|
||||||
2. 在 `104` 安装 `aws cli`
|
|
||||||
3. 创建最小权限 S3 上传凭证并仅保存到 `104`
|
|
||||||
4. 先手工执行一轮 MySQL 备份上传
|
|
||||||
5. 验证 S3 对象、校验文件、恢复可读性
|
|
||||||
6. 再补 Redis RDB 备份
|
|
||||||
7. 最后加 cron 和 S3 Lifecycle
|
|
||||||
|
|
||||||
## 12. 104 上的交互式运维脚本
|
|
||||||
|
|
||||||
为了减少手工敲命令,仓库里现在统一使用一套交互式总入口。
|
|
||||||
|
|
||||||
### 12.1 统一入口
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bash deploy/scripts/hifast_data_sync_tool.sh /root/replica-ops.env
|
|
||||||
```
|
|
||||||
|
|
||||||
菜单支持:
|
|
||||||
|
|
||||||
- 备份 MySQL 到 S3
|
|
||||||
- 备份 Redis 到 S3
|
|
||||||
- 导出 MySQL dump
|
|
||||||
- 导入 MySQL dump 到 AWS RDS
|
|
||||||
- 导出后直接导入
|
|
||||||
- 导出 Redis RDB
|
|
||||||
- 导入 Redis RDB 到 Docker / 宿主机 Redis
|
|
||||||
- 查看 MySQL / Redis 当前主从状态
|
|
||||||
- 强制重拉 MySQL 主从
|
|
||||||
- 强制重拉 Redis 主从
|
|
||||||
- 把 MySQL 从库提升为可写主库
|
|
||||||
- 把 Redis 从库提升为主库
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
global:
|
|
||||||
scrape_interval: 15s
|
|
||||||
evaluation_interval: 15s
|
|
||||||
|
|
||||||
scrape_configs:
|
|
||||||
- job_name: prometheus
|
|
||||||
static_configs:
|
|
||||||
- targets:
|
|
||||||
- prometheus:9090
|
|
||||||
|
|
||||||
- job_name: grafana
|
|
||||||
metrics_path: /metrics
|
|
||||||
static_configs:
|
|
||||||
- targets:
|
|
||||||
- grafana:3000
|
|
||||||
|
|
||||||
- job_name: node-exporter
|
|
||||||
static_configs:
|
|
||||||
- targets:
|
|
||||||
- node-exporter:9100
|
|
||||||
|
|
||||||
- job_name: cadvisor
|
|
||||||
static_configs:
|
|
||||||
- targets:
|
|
||||||
- cadvisor:8080
|
|
||||||
|
|
||||||
- job_name: nginx-exporter
|
|
||||||
static_configs:
|
|
||||||
- targets:
|
|
||||||
- nginx-exporter:9113
|
|
||||||
|
|
||||||
- job_name: loki
|
|
||||||
static_configs:
|
|
||||||
- targets:
|
|
||||||
- loki:3100
|
|
||||||
|
|
||||||
- job_name: tempo
|
|
||||||
static_configs:
|
|
||||||
- targets:
|
|
||||||
- tempo:3200
|
|
||||||
@@ -1,229 +0,0 @@
|
|||||||
//go:build ignore
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"sort"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type remoteOrder struct {
|
|
||||||
OrderNo string `json:"order_no"`
|
|
||||||
OutOrderNo string `json:"out_order_no"`
|
|
||||||
OrderMoney json.Number `json:"order_money"`
|
|
||||||
OrderStatus string `json:"order_status"`
|
|
||||||
PaymentTime *int64 `json:"payment_time"`
|
|
||||||
CreateTime int64 `json:"create_time"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type recoveryFile struct {
|
|
||||||
Orders []recoveryOrder `json:"orders"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type recoveryOrder struct {
|
|
||||||
OutOrderNo string `json:"out_order_no"`
|
|
||||||
OrderNo string `json:"order_no"`
|
|
||||||
OrderStatus string `json:"order_status"`
|
|
||||||
SubscribeId int64 `json:"subscribe_id"`
|
|
||||||
ExpireAt int64 `json:"expire_at"`
|
|
||||||
Quantity int64 `json:"quantity"`
|
|
||||||
PaidAt int64 `json:"paid_at,omitempty"`
|
|
||||||
Note string `json:"note,omitempty"`
|
|
||||||
PaymentOrderNo string `json:"payment_order_no,omitempty"`
|
|
||||||
OrderMoneyCents int64 `json:"order_money_cents,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type adminOrderResponse struct {
|
|
||||||
Code uint32 `json:"code"`
|
|
||||||
Msg string `json:"msg"`
|
|
||||||
Data struct {
|
|
||||||
Total int64 `json:"total"`
|
|
||||||
List []adminOrder `json:"list"`
|
|
||||||
} `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type adminOrder struct {
|
|
||||||
OrderNo string `json:"order_no"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
var (
|
|
||||||
input string
|
|
||||||
output string
|
|
||||||
adminOrders string
|
|
||||||
subscribeID int64
|
|
||||||
onlyMissing bool
|
|
||||||
)
|
|
||||||
flag.StringVar(&input, "in", "aaa.json", "input payment platform orders JSON")
|
|
||||||
flag.StringVar(&output, "out", "internal/recovery/recovery_orders.json", "output recovery orders JSON")
|
|
||||||
flag.StringVar(&adminOrders, "admin-orders", "", "optional admin order list response JSON; when provided, matched out_order_no values are skipped")
|
|
||||||
flag.Int64Var(&subscribeID, "subscribe-id", 1, "subscribe id to use for recovered subscriptions")
|
|
||||||
flag.BoolVar(&onlyMissing, "only-missing", true, "only output payment success orders missing from admin order list when -admin-orders is set")
|
|
||||||
flag.Parse()
|
|
||||||
|
|
||||||
orders, err := readOrders(input)
|
|
||||||
must(err)
|
|
||||||
existingOrders := map[string]struct{}{}
|
|
||||||
if adminOrders != "" {
|
|
||||||
existingOrders, err = readAdminOrderSet(adminOrders)
|
|
||||||
must(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
daysByAmount := map[int64]int64{
|
|
||||||
1953: 7,
|
|
||||||
4193: 30,
|
|
||||||
9093: 90,
|
|
||||||
31500: 365,
|
|
||||||
}
|
|
||||||
|
|
||||||
out := recoveryFile{Orders: make([]recoveryOrder, 0, len(orders))}
|
|
||||||
var skipped []string
|
|
||||||
var matchedExisting int
|
|
||||||
seen := make(map[string]struct{})
|
|
||||||
for _, item := range orders {
|
|
||||||
if strings.ToLower(strings.TrimSpace(item.OrderStatus)) != "success" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
localOrderNo := strings.TrimSpace(item.OutOrderNo)
|
|
||||||
if localOrderNo == "" {
|
|
||||||
skipped = append(skipped, fmt.Sprintf("%s missing out_order_no", item.OrderNo))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if _, ok := seen[localOrderNo]; ok {
|
|
||||||
skipped = append(skipped, fmt.Sprintf("%s duplicate out_order_no", localOrderNo))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if _, exists := existingOrders[localOrderNo]; exists && onlyMissing {
|
|
||||||
matchedExisting++
|
|
||||||
seen[localOrderNo] = struct{}{}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
amount := decimalYuanToCents(item.OrderMoney)
|
|
||||||
days, ok := daysByAmount[amount]
|
|
||||||
if !ok {
|
|
||||||
skipped = append(skipped, fmt.Sprintf("%s unknown amount %s", localOrderNo, item.OrderMoney.String()))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
base := item.CreateTime
|
|
||||||
if item.PaymentTime != nil && *item.PaymentTime > 0 {
|
|
||||||
base = *item.PaymentTime
|
|
||||||
}
|
|
||||||
if base <= 0 {
|
|
||||||
skipped = append(skipped, fmt.Sprintf("%s missing payment/create time", localOrderNo))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
seen[localOrderNo] = struct{}{}
|
|
||||||
out.Orders = append(out.Orders, recoveryOrder{
|
|
||||||
OutOrderNo: localOrderNo,
|
|
||||||
OrderNo: strings.TrimSpace(item.OrderNo),
|
|
||||||
OrderStatus: "success",
|
|
||||||
SubscribeId: subscribeID,
|
|
||||||
ExpireAt: time.Unix(base, 0).Add(time.Duration(days) * 24 * time.Hour).UnixMilli(),
|
|
||||||
Quantity: days,
|
|
||||||
PaidAt: base,
|
|
||||||
Note: "data recovery",
|
|
||||||
PaymentOrderNo: strings.TrimSpace(item.OrderNo),
|
|
||||||
OrderMoneyCents: amount,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Slice(out.Orders, func(i, j int) bool {
|
|
||||||
return out.Orders[i].OutOrderNo < out.Orders[j].OutOrderNo
|
|
||||||
})
|
|
||||||
|
|
||||||
must(writeJSON(output, out))
|
|
||||||
fmt.Printf("converted recovery orders=%d matched_existing=%d skipped=%d output=%s\n", len(out.Orders), matchedExisting, len(skipped), output)
|
|
||||||
for _, msg := range skipped {
|
|
||||||
fmt.Printf("skip: %s\n", msg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func readAdminOrderSet(path string) (map[string]struct{}, error) {
|
|
||||||
data, err := os.ReadFile(path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var resp adminOrderResponse
|
|
||||||
if err := json.Unmarshal(data, &resp); err == nil && resp.Data.List != nil {
|
|
||||||
if resp.Code != 0 && resp.Code != 200 {
|
|
||||||
return nil, fmt.Errorf("admin order response failed: code=%d msg=%s", resp.Code, resp.Msg)
|
|
||||||
}
|
|
||||||
return adminOrdersToSet(resp.Data.List), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var list []adminOrder
|
|
||||||
if err := json.Unmarshal(data, &list); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return adminOrdersToSet(list), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func adminOrdersToSet(list []adminOrder) map[string]struct{} {
|
|
||||||
set := make(map[string]struct{}, len(list))
|
|
||||||
for _, item := range list {
|
|
||||||
orderNo := strings.TrimSpace(item.OrderNo)
|
|
||||||
if orderNo != "" {
|
|
||||||
set[orderNo] = struct{}{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return set
|
|
||||||
}
|
|
||||||
|
|
||||||
func readOrders(path string) ([]remoteOrder, error) {
|
|
||||||
data, err := os.ReadFile(path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var orders []remoteOrder
|
|
||||||
if err := json.Unmarshal(data, &orders); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return orders, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeJSON(path string, data recoveryFile) error {
|
|
||||||
bytes, err := json.MarshalIndent(data, "", " ")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
bytes = append(bytes, '\n')
|
|
||||||
return os.WriteFile(path, bytes, 0644)
|
|
||||||
}
|
|
||||||
|
|
||||||
func decimalYuanToCents(n json.Number) int64 {
|
|
||||||
s := strings.TrimSpace(n.String())
|
|
||||||
if s == "" {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
parts := strings.SplitN(s, ".", 2)
|
|
||||||
yuan, _ := strconv.ParseInt(parts[0], 10, 64)
|
|
||||||
cents := yuan * 100
|
|
||||||
if len(parts) == 1 {
|
|
||||||
return cents
|
|
||||||
}
|
|
||||||
frac := parts[1]
|
|
||||||
if len(frac) > 2 {
|
|
||||||
frac = frac[:2]
|
|
||||||
}
|
|
||||||
for len(frac) < 2 {
|
|
||||||
frac += "0"
|
|
||||||
}
|
|
||||||
f, _ := strconv.ParseInt(frac, 10, 64)
|
|
||||||
if strings.HasPrefix(parts[0], "-") {
|
|
||||||
return cents - f
|
|
||||||
}
|
|
||||||
return cents + f
|
|
||||||
}
|
|
||||||
|
|
||||||
func must(err error) {
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintln(os.Stderr, err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,710 +0,0 @@
|
|||||||
//go:build ignore
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"crypto/md5"
|
|
||||||
"encoding/hex"
|
|
||||||
"encoding/json"
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
"sort"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/hibiken/asynq"
|
|
||||||
"github.com/perfect-panel/server/internal/config"
|
|
||||||
"github.com/perfect-panel/server/internal/model/order"
|
|
||||||
"github.com/perfect-panel/server/internal/model/payment"
|
|
||||||
"github.com/perfect-panel/server/internal/model/subscribe"
|
|
||||||
"github.com/perfect-panel/server/internal/model/user"
|
|
||||||
"github.com/perfect-panel/server/pkg/conf"
|
|
||||||
"github.com/perfect-panel/server/pkg/orm"
|
|
||||||
"github.com/perfect-panel/server/pkg/tool"
|
|
||||||
queueTypes "github.com/perfect-panel/server/queue/types"
|
|
||||||
"github.com/redis/go-redis/v9"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
defaultBaseURL = "https://gfiuseui.lkfezg.cn"
|
|
||||||
|
|
||||||
// Hard-code Mihapay V2 credentials here when this reconciliation script should
|
|
||||||
// not depend on payment.config. Leave them empty to read pid/key/url from DB.
|
|
||||||
hardcodedMerchantID = "80567"
|
|
||||||
hardcodedMerchantKey = "475951745395f05a3be2ba7ca8235ba3"
|
|
||||||
hardcodedBaseURL = defaultBaseURL
|
|
||||||
)
|
|
||||||
|
|
||||||
type options struct {
|
|
||||||
configPath string
|
|
||||||
baseURL string
|
|
||||||
paymentID int64
|
|
||||||
method string
|
|
||||||
start string
|
|
||||||
end string
|
|
||||||
page int
|
|
||||||
limit int
|
|
||||||
maxPages int
|
|
||||||
apply bool
|
|
||||||
amountDiff int64
|
|
||||||
verbose bool
|
|
||||||
printNotFound bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type merchantConfig struct {
|
|
||||||
merchantID string
|
|
||||||
key string
|
|
||||||
baseURL string
|
|
||||||
}
|
|
||||||
|
|
||||||
type orderListResponse struct {
|
|
||||||
Code int `json:"code"`
|
|
||||||
Msg string `json:"msg"`
|
|
||||||
Data struct {
|
|
||||||
List []remoteOrder `json:"list"`
|
|
||||||
Total int `json:"total"`
|
|
||||||
} `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type remoteOrder struct {
|
|
||||||
OrderNo string `json:"order_no"`
|
|
||||||
OutOrderNo string `json:"out_order_no"`
|
|
||||||
OrderMoney json.Number `json:"order_money"`
|
|
||||||
UserMoney json.Number `json:"user_money"`
|
|
||||||
OrderStatus string `json:"order_status"`
|
|
||||||
NotifyStatus string `json:"notify_status"`
|
|
||||||
CreateTime int64 `json:"create_time"`
|
|
||||||
PaymentTime int64 `json:"payment_time"`
|
|
||||||
NotifyTime int64 `json:"notify_time"`
|
|
||||||
ClientIP string `json:"client_ip"`
|
|
||||||
Device string `json:"device"`
|
|
||||||
OrderRemark string `json:"order_remark"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type matchedOrder struct {
|
|
||||||
Remote remoteOrder
|
|
||||||
Local order.Order
|
|
||||||
RemoteCents int64
|
|
||||||
Audit subscriptionAudit
|
|
||||||
Action string
|
|
||||||
Reason string
|
|
||||||
PaymentAudit string
|
|
||||||
}
|
|
||||||
|
|
||||||
type subscriptionAudit struct {
|
|
||||||
Checked bool
|
|
||||||
Status string
|
|
||||||
UserSubID int64
|
|
||||||
UserID int64
|
|
||||||
StartTime time.Time
|
|
||||||
ExpireTime time.Time
|
|
||||||
ExpectedExpire time.Time
|
|
||||||
SubscribeUnit string
|
|
||||||
Quantity int64
|
|
||||||
Reason string
|
|
||||||
}
|
|
||||||
|
|
||||||
type reconcileStats struct {
|
|
||||||
RemotePages int
|
|
||||||
RemoteOrders int
|
|
||||||
RemotePaid int
|
|
||||||
RemoteMissingNo int
|
|
||||||
DuplicateRemote int
|
|
||||||
LocalNotFound int
|
|
||||||
LocalDateFiltered int
|
|
||||||
LocalMethodSkip int
|
|
||||||
Matched int
|
|
||||||
StatusCounts map[string]int
|
|
||||||
LocalNotFoundOrders []remoteOrder
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
ctx := context.Background()
|
|
||||||
opt := parseFlags()
|
|
||||||
|
|
||||||
var cfg config.Config
|
|
||||||
conf.MustLoad(opt.configPath, &cfg)
|
|
||||||
|
|
||||||
db, err := orm.ConnectMysql(orm.Mysql{Config: cfg.MySQL})
|
|
||||||
must(err, "connect mysql")
|
|
||||||
sqlDB, err := db.DB()
|
|
||||||
must(err, "get sql db")
|
|
||||||
defer sqlDB.Close()
|
|
||||||
|
|
||||||
var redisClient *redis.Client
|
|
||||||
var queueClient *asynq.Client
|
|
||||||
if opt.apply {
|
|
||||||
redisClient = redis.NewClient(&redis.Options{
|
|
||||||
Addr: cfg.Redis.Host,
|
|
||||||
Password: cfg.Redis.Pass,
|
|
||||||
DB: cfg.Redis.DB,
|
|
||||||
})
|
|
||||||
defer redisClient.Close()
|
|
||||||
|
|
||||||
queueClient = asynq.NewClient(asynq.RedisClientOpt{
|
|
||||||
Addr: cfg.Redis.Host,
|
|
||||||
Password: cfg.Redis.Pass,
|
|
||||||
DB: 5,
|
|
||||||
})
|
|
||||||
defer queueClient.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
merchant, paymentInfo, err := resolveMerchantConfig(ctx, db, opt)
|
|
||||||
must(err, "resolve mihapay config")
|
|
||||||
if opt.paymentID > 0 {
|
|
||||||
opt.method = paymentInfo.Platform
|
|
||||||
}
|
|
||||||
baseURL := opt.baseURL
|
|
||||||
if baseURL == defaultBaseURL && merchant.baseURL != "" {
|
|
||||||
baseURL = merchant.baseURL
|
|
||||||
}
|
|
||||||
|
|
||||||
client := &http.Client{Timeout: 15 * time.Second}
|
|
||||||
matches, stats, err := reconcile(ctx, client, db, redisClient, queueClient, opt, baseURL, merchant, paymentInfo)
|
|
||||||
must(err, "reconcile orders")
|
|
||||||
|
|
||||||
printSummary(matches, stats, opt)
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseFlags() options {
|
|
||||||
opt := options{}
|
|
||||||
flag.StringVar(&opt.configPath, "config", "etc/ppanel.yaml", "ppanel config path")
|
|
||||||
flag.StringVar(&opt.baseURL, "base-url", defaultBaseURL, "Mihapay base URL")
|
|
||||||
flag.Int64Var(&opt.paymentID, "payment-id", 0, "payment table id to use for merchant credentials")
|
|
||||||
flag.StringVar(&opt.method, "method", "epay", "local order method/platform to match when payment-id is not set")
|
|
||||||
flag.StringVar(&opt.start, "start", "", "local order created_at start, e.g. 2026-05-05 00:00:00")
|
|
||||||
flag.StringVar(&opt.end, "end", "", "local order created_at end, e.g. 2026-05-06 00:00:00")
|
|
||||||
flag.IntVar(&opt.page, "page", 1, "remote start page")
|
|
||||||
flag.IntVar(&opt.limit, "limit", 100, "remote page size")
|
|
||||||
flag.IntVar(&opt.maxPages, "max-pages", 50, "maximum remote pages to fetch")
|
|
||||||
flag.BoolVar(&opt.apply, "apply", false, "apply fixes; default is dry-run")
|
|
||||||
flag.Int64Var(&opt.amountDiff, "amount-diff-cents", 1, "allowed amount difference in cents")
|
|
||||||
flag.BoolVar(&opt.verbose, "v", false, "print diagnostic details")
|
|
||||||
flag.BoolVar(&opt.printNotFound, "print-not-found", false, "print remote paid orders that have no matching local order")
|
|
||||||
flag.Parse()
|
|
||||||
|
|
||||||
if opt.start == "" {
|
|
||||||
now := time.Now()
|
|
||||||
opt.start = time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()).Format("2006-01-02 15:04:05")
|
|
||||||
}
|
|
||||||
if opt.end == "" {
|
|
||||||
start, err := time.ParseInLocation("2006-01-02 15:04:05", opt.start, time.Local)
|
|
||||||
if err == nil {
|
|
||||||
opt.end = start.AddDate(0, 0, 1).Format("2006-01-02 15:04:05")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
opt.baseURL = strings.TrimRight(opt.baseURL, "/")
|
|
||||||
if opt.page <= 0 {
|
|
||||||
opt.page = 1
|
|
||||||
}
|
|
||||||
if opt.limit <= 0 || opt.limit > 500 {
|
|
||||||
opt.limit = 100
|
|
||||||
}
|
|
||||||
if opt.maxPages <= 0 {
|
|
||||||
opt.maxPages = 1
|
|
||||||
}
|
|
||||||
return opt
|
|
||||||
}
|
|
||||||
|
|
||||||
func resolveMerchantConfig(ctx context.Context, db *gorm.DB, opt options) (merchantConfig, *payment.Payment, error) {
|
|
||||||
if hardcodedMerchantID != "" && hardcodedMerchantKey != "" {
|
|
||||||
return merchantConfig{
|
|
||||||
merchantID: hardcodedMerchantID,
|
|
||||||
key: hardcodedMerchantKey,
|
|
||||||
baseURL: strings.TrimRight(hardcodedBaseURL, "/"),
|
|
||||||
}, nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var p payment.Payment
|
|
||||||
query := db.WithContext(ctx).Model(&payment.Payment{})
|
|
||||||
if opt.paymentID > 0 {
|
|
||||||
query = query.Where("id = ?", opt.paymentID)
|
|
||||||
} else {
|
|
||||||
query = query.Where("LOWER(REPLACE(platform, '_', '')) = LOWER(REPLACE(?, '_', ''))", opt.method)
|
|
||||||
}
|
|
||||||
if err := query.First(&p).Error; err != nil {
|
|
||||||
return merchantConfig{}, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var epayCfg payment.EPayConfig
|
|
||||||
if err := epayCfg.Unmarshal([]byte(p.Config)); err != nil {
|
|
||||||
return merchantConfig{}, nil, err
|
|
||||||
}
|
|
||||||
if epayCfg.Pid == "" || epayCfg.Key == "" {
|
|
||||||
return merchantConfig{}, nil, fmt.Errorf("payment id %d has empty pid/key", p.Id)
|
|
||||||
}
|
|
||||||
return merchantConfig{merchantID: epayCfg.Pid, key: epayCfg.Key, baseURL: strings.TrimRight(epayCfg.Url, "/")}, &p, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func reconcile(
|
|
||||||
ctx context.Context,
|
|
||||||
client *http.Client,
|
|
||||||
db *gorm.DB,
|
|
||||||
redisClient *redis.Client,
|
|
||||||
queueClient *asynq.Client,
|
|
||||||
opt options,
|
|
||||||
baseURL string,
|
|
||||||
merchant merchantConfig,
|
|
||||||
paymentInfo *payment.Payment,
|
|
||||||
) ([]matchedOrder, reconcileStats, error) {
|
|
||||||
var matches []matchedOrder
|
|
||||||
var stats reconcileStats
|
|
||||||
stats.StatusCounts = make(map[string]int)
|
|
||||||
seen := make(map[string]struct{})
|
|
||||||
page := opt.page
|
|
||||||
|
|
||||||
for fetchedPages := 0; fetchedPages < opt.maxPages; fetchedPages++ {
|
|
||||||
resp, err := fetchRemoteOrders(ctx, client, baseURL, merchant, page, opt.limit)
|
|
||||||
if err != nil {
|
|
||||||
return matches, stats, err
|
|
||||||
}
|
|
||||||
if resp.Code != 1 {
|
|
||||||
return matches, stats, fmt.Errorf("remote orderlist failed: code=%d msg=%s", resp.Code, resp.Msg)
|
|
||||||
}
|
|
||||||
stats.RemotePages++
|
|
||||||
stats.RemoteOrders += len(resp.Data.List)
|
|
||||||
if opt.verbose {
|
|
||||||
fmt.Printf("remote page=%d list=%d total=%d\n", page, len(resp.Data.List), resp.Data.Total)
|
|
||||||
}
|
|
||||||
if len(resp.Data.List) == 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, remote := range resp.Data.List {
|
|
||||||
status := strings.TrimSpace(remote.OrderStatus)
|
|
||||||
if status == "" {
|
|
||||||
status = "<empty>"
|
|
||||||
}
|
|
||||||
stats.StatusCounts[status]++
|
|
||||||
if !isRemotePaidStatus(remote.OrderStatus) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
stats.RemotePaid++
|
|
||||||
if remote.OutOrderNo == "" {
|
|
||||||
stats.RemoteMissingNo++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if _, ok := seen[remote.OutOrderNo]; ok {
|
|
||||||
stats.DuplicateRemote++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
seen[remote.OutOrderNo] = struct{}{}
|
|
||||||
|
|
||||||
item, ok := matchRemoteOrder(ctx, db, opt, &stats, remote)
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
stats.Matched++
|
|
||||||
matches = append(matches, item)
|
|
||||||
if opt.apply && (item.Action == "补单" || item.Action == "重投队列") {
|
|
||||||
if err := applyOrderFix(ctx, db, redisClient, queueClient, paymentInfo, item); err != nil {
|
|
||||||
return matches, stats, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
page++
|
|
||||||
if resp.Data.Total > 0 && page*opt.limit >= resp.Data.Total+opt.limit {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return matches, stats, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func isRemotePaidStatus(status string) bool {
|
|
||||||
switch strings.ToLower(strings.TrimSpace(status)) {
|
|
||||||
case "paid", "success", "succeeded", "trade_success", "completed", "complete", "1":
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func fetchRemoteOrders(ctx context.Context, client *http.Client, baseURL string, merchant merchantConfig, page, limit int) (*orderListResponse, error) {
|
|
||||||
params := map[string]string{
|
|
||||||
"merchantId": merchant.merchantID,
|
|
||||||
"page": strconv.Itoa(page),
|
|
||||||
"limit": strconv.Itoa(limit),
|
|
||||||
"order": "create_time desc",
|
|
||||||
}
|
|
||||||
params["sign"] = sign(params, merchant.key)
|
|
||||||
|
|
||||||
form := url.Values{}
|
|
||||||
for k, v := range params {
|
|
||||||
form.Set(k, v)
|
|
||||||
}
|
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, baseURL+"/api/v2/orderlist", strings.NewReader(form.Encode()))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
||||||
|
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
||||||
return nil, fmt.Errorf("remote status %d: %s", resp.StatusCode, string(body))
|
|
||||||
}
|
|
||||||
|
|
||||||
decoder := json.NewDecoder(bytes.NewReader(body))
|
|
||||||
decoder.UseNumber()
|
|
||||||
var out orderListResponse
|
|
||||||
if err := decoder.Decode(&out); err != nil {
|
|
||||||
return nil, fmt.Errorf("decode remote response: %w body=%s", err, string(body))
|
|
||||||
}
|
|
||||||
return &out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func matchRemoteOrder(ctx context.Context, db *gorm.DB, opt options, stats *reconcileStats, remote remoteOrder) (matchedOrder, bool) {
|
|
||||||
var local order.Order
|
|
||||||
if err := db.WithContext(ctx).Model(&order.Order{}).Where("order_no = ?", remote.OutOrderNo).First(&local).Error; err != nil {
|
|
||||||
stats.LocalNotFound++
|
|
||||||
stats.LocalNotFoundOrders = append(stats.LocalNotFoundOrders, remote)
|
|
||||||
if opt.verbose {
|
|
||||||
fmt.Printf("skip local_not_found out_order_no=%s remote_no=%s status=%s money=%s\n", remote.OutOrderNo, remote.OrderNo, remote.OrderStatus, remote.OrderMoney)
|
|
||||||
}
|
|
||||||
return matchedOrder{}, false
|
|
||||||
}
|
|
||||||
if opt.method != "" && local.Method != opt.method {
|
|
||||||
stats.LocalMethodSkip++
|
|
||||||
if opt.verbose {
|
|
||||||
fmt.Printf("skip method out_order_no=%s local_method=%s want=%s\n", remote.OutOrderNo, local.Method, opt.method)
|
|
||||||
}
|
|
||||||
return matchedOrder{}, false
|
|
||||||
}
|
|
||||||
if opt.start != "" && local.CreatedAt.Format("2006-01-02 15:04:05") < opt.start {
|
|
||||||
stats.LocalDateFiltered++
|
|
||||||
return matchedOrder{}, false
|
|
||||||
}
|
|
||||||
if opt.end != "" && local.CreatedAt.Format("2006-01-02 15:04:05") >= opt.end {
|
|
||||||
stats.LocalDateFiltered++
|
|
||||||
return matchedOrder{}, false
|
|
||||||
}
|
|
||||||
|
|
||||||
remoteCents := decimalYuanToCents(remote.OrderMoney)
|
|
||||||
item := matchedOrder{
|
|
||||||
Remote: remote,
|
|
||||||
Local: local,
|
|
||||||
RemoteCents: remoteCents,
|
|
||||||
PaymentAudit: "远端已支付",
|
|
||||||
}
|
|
||||||
item.Audit = auditSubscription(ctx, db, local)
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case local.Status == 5:
|
|
||||||
item.Action = "跳过"
|
|
||||||
item.Reason = "本地已完成"
|
|
||||||
case remoteCents > 0 && abs64(local.Amount-remoteCents) > opt.amountDiff:
|
|
||||||
item.Action = "跳过"
|
|
||||||
item.Reason = fmt.Sprintf("金额不匹配 本地=%d 远端=%d", local.Amount, remoteCents)
|
|
||||||
case local.Status == 2:
|
|
||||||
item.Action = "重投队列"
|
|
||||||
item.Reason = "本地已支付但未完成"
|
|
||||||
case local.Status != 1 && local.Status != 3:
|
|
||||||
item.Action = "跳过"
|
|
||||||
item.Reason = fmt.Sprintf("本地状态 %d 不适合自动补单", local.Status)
|
|
||||||
default:
|
|
||||||
item.Action = "补单"
|
|
||||||
item.Reason = "远端已支付,本地未支付/已关闭"
|
|
||||||
}
|
|
||||||
return item, true
|
|
||||||
}
|
|
||||||
|
|
||||||
func auditSubscription(ctx context.Context, db *gorm.DB, local order.Order) subscriptionAudit {
|
|
||||||
audit := subscriptionAudit{
|
|
||||||
Checked: true,
|
|
||||||
Quantity: local.Quantity,
|
|
||||||
}
|
|
||||||
if local.Type != 1 && local.Type != 2 {
|
|
||||||
audit.Status = "跳过"
|
|
||||||
audit.Reason = fmt.Sprintf("订单类型 %d 不核对订阅时间", local.Type)
|
|
||||||
return audit
|
|
||||||
}
|
|
||||||
|
|
||||||
var plan subscribe.Subscribe
|
|
||||||
if err := db.WithContext(ctx).Model(&subscribe.Subscribe{}).Where("id = ?", local.SubscribeId).First(&plan).Error; err != nil {
|
|
||||||
audit.Status = "异常"
|
|
||||||
audit.Reason = "套餐不存在: " + err.Error()
|
|
||||||
return audit
|
|
||||||
}
|
|
||||||
audit.SubscribeUnit = plan.UnitTime
|
|
||||||
|
|
||||||
var userSub user.Subscribe
|
|
||||||
err := db.WithContext(ctx).Model(&user.Subscribe{}).Where("order_id = ?", local.Id).Order("id DESC").First(&userSub).Error
|
|
||||||
if err == nil {
|
|
||||||
audit.UserSubID = userSub.Id
|
|
||||||
audit.UserID = userSub.UserId
|
|
||||||
audit.StartTime = userSub.StartTime
|
|
||||||
audit.ExpireTime = userSub.ExpireTime
|
|
||||||
audit.ExpectedExpire = userSub.ExpireTime
|
|
||||||
audit.Status = "已核对"
|
|
||||||
audit.Reason = "订阅已绑定当前订单"
|
|
||||||
return audit
|
|
||||||
}
|
|
||||||
|
|
||||||
if local.Type == 2 && local.SubscribeToken != "" {
|
|
||||||
err = db.WithContext(ctx).Model(&user.Subscribe{}).Where("token = ?", local.SubscribeToken).First(&userSub).Error
|
|
||||||
if err == nil {
|
|
||||||
base := subscriptionRenewalBaseTime(time.Now(), userSub.ExpireTime, userSub.FinishedAt)
|
|
||||||
audit.UserSubID = userSub.Id
|
|
||||||
audit.UserID = userSub.UserId
|
|
||||||
audit.StartTime = userSub.StartTime
|
|
||||||
audit.ExpireTime = userSub.ExpireTime
|
|
||||||
audit.ExpectedExpire = tool.AddTime(plan.UnitTime, local.Quantity, base)
|
|
||||||
if local.Status == 5 {
|
|
||||||
audit.Status = "异常"
|
|
||||||
audit.Reason = "订单已完成但订阅 order_id 未指向该订单"
|
|
||||||
} else {
|
|
||||||
audit.Status = "待激活"
|
|
||||||
audit.Reason = "续费订单未完成,预计激活后续期"
|
|
||||||
}
|
|
||||||
return audit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if local.Status == 5 {
|
|
||||||
audit.Status = "异常"
|
|
||||||
audit.Reason = "订单已完成但未找到对应订阅"
|
|
||||||
return audit
|
|
||||||
}
|
|
||||||
|
|
||||||
audit.Status = "待激活"
|
|
||||||
audit.ExpectedExpire = tool.AddTime(plan.UnitTime, local.Quantity, time.Now())
|
|
||||||
audit.Reason = "订单未完成,订阅时间需激活队列生成"
|
|
||||||
return audit
|
|
||||||
}
|
|
||||||
|
|
||||||
func subscriptionRenewalBaseTime(now time.Time, expire time.Time, finishedAt *time.Time) time.Time {
|
|
||||||
base := expire
|
|
||||||
if finishedAt != nil && finishedAt.After(base) {
|
|
||||||
base = *finishedAt
|
|
||||||
}
|
|
||||||
if base.Before(now) {
|
|
||||||
base = now
|
|
||||||
}
|
|
||||||
return base
|
|
||||||
}
|
|
||||||
|
|
||||||
func applyOrderFix(
|
|
||||||
ctx context.Context,
|
|
||||||
db *gorm.DB,
|
|
||||||
redisClient *redis.Client,
|
|
||||||
queueClient *asynq.Client,
|
|
||||||
paymentInfo *payment.Payment,
|
|
||||||
item matchedOrder,
|
|
||||||
) error {
|
|
||||||
return db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
||||||
updates := map[string]interface{}{
|
|
||||||
"status": 2,
|
|
||||||
"trade_no": item.Remote.OrderNo,
|
|
||||||
"updated_at": time.Now(),
|
|
||||||
}
|
|
||||||
if paymentInfo != nil {
|
|
||||||
updates["payment_id"] = paymentInfo.Id
|
|
||||||
updates["method"] = paymentInfo.Platform
|
|
||||||
}
|
|
||||||
result := tx.Model(&order.Order{}).
|
|
||||||
Where("order_no = ? AND status IN ?", item.Local.OrderNo, []uint8{1, 2, 3}).
|
|
||||||
Updates(updates)
|
|
||||||
if result.Error != nil {
|
|
||||||
return result.Error
|
|
||||||
}
|
|
||||||
if result.RowsAffected == 0 {
|
|
||||||
return fmt.Errorf("order %s was not updated; status changed concurrently", item.Local.OrderNo)
|
|
||||||
}
|
|
||||||
clearOrderCache(ctx, redisClient, item.Local)
|
|
||||||
return enqueueActivation(ctx, queueClient, item.Local.OrderNo)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func enqueueActivation(ctx context.Context, queueClient *asynq.Client, orderNo string) error {
|
|
||||||
payload, err := json.Marshal(queueTypes.ForthwithActivateOrderPayload{OrderNo: orderNo})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_, err = queueClient.EnqueueContext(ctx, asynq.NewTask(queueTypes.ForthwithActivateOrder, payload, asynq.MaxRetry(5)))
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func clearOrderCache(ctx context.Context, redisClient *redis.Client, local order.Order) {
|
|
||||||
keys := []string{
|
|
||||||
fmt.Sprintf("cache:order:id:%d", local.Id),
|
|
||||||
fmt.Sprintf("cache:order:no:%s", local.OrderNo),
|
|
||||||
}
|
|
||||||
_ = redisClient.Del(ctx, keys...).Err()
|
|
||||||
}
|
|
||||||
|
|
||||||
func sign(params map[string]string, key string) string {
|
|
||||||
keys := make([]string, 0, len(params))
|
|
||||||
for k, v := range params {
|
|
||||||
if k != "sign" && k != "sign_type" && v != "" {
|
|
||||||
keys = append(keys, k)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sort.Strings(keys)
|
|
||||||
|
|
||||||
parts := make([]string, 0, len(keys))
|
|
||||||
for _, k := range keys {
|
|
||||||
parts = append(parts, k+"="+params[k])
|
|
||||||
}
|
|
||||||
sum := md5.Sum([]byte(strings.Join(parts, "&") + key))
|
|
||||||
return hex.EncodeToString(sum[:])
|
|
||||||
}
|
|
||||||
|
|
||||||
func decimalYuanToCents(n json.Number) int64 {
|
|
||||||
s := strings.TrimSpace(n.String())
|
|
||||||
if s == "" {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
parts := strings.SplitN(s, ".", 2)
|
|
||||||
yuan, _ := strconv.ParseInt(parts[0], 10, 64)
|
|
||||||
cents := yuan * 100
|
|
||||||
if len(parts) == 1 {
|
|
||||||
return cents
|
|
||||||
}
|
|
||||||
frac := parts[1]
|
|
||||||
if len(frac) > 2 {
|
|
||||||
frac = frac[:2]
|
|
||||||
}
|
|
||||||
for len(frac) < 2 {
|
|
||||||
frac += "0"
|
|
||||||
}
|
|
||||||
f, _ := strconv.ParseInt(frac, 10, 64)
|
|
||||||
if strings.HasPrefix(parts[0], "-") {
|
|
||||||
return cents - f
|
|
||||||
}
|
|
||||||
return cents + f
|
|
||||||
}
|
|
||||||
|
|
||||||
func printSummary(matches []matchedOrder, stats reconcileStats, opt options) {
|
|
||||||
var fix, requeue, skip int
|
|
||||||
for _, item := range matches {
|
|
||||||
switch item.Action {
|
|
||||||
case "补单":
|
|
||||||
fix++
|
|
||||||
case "重投队列":
|
|
||||||
requeue++
|
|
||||||
default:
|
|
||||||
skip++
|
|
||||||
}
|
|
||||||
fmt.Printf("%s order_no=%s remote_no=%s local_status=%d amount=%d remote_amount=%d reason=%s\n",
|
|
||||||
item.Action,
|
|
||||||
item.Local.OrderNo,
|
|
||||||
item.Remote.OrderNo,
|
|
||||||
item.Local.Status,
|
|
||||||
item.Local.Amount,
|
|
||||||
item.RemoteCents,
|
|
||||||
item.Reason,
|
|
||||||
)
|
|
||||||
if item.Audit.Checked {
|
|
||||||
fmt.Printf(" payment=%s subscription=%s sub_id=%d user_id=%d expire=%s expected=%s note=%s\n",
|
|
||||||
item.PaymentAudit,
|
|
||||||
item.Audit.Status,
|
|
||||||
item.Audit.UserSubID,
|
|
||||||
item.Audit.UserID,
|
|
||||||
formatTime(item.Audit.ExpireTime),
|
|
||||||
formatTime(item.Audit.ExpectedExpire),
|
|
||||||
item.Audit.Reason,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mode := "DRY-RUN"
|
|
||||||
if opt.apply {
|
|
||||||
mode = "APPLIED"
|
|
||||||
}
|
|
||||||
fmt.Printf("\nremote pages=%d orders=%d paid=%d missing_out_order_no=%d duplicates=%d\n",
|
|
||||||
stats.RemotePages, stats.RemoteOrders, stats.RemotePaid, stats.RemoteMissingNo, stats.DuplicateRemote)
|
|
||||||
if len(stats.StatusCounts) > 0 {
|
|
||||||
keys := make([]string, 0, len(stats.StatusCounts))
|
|
||||||
for key := range stats.StatusCounts {
|
|
||||||
keys = append(keys, key)
|
|
||||||
}
|
|
||||||
sort.Strings(keys)
|
|
||||||
fmt.Print("remote status_counts=")
|
|
||||||
for i, key := range keys {
|
|
||||||
if i > 0 {
|
|
||||||
fmt.Print(", ")
|
|
||||||
}
|
|
||||||
fmt.Printf("%s:%d", key, stats.StatusCounts[key])
|
|
||||||
}
|
|
||||||
fmt.Println()
|
|
||||||
}
|
|
||||||
fmt.Printf("local not_found=%d method_skipped=%d date_filtered=%d matched=%d\n",
|
|
||||||
stats.LocalNotFound, stats.LocalMethodSkip, stats.LocalDateFiltered, stats.Matched)
|
|
||||||
if opt.printNotFound && len(stats.LocalNotFoundOrders) > 0 {
|
|
||||||
printNotFoundOrders(stats.LocalNotFoundOrders)
|
|
||||||
}
|
|
||||||
fmt.Printf("\n%s summary: matched=%d fixable=%d requeue=%d skipped=%d\n", mode, len(matches), fix, requeue, skip)
|
|
||||||
}
|
|
||||||
|
|
||||||
func printNotFoundOrders(items []remoteOrder) {
|
|
||||||
fmt.Println("\nlocal_not_found orders:")
|
|
||||||
fmt.Println("out_order_no\tremote_no\tstatus\torder_money\tuser_money\tcreate_time\tpayment_time\tclient_ip\tdevice\tremark")
|
|
||||||
for _, item := range items {
|
|
||||||
fmt.Printf("%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n",
|
|
||||||
item.OutOrderNo,
|
|
||||||
item.OrderNo,
|
|
||||||
item.OrderStatus,
|
|
||||||
item.OrderMoney.String(),
|
|
||||||
item.UserMoney.String(),
|
|
||||||
formatUnixTime(item.CreateTime),
|
|
||||||
formatUnixTime(item.PaymentTime),
|
|
||||||
oneLine(item.ClientIP),
|
|
||||||
oneLine(item.Device),
|
|
||||||
oneLine(item.OrderRemark),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func formatTime(t time.Time) string {
|
|
||||||
if t.IsZero() {
|
|
||||||
return "-"
|
|
||||||
}
|
|
||||||
return t.Format("2006-01-02 15:04:05")
|
|
||||||
}
|
|
||||||
|
|
||||||
func formatUnixTime(v int64) string {
|
|
||||||
if v <= 0 {
|
|
||||||
return "-"
|
|
||||||
}
|
|
||||||
if v > 1_000_000_000_000 {
|
|
||||||
return time.UnixMilli(v).Format("2006-01-02 15:04:05")
|
|
||||||
}
|
|
||||||
return time.Unix(v, 0).Format("2006-01-02 15:04:05")
|
|
||||||
}
|
|
||||||
|
|
||||||
func oneLine(s string) string {
|
|
||||||
s = strings.ReplaceAll(s, "\t", " ")
|
|
||||||
s = strings.ReplaceAll(s, "\r", " ")
|
|
||||||
s = strings.ReplaceAll(s, "\n", " ")
|
|
||||||
return strings.TrimSpace(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
func abs64(v int64) int64 {
|
|
||||||
if v < 0 {
|
|
||||||
return -v
|
|
||||||
}
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
func must(err error, step string) {
|
|
||||||
if err == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fmt.Fprintf(os.Stderr, "%s: %v\n", step, err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
server:
|
|
||||||
http_listen_port: 3200
|
|
||||||
grpc_listen_port: 9095
|
|
||||||
|
|
||||||
distributor:
|
|
||||||
receivers:
|
|
||||||
otlp:
|
|
||||||
protocols:
|
|
||||||
grpc:
|
|
||||||
endpoint: 0.0.0.0:4317
|
|
||||||
http:
|
|
||||||
endpoint: 0.0.0.0:4318
|
|
||||||
|
|
||||||
ingester:
|
|
||||||
max_block_duration: 5m
|
|
||||||
|
|
||||||
compactor:
|
|
||||||
compaction:
|
|
||||||
block_retention: 168h
|
|
||||||
|
|
||||||
storage:
|
|
||||||
trace:
|
|
||||||
backend: local
|
|
||||||
wal:
|
|
||||||
path: /var/tempo/wal
|
|
||||||
local:
|
|
||||||
path: /var/tempo/blocks
|
|
||||||
|
|
||||||
metrics_generator:
|
|
||||||
registry:
|
|
||||||
external_labels:
|
|
||||||
source: tempo
|
|
||||||
cluster: ppanel
|
|
||||||
storage:
|
|
||||||
path: /var/tempo/generator/wal
|
|
||||||
remote_write:
|
|
||||||
- url: http://prometheus:9090/api/v1/write
|
|
||||||
Reference in New Issue
Block a user