微服务架构设计实战:从单体到分布式——一个医疗Python仔的血泪史
大家好,我是老张(不是那个卖保险的老张),目前在一家医疗软件公司干 Python 开发,快两年了。平时主要用 VSCode 写代码,插件装得比公司茶水间的咖啡胶囊还多——Pylance、Black Formatter、GitLens、Remote - SSH……基本属于“不装十个插件不敢点运行”的类型。
说来惭愧,虽然主语言是 Python,但最近一年被微服务这玩意儿狠狠教育了一顿。事情还得从去年 Q3 说起——我们那个跑了五年的 Java 单体系统,在双11前两周差点崩了。别笑,医疗系统也有“大促”:医保结算季 + 医院年底冲KPI,流量直接翻三倍。当时运维小哥凌晨三点给我打电话:“老张,API 网关 502 了,你那 Python 脚本是不是又把数据库连接池占满了?”
我???明明是 Java 主服务扛不住啊!但谁让我是团队里唯一会写 Python 又懂点架构的人呢(其实是没人愿意碰这个烂摊子)。于是领导拍板:“趁这次重构,咱们上微服务!”
从“屎山”到“乐高”:为什么非拆不可?
先交代下背景。我们的核心系统叫 MediCore,一个典型的 Spring Boot + MyBatis 单体应用,数据库是 MySQL。业务逻辑全堆在一个 WAR 包里:患者管理、处方开单、医保对接、报表生成……全耦合在一起。改个挂号流程,得重新跑两小时的集成测试——测试妹子每次看我的眼神都像在看人渣。
更可怕的是性能问题。比如“处方审核”模块,高峰期要调用外部医保接口,同步阻塞,导致整个 Tomcat 线程池打满。而另一边,“患者信息查询”这种只读操作也被拖垮。典型的“一颗老鼠屎坏了一锅汤”。
产品经理还总提些离谱需求:“能不能让处方审核异步化,但前端实时显示状态?”——行啊,但你得等我改完这 5000 行事务代码。
所以,拆!必须拆!
但别误会,我不是那种上来就喊“上 Kubernetes + Service Mesh”的理想派。在医疗行业,稳定性 > 一切。我们得一步步来。
实战第一步:识别边界,别乱切!
很多教程一上来就说“按业务域拆”,听着很酷,但实操起来容易翻车。我们一开始也热血沸腾,想把“患者”、“药品”、“账单”全拆成独立服务。结果架构评审会上,运维大哥幽幽地说:“你们考虑过分布式事务吗?医保对接失败了,患者数据回滚,但药品库存已经扣了,咋办?”
全场沉默。
后来我们学乖了,先画 上下文映射图(Context Map),重点看:
- 哪些模块变更频率高?
- 哪些模块对延迟敏感?
- 哪些模块依赖外部系统?
最终决定先拆出两个服务:
- AuditLogService(审计日志)——高频写入,但和其他业务几乎无交互
- AsyncTaskService(异步任务)——专门处理医保校验、短信通知这类耗时操作
这两个服务的特点是:低耦合、高内聚、容错性强。就算挂了,主流程还能走(顶多状态更新延迟)。
💡 面试题预警:面试官常问“如何划分微服务边界?” 别背 DDD 那套理论,直接说:“先找那些‘改了不会影响别人’或者‘挂了主流程还能跑’的模块,优先拆它们。” 这才是实战经验。
技术选型:Java 为主,Python 打辅助
虽然我是 Python 党,但公司技术栈以 Java 为主。新服务还是用 Spring Boot 写,但 AsyncTaskService 的调度器我坚持用 Python 重写——为啥?
因为 Celery + Redis 的组合,比 Java 的 Quartz 灵活太多了!特别是动态添加任务、优雅重启这些需求,Python 几行代码搞定,Java 得配半天 XML(好吧,现在是注解,但依然啰嗦)。
举个例子,医保校验任务需要根据医院配置动态调整超时时间。在 Python 里:
# async_worker.py
from celery import Celery
app = Celery('medisvc', broker='redis://localhost:6379/0')
@app.task(bind=True, autoretry_for=(TimeoutError,), retry_kwargs={'max_retries': 3})
def verify_medical_insurance(self, patient_id, hospital_config):
timeout = hospital_config.get('verify_timeout', 30)
# 调用医保接口...
response = requests.post(
'https://insurance.gov.cn/api/verify',
json={'patient_id': patient_id},
timeout=timeout # 动态超时!
)
return response.json()
而在 Java 里,你得写个 RetryTemplate,再配个 BackOffPolicy,最后发现超时还是写死的……算了,不吐槽了。
不过,服务间通信我们统一用 gRPC。为什么不用 REST?两个原因:
- 医疗数据字段多(一个患者对象上百个字段),JSON 太冗余
- gRPC 的强类型 + 自动生成 client,减少前后端扯皮
我们用 Protocol Buffers 定义接口:
// patient.proto
syntax = "proto3";
package medisvc;
service PatientService {
rpc GetPatient(GetPatientRequest) returns (Patient);
}
message GetPatientRequest {
string patient_id = 1;
}
message Patient {
string id = 1;
string name = 2;
string id_card = 3; // 敏感字段,注意加密!
// ... 其他 50+ 字段
}
然后 Java 和 Python 各自生成 stub,互调毫无压力。运维小哥再也不用担心我传错字段类型了(曾经因为把 int 写成 string 导致医保结算失败,被请去喝茶)。
性能优化:别让微服务变“龟速服务”
拆完服务,第一轮压测直接翻车——平均延迟从 200ms 涨到 800ms!原因很简单:原来一次数据库查询的事,现在要跨 3 个服务调用。
我们做了几件事:
1. 数据库读写分离 + 缓存穿透防护
每个微服务都有自己的数据库(甚至自己的 MySQL 实例),但有些数据需要共享,比如“药品目录”。我们搞了个 DrugCatalogService,用 Redis 缓存全量药品数据,并设置二级缓存(本地 Caffeine + Redis)。
关键代码(Python 版):
# drug_catalog.py
import redis
from cachetools import TTLCache
local_cache = TTLCache(maxsize=10000, ttl=60) # 本地缓存60秒
redis_client = redis.Redis(host='redis-cluster')
def get_drug(drug_code: str) -> dict:
# 先查本地缓存
if drug_code in local_cache:
return local_cache[drug_code]
# 再查 Redis
cached = redis_client.hget('drug_catalog', drug_code)
if cached:
drug = json.loads(cached)
local_cache[drug_code] = drug # 回填本地缓存
return drug
# 最后查 DB(防穿透:空值也缓存)
drug = db.query("SELECT * FROM drugs WHERE code = ?", drug_code)
if drug:
redis_client.hset('drug_catalog', drug_code, json.dumps(drug))
redis_client.expire('drug_catalog', 3600)
else:
redis_client.hset('drug_catalog', drug_code, '{}') # 空值缓存
redis_client.expire(drug_code, 300) # 短期过期
local_cache[drug_code] = drug or {}
return drug or {}
2. 异步非阻塞调用链
对于非关键路径,全部改成异步。比如用户提交处方后:
- 主流程:保存处方 → 返回成功(< 200ms)
- 异步:触发医保校验 → 发短信 → 记审计日志
Java 服务用 @Async,Python 用 Celery,两边通过 Kafka 解耦。这样即使医保接口慢,也不影响用户体验。
3. 服务网格兜底(轻量级版)
没上 Istio(太重),但我们用 Nginx + Lua 自制了一个简易熔断器:
# nginx.conf
lua_shared_dict circuit_breaker 10m;
init_by_lua_block {
require "resty.core"
}
location /api/patient {
access_by_lua_block {
local cb = ngx.shared.circuit_breaker
local fail_count = cb:get("patient_service_fail") or 0
if fail_count > 10 then
ngx.exit(503) -- 熔断!
end
}
proxy_pass http://patient-service;
error_page 502 504 = @fallback;
}
location @fallback {
content_by_lua_block {
-- 返回缓存数据 or 默认值
ngx.say('{"error": "service temporarily unavailable"}')
}
}
上线后,某次医保接口宕机,我们的处方服务依然能返回“医保校验中”,而不是直接 500。
生产环境踩坑实录
坑1:分布式 ID 冲突
最初用数据库自增 ID,拆库后直接炸了。后来切换到 雪花算法(Snowflake),但 Java 和 Python 的实现时钟不同步,导致 ID 重复!
解决方案:统一用 Twitter 的 snowflake 服务(Go 写的),所有服务通过 HTTP 获取 ID。虽然多一次网络调用,但省心。
坑2:日志追踪断裂
用户报障:“昨天开的处方没同步到医保!” 我们查日志,发现 patient-service、prescription-service、insurance-service 各自为政,根本串不起来。
紧急引入 OpenTelemetry,在 gRPC 拦截器里透传 trace_id:
# python gRPC client
from opentelemetry import trace
tracer = trace.get_tracer(__name__)
def call_insurance_service(patient_id):
with tracer.start_as_current_span("verify_insurance"):
span = trace.get_current_span()
metadata = (("traceparent", span.get_span_context().trace_id),)
return stub.VerifyInsurance(
VerifyRequest(patient_id=patient_id),
metadata=metadata
)
Java 侧用 Spring Cloud Sleuth 自动集成。现在 Kibana 里一条链路清清楚楚,运维小哥终于不用求着我看日志了。
坑3:配置爆炸
每个服务 20+ 配置项(DB 地址、Redis 密码、超时时间……),YAML 文件堆成山。后来上了 Apollo 配置中心,但 Python 服务接入费了老大劲——官方只有 Java SDK。
最后自己撸了个 Apollo Python Client,支持热更新:
# apollo_client.py
from apollo.client import ApolloClient
client = ApolloClient(app_id="async-task-svc", config_server_url="http://apollo:8080")
client.start()
# 动态获取配置
timeout = client.get_value("verify.timeout", default=30)
# 监听变更
@client.on("verify.timeout")
def on_timeout_change(new_value):
global CURRENT_TIMEOUT
CURRENT_TIMEOUT = int(new_value)
现在改个超时时间,不用重启服务,产品狗都惊了。
性能对比:数字不说谎
拆微服务三个月后,我们做了全面压测(模拟双11流量):
| 指标 | 单体架构 | 微服务架构 | 提升 |
|---|---|---|---|
| 平均响应时间 | 420ms | 180ms | ↓ 57% |
| 错误率(5xx) | 3.2% | 0.4% | ↓ 87% |
| 部署频率 | 1次/周 | 20+次/天 | ↑ 2000% |
| 故障隔离 | 无 | 单服务故障不影响全局 | ✅ |
最爽的是,现在改“短信模板”只需要动 AsyncTaskService,不用再拉全组人回归测试了。测试妹子请我喝了杯瑞幸,感动哭。
给想跳槽的同学:微服务面试题怎么答?
最近帮朋友内推,发现很多人对微服务的理解还停留在“拆服务”层面。其实面试官更关心:
- 你怎么保证数据一致性? → 答:Saga 模式 + 补偿事务,关键操作加幂等。
- 服务雪崩怎么办? → 答:熔断(Hystrix/Sentinel)+ 限流(令牌桶)+ 降级(缓存兜底)。
- 链路追踪怎么做? → 答:OpenTelemetry + Jaeger,关键 Span 打标签。
记住:不要只说概念,一定要结合业务场景。比如:“在医保校验场景,我们用 Saga 模式:先冻结额度 → 校验 → 成功则扣款,失败则解冻。补偿接口 idempotent by patient_id + task_id。”
最后叨叨几句
从单体到微服务,不是技术升级,而是组织能力的升级。我们团队现在有专门的 SRE、DBA、安全审计,每周做 Chaos Engineering(故意 kill pod 看系统健壮性)。虽然加班还是多,但至少不用再为别人的 Bug 背锅了。
至于我?还在和 VSCode 插件斗智斗勇。上周装了个新插件,结果格式化把 gRPC 的 proto 注释删了,被 Java 组追着骂……程序员的命也是命啊!
如果你也在医疗行业,或者正被单体架构折磨,欢迎留言交流。别一个人硬扛,微服务的路上,坑都是大家一起踩出来的。
(完)
P.S. 下周准备用 Python 写个 gRPC 网关聚合层,把多个服务的 API 拼成 GraphQL。要是成了,再写篇《用 FastAPI 打造医疗 GraphQL 网关》。产品经理已经摩拳擦掌了……保佑我别翻车。

评论 0