0428
Build docker and publish / build (20.15.1) (push) Successful in 5m25s

This commit is contained in:
2026-04-28 17:36:49 -07:00
parent ac57272018
commit 3ae85f68ea
12 changed files with 9 additions and 1077 deletions
-717
View File
@@ -1,717 +0,0 @@
# PPanel 双机部署:远程 MySQL 主从、Redis 主从/Sentinel、多实例与监控
本文档说明如何部署两台机器:
- 主机:运行主 MySQL、主 Redis、PPanel Server、Grafana、Prometheus、Loki、Tempo。
- 从机:运行 MySQL 从库、Redis 从库、Redis Sentinel、额外 PPanel Server。
- Nginx:只做 HTTP 负载均衡,不做数据库读写分离。
- MCP Grafana:安装在本地电脑,不部署到服务器,用于后续排查线上 bug。
重要原则:
- 所有 `ppanel-server` 实例都连接主 MySQL 和主 Redis。
- 从机 MySQL 只做复制/灾备,不给业务直接写入。
- 当前后端只有单个 `MySQL.Addr` 和单个 `Redis.Host`,不要让业务实例连接本机从库。
- 故障切换采用人工确认,避免自动切换造成脑裂。
## 1. 拓扑与端口
示例变量:
```text
主机公网 IP: MASTER_PUBLIC_IP
从机公网 IP: REPLICA_PUBLIC_IP
业务域名: api.example.com
数据库名: ppanel
```
主机端口:
| 端口 | 用途 | 建议暴露范围 |
| --- | --- | --- |
| `8080` | 主机 PPanel Server | Nginx 所在机器 |
| `3306` | 主 MySQL | 只允许从机 IP |
| `6379` | 主 Redis | 只允许从机 IP |
| `3333` | Grafana | 默认 `127.0.0.1`,通过 SSH 隧道 |
| `9090` | Prometheus | 默认 `127.0.0.1` |
| `4317` | Tempo OTLP | 默认 `127.0.0.1` |
从机端口:
| 端口 | 用途 | 建议暴露范围 |
| --- | --- | --- |
| `8080` | 从机 PPanel Server | Nginx 所在机器 |
| `3307` | 本机 MySQL 从库调试端口 | 默认 `127.0.0.1` |
| `6380` | 本机 Redis 从库调试端口 | 默认 `127.0.0.1` |
| `26379` | 本机 Redis Sentinel | 默认 `127.0.0.1` |
## 2. 文件准备
主机上传:
```text
docker-compose.cloud.yml
.env
configs/
mysql/
redis/
loki/
grafana/
prometheus/
tempo/
config/
```
从机上传:
```text
docker-compose.replica.yml
.env
configs/
mysql/
redis/
tempo/
cache/
```
两台机器都创建运行目录:
```bash
mkdir -p configs logs cache tempo_data mysql redis
```
主机如果使用监控 exporter,还需要准备 MySQL exporter 配置:
```bash
cp mysql/.my.cnf.example mysql/.my.cnf
```
编辑 `mysql/.my.cnf`,把密码改成真实 `MYSQL_ROOT_PASSWORD`。该文件已被 `.gitignore` 忽略,不要提交。
## 3. 主机 `.env`
在主机复制 `.env.example`
```bash
cp .env.example .env
```
主机 `.env` 示例:
```dotenv
MYSQL_ROOT_PASSWORD=CHANGE_ME_STRONG_MYSQL_ROOT_PASSWORD
MYSQL_DATABASE=ppanel
MYSQL_PUBLISH_ADDR=0.0.0.0
MYSQL_REPLICATION_USER=replicator
MYSQL_REPLICATION_PASSWORD=CHANGE_ME_STRONG_REPLICATION_PASSWORD
REDIS_PASSWORD=CHANGE_ME_STRONG_REDIS_PASSWORD
REDIS_PUBLISH_ADDR=0.0.0.0
GRAFANA_PASSWORD=CHANGE_ME_STRONG_GRAFANA_PASSWORD
PPANEL_SERVER_TAG=latest
```
如果只想让 MySQL/Redis 监听内网 IP,把 `MYSQL_PUBLISH_ADDR``REDIS_PUBLISH_ADDR` 改成内网地址。
## 4. 主机 `configs/ppanel.yaml`
主机业务实例连接本机主库和主 Redis:
```yaml
MySQL:
Addr: 127.0.0.1:3306
Username: root
Password: CHANGE_ME_STRONG_MYSQL_ROOT_PASSWORD
Dbname: ppanel
Config: charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai
MaxIdleConns: 10
MaxOpenConns: 100
SlowThreshold: 1000
Redis:
Host: 127.0.0.1:6379
Pass: CHANGE_ME_STRONG_REDIS_PASSWORD
DB: 0
```
注意:启用当前 compose 后 Redis 有密码,`Redis.Pass` 必须填写。
## 5. 主机防火墙
主机必须只允许从机 IP 访问 MySQL 和 Redis。
UFW 示例:
```bash
ufw allow from REPLICA_PUBLIC_IP to any port 3306 proto tcp
ufw allow from REPLICA_PUBLIC_IP to any port 6379 proto tcp
ufw deny 3306/tcp
ufw deny 6379/tcp
ufw reload
```
firewalld 示例:
```bash
firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="REPLICA_PUBLIC_IP" port protocol="tcp" port="3306" accept'
firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="REPLICA_PUBLIC_IP" port protocol="tcp" port="6379" accept'
firewall-cmd --reload
```
如果使用云厂商安全组,也要同步设置白名单。
## 6. 启动主机
检查 compose
```bash
docker compose -f docker-compose.cloud.yml config
```
启动:
```bash
docker compose -f docker-compose.cloud.yml up -d
```
检查服务:
```bash
docker ps
docker logs --tail=100 ppanel-mysql
docker logs --tail=100 ppanel-redis
docker logs --tail=100 ppanel-server
```
检查 MySQL GTID
```bash
docker exec ppanel-mysql mysql -uroot -p"$MYSQL_ROOT_PASSWORD" -e "SHOW VARIABLES LIKE 'gtid_mode';"
docker exec ppanel-mysql mysql -uroot -p"$MYSQL_ROOT_PASSWORD" -e "SHOW MASTER STATUS\\G"
```
检查 Redis
```bash
docker exec ppanel-redis redis-cli -a "$REDIS_PASSWORD" ping
```
如果主库已有数据,`mysql/init-master.sh` 不会自动重跑。手动创建复制账号:
```bash
docker exec -i ppanel-mysql mysql -uroot -p"$MYSQL_ROOT_PASSWORD" <<SQL
CREATE USER IF NOT EXISTS '$MYSQL_REPLICATION_USER'@'%' IDENTIFIED WITH mysql_native_password BY '$MYSQL_REPLICATION_PASSWORD';
GRANT REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO '$MYSQL_REPLICATION_USER'@'%';
FLUSH PRIVILEGES;
SQL
```
从从机测试主机端口:
```bash
mysqladmin ping -hMASTER_PUBLIC_IP -P3306 -u"$MYSQL_REPLICATION_USER" -p"$MYSQL_REPLICATION_PASSWORD"
redis-cli -h MASTER_PUBLIC_IP -p 6379 -a "$REDIS_PASSWORD" ping
```
## 7. 主库已有数据的备份
已有生产数据时,先从主库导出一致性 GTID 备份:
```bash
docker exec ppanel-mysql mysqldump \
-uroot -p"$MYSQL_ROOT_PASSWORD" \
--single-transaction \
--routines \
--triggers \
--events \
--set-gtid-purged=ON \
--databases "$MYSQL_DATABASE" \
| gzip > ppanel-gtid.sql.gz
```
把备份复制到从机:
```bash
scp ppanel-gtid.sql.gz root@REPLICA_PUBLIC_IP:/root/ppanel-gtid.sql.gz
```
## 8. 从机 `.env`
从机也复制 `.env.example`
```bash
cp .env.example .env
```
从机 `.env` 示例:
```dotenv
MYSQL_ROOT_PASSWORD=CHANGE_ME_STRONG_MYSQL_ROOT_PASSWORD
MYSQL_DATABASE=ppanel
MYSQL_REPLICATION_USER=replicator
MYSQL_REPLICATION_PASSWORD=CHANGE_ME_STRONG_REPLICATION_PASSWORD
REDIS_PASSWORD=CHANGE_ME_STRONG_REDIS_PASSWORD
MASTER_MYSQL_HOST=MASTER_PUBLIC_IP
MASTER_MYSQL_PORT=3306
MASTER_REDIS_HOST=MASTER_PUBLIC_IP
MASTER_REDIS_PORT=6379
GRAFANA_PASSWORD=CHANGE_ME_STRONG_GRAFANA_PASSWORD
PPANEL_SERVER_TAG=latest
```
`MYSQL_ROOT_PASSWORD` 建议和主机一致,方便备份导入和后续切换。
## 9. 从机 `configs/ppanel.yaml`
从机业务实例仍然连接主机 MySQL 和主机 Redis
```yaml
MySQL:
Addr: MASTER_PUBLIC_IP:3306
Username: root
Password: CHANGE_ME_STRONG_MYSQL_ROOT_PASSWORD
Dbname: ppanel
Config: charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai
MaxIdleConns: 10
MaxOpenConns: 100
SlowThreshold: 1000
Redis:
Host: MASTER_PUBLIC_IP:6379
Pass: CHANGE_ME_STRONG_REDIS_PASSWORD
DB: 0
Trace:
Endpoint: "127.0.0.1:4317"
```
不要写成:
```yaml
MySQL:
Addr: 127.0.0.1:3307
Redis:
Host: 127.0.0.1:6380
```
这样会让业务连接本机从库,写请求会失败或造成状态不一致。
## 10. 启动从机基础服务
检查 compose
```bash
docker compose -f docker-compose.replica.yml config
```
启动 MySQL 从库、Redis 从库、Sentinel 和 Tempo
```bash
docker compose -f docker-compose.replica.yml up -d mysql-replica redis-replica redis-sentinel tempo
```
查看日志:
```bash
docker logs --tail=100 ppanel-mysql-replica
docker logs --tail=100 ppanel-redis-replica
docker logs --tail=100 ppanel-redis-sentinel
```
## 11. 导入主库备份到从库
导入前临时关闭从库只读:
```bash
docker exec -i ppanel-mysql-replica mysql -uroot -p"$MYSQL_ROOT_PASSWORD" <<SQL
SET GLOBAL super_read_only = OFF;
SET GLOBAL read_only = OFF;
SQL
```
导入备份:
```bash
gunzip -c /root/ppanel-gtid.sql.gz | docker exec -i ppanel-mysql-replica mysql -uroot -p"$MYSQL_ROOT_PASSWORD"
```
如果从库是全新空库,只能导入一次。重复导入前需要确认是否要重建 `mysql_replica_data`,避免覆盖或重复数据。
## 12. 配置 MySQL GTID 复制
执行复制配置 profile
```bash
docker compose -f docker-compose.replica.yml --profile replication-setup up mysql-replica-setup
```
该步骤会:
- 等待主库可连接。
- 等待本机从库可连接。
- 执行 `CHANGE REPLICATION SOURCE TO ... SOURCE_AUTO_POSITION=1`
- 启动复制。
- 重新开启 `read_only``super_read_only`
检查状态:
```bash
docker exec ppanel-mysql-replica mysql -uroot -p"$MYSQL_ROOT_PASSWORD" -e "SHOW REPLICA STATUS\\G"
```
必须确认:
```text
Replica_IO_Running: Yes
Replica_SQL_Running: Yes
Last_IO_Error:
Last_SQL_Error:
```
`Seconds_Behind_Source` 可以短暂为非 0,但应逐渐回到较低值。
## 13. 验证 MySQL 主从同步
在主库创建测试数据:
```bash
docker exec -i ppanel-mysql mysql -uroot -p"$MYSQL_ROOT_PASSWORD" "$MYSQL_DATABASE" <<SQL
CREATE TABLE IF NOT EXISTS replication_check (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
message VARCHAR(64) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO replication_check (message) VALUES ('gtid-ok');
SQL
```
在从库查询:
```bash
docker exec ppanel-mysql-replica mysql -uroot -p"$MYSQL_ROOT_PASSWORD" "$MYSQL_DATABASE" -e "SELECT * FROM replication_check ORDER BY id DESC LIMIT 3;"
```
确认能看到 `gtid-ok`
## 14. 验证 Redis 主从/Sentinel
主机写入测试 key
```bash
docker exec ppanel-redis redis-cli -a "$REDIS_PASSWORD" SET replication:check redis-ok
```
从机读取:
```bash
docker exec ppanel-redis-replica redis-cli -a "$REDIS_PASSWORD" GET replication:check
```
检查从库角色:
```bash
docker exec ppanel-redis-replica redis-cli -a "$REDIS_PASSWORD" INFO replication
```
应看到:
```text
role:slave
master_host:MASTER_PUBLIC_IP
master_link_status:up
```
检查 Sentinel
```bash
docker exec ppanel-redis-sentinel redis-cli -p 26379 SENTINEL masters
```
## 15. 启动从机业务实例
确认从机 `configs/ppanel.yaml` 指向主库和主 Redis 后启动:
```bash
docker compose -f docker-compose.replica.yml up -d ppanel-server
```
检查:
```bash
docker logs --tail=100 ppanel-server
curl -I http://127.0.0.1:8080
```
如果从机业务实例报 MySQL/Redis 连接失败,先检查:
- 主机安全组/防火墙是否放行从机 IP。
- `configs/ppanel.yaml` 是否写主机公网 IP。
- `Redis.Pass` 是否和 `REDIS_PASSWORD` 一致。
## 16. Nginx 多实例转发
Nginx 只转发 HTTP 流量,不负责数据库读写分离。可参考 `config/nginx-ppanel-upstream.conf.example`
```nginx
upstream ppanel_backend {
server MASTER_PUBLIC_IP:8080 max_fails=3 fail_timeout=10s;
server REPLICA_PUBLIC_IP:8080 max_fails=3 fail_timeout=10s;
}
server {
listen 443 ssl http2;
server_name api.example.com;
location / {
proxy_pass http://ppanel_backend;
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;
}
}
```
应用配置后检查:
```bash
nginx -t
systemctl reload nginx
```
建议先只把从机加入 upstream,但设置较低权重或观察日志,确认登录、验证码、订单、支付回调等写操作正常后再正式承载流量。
## 17. 监控访问
Grafana 默认绑定主机本地端口 `3333`
```bash
ssh -L 3333:localhost:3333 root@MASTER_PUBLIC_IP
```
本地浏览器打开:
```text
http://localhost:3333
```
Prometheus 默认绑定主机本地端口 `9090`
```bash
ssh -L 9090:localhost:9090 root@MASTER_PUBLIC_IP
```
本地浏览器打开:
```text
http://localhost:9090
```
常用检查:
```bash
docker ps
docker logs --tail=100 ppanel-prometheus
docker logs --tail=100 ppanel-grafana
docker logs --tail=100 ppanel-loki
docker logs --tail=100 ppanel-tempo
```
## 18. 本地 Grafana MCP 排障入口
`mcp-grafana` 不部署在服务器 compose 中。它安装在本地电脑的 MCP 客户端环境里,通过 Grafana URL 和 Service Account Token 远程查询指标、日志、trace 和 dashboard。
先通过 SSH 隧道让本地能访问 Grafana
```bash
ssh -L 3333:localhost:3333 root@MASTER_PUBLIC_IP
```
在 Grafana 中创建 Service Account Token,然后在本地 MCP 客户端配置:
```json
{
"mcpServers": {
"grafana": {
"command": "mcp-grafana",
"env": {
"GRAFANA_URL": "http://localhost:3333",
"GRAFANA_SERVICE_ACCOUNT_TOKEN": "glsa_xxx"
}
}
}
}
```
如果本地使用 Docker 跑 MCP server
```bash
docker run --rm -p 127.0.0.1:8000:8000 \
-e GRAFANA_URL=http://host.docker.internal:3333 \
-e GRAFANA_SERVICE_ACCOUNT_TOKEN=glsa_xxx \
grafana/mcp-grafana
```
客户端 SSE 地址:
```text
http://localhost:8000/sse
```
不要把 Grafana 或本地 MCP SSE 暴露公网。
## 19. 日常巡检命令
MySQL 主库:
```bash
docker exec ppanel-mysql mysql -uroot -p"$MYSQL_ROOT_PASSWORD" -e "SHOW MASTER STATUS\\G"
```
MySQL 从库:
```bash
docker exec ppanel-mysql-replica mysql -uroot -p"$MYSQL_ROOT_PASSWORD" -e "SHOW REPLICA STATUS\\G"
```
Redis 主库:
```bash
docker exec ppanel-redis redis-cli -a "$REDIS_PASSWORD" INFO replication
```
Redis 从库:
```bash
docker exec ppanel-redis-replica redis-cli -a "$REDIS_PASSWORD" INFO replication
```
业务实例:
```bash
docker logs --tail=100 ppanel-server
curl -I http://127.0.0.1:8080
```
Compose 状态:
```bash
docker compose -f docker-compose.cloud.yml ps
docker compose -f docker-compose.replica.yml ps
```
## 20. 手动故障切换
### MySQL 主库故障
在从机确认复制尽量追平后执行:
```bash
docker exec -i ppanel-mysql-replica mysql -uroot -p"$MYSQL_ROOT_PASSWORD" <<SQL
STOP REPLICA;
RESET REPLICA ALL;
SET GLOBAL super_read_only = OFF;
SET GLOBAL read_only = OFF;
SQL
```
然后把所有 `ppanel-server``configs/ppanel.yaml` 改为:
```yaml
MySQL:
Addr: REPLICA_PUBLIC_IP:3307
```
如果要让新主库对远程服务开放,需要把从机 compose 的 MySQL 端口从 `127.0.0.1:3307:3306` 改成内网/公网白名单可访问的地址,并更新防火墙。
重启业务实例:
```bash
docker compose -f docker-compose.cloud.yml restart ppanel-server
docker compose -f docker-compose.replica.yml restart ppanel-server
```
### Redis 主库故障
在从机提升 Redis
```bash
docker exec ppanel-redis-replica redis-cli -a "$REDIS_PASSWORD" REPLICAOF NO ONE
```
然后把所有 `ppanel-server``configs/ppanel.yaml` 改为:
```yaml
Redis:
Host: REPLICA_PUBLIC_IP:6380
```
如果要让新 Redis 主库对远程服务开放,需要把从机 compose 的 Redis 端口从 `127.0.0.1:6380:6379` 改成内网/公网白名单可访问的地址,并更新防火墙。
重启业务实例:
```bash
docker compose -f docker-compose.cloud.yml restart ppanel-server
docker compose -f docker-compose.replica.yml restart ppanel-server
```
## 21. 常见问题
### `Replica_IO_Running: No`
检查:
- 主机防火墙是否允许从机 IP 访问 `3306`
- `MYSQL_REPLICATION_USER``MYSQL_REPLICATION_PASSWORD` 是否一致。
- 主库是否已经创建复制账号。
- `MASTER_MYSQL_HOST` 是否写错。
查看错误:
```bash
docker exec ppanel-mysql-replica mysql -uroot -p"$MYSQL_ROOT_PASSWORD" -e "SHOW REPLICA STATUS\\G" | egrep "Last_IO_Error|Last_SQL_Error"
```
### `Replica_SQL_Running: No`
通常是导入备份或 GTID 状态不一致。先看错误:
```bash
docker exec ppanel-mysql-replica mysql -uroot -p"$MYSQL_ROOT_PASSWORD" -e "SHOW REPLICA STATUS\\G" | egrep "Last_SQL_Error|Retrieved_Gtid_Set|Executed_Gtid_Set"
```
不要直接跳过事务。生产环境应先备份当前从库,再决定是否重建从库数据卷重新导入。
### 从机业务登录异常
检查从机 `configs/ppanel.yaml`
- `MySQL.Addr` 必须是主机 MySQL。
- `Redis.Host` 必须是主机 Redis。
- `Redis.Pass` 必须填写。
### Redis 从库不同步
检查:
```bash
docker logs --tail=100 ppanel-redis-replica
docker exec ppanel-redis-replica redis-cli -a "$REDIS_PASSWORD" INFO replication
```
常见原因是主机 `6379` 未对白名单放行,或 `REDIS_PASSWORD` 不一致。
### Grafana MCP 连接失败
检查:
- SSH 隧道是否仍在运行。
- 本地是否能打开 `http://localhost:3333`
- Grafana Service Account Token 是否有效。
- 本地 MCP 配置中的 `GRAFANA_URL` 是否是 `http://localhost:3333`