博客 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 方法。
- ISinkChannel 触发 onFinish 回调时,如果发现所有的 ISinkChannel 都关闭了,会 close ShuffleSinkHandle。调用线程为触发 ISinkChannel onFinish 回调的线程
- SinkChannel 触发
- LocalSinkChannel 触发
- 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 方法。
- ShuffleSinkHandle.close() 中,会关闭其管理的所有 channel,此时调用线程与 ShuffleSinkHandle.close() 一致。
- 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 状态
总结
- 可以看到,我们在涉及到同时持有 SharedTsBlockQueue 锁和 LocalSinkChannel 锁的情况时,我们约定了一定要先拿 SharedTsBlockQueue 的锁。这是为了避免死锁情况,比如 LocalSourceHandle 在自己的方法里先拿了 SharedTsBlockQueue 的锁,然后尝试拿 LocalSinkChannel 的锁,而调用 LocalSinkChannel 方法的另一个线程先拿了 LocalSinkChannel 的锁,然后再尝试拿 SharedTsBlockQueue 的锁,这样就可能导致死锁。
- SharedTsBlockQueue 的方法绝大部分都没有 synchronized 关键字,对其状态的互斥修改由 LocalSinkChannel/LocalSourceHandle 保证,这两者在调用 SharedTsBlockQueue 方法前,均会先拿 SharedTsBlockQueue 的锁。
- isFull() 和 send 方法之间不会出现并发调用,而 close()/setNoMoreTsBlocks 两个方法会与 isFull() 和 send 出现并发调用的情况。
- 核心临界区(任何对这些变量进行修改的操作都需要持有 LocalSinkChannel 锁):
- closed 和 aborted 状态
- 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 方法。
- ShuffleSinkHandle.close() 中,会关闭其管理的所有 channel,此时调用线程与 ShuffleSinkHandle.close() 一致。
- SourceHandle 发送 closeSinkChannelEvent 给 MppDataExchangeManager,由其调用 SinkChannel.close()。
拿锁顺序/临界区
临界区:
- closed 和 aborted 状态
- blocked 变量
- bufferRetainedSizeInBytes
- sequenceIdToTsBlock
总结
- SinkChannel 的并发控制相较 LocalSinkChannel 更简单,因为 LocalSinkChannel 还需要和 LocalSourceHandle 和 SharedTsBlockQueue 交互。
- SinkChannel 锁粒度较大,基本上任何对成员变量的修改都是持有锁之后才能进行。