Apache IoTDB 数据传输模块 ISink 接口描述

博客 https://zhuanlan.zhihu.com/p/601886815 的 update。

整体组件与之前博客中的描述相似,但是在之前的 ISourceHandle 和 ISinkHandle 一一对应的基础上进行了改动:

  • 抽象了父类接口 ISink,ISinkChannel 和 ISinkHandle 继承此接口,其中
    • Driver(查询子任务) 持有 ISink,通过 ISink 向下游子任务发送生产好的数据。ISink 接口定义了数据发送逻辑(isFull, send),定义了生命周期管理逻辑(setNoMoreTsBlocks, close, abort)
    • ISinkChannel 负责实际发送数据,实现类为 LocalSinkChannel 和 SinkChannel(即原 LocalSinkHandle 和 SinkHandle )
    • ISinkHandle 负责管理一组 ISinkChannel,需要发送数据时,ISinkHandle 会根据 index 选定一个 ISinkChannel 发送,同时 ISinkHandle 处于一个 FragmentInstance 的顶端,一个 FI 持有一个 ISinkHandle,ISinkHandle 的回调逻辑(如 onFinish)会变更 FI 的状态,进而触发 FI 的状态变更回调函数。
  • 现在 ISourceHandle 与 ISinkChannel 一一对应,一个 ISourceHandle 需要的数据只从一个 ISinkChannel 处获取。
  • 一组 ISinkChannel 由一个 ISinkHandle管理,一个 ISinkHandle 逻辑上对应了多个 ISourceHandle,即现在一个 FragmentInstance 可以通过 ISinkHandle 向多个 FI 发送数据。

接下来对 ISink 接口及其实现类做简要分析,主要包括接口限制,交互逻辑和锁粒度分析。

写此文档时 Apache IoTDB master 分支 commit id 为:0a15a90

ISink 接口描述

接口名接口描述接口限制
ListenableFuture<?> isFull();在调用 send(TsBlock tsBlock)前会调用此方法,返回的 future 为 complete 状态,才能进一步发送 TsBlock。checkState() 如果为 abort 状态,则应该抛异常,因为我们认为 ISink 被 abort 后不应该再被调用此方法。如果发现 ISink 是 closed 状态,我们认为这不是异常状态,因为 ISink 可能在某些场景下被提前关闭(查询语句中有 limit 子句,提前关闭)。返回 comleted future。不阻塞后续运行,因为 send 方法也会对状态进行相应检查。
void send(TsBlock tsBlock);此接口用于 Driver 向下游消费者发送数据,在通过 isFull() 判断不阻塞后,会尝试使用算子生产一批数据,然后通过 ISink 发送给下游。checkState() 如果为 abort 状态,则应该抛异常,因为我们认为 ISink 被 abort 后不应该再被调用此方法。如果发现 ISink 是 closed 状态,我们认为这不是异常状态,因为 ISink 可能在某些场景下被提前关闭(查询语句中有 limit 子句,提前关闭)。直接 return,忽略要发送的 TsBlock。blocked 变量必须为 complete 状态,才能发送数据,否则是异常状态要抛异常。如果 noMoreTsBlocks 为 true,直接返回。发送完数据后,要根据发送的状态更新 blocked 变量(isFull() 会返回 blocked 变量告知外界当前 ISink 是否可以发送数据)
void setNoMoreTsBlocks();调用此接口会通知下游消费者 ISourceHandle ISink 不会/不需要再发送任何数据。状态检查,closed || aborted 时直接返回。对于 ISinkChannel,调用此方法时还需要检查自身是否已经 finish,是否需要调用 listener.onFinish()
void close();正常关闭此 ISink接口需要保持幂等性,closed || aborted 时直接返回。blocked 变量不为 null 时,tryComplete占用内存 bufferRetainedSizeInBytes > 0时,尝试向 MemoryPool 归还内存
void abort();异常 abort 此 ISink接口需要保持幂等性,closed || aborted 时直接返回。blocked 变量不为 null 时,tryCancel占用内存 bufferRetainedSizeInBytes > 0时,尝试向 MemoryPool 归还内存

