Flink 源码之数据写入HBase

Flink源码分析系列文档目录

请点击:Flink 源码分析系列文档目录

前言

近期有同事询问Flink写入数据到HBase的方法。借着这个机会分析下Flink 写入HBase相关部分的源代码。

HBaseSinkFunction

HBaseSinkFunction是Flink写入数据到HBase的官方功能实现。

当然,作为一个数据落地端,HBaseSinkFunction毫无疑问需要实现SinkFunction接口。SinkFunction的使用参见:Flink 源码之两阶段提交

作为一个有状态的数据落地端,HBaseSinkFunction继承自RichSinkFunction。我们从它的open方法开始,分析下Flink如何做到:

  • 如何初始化和关闭HBase连接
  • 如何高效率将数据插入HBase
  • 如何处理checkpoint问题

open方法

我们查看下它的open方法,如下所示:

@Override
public void open(Configuration parameters) throws Exception {
    LOG.info("start open ...");
    // 获取HBase的配置
    org.apache.hadoop.conf.Configuration config = prepareRuntimeConfiguration();
    try {
        // 初始化mutationConverter
        // mutationConverter负责转换Flink元素为HBase Mutation
        this.mutationConverter.open();
        // 记录尚未flush到HBase的数据条数
        this.numPendingRequests = new AtomicLong(0);

        // 创建HBase连接
        if (null == connection) {
            this.connection = ConnectionFactory.createConnection(config);
        }
        // create a parameter instance, set the table name and custom listener reference.
        // 创建BufferedMutator构建参数
        // 包含写入HBase的目标表名称和ExceptionListener
        BufferedMutatorParams params =
            new BufferedMutatorParams(TableName.valueOf(hTableName)).listener(this);
        // 设置BufferedMutator的写缓存字节数
        if (bufferFlushMaxSizeInBytes > 0) {
            params.writeBufferSize(bufferFlushMaxSizeInBytes);
        }
        // 根据前面的参数,创建一个BufferedMutator
        this.mutator = connection.getBufferedMutator(params);

        // 如果flush间隔大于0ms,且最大待flush数据条数不等于1时
        // 创建一个定时flush BufferedMutator的任务
        if (bufferFlushIntervalMillis > 0 && bufferFlushMaxMutations != 1) {
            this.executor =
                Executors.newScheduledThreadPool(
                1, new ExecutorThreadFactory("hbase-upsert-sink-flusher"));
            this.scheduledFuture =
                this.executor.scheduleWithFixedDelay(
                () -> {
                    if (closed) {
                        return;
                    }
                    try {
                        flush();
                    } catch (Exception e) {
                        // fail the sink and skip the rest of the items
                        // if the failure handler decides to throw an exception
                        // 保存发生的异常,等到检查的时候抛出
                        failureThrowable.compareAndSet(null, e);
                    }
                },
                bufferFlushIntervalMillis,
                bufferFlushIntervalMillis,
                TimeUnit.MILLISECONDS);
        }
    } catch (TableNotFoundException tnfe) {
        LOG.error("The table " + hTableName + " not found ", tnfe);
        throw new RuntimeException("HBase table '" + hTableName + "' not found.", tnfe);
    } catch (IOException ioe) {
        LOG.error("Exception while creating connection to HBase.", ioe);
        throw new RuntimeException("Cannot create connection to HBase.", ioe);
    }
    LOG.info("end open.");
}

由以上分析可知,open方法创建了HBase的BufferedMutator,用于缓存待插入的数据,以批量方式将这些数据插入。相比依次插入单条数据而言,极大的提升了操作HBase的效率。

获取HBase配置的过程

这部分逻辑位于prepareRuntimeConfiguration方法。

获取HBase配置文件的逻辑如下列表所示。按照从上到下的顺序查找hbase-site.xml文件,无论找到与否,都会尝试整个列表。列表后者找到的配置项会覆盖前者。

  • 首先从classpath中查找hbase-site.xmlhbase-default.xml文件。
  • HBASE_HOME环境变量的conf目录查找hbase-site.xmlhbase-default.xml文件。
  • HBASE_CONF_DIR环境变量查找hbase-site.xmlhbase-default.xml文件。

prepareRuntimeConfiguration方法代码如下所示:

private org.apache.hadoop.conf.Configuration prepareRuntimeConfiguration() throws IOException {
    // create default configuration from current runtime env (`hbase-site.xml` in classpath)
    // first,
    // and overwrite configuration using serialized configuration from client-side env
    // (`hbase-site.xml` in classpath).
    // user params from client-side have the highest priority
    
    // 获取Configuration
    org.apache.hadoop.conf.Configuration runtimeConfig =
            HBaseConfigurationUtil.deserializeConfiguration(
                    serializedConfig, HBaseConfigurationUtil.getHBaseConfiguration());

    // do validation: check key option(s) in final runtime configuration
    // 检查获取到的HBase配置
    // 如果配置中不包含hbase.zookeeper.quorum配置项(HBase必须),抛出异常
    if (StringUtils.isNullOrWhitespaceOnly(runtimeConfig.get(HConstants.ZOOKEEPER_QUORUM))) {
        LOG.error(
                "Can not connect to HBase without {} configuration",
                HConstants.ZOOKEEPER_QUORUM);
        throw new IOException(
                "Check HBase configuration failed, lost: '"
                        + HConstants.ZOOKEEPER_QUORUM
                        + "'!");
    }

    return runtimeConfig;
}

