第 2 篇:「Fluss 架构深入」—— CoordinatorServer、TabletServer 与存储引擎

第 2 篇:「Fluss 架构深入」------ CoordinatorServer、TabletServer 与存储引擎

阅读本文你将了解: Fluss 集群的完整架构、CoordinatorServer 和 TabletServer 的职责分工、LogStore 和 KvStore 的存储机制、Tablet/Bucket 如何组织数据、以及远程存储和 ZooKeeper 的角色。


2.1 架构全景

Fluss 集群由四层构成:

复制代码
                    ┌──────────────────────────────────┐
                    │    Flink / Spark / 自定义客户端      │
                    │  (Stream Read/Write, PK Lookup)    │
                    └──────────┬──────────┬─────────────┘
                               │          │
                    ┌──────────▼──┐  ┌───▼──────────┐
                    │ Coordinator │  │ TabletServer  │
                    │   Server    │  │    x N        │
                    │             │  │               │
                    │ ┌─────────┐ │  │ ┌───────────┐ │
                    │ │ 元数据   │ │  │ │ LogStore  │ │
                    │ │ Tablet  │ │  │ │ (WAL/流)   │ │
                    │ │ 分配    │ │  │ ├───────────┤ │
                    │ │ Rebalance│ │  │ │ KvStore   │ │
                    │ └─────────┘ │  │ │ (RocksDB) │ │
                    └──────┬──────┘  │ └───────────┘ │
                           │         └───────┬───────┘
                    ┌──────▼──────────────────────┐
                    │      ZooKeeper              │
                    │   (集群协调 / 元数据存储)     │
                    └─────────────────────────────┘
                    ┌─────────────────────────────┐
                    │      Remote Storage          │
                    │   (S3 / Iceberg / Paimon /  │
                    │    Lance / RustFS)           │
                    └─────────────────────────────┘

关键设计原则

  1. 无共享架构:每个 TabletServer 独立运行,不共享本地存储
  2. 计算存储分离:CoordinatorServer 只管元数据和调度,TabletServer 只管数据
  3. 存算彻底分离:状态从 Flink 中剥离到 Fluss,Flink 任务变成纯计算节点

2.2 CoordinatorServer 源码剖析

CoordinatorServer 是集群的「大脑」。其核心类结构如下:

复制代码
org.apache.fluss.server.coordinator
├── CoordinatorServer.java        # 启动入口
├── CoordinatorService.java       # 核心服务逻辑
├── TableManager.java             # 表元数据管理
├── TabletAllocator.java          # Tablet 分配策略
└── RebalanceCoordinator.java     # 重平衡协调

2.2.1 启动流程

java 复制代码
// 简化自 org.apache.fluss.server.coordinator.CoordinatorServer
public class CoordinatorServer {
    
    private final Configuration conf;
    private final CoordinatorService coordinatorService;
    
    public void start() throws Exception {
        // 1. 初始化 ZooKeeper 连接
        ZooKeeperClient zkClient = new ZooKeeperClient(conf.get(ZOOKEEPER_ADDRESS));
        
        // 2. 初始化元数据管理器
        MetaDataManager metaDataManager = new MetaDataManager(zkClient);
        
        // 3. 初始化 Tablet 分配器
        TabletAllocator tabletAllocator = new TabletAllocator(metaDataManager);
        
        // 4. 启动 RPC 服务
        RpcServer rpcServer = new RpcServer(conf.get(COORDINATOR_PORT));
        rpcServer.start();
        
        // 5. 启动核心协调服务
        coordinatorService = new CoordinatorService(
            conf, zkClient, metaDataManager, tabletAllocator, rpcServer
        );
        coordinatorService.start();
    }
}

2.2.2 元数据管理

java 复制代码
// 简化自 TableManager 的实现逻辑
public class TableManager {
    
    // 核心元数据结构
    private final Map<TablePath, TableDescriptor> tables;
    private final Map<Long, List<TabletServerInfo>> tabletAssignments;
    
    /**
     * 创建表时的核心流程
     */
    public void createTable(TablePath tablePath, TableDescriptor descriptor) {
        // 1. 校验表 Schema(PK 表必须定义主键)
        SchemaValidator.validate(descriptor);
        
        // 2. 计算所需 Tablet 数量
        int numBuckets = descriptor.getBucketNum();  // 用户指定或默认
        int numPartitions = descriptor.getPartitionKeys().isEmpty() 
            ? 1 
            : calculatePartitions(descriptor);
        int totalTablets = numBuckets * numPartitions;
        
        // 3. 分配 Tablet 到 TabletServer
        List<TabletAssignment> assignments = tabletAllocator.allocate(
            totalTablets, getAvailableServers()
        );
        
        // 4. 向 ZooKeeper 写入元数据
        zkClient.createTableNode(tablePath, descriptor, assignments);
        
        // 5. 通知 TabletServer 加载新 Tablet
        for (TabletAssignment assignment : assignments) {
            notifyServerToLoadTablet(assignment);
        }
    }
}

