Flink 单并行度内使用多线程,提高写入性能10倍

目录

分析痛点

方案一:同步批量请求优化为异步请求

方案二:多线程 Client 模式

实现原理:Flink 算子内多线程

代码实现


分析痛点

        笔者线上有一个 Flink 任务消费 Kafka 数据,将数据转换后,在 Flink 的 Sink 算子内部调用第三方 api 将数据上报到第三方的数 据分析平台。这里使用批量同步 api,即:每 50 条数据请求一次第三方接口,可以通过批量 api 来提高请求效率。由于调用的外网接口,所以每次调用 api 比较耗时。假如批次大小为 50,且请求接口的平均响应时间为 50ms,使用同步 api,因此第一次请求响应以后才会发起第二次请求。请求示意图如下所示:

        平均下来,每 50 ms 向第三方服务器发送 50 条数据,也就是每个并行度 1 秒钟处理 1000 条数据。假设当前业务数据量为每秒 10 万条数据,那么 Flink Sink 算子的并行度需要设置为 100 才能正常处理线上数据。从 Flink 资源分配来讲,100 个并行度需要申请 100 颗 CPU,因此当前 Flink 任务需要占用集群中 100 颗 CPU 以及不少的内存资。请问此时 Flink Sink 算子的 CPU 或者内存压力大吗?上述请求示意图可以看出 Flink 任务发出请求到响应这 50 ms 期间,Flink Sink 算子只是在 wait,并没有实质性的工作。因此,CPU 使用率肯定很低,当前任务的瓶颈明显在网络 IO。最后结论是 Flink 任务申请了 100 颗 CPU,导致 yarn 或其他资源调度框架没有资源了,但是这 100 颗 CPU 的使用率并不高,这里能不能优化通过提高 CPU 的使用率,从而少申请一些 CPU 呢?

 

方案一:同步批量请求优化为异步请求

        首先可能想到可能是将同步请求改为异步请求,使得任务不会阻塞在网络请求这一环节,请求示意图如下所示。

异步请求第三方示意图.png

        异步请求相比同步请求而言,优化点在于每次发出请求时,不需要等待请求响应后再发送下一次请求,而是当下一批次的 50 条数据准备好之后,直接向第三方服务器发送请求。每次发送请求后,Flink Sink 算子的客户端需要注册监听器来等待响应,当响应失败时需要做重试或者回滚策略。通过异步请求的方式,可以优化网络瓶颈,假如 Flink Sink 算子的单个并行度平均 10ms 接收到 50 条数据,那么使用异步 api 的方式平均 1 秒可以处理 5000 条数据,整个 Flink 任务的性能提高了 5 倍。对于每秒 10 万数据量的业务,这里仅需要申请 20 颗 CPU 资源即可。关于异步 api 的具体使用,可以根据场景具体设计,这里不详细讨论。

 

方案二:多线程 Client 模式

        对于一些不支持异步 api 的场景,可能并不能使用上述优化方案,同样,为了提高 CPU 使用率,可以在 Flink Sink 端使用多线程的方案。如下图所示,可以在 Flink Sink 端开启 5 个请求第三方服务器的 Client 线程:Client1、Client2、Client3、Client4、Client5。这五个线程内分别使用同步批量请求的 Client,单个 Client 还是保持 50 条记录为一个批次,即 50 条记录请求一次第三方 api。请求第三方 api 耗时主要在于网络 IO(性能瓶颈在于网络请求延迟),因此如果变成 5 个 Client 线程,每个 Client 的单次请求平均耗时还能保持在 50ms,除非网络请求已经达到了带宽上限或整个任务又遇到其他瓶颈。所以,多线程模式下使用同步批量 api 也能将请求效率提升 5 倍。 

多线程请求示意图.png

       多线程的方案,不仅限于请求第三方接口,对于非 CPU 密集型的任务都可以使用该方案在降低 CPU 数量的同时,使得单个 CPU 承担多个线程的工作,从而提高 CPU 利用率。例如:请求 HBase 的任务或磁盘 IO 是瓶颈的任务,都可以降低任务的并行度,使得每个并行度内处理多个线程。

 

实现原理:Flink 算子内多线程

        Sink 算子的单个并行度内现在有 5 个 Client 用于消费数据,但 Sink 算子的数据都来自于上游算子。如下图所示,一个简单的实现方式是 Sink 算子接收到上游数据后通过轮循或随机的策略将数据分发给 5 个 Client 线程。

