日志服务Flink Connector《支持Exactly Once》

介绍如何利用Flink与阿里云日志服务进行高效数据处理,包括数据消费与生产的配置方法,以及如何实现exactly-once语义。

阿里云日志服务是针对实时数据一站式服务,用户只需要将精力集中在分析上,过程中数据采集、对接各种存储计算、数据索引和查询等琐碎工作等都可以交给日志服务完成。

日志服务中最基础的功能是LogHub,支持数据实时采集消费,实时消费家族除 Spark Streaming、Storm、StreamCompute(Blink外),目前新增Flink啦。

image

Flink Connector

Flink log connector是阿里云日志服务提供的,用于对接flink的工具,包括两部分,消费者(Consumer)和生产者(Producer)。

消费者用于从日志服务中读取数据,支持exactly once语义,支持shard负载均衡.

生产者用于将数据写入日志服务,使用connector时,需要在项目中添加maven依赖:


代码:Github

用法

  1. 请参考日志服务文档,正确创建Logstore。
  2. 如果使用子账号访问,请确认正确设置了LogStore的RAM策略。参考授权RAM子用户访问日志服务资源

1. Log Consumer

在Connector中, 类FlinkLogConsumer提供了订阅日志服务中某一个LogStore的能力,实现了exactly once语义,在使用时,用户无需关心LogStore中shard数
量的变化,consumer会自动感知。

flink中每一个子任务负责消费LogStore中部分shard,如果LogStore中shard发生split或者merge,子任务消费的shard也会随之改变。

1.1 配置启动参数

上面是一个简单的消费示例,我们使用java.util.Properties作为配置工具,所有Consumer的配置都可以在ConfigConstants中找到。

注意,flink stream的子任务数量和日志服务LogStore中的shard数量是独立的,如果shard数量多于子任务数量,每个子任务不重复的消费多个shard,如果少于,

那么部分子任务就会空闲,等到新的shard产生。

1.2 设置消费起始位置

Flink log consumer支持设置shard的消费起始位置,通过设置属性ConfigConstants.LOG_CONSUMER_BEGIN_POSITION,就可以定制消费从shard的头尾或者某个特定时间开始消费,具体取值如下:

  • Consts.LOG_BEGIN_CURSOR: 表示从shard的头开始消费,也就是从shard中最旧的数据开始消费。
  • Consts.LOG_END_CURSOR: 表示从shard的尾开始,也就是从shard中最新的数据开始消费。
  • UnixTimestamp: 一个整型数值的字符串,用1970-01-01到现在的秒数表示, 含义是消费shard中这个时间点之后的数据。

三种取值举例如下:


1.3 监控:消费进度(可选)

