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

一、前言

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

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

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

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

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

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

2.1 控制台入口

2.2 第一台实例配置

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

配置项 说明
计费类型 按量计费(测试)/包年包月(生产) 费用估算按包年包月计算
地域 华南1(广州) 广州地域支持MySQL和Redis服务
可用区 可用区A 与第二台实例分属不同可用区
规格 ecs.g3ie.xlargeecs.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做统一部署:

  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 把这两件事合在一起做了

  • 静态资源:边缘节点缓存,就近返回
  • 动态请求:不走公网直连,通过智能路由算法找一条最快最稳的链路回源,绕开拥堵节点,同时做协议层优化
相关推荐
掘金者阿豪3 小时前
《高可用读写分离集群实战》系列(一)
后端
Dilee3 小时前
Spring AI 2.0.0 Prompt 最小 Demo:system、user、template 到底怎么分工
后端
未秃头的程序猿3 小时前
Java 26正式发布!这3个新特性,让代码量直接减半
java·后端·面试
hunterandroid3 小时前
Compose 状态管理:remember、rememberSaveable 与状态提升
前端
小旭Coding3 小时前
卧靠!Go 传给前端的 int64 竟然变成了这个?
后端
用户298698530143 小时前
Word 文档文本查找与替换的 Java 实现方案
java·后端
星栈3 小时前
Dioxus 接数据库最容易写歪的 3 个地方:sqlx + SQLite 怎么接才顺
前端·rust·前端框架
晴虹4 小时前
vue3-scroll-more:横向滚动条-元素或页签过多滚动显示处理的组件
前端·vue.js