面试官问高并发系统设计,我直接说用Python做。他笑了,说Python做高并发是笑话。我没慌,从兜里掏出手机,打开一个压测报告给他看。QPS到了两万八,他眼睛亮了。
很多人觉得Python慢,这是事实。GIL锁把多线程绑死了,单进程跑满一个核就算不错。但你得换个思路,Python不用来扛并发,用它做协调层和业务逻辑层。真正处理请求的是底层服务,Python只负责调度和管理。
我第一个方案是异步IO加多进程。用asyncio写核心业务,把I/O操作全部改成协程。数据库查询走aiomysql,Redis用aioredis,HTTP请求用aiohttp。这样单进程能处理上千个并发连接。然后开四个进程,每个进程一个事件循环,利用multiprocessing模块的Pool来管理。
面试官问我为什么不用线程。我说线程在Python里是假的并行,GIL让它们轮流干活。协程才是真正的并发利器,切换开销几乎为零。他点点头,让我继续说。
第二个关键是缓存策略。我设计了三层缓存。第一层是本地内存缓存,用lru_cache装饰器或者自己写一个字典加上过期时间。这层挡掉大部分重复查询。第二层是Redis集群,负责共享状态和热点数据。第三层是数据库,只处理写操作和缓存穿透的读操作。
缓存更新用了消息队列,Crew来了更新事件就推送到RabbitMQ里。消费者进程收到消息后,主动删除或者更新对应的缓存。这样避免了缓存雪崩和穿透。面试官问我缓存穿透怎么彻底解决,我说加布隆过滤器,在Redis里用布隆过滤器判断key是否存在,不存在直接返回空结果。
系统架构我画了张图。前面是Nginx做负载均衡,后面挂四个Gunicorn进站。每个进站跑Flask应用,开四个worker进程。worker里用asyncio跑协程。再后面是Celery处理异步任务,比如发邮件、生成报表这些耗时操作。数据库主从分离,主库写数据,从库读数据。读写比例大概八比二,从库挂了自动切到主库。
数据库连接池我用psycopg2的ThreadedConnectionPool,设置最小连接数10个,最大50个。超时连接自动回收。SQL查询尽量走索引,慢查询通过slow query log监控。单表超过五百万行就分表,按用户ID做hash分到32张表里。
面试官问我如果请求量再翻一倍怎么办。我说加机器就行。系统是水平扩展的,Nginx层可以加多台做DNS轮询。Gunicorn层可以增加到八台。Redis做分片集群,数据均匀分布到多个节点。数据库用读写分离加分库分表。所有服务都部署在Docker容器里,用Kubernetes做自动扩容。设置CPU使用率超过70%就自动加Pod,低于30%就减。
他还问了降级方案。我说接口设计了熔断器,用pybreaker库实现。当错误率超过50%,熔断器打开,直接返回降级数据。比如商品详情页,如果推荐算法服务挂了,就返回默认推荐列表。用户头像加载失败,用默认头像替换。核心业务不能降级,比如下单支付,这些必须保证正确性。
压测是用Locust做的,模拟五千个用户持续跑十分钟。结果发现瓶颈在数据库连接池,连接数设置太小导致排队。调整到最大100个连接后,QPS从一万二提升到两万八。还有一个坑是Python的垃圾回收,并发高了触发GC导致吞吐量抖动。我用了gc模块手动控制,在请求处理完后乖乖调用gc.collect()。
面试官最后问了个刁钻的问题:如果某个worker进程卡住了怎么办。我说用supervisor监控进程状态,挂了自动重启。卡住的请求设置超时时间,用asyncio.wait_for控制最长等待时间5秒。超过5秒直接返回系统繁忙,让客户端重试。日志系统记录所有异常,通过ELK收集分析,每天出报告改进。
整个方案现场画在白板上,面试官拍照留了。他说Python能做到这个地步已经不错了。后来收到offer,入职后发现他们用Java写的高并发系统,我进去做Python的辅助服务。不管语言怎么样,设计思路才是面试官想看的。