轮循或随机策略将数据分发给 Client 线程.png

       但是轮循或者随机策略会存在问题,假如 5 个 Client 中 Client3 线程消费较慢,会导致给 Client3 分发数据时被阻塞,从而使得其他正常消费的线程 Client1、2、4、5 也被分发不到数据。

       为了解决上述问题,可以在 Sink 算子内申请一个数据缓冲队列,队列有先进先出(FIFO)的特性。Sink 算子接收到的数据直接插入到队列尾部,五个 Client 线程不断地从队首取数据并消费,即:Sink 算子先接收的数据 Client 先消费,后接收的数据 Client 后消费。若队列一直是满的,说明 Client 线程消费较慢、Sink 算子上游生产数据较快。若队列一直为空,说明 Client 线程消费较快、Sink 算子的上游生产数据较慢。五个线程共用同一个队列完美地解决了单个线程消费慢的问题,当 Client3 线程阻塞时,不影响其他线程从队列中消费数据。这里使用队列还起到***削峰填谷***的作用。

接收到的数据 put 到数据缓冲队列.png

 

代码实现

         Sink 能保证 Exactly Once 吗?答:不能保证 Exactly Once,Flink 要想端对端保证 Exactly Once,必须要求外部组件支持事务,这里第三方接口明显不支持事务。那么上述 Sink 能保证 At Lease Once 吗?言外之意,上述 Sink 会丢数据吗?答:会丢数据。因为上述案例中使用的批量 api 来消费数据,假如批量 api 是每积攒 50 条数据请求一次第三方接口,当 Checkpoint 时可能只积攒了 30 条数据,所以 Checkpoint 时内存中可能还有数据未发送到外部系统。而且 Checkpoint 时数据缓冲队列中可能还有缓存的数据,因此上述 Sink 在 Checkpoint 时会出现 Checkpoint 之前的数据未完全消费的情况。

         例如,Flink 任务消费的 Kafka 数据,当 Checkpoint 时,Flink 任务消费到 offset 为 10000 的位置,但实际上 offset 10000 之前的一小部分数据可能还在数据缓冲队列中还未完全消费,或者因为没积攒够一定批次所以数据缓存在 client 中,并未请求到第三方。当任务失败后,Flink 任务从 Checkpoint 处恢复,会从 offset 为 10000 的位置开始消费,此时 offset 10000 之前的一小部分缓存在内存缓冲队列中的数据不会再去消费,于是就出现了丢数据情况。 

 

        如何保证数据不丢失呢?很简单,可以在 Checkpoint 时强制将数据缓冲区的数据全部消费完,并对 client 执行过 flush 操作,保证 client 端不会缓存数据。实现思路:Sink 算子可以实现 CheckpointedFunction 接口,当 Checkpoint 时,会调用 snapshotState 方法,方法内可以触发 client 的 flush 操作。但 client 在 MultiThreadConsumerClient 对应的五个线程中,这里需要考虑线程同步的问题,即:Sink 算子的 snapshotState 方法中做一个操作,要使得五个 Client 线程感知到当前正在执行 Checkpoint,此时应该把数据缓冲区的数据全部消费完,并对 client 执行过 flush 操作。

       如何实现呢?需要借助 CyclicBarrier,CyclicBarrier 会让所有线程都等待某个操作完成后才会继续下一步行动。在这里可以使用 CyclicBarrier,让 Checkpoint 等待所有的 client 将数据缓冲区的数据全部消费完并对 client 执行过 flush 操作,言外之意,offset 10000 之前的数据必须全部消费完成才允许 Checkpoint 执行完成。这样就可以保证 Checkpoint 时不会有数据被缓存在内存,可以保证数据源 offset 10000 之前的数据都消费完成。 

public class SinkToMySQLMultiThread extends RichSinkFunction<Student> implements CheckpointedFunction {

    private Logger LOG = LoggerFactory.getLogger(SinkToMySQLMultiThread.class);

    // Client 线程的默认数量
    private final int DEFAULT_CLIENT_THREAD_NUM = 5;
    // 数据缓冲队列的默认容量
    private final int DEFAULT_QUEUE_CAPACITY = 5000;

    private LinkedBlockingQueue<Student> bufferQueue;
    private CyclicBarrier clientBarrier;


