介绍

生产者-消费者模型用于解耦生产者与消费者,平衡两者之间的能力不平衡,该模型广泛应用于各个系统中,Hudi也使用了该模型控制对记录的处理,即记录会被生产者生产至队列中,然后由消费者从队列中消费,更具体一点,对于更新操作,生产者会将文件中老的记录放入队列中等待消费者消费,消费后交由HoodieMergeHandle处理;对于插入操作,生产者会将新记录放入队列中等待消费者消费,消费后交由HandleCreateHandle处理。

入口

前面的文章中提到过无论是HoodieCopyOnWriteTable#handleUpdate处理更新时直接生成了一个SparkBoundedInMemoryExecutor对象,还是HoodieCopyOnWriteTable#handleInsert处理插入时生成了一个CopyOnWriteLazyInsertIterable对象,再迭代时调用该对象的CopyOnWriteLazyInsertIterable#computeNext方法生成SparkBoundedInMemoryExecutor对象。最后两者均会调用SparkBoundedInMemoryExecutor#execute开始记录的处理,该方法核心代码如下

  public E execute() {
try {
ExecutorCompletionService<Boolean> producerService = startProducers();
Future<E> future = startConsumer();
// Wait for consumer to be done
return future.get();
} catch (Exception e) {
throw new HoodieException(e);
}
}

该方法会启动所有生产者和单个消费者进行处理。

Hudi定义了BoundedInMemoryQueueProducer接口表示生产者,其子类实现如下

  • FunctionBasedQueueProducer,基于Function来生产记录,在合并日志log文件和数据parquet文件时使用,以便提供RealTimeView
  • IteratorBasedQueueProducer,基于迭代器来生产记录,在插入更新时使用。

定义了BoundedInMemoryQueueConsumer类表示消费者,其主要子类实现如下

  • CopyOnWriteLazyInsertIterable$CopyOnWriteInsertHandler,主要处理CopyOnWrite表类型时的插入。

    • MergeOnReadLazyInsertIterable$MergeOnReadInsertHandler,主要处理MergeOnRead

表类型时的插入,其为CopyOnWriteInsertHandler的子类。

  • CopyOnWriteLazyInsertIterable$UpdateHandler,主要处理CopyOnWrite表类型时的更新。

整个生产消费相关的类继承结构非常清晰。

对于生产者的启动,startProducers方法核心代码如下

  public ExecutorCompletionService<Boolean> startProducers() {
// Latch to control when and which producer thread will close the queue
final CountDownLatch latch = new CountDownLatch(producers.size());
final ExecutorCompletionService<Boolean> completionService =
new ExecutorCompletionService<Boolean>(executorService);
producers.stream().map(producer -> {
return completionService.submit(() -> {
try {
preExecute();
producer.produce(queue);
} catch (Exception e) {
logger.error("error producing records", e);
queue.markAsFailed(e);
throw e;
} finally {
synchronized (latch) {
latch.countDown();
if (latch.getCount() == 0) {
// Mark production as done so that consumer will be able to exit
queue.close();
}
}
}
return true;
});
}).collect(Collectors.toList());
return completionService;
}

该方法使用CountDownLatch来协调生产者线程与消费者线程的退出动作,然后调用produce方法开始生产,对于插入更新时的IteratorBasedQueueProducer而言,其核心代码如下

  public void produce(BoundedInMemoryQueue<I, ?> queue) throws Exception {
...
while (inputIterator.hasNext()) {
queue.insertRecord(inputIterator.next());
}
...
}

可以看到只要迭代器还有记录(可能为插入时的新记录或者更新时的旧记录),就会往队列中不断写入。

对于消费者的启动,startConsumer方法的核心代码如下

  private Future<E> startConsumer() {
return consumer.map(consumer -> {
return executorService.submit(() -> {
...
preExecute();
try {
E result = consumer.consume(queue);
return result;
} catch (Exception e) {
queue.markAsFailed(e);
throw e;
}
});
}).orElse(CompletableFuture.completedFuture(null));
}

消费时会先进行执行前的准备,然后开始消费,其中consume方法的核心代码如下

  public O consume(BoundedInMemoryQueue<?, I> queue) throws Exception {
Iterator<I> iterator = queue.iterator(); while (iterator.hasNext()) {
consumeOneRecord(iterator.next());
} // Notifies done
finish(); return getResult();
}

可以看到只要队列中还有记录,就可以获取该记录,然后调用不同BoundedInMemoryQueueConsumer子类的consumeOneRecord进行更新插入处理。

值得一提的是Hudi对队列进行了流控,生产者不能无限制地将记录写入队列中,队列缓存的大小由用户配置,队列能放入记录的条数由采样的记录大小和队列缓存大小控制。

