功能点 6:Arrow 列式格式 —— 源码阅读笔记
对应源码阅读计划功能点 6:ArrowLogWriter/Reader、ColumnProjector 列裁剪、Arrow 内存管理。
笔记 6.1:ArrowLogWriter —— 列式写入
文件:ArrowLogWriter.java
路径:fluss-common/src/main/java/org/apache/fluss/record/ArrowLogWriter.java
核心实现
public class ArrowLogWriter implements LogWriter {
private final VectorSchemaRoot root; // Arrow 的列式内存结构
private final BufferAllocator allocator;
private final ArrowStreamWriter ipcWriter;
/**
* 将一批行数据以列式写入 Arrow IPC 格式
*/
public void write(LogRecordBatch batch) {
List<RowData> rows = batch.getRows();
int rowCount = rows.size();
// ★ 关键:按列填充数据(不是按行!)
for (int colIdx = 0; colIdx < schema.getFieldCount(); colIdx++) {
FieldVector vector = root.getVector(colIdx);
vector.allocateNew(); // 预分配内存
// 逐列写入(与行式相反)
for (int rowIdx = 0; rowIdx < rowCount; rowIdx++) {
setVectorValue(vector, rowIdx, rows.get(rowIdx), colIdx);
}
vector.setValueCount(rowCount);
}
root.setRowCount(rowCount);
// ★ 写入 Arrow IPC 格式
// Arrow IPC = 元数据 (Schema) + 数据体 (RecordBatch)
ipcWriter.writeBatch();
}
/**
* 将单个单元格的值写入对应的 Arrow Vector
*/
private void setVectorValue(FieldVector vector, int index, RowData row, int col) {
DataType type = schema.getField(col).getDataType();
switch (type.getPrimaryType()) {
case INT:
((IntVector) vector).set(index, row.getInt(col));
break;
case BIGINT:
((BigIntVector) vector).set(index, row.getLong(col));
break;
case STRING:
byte[] bytes = row.getString(col).getBytes();
((VarCharVector) vector).setSafe(index, bytes);
break;
case DECIMAL:
BigDecimal value = row.getDecimal(col);
((DecimalVector) vector).set(index, value);
break;
// ... 其他类型
}
}
}
写入到 .log 文件的 Arrow IPC 布局
.log 文件中的 Arrow 数据布局:
┌──────────────────────────────────────────┐
│ Arrow IPC Message: Schema │
│ - field[0]: user_id (INT64, not null) │
│ - field[1]: name (UTF8, nullable) │
│ - field[2]: amount (DECIMAL(10,2)) │
│ ... │
├──────────────────────────────────────────┤
│ Arrow IPC Message: RecordBatch 1 │
│ ┌────────────────────────────────────┐ │
│ │ user_id column: [1, 2, 3, ..., N] │ │ ← 连续内存
│ │ name column: ["Alice","Bob"...] │ │ ← 连续内存
│ │ amount column: [99.9, 49.9, ...] │ │ ← 连续内存
│ └────────────────────────────────────┘ │
├──────────────────────────────────────────┤
│ Arrow IPC Message: RecordBatch 2 │
│ ... │
└──────────────────────────────────────────┘
关键优势:同一列的所有值存储在连续内存中,CPU 缓存友好、SIMD 友好。
笔记 6.2:ArrowLogReader —— 列式读取
文件:ArrowLogReader.java
public class ArrowLogReader {
private final int[] projectedColumns; // 查询需要的列的索引
private final BufferAllocator allocator;
/**
* 读取一个 RecordBatch 并进行列裁剪
*/
public ArrowRecordBatch readBatch(FileChannel channel, long position) {
// 1. 从指定位置读取 Arrow IPC 消息
ArrowStreamReader reader = new ArrowStreamReader(channel, allocator);
// 2. 读取完整的 RecordBatch (所有列)
reader.loadNextBatch();
VectorSchemaRoot fullRoot = reader.getVectorSchemaRoot();
// 3. ★ 列裁剪:只保留查询需要的列
if (projectedColumns.length < fullRoot.getSchema().getFieldCount()) {
return project(fullRoot, projectedColumns);
}
// 4. 返回完整数据(无裁剪)
return new ArrowRecordBatch(fullRoot);
}
}
笔记 6.3:ColumnProjector —— 零拷贝列裁剪
文件:ColumnProjector.java
public class ColumnProjector {
/**
* 列裁剪的核心实现
* 从完整的 VectorSchemaRoot 中提取需要的列
* ★ 零拷贝:不复制数据,只修改引用
*/
public static VectorSchemaRoot project(
VectorSchemaRoot fullRoot,
int[] columnIndices) {
// 1. 创建投影后的 Schema
Schema fullSchema = fullRoot.getSchema();
Schema projectedSchema = projectSchema(fullSchema, columnIndices);
// 2. ★ 零拷贝:直接引用原 Vector,不复制数据
List<FieldVector> projectedVectors = new ArrayList<>();
for (int colIdx : columnIndices) {
FieldVector originalVector = fullRoot.getVector(colIdx);
// ★ TransferPair:创建对同一块内存的引用
// 底层共享 DirectByteBuffer,零拷贝!
TransferPair transfer = originalVector.getTransferPair(allocator);
transfer.transfer(); // 不复制数据,只转移所有权/引用
projectedVectors.add(transfer.getTo());
}
// 3. 创建投影后的 VectorSchemaRoot
VectorSchemaRoot projected = new VectorSchemaRoot(
projectedSchema, projectedVectors, fullRoot.getRowCount()
);
return projected;
}
}
零拷贝原理
原始数据 (200 列,Arrow RecordBatch)
┌────────────────────────────────────────────────────┐
│ col_0 │ col_1 │ ... │ user_id │ ... │ amount │ ... │ col_199 │
│ [val0] │ [val1] │ ... │ [100] │ ... │ [99.9] │ ... │ [val199]│
│ ↑ ↑
└───────────────────────────┼────────────────┼────────┘
│ │
查询只投影 user_id 和 amount:
│ │
┌───────┘ ┌───────┘
▼ ▼
┌──────────────┐ ┌──────────────┐
│ user_id │ │ amount │
│ [100] │ │ [99.9] │
└──────────────┘ └──────────────┘
↑ 这两个 Vector 指向原始内存缓冲区
↑ 没有复制数据,只是创建了新引用!
内存不复制,只修改指针引用。 这就是为什么 Fluss 能做到列裁剪几乎零 CPU 开销。
笔记 6.4:Arrow 内存管理
文件:ArrowMemoryAllocator.java
public class ArrowMemoryAllocator {
private final BufferAllocator allocator;
// 分配策略:
// 1. Direct Memory(堆外内存)→ 避免 GC 压力
// 2. Chunk 分配 → 减少内存碎片
// 3. 引用计数 → 自动释放
public ArrowBuf allocate(long size) {
// 从 Netty 的 PooledByteBufAllocator 升级为
// 自定义 Bump-Pointer ChunkedAllocationManager
return allocator.buffer(size);
}
}
Arrow 与 Flink 的类型映射
// org.apache.fluss.flink.row.RowDataSerializationSchema
public class RowDataSerializationSchema {
public ArrowRecordBatch serialize(List<RowData> rows) {
// Flink RowData → Arrow Vector 的类型映射:
//
// Flink Type → Arrow Type
// ─────────────────────────────────
// IntType → IntVector
// BigIntType → BigIntVector
// VarCharType → VarCharVector
// DecimalType → DecimalVector
// TimestampType → TimeStampMicroVector
// ArrayType → ListVector
// MapType → MapVector
ArrowLogWriter writer = new ArrowLogWriter(arrowSchema);
for (RowData row : rows) {
writer.write(row);
}
return writer.flush();
}
}
阅读小结
| 已理解 | 尚未深入 |
|---|---|
| ✅ Arrow 列式写入(逐列填充 Vector) | ⬜ Arrow IPC 协议的消息边界和压缩 |
| ✅ 列裁剪的 TransferPair 零拷贝机制 | ⬜ FilterContext 如何与列裁剪联动 |
| ✅ Arrow BufferAllocator 的 Direct Memory 策略 | ⬜ LargeVarCharVector 的大字符串处理 |
| ✅ Flink RowData ↔ Arrow Vector 的类型映射 | ⬜ V1 log batch format 的统计信息收集 |
下一步:功能点 7——副本管理器、ISR 判定、Leader 选举、Follower 同步机制。

607

被折叠的 条评论
为什么被折叠?



