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

16 KiB
Raw Blame History

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. 拓扑与端口

示例变量:

主机公网 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. 文件准备

主机上传:

docker-compose.cloud.yml
.env
configs/
mysql/
redis/
loki/
grafana/
prometheus/
tempo/
config/

从机上传:

docker-compose.replica.yml
.env
configs/
mysql/
redis/
tempo/
cache/

两台机器都创建运行目录:

mkdir -p configs logs cache tempo_data mysql redis

主机如果使用监控 exporter还需要准备 MySQL exporter 配置:

cp mysql/.my.cnf.example mysql/.my.cnf

编辑 mysql/.my.cnf,把密码改成真实 MYSQL_ROOT_PASSWORD。该文件已被 .gitignore 忽略,不要提交。

3. 主机 .env

在主机复制 .env.example

cp .env.example .env

主机 .env 示例:

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 监听内网 IPMYSQL_PUBLISH_ADDRREDIS_PUBLISH_ADDR 改成内网地址。

4. 主机 configs/ppanel.yaml

主机业务实例连接本机主库和主 Redis

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 示例:

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 示例:

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

docker compose -f docker-compose.cloud.yml config

启动:

docker compose -f docker-compose.cloud.yml up -d

检查服务:

docker ps
docker logs --tail=100 ppanel-mysql
docker logs --tail=100 ppanel-redis
docker logs --tail=100 ppanel-server

检查 MySQL GTID

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

docker exec ppanel-redis redis-cli -a "$REDIS_PASSWORD" ping

如果主库已有数据,mysql/init-master.sh 不会自动重跑。手动创建复制账号:

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

从从机测试主机端口:

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 备份:

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

把备份复制到从机:

scp ppanel-gtid.sql.gz root@REPLICA_PUBLIC_IP:/root/ppanel-gtid.sql.gz

8. 从机 .env

从机也复制 .env.example

cp .env.example .env

从机 .env 示例:

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

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"

不要写成:

MySQL:
  Addr: 127.0.0.1:3307
Redis:
  Host: 127.0.0.1:6380

这样会让业务连接本机从库,写请求会失败或造成状态不一致。

10. 启动从机基础服务

检查 compose

docker compose -f docker-compose.replica.yml config

启动 MySQL 从库、Redis 从库、Sentinel 和 Tempo

docker compose -f docker-compose.replica.yml up -d mysql-replica redis-replica redis-sentinel tempo

查看日志:

docker logs --tail=100 ppanel-mysql-replica
docker logs --tail=100 ppanel-redis-replica
docker logs --tail=100 ppanel-redis-sentinel

11. 导入主库备份到从库

导入前临时关闭从库只读:

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

导入备份:

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

docker compose -f docker-compose.replica.yml --profile replication-setup up mysql-replica-setup

该步骤会:

  • 等待主库可连接。
  • 等待本机从库可连接。
  • 执行 CHANGE REPLICATION SOURCE TO ... SOURCE_AUTO_POSITION=1
  • 启动复制。
  • 重新开启 read_onlysuper_read_only

检查状态:

docker exec ppanel-mysql-replica mysql -uroot -p"$MYSQL_ROOT_PASSWORD" -e "SHOW REPLICA STATUS\\G"

必须确认:

Replica_IO_Running: Yes
Replica_SQL_Running: Yes
Last_IO_Error:
Last_SQL_Error:

Seconds_Behind_Source 可以短暂为非 0但应逐渐回到较低值。

13. 验证 MySQL 主从同步

在主库创建测试数据:

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

在从库查询:

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

docker exec ppanel-redis redis-cli -a "$REDIS_PASSWORD" SET replication:check redis-ok

从机读取:

docker exec ppanel-redis-replica redis-cli -a "$REDIS_PASSWORD" GET replication:check

检查从库角色:

docker exec ppanel-redis-replica redis-cli -a "$REDIS_PASSWORD" INFO replication