在生产时,会调用BoundedInMemoryQueue#insertRecord将记录写入队列,其核心代码如下

  public void insertRecord(I t) throws Exception {
...
rateLimiter.acquire();
// We are retrieving insert value in the record queueing thread to offload computation
// around schema validation
// and record creation to it.
final O payload = transformFunction.apply(t);
adjustBufferSizeIfNeeded(payload);
queue.put(Option.of(payload));
}

首先获取一个许可(Semaphore),未成功获取会被阻塞直至成功获取,然后获取记录的负载以便调整队列,然后放入内部队列(LinkedBlockingQueue)中,其中adjustBufferSizeIfNeeded方法的核心代码如下

  private void adjustBufferSizeIfNeeded(final O payload) throws InterruptedException {
if (this.samplingRecordCounter.incrementAndGet() % RECORD_SAMPLING_RATE != 0) {
return;
} final long recordSizeInBytes = payloadSizeEstimator.sizeEstimate(payload);
final long newAvgRecordSizeInBytes =
Math.max(1, (avgRecordSizeInBytes * numSamples + recordSizeInBytes) / (numSamples + 1));
final int newRateLimit =
(int) Math.min(RECORD_CACHING_LIMIT, Math.max(1, this.memoryLimit / newAvgRecordSizeInBytes)); // If there is any change in number of records to cache then we will either release (if it increased) or acquire
// (if it decreased) to adjust rate limiting to newly computed value.
if (newRateLimit > currentRateLimit) {
rateLimiter.release(newRateLimit - currentRateLimit);
} else if (newRateLimit < currentRateLimit) {
rateLimiter.acquire(currentRateLimit - newRateLimit);
}
currentRateLimit = newRateLimit;
avgRecordSizeInBytes = newAvgRecordSizeInBytes;
numSamples++;
}

首先看是否已经达到采样频率,然后计算新的记录平均大小和限流速率,如果新的限流速率大于当前速率,则可释放一些许可(供阻塞的生产者获取后继续生产),否则需要获取(回收)一些许可(许可变少后生产速率自然就降低了)。该操作可根据采样的记录大小动态调节速率,不至于在记录负载太大和记录负载太小时,放入同等个数,从而起到动态调节作用。

在消费时,会调用BoundedInMemoryQueue#readNextRecord读取记录,其核心代码如下

  private Option<O> readNextRecord() {
...
rateLimiter.release();
Option<O> newRecord = Option.empty();
while (expectMoreRecords()) {
try {
throwExceptionIfFailed();
newRecord = queue.poll(RECORD_POLL_INTERVAL_SEC, TimeUnit.SECONDS);
if (newRecord != null) {
break;
}
} catch (InterruptedException e) {
throw new HoodieException(e);
}
}
... if (newRecord != null && newRecord.isPresent()) {
return newRecord;
} else {
// We are done reading all the records from internal iterator.
this.isReadDone.set(true);
return Option.empty();
}
}

可以看到首先会释放一个许可,然后判断是否还可以读取记录(还在生产或者停止生产但队列不为空都可读取),然后从内部队列获取记录或返回。

上述便是生产者-消费者在Hudi中应用的分析。

总结

Hudi采用了生产者-消费者模型来控制记录的处理,与传统多生产者-多消费者模型不同的是,Hudi现在只支持多生产者-单消费者模型,单消费者意味着Hudi暂时不支持文件的并发写入。而对于生产消费的队列的实现,Hudi并未仅仅只是基于LinkedBlockingQueue,而是采用了更精细化的速率控制,保证速率会随着记录负载大小的变化和配置的队列缓存大小而动态变化,这也降低了系统发生OOM的概率。

