【大数据高并发核心场景实战】缓存层 - 日亿万级请求日志收集

上一章咱们给数据库装了个"写缓存",相当于在市中心车库外建了个临时停车场。确实,车流(写操作)不堵门了。但问题是------这停车场是露天的,且只有三个车位。

一旦遇上"双十一"式的高频数据洪流(想象一群饿疯了的吃货同时涌向自助餐厅),这方案立刻露出短板:缓存写满的速度比手机掉电还快 ,数据要么排队等到天荒地老,要么面临丢失风险。

显然,临时方案扛不住持久战。接下来咱们关掉美颜,直面痛点,一步步拆解如何为持续的高频写入设计一个既扛得住、又稳得起 的系统方案。

道路就在前方,咱们开始铺路。

1 业务背景:日亿万级请求日志收集如何不影响主业务

业务狂飙突进,某天,一家公司的日活用户冲上了 500万
基于当时的模式,业务方一拍桌子要求:全面埋点 ,精准记录用户在特定页面的所有行为。

目的很明确:

  1. 数据分析 ------ 理解用户,优化产品。
  2. 与第三方结算费用 ------ 这事关商业模式,深层逻辑本章暂不展开。

与此同时,业务方还要求能在后台实时查询 用户行为与统计报表。不过,此"实时"并非毫秒级的较真------略有延迟可以接受,更准确地说,是 "准实时"

为了方便后续理解方案设计,这里对真实业务中复杂的数据结构进行了去枝减蔓 ,仅保留核心字段。如下表所示。

指标 备注
IMEI 用户设备的IMEI
定位点 经纬度
用户ID 用户唯一ID
目标ID 每个页面、按钮、banner都有唯一ID
目标类型 页面、按钮、baner等
事件动作 点击、进人、跳出等
FromURL 来源URL
CurrentURL 当前URL
TOURL 去向URI
动作时间 触发这个动作的时间
进人时间 进人该页面的时间
跳出时间 跳出该页面的时间
... ...

在这样一套数据结构支撑下,业务团队在后台查询时,手里就像握着一把 "多维数据遥控器"

第一,灵活查明细
他们可以任意组合筛选条件------按城市(由经纬度实时换算)、性别、年龄 按目标类型、具体事件动作
......实时透视每一位用户的行为轨迹,跟玩数据侦探游戏一样。

第二,秒出统计报表
系统还能从多个维度快速聚合出业务指标,例如:

  • 按天/周/月/年 + 性别 + 年龄 等多维度交叉分析
  • 查看每个目标ID的总点击量、人均点击次数、页面转化率等

(这还只是统计需求的冰山一角,业务方的报表愿望清单往往比你想象的长得多。)

而为了支撑费用结算 这个关键任务,所需收集的数据结构(简化版)则如下表所示。

字段 备注
日期 结算的日期
目标ID 原始数据中的目标 ID,比如页面 ID、按钮 ID、banner ID
点击人数 有多少人点击了目标 (同一人点多次算一次)
点击人次 有多少人次点击 (同一人点多次算多次)
费用 此目标 ID当天的收费总计

这样一来,数据不仅要"流得进来",还得能"被看得清、算得明"。下一步,就是设计一套能扛住高并发写入、同时支持灵活查询与实时统计的架构方案。

2. 技术选型思路

