Flink进阶(四):Flink中的Exactly Once

Flink内部Exactly Once

通过checkpoint和状态来保证
checkpoint流程:
Flink在数据流中加入了一个叫barrier的东西(中文名栅栏),barrier在SourceTask处生成,一致到SinkTask,期间所有的Task只要碰到barrier就会触发自身进行快照。barrier的作用就是为了把数据区分开,barrier之前的数据是本次checkpoint之前必须处理完的,barrier之后的数据是本次checkpoint之前不能处理的数据;SourceTask会把数据和barrier一起往下游发送,如果下游某个Task收到barrier,意味着barrier之前的数据已经被处理完了,此时会暂停处理barrier之后的数据,他会做快照。
何为快照?
在checkpoint过程中有一个同步做快照的过程,快照就是把当前的状态信息保存到磁盘,在快照期间是暂停处理barrier之后的数据的,因为如果快照期间Task还在处理数据,可能会导致状态信息还没保存到磁盘,状态就已经变化了,此时写入到磁盘的状态信息就不对了,下次从此处checkpoint重启时,就会发生重复消费。
同步快照的过程不能处理barrier之后的数据,但是为了高可用需要将快照信息上传到HDFS,这个过程是异步的,因为此时处理barrier之后的数据不会影响磁盘上的快照信息。

单并行度下的checkpoint过程
  1. jobmanager向SourceTaske发送checkpoint,SourceTask会在数据流中安插barrier,安插好之后会把barrier和数据一起发到下游,然后自身做快照,并将快照信息(比如kafka的offset信息(0,100))发送到HDFS上。
  2. 下游的PV task收到barrier后,也会做快照,把快照信息(比如此时统计的PV值(app1,100))发送到HDFS上。
  3. 此时的checkpoint就保存了offset100处PV值的统计值为100;下次从这个checkpoint处重启时,获取到(0,100)和(app1,100),再次统计,确保Exactly Once
多并行度下的checkpoint过程
  1. 所有的Operator运行过程中接收到上游算子发送的barrier后,对自身状态进行一次快照,保存到HDFS上
  2. barrier对齐问题: 当一个Operator收到上游两个SourceTask的barrier时,由于不能保证两个barrier同时到达,那Operator实例到底什么时候进行快照呢?答案是:做快照前需要等待barrier对齐,就是等待所有输入流的barrier都到达。
  3. Operator在收到某个输入流的barrier时,就不会处理来自该流的任何数据了,而是把该流的数据放到缓冲区,等待其他流的barrier达到;一旦所有流的barrier都达到后,Operator实例就会把已经处理完成的数据和n个barrier一起发到下游,然后对状态信息做快照,做完快照会先处理缓冲去数据,就可以正常处理输入流的数据了;为了加快下游的checkpoint,会先发送barrier到下游再做快照。
  4. 如果checkpoint持续时长超过设定的超时时间,会把这次缠身的所有状态数据删除。
  5. 如果barrier不对齐的话:先到达的数据流会继续处理数据,等到所有barrier达到后,此时Operator记录的状态信息就和先到达数据流的SourceTask中的offset信息不一致,会多处理一些数据,而多处理的这些数据就是等待其他barrier到达时间内的数据。

所以实现barrier对齐就可以实现Exactly Once,如果barrier不对齐就是At Least Once。
实现barrier对齐是要付出代价的。
Flink Web UI 的 Checkpoint 选项卡中可以看到 barrier 对齐的耗时,如果发现耗时比较长,且对 Exactly Once 语义要求不高时,可以考虑使用该优化方案。

