图解 Fluss(三):读写全链路 —— 一次写入、一次扫描、一次点查

图解 Fluss(三):读写全链路 ------ 一次写入、一次扫描、一次点查

阅读本文你将了解: 一条记录从客户端到落盘要经过哪些步骤、Exactly-Once 是怎么保证的、列裁剪为什么能把网络传输降两个数量级、以及 Lookup Join 的三级缓存如何让维表关联做到亚毫秒级。

配套图表:seq-01-write-pathseq-02-read-pathseq-03-pk-lookup

难度:⭐⭐⭐ | 适合人群:要做性能调优与 Exactly-Once 方案设计的开发


一、场景:一个实时大屏的三个请求

某电商平台要做一个实时大屏,每秒处理 50 万条订单事件。这个大屏背后有三个完全不同的数据请求:

请求 A:订单写入

sql 复制代码
INSERT INTO orders SELECT * FROM kafka_source;   -- 50 万 QPS

要求:不能丢、不能重复。这是写入路径

请求 B:大屏聚合

sql 复制代码
SELECT SUM(amount), COUNT(*) FROM orders WHERE event_time > NOW() - INTERVAL '5' MINUTE;

orders 表有 180 个字段,但这个查询只用 2 个。这是扫描路径,要求高吞吐、低延迟。

请求 C:订单详情关联用户

sql 复制代码
SELECT o.order_id, u.user_name, u.city
FROM orders o
LEFT JOIN user_profile FOR SYSTEM_TIME AS OF o.proc_time AS u
ON o.user_id = u.user_id;

每条订单都要查一次用户表,QPS 和订单写入一样高。这是点查路径,要求极低延迟。

三条路径,三种完全不同的优化思路。 这一篇我们逐条跟踪。


二、图 1:PK 表写入路径

整条链路分三个阶段,图里用分隔线标得很清楚。

2.1 阶段一:元数据定位

复制代码
Client -> Coordinator : GetTableMetadata(tablePath)
Coord  --> Client    : TableDescriptor + Bucket 映射

Client -> Client : 计算 bucketId = hash(pk) % bucket.num
Client -> Client : 定位 bucket 的 Leader TabletServer

注意这个阶段只在初始化时执行一次,不是每条记录都问一次 Coordinator。客户端拿到 TableDescriptor 后会缓存:

java 复制代码
// 客户端侧的逻辑(简化示意)
// TableBucket 的真实路径:org.apache.fluss.metadata.TableBucket
public void write(RowData row) {
    // 1. 从本地缓存取表元数据(首次或失效时才请求 Coordinator)
    TableDescriptor descriptor = metadataCache.get(tablePath);

    // 2. 计算 bucket(本地计算,零网络开销)
    int bucketId = BucketingFunction.hash(descriptor.getBucketKeys(), descriptor.getBucketNum(), row);
    //  等价于:MurmurHash3.hash(primaryKey) % bucket.num

    // 3. 从缓存的路由表找 Leader(首次或 Leader 变更时才请求 Coordinator)
    ServerNode leader = routingCache.get(new TableBucket(tableId, bucketId));

    // 4. 直连 TabletServer 写入
    client.send(new WriteRecordRequest(leader, bucketId, batch, acks));
}

这个设计是 Fluss 高吞吐的前提之一:Coordinator 不在数据路径上。如果每条记录都要问一次 Coordinator"我该写到哪",50 万 QPS 会把 Coordinator 打爆。

三个关键缓存:

缓存 内容 失效时机
表元数据缓存 Schema、bucket.num、分区信息 表结构变更(Coordinator 主动推送)
路由缓存 bucketId → Leader TabletServer 地址 Leader 变更(请求返回 NOT_LEADER 时刷新)
连接池 到各 TabletServer 的 TCP 长连接 连接断开

2.2 阶段二:写入(先 WAL 后 RocksDB)

图中两个 group 块标出了两次写入:

