一、前言
现在在公司负责全栈开发在线教育系统,技术栈是Node.js + Nest.js + MySQL + Redis。之前一直跑在一台4核16GB的服务器上,前期用户量不大没什么问题。今年用户涨得比较快,日活从几百到了五六千,单机就有点顶不住了,接口响应从200ms左右飘到2秒以上,日志时不时有内存告警,WebSocket连接也经常断,用户上课体验比较差,一直在反馈系统问题。
在研究后决定重新搞一下架构,这次架构改造的核心思路是计算和存储分离,前端挂负载均衡,后端用托管数据库,不再像之前那样所有东西都挤在一台机器上。目标就是单台服务器挂了服务不能停,并发能力也提上来,数据库不能是单点。
本架构基于火山引擎广州地域,采用2台云服务器作为计算节点,分别部署在可用区A和可用区B,通过负载均衡器对外暴露ip方便域名解析dns来访问服务。
把mysql和redis单独出来,不放在服务器上面,数据安全,性能也会更好,也方便回滚和备份数据。
为了避免服务器和数据库之间访问速度慢,所以采用部署在同一个机房里面,采用内网ip来进行连接,还准备采用火山引擎全站加速DCDN来加速不同地域的接口访问速度,所以配置整体架构就是下面这样:

二、第一步:创建云服务器实例(2台)
2.1 控制台入口
- 导航:控制台首页 → 计算 → 云服务器
- 直达链接:console.volcengine.com/ecs
2.2 第一台实例配置
登录火山引擎控制台,在计算菜单下找到云服务器ECS,点了创建实例。

| 配置项 | 值 | 说明 |
|---|---|---|
| 计费类型 | 按量计费(测试)/包年包月(生产) | 费用估算按包年包月计算 |
| 地域 | 华南1(广州) |
广州地域支持MySQL和Redis服务 |
| 可用区 | 可用区A |
与第二台实例分属不同可用区 |
| 规格 | ecs.g3ie.xlarge → ecs.g3ie.large |
4核16GB,已按需求下调 |
| 镜像 | Ubuntu 22.04 LTS |
与原环境一致 |
| 系统盘 | 40GB SSD |
保持不变 |
| 网络 | 选择默认VPC和子网 | 确保网络互通 |
| 公网IP | 分配 | 方便临时调试和出问题的时候直接连服务器排查 |
| 安全组 | 暂不指定,后续创建 | 单独配置防火墙规则 |
第一台起名叫kcl-server-1,选了华南1(广州)的可用区A。规格选的ecs.g3ie.large,4核16GB,这个配置是压测下来比较合适的,再小CPU容易打满,再大就浪费钱了。镜像用的Ubuntu 22.04 LTS,系统盘40GB SSD。网络直接用默认VPC。安全组先不绑,后面统一配置。
2.3 第二台实例配置
| 配置项 | 值 |
|---|---|
| 名称 | kcl-server-2 |
| 地域 | 华南1(广州) |
| 可用区 | 可用区B |
| 规格 | ecs.g3ie.large(4核16GB) |
| 其他 | 与第一台实例保持一致 |
第二台kcl-server-2配置一模一样,唯一区别是可用区选的B,这样就算广州某个可用区出问题,另一个还能顶上。两台都创建好之后,确认了一下它们在同一个VPC里,内网能通。最开始选到了不同的子网,内网不通排查了半天,这次长记性了。
三、第三步:创建云数据库 MySQL 版(含读写分离)
数据库单独拎出来放到云上,不跟服务器挤一起。原来单机的时候MySQL和Node.js抢CPU和内存,两边都受影响。

