当每天有数亿个事件(约每秒几千个,峰值更高)时,制胜之举是将写入摄取(ingestion)与对外服务(serving)解耦:把写入吸收进一个持久化 log,把流只处理一次,然后扇出(fan out)到两个为相反任务而调优的存储——为家长预聚合(读取快),为 ML 保留原始且全保真的数据。
**心智模型:**一个收发室——collector 接收邮件,Kafka 是传送带,而每一件都会被复制到两个货架上:一个供家长扫一眼的,以及一个供 ML 团队深挖的。
Client ─(batch)─▶ Collector ─▶ Kafka ─▶ Stream proc ─┬─▶ OLAP rollup (daily summaries)
(learning app) (HTTP) (buffer, (read once) └─▶ S3 / Parquet (raw events)
replay)
// 一个 consumer 只读一次 Kafka 并写入两个 sink
for await (const batch of kafka.consume('events')) {
await lake.appendParquet(batch); // raw -> S3/Parquet (ML)
for (const e of batch)
rollup.add({ childId: e.childId, day: dayOf(e.ts) },// aggregate -> OLAP (reports)
{ lessons: 1, studiedMs: e.studiedMs ?? 0 });
await rollup.flush();
}
Parent app ─▶ API ─▶ OLAP rollup (pre-aggregated: report = O(1), no scan)
ML / Data ─▶ Spark / warehouse ─▶ S3 / Parquet (raw, flexible, ad-hoc)
家长访问的是预聚合的摘要,所以一次报表就是一次键查找,绝不是对数十亿行的扫描。ML 查询原始 lake,为特征工程保留每一个字段。
面试官在考察你是否把写优化的 ingest 与读优化的 serving分开、是否避免为每次家长报表都扫描原始事件,以及是否为 ML 保留原始数据。弱:"全放进 Postgres 然后查询它。"**强:**持久化 buffer + 两条服务路径 + replay + 一份 schema 契约,并对规模有直觉。
预聚合带来即时读取,但会预先固定维度;原始 lake 灵活但慢。两者都保留——这就是经典的一次写入、多次读取(write-once, read-many)拆分。
一个包含详细解答的 IT 面试题库——从初级到高级。
捐赠