作为一名个人开发者,我只有一台云服务器,预算有限。当我搜索"高可用"时,满屏都是K8s、Service Mesh、混沌工程......这些方案看起来很棒,但对我这个体量的项目,是不是杀鸡用牛刀?
一、引子:一个困惑引发的思考
先说一下我的项目背景:
- 在线简历:一个静态展示页面
- 在线音乐/视频:几百首歌、几十个视频,Nginx直接 serve
- 后台管理系统:用 Gin 写的管理端
- 数据存储:mysql + 本地磁盘文件
说白了,就是一个小型个人项目。
那天我在思考一个问题:如果我的服务器宕机了怎么办?如果进程崩溃了怎么办?如果数据库丢了怎么办?
于是我打开了技术社区,搜索"高可用"。
搜索结果让我陷入沉思:K8s、Docker Swarm、Consul、etcd、服务网格、混沌工程......每一个方案都看起来"很专业",但每一个方案的复杂度都让我望而却步。
我只是想解决几个简单问题:
- 进程崩了能自动重启
- 服务器重启后服务能自己起来
- 数据库能自动备份
真的需要把K8s那一整套搬过来吗? 而且我安装过Prometheus + Grafana + Alertmanager 监控方案,根本就跑不起来,服务器配置太低,一运行起来服务器就卡死了,
这篇文章,就是我折腾了一圈后,给出的答案。
二、重新定义:什么是对中小项目真正有用的"高可用"
在开始之前,我们先纠正一个认知误区。
高可用 ≠ 99.999%的可用性。
99.999%意味着一年宕机时间不超过5分钟,那是阿里云、AWS这种体量需要考虑的事情。对于中小项目,99.9%就够了------一年宕机8.5小时,完全可以接受。
那我们真正需要解决的是什么问题?
| 故障场景 | 发生的可能性 | 影响 | 解决方案 | 投入成本 |
|---|---|---|---|---|
| 进程崩溃 | 中(代码bug、OOM) | 服务不可用,需要手动重启 | 进程守护,自动重启 | 几乎为0 |
| 服务器重启 | 低(系统更新、意外重启) | 服务全挂,需要手动启动 | 开机自启配置 | 几乎为0 |
| 数据库损坏 | 低(硬盘故障、误操作) | 数据丢失,业务受损 | 定期自动备份 | 5分钟配置 |
| 流量暴涨 | 极低(个人项目) | 响应慢,但影响有限 | 单机够用,先不管 | 0 |
| 机器永久损坏 | 极低(云服务器有冗余) | 服务全挂 | 云厂商保证 | 0 |
看到没?用最简单的手段,就能解决80%以上的故障场景。
这就是中小项目的务实高可用方案。
三、方案一:单机部署的核心配置(我现在用的方案)
我的服务器配置很简单:一台云服务器(2核2G),安装 Nginx、Gin后端、mysql,所有文件存本地磁盘。
架构图长这样:
text
scss
┌─────────────────────────┐
│ 用户 │
└───────────┬─────────────┘
│
▼
┌─────────────────────────┐
│ Nginx │
│ (静态文件 + 反向代理) │
└───────────┬─────────────┘
│
┌──────────────────┼──────────────────┐
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 静态简历 │ │ 音乐/视频 │ │ Gin后端 │
│ (HTML/CSS) │ │ (本地文件) │ │ (API服务) │
└─────────────┘ └─────────────┘ └──────┬──────┘
│
▼
┌─────────────┐
│ mysql │
│ (数据存储) │
└─────────────┘
3.1 用 Systemd 管理服务,实现开机自启和崩溃重启
为什么不直接用 Docker?
因为我这个项目足够简单:一个编译好的 Gin 二进制文件,依赖关系清晰。用 Systemd 直接管理进程,比再套一层 Docker 更轻量、更直接。
步骤1:创建 service 文件
bash
bash
sudo vim /etc/systemd/system/gin.service
写入以下内容:
ini
ini
[Unit]
Description=Gin Data Management Server
After=network.target postgresql.service
[Service]
Type=simple
User=www-data
WorkingDirectory=/opt/myapp
ExecStart=/opt/myapp/gin-server
Restart=always # 挂了自动重启
RestartSec=5 # 5秒后重试
Environment="DB_HOST=localhost"
Environment="DB_PORT=5432"
[Install]
WantedBy=multi-user.target
步骤2:启用并启动服务
bash
bash
sudo systemctl daemon-reload
sudo systemctl enable gin.service # 开机自启
sudo systemctl start gin.service
步骤3:验证
bash
bash
# 查看服务状态
sudo systemctl status gin.service
# 查看日志
sudo journalctl -u gin.service -f
现在,我的 Gin 服务具备了:
- ✅ 服务器重启后自动启动
- ✅ 进程崩溃后自动重启
- ✅ 统一的日志管理
3.2 Nginx:系统包安装,自带 Systemd 管理
Nginx 就更简单了,直接用系统包管理安装,它自带了 systemd 服务。
bash
bash
# Ubuntu/Debian
sudo apt install -y nginx
# 查看Nginx的systemd配置,它默认就有 Restart=on-failure
cat /lib/systemd/system/nginx.service | grep Restart
# 输出:Restart=on-failure
sudo systemctl enable nginx # 开机自启
sudo systemctl start nginx
Nginx 配置也很简单:
nginx
ini
// 只做示例,并不是我的真实配置
server {
listen 80;
server_name your-domain.com;
# 简历静态页
root /var/www/resume;
# Gin API 反向代理
location /api/ {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
3.3 PostgreSQL:同样用 Systemd 管理
bash
vbscript
sudo apt install -y mysql-server
sudo systemctl enable mysql-server
sudo systemctl start mysql-server
到此为止,我的三个核心组件都有了进程守护和开机自启。
但是,还有一个最重要的问题没解决:数据备份。
3.4 数据库自动备份:最重要的防线
说实话,进程崩了可以重启,服务器挂了可以重买,但数据丢了就真的没了。
所以备份比高可用更重要。
步骤1:创建备份脚本
bash
bash
sudo mkdir -p /data/backup/mysql /data/backup/scripts
cd /data/backup/scripts
sudo vim mysql_backup.sh
脚本内容:
bash
bash
#!/bin/bash
# ========== 配置区域 ==========
BACKUP_DIR="/data/backup/mysql"
DATE=$(date +%Y%m%d_%H%M%S)
RETENTION_DAYS=30
DB_USER="root"
DB_PASS="你的数据库密码"
DB_NAME="shangcheng" # 你的数据库名
# =============================
mkdir -p ${BACKUP_DIR}
echo "开始备份数据库: ${DB_NAME} 时间: $(date '+%Y-%m-%d %H:%M:%S')"
mysqldump -u${DB_USER} -p${DB_PASS} ${DB_NAME} | gzip > ${BACKUP_DIR}/${DB_NAME}_${DATE}.sql.gz
if [ $? -eq 0 ]; then
echo "✅ 备份成功: ${BACKUP_DIR}/${DB_NAME}_${DATE}.sql.gz"
find ${BACKUP_DIR} -name "*.sql.gz" -mtime +${RETENTION_DAYS} -delete
echo "已清理 ${RETENTION_DAYS} 天前的旧备份"
else
echo "❌ 备份失败"
exit 1
fi
步骤2:添加执行权限并测试
bash
bash
sudo chmod +x mysql_backup.sh
sudo ./mysql_backup.sh
# 看到输出:
# 开始备份数据库: shangcheng 时间: 2026-06-08 08:08:31
# ✅ 备份成功: /data/backup/mysql/shangcheng_20260608_080831.sql.gz
# 已清理 30 天前的旧备份
步骤3:配置定时任务
bash
crontab -e
添加一行(每天凌晨2:30执行):
cron
javascript
30 2 * * * /data/backup/scripts/mysql_backup.sh >> /var/log/mysql_backup.log 2>&1
步骤4:验证备份文件可用
bash
bash
# 查看备份文件
ls -lh /data/backup/mysql/
# -rw-r--r-- 1 root root 156K 6月 8 08:08 shangcheng_20260608_080831.sql.gz
# 验证内容
gunzip -c /data/backup/mysql/shangcheng_20260608_080831.sql.gz | head -20
# 应该能看到 CREATE TABLE、INSERT INTO 等 SQL 语句
看到备份文件生成的那一刻,我悬着的心终于放下了。
四、方案二:进阶高可用(当你有了2-3台服务器)
如果你的项目长大了,日活上了几千,或者业务不能接受停机,那可以考虑升级方案。
这时候,云服务是你的朋友,不是敌人。
4.1 放弃自建 Nginx 主备,直接用云负载均衡器
先说个结论:不要自己搭 Keepalived + Nginx 主备。
为什么?因为运维成本高于云服务费用。
看看对比:
| 维度 | 自建 Nginx+Keepalived | 云负载均衡器(CLB/SLB) |
|---|---|---|
| 自身高可用 | 需要自己配置主备、VIP漂移 | 云厂商保证99.95%+ |
| 故障切换 | 3-10秒 | 秒级自动 |
| 运维成本 | 维护2台Nginx+配置 | 控制台点几下 |
| 成本 | 2台服务器+公网IP | 20-30元/月 |
云负载均衡器的使用超级简单(以腾讯云CLB为例):
- 购买一个负载均衡实例(会自动分配一个VIP)
- 配置监听器:HTTP协议,80/443端口
- 绑定后端云服务器:把2-3台CVM加进去
- 开启健康检查:自动摘除故障节点
就这么简单,不需要碰 Keepalived、VIP、心跳检测。
架构变成了这样:
text
scss
┌─────────────────────────┐
│ 用户 │
└───────────┬─────────────┘
│
▼
┌─────────────────────────┐
│ 云负载均衡器(CLB) │
│ (云厂商托管,高可用) │
└───────────┬─────────────┘
│
┌───────────────────┼───────────────────┐
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ CVM1 │ │ CVM2 │ │ CVM3 │
│ (应用) │ │ (应用) │ │ (应用) │
└─────────────┘ └─────────────┘ └─────────────┘
│ │ │
└───────────────────┼───────────────────┘
│
▼
┌─────────────────────────┐
│ 云Redis + 云RDS │
│ (托管式高可用) │
└─────────────────────────┘
4.2 Session/JWT 状态共享
当你有多个应用实例时,会面临一个问题:用户登录状态怎么共享?
最简单的方案:JWT + Redis存状态。
这个方案的好处是:
- JWT负责身份认证(无状态、跨端友好)
- Redis负责状态吊销(解决JWT"不能踢人下线"的痛点)
代码示例(Gin中间件):
go
go
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("Authorization")
if token == "" {
c.JSON(401, gin.H{"error": "未提供token"})
c.Abort()
return
}
// 1. 验证JWT签名
claims, err := ValidateJWT(token)
if err != nil {
c.JSON(401, gin.H{"error": "无效token"})
c.Abort()
return
}
// 2. 检查Redis中是否存在(实现踢人下线)
exists := redisClient.Exists(c, "user:"+claims.UserID+":token").Val()
if exists == 0 {
c.JSON(401, gin.H{"error": "token已失效"})
c.Abort()
return
}
c.Set("userID", claims.UserID)
c.Next()
}
}
4.3 数据库高可用:直接上云数据库
自建数据库主从,需要配置复制、写切换脚本、监控......太麻烦了。
直接换成云数据库RDS,自带高可用版:
| 对比 | 自建主从 | 云RDS高可用版 |
|---|---|---|
| 主备切换 | 手动或写脚本 | 自动,秒级切换 |
| 数据备份 | 自己写脚本 | 自动备份,支持按时间点恢复 |
| 监控告警 | 自己搭 | 控制台直接看 |
| 成本 | 2台服务器 | 约200元/月 |
200元/月买一个不用操心的数据库,我觉得很值。
4.4 成本核算
升级到进阶方案后的月成本:
| 组件 | 规格 | 月成本 |
|---|---|---|
| 云负载均衡CLB | 标准型 | ~30元 |
| 云服务器CVM | 2核4G × 3台 | ~150元/台 ×3 = 450元 |
| 云Redis | 1GB主从版 | ~80元 |
| 云RDS | 1核1G高可用版 | ~200元 |
| 合计 | ~760元/月 |
760元/月,换来99.9%的可用性,企业级的高可用架构。
五、横向对比:各个方案怎么选
我整理了一个完整的对比表格,方便你做选择:
| 维度 | 单机+Systemd | 单机+Docker | 多机+云负载均衡 | K8s |
|---|---|---|---|---|
| 月成本 | ~150元 | ~150元 | ~600-800元 | 1000+元 |
| 运维复杂度 | 低 | 中 | 中低 | 高 |
| 学习成本 | 低 | 中 | 中 | 高 |
| 可用性 | ~99% | ~99% | 99.9% | 99.99% |
| 扩容速度 | 手动换配置 | 手动起容器 | 自动弹性 | 自动弹性 |
| 适用场景 | 个人项目、内部系统 | 标准化部署 | 对外服务、日活几千 | 大型微服务 |
我的建议:
- 刚起步:选"单机+Systemd",性价比最高
- 需要标准化:选"单机+Docker",环境一致性更好
- 需要对外服务:选"多机+云负载均衡"
- 团队够大、业务够复杂:再考虑K8s
六、JWT vs Session 的深度辨析
这个话题经常被讨论,我也踩过坑,这里说说我的理解。
6.1 常见误区
误区1:"JWT不能踢人下线"
这是指纯无状态的JWT。但如果我们用 JWT + Redis存状态,完全可以实现踢人下线。
误区2:"Session不能跨服务共享"
这完全是错的。Session完全可以存在Redis里实现共享,Spring Session、PHP的session_set_save_handler都是干这个的。
6.2 对比表格
| 维度 | Session+Redis | 纯JWT | JWT+Redis(推荐) |
|---|---|---|---|
| 踢人下线 | ✅ 删Redis | ❌ 不行 | ✅ 删Redis |
| 跨服务共享 | ✅ 共用Redis | ✅ 天生 | ✅ 共用Redis |
| 移动端支持 | ⚠️ 依赖Cookie | ✅ 友好 | ✅ 友好 |
| 服务端存储 | 是 | 否 | 是(状态存Redis) |
| 性能 | 查一次Redis | 验签(CPU) | 验签+查Redis |
6.3 结论
对于大多数业务系统,推荐用 JWT + Redis 存状态。
- JWT负责身份认证(无状态、跨端友好、标准统一)
- Redis负责状态吊销(踢人下线、权限实时生效)
这样既享受了JWT的便利,又解决了核心痛点。
七、踩坑记录和最佳实践
这些都是我实际踩过的坑,希望你不用再踩一遍。
坑1:以为备份配置好了,但从没验证过
现象:配置了crontab备份,但半年后恢复时发现备份文件是空的。
原因:mysqldump命令执行失败(密码错误、磁盘满了),但没有告警。
解决方案:
bash
bash
# 定期验证备份文件
gunzip -c /data/backup/mysql/*.sql.gz | head -20
# 配置告警(可以写个脚本检查备份文件大小)
坑2:Session存本地内存,用户频繁掉线
现象:用户刚登录,刷新一下就要重新登录。
原因:Nginx负载均衡把请求分到了不同的服务器,Session存在服务器A,请求到了服务器B。
解决方案:Session存Redis,或改用JWT+Redis。
坑3:Nginx日志打满磁盘
现象:服务器突然不可用,ssh都连不上。
原因:访问日志和错误日志没有轮转,把磁盘占满了。
解决方案:配置logrotate
bash
bash
# /etc/logrotate.d/nginx
/var/log/nginx/*.log {
daily
missingok
rotate 14
compress
delaycompress
notifempty
create 0640 nginx adm
sharedscripts
postrotate
[ -f /var/run/nginx.pid ] && kill -USR1 `cat /var/run/nginx.pid`
endscript
}
坑4:服务器重启后服务没起来
现象:重启服务器后,网站打不开。
原因 :忘了配置 systemctl enable。
解决方案:
bash
bash
# 检查是否已启用
systemctl is-enabled gin.service
# 如果显示disabled,启用它
systemctl enable gin.service
八、总结:高可用不是技术炫耀,而是风险控制
写这篇文章,我想传达几个观点:
1. 不要为了"高可用"而高可用
技术是手段,不是目的。用最简单的方案解决最核心的问题,比堆砌技术更有价值。
2. 从小处着手,按需升级
从单机+备份开始,等业务真的需要了,再考虑多机、容器化、K8s。
3. 云服务是中小项目的朋友
20-30元的负载均衡,200元的云数据库高可用版,买的是安心和节省的时间。
4. 最重要的高可用方案是备份+恢复演练
进程崩了可以重启,服务器挂了可以重买,但数据丢了就真的没了。
5. 定期验证你的备份
配置了备份不等于安全,验证了备份才算。
附:完整脚本和配置
1. Systemd service文件
ini
ini
# /etc/systemd/system/gin.service
[Unit]
Description=Gin Data Management Server
After=network.target postgresql.service
[Service]
Type=simple
User=www-data
WorkingDirectory=/opt/myapp
ExecStart=/opt/myapp/gin-server
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
2. MySQL自动备份脚本
bash
bash
#!/bin/bash
# /data/backup/scripts/mysql_backup.sh
BACKUP_DIR="/data/backup/mysql"
DATE=$(date +%Y%m%d_%H%M%S)
RETENTION_DAYS=30
DB_USER="root"
DB_PASS="your_password"
DB_NAME="your_database"
mkdir -p ${BACKUP_DIR}
mysqldump -u${DB_USER} -p${DB_PASS} ${DB_NAME} | gzip > ${BACKUP_DIR}/${DB_NAME}_${DATE}.sql.gz
find ${BACKUP_DIR} -name "*.sql.gz" -mtime +${RETENTION_DAYS} -delete
3. Crontab配置
cron
javascript
# 每天凌晨2:30备份数据库
30 2 * * * /data/backup/scripts/mysql_backup.sh >> /var/log/mysql_backup.log 2>&1
4. Nginx配置片段
nginx
bash
server {
listen 80;
server_name your-domain.com;
root /var/www/resume;
location /music/ {
alias /var/www/music/;
}
location /api/ {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
}
}
5. Gin中间件:JWT+Redis验证
go
go
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("Authorization")
claims, err := ValidateJWT(token)
if err != nil {
c.JSON(401, gin.H{"error": "无效token"})
c.Abort()
return
}
exists := redisClient.Exists(c, "user:"+claims.UserID).Val()
if exists == 0 {
c.JSON(401, gin.H{"error": "token已失效"})
c.Abort()
return
}
c.Set("userID", claims.UserID)
c.Next()
}
}
最后
希望这篇文章能帮到和你一样,正在思考"中小项目怎么做高可用"的开发者。
记住:高可用不是目的,服务稳定、数据安全才是。
你的项目是怎么做高可用的?遇到过什么坑?欢迎在评论区交流讨论。
如果这篇文章对你有帮助,欢迎点赞、收藏、转发~