java 复制代码
// TabletServer 侧处理 WriteRecordRequest(简化)
public CompletableFuture<WriteRecordResponse> handleWrite(WriteRecordRequest request) {
    int bucketId = request.getBucketId();

    // ---- 写入 LogTablet (WAL) ----
    LogTablet logTablet = logManager.getOrCreateLogTablet(tablePath, bucketId);
    LogAppendInfo appendInfo = logTablet.append(request.getBatch());
    //  内部:检查是否需要 Roll Segment(段写满 1GB 或超过时间阈值)
    //       返回新记录的起始 offset

    // ---- 写入 KvTablet (RocksDB) ----
    if (tableDescriptor.hasPrimaryKey()) {
        KvTablet kvTablet = kvManager.getOrCreateKvTablet(tablePath, bucketId);
        for (Record record : request.getBatch()) {
            kvTablet.put(record.getKey(), record.getValue());
            //  内部:先写 RocksDB 自身 WAL,再写 MemTable
            //       如果配置了 merge-engine,这里会先读旧值再合并
        }
    }

    // ---- 等待副本确认 ----
    return waitForAcks(appendInfo.getBaseOffset() + appendInfo.getCount() - 1, request.getAcks());
}

图右侧的 note 总结了顺序保证:

复制代码
写入顺序保证:
1. 先写 WAL (LogTablet) 保证持久性
2. 再写 RocksDB (KvTablet) 提供查询能力
3. acks=all 时等待所有 ISR 确认后才返回

为什么这个顺序不能反? 上一篇已经答过:LogTablet 是参与副本复制的共享 WAL,RocksDB 的本地 WAL 不是。反过来的话,Follower 就没有办法追平数据。

2.3 阶段三:副本同步与 High Watermark

复制代码
Leader -> Follower : Replicate(batch, offset)
Follower -> Follower : 追加到本地 LogTablet
Follower --> Leader : ACK(offset)

Leader -> Leader : 等待所有 ISR 确认
Leader -> Leader : advanceHighWatermark(offset)
Leader --> Client : 写入成功

ISR(In-Sync Replicas) 是能够跟上 Leader 的副本集合。High Watermark(HW)的定义是:

复制代码
HW = min(所有 ISR 副本的 LEO)

其中 LEO(Log End Offset)是每个副本的日志末端 offset。

举例说明 HW 的作用:

复制代码
                 LEO              HW
Leader:   [0 1 2 3 4 5 6]  →  7      4
Follower1:[0 1 2 3 4 5]    →  6      -
Follower2:[0 1 2 3 4]      →  5      -

HW = min(7, 6, 5) = 5

HW 的意义:offset < HW 的记录,已经至少被所有 ISR 副本确认,消费者一定能读到它,即使 Leader 立刻宕机也不会丢。

这就是 acks 参数的语义:

acks 行为 持久性 延迟
acks=0 写入 Leader 内存即返回 最低,可能丢 最低
acks=1 写入 Leader 的 LogTablet 即返回 Leader 宕机可能丢
acks=all 等待所有 ISR 确认 最高 较高

生产环境推荐 acks=all + min.insync.replicas=2

properties 复制代码
# 客户端配置
client.request.acks: all
# 服务端配置:ISR 最少要有 2 个副本,否则拒绝写入
tablet-server.min.insync.replicas: 2

这个组合很重要 :如果只配 acks=all 而不配 min.insync.replicas,当 ISR 只剩 Leader 一个时,acks=all 会退化成 acks=1------你以为数据有三副本,实际上只有一份。

2.4 Exactly-Once 是怎么来的?

图里最后一行写着 写入成功 (Exactly-Once)。这需要两层机制叠加

第一层:Flink Sink 的两阶段提交

java 复制代码
class FlussSinkCommitter implements SinkCommitter {
    // Checkpoint 触发时调用:预提交,数据已写入但对外不可见
    public List<CommitRequest> prepareCommit() { /* ... */ }