3.1 主节点实例配置
| 配置项 | 值 | 说明 |
|---|---|---|
| 计费类型 | 按量计费(测试)/包年包月(生产) | |
| 地域 | 华南1(广州) |
与云服务器同地域 |
| 可用区 | 可用区B |
|
| 数据库版本 | MySQL 8.0 |
|
| 系列 | 高可用版 |
主备架构,自动故障切换 |
| 规格 | 2核8GB |
与服务器规格对齐,控制成本 |
| 存储空间 | 30GB |
根据数据量调整 |
| 网络 | 选择和云服务器相同的VPC |
主实例选了广州可用区B,MySQL 8.0,高可用版,主备自动切换,规格2核8GB,存储30GB,网络跟服务器同一个VPC。内网延迟测下来大概0.2ms,影响可以忽略。
3.2 增加只读节点(实现读写分离)
在主实例创建完成后,通过增加只读节点扩展读能力。
| 配置项 | 值 | 说明 |
|---|---|---|
| 节点类型 | 只读节点 |
|
| 可用区 | 可用区A |
与主节点分属不同可用区,提升容灾性 |
| 规格 | 2核8GB |
与主节点规格保持一致 |
| 数量 | 1个(可按需扩展) |
最多支持10个只读节点 |
建完主库之后加了一个只读节点,规格也是2核8GB,放在可用区A。主要是AI助教这种场景读多写少,压测的时候主库CPU在并发上来后直接飙到80%以上,读请求和写请求互相抢,加完只读节点之后好了很多。
⚠️ 注意:增加只读节点期间会有连接闪断,建议在业务低峰期操作。
3.3 开启数据库代理(自动读写分离)
数据库代理是读写分离的核心组件,提供统一的连接入口并自动将读/写请求路由到对应节点。
| 配置项 | 值 | 说明 |
|---|---|---|
| 代理节点数 | 2个(推荐) |
保证代理服务自身高可用 |
| 读写分离 | 开启 |
自动将读请求转发至只读节点 |
| 事务拆分 | 开启(可选) |
将事务中的读请求分离,进一步优化 |
| 连接池 | 按需开启 | 减少频繁建连开销 |
开了数据库代理之后,应用只需要连代理提供的统一地址就行,代理自动把SELECT转到只读节点,INSERT/UPDATE/DELETE走主节点。代码里完全不用区分主库和只读库,省了很多事。代理开了2个节点,避免代理本身挂了。
数据库代理地址 :创建完成后,系统会生成一个代理连接地址 (proxy-xxx.volcengine.com),应用程序通过该地址访问数据库,无需感知主库和只读库的具体地址。
3.4 连接信息(创建后获取)
| 配置项 | 值 | 用途 |
|---|---|---|
| 主节点内网地址 | 172.xx.xx.xx:3306(自动分配) |
管理/维护用(一般不直连) |
| 代理连接地址 | proxy-xxx.volcengine.com:3306 |
应用连接地址(推荐) |
| 数据库名 | kcl_database(手动创建) |
|
| 用户名 | kcl_app_user(手动创建) |
|
| 只读节点内网地址 | 172.xx.xx.xx:3306(自动分配) |
管理/维护用(一般不直连) |
✅ 最佳实践 :应用程序应始终使用代理连接地址进行连接,这样代理组件会自动处理读写分离,无需在代码中区分主库和只读库。
四、第四步:创建缓存数据库 Redis 版

4.1 实例配置
| 配置项 | 值 | 说明 |
|---|---|---|
| 地域 | 华南1(广州) |
与云服务器同地域 |
| 可用区 | 可用区A |
与MySQL区分可用区 |
| 实例类型 | 主备实例 |
高可用保障 |
| 版本 | Redis 7.0 |
|
| 规格 | 2GB |
与原环境一致 |
| 网络 | 选择和云服务器相同的VPC |
Redis用的主备实例,2GB规格, 规格可以看自己项目使用情况,后面可以调整,版本7.0,放在可用区A。
主备是为了高可用,主库挂了备库自动切上来。
4.2 连接信息(创建后获取)
| 配置项 | 值 |
|---|---|
| 内网地址 | 172.xx.xx.xx(自动分配) |
| 端口 | 6379 |
| 连接密码 | 创建时必须设置 |
五、第五步:创建负载均衡器