2.2.3 Tablet 分配策略

Fluss 的 Tablet 分配采用轮询+容量感知策略:

java 复制代码
// 简化分配逻辑
public class TabletAllocator {
    
    public List<TabletAssignment> allocate(int numTablets, List<TabletServerInfo> servers) {
        List<TabletAssignment> assignments = new ArrayList<>();
        
        // 按当前负载排序(轻载优先)
        servers.sort(Comparator.comparing(TabletServerInfo::getTabletCount));
        
        int serverIndex = 0;
        for (int i = 0; i < numTablets; i++) {
            TabletServerInfo selected = servers.get(serverIndex % servers.size());
            assignments.add(new TabletAssignment(i, selected.getServerId()));
            selected.incrementTabletCount();
            serverIndex++;
        }
        
        return assignments;
    }
}

2.2.4 Rebalance 机制

当集群扩缩容时,CoordinatorServer 触发 Rebalance:

复制代码
触发条件:
  ├── 新增 TabletServer 节点
  ├── TabletServer 节点下线
  ├── Tablet 负载严重不均衡
  └── 管理员手动触发

Rebalance 流程:
  1. 计算目标分配方案(最优 Tablet 分布)
  2. 对比当前分配方案与目标方案的差异
  3. 生成迁移计划(最小迁移成本)
  4. 分批执行迁移(避免一次性迁移过多 Tablet)
  5. 迁移完成后更新 ZooKeeper 元数据

2.3 TabletServer 源码剖析

2.3.1 核心类结构

复制代码
org.apache.fluss.server.tablet
├── TabletServer.java            # 启动入口
├── TabletService.java           # Tablet 生命周期管理
├── ReplicaManager.java          # 副本管理
├── log/
│   ├── LogTablet.java           # Log 存储实现
│   ├── LogSegment.java          # 段文件管理
│   └── LogManager.java          # Log 整体管理
└── kv/
    ├── KvTablet.java            # KV 存储实现
    ├── KvManager.java           # KV 整体管理
    └── rocksdb/
        └── RocksDBKv.java       # RocksDB 封装

2.3.2 TabletServer 启动与 Tablet 加载

java 复制代码
public class TabletServer {
    
    private final TabletService tabletService;
    private final ReplicaManager replicaManager;
    
    public void start() {
        // 1. 初始化网络层
        TransportLayer transport = new TransportLayer(conf);
        
        // 2. 初始化 LogManager(管理所有 LogTablet)
        LogManager logManager = new LogManager(conf);
        
        // 3. 初始化 KvManager(管理所有 KvTablet)
        KvManager kvManager = new KvManager(conf);
        
        // 4. 初始化副本管理器
        replicaManager = new ReplicaManager(logManager, kvManager);
        
        // 5. 向 CoordinatorServer 注册
        registerToCoordinator();
        
        // 6. 启动 Tablet 服务
        tabletService = new TabletService(replicaManager, transport);
        tabletService.start();
    }
    
    /**
     * 当 CoordinatorServer 通知加载 Tablet 时调用
     */
    public void loadTablet(TabletAssignment assignment) {
        long tabletId = assignment.getTabletId();
        TablePath tablePath = assignment.getTablePath();
        
        // 如果是 Leader 副本,需要同时加载 LogTablet 和 KvTablet
        if (assignment.isLeader()) {
            // 加载 LogTablet(WAL)
            LogTablet logTablet = replicaManager.getOrCreateLogTablet(
                tablePath, tabletId
            );
            
            // 如果是 PK 表,还需要加载 KvTablet
            if (assignment.getTableType() == TableType.PRIMARY_KEY) {
                KvTablet kvTablet = replicaManager.getOrCreateKvTablet(
                    tablePath, tabletId
                );
                // 可能需要从 LogTablet 重放 WAL 恢复 KvTablet
                kvTablet.recoverFromLog(logTablet);
            }
        }
    }
}

2.3.3 LogTablet:日志存储引擎

LogTablet 是 Fluss 写入路径的核心。每个 LogTablet 由多个 LogSegment 组成。

java 复制代码
// 简化自 org.apache.fluss.server.tablet.log.LogTablet
public class LogTablet {
    
    private final long tabletId;
    private final ConcurrentSkipListMap<Long, LogSegment> segments;
    private final ReplicaManager replicaManager;
    