应看到:

role:slave
master_host:MASTER_PUBLIC_IP
master_link_status:up

检查 Sentinel

docker exec ppanel-redis-sentinel redis-cli -p 26379 SENTINEL masters

15. 启动从机业务实例

确认从机 configs/ppanel.yaml 指向主库和主 Redis 后启动:

docker compose -f docker-compose.replica.yml up -d ppanel-server

检查:

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

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;
    }
}

应用配置后检查:

nginx -t
systemctl reload nginx

建议先只把从机加入 upstream但设置较低权重或观察日志确认登录、验证码、订单、支付回调等写操作正常后再正式承载流量。

17. 监控访问

Grafana 默认绑定主机本地端口 3333

ssh -L 3333:localhost:3333 root@MASTER_PUBLIC_IP

本地浏览器打开:

http://localhost:3333

Prometheus 默认绑定主机本地端口 9090

ssh -L 9090:localhost:9090 root@MASTER_PUBLIC_IP

本地浏览器打开:

http://localhost:9090

常用检查:

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

ssh -L 3333:localhost:3333 root@MASTER_PUBLIC_IP

在 Grafana 中创建 Service Account Token然后在本地 MCP 客户端配置:

{
  "mcpServers": {
    "grafana": {
      "command": "mcp-grafana",
      "env": {
        "GRAFANA_URL": "http://localhost:3333",
        "GRAFANA_SERVICE_ACCOUNT_TOKEN": "glsa_xxx"
      }
    }
  }
}

如果本地使用 Docker 跑 MCP server

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 地址:

http://localhost:8000/sse

不要把 Grafana 或本地 MCP SSE 暴露公网。

19. 日常巡检命令

MySQL 主库:

docker exec ppanel-mysql mysql -uroot -p"$MYSQL_ROOT_PASSWORD" -e "SHOW MASTER STATUS\\G"

MySQL 从库:

docker exec ppanel-mysql-replica mysql -uroot -p"$MYSQL_ROOT_PASSWORD" -e "SHOW REPLICA STATUS\\G"

Redis 主库:

docker exec ppanel-redis redis-cli -a "$REDIS_PASSWORD" INFO replication

Redis 从库:

docker exec ppanel-redis-replica redis-cli -a "$REDIS_PASSWORD" INFO replication

业务实例:

docker logs --tail=100 ppanel-server
curl -I http://127.0.0.1:8080

Compose 状态:

docker compose -f docker-compose.cloud.yml ps
docker compose -f docker-compose.replica.yml ps

20. 手动故障切换

MySQL 主库故障

在从机确认复制尽量追平后执行:

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-serverconfigs/ppanel.yaml 改为:

MySQL:
  Addr: REPLICA_PUBLIC_IP:3307

如果要让新主库对远程服务开放,需要把从机 compose 的 MySQL 端口从 127.0.0.1:3307:3306 改成内网/公网白名单可访问的地址,并更新防火墙。

重启业务实例:

docker compose -f docker-compose.cloud.yml restart ppanel-server
docker compose -f docker-compose.replica.yml restart ppanel-server

Redis 主库故障

在从机提升 Redis

docker exec ppanel-redis-replica redis-cli -a "$REDIS_PASSWORD" REPLICAOF NO ONE

然后把所有 ppanel-serverconfigs/ppanel.yaml 改为:

Redis:
  Host: REPLICA_PUBLIC_IP:6380

如果要让新 Redis 主库对远程服务开放,需要把从机 compose 的 Redis 端口从 127.0.0.1:6380:6379 改成内网/公网白名单可访问的地址,并更新防火墙。

重启业务实例:

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_USERMYSQL_REPLICATION_PASSWORD 是否一致。
  • 主库是否已经创建复制账号。
  • MASTER_MYSQL_HOST 是否写错。

查看错误:

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 状态不一致。先看错误:

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 从库不同步

检查:

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