微服务架构设计实战:从单体到分布式
上周五晚上十点半,我一边在 MacBook Pro 上敲 docker-compose down,一边听着隔壁工位小张和产品经理对线:“你说微服务能三天上线?那你来写啊!”——熟悉的配方,熟悉的味道。作为组里唯一一个 DBA 出身的后端开发(没错,就是那种看不得慢查询、见不得没索引、连 JSON 字段都恨不得建个 GIN 索引的人),我其实早就想把我们那个“祖传单体”给拆了。但现实是:老板要快,测试要稳,运维要日志,而我……只想早点回家撸猫。
起因:一个快撑不住的“巨石”
我们的核心系统是用 Python 写的 Django 单体应用,跑在 PostgreSQL 上。刚入职那会儿(快两年了),代码还能勉强维护,数据库也就几十张表。但随着业务爆炸式增长——尤其是去年搞了个“区块链积分上链”功能(别问,问就是老板看了某峰会 PPT)——整个系统开始频繁抽风。
典型场景:用户兑换积分触发链上交易,同时要扣库存、发通知、记账、写审计日志……全在一个事务里。结果?数据库连接池打满,PostgreSQL 的 max_connections 被干到 95%,线上直接 502。当时看着 Grafana 面板上飙红的 CPU 和堆积如山的 WAL 日志,我真的想砸电脑。
更离谱的是,每次改个小需求,比如加个字段,都要全量回归测试。测试同学看我的眼神都带着怨念:“你这又动数据库了?上次那个外键删了没?”
拆!但怎么拆?
领导拍板:微服务化。但具体怎么搞?市面上方案一堆:Spring Cloud、Dubbo、Go-micro、甚至有人提议上 Service Mesh。但我们技术栈是 Python + PostgreSQL,团队也没人会 Java,所以 Spring Cloud 直接出局。至于区块链部分?它本身是异步上链的,其实天然适合独立部署。
经过几轮撕逼(哦不,是技术评审),我们定了三个原则:
- 数据库必须隔离:每个服务独占自己的 DB schema,严禁跨库 join(DBA 的底线!)
- 语言保持 Python:虽然性能不是最优,但团队熟悉,开发效率高
- 通信协议轻量:HTTP/JSON 足够,暂时不上 gRPC(除非真有性能瓶颈)
于是,我们把系统粗暴拆成四块:
- user-service:用户管理、鉴权
- inventory-service:库存、商品
- reward-service:积分、兑换逻辑(含区块链对接)
- notification-service:消息推送
技术选型:Python 微服务全家桶
既然是 Python,那框架就得挑靠谱的。Django 太重,Flask 又太裸。最后我们选了 FastAPI —— 异步支持好、自动生成 OpenAPI、类型提示友好,而且和 Pydantic 配合得天衣无缝。数据库方面,继续用 PostgreSQL,但每个服务一个独立数据库实例(AWS RDS 分开部署)。
服务发现?初期直接用 Consul,配置简单,UI 还能看。后来发现节点不多,干脆改用 Nginx + upstream 静态注册,省事。消息队列选了 RabbitMQ,因为团队有人玩过,Kafka 太重,Redis Streams 又怕丢消息(金融相关,不敢赌)。
| 组件 | 选型 | 理由 |
|---|---|---|
| Web 框架 | FastAPI | 异步、文档自动生成、类型安全 |
| 数据库 | PostgreSQL (per service) | ACID 保证、JSONB 支持、DBA 熟悉 |
| 服务注册 | Nginx static upstream | 简单、稳定、运维友好 |
| 消息队列 | RabbitMQ | 可靠、有 DLX、团队有经验 |
| 区块链对接 | web3.py + 异步 worker | 官方库、支持 async |
踩坑实录:那些让我半夜惊醒的 Bug
坑 1:分布式事务?不存在的!
最头疼的是“用户兑换积分 → 扣库存 → 上链”这个流程。单体时代一个事务搞定,现在分三个服务,咋保证一致性?
我们试过 Saga 模式,但补偿逻辑太复杂。最后妥协:最终一致性 + 幂等 + 人工兜底。
具体做法:
- reward-service 发起兑换请求,先本地记录“待处理”
- 通过 RabbitMQ 发送
inventory_deduct消息 - inventory-service 扣库存成功后,回发
deduct_success - reward-service 收到后,调用区块链合约(异步),成功则更新状态
关键代码(简化版):
# reward-service 中的兑换逻辑
@app.post("/redeem")
async def redeem_points(user_id: int, item_id: int):
# 1. 本地创建兑换记录(状态: pending)
redeem_id = await db.create_redeem(user_id, item_id, status="pending")
# 2. 发送扣库存消息(带 redeem_id 用于幂等)
await rabbitmq.publish("inventory.deduct", {
"redeem_id": redeem_id,
"item_id": item_id,
"user_id": user_id
})
return {"redeem_id": redeem_id}
inventory-service 收到消息后,先查是否已处理过该 redeem_id(防重复),再扣库存。如果失败,消息进死信队列,人工介入。
吐槽:产品经理说“用户不能看到失败”,结果我们搞了个“延迟到账”页面,其实是后台在重试……用户以为高科技,其实是我们没辙。
坑 2:数据库连接池爆炸
FastAPI 是异步的,但我们用的 asyncpg 连接池默认只开 10 个连接。压测时,只要并发一高,就报 too many connections for role。
解决方法:动态调整连接池大小 + 限流。
# database.py
from asyncpg import create_pool
DATABASE_URL = "postgresql://user:pass@host:5432/reward_db"
# 根据环境调整 pool_size
POOL_SIZE = 20 if os.getenv("ENV") == "prod" else 5
async def get_db():
pool = await create_pool(DATABASE_URL, min_size=5, max_size=POOL_SIZE)
async with pool.acquire() as conn:
yield conn
同时在 Nginx 层加了 limit_req,防止单个 IP 打爆服务。
坑 3:区块链回调地狱
web3.py 的事件监听是阻塞的!我们一开始直接在 FastAPI 里开线程监听合约事件,结果主线程被卡死。后来改成 Celery + Redis 异步处理:
# blockchain_worker.py
from celery import Celery
from web3 import Web3
app = Celery('blockchain', broker='redis://localhost:6379')
@app.task
def listen_to_chain_events():
w3 = Web3(Web3.HTTPProvider('https://mainnet.infura.io/v3/...'))
contract = w3.eth.contract(address=..., abi=...)
# 使用 filter 轮询(非最优,但稳定)
event_filter = contract.events.Redeem.createFilter(fromBlock='latest')
while True:
for event in event_filter.get_new_entries():
# 处理上链成功事件
update_redeem_status(event['args']['redeemId'], 'completed')
time.sleep(15) # 避免 Infura 限流
血泪教训:别在异步 Web 框架里混用阻塞 I/O,除非你想体验“服务假死”。
效果如何?值不值得?
上线三个月,效果喜人:
- 数据库负载下降 60%,PostgreSQL 的
active connections从 80+ 降到 20 以下 - 新功能上线时间从 2 周缩短到 3 天(独立部署,互不影响)
- 区块链模块故障不再拖垮整个系统(曾经因为 Infura 限流导致全站不可用)
当然,代价也有:运维复杂度上升,日志得用 ELK 聚合,监控得配 Prometheus + Grafana。但作为 DBA 出身的人,看到每个服务都有清晰的数据边界、合理的索引、干净的慢查询日志……值了!
最后几句真心话
微服务不是银弹,尤其对小团队。如果你的系统还没到“改一行代码全公司加班”的地步,别急着拆。但一旦决定拆,请记住:
- 数据库隔离是底线(求你们了,别跨服务 join)
- 异步解耦能救命(消息队列是好朋友)
- Python 能做微服务,但别硬扛高并发(实在不行就上 Go 写核心模块)
至于区块链?说实话,除了“显得高大上”,它给我们的系统带来的更多是复杂度。但既然老板要,我们就把它关进“reward-service”这个笼子里,尽量少让它出来捣乱。
现在,我终于能在周五晚上十点前关掉 MacBook,而不是对着满屏的 psycopg2.OperationalError 发呆了。这,大概就是微服务给普通程序员最实在的礼物吧。
(完)

评论 0