完整架构
开篇
三年了。
我翻出了第一天画的架构图——一个框。里面写着「Flask + SQLite」,旁边画了个箭头指向「用户」。就这么简单。
现在再看,已经是一个复杂的分布式系统了。
那时候的我,根本不知道什么是负载均衡,不知道缓存应该怎么设计,更不知道数据库分片是什么。我只是想做一个能用的短链接服务,让别人分享链接时不再那么丑陋。
三年,1095 天,从一台服务器到分布式架构。每一步,都是被逼出来的。
架构演进回顾
第 1 天:一切的开始
┌─────────────────────────────┐
│ 单台服务器 │
│ ┌───────────────────────┐ │
│ │ Flask + SQLite │ │
│ └───────────────────────┘ │
└─────────────┬───────────────┘
│
▼
┌─────────┐
│ 用户 │
└─────────┘技术栈: Flask + SQLite,一台服务器(2 核 4G,¥200/月)
代码:
from flask import Flask, redirect
import sqlite3
app = Flask(__name__)
database = sqlite3.connect('shortener.db')
@app.route('/<short_code>')
def redirect_url(short_code):
url = database.execute(
'SELECT original_url FROM urls WHERE short_code = ?',
(short_code,)
).fetchone()
if url:
database.execute(
'INSERT INTO clicks (short_code, clicked_at) VALUES (?, ?)',
(short_code, datetime.now())
)
database.commit()
return redirect(url[0])
return 'Not found', 404问题: 数据库查询慢、点击统计同步影响性能、单点故障
数据: 日均点击 100 次,短链 50 条,响应时间 200ms
第 2 周:第一次优化
┌─────────────────────────────┐
│ 单台服务器 │
│ ┌───────────────────────┐ │
│ │ Flask + Redis │ │
│ │ ↓ │ │
│ │ SQLite │ │
│ └───────────────────────┘ │
└─────────────┬───────────────┘
│
▼
┌─────────┐
│ 用户 │
└─────────┘变化: 加了 Redis 缓存热点短链,缓存命中率 80%,响应时间从 200ms 降到 50ms
为什么加 Redis?
那天晚上,我收到第一封用户邮件:「你的服务好慢啊,等半天才能跳转。」
我查了日志,发现有几个短链被频繁访问。每次都要查数据库,确实慢。
于是我在凌晨两点装上了 Redis:
@app.route('/<short_code>')
def redirect_with_cache(short_code):
cached_url = redis_client.get(f'url:{short_code}')
if cached_url:
return redirect(cached_url)
url = database.execute(
'SELECT original_url FROM urls WHERE short_code = ?',
(short_code,)
).fetchone()
if url:
redis_client.setex(f'url:{short_code}', 3600, url[0])
return redirect(url[0])
return 'Not found', 404代价: 多了一台 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%。我收到告警短信时,正在吃晚饭。
赶回电脑前,发现一台服务器扛不住了。
「加机器吧。」我对自已说。
upstream backend {
server 192.168.1.10:5000;
server 192.168.1.11:5000;
server 192.168.1.12:5000;
}
server {
listen 80;
location / {
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}代价: 服务器从 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。数据乱了。
class Snowflake:
def __init__(self, machine_id):
self.machine_id = machine_id
self.sequence = 0
self.last_timestamp = -1
def next_id(self):
timestamp = int(time.time() * 1000)
if timestamp == self.last_timestamp:
self.sequence = (self.sequence + 1) & 0xFFF
else:
self.sequence = 0
self.last_timestamp = timestamp
return ((timestamp - 1288834974657) << 22) | \
(self.machine_id << 12) | self.sequence代价: 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 台)
upstream backend {
server 192.168.1.10:5000 weight=3;
server 192.168.1.11:5000 weight=2;
server 192.168.1.12:5000 weight=1;
check interval=3000 rise=2 fall=3 timeout=1000;
}
server {
listen 443 ssl http2;
ssl_certificate /etc/nginx/ssl/cert.pem;
ssl_certificate_key /etc/nginx/ssl/key.pem;
location / {
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_connect_timeout 10ms;
proxy_read_timeout 100ms;
}
}关键特性: 加权轮询、健康检查、HTTP/2 支持、SSL 终止
2. API 网关层
class APIGateway:
def __init__(self):
self.rate_limiter = RedisRateLimiter()
self.authenticator = JWTAuthenticator()
self.circuit_breaker = CircuitBreaker()
async def handle_request(self, request):
# 1. 限流检查
if not self.rate_limiter.is_allowed(
request.client_ip, limit=1000, window=1):
return {'error': 'Rate limit exceeded'}, 429
# 2. 鉴权(仅管理 API)
if request.path.startswith('/api/'):
if not self.authenticator.validate(request.token):
return {'error': 'Unauthorized'}, 401
# 3. 熔断检查
if self.circuit_breaker.is_open('backend'):
return {'error': 'Service unavailable'}, 503
# 4. 路由到后端
try:
response = await self.proxy_to_backend(request)
self.circuit_breaker.record_success('backend')
return response
except Exception as e:
self.circuit_breaker.record_failure('backend')
return {'error': 'Internal error'}, 500限流策略: 全局(10 万 QPS)、用户级、IP 级三级限流
3. 应用服务层
重定向服务(核心路径优化)
class RedirectService:
def __init__(self):
self.local_cache = LRUCache(maxsize=10000)
self.redis_client = RedisCluster(nodes=['redis1', 'redis2'])
async def handle_redirect(self, short_code, request):
# L1: 本地缓存(~1ms)
url = self.local_cache.get(short_code)
if url:
await self._record_click_async(short_code, request)
return RedirectResponse(url)
# L2: Redis 缓存(~5ms)
url = await self.redis_client.get(f'url:{short_code}')
if url:
self.local_cache.set(short_code, url)
await self._record_click_async(short_code, request)
return RedirectResponse(url)
# L3: 数据库(~50ms)
url = await self._get_from_database(short_code)
if url:
await self.redis_client.setex(f'url:{short_code}', 3600, url)
self.local_cache.set(short_code, url)
await self._record_click_async(short_code, request)
return RedirectResponse(url)
return NotFoundResponse()
async def _record_click_async(self, short_code, request):
click_event = {
'short_code': short_code,
'timestamp': int(time.time() * 1000),
'ip': request.client_ip,
'user_agent': request.headers.get('User-Agent'),
}
await self.kafka_producer.send('clicks', click_event)性能优化对比:
| 优化阶段 | P50 延迟 | P99 延迟 | QPS |
|---|---|---|---|
| 初始版本 | 200ms | 1000ms | 1000 |
| +Redis 缓存 | 20ms | 100ms | 10000 |
| + 本地缓存 | 5ms | 50ms | 50000 |
| + 异步处理 | 2ms | 20ms | 100000 |
4. 缓存层
Redis 集群架构
class RedisClusterManager:
def __init__(self):
self.hot_cache = RedisCluster(
nodes=[{'host': 'redis1', 'port': 6379},
{'host': 'redis2', 'port': 6379}],
password=os.getenv('REDIS_PASSWORD'),
)
def get_url(self, short_code):
url = self.hot_cache.get(f'url:{short_code}')
if url:
self.hot_cache.zincrby('url:access_count', 1, short_code)
return url
return None
def warm_up_cache(self, top_n=10000):
popular_codes = self.hot_cache.zrevrange(
'url:access_count', 0, top_n - 1)
pipe = self.hot_cache.pipeline()
for code in popular_codes:
pipe.get(f'url:{code}')
return dict(zip(popular_codes, pipe.execute()))缓存策略: L1 本地缓存(1 万条,5 分钟)、L2 Redis 缓存(100 万条,1 小时)、L3 数据库
5. 数据层
MySQL 分片集群
class ShardedDatabaseManager:
def __init__(self):
self.shards = {
'a-m': MySQLConnection('shard1.example.com'),
'n-z': MySQLConnection('shard2.example.com')
}
def get_shard(self, short_code):
first_char = short_code[0].lower()
if first_char in 'abcdefghijklm':
return self.shards['a-m']
return self.shards['n-z']
def create_short_url(self, short_code, original_url):
shard = self.get_shard(short_code)
return shard.execute(
'INSERT INTO urls (short_code, original_url, created_at) VALUES (?, ?, ?)',
(short_code, original_url, datetime.now())
)分片策略: 按短码首字母分片(2 个分片),实现简单,适合短码随机分布场景
6. 消息队列
Kafka 异步处理
class ClickEventProcessor:
def __init__(self):
self.consumer = KafkaConsumer(
'clicks',
bootstrap_servers=['kafka1:9092', 'kafka2:9092'],
group_id='click-processor'
)
def process_clicks(self):
for message in self.consumer:
event = json.loads(message.value)
self.clickhouse.insert('clicks', event)
redis_client.incr(f'click:{event["short_code"]}')
self.elasticsearch.index('clicks', event)数据与成本
三年后的数据
业务数据:
- 短链总数: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 个付费用户。它不完美,但它能用,它稳定,它在赚钱。
这就够了。