ISink 部分接口实现锁粒度分析

ShuffleSinkHandle

ListenableFuture<?> isFull();

调用线程

isFull() 只可能被 Driver 调用,即被 Query-Worker-Thread 线程池中的线程调用。

Driver 一次只会被一个线程拿到,所以 ShuffleSinkHandle 的 isFull() 本身不存在并发调用。

拿锁顺序/临界区

加了 synchronized 关键字,所以可能在持有锁的情况下进一步调用 ISinkChannel.open(),而 SinkChannel.open() 会拿 SinkChannel 的锁,可能的拿锁情况是:

Lock ShuffleSinkHandle -> Lock SinkChannel

void send(TsBlock tsBlock);

调用线程

与 isFull() 方法相同,只可能被 Driver 调用,即被 Query-Worker-Thread 线程池中的线程调用。且与 isFull() 不会有并发调用的情况,在 isFull() 被调用完成后才可能进行 send

拿锁顺序/临界区

加了 synchronized 关键字。

如果当前 channel 是 SinkChannel,拿锁顺序:

Lock ShuffleSinkHandle -> Lock SinkChannel

如果当前 channel 是 LocalSinkChannel,拿锁顺序:

Lock ShuffleSinkHandle -> Lock LocalSinkChannel/SharedTsBlockQueue

void close();

调用线程

两个地方可能调用 ShuffleSinkHandle 的 close 方法。

  1. ISinkChannel 触发 onFinish 回调时,如果发现所有的 ISinkChannel 都关闭了,会 close ShuffleSinkHandle。调用线程为触发 ISinkChannel onFinish 回调的线程
    1. SinkChannel 触发
    2. LocalSinkChannel 触发
  2. FragmentInstanceExecution 中 FI 状态发生变化时的回调方法,暂时可以不考虑。
拿锁顺序/临界区

方法本身没有加锁。这是因为我们在 ISinkListener 中保证了 ShuffleSinkHandle 的 close 方法不会因为回调方法被并发调用,此方法中关闭所有 channel 时,并发控制由 channel 自身完成。

ISinkChannel 触发 onFinish 方法然后进入此方法时可能的拿锁情况:

  • SinkChannel 触发 onFinish 回调时,可能持有 SinkChannel 的锁。
  • LocalSinkChannel 触发 onFinish 回调时,必定先拿了 SharedTsBlockQueue 的锁,再拿了 LocalSinkChannel 的锁。

ShuffleSinkHandle 后续会尝试 close 其管理的所有 channel,可能会尝试拿 channel 的锁,此时不会有死锁,因为触发此 close 方法的 channel 的锁已经拿到了,而 close 其它 channel 只需要等待拿锁即可。

LocalSinkChannel

ListenableFuture<?> isFull();

调用线程

ISinkChannel 的 isFull() 只可能被 ShuffleSinkHandle 调用,调用线程与 ShuffleSinkHandle.isFull() 一致。

拿锁顺序/临界区

加了 synchronized 关键字,由 ShuffleSinkHandle.isFull() 调用,此时已经拿到了 ShuffleSinkHandle 的锁。

拿锁顺序:

Lock ShuffleSinkHandle -> Lock LocalSinkChannel

临界区:

  • closed 和 aborted 状态,所有对这两个状态的更改操作都要在拿到 LocalSinkChannel 的锁之后才能进行。
  • blocked 变量,对 blocked 的修改也需要拿到 LocalSinkChannel 的锁之后进行。

void send(TsBlock tsBlock);

调用线程

与 isFull() 方法相同,只可能被 ShuffleSinkHandle 调用,调用线程与 ShuffleSinkHandle.send() 一致。

拿锁顺序/临界区

先拿 SharedTsBlockQueue 锁,再拿 LocalSinkChannel 的锁。临界区为 blocked 变量的状态。

void close();

调用线程

两个地方可能调用 LocalSinkChannel 的 close 方法。

  1. ShuffleSinkHandle.close() 中,会关闭其管理的所有 channel,此时调用线程与 ShuffleSinkHandle.close() 一致。
  2. SharedTsBlockQueue.close() 中,会调用 LocalSinkChannel.close()。SharedTsBlockQueue.close() 由 LocalSourceHandle.close() 调用。