基于以上业务场景,项目组提炼出 六大核心需求 ,并给出了对应的技术选型思路:

  1. 原始数据海量 → 初步选用 HBase 持久化,专治数据"膨胀症"。
  2. 埋点响应要快 → 埋点服务先将日志丢进缓存层 ,保证用户无感。具体缓存方案有几副"牌"可打,稍后详解。
  3. 后台可查询原始数据 → 若直接用 HBase 查询,速度略慢。故额外引入 Elasticsearch ,专门承载查询条件字段与活动ID,实现"秒查"。(用ES快速筛选出查询条件,再用条件去HBase拿完整数据,各司其职,既快又省。
  4. 多样统计报表 → 虽可选用 Kibana、Grafana 等现成可视化工具,但为追求灵活度,决定自主设计 统计功能。
  5. 依埋点生成结算数据 → 费用相关数据存 MySQL ,保证事务清晰、计算稳妥。
  6. 需框架处理缓存数据并分流至 ES / HBase / MySQL → 因业务要求"准实时",必须选用实时处理引擎。当前主流三兄弟:Storm、Spark Streaming、Apache Flink ,选谁?下文分解。

根据以上思路,初步架构如下图所示。

眼尖的你一定发现了,这张架构图上还挂着醒目的问号 ❓没错,悬念就此埋下------它们正对应着接下来要拆解的四道核心难题。咱们往下看,问题马上揭晓。

2.1 使用什么技术保存埋点数据的第一现场

面对海量埋点数据,第一道关卡就是:用啥技术接住这"第一现场"? 主流选项有三:Redis、Kafka、本地日志 。项目组最终拍了板:本地日志。为啥不选 Redis 或 Kafka?来,我们掰开揉碎说一说。

2.1.1 先说 Redis:快,但有"记忆漏洞"

Redis 靠 AOF 机制持久化数据(写缓存章节提过),但其刷盘节奏是个关键选择:

  • 若配置 appendfsync everysec每秒刷一次盘 。速度不错,但宕机可能丢一秒数据
  • 若配置 appendfsync always每次操作都刷盘 。数据稳了,但速度会有影响

埋点要求响应飞快,等不及 always;又怕丢数据,不敢信 everysec。Redis:出局。

2.1.2 再说 Kafka:稳,却要"等确认"

Kafka 通过多副本保障可靠性,其 Producer 的 acks 配置直接决定速度与安全的取舍:

acks 值 行为 结果
0 Leader 不收妥就直接回复客户端 ⚡️ 速度起飞,📉 数据可能"蒸发"
1 Leader 落盘即回复,不等 Follower 同步 ⚡️ 速度较快,📉 副本未同步,仍有丢失风险
all 等 Leader + 指定数量 Follower 全部同步后才回复 📦 数据稳如磐石,🐢 响应速度感人

想要数据不丢?就得选 acks=all,但响应延迟就上去了。Kafka:要么快,要么稳,难两全

****所以选本地日志:鱼与熊掌这次兼得了
既不想丢数据,又要响应快------项目组最终决定:直接把埋点数据写入服务器本地日志文件

  • 💡 响应极快:写完本地就返回,无需等待网络同步或副本确认。
  • 💾 数据可靠:日志文件即时落盘,系统崩溃也不怕。
  • 🔄 后续可异步处理:日志文件可作为可靠数据源,供下游实时或批量消费。

结论 :在高并发埋点场景下,本地日志成为了那个兼顾性能与可靠性的"朴实却聪明"的起点。接下来的问题,就是如何把这些日志高效、有序地"搬运"到下游存储了。

2.2 日志搬运工-如何收集日志数据到持久化层

最简单的办法,当然是直接用 Logstash 把日志文件数据"灌入"Elasticsearch。但问题来了:业务要求存进ES的记录,是一份"完整档案" (需包含城市、性别、年龄等业务字段),而原始日志里只有用户行为"骨架",血肉(业务数据)还得去业务系统里抽。这样一来,光有Logstash就不够用了------中间缺了一道"数据拼装"工序。

🧩** 如果非要Logstash直传,有三种"补丁式"方案,但各有硬伤:**

方案 操作 问题
1. 自定义Filter 在Logstash里用Ruby写过滤器,现场调业务接口补数据 代码臃肿,维护吃力,耦合度高
2. 改客户端 埋点时就在客户端提前捞好业务数据一起上报 业务方拍桌子拒绝:查询条件每变一次就得发版一次,太折腾
3. 改服务端 服务端写日志前,先查库补全数据再落盘 服务端不同意:每个请求都同步查库,性能骤降,用户体验受损

****不仅如此,不用Logstash直传还有两个关键原因:

  1. 数据要分送两处 (ES + HBase),Logstash多输出源在同一个Pipeline里,一旦一个输出源"卡壳",另一个也跟着"停工"------互相拖累。
  2. MySQL里的费用结算数据 ,需要动态分析计算埋点日志才能生成,这已超出Logstash"搬运"的职责范围,它算不明白。

结论:必须请出"计算框架"

正因为数据需要补全、拆分、计算 ,项目组最终决定:引入一个实时计算框架,让它负责这段"复杂搬运+加工"流水线。至此,完整的技术架构演进如图所示。

最终确定的方案流程如下:日志文件 → Logstash → 消息队列(MQ)→ 实时计算框架 → 持久化层

简单说,就是先把日志"搬进"消息队列,再交给实时计算框架集中处理。框架的核心任务有二:
1️⃣ 补全业务数据 ------为原始埋点记录注入城市、性别、年龄等字段;
2️⃣ 生成费用数据 ------动态统计并计算出可用于结算的结构化结果。
处理完成后,数据会分门别类存入相应的持久化存储中。


****关于 Logstash 的补充说明
Logstash 本身由 Ruby 编写,资源消耗较大,因此官方推出了更轻量的 Filebeat 。常见做法是使用 Filebeat 收集日志,再交由 Logstash 进行过滤处理。

如果不需要 Logstash 强大的过滤功能,也可以直接用 Filebeat 将数据发送至 Kafka。但 Filebeat 采用轮询机制 检测文件变化,可能存在明显采集延迟;而 Logstash 能够实时监听 文件变动,时效性更好。

综合考量后,项目组决定继续沿用 Logstash ------其资源消耗在可接受范围内,且能保证数据采集的实时性。


接下来,我们将分别探讨两个关键组件:消息队列 Kafka分布式实时计算框架 的选择与设计。

2.3 为什么使用Kafka

Kafka,出身名门(LinkedIn开源),天生就是为吞日志而生的。它有两个醒目标签:超高吞吐 + 无限堆积(数据扩展性近乎无上限)。官方曾晒出一组"成绩单":仅用3台普通配置的机器 ,就能实现每秒写入200万条记录。

它凭什么这么快?

关键藏在存储设计 里。来看一张官方的结构示意图:

你可以把 Kafka 的每个 Topic 分区想象成一个不断增长的巨型文件 ,而这个大文件又由许多 Segment 小文件 拼接而成。

其读写模式极其简洁:
✍️ Producer 只做"顺序写" ------永远在文件末尾追加数据;
📖 Consumer 只做"顺序读" ------沿着文件依次读取。

这种顺序读写 的架构,让读操作几乎不阻塞写操作,磁盘 I/O 效率拉满,吞吐量自然飙高。

为什么适合日志场景?

除了读写快,Kafka 还有一个"懒人优势":只要磁盘够大,消息就能一直堆下去 (理论无限堆积)。
这对于海量、持续产生的日志数据来说,简直是量身定做的解决方案------既能扛住洪峰流量,又不必频繁清理历史数据。

于是,在日志收集这条赛道上,Kafka 凭借快 + 稳 + 能囤 的三板斧,顺利入选项目的核心枢纽。

2.4 使用什么技术把Kafka的数据迁移到持久化层

2.4.1 技术选型

要把 Kafka 中海量的数据"消化"并存入各个持久层,就得请出 分布式实时计算框架。原因很明确:

  1. 数据量太大,必须"分布式"群殴 :每天上亿条埋点数据,要在业务要求的时间内完成分析、补全、统计,并分别写入 ES、HBase 和 MySQL。单机肯定跑不动,必须靠多个节点并发处理 ------这是典型的大数据分布式计算诉求。
  2. 业务要求"准实时" :报表和查询不能等隔夜批处理,必须有个实时流水线 来持续处理数据流。

目前主流的分布式实时计算框架有 Storm、Spark Streaming、Apache Flink 三剑客。选哪个?答案是:看家底和喜好。

  • 如果公司已有现成的,直接用,别折腾。
  • 如果从零开始,那就凭偏好选。

作者推荐 Apache Flink ,理由有二:一是性能强悍 :阿里巴巴在双11等大促中,靠它实现了每秒处理17亿条数据 的峰值。二是机制贴心 :它提供 Exactly-Once(精确一次) 的容错保证,还有基于事件时间(Event Time) 的窗口计算能力------这两点对于业务来说至关重要。

2.4.2 容错机制:消息到底被处理了几次?

流处理中系统难免出故障,这就引出一个关键问题:消息会不会丢?会不会重复? 不同的容错机制决定了数据的一致性级别:

机制 含义 效果
At-Most-Once 至多一次 消息可能"丢了就丢了",可能丢失数据
At-Least-Once 至少一次 消息可能"发了又发",可能重复处理
Exactly-Once 精确一次 消息有且仅被正确处理一次 ,理想状态

在需要精确计费、统计的业务中,Exactly-Once 无疑是黄金标准 。Flink 的容错机制正是为此设计,能在保证高吞吐的同时,做到数据不丢不重。

2.4.3 时间窗口:按谁的时钟算?

统计"每小时用户数"时,如果按处理框架收到消息的时间 (处理时间)来划分窗口,可能会出错。
举个栗子 🌰:用户一条行为日志实际发生在 <font style="color:rgb(15, 17, 21);">6:30</font>,但框架 <font style="color:rgb(15, 17, 21);">6:32</font> 才收到。如果按处理时间统计 <font style="color:rgb(15, 17, 21);">6:01~6:30</font> 的数据,这条记录就被漏掉了------这显然不符合业务逻辑。

这里有一个简单的Flink代码示例,展示如何定义基于事件时间的一小时滚动窗口,并将每小时内的用户聚集到一个列表中:

java 复制代码
// 创建Flink流处理执行环境
Final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
// 设置时间语义为处理时间(使用系统时钟时间)
env.setStreamTimeCharacteristic(TimeCharacteristic.ProcessingTime);
// 可选的其他时间语义(当前被注释):
// env.setStreamTimeCharacteristic(TimeCharacteristic.IngestionTime);  // 使用数据摄入时间
// env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);       // 使用事件自带时间戳
// 创建Kafka数据源,从指定topic读取数据
DataStream<MyEvent> stream = env.addSource(
    new FlinkKafkaConsumer<MyEvent>(
        topic,   // Kafka主题名称
        schema,  // 消息反序列化模式
        props    // Kafka连接配置
    )
);
// 构建数据处理流水线
stream
// 按用户ID进行数据分区(相同用户的数据发往同一分区)
.keyBy((event) -> event.getUser())
// 定义1小时的滚动时间窗口(基于处理时间)
.timeWindow(Time.hours(1))
// 增量聚合:使用reduce函数合并窗口内数据
.reduce((a, b) -> a.add(b))  // 假设MyEvent有add()方法
// 输出结果到目标系统(sink未指定具体实现)
.addSink(...);

业务统计真正该依据的,是日志中记录的 事件发生时间(Event Time)Flink 的杀手锏之一,正是其基于事件时间(Event Time)的窗口机制 ,它能正确处理这种乱序到达的数据,确保统计结果忠实于用户真实的操作时刻,而不是系统的处理时钟。

总结 :选择 Flink,不仅是看中它的性能天花板 ,更是因为它用 Exactly-Once 机制 保证了数据处理的准确性,并用 Event Time 时间窗口 保障了业务统计的真实性。这两大特性,让它成为处理复杂实时流水线的可靠选择。

3.整体方案

最终,我们得到了如图所示的完整架构方案。

这个系统的运作,就像一个高度协同的数据流水线,其核心流程可以清晰地分为五步。

1.就地记录,保存"第一现场" :所有埋点请求到达后台服务后,第一时间被原封不动地写入服务器本地日志文件

2.日志搬运,送入中转站Logstash 扮演勤恳的搬运工,从各服务器日志文件中抽取原始数据,不做任何加工,直接打包送入 Kafka 消息队列。这一步的目标是快速汇集,统一入口

3.实时加工,智能分流 : Apache Flink 作为核心计算引擎,从 Kafka 中持续拉取原始数据流。在这里,数据完成了"华丽变身":补全业务字段、进行统计计算,然后被精准地分派到三个不同的目的地:Elasticsearch、HBase 和 MySQL。

4.查询分工,联合作战

  • Elasticsearch 存储用于快速检索的索引字段(如用户ID、城市、事件类型等),负责处理复杂的条件筛选。它像一个超级目录,先快速找到符合条件的数据ID列表。
  • HBase 则存储全量的、详细的原始请求数据。当 ES 返回ID列表后,系统再根据 ID 去 HBase 里提取完整的"档案"

这种"索引+数据"的分离设计,既保证了查询速度,又满足了数据详查的需求。

5.结算数据,单独安放

经由 Flink 加工生成的、用于费用结算的结构化数据 ,被存入 MySQL。这类数据查询和处理频率相对较低,但要求较高的准确性和事务性,关系型数据库正是其合适归宿。


总结一下 :这套架构从本地日志的"快写"开始,经由 Kafka 缓冲,通过 Flink 进行实时加工与分流,最终让数据在 ES、HBase 和 MySQL 中各司其职,共同支撑起海量埋点数据的采集、处理、查询与结算的全链路需求,实现了性能、可靠性与灵活性的平衡。

4.小结

本章没有深挖复杂的技术细节,而是重点呈现了技术选型背后的"心路历程" ,希望能为你构建架构思维提供一份参考。

之所以未重复展开某些技术场景,是因为它们在前几章已有涉及。学习架构的秘诀在于:复杂的局面,往往是简单场景的叠加与复用 。因此后续内容会对已介绍过的场景适当精简,以便聚焦于更重要的新知识。这可能会让行文更"干"一些,但读到这里的你,已经可以带着自己的理解去吸收它们了。

说回我们设计的这个架构。它落地后表现稳健:数据丢失率极低,扩展性也经受住了考验 ------即使日活跃升至几千万,系统依然扛得住(当然,机器该加还得加,历史数据也要记得定时清理)。

还记得写缓存方案中留下的两个"未解题"吗?其中长期高并发写入 这一题,本章给出了答案。而另一题------高并发且需要争抢资源 的场景,正是我们下一章要攻克的**"秒杀"架构** 。这是一个综合性强、面试高频的重要战场,我们接着往下看。