    /**
     * 追加消息到 LogTablet
     */
    public long append(LogRecordBatch batch) {
        // 1. 获取当前活跃的 Segment
        LogSegment activeSegment = getActiveSegment();
        
        // 2. 检查 Segment 是否已满(默认 1GB)
        if (activeSegment.size() >= maxSegmentSize) {
            activeSegment = rollNewSegment();
        }
        
        // 3. 写入数据到 .log 文件
        long offset = activeSegment.append(batch);
        
        // 4. 更新 .index 文件(稀疏索引:offset → 物理位置)
        if (shouldIndex(offset)) {
            activeSegment.appendIndex(offset, activeSegment.getPosition());
        }
        
        // 5. 如果是 Leader,同步到 Follower 副本(ISR 机制)
        if (isLeader()) {
            replicaManager.replicateToFollowers(tabletId, offset);
        }
        
        return offset;
    }
}

LogSegment 物理布局:

复制代码
LogTablet (tablet_id=0)
├── 00000000000000000000.log     # Segment 0: offset 0 ~ 9999
├── 00000000000000000000.index   # 稀疏索引(每 4KB 一条)
├── 00000000000000010000.log     # Segment 1: offset 10000 ~ 19999
├── 00000000000000010000.index
├── 00000000000000020000.log     # Segment 2: offset 20000 ~ ...
└── 00000000000000020000.index

2.3.4 KvTablet:RocksDB 封装

PK 表的可更新和可查询能力来源于 KvTablet。

java 复制代码
// 简化自 org.apache.fluss.server.tablet.kv.KvTablet
public class KvTablet {
    
    private final long tabletId;
    private final RocksDBKv rocksDB;
    private final LogTablet walLogTablet;
    
    /**
     * 写入(Put/Upsert)操作
     */
    public void put(byte[] key, byte[] value) {
        // 1. 先写入 WAL(LogTablet),保证持久性
        long walOffset = walLogTablet.append(
            new LogRecord(key, value, RecordType.PUT)
        );
        
        // 2. 写入 RocksDB
        rocksDB.put(key, value);
    }
    
    /**
     * 删除操作
     */
    public void delete(byte[] key) {
        // 1. WAL 记录删除
        walLogTablet.append(new LogRecord(key, null, RecordType.DELETE));
        
        // 2. RocksDB 标记删除
        rocksDB.delete(key);
    }
    
    /**
     * 点查询
     */
    public byte[] get(byte[] key) {
        // 直接从 RocksDB 读取,亚毫秒级延迟
        return rocksDB.get(key);
    }
    
    /**
     * 故障恢复:从 WAL 重放所有操作到 RocksDB
     */
    public void recoverFromLog(LogTablet logTablet) {
        LogScanner scanner = logTablet.newScanner(0); // 从 offset 0 开始
        LogRecord record;
        while ((record = scanner.next()) != null) {
            switch (record.getType()) {
                case PUT:
                    rocksDB.put(record.getKey(), record.getValue());
                    break;
                case DELETE:
                    rocksDB.delete(record.getKey());
                    break;
            }
        }
    }
}

RocksDB 配置:

java 复制代码
// Fluss 对 RocksDB 的优化配置
public class RocksDBConfig {
    // 使用 Bloom Filter 加速点查询
    public static final boolean BLOOM_FILTER = true;  // 10 bits per key
    
    // Block Cache 大小(默认 256MB)
    public static final long BLOCK_CACHE_SIZE = 256 * 1024 * 1024;
    
    // LSM 树配置:减少写放大
    public static final int WRITE_BUFFER_SIZE = 64 * 1024 * 1024;  // 64MB
    public static final int MAX_WRITE_BUFFER_NUMBER = 3;
    
    // Compaction 策略:Level Compaction
    public static final int NUM_LEVELS = 7;
}

2.4 数据分布层次

复制代码
Database: "ecommerce"
│
├── Table: "orders"
│   ├── Partition: dt=2026-08-01
│   │   ├── Bucket 0 → Tablet 0 (LogTablet + KvTablet)
│   │   ├── Bucket 1 → Tablet 1 (LogTablet + KvTablet)
│   │   ├── Bucket 2 → Tablet 2 (LogTablet + KvTablet)
│   │   └── Bucket 3 → Tablet 3 (LogTablet + KvTablet)
│   ├── Partition: dt=2026-08-02
│   │   ├── Bucket 0 → Tablet 4
│   │   ├── Bucket 1 → Tablet 5
│   │   └── ...
│   └── ...
│
├── Table: "user_profiles" (PK Table, 无分区)
│   ├── Bucket 0 → Tablet 100 (LogTablet + KvTablet)
│   ├── Bucket 1 → Tablet 101
│   └── Bucket 2 → Tablet 102

Bucket 分配规则

java 复制代码
/**
 * 根据 bucketing key 决定数据落入哪个 Bucket
 * 默认使用 Hash 分区
 */
public class BucketingFunction {
    
    public int getBucket(byte[] bucketingKey, int numBuckets) {
        int hash = MurmurHash3.hash32(bucketingKey);
        return Math.abs(hash) % numBuckets;
    }
}

