记录在公司把单服务器升级成多服务器架构过程

53 阅读16分钟

一、前言

现在在公司负责全栈开发在线教育系统,技术栈是Node.js + Nest.js + MySQL + Redis。之前一直跑在一台4核16GB的服务器上,前期用户量不大没什么问题。今年用户涨得比较快,日活从几百到了五六千,单机就有点顶不住了,接口响应从200ms左右飘到2秒以上,日志时不时有内存告警,WebSocket连接也经常断,用户上课体验比较差,一直在反馈系统问题。

在研究后决定重新搞一下架构,这次架构改造的核心思路是计算和存储分离,前端挂负载均衡,后端用托管数据库,不再像之前那样所有东西都挤在一台机器上。目标就是单台服务器挂了服务不能停,并发能力也提上来,数据库不能是单点。

本架构基于火山引擎广州地域,采用2台云服务器作为计算节点,分别部署在可用区A和可用区B,通过负载均衡器对外暴露ip方便域名解析dns来访问服务。

把mysql和redis单独出来,不放在服务器上面,数据安全,性能也会更好,也方便回滚和备份数据。

为了避免服务器和数据库之间访问速度慢,所以采用部署在同一个机房里面,采用内网ip来进行连接,还准备采用火山引擎全站加速DCDN来加速不同地域的接口访问速度,所以配置整体架构就是下面这样:

ChatGPT Image 2026年6月25日 10_17_10.png

二、第一步:创建云服务器实例(2台)

2.1 控制台入口

2.2 第一台实例配置

登录火山引擎控制台,在计算菜单下找到云服务器ECS,点了创建实例。

b9c4587857dbec124a85c19e5dd05a07.png

配置项说明
计费类型按量计费(测试)/包年包月(生产)费用估算按包年包月计算
地域华南1(广州)广州地域支持MySQL和Redis服务
可用区可用区A与第二台实例分属不同可用区
规格ecs.g3ie.xlargeecs.g3ie.large4核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和内存,两边都受影响。

e4f2bf0bd80ca34840002976d19ca287.png

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 版

21138d6d206a8995fbf3585f86a54c39.png

4.1 实例配置

配置项说明
地域华南1(广州)与云服务器同地域
可用区可用区A与MySQL区分可用区
实例类型主备实例高可用保障
版本Redis 7.0
规格2GB与原环境一致
网络选择和云服务器相同的VPC

Redis用的主备实例,2GB规格, 规格可以看自己项目使用情况,后面可以调整,版本7.0,放在可用区A。

主备是为了高可用,主库挂了备库自动切上来。

4.2 连接信息(创建后获取)

配置项
内网地址172.xx.xx.xx(自动分配)
端口6379
连接密码创建时必须设置

五、第五步:创建负载均衡器

72532edb5da0ef603ff05e9115f2251d.png

配置项
名称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允许TCP800.0.0.0/0HTTP流量
allow-https允许TCP4430.0.0.0/0HTTPS(后续配置)
allow-ssh允许TCP22您的办公网IPSSH管理

HTTP的80端口对0.0.0.0/0开放,HTTPS的443暂时留着后面配证书的时候用,SSH的22端口只允许办公室公网IP访问,避免暴露在公网上被扫。

七、第七步:服务器内服务端代码配置

两台服务器分别登录上去,把项目代码部署好。代码从代码仓库拉下来之后,主要就是改一下数据库和Redis的连接地址,之前用的是公网IP或者localhost,现在全要换成内网地址。

7.1 环境变量配置

项目里用的配置文件是config.js或者.env,里面数据库和Redis的地址改成火山引擎给的内网地址:

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

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。

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 启用配置

# 检查配置语法是否正确
nginx -t

# 重新加载Nginx配置
nginx -s reload

九、第九步:域名与 DNS 配置

9.1 DNS 记录

在域名注册商的控制台添加A记录,把域名指向负载均衡器的公网IP。

配置项
域名xxx.com
记录类型A 记录
解析值34.128.xxx.xxx
TTL600 秒(建议)

9.2 验证

DNS解析生效后,浏览器访问 http://xxx.com,看能不能正常打开页面,API接口能不能正常返回数据,WebSocket连接能不能建立。如果哪块有问题,优先检查一下服务器上的服务是不是正常跑着、端口对不对、负载均衡的健康检查是否通过。

十、后续计划

整体搭下来不算复杂,火山引擎控制台操作逻辑还算清晰,文档也基本能对上,基础架构搭完之后,还有几个事情需要陆续搞定,按优先级列一下:

10.1、Docker统一部署