    private Connection connection = null;
    private PreparedStatement ps = null;

    /**
     * open()
     */
    @Override
    public void open(Configuration parameters) throws Exception {

        super.open(parameters);

        // new 一个容量为 DEFAULT_CLIENT_THREAD_NUM 的线程池
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(DEFAULT_CLIENT_THREAD_NUM, DEFAULT_CLIENT_THREAD_NUM,
                0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>());

        // new 一个容量为 DEFAULT_QUEUE_CAPACITY 的数据缓冲队列
        this.bufferQueue = Queues.newLinkedBlockingQueue(DEFAULT_QUEUE_CAPACITY);

        // barrier 需要拦截 (DEFAULT_CLIENT_THREAD_NUM + 1) 个线程
        this.clientBarrier = new CyclicBarrier(DEFAULT_CLIENT_THREAD_NUM + 1);

        // 创建并开启消费者线程
        MultiThreadConsumerClient consumerClient = new MultiThreadConsumerClient(bufferQueue, clientBarrier);
        for (int i=0; i < DEFAULT_CLIENT_THREAD_NUM; i++) {
            threadPoolExecutor.execute(consumerClient);
        }

    }


    @Override
    public void invoke(Student value, Context context) throws Exception {
        // 往 bufferQueue 的队尾添加数据
        bufferQueue.put(value);
    }

    @Override
    public void snapshotState(FunctionSnapshotContext functionSnapshotContext) throws Exception {
        LOG.info("snapshotState : 所有的 client 准备 flush !!!");
        // barrier 开始等待
        clientBarrier.await();
    }

    @Override
    public void initializeState(FunctionInitializationContext functionInitializationContext) throws Exception {
    }

}

        SinkToMySQLMultiThread 实现了 CheckpointedFunction 接口,在 open 方法中增加了 CyclicBarrier 的初始化,CyclicBarrier 预期容量设置为 client 线程数加一,表示当 client 线程数加一个线程都执行了 await 操作时,所有的线程的 await 方法才会执行完成。这里为什么要加一呢?因为除了 client 线程外, snapshotState 方法中也需要执行过 await。

        当 Checkpoint 时 snapshotState 方法中执行 clientBarrier.await(),等待所有的 client 线程将缓冲区数据消费完。snapshotState 方法执行过程中 invoke 方法不会被执行,即:Checkpoint 过程中数据缓冲队列不会增加数据,所以 client 线程很快就可以将缓冲队列中的数据消费完。

/**
 * 多线程
 * 每5条批量写一次数据库
 */
public class MultiThreadConsumerClient implements Runnable {

    private Logger LOG = LoggerFactory.getLogger(MultiThreadConsumerClient.class);

    private LinkedBlockingQueue<Student> bufferQueue;
    private CyclicBarrier barrier;

    private Connection connection = null;
    private PreparedStatement ps = null;

    public MultiThreadConsumerClient(
            LinkedBlockingQueue<Student> bufferQueue, CyclicBarrier barrier) {
        this.bufferQueue = bufferQueue;
        this.barrier = barrier;
    }