Flink log consumer支持设置消费进度监控,所谓消费进度就是获取每一个shard实时的消费位置,这个位置使用时间戳表示,详细概念可以参考
文档消费组-查看状态,[消费组-监控报警
](https://help.aliyun.com/document_detail/55912.html)




注意上面代码是可选的,如果设置了,consumer会首先创建consumerGroup,如果已经存在,则什么都不做,consumer中的snapshot会自动同步到日志服务的consumerGroup中,用户可以在日志服务的控制台查看consumer的消费进度。

1.4 容灾和exactly once语义支持

当打开Flink的checkpointing功能时,Flink log consumer会周期性的将每个shard的消费进度保存起来,当作业失败时,flink会恢复log consumer,并
从保存的最新的checkpoint开始消费。

写checkpoint的周期定义了当发生失败时,最多多少的数据会被回溯,也就是重新消费,使用代码如下:


注意上面代码是可选的,如果设置了,consumer会首先创建consumerGroup,如果已经存在,则什么都不做,consumer中的snapshot会自动同步到日志服务的consumerGroup中,用户可以在日志服务的控制台查看consumer的消费进度。

1.4 容灾和exactly once语义支持

当打开Flink的checkpointing功能时,Flink log consumer会周期性的将每个shard的消费进度保存起来,当作业失败时,flink会恢复log consumer,并
从保存的最新的checkpoint开始消费。

写checkpoint的周期定义了当发生失败时,最多多少的数据会被回溯,也就是重新消费,使用代码如下:


更多Flink checkpoint的细节请参考Flink官方文档Checkpoints

1.5 补充材料:关联 API与权限设置

Flink log consumer 会用到的阿里云日志服务接口如下:

  • GetCursorOrData

    用于从shard中拉数据, 注意频繁的调用该接口可能会导致数据超过日志服务的shard quota, 可以通过ConfigConstants.LOG_FETCH_DATA_INTERVAL_MILLIS和ConfigConstants.LOG_MAX_NUMBER_PER_FETCH
    控制接口调用的时间间隔和每次调用拉取的日志数量,shard的quota参考文章[shard简介](https://help.aliyun.com/document_detail/28976.html).

注意上面代码是可选的,如果设置了,consumer会首先创建consumerGroup,如果已经存在,则什么都不做,consumer中的snapshot会自动同步到日志服务的consumerGroup中,用户可以在日志服务的控制台查看consumer的消费进度。

1.4 容灾和exactly once语义支持

当打开Flink的checkpointing功能时,Flink log consumer会周期性的将每个shard的消费进度保存起来,当作业失败时,flink会恢复log consumer,并
从保存的最新的checkpoint开始消费。

写checkpoint的周期定义了当发生失败时,最多多少的数据会被回溯,也就是重新消费,使用代码如下:


  • ListShards

     用于获取logStore中所有的shard列表,获取shard状态等.如果您的shard经常发生分裂合并,可以通过调整接口的调用周期来及时发现shard的变化。
    // 设置每30s调用一次ListShards
    configProps.put(ConfigConstants.LOG_SHARDS_DISCOVERY_INTERVAL_MILLIS, "30000");
  • CreateConsumerGroup

    该接口调用只有当设置消费进度监控时才会发生,功能是创建consumerGroup,用于同步checkpoint。
  • ConsumerGroupUpdateCheckPoint

    该接口用户将flink的snapshot同步到日志服务的consumerGroup中。
    

子用户使用Flink log consumer需要授权如下几个RAM Policy:


注意上面代码是可选的,如果设置了,consumer会首先创建consumerGroup,如果已经存在,则什么都不做,consumer中的snapshot会自动同步到日志服务的consumerGroup中,用户可以在日志服务的控制台查看consumer的消费进度。

1.4 容灾和exactly once语义支持

当打开Flink的checkpointing功能时,Flink log consumer会周期性的将每个shard的消费进度保存起来,当作业失败时,flink会恢复log consumer,并
从保存的最新的checkpoint开始消费。

写checkpoint的周期定义了当发生失败时,最多多少的数据会被回溯,也就是重新消费,使用代码如下:


2. Log Producer

FlinkLogProducer 用于将数据写到阿里云日志服务中。

注意producer只支持Flink at-least-once语义,这就意味着在发生作业失败的情况下,写入日志服务中的数据有可能会重复,但是绝对不会丢失。

用法示例如下,我们将模拟产生的字符串写入日志服务:

// 将数据序列化成日志服务的数据格式
class SimpleLogSerializer implements LogSerializationSchema<String> {

    public RawLogGroup serialize(String element) {
        RawLogGroup rlg = new RawLogGroup();
        RawLog rl = new RawLog();
        rl.setTime((int)(System.currentTimeMillis() / 1000));
        rl.addContent("message", element);
        rlg.addLog(rl);
        return rlg;
    }
}
public class ProducerSample {
    public static String sEndpoint = "cn-hangzhou.log.aliyuncs.com";
    public static String sAccessKeyId = "";
    public static String sAccessKey = "";
    public static String sProject = "ali-cn-hangzhou-sls-admin";
    public static String sLogstore = "test-flink-producer";
    private static final Logger LOG = LoggerFactory.getLogger(ConsumerSample.class);


    public static void main(String[] args) throws Exception {

        final ParameterTool params = ParameterTool.fromArgs(args);
        final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.getConfig().setGlobalJobParameters(params);
        env.setParallelism(3);

        DataStream<String> simpleStringStream = env.addSource(new EventsGenerator());

        Properties configProps = new Properties();
        // 设置访问日志服务的域名
        configProps.put(ConfigConstants.LOG_ENDPOINT, sEndpoint);
        // 设置访问日志服务的ak
        configProps.put(ConfigConstants.LOG_ACCESSSKEYID, sAccessKeyId);
        configProps.put(ConfigConstants.LOG_ACCESSKEY, sAccessKey);
        // 设置日志写入的日志服务project
        configProps.put(ConfigConstants.LOG_PROJECT, sProject);
        // 设置日志写入的日志服务logStore
        configProps.put(ConfigConstants.LOG_LOGSTORE, sLogstore);

        FlinkLogProducer<String> logProducer = new FlinkLogProducer<String>(new SimpleLogSerializer(), configProps);

        simpleStringStream.addSink(logProducer);

        env.execute("flink log producer");
    }
    // 模拟产生日志
    public static class EventsGenerator implements SourceFunction<String> {
        private boolean running = true;

        @Override
        public void run(SourceContext<String> ctx) throws Exception {
            long seq = 0;
            while (running) {
                Thread.sleep(10);
                ctx.collect((seq++) + "-" + RandomStringUtils.randomAlphabetic(12));
            }
        }

        @Override
        public void cancel() {
            running = false;
        }
    }
}
2.1 初始化

Producer初始化主要需要做两件事情:

  • 初始化配置参数Properties, 这一步和Consumer类似, Producer有一些定制的参数,一般情况下使用默认值即可,特殊场景可以考虑定制:

    // 用于发送数据的io线程的数量,默认是8
    ConfigConstants.LOG_SENDER_IO_THREAD_COUNT
    // 该值定义日志数据被缓存发送的时间,默认是3000
    ConfigConstants.LOG_PACKAGE_TIMEOUT_MILLIS
    // 缓存发送的包中日志的数量,默认是4096
    ConfigConstants.LOG_LOGS_COUNT_PER_PACKAGE
    // 缓存发送的包的大小,默认是3Mb
    ConfigConstants.LOG_LOGS_BYTES_PER_PACKAGE
    // 作业可以使用的内存总的大小,默认是100Mb
    ConfigConstants.LOG_MEM_POOL_BYTES
    上述参数不是必选参数,用户可以不设置,直接使用默认值。
  • 重载LogSerializationSchema,定义将数据序列化成RawLogGroup的方法。

    RawLogGroup是log的集合,每个字段的含义可以参考文档[日志数据模型](https://help.aliyun.com/document_detail/29054.html)。
    

如果用户需要使用日志服务的shardHashKey功能,指定数据写到某一个shard中,可以使用LogPartitioner产生数据的hashKey,用法例子如下:

FlinkLogProducer<String> logProducer = new FlinkLogProducer<String>(new SimpleLogSerializer(), configProps);
logProducer.setCustomPartitioner(new LogPartitioner<String>() {
            // 生成32位hash值
            public String getHashKey(String element) {
                try {
                    MessageDigest md = MessageDigest.getInstance("MD5");
                    md.update(element.getBytes());
                    String hash = new BigInteger(1, md.digest()).toString(16);
                    while(hash.length() < 32) hash = "0" + hash;
                    return hash;
                } catch (NoSuchAlgorithmException e) {
                }
                return  "0000000000000000000000000000000000000000000000000000000000000000";
            }
        });

注意LogPartitioner是可选的,不设置情况下, 数据会随机写入某一个shard。

2.2 权限设置:RAM Policy

Producer依赖日志服务的API写数据,如下:

  • log:PostLogStoreLogs
  • log:ListShards

当RAM子用户使用Producer时,需要对上述两个API进行授权:

接口 资源
log:PostLogStoreLogs acs:log: regionName: regionName:{projectOwnerAliUid}:project/ projectName/logstore/ projectName/logstore/{logstoreName}
log:ListShards acs:log: regionName: regionName:{projectOwnerAliUid}:project/ projectName/logstore/ projectName/logstore/{logstoreName}

注意上面代码是可选的,如果设置了,consumer会首先创建consumerGroup,如果已经存在,则什么都不做,consumer中的snapshot会自动同步到日志服务的consumerGroup中,用户可以在日志服务的控制台查看consumer的消费进度。

1.4 容灾和exactly once语义支持

当打开Flink的checkpointing功能时,Flink log consumer会周期性的将每个shard的消费进度保存起来,当作业失败时,flink会恢复log consumer,并
从保存的最新的checkpoint开始消费。

写checkpoint的周期定义了当发生失败时,最多多少的数据会被回溯,也就是重新消费,使用代码如下:

<think>我们根据提供的引用内容来回答用户关于Flink与Paimon集成时如何保证exactly-once语义的问题。引用[1]提到Paimon读取外部数据库的CDC数据是通过FlinkCDC实现的,同时Paimon表自身也可以产生CDC数据。这说明了Paimon与FlinkCDC的紧密集成。引用[2]解释了Flinkexactly-once机制依赖于有状态流处理,即Flink通过状态管理和检查点(checkpoint)机制来保证exactly-once语义。引用[3]则明确指出,FlinkCDCsink到Paimon时暂不支持exactly-once写入,而是通过幂等写来实现。幂等写可以多次执行而不改变结果,从而在重播时不会产生重复数据。因此,我们需要明确两点:1.Flink自身通过检查点机制和状态持久化来保证exactly-once语义,但这需要sink端也支持。2.当前Paimon作为sink时,对于FlinkCDC的数据写入,官方文档(引用[3])说明暂不支持exactly-once,而是采用幂等写来达到类似效果。那么,Paimon表自身是否支持exactly-once呢?实际上,Paimon的设计中,其表的数据写入是通过Flinkconnector进行的,而该connector利用Flink的checkpoint机制实现了exactly-once语义。然而,根据引用[3]的信息,在FlinkCDC数据写入Paimon这个特定场景下,目前不支持exactly-once,而是通过幂等写来保证一致性。这里需要区分两个过程:-过程A:将外部数据(如MySQLCDC数据)通过FlinkCDC导入到Paimon表。-过程B:在Flink作业中,对Paimon表进行读写(包括插入、更新)操作。对于过程A(FlinkCDC数据写入Paimon表),引用[3]明确说明暂不支持exactly-once,而是通过幂等写来保证。这是因为Paimon在处理CDC数据时,设计上采用了主键唯一约束来实现幂等,即相同主键的多次写入不会产生重复。对于过程B(Flink作业直接读写Paimon表),Paimon的Flinkconnector支持exactly-once语义。这依赖于Flink的检查点机制。在Flink作业进行checkpoint时,Paimon会提交事务(commit),确保数据一次性写入。同时,在失败恢复时,Flink会回滚到上次成功的checkpoint,然后重新处理,但由于Paimon的表具有事务能力(基于snapshot),可以保证数据的一致性。总结:1.当使用Flink将CDC数据(如MySQLCDC)写入Paimon时,暂不支持exactly-once,而是采用幂等写来保证一致性(见引用[3])。这是因为写入过程中涉及的主键操作天然支持幂等,多次重放相同主键的数据不会导致重复。2.而在Flink作业中直接读写Paimon表(非CDC数据导入)时,Paimonconnector支持Flinkexactly-once语义,这是通过Flink的检查点机制和Paimon的事务机制共同保证的。需要注意的是,官方文档和社区的发展可能会改变这一现状,建议随时关注最新版本特性。因此,用户的问题“FlinkPaimonexactlyonce语义实现机制”需要根据具体使用场景来区分:-如果是Flink读写Paimon表(过程B),那么是通过Flink的检查点机制和Paimon的事务机制实现exactly-once语义。-如果是将FlinkCDC数据写入Paimon(过程A),则暂不支持exactly-once,而是采用幂等写(依赖主键)来实现一致性。引用[3]明确说明:“FlinkCDCsinkpaimon暂不支持exactly-once写入,而通过幂等写”,所以这个过程目前是不支持exactly-once的。最后,生成相关问题供用户深入探讨。</think> ### Flink 与 Paimon 集成时的 Exactly-Once 语义实现机制 #### **核心机制分析** 1. **Paimon 对 Flink CDC 数据的处理** Paimon 读取外部数据库(如 MySQL)的 CDC 数据是通过 Flink CDC 实现的[^1]。在这一过程中,Flink CDC 提供数据流的 **Exactly-Once 读取**,确保数据源到 Flink 作业的精确一次消费[^2]。 2. **Paimon 作为 Sink 时的写入机制** - **暂不支持 Exactly-Once 写入**: 当前版本中,Flink CDC 数据写入 Paimon 表时,**不支持原生 Exactly-Once 语义**[^3]。这是因为 Paimon 的写入机制尚未完全集成 Flink 的 `TwoPhaseCommitSink`(两阶段提交)。 - **幂等写入替代方案**: Paimon 采用 **主键约束的幂等写入** 保证一致性[^3]。当数据因故障重放时: - 若主键不存在则插入 - 若主键已存在则覆盖更新 例如,对主键为 `id` 的表执行 `INSERT INTO paimon_table VALUES (1, 'A')` 后重试,不会产生重复数据,仅保留最新值。 3. **Paimon 自身表的 CDC 数据生成** Paimon 表可独立产生 CDC 数据流(如快照变更记录),这类操作通过 **Flink 状态管理** 保证 Exactly-Once: - Flink 的检查点机制确保状态一致性(如窗口聚合结果)[^2]。 - Paimon 利用 **事务性快照** 隔离写入,避免中间状态暴露。 --- #### **Exactly-Once 实现的核心依赖** | **能力** | 支持情况 | 实现原理 | |------------------------|---------------------------|------------------------------------------------------------------------| | **Flink CDC 读取** | ✔️ 支持 Exactly-Once | 基于 Flink 检查点机制 + 日志偏移量回滚[^2] | | **Paimon CDC 数据写入** | ❌ 不支持原生 Exactly-Once | 幂等写(主键覆盖)[^3] | | **Paimon 表状态更新** | ✔️ 支持 Exactly-Once | Flink 状态检查点 + Paimon 快照事务 | --- #### **典型场景示例** ```mermaid graph LR A[MySQL CDC] --> B(Flink Job) B --> C{Paimon Sink} C -->|幂等写| D[(Paimon Table)] D -->|CDC 日志| E[Downstream System] ``` 1. **数据写入流程**: MySQL 的 CDC 数据通过 Flink 写入 Paimon 时: - Flink 作业故障恢复后,因重放导致数据重复写入 Paimon。 - 通过主键幂等写,Paimon 仅保留最新数据,**结果层面实现 Exactly-Once**。 2. **下游消费流程**: Paimon 表产生的 CDC 日志(如 `INSERT/UPDATE` 记录)被下游消费时: - 通过 Flink 状态管理保证处理逻辑的 Exactly-Once[^2]。 --- #### **限制与优化方向** 1. **当前限制**: - 幂等写依赖主键:无主键表无法保证 Exactly-Once。 - 写入性能:主键覆盖操作比追加写成本更高。 2. **未来优化**: - 集成 `TwoPhaseCommitSink` 实现原生 Exactly-Once 写入。 - 动态主键分区优化,减少覆盖写开销[^3]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值