    // 所有并行 writer 都预提交成功后调用:真正提交,数据对外可见
    public void commit(List<CommitRequest> commits) { /* ... */ }
}

第二层:服务端的幂等写入

java 复制代码
class WriterStateManager {
    // 每个 Writer (producer) 有唯一的 writerId 和递增的 sequence number
    // 服务端记录每个 writerId 的最后 sequence:
    //   - seq == lastSeq + 1  → 正常接收
    //   - seq <= lastSeq      → 重复写入,直接返回成功(幂等)
    //   - seq >  lastSeq + 1  → 数据丢失,返回错误
}

两层叠加的效果:

复制代码
场景:Flink Checkpoint 失败后从上次成功的 checkpoint 恢复,重放一批数据

传统 Kafka Sink:这批数据会被重复写入 → 下游看到重复
Fluss Sink:     两阶段提交保证预提交的数据没有 commit,
                 重放时 sequence number 相同,服务端识别为重复写入并丢弃
                 → 下游只看到一次

配置方式:

sql 复制代码
-- Flink SQL
SET 'execution.checkpointing.mode' = 'EXACTLY_ONCE';
SET 'execution.checkpointing.interval' = '30s';

CREATE TABLE orders_sink (...) WITH (
    'connector' = 'fluss',
    'bootstrap.servers' = 'coordinator:9123',
    'sink.delivery-guarantee' = 'exactly-once'   -- 默认值
);

2.5 请求 A 的答案

回到开头的"订单写入 50 万 QPS":

优化点 效果
元数据本地缓存 Coordinator 零压力,不在数据路径上
bucket 路由本地计算 无额外网络往返
顺序写 WAL 磁盘顺序 IO,单盘可到 500MB/s+
批量追加 攒批减少 RPC 次数
acks=all + min.insync.replicas=2 不丢数据

调优参数:

properties 复制代码
# 客户端批量参数
client.writer.batch.size: 1MB        # 攒批大小
client.writer.linger.ms: 10          # 最多等 10ms 攒批
client.writer.buffer.memory: 64MB    # 客户端缓冲区

三、图 2:流式读取路径

3.1 关键:FetchRequest 带着"投影列"和"过滤条件"

注意图里第一条消息:

复制代码
Client -> Server : FetchRequest(tabletId, offset, projectedColumns, predicates)

这是 Fluss 和 Kafka 最本质的区别之一。 Kafka 的 FetchRequest 只有 topic + partition + offset,服务端只能把完整的字节流吐给你;而 Fluss 的请求里带了:

  • projectedColumns:我只要这几列
  • predicates:我的 WHERE 条件

服务端因此可以做两件优化,图中用两个 group 块标出。

3.2 优化一:列裁剪(Column Projection)

复制代码
Log -> Arrow : 读取 Arrow RecordBatch (全列)
Arrow -> Arrow : 通过 TransferPair 零拷贝投影,只保留 projectedColumns
Arrow --> Log : 投影后的 RecordBatch (仅需要的列)

为什么能"零拷贝"? 因为底层数据是 Arrow 列式格式。

Arrow 的内存布局是按列连续存储的:

复制代码
行式(Kafka / 传统):
  row1: [id|name|age|city|amount|...]
  row2: [id|name|age|city|amount|...]
  → 要读第 2 列,必须把每一行的完整字节都读出来

列式(Arrow):
  col_id:     [1, 2, 3, ...]        ← 连续内存块 A
  col_name:   ['a','b','c', ...]    ← 连续内存块 B
  col_age:    [20, 25, 30, ...]     ← 连续内存块 C
  → 要读第 2 列,直接引用内存块 B,不需要碰 A 和 C

"投影"的本质就是:把不需要的列的 FieldVector 引用丢掉,只保留需要的。Arrow 的 TransferPair 机制让这个操作是 O(1) 的引用转移,不产生任何数据拷贝:

java 复制代码
class ColumnProjector {
    public VectorSchemaRoot project(VectorSchemaRoot full, int[] projectedColumns) {
        // 对每个需要的列:
        //   TransferPair pair = vector.getTransferPair(allocator);
        //   pair.transfer();      ← 零拷贝,只是转移 buffer 所有权
        //   result.add(pair.getTo());
        // 不需要的列:直接 close() 释放
    }
}

3.3 优化二:谓词下推(Predicate Pushdown)

复制代码
Log -> Log : 应用 WHERE 过滤条件,对 Arrow Vector 向量化过滤

这一步是向量化执行:不是一行一行判断,而是对一个 Arrow Vector 批量判断,生成一个 selection vector(位图)。

java 复制代码
// 伪代码:向量化过滤
public SelectionVector applyPredicate(FieldVector amountVector, Predicate pred) {
    // 传统:for each row: if (row.amount > 100) keep
    // 向量化:一次性处理 1024 个值,生成 bitmask
    //   long[] bitmask = new long[1024 / 64];
    //   for (int i = 0; i < 1024; i++) {
    //       if (amountVector.get(i) > 100) bitmask[i/64] |= (1L << (i%64));
    //   }
}

向量化过滤的好处:

  1. CPU cache 友好:连续内存访问
  2. 可被 JIT 向量化:现代 JVM 能把这种循环编译成 SIMD 指令
  3. 分支预测友好:没有 per-row 的 if 分支跳转

3.4 效果量化

图中 note 给出了一个非常直观的数字:

复制代码
列裁剪 + 谓词下推在服务端完成:
200 列的表只读 2 列 → 网络传输减少 99%
WHERE 过滤在 TabletServer 执行 → 减少无效数据传输

我们算一下那个"180 字段,只用 2 个"的大屏查询:

传统方案(Kafka) Fluss
网络传输 50万 QPS × 2KB/行 = 1 GB/s 50万 QPS × 20B = 10 MB/s
反序列化 CPU 180 字段全解析 只解析 2 列
Flink TM 内存 完整对象,GC 压力大 只有 2 列的对象
过滤位置 Flink 侧(数据已经传过来了) TabletServer 侧

网络传输降低 99%,这个量级的差距足以让一个跑不动的作业变成跑得很轻松。

3.5 客户端侧的收尾

复制代码
Client -> Client : Arrow → Flink RowData 转换
Client -> Client : 更新 Checkpoint offset

第二步是 Flink 容错的关键:offset 随 Checkpoint 一起持久化。Flink 的 Checkpoint 完成后,offset 才被提交;故障恢复时从 Checkpoint 里的 offset 重新消费。

3.6 请求 B 的答案

sql 复制代码
SELECT SUM(amount), COUNT(*) FROM orders
WHERE event_time > NOW() - INTERVAL '5' MINUTE;

Flink 优化器会把 SUM(amount) 用到的列推导出来,生成 projectedColumns = [amount, event_time],把 event_time > ... 下推为 predicate。TabletServer 只返回这两列的过滤后数据。

验证方法------看 Flink 的执行计划:

sql 复制代码
EXPLAIN SELECT SUM(amount), COUNT(*) FROM orders
WHERE event_time > NOW() - INTERVAL '5' MINUTE;

如果看到 Source 上有 projectedFields 或类似的提示,说明列裁剪生效了。


四、图 3:PK Lookup 路径

4.1 三级优化链

图下方的 note 总结了 Lookup 的性能优化链:

复制代码
性能优化链:
1. LRU 缓存命中 → 零网络开销
2. Bloom Filter → 快速排除不存在的 key
3. RocksDB LSM → 亚毫秒级点查询

这三级的成本递减、命中率递减,构成了一个典型的缓存层次结构:

复制代码
第 1 级:LRU 缓存(Flink TaskManager 本地堆内存)
   成本:~100 ns
   命中率:取决于维表大小和缓存容量,热点数据可到 80%+