同一个 Bucket 的 LogTablet 和 KvTablet 始终分配在同一个 TabletServer 上,以保证:

  • 写入路径:先写 LogTablet(WAL) 再写 KvTablet,本地操作无网络延迟
  • 恢复路径:KvTablet 从本地 LogTablet 重放 WAL,快速恢复

2.5 副本机制与 ISR

Fluss 的副本机制仅应用于 LogTablet。KvTablet 当前不支持副本(通过 WAL 恢复)。

复制代码
Tablet 0 (replication factor = 3)
├── Leader   → TabletServer-1  (处理读写)
├── Follower → TabletServer-2  (同步复制)
└── Follower → TabletServer-3  (同步复制)

ISR (In-Sync Replica) = { TabletServer-1, TabletServer-2, TabletServer-3 }

写入一致性保证

复制代码
Client Write
    │
    ▼
Leader (TabletServer-1)
    │
    ├── 1. 写入本地 LogTablet
    │
    ├── 2. 同步复制到 ISR 中的 Follower
    │   ├── Follower-1 (TabletServer-2): ACK ✓
    │   └── Follower-2 (TabletServer-3): ACK ✓
    │
    ├── 3. 收到所有 ISR 的 ACK(或达到 min.insync.replicas)
    │
    └── 4. 返回成功给客户端

Leader 故障切换

复制代码
1. ZooKeeper 检测到 Leader (TabletServer-1) 心跳超时
2. CoordinatorServer 从 ISR 中选择新 Leader
   → 选择 TabletServer-2(log end offset 与旧 Leader 最接近)
3. CoordinatorServer 更新 ZooKeeper 元数据
4. TabletServer-2 晋升为 Leader,开始处理读写
5. 客户端自动重定向到新 Leader

2.6 远程存储层

远程存储基于 S3 兼容接口,核心配置:

yaml 复制代码
# fluss-conf.yaml
remote.data.dir: s3://my-bucket/fluss-data/
s3.endpoint: https://s3.amazonaws.com
s3.access-key: AKIAIOSFODNN7EXAMPLE
s3.secret-key: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY

分层存储流程

复制代码
LogTablet Local Disk (SSD)
    │  保留 1-3 天数据
    │  写入:低延迟 < 1ms
    │  读取:热数据直接本地读取
    │
    ▼  Tiering Service 定期执行
    │
Remote Storage (S3/对象存储)
    │  保留 30-365 天数据
    │  格式:Parquet(高压缩比)
    │  读取:客户端可直接批量读取

源码支撑: org.apache.fluss.server.tiering.TieringService 负责定期将本地热数据 Compaction 为 Parquet 格式后上传到远程存储。


2.7 总结与下一篇预告

组件 核心职责
CoordinatorServer 元数据管家、Tablet 分配、Rebalance、Leader 选举
TabletServer 数据存储(LogStore 写,KvStore 查)、副本同步
LogTablet 仅追加的 WAL 日志,Segment 分段存储,.index 稀疏索引
KvTablet RocksDB LSM 引擎,支持点查、更新、删除,WAL 保证持久性
ZooKeeper 集群协调,未来将被 Raft + KvStore 替代
Remote Storage 冷数据存档,降低本地存储成本

下一篇我们将学习 Fluss 最核心的用户接口:表设计。如何选择 Log Table 还是 PK Table?分区和分桶如何搭配?Schema Evolution 怎么用?这些都是生产环境中每天都在面对的问题。


本文基于 Apache Fluss 0.9.1 源码。项目 GitHub: https://github.com/apache/fluss

相关推荐
无忧智库2 小时前
企业数字化底座与数字化转型方案拆解:从贴源数据区到管理分析平台的完整落地路径(PPT)
大数据·架构
无忧智库2 小时前
烟草物流企业数字化转型规划方案拆解:从战略绩效到物流作业的全链条信息化设计(PPT)
大数据·架构
小园子的小菜4 小时前
RocketMQ 核心原理、架构优势与核心机制全解
架构·rocketmq
敲代码的玉米C4 小时前
让两个模型一写一审
前端·人工智能·架构
草莓熊Lotso5 小时前
【Linux网络】从0手写Reactor反应堆(二):完善核心细节——ET非阻塞读写、分层架构与回调机制
linux·运维·服务器·网络·c++·tcp/ip·架构
敲代码的玉米C5 小时前
补 322 个测试,挖出 19 个 bug
前端·人工智能·架构
敲代码的玉米C5 小时前
怎么让 Agent 没法假装自己成功了
前端·人工智能·架构
阿图灵5 小时前
Agentic AI 架构入门(三):Agent 的七大组件与 PRAL 循环
人工智能·架构·llm·rag·ai agent·智能体·agentic ai
YuePeng5 小时前
Java 开发者的 Django Admin,终于来了
后端·架构·github