目前两台服务器是分别手动部署的,后续维护起来比较麻烦,代码更新要登录两台机器分别拉代码重启,容易出错。计划引入Docker做统一部署:

  1. 制作项目镜像 写一个Dockerfile,把Node.js项目和依赖打包成镜像,推送到火山引擎的镜像仓库CR(Container Registry)。镜像打好tag,比如kcl-app:v1.0.0,每次发版打一个新tag。

  2. 部署容器化 两台服务器上分别安装Docker,写一个docker-compose.yml,把Node.js应用、Nginx都定义成服务。配置文件和环境变量用环境变量文件挂载进去,不要在镜像里写死。

  3. 容器统一管理 后续更新代码只需要构建新镜像,然后在两台服务器上执行docker pull拉取新镜像,docker-compose up -d重启容器就行。两台机器操作可以写个简单脚本批量执行,不用再手动git pull和pm2重启了。

  4. 镜像版本管理 CR仓库里保留最近5个版本的镜像,方便出问题的时候快速回滚到上一个稳定版本。

  5. 后续可考虑Kubernetes 如果节点数量继续增加,单纯靠手动操作容器就有点吃力了,可以调研火山引擎的VKE(容器服务),把服务器纳入Kubernetes集群统一调度,滚动更新、自动扩缩容都交给K8s来处理。

10.2、数据库相关

  1. MySQL自动备份 MySQL控制台里配置自动备份策略,设成每天凌晨2点备份一次,保留7天。另外手动做一次全量备份,以防万一。

  2. MySQL慢查询监控 开启慢查询日志,阈值设成2秒,每周Review一次慢查询列表,把耗时的SQL优化掉。

  3. 数据库代理监控 关注数据库代理的连接数和请求延迟,如果代理节点CPU过高,考虑增加代理节点数。

10.3、HTTPS与安全

  1. HTTPS证书配置 SSL证书已经在申请了,等签发下来之后,负载均衡这边加一个HTTPS监听器(443端口),把证书配上去。HTTP 80端口配置强制跳转到HTTPS。

  2. 安全组策略收紧 当前安全组规则还比较粗,后续按最小权限原则细化:数据库和Redis的安全组只允许两台云服务器的内网IP访问,不允许公网直接访问。负载均衡的安全组只开放80和443,其他端口全部禁止。

  3. 服务器系统更新 每月定时更新系统补丁,执行apt update && apt upgrade -y,更新前先在测试环境验证。

  4. 日志采集配置 配置云日志服务,把Nginx访问日志和Node.js应用日志采集到日志中心,方便后续按关键字检索和排障。

10.4、高可用验证

  1. 多可用区容灾验证 计划找个低峰期,手动把可用区A的一台服务器停机,观察负载均衡是否自动把所有流量切到可用区B的机器上,验证跨可用区容灾是否生效。

  2. MySQL主备切换演练 找个维护窗口,在控制台手动触发一次MySQL主备切换,验证切换耗时和切换后应用是否自动重连成功,心里有个底。

  3. 服务器镜像备份 两台服务器的系统盘分别做个镜像快照,万一机器坏了能快速恢复。

10.5、监控告警

  1. 云监控告警配置 云监控里把下面几个指标配上告警,超过阈值发短信通知:

    • MySQL CPU使用率 > 80%
    • MySQL连接数 > 1000
    • MySQL磁盘使用率 > 85%
    • MySQL慢查询数 > 10条/分钟
    • 服务器CPU使用率 > 85%
    • 服务器内存使用率 > 90%
    • 服务器磁盘使用率 > 85%
    • 负载均衡后端异常服务器数量 > 0
  2. 容器资源监控 如果上了Docker,配置容器级别的监控,关注每个容器的CPU和内存使用情况,避免某个容器把整台机器的资源吃光。

  3. 云资源费用告警 在费用中心设置预算告警,当月费用超过预算的80%时发通知提醒,避免资源超配导致费用失控。

10.6、性能与容量

  1. 性能压测 用wrk或者JMeter压一下线上环境的真实QPS上限,看看瓶颈到底在哪,为后续扩容做准备。

  2. 容量评估 根据压测结果和业务增长趋势,评估半年内的资源需求,提前规划扩容方案。MySQL如果持续吃满2核8GB的规格,考虑升级到4核16GB或者再增加只读节点。

  3. Redis缓存策略优化 检查Redis的内存使用率和淘汰策略,确认key的过期时间设置合理,避免内存被打满。当前配置是2GB,如果缓存命中率持续走高,考虑升级到4GB。

10.7 火山引擎全站加速 DCDN 配置

全站加速(Dynamic Content Delivery Network,DCDN)  是火山引擎推出的一项网络加速服务,可以理解为传统 CDN 的升级版,开启后可以有效优化接口的访问速度。

核心区别在于:

传统 CDN 只擅长加速图片、视频、网页文件这些静态资源,节点上缓存了就能直接返回。但遇到用户登录、查询订单、提交表单这类动态请求,CDN 没法缓存,只能透传给源站,跨国或者跨网的时候就很慢

DCDN 把这两件事合在一起做了

  • 静态资源:边缘节点缓存,就近返回
  • 动态请求:不走公网直连,通过智能路由算法找一条最快最稳的链路回源,绕开拥堵节点,同时做协议层优化