    @Override
    public void run() {

        try {
            String driver = "com.mysql.jdbc.Driver";
            String url = "jdbc:mysql://192.168.2.101:3306/test";
            String username = "ambari";
            String password = "ambari";
            //1.加载驱动
            Class.forName(driver);
            //2.创建连接
            connection = DriverManager.getConnection(url, username, password);
            String sql = "insert into Student(id,name,password,age)values(?,?,?,?);";
            //3.获得执行语句
            ps = connection.prepareStatement(sql);

            int batchSize = 0;

            Student entity;
            while (true){

                    // 从 bufferQueue 的队首消费数据,并设置 timeout
                    entity = bufferQueue.poll(50, TimeUnit.MILLISECONDS);
                    // entity != null 表示 bufferQueue 有数据
                    if(entity != null){

                        System.out.println(Thread.currentThread().getName());
                        System.out.println(batchSize);

                        // 执行 client 消费数据的逻辑
                        doSomething(entity);

                        batchSize ++ ;

                        if(batchSize > 5) {
                            ps.executeBatch();
                            batchSize = 0;
                        }


                    } else {
                        // entity == null 表示 bufferQueue 中已经没有数据了,
                        // 且 barrier wait 大于 0 表示当前正在执行 Checkpoint,
                        // client 需要执行 flush,保证 Checkpoint 之前的数据都消费完成
                        //System.out.println(barrier.getNumberWaiting());
                        if ( barrier.getNumberWaiting() > 0 ) {
                            LOG.info("MultiThreadConsumerClient 执行 flush, " +  "当前 wait 的线程数:" + barrier.getNumberWaiting());

                            // client 执行 flush 操作,防止丢数据
                            ps.executeBatch();
                            batchSize = 0;

                            barrier.await();
                        }
                    }

            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    // client 消费数据的逻辑
    private void doSomething(Student entity) throws Exception{

            //4.批量插入
            ps.setInt(1, entity.getId());
            ps.setString(2, entity.getName());
            ps.setString(3, entity.getPassword());
            ps.setInt(4, entity.getAge());
            ps.addBatch();

    }

}

        从数据缓冲队列中 poll 数据时,增加了 timeout 时间为 50ms。如果从队列中拿到数据,则执行消费数据的逻辑,若拿不到数据说明数据缓冲队列中数据消费完了。此时需要判断是否有等待的 CyclicBarrier,如果有等待的 CyclicBarrier 说明此时正在执行 Checkpoint,所以 client 需要执行 flush 操作。flush 完成后,Client 线程执行 barrier.await() 操作。当所有的 Client 线程都执行到 await 时,所有的 barrier.await() 都会被执行完。此时 Sink 算子的 snapshotState 方法就会执行完。通过这种策略可以保证 Checkpoint 时将数据缓冲区中的数据消费完,client 执行 flush 操作可以保证 client 端不会缓存数据。

 

代码地址:https://github.com/jztian/flink-demo/tree/master/src/main/java/sinkmysqlMultiThread

如果觉得还不错,给个start呗!

 

<think>好的,我现在需要帮助用户解决关于Flink写入HBase进行压力测试和配置调优的问题。首先,我得先理清楚用户的需求。用户提到了压力测试的方法和配置调优的最佳实践,还引用了几个相关的资料,我需要结合这些引用来给出合理的建议。 首先,压力测试的方法。用户可能想知道如何模拟高负载情况,测试Flink写入HBase的性能。我应该考虑测试工具,比如使用Flink的DataGen或自定义生成器来生成数据。同时,监控关键指标,如吞吐量、延迟、资源使用情况等,这些都很重要。引用2中提到写入缓冲区大小的问题,可能需要调整hbase.client.write.buffer参数,这应该在测试中进行验证。 然后是配置调优部分。根据引用1,HBase配置的优先级会影响Flink的行为,所以需要确保配置正确。比如设置正确的zk路径(引用3)、调整写入缓冲区(引用2)以及批量提交参数。另外,引用4提到HBase适合作为维度表的存储,可能涉及幂等性和点查优化,但用户的问题更关注写入压力,所以这部分可能暂时不用深入。 接下来是代码优化。比如使用异步写入(AsyncTable)而不是同步操作,减少次提交的数据量,分区和并行度调整。异步写入可以提升吞吐量,这在压力测试中很关键。引用5提供的链接可能有更多实现细节,需要确认是否有相关示例。 资源调优方面,Flink的内存设置和HBase的RegionServer配置都很重要。如果JVM内存不足会导致GC频繁,影响性能。HBase的Region分布均匀也能避免热点问题,提升写入速度。 高可用和错误处理部分,用户可能关心在高压下系统的稳定性,比如设置重试机制和检查点配置。引用3提到zk路径的问题,如果配置错误会导致连接失败,这在压力测试中需要特别注意。 最后,最佳实践部分需要总结前面提到的点,给出步骤建议,比如分阶段测试、参数对比、监控分析等。同时,提醒用户根据实际数据特征调整参数,比如数据大小和分布。 现在需要检查是否有遗漏的引用内容。引用1提到配置优先级,需要确保在压力测试中使用正确的方式覆盖配置,比如通过环境变量或SQL参数。引用2的写入缓冲区大小调整是关键配置之一,必须包含在调优建议中。引用3的zk路径设置正确与否影响连接,压力测试前需验证。引用4虽然主要讲应用场景,但强调HBase的写入特性,可能影响测试设计。引用5的链接可能有具体代码示例,但用户未直接引用内容,可能需要参考但不必深入。 总结下来,回答的结构应该分为压力测试方法、配置调优、代码优化、资源调优、高可用与错误处理、最佳实践步骤,并结合各个引用中的要点。需要确保建议具体,比如给出具体参数值范围,并解释其影响。同时,提醒用户监控相关指标,并根据测试结果迭代优化配置。</think>以下是关于Flink写入HBase压力测试及配置优化的系统化解决方案: --- ### 一、压力测试方法 1. **数据生成策略** - 使用Flink内置的`DataGen`连接器或自定义数据生成器模拟高吞吐数据,确保测试数据量与生产环境匹配。 - 通过调整并行度控制数据生成速率,例如设置`env.setParallelism(8)`以模拟多并发场景[^2]。 2. **关键监控指标** - **Flink侧**:TaskManager CPU/内存使用率、Checkpoint耗时、背压情况(通过Flink Web UI或Metrics Reporter)。 - **HBase侧**:RegionServer写入吞吐量(`WriteRequests`)、MemStore刷新频率、BlockCache命中率(通过HBase Metrics或JMX)。 3. **基准测试工具** - 结合YCSB(Yahoo! Cloud Serving Benchmark)或自定义工具对HBase独压测,对比Flink写入性能瓶颈[^4]。 --- ### 二、核心配置调优 1. **HBase客户端参数** - **`hbase.client.write.buffer`**:调整批量提交缓冲区大小,建议从默认2MB逐步增至6MB,减少RPC调用次数。 ```xml <property> <name>hbase.client.write.buffer</name> <value>6291456</value> </property> ``` - **`hbase.regionserver.handler.count`**:增加RegionServer处理线程数(默认30→50),提升并发处理能力。 2. **Flink与HBase交互优化** - **批量提交**:通过`TableOutputFormat`设置`setAutoFlush(false)`,手动控制`flushCommits()`频率。 - **异步写入**:使用`AsyncTable`接口替代同步API,减少线程阻塞(需HBase 2.0+)[^5]。 --- ### 三、代码层优化 1. **并行度与分区策略** - 根据HBase Region数量调整Flink并行度,避免个TaskManager负载过重。 - 对RowKey进行散列或预分区,确保数据均匀分布,防止Region热点。 2. **反压处理** - 启用Flink Checkpoint(间隔30-60秒),结合`enableChangelogStateBackend`减少状态恢复时间。 - 使用`BufferedUpsertSink`等优化后的Sink实现,平衡写入速率与资源消耗。 --- ### 四、资源调优 1. **Flink资源配置** - TaskManager堆内存至少4GB,JVM参数中增加`-XX:+UseG1GC`优化垃圾回收。 - 网络缓冲区(`taskmanager.network.memory.fraction`)提升至0.3,减少反压概率。 2. **HBase集群优化** - RegionServer堆内存建议16-32GB,MemStore占比不超过40%(`hbase.regionserver.global.memstore.size`)。 - 启用HBase BucketCache(SSD缓存)加速随机写入[^1]。 --- ### 五、高可用与错误处理 1. **重试与超时机制** ```java configuration.set("hbase.client.retries.number", "5"); // 重试次数 configuration.set("hbase.client.pause", "1000"); // 重试间隔 ``` 2. **ZooKeeper配置校验** - 确认`zookeeper.znode.parent`路径与实际HBase ZK节点一致(如`/hbase-secure`或`/hbase-unsecure`)[^3]。 --- ### 六、最佳实践步骤 1. **分阶段压测** - 第一阶段:TaskManager低负载测试,验证基础配置。 - 第二阶段:逐步增加并行度至集群上限,观察资源瓶颈。 2. **参数对比测试** - 对比不同`hbase.client.write.buffer`值下的吞吐量(如2MB vs 6MB),记录GC频率变化。 3. **监控与分析** - 使用Grafana+Prometheus实时监控,重点关注Flink反压率与HBase MemStore刷新次数。 --- ### 示例配置对比 | 参数 | 默认值 | 优化建议 | 影响维度 | |-------------------------------|-----------|----------------|------------------| | `hbase.client.write.buffer` | 2MB | 4-6MB | 吞吐量↑,延迟↓ | | `taskmanager.network.memory` | 64MB | 512MB | 反压概率↓ | | `hbase.regionserver.handlers` | 30 | 50 | 并发处理能力↑ | ---
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值