第 2 级:Bloom Filter(TabletServer 内存)
   成本:~1 μs(只判断"可能存在"还是"一定不存在")
   作用:对于不存在的 key,直接返回 null,避免 RocksDB 查询

第 3 级:RocksDB LSM 查询
   成本:~100 μs ~ 1 ms
   路径:MemTable → Immutable MemTable → L0 → L1 → ... → L6

4.2 第一级:LRU 缓存

java 复制代码
class FlussLookupFunction extends LookupFunction {
    private final Cache<RowData, RowData> lookupCache;   // Guava/Caffeine LRU
    private final FlussConnection connection;

    public void eval(Object... joinKeys) {
        RowData key = toRowData(joinKeys);

        RowData cached = lookupCache.getIfPresent(key);
        if (cached != null) {
            collect(cached);          // 命中,零网络开销
            return;
        }
        // 未命中,走下面的远程查询
        // ...
        lookupCache.put(key, value);  // 回填缓存
    }
}

配置:

sql 复制代码
CREATE TABLE user_profile (
    user_id BIGINT,
    user_name STRING,
    city STRING,
    PRIMARY KEY (user_id) NOT ENFORCED
) WITH (
    'bucket.num' = '16',
    'lookup.cache.max-rows' = '100000',     -- 最多缓存 10 万行
    'lookup.cache.ttl' = '1h'               -- 缓存 1 小时过期
);

调优要点:

参数 影响 建议
lookup.cache.max-rows 命中率 vs 内存 维表小(< 100万行)可以全缓存;大表按热点比例设 10%-20%
lookup.cache.ttl 数据新鲜度 维表更新频繁 → 设短(分钟级);静态维表 → 设长或不设

缓存一致性的坑 :如果维表在 Fluss 里被更新了,Flink 侧的 LRU 缓存不会自动失效,要等 TTL 过期。所以维表更新频率高且要求强一致的场景,TTL 不能设太长

4.3 第二级:Bloom Filter

复制代码
Server -> Bloom : mayContain(key)
Bloom --> Server : 不存在 (快速返回)   →  直接返回 null
Bloom --> Server : 可能存在            →  继续查 RocksDB

Bloom Filter 的特点是:可能误报"存在",但绝不会误报"不存在"

  • 返回"不存在" → 100% 确定这个 key 没有,可以安全返回 null
  • 返回"可能存在" → 大概率存在,需要真的查一次 RocksDB

上一篇讲过 BLOOM_BITS_PER_KEY = 10.0,这个配置下假阳性率约 1%。也就是说:

复制代码
查 10000 个不存在的 key:
  - 约 9900 个被 Bloom Filter 拦截 → 零磁盘 IO
  - 约 100 个假阳性 → 白跑一次 RocksDB 查询,但返回 null

这个优化对于"大量 key 命不中"的场景效果拔群。比如风控场景,大部分用户不在黑名单里,Bloom Filter 能拦掉 99% 的无谓查询。

4.4 第三级:RocksDB LSM 查询

复制代码
Kv -> Kv : 查 MemTable → Immutable → L0~L6 SST

RocksDB 的读路径(图中标注的顺序):

复制代码
1. Active MemTable(内存跳表,最新写入)   ~ 100 ns
2. Immutable MemTable(正在 flush 的)     ~ 100 ns
3. Block Cache(读缓存)                   ~ 1 μs
4. L0 SST 文件(可能有重叠,要查多个)      ~ 10-100 μs
5. L1 ~ L6 SST 文件(每层有序,二分查找)   ~ 10-100 μs/层

读放大问题 :LSM 树的层级越多,查询可能要读的文件越多。这就是为什么 BLOCK_CACHE_SIZE 和 Bloom Filter 都很重要------它们能把大部分查询挡在第 4 步之前。

4.5 请求 C 的答案

sql 复制代码
SELECT o.order_id, u.user_name, u.city
FROM orders o
LEFT JOIN user_profile FOR SYSTEM_TIME AS OF o.proc_time AS u
ON o.user_id = u.user_id;

