高并发系统设计:从理论到实践——一个Spark老油条的血泪复盘

前端里的光
2025-12-19 12:14
阅读 2637

大家好,我是阿哲,坐标深圳南山科技园,混迹于某腾讯系大厂,做了三年大数据开发,日常主要和 Spark、Kafka、Flink 打交道。你可能会问:一个搞大数据的,怎么突然写起高并发系统设计了?

这事还得从去年双11说起。

我们团队本来负责的是离线数仓和实时计算管道,但去年老板拍板要做一个“秒杀活动实时监控大盘”——要求支持每秒10万+的事件写入,并在500ms内完成聚合展示。听起来是不是很熟悉?没错,这活儿本该后端兄弟扛,但产品经理甩锅说:“你们不是天天处理海量数据吗?这点并发算啥?”

于是,我这个只会写 Scala 的 Spark 工程师,被迫拿起了 Java,踏上了高并发系统的“代码人生”之路。


一、现实狠狠打了我的脸

刚开始我还挺自信:不就是高并发嘛,多线程、连接池、缓存,背过八股文谁不会?结果第一版上线当天晚上,测试同学压测到 3w QPS 时,系统直接 OOM,CPU 爆满,日志里全是 java.lang.OutOfMemoryError: GC overhead limit exceeded

当时坐在工位上,耳机里放着 Lo-fi Beats(我写代码必备),看着监控面板一片红,真的想砸电脑。

更尴尬的是,第二天晨会,运维大哥幽幽来一句:“你们这服务把 Redis cluster 的某个分片打满了,其他业务都受影响了。”
我:😅

痛定思痛,我意识到——高并发不是堆技术名词,而是对资源的极致调度


二、重新理解“资源”:CPU、内存、IO、网络,一个都不能少

以前写 Spark 作业,关注的是 executor 内存、shuffle 分区数、GC 调优。但做在线高并发服务,资源维度更细:

  • CPU:线程上下文切换成本、锁竞争
  • 内存:对象生命周期、缓存大小、GC 频率
  • IO:数据库连接、磁盘写、网络往返
  • 网络:TCP 连接数、带宽、延迟

我们的核心链路是:用户点击 → HTTP 请求 → 写入 Kafka → 实时计算 → 写入 Redis → 前端拉取。

瓶颈居然出在最前面的 HTTP 入口层!用 Spring Boot 写的 Controller,每个请求新建线程,线程池配置还是默认的 corePoolSize=8,根本扛不住。

解法1:异步非阻塞 + 合理线程模型

改用 CompletableFuture + 自定义线程池,关键代码如下:

@RestController
public class EventController {

    private final ExecutorService ioExecutor = new ThreadPoolExecutor(
        50, 200, 60L, TimeUnit.SECONDS,
        new LinkedBlockingQueue<>(1000),
        new ThreadFactoryBuilder().setNameFormat("event-io-pool-%d").build(),
        new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略很重要!
    );

    @PostMapping("/event")
    public CompletableFuture<ResponseEntity<String>> handleEvent(@RequestBody Event event) {
        return CompletableFuture.supplyAsync(() -> {
            // 异步写 Kafka
            kafkaProducer.send(event);
            return ResponseEntity.ok("ok");
        }, ioExecutor);
    }
}

注:这里特意用了 CallerRunsPolicy,当队列满时由调用线程自己执行,避免丢请求(虽然会慢一点,但比 500 强)。


三、数据库不是垃圾桶,别乱塞!

早期为了快,我们直接把原始事件写 MySQL,结果主库 CPU 100%,binlog 堆积如山。DBA 在群里@我:“再这么搞,明天就停你权限。”

后来改用 分层写入策略

层级 存储 用途 TTL
L1 Kafka 原始事件缓冲 7天
L2 Redis (Hash + SortedSet) 实时聚合指标 24h
L3 ClickHouse 离线分析 永久

