hi-server/doc/remote-replication-zh.md
shanshanzhong 68c7b0a8ec
All checks were successful
Build docker and publish / build (20.15.1) (push) Successful in 5m52s
chore(deploy): add replication deployment assets
2026-04-28 05:22:48 -07:00

718 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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`