生产者-消费者模型在Hudi中的应用的更多相关文章

  1. 进程间通信IPC机制和生产者消费者模型

    1.由于进程之间内存隔离,那么要修改共享数据时可以利用IPC机制 我们利用队列去处理相应数据 #管道 #队列=管道+锁 from multiprocessing import Queue # q=Qu ...

  2. 【python】-- 队列(Queue)、生产者消费者模型

    队列(Queue) 在多个线程之间安全的交换数据信息,队列在多线程编程中特别有用 队列的好处: 提高双方的效率,你只需要把数据放到队列中,中间去干别的事情. 完成了程序的解耦性,两者关系依赖性没有不大 ...

  3. golang实现生产者消费者模型

    生产者消费者模型分析 操作系统中的经典模型,由若干个消费者和生产者,消费者消耗系统资源,生产者创造系统资源,资源的数量要保持在一个合理范围(小于数量上限,大约0).而消费者和生产者是通过并发或并行方式 ...

  4. 如何在 Java 中正确使用 wait, notify 和 notifyAll – 以生产者消费者模型为例

    wait, notify 和 notifyAll,这些在多线程中被经常用到的保留关键字,在实际开发的时候很多时候却并没有被大家重视.本文对这些关键字的使用进行了描述. 在 Java 中可以用 wait ...

  5. Python中的生产者消费者模型

    ---恢复内容开始--- 了解知识点: 1.守护进程: ·什么是守护进程: 守护进程其实就是一个‘子进程’,守护即伴随,守护进程会伴随主进程的代码运行完毕后而死掉 ·为何用守护进程: 当该子进程内的代 ...

  6. 第23章 java线程通信——生产者/消费者模型案例

    第23章 java线程通信--生产者/消费者模型案例 1.案例: package com.rocco; /** * 生产者消费者问题,涉及到几个类 * 第一,这个问题本身就是一个类,即主类 * 第二, ...

  7. Java多线程15:Queue、BlockingQueue以及利用BlockingQueue实现生产者/消费者模型

    Queue是什么 队列,是一种数据结构.除了优先级队列和LIFO队列外,队列都是以FIFO(先进先出)的方式对各个元素进行排序的.无论使用哪种排序方式,队列的头都是调用remove()或poll()移 ...

  8. Java多线程14:生产者/消费者模型

    什么是生产者/消费者模型 一种重要的模型,基于等待/通知机制.生产者/消费者模型描述的是有一块缓冲区作为仓库,生产者可将产品放入仓库,消费者可以从仓库中取出产品,生产者/消费者模型关注的是以下几个点: ...

  9. Java生产者消费者模型

    在Java中线程同步的经典案例,不同线程对同一个对象同时进行多线程操作,为了保持线程安全,数据结果要是我们期望的结果. 生产者-消费者模型可以很好的解释这个现象:对于公共数据data,初始值为0,多个 ...

随机推荐

  1. js中clientWidth, scrollWidth, innerWidth, outerWidth,offsetWidth的区别

    js中clientWidth, scrollWidth, innerWidth, outerWidth,offsetWidth的属性汇总,测试浏览器:ie7~ie11.chrome 和 firefox ...

  2. Leetcode(8)字符串转换整数

    Leetcode(8)字符串转换整数 [题目表述]: 请你来实现一个 atoi 函数,使其能将字符串转换成整数. 首先,该函数会根据需要丢弃无用的开头空格字符,直到寻找到第一个非空格的字符为止. 当我 ...

  3. 设计模式(三)Template Method模式

    在父类中定义处理流程的框架,在子类中实现具体处理的模式就称为Template Method模式即模板方法模式. 根据下面的示例程序理解模板方法模式. package BigJunOba.bjtu.Te ...

  4. Java基础(二十九)Java IO(6)对象序列化(Object Serialization)

    参考之前整理过的Java序列化与反序列化:https://www.cnblogs.com/BigJunOba/p/9127414.html 使用对象输入输出流可以实现对象序列化与反序列化,可以直接存取 ...

  5. (七)javac编译

    文章目录 1.基本格式 2.目标路径 2.1 缺省项 2.2 指定路径 2.2.1 全路径 2.2.2 相对路径 3.源文件 3.1 无第三方库 3.1.1 基本方法 3.1.2 添加目录 3.1.3 ...

  6. 前端技术之:JSON.stringfy详细说明

    JSON.stringify() 语法JSON.stringify(value[, replacer[, space]]) value 被序列化为字符串的对象 replacer 根据类型不同,其行为也 ...

  7. oracle数据库锁表,什么SQL引起了锁表?ORACLE解锁的方法

    --查询数据库锁表记录 select sess.sid, sess.serial#, lo.oracle_username, lo.os_user_name, ao.object_name, lo.l ...

  8. VB.NET 与 SAP RFC连接问题点

    与SAP RFC连接,电脑上必须要安装SAP软件,否则会报错ActiveX 输入工单号,无法带出SAP内接口RFC信息. 确认原因为:RFC接口需求的工单参数需要在前面加两位00,例如:1000541 ...

  9. UVA12433 【Rent a Car】

    这题应该算是比较难的一道网络流的题,(但却在我校OJ考试上出现了),但是大家只要能理解此图的建边方式就行. 假设有5天的租车需求,虚拟出2*n+2 即 12个节点,0为源点,12为汇点. 1,源点到1 ...

  10. C++学习笔记7_多态

    1. 类与类之间的关系class A{ public: int a; void funcA() {}}包含: class B { public: void funcB(){} A a; }//如果类B ...