完整架构
开篇
三年了。
我翻出了第一天画的架构图——一个框。里面写着「Flask + SQLite」,旁边画了个箭头指向「用户」。就这么简单。
现在再看,已经是一个复杂的分布式系统了。
那时候的我,根本不知道什么是负载均衡,不知道缓存应该怎么设计,更不知道数据库分片是什么。我只是想做一个能用的短链接服务,让别人分享链接时不再那么丑陋。
三年,1095 天,从一台服务器到分布式架构。每一步,都是被逼出来的。
架构演进回顾
第 1 天:一切的开始
┌─────────────────────────────┐
│ 单台服务器 │
│ ┌───────────────────────┐ │
│ │ Flask + SQLite │ │
│ └───────────────────────┘ │
└─────────────┬───────────────┘
│
▼
┌─────────┐
│ 用户 │
└─────────┘技术栈: Flask + SQLite,一台服务器(2 核 4G,¥200/月)
代码:
落地思路
- 这里省略具体语法,只保留设计层面的职责边界。
- 读这段时重点看:输入是什么、系统做哪些判断、状态如何变化、失败时如何兜底。
问题: 数据库查询慢、点击统计同步影响性能、单点故障
数据: 日均点击 100 次,短链 50 条,响应时间 200ms
第 2 周:第一次优化
┌─────────────────────────────┐
│ 单台服务器 │
│ ┌───────────────────────┐ │
│ │ Flask + Redis │ │
│ │ ↓ │ │
│ │ SQLite │ │
│ └───────────────────────┘ │
└─────────────┬───────────────┘
│
▼
┌─────────┐
│ 用户 │
└─────────┘变化: 加了 Redis 缓存热点短链,缓存命中率 80%,响应时间从 200ms 降到 50ms
为什么加 Redis?
那天晚上,我收到第一封用户邮件:「你的服务好慢啊,等半天才能跳转。」
我查了日志,发现有几个短链被频繁访问。每次都要查数据库,确实慢。
于是我在凌晨两点装上了 Redis:
落地思路
- 这里省略具体语法,只保留设计层面的职责边界。
- 读这段时重点看:输入是什么、系统做哪些判断、状态如何变化、失败时如何兜底。
代价: 多了一台 Redis 服务器(¥100/月),但响应时间降到了 50ms,值了。
第 2 个月:数据库迁移
┌───────────────────────────┐
│ 应用服务器 │
│ Flask + Redis │
└─────────┬─────────────────┘
│
▼
┌───────────────────────────┐
│ MySQL 服务器 │
│ (urls + clicks 表) │
└───────────────────────────┘变化: SQLite → MySQL,第一次数据库迁移通宵完成
为什么要迁移?
第 60 天,我的服务有了第一个「大客户」——一个技术博客,每天能带来 5000 次点击。
那天下午,数据库锁死了。
SQLite 是文件锁,写操作会阻塞所有读操作。点击统计是写操作,重定向是读操作。结果就是:有人点击,其他人就等。
我在群里求助,有人告诉我:「上 MySQL 吧,SQLite 扛不住的。」
迁移过程:导出 SQLite → 转换格式 → 导入 MySQL → 修改代码 → 凌晨 3 点切换完成
代价: 又多了一台 MySQL 服务器(¥300/月),但再也不怕数据库锁死了。
第 6 个月:第一次扩容
┌─────────────┐
│ 用户 │
└──────┬──────┘
│
▼
┌──────────────────┐
│ Nginx 负载均衡 │
└────────┬─────────┘
│
┌─────────────┼─────────────┐
│ │ │
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│服务器 1 │ │服务器 2 │ │服务器 3 │
│Flask+R │ │Flask+R │ │Flask+R │
└────┬────┘ └────┬────┘ └────┬────┘
└────────────┼────────────┘
│
▼
┌───────────────┐
│ MySQL 主从 │
└───────────────┘变化: 3 台应用服务器 + Nginx 负载均衡,MySQL 主从复制(一主两从)
为什么要扩容?
第 180 天,我的服务有了 10 万条短链,日均点击 10 万次。
那天晚上,服务器 CPU 飙到 100%。我收到告警短信时,正在吃晚饭。
赶回电脑前,发现一台服务器扛不住了。
「加机器吧。」我对自已说。
入口配置要点
- 入口层负责接住流量,并把请求转发到后端服务。
- 代理配置的重点是保留必要请求信息,方便后端识别来源和生成日志。
代价: 服务器从 2 台变成 5 台,月成本¥1000,但能扛住 10 万 QPS 了。
第 1 年:CDN + 分布式 ID
┌─────────────┐
│ 用户 │
└──────┬──────┘
│
▼
┌──────────────────┐
│ CDN + 智能 DNS │
└────────┬─────────┘
│
┌─────────────┼─────────────┐
│ │ │
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│ 北京节点 │ │ 上海节点 │ │ 广州节点 │
│ Nginx+ │ │ Nginx+ │ │ Nginx+ │
│ Flask │ │ Flask │ │ Flask │
└────┬────┘ └────┬────┘ └────┬────┘
└────────────┼────────────┘
│
▼
┌───────────────┐
│ Snowflake ID │
│ MySQL 分片 │
└───────────────┘变化: 接入 CDN,实现 Snowflake 分布式 ID 生成器,数据库开始分库分表
为什么需要分布式 ID?
第 365 天,我的服务有了 100 万条短链。
问题出现了:自增 ID 在多服务器环境下会冲突。服务器 A 生成 ID=1001,服务器 B 也生成 ID=1001。两条不同的短链,同一个 ID。数据乱了。
落地思路
- 这里省略具体语法,只保留设计层面的职责边界。
- 读这段时重点看:输入是什么、系统做哪些判断、状态如何变化、失败时如何兜底。
代价: CDN 费用¥500/月,但全国用户访问速度都快了。
第 3 年:完整的微服务架构
这就是现在的架构。
三年,1095 天,从一台服务器到分布式系统。每一步,都是被问题逼出来的。
最终架构详解
┌─────────────┐
│ 用户 │
└──────┬──────┘
│
▼
┌─────────────────────────────┐
│ CDN + 智能 DNS │
│ (按地域返回最近的服务器) │
└─────────────┬───────────────┘
│
┌─────────────────────┼─────────────────────┐
│ │ │
▼ ▼ ▼
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ 北京地域 │ │ 上海地域 │ │ 广州地域 │
│ ┌───────────┐ │ │ ┌───────────┐ │ │ ┌───────────┐ │
│ │ Nginx×2 │ │ │ │ Nginx×2 │ │ │ │ Nginx×2 │ │
│ └─────┬─────┘ │ │ └─────┬─────┘ │ │ └─────┬─────┘ │
│ ┌─────┴─────┐ │ │ ┌─────┴─────┐ │ │ ┌─────┴─────┐ │
│ │ API 网关 │ │ │ │ API 网关 │ │ │ │ API 网关 │ │
│ └─────┬─────┘ │ │ └─────┬─────┘ │ │ └─────┬─────┘ │
│ ┌─────┴─────────────────────────────┴─────┐ │
│ │ 应用服务器集群 │ │
│ │ 北京:4 台 上海:3 台 广州:3 台 │ │
│ └───────────────────┬─────────────────────┘ │
└─────────────────────┼───────────────────────┘
│
┌───────────────┼───────────────┐
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Redis 集群 │ │MySQL 分片 │ │ Kafka │
│ 2 主 2 从 │ │ 2 主 2 从×2 │ │ 2 节点 │
└─────────────┘ └─────────────┘ └─────────────┘
│
┌───────────────┼───────────────┐
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ ELK │ │ Prometheus │ │ 告警系统 │
│ 日志收集 │ │ + Grafana │ │ (钉钉) │
└─────────────┘ └─────────────┘ └─────────────┘核心组件详解
1. 负载均衡层
Nginx 集群(6 台,每地域 2 台)
入口配置要点
- 入口层负责接住流量,并把请求转发到后端服务。
- 代理配置的重点是保留必要请求信息,方便后端识别来源和生成日志。
关键特性: 加权轮询、健康检查、HTTP/2 支持、SSL 终止
2. API 网关层
落地思路
- 这里省略具体语法,只保留设计层面的职责边界。
- 读这段时重点看:输入是什么、系统做哪些判断、状态如何变化、失败时如何兜底。
限流策略: 全局(10 万 QPS)、用户级、IP 级三级限流
3. 应用服务层
重定向服务(核心路径优化)
落地思路
- 这里省略具体语法,只保留设计层面的职责边界。
- 读这段时重点看:输入是什么、系统做哪些判断、状态如何变化、失败时如何兜底。
性能优化对比:
| 优化阶段 | P50 延迟 | P99 延迟 | QPS |
|---|---|---|---|
| 初始版本 | 200ms | 1000ms | 1000 |
| +Redis 缓存 | 20ms | 100ms | 10000 |
| + 本地缓存 | 5ms | 50ms | 50000 |
| + 异步处理 | 2ms | 20ms | 100000 |
4. 缓存层
Redis 集群架构
落地思路
- 这里省略具体语法,只保留设计层面的职责边界。
- 读这段时重点看:输入是什么、系统做哪些判断、状态如何变化、失败时如何兜底。
缓存策略: L1 本地缓存(1 万条,5 分钟)、L2 Redis 缓存(100 万条,1 小时)、L3 数据库
5. 数据层
MySQL 分片集群
落地思路
- 这里省略具体语法,只保留设计层面的职责边界。
- 读这段时重点看:输入是什么、系统做哪些判断、状态如何变化、失败时如何兜底。
分片策略: 按短码首字母分片(2 个分片),实现简单,适合短码随机分布场景
6. 消息队列
Kafka 异步处理
落地思路
- 这里省略具体语法,只保留设计层面的职责边界。
- 读这段时重点看:输入是什么、系统做哪些判断、状态如何变化、失败时如何兜底。
数据与成本
三年后的数据
业务数据:
- 短链总数:1000 万条
- 日均点击:500 万次
- 峰值 QPS:8000
- 付费用户:500 个
- 月收入:¥50000
性能数据:
- P50 延迟:5ms
- P99 延迟:50ms
- 缓存命中率:95%
- 可用性:99.9%
成本分析
服务器成本:
应用服务器(10 台):
北京:4 台 × ¥400 = ¥1600
上海:3 台 × ¥400 = ¥1200
广州:3 台 × ¥400 = ¥1200
小计:¥4000/月
Redis 集群(4 台):2 主 2 从 × ¥500 = ¥2000/月
MySQL 集群(4 台):2 主 2 从 × ¥600 = ¥2400/月
Kafka(2 台):2 节点 × ¥400 = ¥800/月
监控/日志(2 台):ELK + Prometheus × ¥400 = ¥800/月
服务器总成本:¥10000/月其他成本:
带宽:3 个地域 × 50Mbps × ¥13/Mbps = ¥2000/月
CDN:500GB 流量 × ¥2/GB = ¥1000/月
域名和 SSL:¥100/月
其他总成本:¥3100/月总成本:¥13100/月
利润:
月收入:¥50000
月成本:¥13100
月利润:¥36900
利润率:74%终于盈利了。
尾声
回头看,每一个架构决策都是在压力下做出的。
- 加 Redis,是因为用户抱怨慢
- 迁 MySQL,是因为数据库锁死了
- 上负载均衡,是因为 CPU 飙到 100%
- 做分布式 ID,是因为 ID 冲突了
- 接 CDN,是因为全国用户都说卡
但正是这些压力,逼出了最好的设计。
如果一开始就设计一个完美的分布式系统,我可能永远都不会开始。
先跑起来,再优化。遇到问题,解决问题。
这三年,我学到的最重要的事:架构不是一开始设计出来的,是一步步演进出来的。
现在,这个系统每天处理 500 万次点击,服务 500 个付费用户。它不完美,但它能用,它稳定,它在赚钱。
这就够了。