拿锁顺序/临界区

先拿 SharedTsBlockQueue 锁,再拿 LocalSinkChannel 的锁。

临界区:

  • closed 和 aborted 状态
  • blocked 变量
  • SharedTsBlockQueue 状态

void setNoMoreTsBlocks();

调用线程

由 ShuffleSinkHandle 调用,同样是 Query-worker-Thread 线程池的线程。

拿锁顺序/临界区

先拿 SharedTsBlockQueue 锁,再拿 LocalSinkChannel 的锁。

临界区:

  • closed 和 aborted 状态
  • queue.noMoreTsBlocks 状态

总结

  1. 可以看到,我们在涉及到同时持有 SharedTsBlockQueue 锁和 LocalSinkChannel 锁的情况时,我们约定了一定要先拿 SharedTsBlockQueue 的锁。这是为了避免死锁情况,比如 LocalSourceHandle 在自己的方法里先拿了 SharedTsBlockQueue 的锁,然后尝试拿 LocalSinkChannel 的锁,而调用 LocalSinkChannel 方法的另一个线程先拿了 LocalSinkChannel 的锁,然后再尝试拿 SharedTsBlockQueue 的锁,这样就可能导致死锁。
  2. SharedTsBlockQueue 的方法绝大部分都没有 synchronized 关键字,对其状态的互斥修改由 LocalSinkChannel/LocalSourceHandle 保证,这两者在调用 SharedTsBlockQueue 方法前,均会先拿 SharedTsBlockQueue 的锁。
  3. isFull() 和 send 方法之间不会出现并发调用,而 close()/setNoMoreTsBlocks 两个方法会与 isFull() 和 send 出现并发调用的情况。
  4. 核心临界区(任何对这些变量进行修改的操作都需要持有 LocalSinkChannel 锁):
    1. closed 和 aborted 状态
    2. blocked 变量

SinkChannel

ListenableFuture<?> isFull();

调用线程

ISinkChannel 的 isFull() 只可能被 ShuffleSinkHandle 调用,调用线程与 ShuffleSinkHandle.isFull() 一致。

拿锁顺序/临界区

加了 synchronized 关键字,由 ShuffleSinkHandle.isFull() 调用,此时已经拿到了 ShuffleSinkHandle 的锁。

拿锁顺序:

Lock ShuffleSinkHandle -> Lock SinkChannel

临界区:

  • closed 和 aborted 状态,所有对这两个状态的更改操作都要在拿到 SinkChannel 的锁之后才能进行。
  • blocked 变量,对 blocked 的修改也需要拿到 SinkChannel 的锁之后进行。

void send(TsBlock tsBlock);

调用线程

与 isFull() 方法相同,只可能被 ShuffleSinkHandle 调用,调用线程与 ShuffleSinkHandle.send() 一致。

拿锁顺序/临界区

Lock ShuffleSinkHandle -> Lock SinkChannel

临界区:

  • closed 和 aborted 状态
  • noMoreTsBlocks 状态
  • blocked 变量
  • bufferRetainedSizeInBytes
  • sequenceIdToTsBlock

void close();

调用线程

两个地方可能调用 SinkChannel 的 close 方法。

  1. ShuffleSinkHandle.close() 中,会关闭其管理的所有 channel,此时调用线程与 ShuffleSinkHandle.close() 一致。
  2. SourceHandle 发送 closeSinkChannelEvent 给 MppDataExchangeManager,由其调用 SinkChannel.close()。
拿锁顺序/临界区

临界区:

  • closed 和 aborted 状态
  • blocked 变量
  • bufferRetainedSizeInBytes
  • sequenceIdToTsBlock

总结

  1. SinkChannel 的并发控制相较 LocalSinkChannel 更简单,因为 LocalSinkChannel 还需要和 LocalSourceHandle 和 SharedTsBlockQueue 交互。
  2. SinkChannel 锁粒度较大,基本上任何对成员变量的修改都是持有锁之后才能进行。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值