端对端的Exactly Once

  1. 幂等性写入,依赖外部存储介质实现去重,比如HBase,Redis;
    做PV统计时,借助Flink内部的状态去统计,不借助外部存储介质,外部介质承担的角色仅仅是提供数据给业务方查询,所以无论下游使用什么形式的 Sink,只要 Sink 端能够按照主键去重,该统计方案就可以保证 Exactly Once。
  2. TwoPhaseCommitSinkFunction
    对于下游有事务的sink,我们需要把checkpoint和写入外部存储介质做强关联,两次checkpoint之间不允许向外部存储介质提交数据,Checkpoint 的时候再向外部存储提交。如果提交成功,则 Checkpoint 成功,提交失败,则 Checkpoint 也失败。这样在下一次 Checkpoint 之前,如果任务失败,也没有重复数据被提交到外部存储。基于这个思想,Flink实现了TwoPhaseCommitSinkFunction。它是基于2PC一致性协议实现的
    2pc一致性协议:
    1)协调者向参与者发出Vote Request,事务预处理请求,如果所有的参与者都响应了Vote Commit,就进入第二阶段
    2)收到所有参与者的Vote Commit,协调者会向所有参与者发出global_commit,然后所有参与者提交本地事务,并返回ack,收到所有ack后,协调者确认所有事务都提交完毕;
    只要有一个参与在第一阶段回复Vote_Abort,协调者对所有参与者发出global_rollback,参与者回滚事务并返回ack.
    大致流程:
    1)所有并行度初始化,会调用开启事务的方法
    2)调用invoke()方法处理数据
    3)一段时间后做checkpoint,在调用同步快照方法snapshotState()中调用preCommit()方法
    4)snapshotState()方法执行完成后,当所有实例都备份完成后就表示ck成功,jobmanager通知ck完成,各实例会调用notifyCheckpointComplete()方法中调用commit()方法
    5)期间如果出现其中某个并行度出现故障,JobManager 会停止此任务,向所有的实例发送通知,各实例收到通知后,调用 close 方法。
以写入Mysql为例,实现TwoPhaseCommitSinkFunction

直接上代码
TwoPhaseCommitSinkFunction实现了CheckpointedFunction 和 CheckpointListener 接口

@Slf4j
public class MysqlSink extends TwoPhaseCommitSinkFunction<User, Connection, Void> {

    public MysqlSink(){
        super(new KryoSerializer<>(Connection.class,new ExecutionConfig()), VoidSerializer.INSTANCE);

    }

    /**
     * 处理数据:
     * 开启新的事务后,Flink 开始处理数据,每来一条数据都会调用 invoke 方法,按照业务逻辑将数据添加到本次的事务中
     * @param connection
     * @param user
     * @param context
     * @throws Exception
     */
    @Override
    protected void invoke(Connection connection, User user, Context context) throws Exception {
        String sql = "";
        PreparedStatement pst = connection.prepareStatement(sql);
        pst.setString(1,"");
        pst.setString(2,"");
        pst.execute();
    }


    /**
     * 开启一个事务:
     * 状态初始化的 initializeState 方法内或者
     * 每次 Checkpoint 的 snapshotState 方法内都会调用 beginTransaction 方法开启新的事务
     *
     * @throws Exception
     */
    @Override
    protected Connection beginTransaction() throws Exception {
        Class.forName("com.mysql.jdbc.Driver");
        String url = "";
        String user = "";
        String password = "";
        Connection conn = DriverManager.getConnection(url, user, password);
        conn.setAutoCommit(false);
        return null;
    }

    /**
     * 预提交阶段:
     * 等到下一次 Checkpoint 执行 snapshotState 时,会调用 preCommit 方法进行预提交,预提交一般会对事务进行 flush 操作
     * @param connection
     * @throws Exception
     */
    @Override
    protected void preCommit(Connection connection) throws Exception {
        log.info("start preCommit...");
    }


    /**
     * 提交事务:
     * 在各个实例收到jobmanager的checkpoint完成通知后会调用notifyCheckpointComplete()方法
     * 在notifyCheckpointComplete()方法中调用commit()方法
     * @param connection
     */
    @Override
    protected void commit(Connection connection) {
        try {
            connection.commit();
        } catch (SQLException e) {
            log.error("提交失败!!!");
        }
    }

    /**
     * 如果失败了,会调用close()方法
     * close()方法中会调用abort()方法回滚
     * @param connection
     */
    @Override
    protected void abort(Connection connection) {
        try {
            connection.rollback();
        } catch (SQLException e) {
            log.error("回滚失败!!!");
        }
    }
}

添加一点想法:因为FlinkKafkaProducer011这个类集成了TwoPhaseCommitSinkFunction,里面很好的实现了端到端的Exactly Once,所以在我们想自己实现端到端的Exactly Once时可以借助这个类,把处理好的数据用过FlinkKafkaProducer011发到一个新的topic,然后再从这个topic获取数据,比如用druid直接连接kafka,这样也能帮我们实现端到端的Exactly Once。

发布了20 篇原创文章 · 获赞 9 · 访问量 551

猜你喜欢

转载自blog.csdn.net/weixin_42155491/article/details/105334041