图下方的 note 也标注了这条 SQL:

复制代码
对应 SQL:
LEFT JOIN dim FOR SYSTEM_TIME AS OF o.time
LRU 缓存降低服务端查询压力

性能对比:

方案 单次关联延迟 50万 QPS 下的表现
Redis 维表 0.5 - 1 ms(网络 RTT) 需要 Redis 集群,且仍受网络限制
HBase 维表 2 - 10 ms 扛不住,需要大集群
Flink 双流 Join 无网络,但状态巨大 状态 TB 级,Checkpoint 慢
Fluss PK Lookup 缓存命中 ~0.1μs,未命中 ~0.5ms 热点命中率高时整体 P99 < 1ms

核心优势:Fluss 把维表数据存在 TabletServer 本地,配合 LRU + Bloom + RocksDB 三级优化,把"远程查询"变成了"大部分时候的本地内存查询"。


五、动手验证

5.1 观察列裁剪的效果

sql 复制代码
-- 建一张宽表
CREATE TABLE wide_table (
    id BIGINT,
    c01 STRING, c02 STRING, ... c50 STRING,     -- 50 个 STRING 字段
    amount DECIMAL(18,2)
) WITH ('bucket.num' = '8');

-- 只查 2 列
EXPLAIN SELECT id, amount FROM wide_table WHERE amount > 100;

在 TabletServer 的 metrics 里观察:

bash 复制代码
curl http://tablet-server:9125/metrics | grep -i "bytes_out"

对比全表扫描和只查 2 列的网络字节数,差距应该在两个数量级。

5.2 验证 Lookup 缓存命中率

bash 复制代码
curl http://tablet-server:9125/metrics | grep -i lookup

关注指标:

复制代码
fluss_lookup_cache_hit_count
fluss_lookup_cache_miss_count
fluss_lookup_cache_hit_ratio        ← 目标 > 0.8
fluss_lookup_remote_latency_p99     ← 目标 < 1ms

5.3 验证 Exactly-Once

sql 复制代码
-- 开启 Checkpoint
SET 'execution.checkpointing.interval' = '10s';
SET 'execution.checkpointing.mode' = 'EXACTLY_ONCE';

-- 写入并观察
INSERT INTO orders_sink SELECT * FROM source;

手动杀掉 Flink 作业,从 Checkpoint 恢复,检查目标表的数据条数是否有重复:

sql 复制代码
SELECT COUNT(*), COUNT(DISTINCT order_id) FROM orders_sink;
-- 两者相等 → 无重复

六、生产实践要点

6.1 写入调优清单

properties 复制代码
# 客户端
client.request.acks: all
client.writer.batch.size: 1MB
client.writer.linger.ms: 10
client.writer.compression.type: lz4     # 压缩,牺牲 CPU 换带宽

# 服务端
tablet-server.min.insync.replicas: 2
log.segment.size: 1GB
log.flush.interval.messages: 10000      # 攒多少条刷盘
log.flush.interval.ms: 1000

6.2 读取调优清单

properties 复制代码
# 增大 fetch 批次,减少 RPC 往返
client.scanner.fetch.max-bytes: 64MB
client.scanner.fetch.max-wait-ms: 500

# 服务端 Arrow 内存池
arrow.allocator.memory.limit: 2GB

6.3 Lookup 调优清单

sql 复制代码
-- 维表设计的黄金法则:
-- 1. bucket.num 要足够大,避免热点 bucket
-- 2. 开启缓存,并设置合理 TTL
-- 3. 维表字段不要太多(投影只取需要的列)
CREATE TABLE dim_user (
    user_id BIGINT,
    user_name STRING,
    city STRING,
    PRIMARY KEY (user_id) NOT ENFORCED
) WITH (
    'bucket.num' = '64',                  -- 足够分散
    'lookup.cache.max-rows' = '500000',   -- 缓存 50 万行
    'lookup.cache.ttl' = '30min'          -- 30 分钟过期
);

