Paimon Action Jar 实现机制分析

Paimon Action Jar 实现机制分析

目录

  • [1. 概述](#1. 概述)
  • [2. 整体架构设计](#2. 整体架构设计)
  • [3. SPI 服务发现机制](#3. SPI 服务发现机制)
  • [4. Action 执行流程](#4. Action 执行流程)
  • [5. ExpireSnapshotsAction 详细分析](#5. ExpireSnapshotsAction 详细分析)
  • [6. 如何实现自定义 Action](#6. 如何实现自定义 Action)
  • [7. 最佳实践和注意事项](#7. 最佳实践和注意事项)
  • [8. 总结](#8. 总结)

1. 概述

Paimon Action Jar 是 Apache Paimon 提供的一套用于表维护操作的命令行工具框架。通过 flink run 命令,用户可以执行各种维护操作,如快照过期、分区删除、表压缩等。

1.1 使用示例

bash 复制代码
<FLINK_HOME>/bin/flink run \
    /path/to/paimon-flink-action.jar \
    expire_snapshots \
    --warehouse <warehouse-path> \
    --database <database-name> \
    --table <table-name> \
    --retain_max 5 \
    --retain_min 10 \
    --older_than '2024-01-01 12:00:00' \
    --max_deletes 10

1.2 核心特性

  • 插件化架构:基于 Java SPI 实现可扩展的 Action 体系
  • 模块隔离:独立的入口模块避免类加载冲突
  • 执行模式:支持本地执行(LocalAction)和 Flink 作业两种模式
  • 统一接口:所有维护操作遵循统一的 Action 接口规范

2. 整体架构设计

2.1 模块划分

Paimon Action Jar 采用了模块化设计,主要分为两个模块:

位置paimon-flink/paimon-flink-action/

职责

  • 提供唯一的入口类 FlinkActions
  • 作为独立的可执行 JAR,包含 Main-Class 声明
  • 避免与 Flink lib 目录中的 paimon-flink.jar 产生类加载冲突

pom.xml 配置

xml 复制代码
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-shade-plugin</artifactId>
    <executions>
        <execution>
            <configuration>
                <transformers>
                    <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                        <mainClass>org.apache.paimon.flink.action.FlinkActions</mainClass>
                    </transformer>
                </transformers>
            </configuration>
        </execution>
    </executions>
</plugin>

FlinkActions.java

java 复制代码
public class FlinkActions {
    public static void main(String[] args) throws Exception {
        if (args.length < 1) {
            printDefaultHelp();
            System.exit(1);
        }

        Optional<Action> action = ActionFactory.createAction(args);

        if (action.isPresent()) {
            action.get().run();
        } else {
            System.exit(1);
        }
    }
}

位置paimon-flink/paimon-flink-common/

职责

  • 包含所有 Action 的实现类
  • 包含所有 ActionFactory 的实现类
  • 提供 Action 基础设施(ActionBase、LocalAction 等)
  • 包含 Procedure 实现(用于 Flink SQL CALL 语句)

2.2 核心类图

creates
creates
<<interface>>
Factory
+identifier() : String
<<interface>>
ActionFactory
+create(params) : Optional<Action>
+printHelp() : void
+createAction(args) : Optional<Action>
+catalogConfigMap(params) : Map
<<interface>>
Action
+run() : void
+build() : void
<<abstract>>
ActionBase
#catalogOptions Options
#catalog Catalog
#flinkCatalog FlinkCatalog
#env StreamExecutionEnvironment
#batchTEnv StreamTableEnvironment
#forceStartFlinkJob boolean
+ActionBase(catalogConfig)
+run() : void
#initCatalog() : void
#initFlinkEnv(env) : void
#execute(name) : void
<<interface>>
LocalAction
+executeLocally() : void
ExpireSnapshotsActionFactory
+IDENTIFIER "expire_snapshots"
+identifier() : String
+create(params) : Optional<Action>
+printHelp() : void
ExpireSnapshotsAction
-database String
-table String
-retainMax Integer
-retainMin Integer
-olderThan String
-maxDeletes Integer
-options String
+ExpireSnapshotsAction(...)
+executeLocally() : void
CompactAction
-partitions List
-whereSql String
-fullCompaction Boolean
+CompactAction(...)
+build() : void
CompactActionFactory

2.3 设计理念

问题背景

  • Flink 的 flink run 命令要求指定一个包含 Main-Class 的 JAR
  • 如果直接使用 paimon-flink.jar,会导致类加载冲突
  • Flink lib 目录和用户 ClassLoader 中都包含相同的类

解决方案

  • 创建独立的 paimon-flink-action.jar
  • 只包含 FlinkActions 入口类
  • 依赖 paimon-flink-common(scope=provided)
  • 运行时从 Flink lib 目录加载实际的 Action 实现
2.3.2 LocalAction vs 普通 Action

LocalAction

  • 适用于轻量级维护操作
  • 默认在客户端本地执行,不启动 Flink 作业
  • 示例:expire_snapshots、rollback_to、create_tag
  • 优势:执行快速,资源消耗小

普通 Action

  • 适用于需要分布式计算的操作
  • 必须构建完整的 Flink 作业图
  • 示例:compact、merge_into
  • 优势:可以利用 Flink 的并行处理能力

3. SPI 服务发现机制

3.1 什么是 SPI

SPI(Service Provider Interface)是 Java 提供的服务发现机制,允许在运行时动态加载接口的实现类。

3.2 SPI 配置文件

位置paimon-flink/paimon-flink-common/src/main/resources/META-INF/services/org.apache.paimon.factories.Factory

内容片段

复制代码
### action factories
org.apache.paimon.flink.action.CopyFilesActionFactory
org.apache.paimon.flink.action.CompactActionFactory
org.apache.paimon.flink.action.CompactDatabaseActionFactory
org.apache.paimon.flink.action.DropPartitionActionFactory
org.apache.paimon.flink.action.DeleteActionFactory
org.apache.paimon.flink.action.MergeIntoActionFactory
org.apache.paimon.flink.action.RollbackToActionFactory
...
org.apache.paimon.flink.action.ExpireSnapshotsActionFactory  # 第 44 行
org.apache.paimon.flink.action.ExpireChangelogsActionFactory
...

### procedure factories
org.apache.paimon.flink.procedure.CompactDatabaseProcedure
org.apache.paimon.flink.procedure.CompactProcedure
...
org.apache.paimon.flink.procedure.ExpireSnapshotsProcedure  # 第 74 行
...

3.3 SPI 加载流程

3.3.1 FactoryUtil.discoverFactory() 方法

源码位置paimon-api/src/main/java/org/apache/paimon/factories/FactoryUtil.java

java 复制代码
public static <T extends Factory> T discoverFactory(
        ClassLoader classLoader, Class<T> factoryClass, String identifier) {
    // 1. 加载所有 Factory 实现
    final List<Factory> factories = getFactories(classLoader);

    // 2. 过滤出指定类型的 Factory
    final List<Factory> foundFactories =
            factories.stream()
                    .filter(f -> factoryClass.isAssignableFrom(f.getClass()))
                    .collect(Collectors.toList());

    // 3. 根据 identifier 匹配
    final List<Factory> matchingFactories =
            foundFactories.stream()
                    .filter(f -> f.identifier().equals(identifier))
                    .collect(Collectors.toList());

    // 4. 返回匹配的 Factory
    if (matchingFactories.size() == 1) {
        return (T) matchingFactories.get(0);
    }
    
    // 处理错误情况...
}
3.3.2 ServiceLoader 加载
java 复制代码
public static <T> List<T> discoverFactories(ClassLoader classLoader, Class<T> klass) {
    final Iterator<T> serviceLoaderIterator = ServiceLoader.load(klass, classLoader).iterator();

    final List<T> loadResults = new ArrayList<>();
    while (serviceLoaderIterator.hasNext()) {
        try {
            loadResults.add(serviceLoaderIterator.next());
        } catch (NoClassDefFoundError e) {
            // 处理可选依赖缺失的情况
            LOG.debug("NoClassDefFoundError when loading factory", e);
        }
    }

    return loadResults;
}

3.4 ActionFactory.createAction() 流程

源码位置paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/action/ActionFactory.java

java 复制代码
static Optional<Action> createAction(String[] args) {
    // 1. 提取 action 名称(第一个参数)
    String action = args[0].toLowerCase().replaceAll("-", "_");
    String[] actionArgs = Arrays.copyOfRange(args, 1, args.length);
    
    // 2. 使用 SPI 加载对应的 ActionFactory
    ActionFactory actionFactory;
    try {
        actionFactory = FactoryUtil.discoverFactory(
                ActionFactory.class.getClassLoader(), 
                ActionFactory.class, 
                action);
    } catch (FactoryException e) {
        printDefaultHelp();
        throw new UnsupportedOperationException("Unknown action \"" + action + "\".");
    }

    LOG.info("{} job args: {}", actionFactory.identifier(), String.join(" ", actionArgs));

    // 3. 解析命令行参数
    MultipleParameterToolAdapter params = new MultipleParameterToolAdapter(actionArgs);
    
    // 4. 处理 --help 参数
    if (params.has(HELP)) {
        actionFactory.printHelp();
        return Optional.empty();
    }

    // 5. 调用 Factory 创建 Action 实例
    Optional<Action> optionalAction = actionFactory.create(params);

    // 6. 处理 --force_start_flink_job 参数
    if (params.has(FORCE_START_FLINK_JOB)) {
        optionalAction = optionalAction.map(a -> {
            return ((ActionBase) a).forceStartFlinkJob(
                    Boolean.parseBoolean(params.get(FORCE_START_FLINK_JOB)));
        });
    }

    return optionalAction;
}

4. Action 执行流程

4.1 完整执行流程图

ExpireSnapshotsImpl ExpireSnapshotsProcedure ActionBase.run ExpireSnapshotsAction ExpireSnapshotsActionFactory FactoryUtil ActionFactory FlinkActions.main 用户命令行 ExpireSnapshotsImpl ExpireSnapshotsProcedure ActionBase.run ExpireSnapshotsAction ExpireSnapshotsActionFactory FactoryUtil ActionFactory FlinkActions.main 用户命令行 解析 action = "expire_snapshots" 使用 ServiceLoader 加载所有 Factory 从 META-INF/services 文件读取 解析参数 - database - table - retain_max - retain_min - older_than - max_deletes 检测到 LocalAction 且未强制启动 Flink 作业 计算过期快照范围 删除快照文件 删除数据文件 强制启动 Flink 作业模式 在 Flink 算子中执行 executeLocally() 需要构建 Flink 作业图 构建 Source、Transform、Sink alt [LocalAction && !forceStartFlinkJob] [LocalAction && forceStartFlinkJob] [普通 Action (如 CompactAction)] flink run paimon-flink-action.jar expire_snapshots --warehouse ... --database ... --table ... createAction(args) discoverFactory(ActionFactory.class, "expire_snapshots") 过滤并匹配 identifier ExpireSnapshotsActionFactory 实例 create(params) new ExpireSnapshotsAction(...) action 实例 Optional.of(action) Optional~Action~ action.run() super.run() executeLocally() new ExpireSnapshotsProcedure() withCatalog(catalog) call(null, "db.table", retainMax, ...) table.newExpireSnapshots() new ExpireSnapshotsImpl(...) config(expireConfig).expire() 返回删除数量 String[] result 完成 env.fromSequence(0, 0) .flatMap(LocalActionExecutor) execute("ExpireSnapshotsAction") build() execute("CompactAction") 完成 执行成功

4.2 ActionBase.run() 核心逻辑

源码位置paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/action/ActionBase.java

java 复制代码
@Override
public void run() throws Exception {
    // 判断是否为 LocalAction
    if (LocalAction.class.isAssignableFrom(this.getClass())) {
        if (forceStartFlinkJob) {
            // 强制启动 Flink 作业模式
            // 将 LocalAction 包装成 Flink 算子执行
            env.fromSequence(0, 0)
                    .flatMap(new LocalActionExecutor<>(this))
                    .setParallelism(1)
                    .sinkTo(new DiscardingSink<>());
            execute(this.getClass().getSimpleName());
        } else {
            // 默认本地执行模式
            ((LocalAction) this).executeLocally();
        }
    } else {
        // 普通 Action:构建 Flink 作业图并执行
        build();
        execute(this.getClass().getSimpleName());
    }
}

4.3 LocalActionExecutor 包装器

java 复制代码
private static class LocalActionExecutor<T extends ActionBase & LocalAction>
        extends RichFlatMapFunction<Long, Object> {
    private final T action;

    public void open(Configuration parameters) {
        // 在 Flink 算子中初始化 Catalog
        action.initCatalog();
    }

    @Override
    public void flatMap(Long aLong, Collector<Object> collector) throws Exception {
        // 在 Flink 算子中执行 LocalAction
        action.executeLocally();
    }
}

5. ExpireSnapshotsAction 详细分析

5.1 命令行参数

bash 复制代码
<FLINK_HOME>/bin/flink run \
    /path/to/paimon-flink-action.jar \
    expire_snapshots \
    --warehouse <warehouse-path>      # Catalog 配置:数据仓库路径
    --database <database-name>        # 目标数据库名称
    --table <table-name>              # 目标表名称
    --retain_max <num>                # 最多保留的快照数量
    --retain_min <num>                # 至少保留的快照数量
    --older_than <timestamp>          # 删除早于此时间的快照
    --max_deletes <num>               # 单次最多删除的快照数量
    --catalog_conf key=value          # 额外的 Catalog 配置

5.2 参数映射关系

命令行参数 Java 字段 说明
--warehouse catalogOptions.warehouse 通过 catalogConfigMap() 映射
--database ExpireSnapshotsAction.database 直接映射
--table ExpireSnapshotsAction.table 直接映射
--retain_max ExpireSnapshotsAction.retainMax 转换为 Integer
--retain_min ExpireSnapshotsAction.retainMin 转换为 Integer
--older_than ExpireSnapshotsAction.olderThan 时间戳字符串
--max_deletes ExpireSnapshotsAction.maxDeletes 转换为 Integer
--catalog_conf catalogOptions 解析为 Map<String, String>

5.3 ExpireSnapshotsActionFactory 实现

源码位置paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/action/ExpireSnapshotsActionFactory.java

java 复制代码
public class ExpireSnapshotsActionFactory implements ActionFactory {

    public static final String IDENTIFIER = "expire_snapshots";

    private static final String RETAIN_MAX = "retain_max";
    private static final String RETAIN_MIN = "retain_min";
    private static final String OLDER_THAN = "older_than";
    private static final String MAX_DELETES = "max_deletes";
    private static final String OPTIONS = "options";

    @Override
    public String identifier() {
        return IDENTIFIER;
    }

    @Override
    public Optional<Action> create(MultipleParameterToolAdapter params) {
        // 解析参数(可选)
        Integer retainMax =
                params.has(RETAIN_MAX) ? Integer.parseInt(params.get(RETAIN_MAX)) : null;
        Integer retainMin =
                params.has(RETAIN_MIN) ? Integer.parseInt(params.get(RETAIN_MIN)) : null;
        String olderThan = params.has(OLDER_THAN) ? params.get(OLDER_THAN) : null;
        Integer maxDeletes =
                params.has(MAX_DELETES) ? Integer.parseInt(params.get(MAX_DELETES)) : null;
        String options = params.has(OPTIONS) ? params.get(OPTIONS) : null;

        // 创建 Action 实例
        ExpireSnapshotsAction action =
                new ExpireSnapshotsAction(
                        params.getRequired(DATABASE),  // 必需参数
                        params.getRequired(TABLE),     // 必需参数
                        catalogConfigMap(params),      // Catalog 配置
                        retainMax,
                        retainMin,
                        olderThan,
                        maxDeletes,
                        options);

        return Optional.of(action);
    }

    @Override
    public void printHelp() {
        System.out.println("Action \"expire_snapshots\" expire the target snapshots.");
        System.out.println();
        System.out.println("Syntax:");
        System.out.println(
                "  expire_snapshots \\\n"
                        + "--warehouse <warehouse_path> \\\n"
                        + "--database <database> \\\n"
                        + "--table <table> \\\n"
                        + "--retain_max <max> \\\n"
                        + "--retain_min <min> \\\n"
                        + "--older_than <older_than> \\\n"
                        + "--max_delete <max_delete>");
    }
}

5.4 ExpireSnapshotsAction 实现

源码位置paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/action/ExpireSnapshotsAction.java

java 复制代码
public class ExpireSnapshotsAction extends ActionBase implements LocalAction {

    private final String database;
    private final String table;
    private final Integer retainMax;
    private final Integer retainMin;
    private final String olderThan;
    private final Integer maxDeletes;
    private final String options;

    public ExpireSnapshotsAction(
            String database,
            String table,
            Map<String, String> catalogConfig,
            Integer retainMax,
            Integer retainMin,
            String olderThan,
            Integer maxDeletes,
            String options) {
        super(catalogConfig);  // 初始化 Catalog
        this.database = database;
        this.table = table;
        this.retainMax = retainMax;
        this.retainMin = retainMin;
        this.olderThan = olderThan;
        this.maxDeletes = maxDeletes;
        this.options = options;
    }

    @Override
    public void executeLocally() throws Exception {
        // 创建 Procedure 实例
        ExpireSnapshotsProcedure expireSnapshotsProcedure = new ExpireSnapshotsProcedure();
        
        // 设置 Catalog
        expireSnapshotsProcedure.withCatalog(catalog);
        
        // 调用 Procedure(复用 Flink SQL CALL 的逻辑)
        expireSnapshotsProcedure.call(
                null,                      // ProcedureContext(Action 中为 null)
                database + "." + table,    // 表标识符
                retainMax,
                retainMin,
                olderThan,
                maxDeletes,
                options);
    }
}

5.5 ExpireSnapshotsProcedure 实现

源码位置paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/procedure/ExpireSnapshotsProcedure.java

java 复制代码
public class ExpireSnapshotsProcedure extends ProcedureBase {

    @Override
    public String identifier() {
        return "expire_snapshots";
    }

    public String[] call(
            ProcedureContext procedureContext,
            String tableId,
            Integer retainMax,
            Integer retainMin,
            String olderThanStr,
            Integer maxDeletes,
            String options)
            throws Catalog.TableNotExistException {
        
        // 1. 获取表对象
        Table table = table(tableId);
        
        // 2. 解析动态选项
        HashMap<String, String> dynamicOptions = new HashMap<>();
        ProcedureUtils.putAllOptions(dynamicOptions, options);

        // 3. 应用动态选项(创建表的副本)
        table = table.copy(dynamicOptions);
        
        // 4. 创建 ExpireSnapshots 实例
        ExpireSnapshots expireSnapshots = table.newExpireSnapshots();

        // 5. 构建过期配置
        CoreOptions tableOptions = ((FileStoreTable) table).store().options();
        ExpireConfig.Builder builder =
                ProcedureUtils.fillInSnapshotOptions(
                        tableOptions, retainMax, retainMin, olderThanStr, maxDeletes);
        
        // 6. 执行快照过期
        int expiredCount = expireSnapshots.config(builder.build()).expire();
        
        return new String[] {expiredCount + ""};
    }
}

5.6 ExpireSnapshotsImpl 核心实现

源码位置paimon-core/src/main/java/org/apache/paimon/table/ExpireSnapshotsImpl.java

java 复制代码
public class ExpireSnapshotsImpl implements ExpireSnapshots {

    private final SnapshotManager snapshotManager;
    private final ChangelogManager changelogManager;
    private final ConsumerManager consumerManager;
    private final SnapshotDeletion snapshotDeletion;
    private final TagManager tagManager;

    private ExpireConfig expireConfig;

    @Override
    public int expire() {
        // 1. 设置配置
        snapshotDeletion.setChangelogDecoupled(expireConfig.isChangelogDecoupled());
        int retainMax = expireConfig.getSnapshotRetainMax();
        int retainMin = expireConfig.getSnapshotRetainMin();
        int maxDeletes = expireConfig.getSnapshotMaxDeletes();
        long olderThanMills =
                System.currentTimeMillis() - expireConfig.getSnapshotTimeRetain().toMillis();

        // 2. 获取最新和最早的快照 ID
        Long latestSnapshotId = snapshotManager.latestSnapshotId();
        if (latestSnapshotId == null) {
            return 0;  // 没有快照,无需过期
        }

        Long earliest = snapshotManager.earliestSnapshotId();
        if (earliest == null) {
            return 0;
        }

        // 3. 计算过期范围
        // retainMax: 从最新快照算起,最多保留的快照数量
        long min = Math.max(latestSnapshotId - retainMax + 1, earliest);

        // retainMin: 至少保留的快照数量(保护阈值)
        long maxExclusive = latestSnapshotId - retainMin + 1;

        // 保护正在被消费者读取的快照
        maxExclusive =
                Math.min(maxExclusive, consumerManager.minNextSnapshot().orElse(Long.MAX_VALUE));

        // 限制单次删除的快照数量
        maxExclusive = Math.min(maxExclusive, earliest + maxDeletes);

        // 4. 检查时间条件,提前退出
        for (long id = min; id < maxExclusive; id++) {
            if (snapshotManager.snapshotExists(id)
                    && olderThanMills <= snapshotManager.snapshot(id).timeMillis()) {
                return expireUntil(earliest, id);
            }
        }

        // 5. 执行过期
        return expireUntil(earliest, maxExclusive);
    }

    public int expireUntil(long earliestId, long endExclusiveId) {
        // 1. 找到第一个要过期的快照
        long beginInclusiveId = earliestId;
        for (long id = endExclusiveId - 1; id >= earliestId; id--) {
            if (!snapshotManager.snapshotExists(id)) {
                beginInclusiveId = id + 1;
                break;
            }
        }

        // 2. 获取被 Tag 保护的快照
        List<Snapshot> taggedSnapshots = tagManager.taggedSnapshots();

        // 3. 删除数据文件(合并树文件)
        // 范围:(beginInclusiveId, endExclusiveId]
        for (long id = beginInclusiveId + 1; id <= endExclusiveId; id++) {
            if (snapshotManager.snapshotExists(id)) {
                Snapshot snapshot = snapshotManager.snapshot(id);
                
                // 跳过被 Tag 保护的快照
                if (isTaggedSnapshot(snapshot, taggedSnapshots)) {
                    continue;
                }
                
                // 删除该快照的数据文件
                snapshotDeletion.deleteAddedDataFiles(snapshot);
            }
        }

        // 4. 删除 Manifest 文件
        for (long id = beginInclusiveId; id < endExclusiveId; id++) {
            if (snapshotManager.snapshotExists(id)) {
                Snapshot snapshot = snapshotManager.snapshot(id);
                
                if (!isTaggedSnapshot(snapshot, taggedSnapshots)) {
                    snapshotDeletion.deleteAddedManifests(snapshot);
                }
            }
        }

        // 5. 删除快照文件本身
        for (long id = beginInclusiveId; id < endExclusiveId; id++) {
            Snapshot snapshot;
            try {
                snapshot = snapshotManager.tryGetSnapshot(id);
            } catch (FileNotFoundException e) {
                beginInclusiveId = id + 1;
                continue;
            }
            
            // 如果启用了 changelog 解耦,提交 changelog
            if (expireConfig.isChangelogDecoupled()) {
                commitChangelog(new Changelog(snapshot));
            }
            
            // 删除快照文件
            snapshotManager.deleteSnapshot(id);
        }

        // 6. 写入最早快照的提示文件
        writeEarliestHint(endExclusiveId);
        
        LOG.info("Finished expire snapshots, range is [{}, {})", 
                beginInclusiveId, endExclusiveId);
        
        return (int) (endExclusiveId - beginInclusiveId);
    }
}

5.7 快照过期逻辑图





开始 expire
获取最新/最早快照ID
快照存在?
返回 0
计算过期范围
min = max latestId - retainMax + 1, earliest
maxExclusive = latestId - retainMin + 1
考虑消费者保护
限制 maxDeletes
遍历检查时间条件
older_than 条件满足?
调用 expireUntil
继续检查下一个
找到第一个要过期的快照
获取被 Tag 保护的快照列表
删除数据文件
删除 Manifest 文件
删除快照文件
写入 earliest hint
返回删除数量

5.8 参数约束和保护机制

参数 作用 约束条件
retain_max 最多保留的快照数 必须 >= retain_min
retain_min 至少保留的快照数 保护阈值,确保不会删除过多
older_than 时间阈值 只删除早于此时间的快照
max_deletes 单次删除限制 避免一次性删除过多快照
Consumer 保护 自动 正在被消费者读取的快照不会被删除
Tag 保护 自动 被打标签的快照不会被删除

6. 如何实现自定义 Action

假设我们需要实现一个 vacuum_table Action,用于清理表的所有过期数据(包括快照、分区、孤立文件)。

6.1 步骤 1:创建 Action 类

创建文件:VacuumTableAction.java

java 复制代码
package com.example.paimon.action;

import org.apache.paimon.catalog.Identifier;
import org.apache.paimon.flink.action.ActionBase;
import org.apache.paimon.flink.action.LocalAction;
import org.apache.paimon.table.Table;

import java.util.Map;

/**
 * Vacuum table action - 清理表的所有过期数据
 * 包括:过期快照、过期分区、孤立文件
 */
public class VacuumTableAction extends ActionBase implements LocalAction {

    private final String database;
    private final String table;
    private final boolean expireSnapshots;
    private final boolean expirePartitions;
    private final boolean removeOrphanFiles;
    private final Integer retainDays;

    public VacuumTableAction(
            String database,
            String table,
            Map<String, String> catalogConfig,
            boolean expireSnapshots,
            boolean expirePartitions,
            boolean removeOrphanFiles,
            Integer retainDays) {
        super(catalogConfig);
        this.database = database;
        this.table = table;
        this.expireSnapshots = expireSnapshots;
        this.expirePartitions = expirePartitions;
        this.removeOrphanFiles = removeOrphanFiles;
        this.retainDays = retainDays;
    }

    @Override
    public void executeLocally() throws Exception {
        // 1. 获取表对象
        Identifier identifier = Identifier.create(database, table);
        Table tableObj = catalog.getTable(identifier);

        System.out.println("Starting vacuum for table: " + database + "." + table);

        // 2. 过期快照
        if (expireSnapshots) {
            System.out.println("Expiring snapshots...");
            int expiredCount = tableObj.newExpireSnapshots()
                    .config(org.apache.paimon.options.ExpireConfig.builder()
                            .snapshotTimeRetain(java.time.Duration.ofDays(retainDays))
                            .build())
                    .expire();
            System.out.println("Expired " + expiredCount + " snapshots");
        }

        // 3. 过期分区(仅对分区表有效)
        if (expirePartitions && !tableObj.partitionKeys().isEmpty()) {
            System.out.println("Expiring partitions...");
            // 调用分区过期逻辑
            // tableObj.newExpirePartitions()...
        }

        // 4. 删除孤立文件
        if (removeOrphanFiles) {
            System.out.println("Removing orphan files...");
            // 调用孤立文件清理逻辑
            // tableObj.newRemoveOrphanFiles()...
        }

        System.out.println("Vacuum completed successfully");
    }
}

6.2 步骤 2:创建 ActionFactory 类

创建文件:VacuumTableActionFactory.java

java 复制代码
package com.example.paimon.action;

import org.apache.paimon.flink.action.Action;
import org.apache.paimon.flink.action.ActionFactory;
import org.apache.paimon.flink.action.MultipleParameterToolAdapter;

import java.util.Optional;

/**
 * Factory to create {@link VacuumTableAction}.
 */
public class VacuumTableActionFactory implements ActionFactory {

    public static final String IDENTIFIER = "vacuum_table";

    // 参数键定义
    private static final String EXPIRE_SNAPSHOTS = "expire_snapshots";
    private static final String EXPIRE_PARTITIONS = "expire_partitions";
    private static final String REMOVE_ORPHAN_FILES = "remove_orphan_files";
    private static final String RETAIN_DAYS = "retain_days";

    @Override
    public String identifier() {
        return IDENTIFIER;
    }

    @Override
    public Optional<Action> create(MultipleParameterToolAdapter params) {
        // 解析参数(使用默认值)
        boolean expireSnapshots = params.has(EXPIRE_SNAPSHOTS)
                ? Boolean.parseBoolean(params.get(EXPIRE_SNAPSHOTS))
                : true;  // 默认开启

        boolean expirePartitions = params.has(EXPIRE_PARTITIONS)
                ? Boolean.parseBoolean(params.get(EXPIRE_PARTITIONS))
                : true;  // 默认开启

        boolean removeOrphanFiles = params.has(REMOVE_ORPHAN_FILES)
                ? Boolean.parseBoolean(params.get(REMOVE_ORPHAN_FILES))
                : true;  // 默认开启

        Integer retainDays = params.has(RETAIN_DAYS)
                ? Integer.parseInt(params.get(RETAIN_DAYS))
                : 7;  // 默认保留 7 天

        // 创建 Action 实例
        VacuumTableAction action = new VacuumTableAction(
                params.getRequired(DATABASE),
                params.getRequired(TABLE),
                catalogConfigMap(params),
                expireSnapshots,
                expirePartitions,
                removeOrphanFiles,
                retainDays);

        return Optional.of(action);
    }

    @Override
    public void printHelp() {
        System.out.println("Action \"vacuum_table\" cleans up all expired data for a table.");
        System.out.println();
        
        System.out.println("Syntax:");
        System.out.println("  vacuum_table \\");
        System.out.println("    --warehouse <warehouse_path> \\");
        System.out.println("    --database <database> \\");
        System.out.println("    --table <table> \\");
        System.out.println("    [--expire_snapshots <true|false>] \\");
        System.out.println("    [--expire_partitions <true|false>] \\");
        System.out.println("    [--remove_orphan_files <true|false>] \\");
        System.out.println("    [--retain_days <days>]");
        System.out.println();
        
        System.out.println("Options:");
        System.out.println("  --expire_snapshots     : Whether to expire old snapshots (default: true)");
        System.out.println("  --expire_partitions    : Whether to expire old partitions (default: true)");
        System.out.println("  --remove_orphan_files  : Whether to remove orphan files (default: true)");
        System.out.println("  --retain_days          : Days to retain data (default: 7)");
        System.out.println();
        
        System.out.println("Examples:");
        System.out.println("  # Vacuum with all operations");
        System.out.println("  vacuum_table --warehouse hdfs:///warehouse --database mydb --table mytable");
        System.out.println();
        System.out.println("  # Vacuum only snapshots, retain 30 days");
        System.out.println("  vacuum_table --warehouse hdfs:///warehouse --database mydb --table mytable \\");
        System.out.println("    --expire_snapshots true --expire_partitions false --remove_orphan_files false \\");
        System.out.println("    --retain_days 30");
    }
}

6.3 步骤 3:注册到 SPI

创建文件:src/main/resources/META-INF/services/org.apache.paimon.factories.Factory

复制代码
# 自定义 Action Factory
com.example.paimon.action.VacuumTableActionFactory

如果是在 Paimon 源码中添加,需要在现有的 SPI 文件中追加:

paimon-flink/paimon-flink-common/src/main/resources/META-INF/services/org.apache.paimon.factories.Factory

复制代码
### action factories
org.apache.paimon.flink.action.CopyFilesActionFactory
...
org.apache.paimon.flink.action.ExpireSnapshotsActionFactory
com.example.paimon.action.VacuumTableActionFactory  # 添加这一行
...

6.4 步骤 4:构建和打包

6.4.1 Maven pom.xml 配置
xml 复制代码
<project>
    <modelVersion>4.0.0</modelVersion>
    
    <groupId>com.example</groupId>
    <artifactId>paimon-custom-action</artifactId>
    <version>1.0-SNAPSHOT</version>
    
    <dependencies>
        <!-- Paimon Flink Common (provided,运行时由 Flink 提供) -->
        <dependency>
            <groupId>org.apache.paimon</groupId>
            <artifactId>paimon-flink-common</artifactId>
            <version>1.4-SNAPSHOT</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>
    
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>3.2.4</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                        <configuration>
                            <transformers>
                                <!-- 合并 SPI 配置文件 -->
                                <transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
                            </transformers>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>
6.4.2 构建命令
bash 复制代码
mvn clean package

生成文件:target/paimon-custom-action-1.0-SNAPSHOT.jar

6.5 步骤 5:使用自定义 Action

6.5.1 方式 1:独立 JAR

如果自定义 Action 打包为独立 JAR,需要将其放到 Flink lib 目录:

bash 复制代码
# 1. 复制 JAR 到 Flink lib
cp target/paimon-custom-action-1.0-SNAPSHOT.jar $FLINK_HOME/lib/

# 2. 执行 Action
<FLINK_HOME>/bin/flink run \
    /path/to/paimon-flink-action.jar \
    vacuum_table \
    --warehouse hdfs:///path/to/warehouse \
    --database my_database \
    --table my_table \
    --retain_days 30

如果在 Paimon 源码中添加,重新构建 paimon-flink-action.jar

bash 复制代码
# 1. 在 Paimon 源码目录
cd paimon

# 2. 构建项目
mvn clean package -DskipTests

# 3. 找到生成的 JAR
ls paimon-flink/paimon-flink-action/target/paimon-flink-action-*.jar

# 4. 执行 Action
<FLINK_HOME>/bin/flink run \
    paimon-flink/paimon-flink-action/target/paimon-flink-action-1.4-SNAPSHOT.jar \
    vacuum_table \
    --warehouse hdfs:///warehouse \
    --database mydb \
    --table mytable
6.5.3 查看帮助信息
bash 复制代码
<FLINK_HOME>/bin/flink run \
    /path/to/paimon-flink-action.jar \
    vacuum_table \
    --help

输出:

复制代码
Action "vacuum_table" cleans up all expired data for a table.

Syntax:
  vacuum_table \
    --warehouse <warehouse_path> \
    --database <database> \
    --table <table> \
    [--expire_snapshots <true|false>] \
    [--expire_partitions <true|false>] \
    [--remove_orphan_files <true|false>] \
    [--retain_days <days>]

Options:
  --expire_snapshots     : Whether to expire old snapshots (default: true)
  --expire_partitions    : Whether to expire old partitions (default: true)
  --remove_orphan_files  : Whether to remove orphan files (default: true)
  --retain_days          : Days to retain data (default: 7)

Examples:
  # Vacuum with all operations
  vacuum_table --warehouse hdfs:///warehouse --database mydb --table mytable

  # Vacuum only snapshots, retain 30 days
  vacuum_table --warehouse hdfs:///warehouse --database mydb --table mytable \
    --expire_snapshots true --expire_partitions false --remove_orphan_files false \
    --retain_days 30

6.6 测试自定义 Action

6.6.1 单元测试

创建文件:VacuumTableActionTest.java

java 复制代码
package com.example.paimon.action;

import org.apache.paimon.catalog.Catalog;
import org.apache.paimon.catalog.CatalogFactory;
import org.apache.paimon.catalog.Identifier;
import org.apache.paimon.flink.action.ActionFactory;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

import static org.junit.jupiter.api.Assertions.*;

public class VacuumTableActionTest {

    @TempDir
    Path tempDir;

    @Test
    public void testVacuumTableActionFactory() {
        VacuumTableActionFactory factory = new VacuumTableActionFactory();
        assertEquals("vacuum_table", factory.identifier());
    }

    @Test
    public void testCreateAction() {
        String[] args = new String[]{
                "vacuum_table",
                "--warehouse", tempDir.toString(),
                "--database", "test_db",
                "--table", "test_table",
                "--retain_days", "30"
        };

        Optional<org.apache.paimon.flink.action.Action> action = 
                ActionFactory.createAction(args);
        
        assertTrue(action.isPresent());
        assertInstanceOf(VacuumTableAction.class, action.get());
    }

    @Test
    public void testExecuteVacuumAction() throws Exception {
        // 1. 创建测试 Catalog
        Map<String, String> catalogConfig = new HashMap<>();
        catalogConfig.put("warehouse", tempDir.toString());

        // 2. 创建 Action
        VacuumTableAction action = new VacuumTableAction(
                "test_db",
                "test_table",
                catalogConfig,
                true,  // expireSnapshots
                false, // expirePartitions
                false, // removeOrphanFiles
                7);    // retainDays

        // 3. 执行(需要先创建表)
        // action.executeLocally();
    }
}

7. 最佳实践和注意事项

7.1 参数设计

7.1.1 必需参数 vs 可选参数
  • 必需参数 :使用 params.getRequired(key),缺失时抛出异常
  • 可选参数 :使用 params.has(key) 检查,提供默认值
java 复制代码
// 必需参数
String database = params.getRequired(DATABASE);

// 可选参数,带默认值
Integer retainMax = params.has(RETAIN_MAX) 
    ? Integer.parseInt(params.get(RETAIN_MAX)) 
    : 10;
7.1.2 参数验证

在 Factory 的 create() 方法中进行参数验证:

java 复制代码
@Override
public Optional<Action> create(MultipleParameterToolAdapter params) {
    Integer retainMax = params.has(RETAIN_MAX) 
        ? Integer.parseInt(params.get(RETAIN_MAX)) : null;
    Integer retainMin = params.has(RETAIN_MIN) 
        ? Integer.parseInt(params.get(RETAIN_MIN)) : null;

    // 参数验证
    if (retainMax != null && retainMin != null && retainMax < retainMin) {
        throw new IllegalArgumentException(
            "retain_max (" + retainMax + ") must be >= retain_min (" + retainMin + ")");
    }

    // 创建 Action
    return Optional.of(new MyAction(...));
}

7.2 Catalog 配置

7.2.1 使用 catalogConfigMap() 获取配置
java 复制代码
Map<String, String> catalogConfig = catalogConfigMap(params);

这个方法会:

  1. 解析所有 --catalog_conf key=value 参数
  2. 自动添加 --warehouse 参数到配置中
  3. 返回完整的 Catalog 配置 Map
7.2.2 额外的 Catalog 配置

用户可以通过 --catalog_conf 传递额外配置:

bash 复制代码
vacuum_table \
    --warehouse hdfs:///warehouse \
    --database mydb \
    --table mytable \
    --catalog_conf metastore=hive \
    --catalog_conf uri=thrift://localhost:9083

7.3 LocalAction vs 普通 Action

7.3.1 选择 LocalAction

适用场景:

  • 轻量级操作,不需要分布式计算
  • 单表操作,数据量不大
  • 主要是元数据操作(如创建标签、回滚)

示例:

java 复制代码
public class MyAction extends ActionBase implements LocalAction {
    @Override
    public void executeLocally() throws Exception {
        // 直接在客户端执行
    }
}
7.3.2 选择普通 Action

适用场景:

  • 需要分布式处理大量数据
  • 需要构建 Flink 作业图(Source、Transform、Sink)
  • 涉及数据读写和计算

示例:

java 复制代码
public class CompactAction extends ActionBase {
    @Override
    public void build() throws Exception {
        // 构建 Flink 作业图
        DataStream<RowData> source = ...;
        source.transform(...).sinkTo(...);
    }
}

即使是 LocalAction,也可以强制在 Flink 作业中执行:

bash 复制代码
vacuum_table \
    --warehouse hdfs:///warehouse \
    --database mydb \
    --table mytable \
    --force_start_flink_job true

7.4 错误处理

7.4.1 在 Factory 中处理错误
java 复制代码
@Override
public Optional<Action> create(MultipleParameterToolAdapter params) {
    try {
        // 参数解析和验证
        String database = params.getRequired(DATABASE);
        Integer retainDays = Integer.parseInt(params.get(RETAIN_DAYS));
        
        return Optional.of(new MyAction(...));
    } catch (NumberFormatException e) {
        System.err.println("Invalid number format for retain_days: " + e.getMessage());
        return Optional.empty();
    } catch (Exception e) {
        System.err.println("Failed to create action: " + e.getMessage());
        return Optional.empty();
    }
}
7.4.2 在 Action 中处理错误
java 复制代码
@Override
public void executeLocally() throws Exception {
    try {
        // 执行操作
        Table table = catalog.getTable(Identifier.create(database, table));
        // ...
    } catch (Catalog.TableNotExistException e) {
        System.err.println("Table not found: " + database + "." + table);
        throw e;
    } catch (Exception e) {
        System.err.println("Execution failed: " + e.getMessage());
        throw e;
    }
}

7.5 帮助信息

提供详细的帮助信息,包括:

  • Action 的功能描述
  • 完整的语法示例
  • 每个参数的说明
  • 实际使用示例
java 复制代码
@Override
public void printHelp() {
    System.out.println("Action \"my_action\" does something useful.");
    System.out.println();
    
    System.out.println("Syntax:");
    System.out.println("  my_action \\");
    System.out.println("    --warehouse <warehouse_path> \\");
    System.out.println("    --database <database> \\");
    System.out.println("    --table <table> \\");
    System.out.println("    [--param1 <value>] \\");
    System.out.println("    [--param2 <value>]");
    System.out.println();
    
    System.out.println("Parameters:");
    System.out.println("  --warehouse  : (Required) Path to the data warehouse");
    System.out.println("  --database   : (Required) Database name");
    System.out.println("  --table      : (Required) Table name");
    System.out.println("  --param1     : (Optional) Description of param1");
    System.out.println("  --param2     : (Optional) Description of param2");
    System.out.println();
    
    System.out.println("Examples:");
    System.out.println("  # Basic usage");
    System.out.println("  my_action --warehouse /path/to/warehouse --database db --table tbl");
    System.out.println();
    System.out.println("  # With optional parameters");
    System.out.println("  my_action --warehouse /path/to/warehouse --database db --table tbl \\");
    System.out.println("    --param1 value1 --param2 value2");
}

7.6 序列化

7.6.1 Action 必须可序列化

如果 LocalAction 使用强制 Flink 作业模式,Action 对象会被序列化发送到 TaskManager:

java 复制代码
public class MyAction extends ActionBase implements LocalAction, Serializable {
    // 所有字段必须可序列化
    private final String database;  // OK
    private final Integer retainDays;  // OK
    
    // 不可序列化的字段必须标记为 transient
    private transient Catalog catalog;  // OK,由 ActionBase 管理
}
7.6.2 使用 transient 字段

对于不可序列化的字段(如 Catalog、FileIO),标记为 transient 并在运行时重新初始化:

java 复制代码
public class MyAction extends ActionBase implements LocalAction {
    private transient MyHelper helper;
    
    @Override
    public void executeLocally() throws Exception {
        // 在执行时初始化
        if (helper == null) {
            helper = new MyHelper(catalog);
        }
        helper.doSomething();
    }
}

7.7 日志记录

使用 SLF4J 记录关键操作:

java 复制代码
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class MyAction extends ActionBase implements LocalAction {
    private static final Logger LOG = LoggerFactory.getLogger(MyAction.class);
    
    @Override
    public void executeLocally() throws Exception {
        LOG.info("Starting my action for table: {}.{}", database, table);
        
        try {
            // 执行操作
            int result = doSomething();
            LOG.info("Action completed successfully, result: {}", result);
        } catch (Exception e) {
            LOG.error("Action failed", e);
            throw e;
        }
    }
}

7.8 性能优化

7.8.1 避免重复加载

缓存重复使用的对象:

java 复制代码
private transient Table tableCache;

private Table getTable() throws Exception {
    if (tableCache == null) {
        tableCache = catalog.getTable(Identifier.create(database, table));
    }
    return tableCache;
}
7.8.2 批量操作

对于需要处理多个表的 Action,使用批量 API:

java 复制代码
// 不好:逐个处理
for (String tableName : tables) {
    Table table = catalog.getTable(Identifier.create(database, tableName));
    processTable(table);
}

// 更好:批量加载
List<Identifier> identifiers = tables.stream()
    .map(t -> Identifier.create(database, t))
    .collect(Collectors.toList());
List<Table> tables = catalog.getTables(identifiers);
tables.forEach(this::processTable);

7.9 兼容性

7.9.1 向后兼容

添加新参数时,提供默认值以保持向后兼容:

java 复制代码
// 新增参数
Integer newParam = params.has(NEW_PARAM) 
    ? Integer.parseInt(params.get(NEW_PARAM)) 
    : DEFAULT_VALUE;  // 默认值确保向后兼容
7.9.2 废弃参数

如果需要废弃某个参数,先标记为 deprecated:

java 复制代码
@Deprecated
private static final String OLD_PARAM = "old_param";
private static final String NEW_PARAM = "new_param";

@Override
public Optional<Action> create(MultipleParameterToolAdapter params) {
    String value;
    if (params.has(NEW_PARAM)) {
        value = params.get(NEW_PARAM);
    } else if (params.has(OLD_PARAM)) {
        System.err.println("Warning: --old_param is deprecated, use --new_param instead");
        value = params.get(OLD_PARAM);
    } else {
        value = DEFAULT_VALUE;
    }
    // ...
}

8. 总结

8.1 Paimon Action Jar 的核心设计

Paimon Action Jar 通过以下机制实现了灵活、可扩展的表维护操作框架:

8.1.1 模块隔离
  • 独立的入口模块paimon-flink-action 只包含入口类,避免类加载冲突
  • 实现模块分离 :所有实现都在 paimon-flink-common
8.1.2 SPI 扩展机制
  • 基于 Java SPI 的插件化架构
  • 通过 META-INF/services 文件注册 Factory
  • FactoryUtil.discoverFactory() 动态加载实现
8.1.3 分层设计

清晰的职责分层:

  1. Factory 层:参数解析、验证、Action 创建

  2. Action 层:执行调度、模式选择(本地 vs Flink 作业)

  3. Procedure 层:业务逻辑封装(复用 Flink SQL CALL)

  4. Core 层:核心实现(如 ExpireSnapshotsImpl)

    FlinkActions.main()

    ActionFactory.createAction()
    ↓ (SPI 加载)
    ExpireSnapshotsActionFactory.create()

    ExpireSnapshotsAction.run()
    ↓ (LocalAction)
    ExpireSnapshotsAction.executeLocally()

    ExpireSnapshotsProcedure.call()

    ExpireSnapshotsImpl.expire()

8.1.4 灵活的执行模式
  • LocalAction:轻量操作本地执行,快速高效
  • 普通 Action:构建 Flink 作业,分布式处理
  • 强制模式:LocalAction 也可强制使用 Flink 作业
8.1.5 统一的接口规范

所有 Action 遵循统一接口:

  • Action.run() - 执行入口
  • Action.build() - 构建作业图(可选)
  • LocalAction.executeLocally() - 本地执行(可选)

8.2 实现自定义 Action 的关键点

  1. 继承正确的基类

    • 简单操作:extends ActionBase implements LocalAction
    • 复杂作业:extends ActionBase
  2. 实现 Factory

    • 定义唯一的 identifier
    • 解析和验证参数
    • 提供详细的帮助信息
  3. 注册到 SPI

    • META-INF/services/org.apache.paimon.factories.Factory 中注册
  4. 处理好序列化

    • Action 类必须实现 Serializable
    • 不可序列化的字段标记为 transient
  5. 错误处理和日志

    • 提供清晰的错误信息
    • 记录关键操作日志

8.3 ExpireSnapshotsAction 的实现要点

8.3.1 多层保护机制
  • retain_min:确保最少保留数量
  • retain_max:限制最多保留数量
  • older_than:时间条件过滤
  • max_deletes:单次删除限制
  • Consumer 保护:自动检测消费者
  • Tag 保护:保护被标记的快照
8.3.2 分阶段删除
  1. 删除数据文件(合并树文件)
  2. 删除 Manifest 文件
  3. 删除快照文件本身
  4. 更新 earliest hint
8.3.3 性能优化
  • 提前退出:满足时间条件时立即停止
  • 批量操作:避免逐个文件删除
  • 异步模式:支持异步过期(避免反压)

8.4 适用场景

Action 类型 适用场景 示例
LocalAction 轻量维护操作 expire_snapshots, rollback_to, create_tag
普通 Action 分布式计算 compact, merge_into, clone
混合模式 可选执行方式 使用 --force_start_flink_job 切换

8.5 扩展建议

基于 Paimon Action 框架,可以扩展实现:

  1. 数据质量检查 Action:检查表数据的完整性和一致性
  2. 数据备份 Action:备份表的快照到外部存储
  3. 数据迁移 Action:在不同 Catalog 之间迁移表
  4. 统计信息收集 Action:收集表的统计信息用于查询优化
  5. 数据采样 Action:从大表中采样数据用于分析

8.6 参考资源

  • 源码位置

    • 入口:paimon-flink/paimon-flink-action/src/main/java/org/apache/paimon/flink/action/FlinkActions.java
    • Action 实现:paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/action/
    • SPI 配置:paimon-flink/paimon-flink-common/src/main/resources/META-INF/services/
  • 官方文档

  • 相关 Procedure

    • Action 和 Procedure 共享相同的业务逻辑
    • Procedure 用于 Flink SQL CALL 语句
    • Action 用于命令行 flink run 执行

附录

A. 完整的命令行示例

A.1 过期快照
bash 复制代码
# 基本用法:保留最近 10 个快照
<FLINK_HOME>/bin/flink run \
    /path/to/paimon-flink-action.jar \
    expire_snapshots \
    --warehouse hdfs:///warehouse \
    --database my_database \
    --table my_table \
    --retain_max 10

# 高级用法:组合多个条件
<FLINK_HOME>/bin/flink run \
    /path/to/paimon-flink-action.jar \
    expire_snapshots \
    --warehouse hdfs:///warehouse \
    --database my_database \
    --table my_table \
    --retain_max 100 \
    --retain_min 10 \
    --older_than '2024-01-01 00:00:00' \
    --max_deletes 50
A.2 表压缩
bash 复制代码
# 压缩整个表
<FLINK_HOME>/bin/flink run \
    /path/to/paimon-flink-action.jar \
    compact \
    --warehouse hdfs:///warehouse \
    --database my_database \
    --table my_table

# 压缩指定分区
<FLINK_HOME>/bin/flink run \
    /path/to/paimon-flink-action.jar \
    compact \
    --warehouse hdfs:///warehouse \
    --database my_database \
    --table my_table \
    --partition dt=2024-01-01 \
    --partition dt=2024-01-02
A.3 删除孤立文件
bash 复制代码
<FLINK_HOME>/bin/flink run \
    /path/to/paimon-flink-action.jar \
    remove_orphan_files \
    --warehouse hdfs:///warehouse \
    --database my_database \
    --table my_table \
    --older_than '2024-01-01 00:00:00'
A.4 查看所有可用 Action
bash 复制代码
<FLINK_HOME>/bin/flink run \
    /path/to/paimon-flink-action.jar

输出:

复制代码
Usage: <action> [OPTIONS]

Available actions:
  compact
  compact_database
  copy_files
  create_branch
  create_tag
  create_tag_from_timestamp
  create_tag_from_watermark
  delete_branch
  delete_tag
  drop_partition
  expire_changelogs
  expire_partitions
  expire_snapshots
  expire_tags
  fast_forward
  mark_partition_done
  merge_into
  migrate_database
  migrate_table
  remove_orphan_files
  repair
  replace_tag
  reset_consumer
  rewrite_file_index
  rollback_to
  rollback_to_timestamp
  ...

For detailed options of each action, run <action> --help

B. 常见问题

B.1 ClassNotFoundException

问题 :执行 Action 时报 ClassNotFoundException

原因:自定义 Action 的 JAR 没有放到 Flink lib 目录

解决方案

bash 复制代码
cp my-custom-action.jar $FLINK_HOME/lib/
B.2 SPI 未生效

问题:自定义 Action 未被识别

原因:SPI 配置文件路径或格式错误

检查

  1. 文件路径:src/main/resources/META-INF/services/org.apache.paimon.factories.Factory
  2. 文件内容:完整的类名,每行一个
  3. Maven 配置:使用 ServicesResourceTransformer 合并 SPI 文件
B.3 参数解析错误

问题:参数传递后无效

原因:参数名称错误或格式不正确

检查

  1. 参数名使用下划线(retain_max),不是驼峰(retainMax
  2. 参数值格式正确(数字、时间戳等)
  3. 使用 --help 查看正确的参数名称

文档版本 :1.0
最后更新 :2025-01-30
基于 Paimon 版本:1.4-SNAPSHOT

Paimon Action Jar 实现机制分析

相关推荐
Simon_lca2 小时前
迈向绿色未来:全球手机品牌ESG实践深度剖析——聚焦供应链减排与零碳转型
大数据·人工智能·经验分享·智能手机·分类·制造
Sui_Network2 小时前
Sui 2025 年终回顾:Sui 技术栈篇
大数据·人工智能·科技·去中心化·区块链
浔川python社2 小时前
国内两大短视频平台遭黑灰产攻击事件
大数据·人工智能
only-qi2 小时前
深入理解MySQL中的MVCC:多版本并发控制的实现原理
java·数据库·mysql
大任视点2 小时前
米悦MIY:以科技赋能健康生活,打造高端生活家电新标杆
大数据·人工智能
G皮T2 小时前
【Elasticsearch】查询性能调优(六):track_total_hits 影响返回结果的相关性排序吗
大数据·数据库·elasticsearch·搜索引擎·全文检索·性能·opensearch
ZePingPingZe2 小时前
静态代理、JDK和Cglib动态代理、回调
java·开发语言
万粉变现经纪人2 小时前
如何解决 pip install 代理报错 SOCKS5 握手失败 ReadTimeoutError 问题
java·python·pycharm·beautifulsoup·bug·pandas·pip
风月歌2 小时前
2025-2026计算机毕业设计选题指导,java|springboot|ssm项目成品推荐
java·python·小程序·毕业设计·php·源码