获取配置信息的具体逻辑位于HBaseConfigurationUtil.getHBaseConfiguration()。我们继续跟踪。

public static Configuration getHBaseConfiguration() {

    // Instantiate an HBaseConfiguration to load the hbase-default.xml and hbase-site.xml from
    // the classpath.
    // 默认方式,从classpath读取hbase-default.xml和hbase-site.xml
    Configuration result = HBaseConfiguration.create();
    boolean foundHBaseConfiguration = false;

    // We need to load both hbase-default.xml and hbase-site.xml to the hbase configuration
    // The properties of a newly added resource will override the ones in previous resources, so
    // a configuration
    // file with higher priority should be added later.

    // Approach 1: HBASE_HOME environment variables
    String possibleHBaseConfPath = null;

    // 读取HBASE_HOME环境变量
    final String hbaseHome = System.getenv("HBASE_HOME");
    // 如果该环境变量存在,尝试从${HBASE_HOME}/conf读取配置
    if (hbaseHome != null) {
        LOG.debug("Searching HBase configuration files in HBASE_HOME: {}", hbaseHome);
        possibleHBaseConfPath = hbaseHome + "/conf";
    }

    if (possibleHBaseConfPath != null) {
        foundHBaseConfiguration = addHBaseConfIfFound(result, possibleHBaseConfPath);
    }

    // Approach 2: HBASE_CONF_DIR environment variable
    // 读取HBASE_CONF_DIR环境变量,如果该变量存在,尝试从该路径下读取配置
    String hbaseConfDir = System.getenv("HBASE_CONF_DIR");
    if (hbaseConfDir != null) {
        LOG.debug("Searching HBase configuration files in HBASE_CONF_DIR: {}", hbaseConfDir);
        foundHBaseConfiguration =
            addHBaseConfIfFound(result, hbaseConfDir) || foundHBaseConfiguration;
    }

    if (!foundHBaseConfiguration) {
        LOG.warn(
            "Could not find HBase configuration via any of the supported methods "
            + "(Flink configuration, environment variables).");
    }

    return result;
}

Invoke方法

HBaseSinkFunction每次接收到一个数据,都会调用invoke方法。该方法负责将数据转换为HBase的mutation,放入到BufferedMutator中。当BufferedMutator累积的mutation数超过bufferFlushMaxMutations时候,强制flush到HBase中。

@Override
public void invoke(T value, Context context) throws Exception {
    // 检查是否有异常发生
    checkErrorAndRethrow();

    // 使用mutationConverter转换数据为HBase的mutation
    // mutationConverter在后面章节介绍
    // 把这个mutation加入到BufferedMutator中
    mutator.mutate(mutationConverter.convertToMutation(value));

    // flush when the buffer number of mutations greater than the configured max size.
    // numPendingRequests记录了未发送的写入请求个数
    // 如果numPendingRequests大于bufferFlushMaxMutations,强制flush到HBase
    if (bufferFlushMaxMutations > 0
        && numPendingRequests.incrementAndGet() >= bufferFlushMaxMutations) {
        flush();
    }
}

接下来分析下强制把数据写入HBase的flush方法。

private void flush() throws IOException {
    // BufferedMutator is thread-safe
    // 调用BufferedMutator的flush方法
    mutator.flush();
    // 重置pendingRequest计数器为0
    numPendingRequests.set(0);
    // 检查是否有异常发生
    checkErrorAndRethrow();
}

到此为止,HBaseSinkFunction中有关RichSinkFunction的方法已经分析完毕。除了一个close方法,包含关闭BufferedMutatorConnection和周期性flush HBase的定时任务,不再详细分析。

checkpoint相关方法实现

HBaseSinkFunction在checkpoint的时候,如果有尚未写入HBase的数据,必须flush到HBase中。以防止从checkpoint恢复的时候数据丢失。

@Override
public void snapshotState(FunctionSnapshotContext context) throws Exception {
    // 如果pendingRequest不为0,即有缓存的数据
    // 一直执行flush操作,直到数据全存入HBase为止
    while (numPendingRequests.get() != 0) {
        flush();
    }
}

@Override
public void initializeState(FunctionInitializationContext context) throws Exception {
    // 从checkpoint中恢复,什么都不用做
    // nothing to do.
}

MutationConverter

MutationConverter用于将Flink的数据转换为HBase的Mutation

该接口有两个方法:

  • open:初始化converter。
  • convertToMutation:将record转换为Mutation
@Internal
public interface HBaseMutationConverter<T> extends Serializable {

    /** Initialization method for the function. It is called once before conversion method. */
    void open();

    /**
     * Converts the input record into HBase {@link Mutation}. A mutation can be a {@link Put} or
     * {@link Delete}.
     */
    Mutation convertToMutation(T record);
}

HBaseMutationConverter有两个实现类:

  • LegacyMutationConverter:转换Tuple2<Boolean, Row>类型的数据。如果boolean为true,则创建Put操作,如果为false创建Delete操作。
  • RowDataToMutationConverter:转换RowData类型数据。通过RowKind来控制创建出Put操作还是Delete操作。

除此之外,这两个类中大量的逻辑为从HBaseTableSchema获取列族,qualifier和charset等信息,然后从数据解析出每个cf:qualifier对应的value,构造出PutDelete对象。此处不再一一分析。

本博客为作者原创,欢迎大家参与讨论和批评指正。如需转载请注明出处。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容