6.4 常见反模式

反模式 问题 正确做法
acks=0 追求低延迟 数据可能丢 acks=all + 批量写入来降延迟,而不是降低持久性
Lookup 缓存 TTL 设成 1 天 维表更新后读到脏数据 按维表更新频率设 TTL,或改用双流 Join
用 PK 表存纯日志数据 存储成本翻 2.5 倍 纯追加场景用 Log 表
bucket.num 设成 1 完全无法并行,单 bucket 成为瓶颈 至少设为 TabletServer 数的 2-4 倍

七、排障手册

现象 可能原因 排查方向
写入延迟突然升高 ISR 收缩触发 acks=all 等待 检查 Follower 是否有 GC 或网络问题;`curl .../metrics
写入报 NOT_LEADER_FOR_BUCKET Leader 刚切换 客户端会自动刷新路由重试;如果持续报错,检查 Coordinator 是否在频繁 Rebalance
读取吞吐上不去 列裁剪没生效 检查 Flink 执行计划;确认谓词可下推(不支持 UDF 过滤下推)
Lookup P99 高 缓存命中率低 检查 lookup_cache_hit_ratio;调大 max-rows 或缩小维表
数据出现重复 Checkpoint 没开或两阶段提交没生效 确认 execution.checkpointing.mode = EXACTLY_ONCEsink.delivery-guarantee = exactly-once
数据丢失 min.insync.replicas 未配置 补上配置;检查 ISR 最小副本数

八、小结

三条路径,三种优化思路:

路径 核心优化 关键机制
写入 元数据本地缓存 + 顺序写 WAL bucket 路由本地计算、ISR 复制、两阶段提交 + 幂等写入
扫描 服务端列裁剪 + 谓词下推 Arrow 列式格式、TransferPair 零拷贝投影、向量化过滤
点查 三级缓存层次 LRU 本地缓存 → Bloom Filter 快速排除 → RocksDB LSM

三句话记住:

  1. 写入 :Coordinator 不在数据路径上,先 WAL 后 RocksDB,acks=all + min.insync.replicas=2 是持久性的底线。
  2. 扫描:请求里带投影列和谓词,服务端做列裁剪和过滤,宽表场景网络传输能降 99%。
  3. 点查:LRU → Bloom → RocksDB 三级优化,把"远程查询"变成"大部分时候的本地内存查询"。

下一篇,我们关注集群本身:启动选举、副本状态机、以及扩缩容时的数据迁移


相关推荐
头茬韭菜1 小时前
图解 Fluss(五):Flink 接入与数据生命周期 —— Connector、表生命周期与冷热分层
大数据·flink·fluss
StarRocks_labs2 小时前
StarRocks × Fluss × Paimon:流湖仓分析与 ETL 闭环
starrocks·flink·jni·native·paimon·湖仓·fluss
头茬韭菜2 小时前
图解 Fluss(四):分布式协调 —— 选举、副本状态机与
分布式·fluss
头茬韭菜1 天前
图解 Fluss(一):一张图看清整体架构,两张图理解核心服务
架构·fluss
StarRocks_labs1 天前
基于 Fluss、Paimon 与 StarRocks 构建淘天集团湖流一体数据链路
starrocks·olap·schema·paimon·fluss·湖流一体
StarRocks_labs2 天前
StarRocks x Fluss x Paimon 湖流一体方案:构建秒级响应、湖流一体的实时数据引擎
starrocks·kafka·lambda·查询·paimon·fluss·湖流一体
头茬韭菜3 天前
功能点 11-12:客户端、RPC 通信、监控与安全
网络协议·安全·rpc·fluss
头茬韭菜6 天前
功能点 9:Flink Connector
大数据·flink·fluss
头茬韭菜7 天前
功能点 8:Tiering 分层存储与 Lakehouse
fluss