高并发系统设计:从理论到实践

报警声中醒来
2025-12-19 15:07
阅读 2818

作者:深圳某医疗SaaS公司Python后端开发,日常边听周杰伦写CRUD,最近在啃Rust红宝书试图逃离GIL。

上周五晚上十点半,我还在公司死磕一个线上告警——用户挂号接口在晚高峰期间超时率飙升到了15%。产品经理在钉钉上疯狂@我:“明天就是体检高峰期了,这要是崩了,咱俩一起滚蛋。” 我一边啃着冷掉的猪脚饭,一边盯着Grafana面板上那条刺眼的红色曲线,心里默念:“Python你可争点气啊!”


背景:我们不是淘宝,但也不能崩

我们做的是一套面向民营医院和体检中心的预约调度系统。听起来平平无奇,但去年双11那天(没错,医疗行业也有“大促”,叫“年度体检季”),单日请求量一度冲到 80万+,峰值QPS逼近 2000。更坑的是,我们的核心接口(比如 POST /api/v1/bookings)要同时做:

  • 用户身份校验
  • 号源库存扣减(涉及Redis + MySQL双写)
  • 排班规则校验(医生是否上班、是否超限)
  • 第三方医保平台回调(异步但强依赖)

早期版本全用Django ORM一把梭,结果一到高峰期,数据库连接池直接拉爆,运维大哥半夜打电话骂我:“你这代码是拿脚写的吧?”


拆解问题:高并发 ≠ 单纯加机器

很多人以为高并发就是堆服务器,但现实很骨感——资源瓶颈往往藏在最不起眼的地方。我们当时踩了几个经典坑:

  1. 数据库行锁竞争:多个用户抢同一个医生的号,MySQL InnoDB行锁排队,接口响应时间从200ms飙到3s+
  2. 同步阻塞调用第三方:医保平台响应慢(平均800ms),直接拖垮整个请求链路
  3. 缓存击穿:热门医生号源缓存过期瞬间,大量请求穿透到DB

被领导“亲切关怀”之后,我被迫翻开尘封已久的《Designing Data-Intensive Applications》,开始搞点“综合”性的优化。


实战:三层漏斗模型 + 异步化改造

我们的核心思路是:把同步流程拆成“预检 → 扣库存 → 异步处理”三阶段,像筛子一样层层过滤流量。

第一层:Nginx + Lua 限流 & 缓存预热

先在接入层拦掉垃圾流量。用OpenResty写了个简单的Lua脚本,对 /bookings 接口按 user_id 做令牌桶限流(每秒最多3次请求),避免恶意刷号:

location /api/v1/bookings {
    access_by_lua_block {
        local limiter = require "resty.limit.req"
        local limit, err = limiter.new("booking_req", 3, 6) -- burst=6
        if not limit then
            ngx.log(ngx.ERR, "failed to instantiate a limit.req object: ", err)
            return ngx.exit(500)
        end
        local delay, err = limit:incoming(ngx.var.binary_remote_addr, true)
        if not delay then
            if err == "rejected" then
                return ngx.exit(429) -- Too Many Requests
            end
            return ngx.exit(500)
        end
    }
    proxy_pass http://backend;
}

吐槽:运维说Lua脚本难维护,但比起半夜被call醒,我觉得香多了。

第二层:Redis原子操作 + 库存预占

最关键的是号源库存扣减。我们放弃“先查再减”的天真做法,改用 Redis Lua脚本保证原子性:

# booking_service.py
import redis

r = redis.StrictRedis()

def try_reserve_slot(doctor_id: str, count: int = 1) -> bool:
    """ 
    Lua脚本确保:库存足够才扣减,且不会超卖
    """
    lua_script = """
    local stock = tonumber(redis.call('GET', KEYS[1]))
    if not stock or stock < tonumber(ARGV[1]) then
        return 0
    end
    redis.call('DECRBY', KEYS[1], ARGV[1])
    return 1
    """
    key = f"slot_stock:{doctor_id}"
    return bool(r.eval(lua_script, 1, key, count))

同时,每天凌晨用Airflow任务预热第二天的号源到Redis,TTL设为25小时,基本杜绝缓存击穿。

第三层:Celery异步解耦

把医保回调、短信通知这些非核心逻辑扔给Celery。用 Redis作为Broker(比RabbitMQ轻量,适合我们这种小团队),关键配置:

# celery_config.py
broker_url = "redis://localhost:6379/1"
result_backend = "redis://localhost:6379/2"
task_serializer = "json"
accept_content = ["json"]
task_acks_late = True  # 任务失败重试
worker_prefetch_multiplier = 1  # 避免worker堆积任务

现在主接口只需150ms就能返回“预约成功”,后续动作全部异步。测试同学终于不用在提测时抱怨“回调没触发”了(虽然他们开始抱怨“异步任务监控看不到”……)。


效果对比:从崩坏到稳如老狗

上线两周后的压测数据(Locust模拟2000 QPS):

指标 优化前 优化后
平均响应时间 1200ms 180ms
错误率 12.3% 0.2%
DB CPU使用率 95% 40%
第三方调用阻塞 是 否

最爽的是上周体检高峰日——系统平稳扛住 2500 QPS,而我在工位上悠闲地听《七里香》,顺便给新来的实习生安利Rust:“你看,如果用Actix-web重写这个服务,可能连异步队列都省了……”


心得:高并发不是银弹,是权衡的艺术

  1. 别迷信微服务:我们尝试过拆Booking Service,结果分布式事务搞到头秃,最后还是用本地事务+补偿机制搞定。
  2. 监控比代码重要:接入Prometheus + Grafana后,才发现80%的延迟来自DNS解析(内网居然没配Hosts!)。
  3. Python依然能打:用好asyncio + uvicorn + redis-py,配合合理的架构,Python在IO密集型场景完全够用。当然,CPU密集型任务?我建议你看看隔壁Rust同学……

最后:致所有被高并发折磨的同行

写这篇文章时,耳机里正放着《稻香》。想起去年双11凌晨三点,我和运维蹲在机房吃泡面,看着负载曲线慢慢降下来,他说:“你这Python代码,勉强能看了。”

其实哪有什么银弹,不过是把每个环节抠到极致,再加一点运气。如果你也在深圳,欢迎来南山咖啡厅找我吹水——带上你的Rust学习笔记,我请你喝喜茶(报销额度还剩200块)。

技术分享的本质,不是炫耀多牛,而是告诉后来者:这条路,有人走过,坑我都替你踩了。


注:文中所有方案均已在生产环境稳定运行6个月以上。如有雷同,纯属我们都被产品经理逼过。

评论 0

最热最新
暂无评论
报警声中醒来Lv.1
0
影响力
0
文章
0
粉丝