知识卡片

不要在Spout中处理耗时操作:单线程特性会让耗时逻辑连锁拖慢整体吞吐

普通读书笔记卡

内容

Storm里的Spout负责调用nextTuple方法把数据流发射出去,在启用了ACK机制的情况下,fail方法和ACK方法也会在对应事件发生时被触发——这三个方法看起来是各自独立的回调,但一个容易被忽视的关键约束是:在Storm里,Spout本身是单线程的(JStorm的实现有所改进,把这三个方法分到了三个线程分别执行,但原生Storm不是这样)。这意味着如果nextTuple方法本身执行得很耗时,会直接挤占同一个线程本该用来及时处理ACK和fail回调的时间——某条消息被下游成功处理完毕后,Acker会给Spout发一个ACK消息,但如果Spout当时正卡在一次耗时的nextTuple调用里、无法及时消费这个ACK消息,就可能导致ACK消息因为超时而被丢弃,进而触发这条消息被判定为处理失败、需要重新发送;同样地,如果fail或ACK方法本身的处理逻辑耗时较多,也会反过来影响nextTuple方法发射数据的节奏,造成整个Topology的吞吐量下降。这个案例揭示了Spout单线程这个实现细节所带来的一条重要设计约束:因为nextTuple、fail、ACK这三个本该各司其职的回调实际共享着同一个执行线程,任何一个回调里的耗时操作,都会直接挤占另外两个回调本该拿到的执行时间,进而在整个数据处理链路上产生连锁反应(ACK超时导致误判失败、失败又触发重发、重发又进一步加重了本就繁忙的Spout线程负担)。这提示了一条使用任何存在类似”共享线程”约束的框架组件时都要遵循的原则:在动手实现具体业务逻辑之前,先弄清楚这个组件的哪些回调函数实际上共用同一个执行线程,一旦确认了这种共享关系,就必须格外警惕把耗时操作放进这些共享线程负责的任何一个回调里,因为这类耗时操作造成的影响不会局限在它自己所在的这个回调,而会通过共享线程这条隐藏路径,扩散到所有共享同一线程的其他回调上。

参考来源

- 位置:《高可用架构(第1卷)》第6章《大数据与数据库》"6.2 实时计算在点评"节,"6.2.6 Storm使用经验分享"(源文件:_epub-src/OEBPS/Text/Chapter6_2_7.xhtml) - 结论依据:原文说明"在Storm中Spout是单线程……如果nextTuple方法非常耗时,某个消息被成功执行完毕后,Acker会给Spout发送消息,Spout若无法及时消费,可能造成ACK消息超时后被丢弃……法或者ACK方法的操作耗时较多,则会影响Spout发射数据的量,造成Topology吞吐量降低",直接支撑本卡片结论。 - 原始内容:需要明确一点,在Storm中Spout是单线程……如果nextTuple方法非常耗时,某个消息被成功执行完毕后,Acker会给Spout发送消息,Spout若无法及时消费,可能造成ACK消息超时后被丢弃,然后法或者ACK方法的操作耗时较多,则会影响Spout发射数据的量,造成Topology吞吐量降低。