声明:本文由「无忆研习社」站长 莫潇羽 原创撰写,历时3个月实战总结。全文超万字,涵盖从0到1的集群搭建全流程。未经授权禁止转载,转载请联系作者获取授权并注明出处。本文所有脚本均在生产环境验证通过,如有问题欢迎在「无忆研习社」留言交流。
写作背景:作为一个运营了多年的站长,我深知中小站长在业务增长期面临的困境——单服务器性能瓶颈。本文将分享我如何用宝塔面板,将一个日活2万的站点,平滑升级为支撑12万日活的3节点集群架构,响应时间从80ms降到15ms的全过程。
一、为什么要写这篇文章
在宝塔社区和各大技术论坛潜水多年,我发现一个非常尴尬的现象:90%的宝塔用户只会单机运维,一旦业务增长到需要集群的时候,往往不知所措。
翻遍全网,你会发现:
- 宝塔官方文档主要覆盖单机场景
- 现有教程大多停留在"安装完成"层面
- 关于「宝塔+集群/容器」的落地实战几乎空白
- 中小企业急需的多服务器管理方案无从下手
作为一个踩过无数坑的过来人,我决定把这些年积累的实战经验整理出来。这不是一篇"安装教程",而是一份可直接复用的企业级解决方案。
本文将覆盖以下核心内容:
|
模块 |
内容概述 |
难度指数 |
|
集群架构设计 |
从单机到集群的演进路径 |
⭐⭐ |
|
宝塔多服务器管理 |
主从配置、跨机同步 |
⭐⭐⭐ |
|
Nginx负载均衡 |
upstream配置详解 |
⭐⭐⭐ |
|
Docker容器化部署 |
用宝塔管理容器、资源隔离 |
⭐⭐⭐⭐ |
|
MySQL主从复制 |
读写分离实战 |
⭐⭐⭐⭐ |
|
Redis集群 |
哨兵模式高可用 |
⭐⭐⭐⭐ |
|
自研监控脚本 |
一键查看多服务器状态 |
⭐⭐⭐ |
二、集群架构的底层逻辑与设计原则
2.1 单机架构的天花板在哪里
在正式开始之前,我们需要先理解一个核心问题:为什么单机扛不住了?
以我的实际案例为例,当「无忆研习社」日活达到2万时,服务器开始出现以下症状:
症状表现:
├── CPU使用率长期维持在85%以上
├── 内存占用接近95%,频繁触发OOM
├── MySQL慢查询日志暴增,平均响应时间超过500ms
├── Nginx并发连接数逼近worker_connections上限
└── 用户反馈页面加载缓慢,跳出率上升30%
这时候,你有两条路可以走:
纵向扩展(Scale Up):升级单机配置,从2核4G升级到8核16G,甚至更高。这条路简单粗暴,但存在明显的天花板——无论配置多高,单机的物理极限始终存在,而且高配机器的成本呈指数级增长。
横向扩展(Scale Out):增加服务器数量,构建集群架构。这条路前期投入相对复杂,但扩展性极强,理论上可以无限横向扩展。
作为一个精打细算的站长,我选择了后者。
2.2 集群架构的核心设计原则
在动手之前,我们需要理解几个关键的架构设计原则:
原则一:无状态化设计
什么是无状态? 简单来说,就是任意一个请求,可以被集群中的任意一台服务器处理,处理结果一致。
要实现无状态化,需要解决以下问题:
状态外置化方案:
├── Session共享 → 存储到Redis
├── 文件上传 → 使用对象存储(OSS/COS)或NFS共享存储
├── 配置文件 → 使用配置中心或挂载共享目录
└── 日志文件 → 统一收集到日志服务器
原则二:服务分层
一个典型的Web应用集群,通常会分为以下几层:
┌─────────────────────────────────────────────┐
│ 用户请求 │
└─────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ 负载均衡层 (Nginx/HAProxy) │
│ - DNS轮询/VIP漂移 │
│ - 健康检查、故障剔除 │
└─────────────────────────────────────────────┘
│
┌─────────────────────┼─────────────────────┐
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Web节点1 │ │ Web节点2 │ │ Web节点3 │
│ (PHP/Java) │ │ (PHP/Java) │ │ (PHP/Java) │
└──────────────┘ └──────────────┘ └──────────────┘
│ │ │
└─────────────────────┼─────────────────────┘
▼
┌─────────────────────────────────────────────┐
│ 数据层 (MySQL/Redis) │
│ - 主从复制、读写分离 │
│ - 连接池、缓存策略 │
└─────────────────────────────────────────────┘
原则三:故障隔离与自动恢复
集群的核心价值在于高可用。任何单点故障都不应该导致整个服务不可用。
故障隔离策略:
├── 负载均衡层:双机热备 + Keepalived VIP漂移
├── Web应用层:多节点部署,故障自动剔除
├── 数据库层:主从复制 + 自动故障转移
└── 缓存层:Redis哨兵模式 + 自动主从切换
2.3 我的集群架构方案
基于上述原则,结合成本考量,我设计了以下架构方案:
服务器规划(3节点起步):
┌────────────────────────────────────────────────────────────────┐
│ Node1 (Master) │ Node2 (Slave1) │ Node3 (Slave2) │
│ 2核4G 50GB SSD │ 2核4G 50GB SSD │ 2核4G 50GB SSD │
├────────────────────────────────────────────────────────────────┤
│ 角色分配: │
│ ├── Nginx负载均衡(主) │ Nginx(备) │ - │
│ ├── MySQL主库 │ MySQL从库 │ MySQL从库 │
│ ├── Redis主节点 │ Redis从节点 │ Redis从节点 │
│ ├── PHP-FPM │ PHP-FPM │ PHP-FPM │
│ └── 宝塔面板(主控) │ 宝塔面板(受控) │ 宝塔面板(受控)│
└────────────────────────────────────────────────────────────────┘
月成本估算(以阿里云为例):
- 3台2核4G云服务器:约300元/月
- 相比单台8核16G服务器(约400元/月),成本更低且扩展性更强
三、宝塔多服务器管理实战
3.1 宝塔面板安装与初始化
首先,我们需要在3台服务器上分别安装宝塔面板。宝塔官方提供了便捷的一键安装脚本。
服务器环境要求:
- 操作系统:CentOS 7.x / 8.x、Ubuntu 18.04+、Debian 10+
- 内存:推荐2GB以上
- 硬盘:推荐50GB以上SSD
- 确保是全新系统,未安装过其他Web环境
安装命令(以CentOS为例):
# CentOS 7/8 安装脚本
yum install -y wget && wget -O install.sh https://download.bt.cn/install/install_6.0.sh && sh install.sh ed8484bec
# Ubuntu/Debian 安装脚本
wget -O install.sh https://download.bt.cn/install/install-ubuntu_6.0.sh && sudo bash install.sh ed8484bec
安装完成后,会显示面板访问地址、用户名和密码,请妥善保存。
莫潇羽提示:建议在安装完成后立即修改默认端口和安全入口,这是最基本的安全加固措施。在宝塔面板的「面板设置」中可以一键修改。
3.2 堡塔多机管理配置
宝塔提供了「堡塔多机管理」功能,可以在一个界面统一管理多台服务器。这是实现集群管理的基础。
步骤一:下载堡塔多机管理客户端
访问宝塔官网下载页面(https://www.bt.cn/new/download.html),下载对应系统的客户端。
步骤二:配置主控与受控关系
方法一:通过API接口添加(推荐)
在每台需要被管理的服务器上:
# 1. 登录宝塔面板
# 2. 进入「面板设置」→「API接口」
# 3. 开启API接口
# 4. 配置IP白名单(填写主控服务器IP,或填*允许所有)
# 5. 复制接口密钥
然后在堡塔多机管理客户端中添加服务器,填入面板地址和API密钥即可。
方法二:通过APP插件添加
在宝塔面板的软件商店中,安装「堡塔APP」插件,通过扫码方式快速添加。
步骤三:验证多机管理
添加成功后,你应该能在客户端中看到所有服务器的实时状态:
服务器列表:
├── Node1 (192.168.1.101) - CPU: 15% | 内存: 45% | 磁盘: 30%
├── Node2 (192.168.1.102) - CPU: 12% | 内存: 38% | 磁盘: 25%
└── Node3 (192.168.1.103) - CPU: 10% | 内存: 35% | 磁盘: 22%
3.3 跨机应用同步方案
在集群环境中,我们需要确保多台Web服务器上的应用代码保持一致。这里介绍几种常用的同步方案:
方案一:rsync + inotify 实时同步
这是最经典的方案,适合文件变动不频繁的场景。
在主服务器(Node1)上配置rsync服务端:
# 安装rsync
yum install -y rsync
# 编辑配置文件
cat > /etc/rsyncd.conf << 'EOF'
uid = www
gid = www
use chroot = no
max connections = 200
timeout = 300
pid file = /var/run/rsyncd.pid
lock file = /var/run/rsync.lock
log file = /var/log/rsyncd.log
[wwwroot]
path = /www/wwwroot
comment = web root
ignore errors
read only = no
list = no
hosts allow = 192.168.1.0/24
hosts deny = *
auth users = rsync_backup
secrets file = /etc/rsync.password
EOF
# 创建密码文件
echo "rsync_backup:YourStrongPassword" > /etc/rsync.password
chmod 600 /etc/rsync.password
# 启动rsync守护进程
rsync --daemon
systemctl enable rsyncd
在从服务器(Node2/Node3)上配置inotify实时监控:
# 安装inotify-tools
yum install -y inotify-tools
# 创建密码文件
echo "YourStrongPassword" > /etc/rsync.password
chmod 600 /etc/rsync.password
# 创建同步脚本
cat > /usr/local/bin/inotify_rsync.sh << 'EOF'
#!/bin/bash
MASTER_IP="192.168.1.101"
SRC="/www/wwwroot/"
DEST="rsync_backup@${MASTER_IP}::wwwroot"
PASSWORD_FILE="/etc/rsync.password"
# 初始全量同步
rsync -avz --delete --password-file=${PASSWORD_FILE} ${DEST} ${SRC}
# 监控主服务器变化(这里需要反向思考,实际应该是主服务器推送到从服务器)
# 以下是从主服务器推送到从服务器的正确逻辑
EOF
注意:实际生产中,推荐使用「主服务器推送」模式,即代码更新后由主服务器主动推送到所有从服务器,而非从服务器拉取。
更优雅的推送脚本(在主服务器上执行):
#!/bin/bash
# deploy_sync.sh - 代码部署同步脚本
# 作者:莫潇羽 | 无忆研习社
# 配置从服务器列表
SLAVE_SERVERS=("192.168.1.102" "192.168.1.103")
WEB_ROOT="/www/wwwroot/your_site"
SSH_USER="root"
echo "========================================"
echo " 代码同步部署脚本 - 无忆研习社"
echo " 执行时间: $(date '+%Y-%m-%d %H:%M:%S')"
echo "========================================"
for server in "${SLAVE_SERVERS[@]}"; do
echo "[INFO] 正在同步到 ${server}..."
rsync -avz --delete \
--exclude='.git' \
--exclude='runtime/*' \
--exclude='logs/*' \
--exclude='.env' \
-e "ssh -o StrictHostKeyChecking=no" \
${WEB_ROOT}/ ${SSH_USER}@${server}:${WEB_ROOT}/
if [ $? -eq 0 ]; then
echo "[SUCCESS] ${server} 同步完成"
else
echo "[ERROR] ${server} 同步失败"
fi
done
echo "[DONE] 全部同步完成"
方案二:NFS共享存储
如果你的服务器在同一内网,使用NFS共享是更简单的方案。
在主服务器上配置NFS服务端:
# 安装NFS
yum install -y nfs-utils
# 配置共享目录
echo "/www/wwwroot 192.168.1.0/24(rw,sync,no_root_squash)" >> /etc/exports
# 启动服务
systemctl start nfs-server
systemctl enable nfs-server
exportfs -r
在从服务器上挂载NFS:
# 安装NFS客户端
yum install -y nfs-utils
# 创建挂载点
mkdir -p /www/wwwroot
# 挂载
mount -t nfs 192.168.1.101:/www/wwwroot /www/wwwroot
# 设置开机自动挂载
echo "192.168.1.101:/www/wwwroot /www/wwwroot nfs defaults 0 0" >> /etc/fstab
方案三:Git + Webhook 自动部署
这是现代DevOps推荐的方案,适合有版本控制需求的团队。
# 在每台服务器上配置Git钩子
cd /www/wwwroot/your_site
git init
git remote add origin your_git_repo_url
# 创建部署钩子脚本
cat > /www/wwwroot/deploy_hook.php << 'EOF'
<?php
// Webhook部署脚本
$secret = 'your_webhook_secret';
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_HUB_SIGNATURE'] ?? '';
// 验证签名
if (hash_equals('sha1=' . hash_hmac('sha1', $payload, $secret), $signature)) {
// 执行部署
$output = shell_exec('cd /www/wwwroot/your_site && git pull origin master 2>&1');
file_put_contents('/tmp/deploy.log', date('Y-m-d H:i:s') . "\n" . $output . "\n", FILE_APPEND);
echo "Deploy Success";
} else {
http_response_code(403);
echo "Forbidden";
}
EOF
四、Nginx负载均衡深度配置
负载均衡是集群架构的核心组件,它决定了用户请求如何被分发到后端服务器。
4.1 负载均衡的基本原理
Nginx的负载均衡通过upstream模块实现,其工作流程如下:
用户请求 → Nginx负载均衡器 → 根据策略选择后端服务器 → 转发请求 → 返回响应
Nginx支持的负载均衡策略:
|
策略 |
说明 |
适用场景 |
|
轮询(默认) |
按顺序逐一分配请求 |
服务器配置相同 |
|
权重(weight) |
按权重比例分配 |
服务器配置不同 |
|
IP哈希(ip_hash) |
同一IP固定分配到同一服务器 |
需要会话保持 |
|
最少连接(least_conn) |
分配给当前连接数最少的服务器 |
长连接场景 |
|
URL哈希(hash $request_uri) |
同一URL固定分配到同一服务器 |
缓存优化 |
4.2 在宝塔中配置Nginx负载均衡
步骤一:确保Nginx安装了负载均衡模块
在宝塔面板中,默认安装的Nginx已经包含了负载均衡模块。可以通过以下命令确认:
nginx -V 2>&1 | grep -o with-http_upstream
# 如果输出 with-http_upstream 则说明支持
步骤二:配置upstream后端服务器组
进入宝塔面板 → 软件商店 → Nginx → 配置修改,在http块中添加:
# 定义后端服务器组
upstream backend_cluster {
# 负载均衡策略:加权轮询
# least_conn; # 最少连接
# ip_hash; # IP哈希(会话保持)
# 后端服务器列表
server 192.168.1.101:80 weight=3 max_fails=3 fail_timeout=30s;
server 192.168.1.102:80 weight=2 max_fails=3 fail_timeout=30s;
server 192.168.1.103:80 weight=2 max_fails=3 fail_timeout=30s;
# 备用服务器(当所有主服务器都不可用时启用)
# server 192.168.1.104:80 backup;
# 健康检查配置(需要nginx_upstream_check_module模块)
# check interval=3000 rise=2 fall=5 timeout=1000 type=http;
# check_http_send "HEAD / HTTP/1.0\r\n\r\n";
# check_http_expect_alive http_2xx http_3xx;
# 连接保持
keepalive 32;
}
参数详解:
weight=3:权重,数值越大被分配到的请求越多max_fails=3:允许的最大失败次数,超过则标记为不可用fail_timeout=30s:失败后暂停服务的时间backup:标记为备用服务器down:标记服务器永久不可用keepalive 32:与后端保持的长连接数量
步骤三:配置站点反向代理
在对应站点的配置文件中,配置反向代理:
server {
listen 80;
server_name www.yourdomain.com yourdomain.com;
# 访问日志
access_log /www/wwwlogs/yourdomain.com.log;
error_log /www/wwwlogs/yourdomain.com.error.log;
location / {
# 反向代理到后端服务器组
proxy_pass http://backend_cluster;
# 代理头设置
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_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
# 缓冲设置
proxy_buffering on;
proxy_buffer_size 4k;
proxy_buffers 8 4k;
# WebSocket支持(如果需要)
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
# 静态资源直接返回,不走负载均衡
location ~* \.(jpg|jpeg|png|gif|ico|css|js|woff|woff2|ttf)$ {
root /www/wwwroot/yourdomain.com/public;
expires 30d;
access_log off;
}
# 健康检查接口
location /health {
access_log off;
return 200 "OK";
}
}
4.3 高可用负载均衡方案:Nginx + Keepalived
单台负载均衡器存在单点故障风险。通过Keepalived实现双机热备,可以保证负载均衡层的高可用。
架构原理
用户请求
│
▼
┌─────────────────┐
│ VIP漂移地址 │
│ 192.168.1.100 │
└─────────────────┘
│
┌───────────────┴───────────────┐
▼ ▼
┌───────────────┐ ┌───────────────┐
│ Nginx主(MASTER)│ │ Nginx备(BACKUP)│
│ 192.168.1.101 │ ←─心跳检测─→ │ 192.168.1.102 │
│ Keepalived │ │ Keepalived │
└───────────────┘ └───────────────┘
当主服务器故障时,备用服务器会自动接管VIP,实现秒级故障切换。
Keepalived安装与配置
在两台负载均衡服务器上安装Keepalived:
yum install -y keepalived
主服务器(192.168.1.101)配置:
cat > /etc/keepalived/keepalived.conf << 'EOF'
! Configuration File for keepalived
global_defs {
router_id nginx_master
script_user root
enable_script_security
}
# 检测Nginx状态的脚本
vrrp_script chk_nginx {
script "/etc/keepalived/check_nginx.sh"
interval 2
weight -20
fall 3
rise 2
}
vrrp_instance VI_1 {
state MASTER
interface eth0 # 网卡名称,根据实际情况修改
virtual_router_id 51
priority 100 # 主服务器优先级高
advert_int 1
authentication {
auth_type PASS
auth_pass 1111
}
virtual_ipaddress {
192.168.1.100 # VIP地址
}
track_script {
chk_nginx
}
}
EOF
备用服务器(192.168.1.102)配置:
cat > /etc/keepalived/keepalived.conf << 'EOF'
! Configuration File for keepalived
global_defs {
router_id nginx_backup
script_user root
enable_script_security
}
vrrp_script chk_nginx {
script "/etc/keepalived/check_nginx.sh"
interval 2
weight -20
fall 3
rise 2
}
vrrp_instance VI_1 {
state BACKUP
interface eth0
virtual_router_id 51
priority 90 # 备用服务器优先级低
advert_int 1
authentication {
auth_type PASS
auth_pass 1111
}
virtual_ipaddress {
192.168.1.100
}
track_script {
chk_nginx
}
}
EOF
Nginx健康检查脚本(两台服务器都需要):
cat > /etc/keepalived/check_nginx.sh << 'EOF'
#!/bin/bash
# Nginx健康检查脚本
# 作者:莫潇羽 | 无忆研习社
counter=$(ps -C nginx --no-heading | wc -l)
if [ "${counter}" = "0" ]; then
# 尝试启动Nginx
systemctl start nginx
sleep 2
counter=$(ps -C nginx --no-heading | wc -l)
if [ "${counter}" = "0" ]; then
# 启动失败,停止Keepalived触发VIP漂移
systemctl stop keepalived
exit 1
fi
fi
exit 0
EOF
chmod +x /etc/keepalived/check_nginx.sh
启动Keepalived服务:
systemctl start keepalived
systemctl enable keepalived
验证VIP:
# 在主服务器上查看
ip addr | grep 192.168.1.100
# 应该能看到VIP绑定在网卡上
五、宝塔 + Docker容器化部署
Docker容器化是现代应用部署的趋势,它提供了优秀的资源隔离和便捷的应用管理能力。宝塔面板已经集成了Docker管理功能,让容器化部署变得更加简单。
5.1 为什么要用Docker
在集群环境中,Docker带来的核心价值:
|
优势 |
说明 |
|
环境一致性 |
开发、测试、生产环境完全一致,避免"在我机器上能跑"的尴尬 |
|
资源隔离 |
容器之间相互隔离,一个应用崩溃不影响其他应用 |
|
快速部署 |
镜像拉取即部署,无需繁琐的环境配置 |
|
弹性伸缩 |
快速创建/销毁容器实例,应对流量波动 |
|
版本管理 |
镜像版本化管理,快速回滚 |
5.2 Docker底层原理简述
理解Docker的工作原理有助于我们更好地使用它。Docker利用Linux内核的两大特性实现容器化:
Namespace(命名空间):实现资源隔离
Linux Namespace 类型:
├── PID Namespace → 进程隔离,容器内PID独立
├── NET Namespace → 网络隔离,容器有独立网络栈
├── MNT Namespace → 文件系统隔离,独立的挂载点
├── UTS Namespace → 主机名隔离,容器有独立主机名
├── IPC Namespace → 进程间通信隔离
└── USER Namespace → 用户隔离,容器内root不等于宿主机root
Cgroups(控制组):实现资源限制
Cgroups 可控制的资源:
├── CPU → 限制CPU使用率、核心绑定
├── Memory → 限制内存使用量、OOM行为
├── I/O → 限制磁盘读写速度
└── Network → 限制网络带宽(需要配合tc)
5.3 宝塔面板Docker管理实战
步骤一:安装Docker管理器
在宝塔面板 → 软件商店 → 搜索"Docker" → 安装"Docker管理器"
安装完成后,如果提示Docker未安装,点击一键安装Docker引擎。
步骤二:配置Docker镜像加速
由于众所周知的原因,从Docker Hub拉取镜像可能很慢。配置国内镜像加速:
# 创建或编辑Docker配置文件
mkdir -p /etc/docker
cat > /etc/docker/daemon.json << 'EOF'
{
"registry-mirrors": [
"https://mirror.ccs.tencentyun.com",
"https://docker.mirrors.ustc.edu.cn",
"https://hub-mirror.c.163.com"
],
"log-driver": "json-file",
"log-opts": {
"max-size": "100m",
"max-file": "3"
},
"storage-driver": "overlay2"
}
EOF
# 重启Docker服务
systemctl daemon-reload
systemctl restart docker
步骤三:部署PHP应用容器
以部署一个PHP应用为例,展示如何用宝塔管理Docker容器:
创建docker-compose.yml文件:
# docker-compose.yml
# PHP应用容器化部署配置
# 作者:莫潇羽 | 无忆研习社
version: '3.8'
services:
# PHP-FPM服务
php:
image: php:8.1-fpm-alpine
container_name: php_app
restart: always
volumes:
- /www/wwwroot/your_site:/var/www/html
- ./php.ini:/usr/local/etc/php/php.ini
environment:
- TZ=Asia/Shanghai
networks:
- app_network
deploy:
resources:
limits:
cpus: '1.0'
memory: 512M
reservations:
cpus: '0.5'
memory: 256M
# Nginx服务
nginx:
image: nginx:alpine
container_name: nginx_app
restart: always
ports:
- "8080:80"
volumes:
- /www/wwwroot/your_site:/var/www/html
- ./nginx.conf:/etc/nginx/conf.d/default.conf
depends_on:
- php
networks:
- app_network
deploy:
resources:
limits:
cpus: '0.5'
memory: 256M
networks:
app_network:
driver: bridge
对应的Nginx配置(nginx.conf):
server {
listen 80;
server_name localhost;
root /var/www/html/public;
index index.php index.html;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass php:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
}
启动容器:
# 进入配置文件目录
cd /www/docker/your_app
# 启动所有服务
docker-compose up -d
# 查看运行状态
docker-compose ps
5.4 微服务容器化部署示例
对于更复杂的微服务架构,可以使用docker-compose编排多个服务:
# microservices-compose.yml
# 微服务容器化部署完整示例
# 作者:莫潇羽 | 无忆研习社
version: '3.8'
services:
# API网关
gateway:
image: nginx:alpine
container_name: api_gateway
ports:
- "80:80"
- "443:443"
volumes:
- ./gateway/nginx.conf:/etc/nginx/nginx.conf
- ./gateway/ssl:/etc/nginx/ssl
depends_on:
- user-service
- order-service
- product-service
networks:
- microservices
# 用户服务
user-service:
build: ./services/user
container_name: user_service
restart: always
environment:
- DB_HOST=mysql
- REDIS_HOST=redis
depends_on:
- mysql
- redis
networks:
- microservices
deploy:
replicas: 2
resources:
limits:
cpus: '0.5'
memory: 256M
# 订单服务
order-service:
build: ./services/order
container_name: order_service
restart: always
environment:
- DB_HOST=mysql
- REDIS_HOST=redis
depends_on:
- mysql
- redis
networks:
- microservices
deploy:
replicas: 2
resources:
limits:
cpus: '0.5'
memory: 256M
# 商品服务
product-service:
build: ./services/product
container_name: product_service
restart: always
environment:
- DB_HOST=mysql
- REDIS_HOST=redis
depends_on:
- mysql
- redis
networks:
- microservices
deploy:
replicas: 2
resources:
limits:
cpus: '0.5'
memory: 256M
# MySQL数据库
mysql:
image: mysql:8.0
container_name: mysql_db
restart: always
environment:
- MYSQL_ROOT_PASSWORD=YourStrongPassword
- MYSQL_DATABASE=microservices
volumes:
- mysql_data:/var/lib/mysql
- ./mysql/init.sql:/docker-entrypoint-initdb.d/init.sql
networks:
- microservices
# Redis缓存
redis:
image: redis:7-alpine
container_name: redis_cache
restart: always
command: redis-server --requirepass YourRedisPassword
volumes:
- redis_data:/data
networks:
- microservices
networks:
microservices:
driver: bridge
volumes:
mysql_data:
redis_data:
六、MySQL主从复制与读写分离
数据库是Web应用的核心,也是最容易成为性能瓶颈的地方。通过主从复制实现读写分离,可以显著提升数据库的并发处理能力。
6.1 主从复制的原理
MySQL主从复制基于二进制日志(binlog)实现,工作流程如下:
┌─────────────────────────────────────────────────────────────────┐
│ MySQL主从复制工作流程 │
└─────────────────────────────────────────────────────────────────┘
Master主库 Slave从库
│ │
│ 1. 数据写入 │
▼ │
┌─────────┐ │
│ 执行SQL │ │
└────┬────┘ │
│ 2. 记录binlog │
▼ │
┌─────────┐ │
│ Binlog │──────────────────────────────────┤
└─────────┘ 3. I/O线程请求binlog │
▼
┌──────────────┐
│ Relay Log │
│ (中继日志) │
└──────┬───────┘
│ 4. SQL线程重放
▼
┌──────────────┐
│ 执行SQL │
└──────────────┘
MySQL支持三种复制方式:
|
类型 |
说明 |
优缺点 |
|
基于语句(STATEMENT) |
复制SQL语句 |
日志量小,但某些函数可能导致主从不一致 |
|
基于行(ROW) |
复制数据变更 |
数据一致性好,但日志量大 |
|
混合模式(MIXED) |
自动选择 |
综合两者优点 |
6.2 宝塔面板配置MySQL主从复制
环境准备
主库:192.168.1.101 (Node1)
从库1:192.168.1.102 (Node2)
从库2:192.168.1.103 (Node3)
MySQL版本:8.0
确保所有服务器已通过宝塔安装MySQL,版本保持一致。
步骤一:配置主库(Master)
在Node1的宝塔面板中,修改MySQL配置文件(/www/server/mysql/my.cnf):
[mysqld]
# 服务器唯一ID(必须唯一)
server-id = 1
# 开启二进制日志
log-bin = mysql-bin
# 二进制日志格式(推荐ROW)
binlog_format = ROW
# 设置需要复制的数据库(可选,不设置则复制全部)
# binlog-do-db = your_database
# 设置不需要复制的数据库
binlog-ignore-db = mysql
binlog-ignore-db = information_schema
binlog-ignore-db = performance_schema
binlog-ignore-db = sys
# 二进制日志过期时间(天)
expire_logs_days = 7
# 同步写入磁盘(安全但性能略低)
sync_binlog = 1
# GTID模式(MySQL 5.6+推荐)
gtid_mode = ON
enforce_gtid_consistency = ON
重启MySQL服务:
systemctl restart mysqld
# 或在宝塔面板中重启MySQL
步骤二:创建复制用户
在主库中执行:
-- 登录MySQL
mysql -u root -p
-- 创建复制专用用户
CREATE USER 'repl_user'@'192.168.1.%' IDENTIFIED WITH mysql_native_password BY 'YourReplPassword';
-- 授予复制权限
GRANT REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'repl_user'@'192.168.1.%';
-- 刷新权限
FLUSH PRIVILEGES;
-- 查看主库状态(记录File和Position值)
SHOW MASTER STATUS;
输出示例:
+------------------+----------+--------------+------------------+------------------------------------------+
| File | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set |
+------------------+----------+--------------+------------------+------------------------------------------+
| mysql-bin.000001 | 856 | | mysql,... | 3e11fa47-71ca-11e1-9e33-c80aa9429562:1-5 |
+------------------+----------+--------------+------------------+------------------------------------------+
步骤三:配置从库(Slave)
在Node2和Node3的MySQL配置文件中添加:
[mysqld]
# 服务器唯一ID(每台从库不同)
server-id = 2 # Node3设置为3
# 中继日志
relay-log = mysql-relay-bin
# 从库只读(推荐)
read_only = ON
super_read_only = ON
# GTID模式
gtid_mode = ON
enforce_gtid_consistency = ON
# 复制过滤(可选)
# replicate-do-db = your_database
replicate-ignore-db = mysql
replicate-ignore-db = information_schema
replicate-ignore-db = performance_schema
replicate-ignore-db = sys
重启MySQL服务后,配置主从关系:
-- 登录从库MySQL
mysql -u root -p
-- 停止从库复制(如果之前有配置)
STOP SLAVE;
-- 方式一:使用传统方式(指定binlog位置)
CHANGE MASTER TO
MASTER_HOST='192.168.1.101',
MASTER_USER='repl_user',
MASTER_PASSWORD='YourReplPassword',
MASTER_LOG_FILE='mysql-bin.000001',
MASTER_LOG_POS=856;
-- 方式二:使用GTID方式(推荐)
CHANGE MASTER TO
MASTER_HOST='192.168.1.101',
MASTER_USER='repl_user',
MASTER_PASSWORD='YourReplPassword',
MASTER_AUTO_POSITION=1;
-- 启动从库复制
START SLAVE;
-- 查看从库状态
SHOW SLAVE STATUS\G
步骤四:验证主从复制
查看从库状态,确认以下两个值都为Yes:
Slave_IO_Running: Yes
Slave_SQL_Running: Yes
在主库创建测试数据:
CREATE DATABASE test_repl;
USE test_repl;
CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(50));
INSERT INTO users VALUES (1, '莫潇羽');
在从库验证数据已同步:
USE test_repl;
SELECT * FROM users;
-- 应该能看到刚才插入的数据
6.3 读写分离实现方案
主从复制完成后,还需要实现读写分离——写操作走主库,读操作走从库。
方案一:应用层实现(推荐)
在应用代码中配置多数据源,根据操作类型选择数据源。以PHP为例:
<?php
// 数据库连接配置
// 作者:莫潇羽 | 无忆研习社
class Database {
private static $masterConfig = [
'host' => '192.168.1.101',
'port' => 3306,
'username' => 'app_user',
'password' => 'YourPassword',
'database' => 'your_db'
];
private static $slaveConfigs = [
[
'host' => '192.168.1.102',
'port' => 3306,
'username' => 'app_user',
'password' => 'YourPassword',
'database' => 'your_db',
'weight' => 1
],
[
'host' => '192.168.1.103',
'port' => 3306,
'username' => 'app_user',
'password' => 'YourPassword',
'database' => 'your_db',
'weight' => 1
]
];
// 获取主库连接(写操作)
public static function getMaster() {
return self::connect(self::$masterConfig);
}
// 获取从库连接(读操作,加权随机)
public static function getSlave() {
$slave = self::weightedRandom(self::$slaveConfigs);
return self::connect($slave);
}
private static function connect($config) {
$dsn = "mysql:host={$config['host']};port={$config['port']};dbname={$config['database']};charset=utf8mb4";
return new PDO($dsn, $config['username'], $config['password']);
}
private static function weightedRandom($items) {
$totalWeight = array_sum(array_column($items, 'weight'));
$random = mt_rand(1, $totalWeight);
$current = 0;
foreach ($items as $item)