Redis 设计细节

  • HINCRBY 统计 UV(用户ID去重用 HyperLogLog)
  • ZADD + ZREVRANGE 做 TopN 排行榜
  • Key 按小时分片:event:2023111114:user_click

这样,MySQL 彻底解放,只用于存储元数据(比如活动配置),不再承受写压力。


四、限流、熔断、降级:保命三件套

高并发场景下,系统稳定比功能完整更重要。我们引入了 Sentinel(阿里开源的流量控制组件),配置如下:

# sentinel规则(动态加载)
flow:
  - resource: /event
    count: 8000   # QPS 阈值
    grade: 1      # QPS 模式
    controlBehavior: 0 # 快速失败

当流量突增,超过 8k QPS 时,直接返回 {"code": 503, "msg": "too many requests"},前端做友好提示。虽然用户体验打点折,但系统没崩——这才是真正的“优雅”。

另外,我们还做了柔性降级

  • 非核心字段(如用户设备信息)在高压下可丢弃
  • 实时排行榜延迟 1s 更新(用本地缓存暂存)

五、面试题 vs 真实生产:别被八股文骗了

很多人准备高并发面试题,背得滚瓜烂熟:“用 Redis 做缓存穿透防护”、“用消息队列削峰”。但真实场景远比这复杂。

举个例子:缓存雪崩

面试答案通常是“加随机过期时间”。但在生产环境,我们遇到的是:大量 Key 同时失效,导致 DB 瞬间被打爆。

我们的解法是:

  1. 热点 Key 永不过期(后台定时刷新)
  2. 缓存空值(防止恶意刷不存在的 ID)
  3. 二级缓存:本地 Caffeine + Redis
LoadingCache<String, String> localCache = Caffeine.newBuilder()
    .maximumSize(10_000)
    .expireAfterWrite(10, TimeUnit.MINUTES)
    .build(key -> redisClient.get(key)); // 本地缓存未命中才查 Redis

这种组合拳,才是真实世界的防御。


六、性能对比:优化前后数据说话

经过三轮迭代,系统终于扛住了双11峰值。以下是关键指标对比:

指标 优化前 优化后 提升
最大 QPS 12,000 120,000 10x
P99 延迟 1800ms 320ms ↓82%
内存占用 8GB 3.5GB ↓56%
GC 频率 15次/分钟 2次/分钟 ↓87%

最爽的是,双11当晚我在公司躺平刷 B站,系统稳如老狗。隔壁组还在救火,我默默点了杯瑞幸,心想:代码人生,不过如此


七、给同行的几点真心话

  1. 不要迷信“高并发架构图”:很多文章画一堆微服务、网关、注册中心,但你的业务可能只需要一个合理的线程池 + 缓存策略。
  2. 压测要贴近真实:用 JMeter 模拟 10w 并发没用,要看请求分布、参数多样性、网络抖动。
  3. 可观测性 > 一切:日志、Metrics、Trace 缺一不可。我们靠 Arthas + Prometheus + SkyWalking 定位了 80% 的问题。
  4. Java 依然是高并发王者:别听风就是雨说 Go 多快。JVM 经过这么多年优化,配合 Netty、Disruptor,性能完全够用。

结语:从 Spark 到高并发,我的成长

说实话,这次跨界让我重新理解了“系统设计”——它不是炫技,而是在资源受限下,用工程思维做最优权衡

现在回看那段边听音乐边 debug 的日子,虽然苦,但值得。毕竟,能扛住线上流量冲击的代码,才是有生命力的代码

如果你也在深圳,欢迎约 coffee 聊技术(或者一起吐槽产品经理)。最后送大家一句我工位贴的座右铭:

“高并发不是目标,稳定交付才是。”

—— 一个不想再背八股文的大数据工程师,阿哲

评论 0

最热最新
暂无评论
前端里的光Lv.1
0
影响力
0
文章
0
粉丝