| 配置项 | 值 |
|---|---|
| 名称 | kcl-clb |
| 地域 | 华南1(广州) |
| 网络类型 | 公网 |
| 规格 | 小型I |
| 公网IP | 系统自动分配 |
| 后端协议/端口 | HTTP:8050 |
| 监听协议/端口 | HTTP:80 |
| 空闲超时时间 | 1800秒 |
| 健康检查路径 | /health |
登录火山引擎控制台,在网络与CDN菜单下找到负载均衡,点击创建实例。地域选华南1(广州),网络类型选公网,系统会自动分配一个公网IP,用户流量都从这个IP进来。规格选的小型I,目前的量级用这个够用了。
5.1 监听器配置
监听器配置这里:监听端口是80,用户访问负载均衡的公网IP或者域名时走的是HTTP 80端口。后端端口是8050,负载均衡收到请求后会转发到nginx的80端口上。由nginx再转发/api接口到服务端8050端口, 后面会由nginx的配置。
5.2 健康检查
健康检查路径填了/health,这个接口在Node.js应用里写好了,返回200就代表服务正常。负载均衡会定时往这个地址发请求做健康检查,如果连续几次收不到正常响应,就会把这一台服务器从后端组里摘掉,流量全部打到另一台正常的机器上。等恢复健康了再自动加回来。
5.3 配置后端服务器组
创建完监听器之后,需要配置后端服务器组。在负载均衡控制台找到刚才创建的实例,点进去之后有个"后端服务器组"的菜单,点击创建后端服务器组。协议选HTTP,端口填8050,调度算法用加权轮询(权重默认都是10,两台机器均摊流量)。
然后把之前创建的两台云服务器添加进来,具体操作是点击"添加后端服务器",在弹窗里勾选kcl-server-1和kcl-server-2,端口统一填8050,权重保持默认的10。保存之后,负载均衡就会按照1:1的比例把请求分发到两台服务器上。如果后面某台机器的配置更高,可以调大权重让它多承担一些流量。
六、第六步:安全组规则
安全组单独配了一下。
| 规则名称 | 策略 | 协议 | 端口 | 源IP | 用途 |
|---|---|---|---|---|---|
allow-http |
允许 | TCP | 80 | 0.0.0.0/0 |
HTTP流量 |
allow-https |
允许 | TCP | 443 | 0.0.0.0/0 |
HTTPS(后续配置) |
allow-ssh |
允许 | TCP | 22 | 您的办公网IP |
SSH管理 |
HTTP的80端口对0.0.0.0/0开放,HTTPS的443暂时留着后面配证书的时候用,SSH的22端口只允许办公室公网IP访问,避免暴露在公网上被扫。
七、第七步:服务器内服务端代码配置
两台服务器分别登录上去,把项目代码部署好。代码从代码仓库拉下来之后,主要就是改一下数据库和Redis的连接地址,之前用的是公网IP或者localhost,现在全要换成内网地址。
7.1 环境变量配置
项目里用的配置文件是config.js或者.env,里面数据库和Redis的地址改成火山引擎给的内网地址:
javascript
const config = {
mysql: {
host: '172.xx.xx.xx', // MySQL代理的内网地址
port: 3306,
username: 'username',
password: '你的数据库密码',
database: 'kcl_database',
},
redis: {
host: '172.xx.xx.xx', // Redis实例的内网地址
port: 6379,
password: '你的Redis密码',
db: 8,
},
// 其他配置项保持不变
}
启动项目之后确认一下服务正常跑在8050端口,负载均衡的健康检查才能通。两台服务器都做同样的操作。
八、第八步:Nginx 配置(Web端)
8.1 安装 Nginx
bash
sudo apt-get update
sudo apt-get install -y nginx
8.2 配置文件位置
配置文件放在 /etc/nginx/config/kcl_web.conf
8.3 完整 Nginx 配置
目的是托管前端web代码的同时,也通过/api转发到服务端接口端口上,要注意提前申请ssl正式,我用的是certbot。
bash
server {
listen 80;
listen 443 ssl http2;
server_name xxx.com;
# SSL证书配置
ssl_certificate /etc/letsencrypt/live/xxx.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/xxx.com/privkey.pem;
ssl_session_timeout 5m;
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
location / {
root /mnt/new_disk/project/prod/kcl-app-web/dist;
index index.html;
try_files $uri $uri/ /index.html;
if ($request_filename ~* ^.*?.(html|htm)$) {
add_header Cache-Control "private, no-store, no-cache, must-revalidate, proxy-revalidate";
}
if ($request_filename ~* ^.*?.(js|css|jpg|jpeg|png|gif|ico|txt|svg|woff|woff2|ttf|eot|otf)$) {
expires max;
add_header Cache-Control "public, max-age=31536000, immutable";
}
}
# API代理
location /api {
proxy_pass http://127.0.0.1:8050;
if ($request_method = OPTIONS) {
return 204;
}
}
# WebSocket转发
location /socket.io/ {
proxy_pass http://127.0.0.1:8050/socket.io/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
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_read_timeout 3600s;
proxy_send_timeout 3600s;
}
}
8.4 启用配置
bash
# 检查配置语法是否正确
nginx -t
# 重新加载Nginx配置
nginx -s reload
九、第九步:域名与 DNS 配置
9.1 DNS 记录
在域名注册商的控制台添加A记录,把域名指向负载均衡器的公网IP。
| 配置项 | 值 |
|---|---|
| 域名 | xxx.com |
| 记录类型 | A 记录 |
| 解析值 | 34.128.xxx.xxx |
| TTL | 600 秒(建议) |
9.2 验证
DNS解析生效后,浏览器访问 http://xxx.com,看能不能正常打开页面,API接口能不能正常返回数据,WebSocket连接能不能建立。如果哪块有问题,优先检查一下服务器上的服务是不是正常跑着、端口对不对、负载均衡的健康检查是否通过。
十、后续计划
整体搭下来不算复杂,火山引擎控制台操作逻辑还算清晰,文档也基本能对上,基础架构搭完之后,还有几个事情需要陆续搞定,按优先级列一下:
10.1、Docker统一部署
目前两台服务器是分别手动部署的,后续维护起来比较麻烦,代码更新要登录两台机器分别拉代码重启,容易出错。计划引入Docker做统一部署:
-
制作项目镜像 写一个Dockerfile,把Node.js项目和依赖打包成镜像,推送到火山引擎的镜像仓库CR(Container Registry)。镜像打好tag,比如
kcl-app:v1.0.0,每次发版打一个新tag。 -
部署容器化 两台服务器上分别安装Docker,写一个docker-compose.yml,把Node.js应用、Nginx都定义成服务。配置文件和环境变量用环境变量文件挂载进去,不要在镜像里写死。
-
容器统一管理 后续更新代码只需要构建新镜像,然后在两台服务器上执行
docker pull拉取新镜像,docker-compose up -d重启容器就行。两台机器操作可以写个简单脚本批量执行,不用再手动git pull和pm2重启了。 -
镜像版本管理 CR仓库里保留最近5个版本的镜像,方便出问题的时候快速回滚到上一个稳定版本。
-
后续可考虑Kubernetes 如果节点数量继续增加,单纯靠手动操作容器就有点吃力了,可以调研火山引擎的VKE(容器服务),把服务器纳入Kubernetes集群统一调度,滚动更新、自动扩缩容都交给K8s来处理。
10.2、数据库相关
-
MySQL自动备份 MySQL控制台里配置自动备份策略,设成每天凌晨2点备份一次,保留7天。另外手动做一次全量备份,以防万一。
-
MySQL慢查询监控 开启慢查询日志,阈值设成2秒,每周Review一次慢查询列表,把耗时的SQL优化掉。
-
数据库代理监控 关注数据库代理的连接数和请求延迟,如果代理节点CPU过高,考虑增加代理节点数。
10.3、HTTPS与安全
-
HTTPS证书配置 SSL证书已经在申请了,等签发下来之后,负载均衡这边加一个HTTPS监听器(443端口),把证书配上去。HTTP 80端口配置强制跳转到HTTPS。
-
安全组策略收紧 当前安全组规则还比较粗,后续按最小权限原则细化:数据库和Redis的安全组只允许两台云服务器的内网IP访问,不允许公网直接访问。负载均衡的安全组只开放80和443,其他端口全部禁止。
-
服务器系统更新 每月定时更新系统补丁,执行
apt update && apt upgrade -y,更新前先在测试环境验证。 -
日志采集配置 配置云日志服务,把Nginx访问日志和Node.js应用日志采集到日志中心,方便后续按关键字检索和排障。
10.4、高可用验证
-
多可用区容灾验证 计划找个低峰期,手动把可用区A的一台服务器停机,观察负载均衡是否自动把所有流量切到可用区B的机器上,验证跨可用区容灾是否生效。
-
MySQL主备切换演练 找个维护窗口,在控制台手动触发一次MySQL主备切换,验证切换耗时和切换后应用是否自动重连成功,心里有个底。
-
服务器镜像备份 两台服务器的系统盘分别做个镜像快照,万一机器坏了能快速恢复。
10.5、监控告警
-
云监控告警配置 云监控里把下面几个指标配上告警,超过阈值发短信通知:
- MySQL CPU使用率 > 80%
- MySQL连接数 > 1000
- MySQL磁盘使用率 > 85%
- MySQL慢查询数 > 10条/分钟
- 服务器CPU使用率 > 85%
- 服务器内存使用率 > 90%
- 服务器磁盘使用率 > 85%
- 负载均衡后端异常服务器数量 > 0
-
容器资源监控 如果上了Docker,配置容器级别的监控,关注每个容器的CPU和内存使用情况,避免某个容器把整台机器的资源吃光。
-
云资源费用告警 在费用中心设置预算告警,当月费用超过预算的80%时发通知提醒,避免资源超配导致费用失控。
10.6、性能与容量
-
性能压测 用wrk或者JMeter压一下线上环境的真实QPS上限,看看瓶颈到底在哪,为后续扩容做准备。
-
容量评估 根据压测结果和业务增长趋势,评估半年内的资源需求,提前规划扩容方案。MySQL如果持续吃满2核8GB的规格,考虑升级到4核16GB或者再增加只读节点。
-
Redis缓存策略优化 检查Redis的内存使用率和淘汰策略,确认key的过期时间设置合理,避免内存被打满。当前配置是2GB,如果缓存命中率持续走高,考虑升级到4GB。
10.7 火山引擎全站加速 DCDN 配置
全站加速(Dynamic Content Delivery Network,DCDN) 是火山引擎推出的一项网络加速服务,可以理解为传统 CDN 的升级版,开启后可以有效优化接口的访问速度。
核心区别在于:
传统 CDN 只擅长加速图片、视频、网页文件这些静态资源 ,节点上缓存了就能直接返回。但遇到用户登录、查询订单、提交表单这类动态请求,CDN 没法缓存,只能透传给源站,跨国或者跨网的时候就很慢
DCDN 把这两件事合在一起做了
- 静态资源:边缘节点缓存,就近返回
- 动态请求:不走公网直连,通过智能路由算法找一条最快最稳的链路回源,绕开拥堵节点,同时做协议层优化