-
Daemon继承Thread,有个run() 方法, 间隔一定时间执行一次。
-
MasterDaemon继承Daemon,实现了run方法里面真正执行的runOneCycle() 方法。
-
RoutineLoadTaskScheduler 继承 MasterDaemon ,实现了runOneCycle() 里面真正执行的runAfterCatalogReady() 方法。另外一个继承MasterDaemon的RoutineLoadScheduler 类也是如此。
-
FE初始化的时候,通过RoutineLoadTaskScheduler的构造方法,注入RoutineLoadManager实例。并调用父类构造方法,注入1ms的轮询间隔。RoutineLoadScheduler 也是如此,不过注入的是10s。
-
RoutineLoadTaskScheduler 有个私有的类成员属性needScheduleTasksQueue, 类型是LinkedBlockingQueue ,状态为运行的routine load 会创建routine loadroutineLoadTaskInfo(RoutineLoadTaskInfo)调用RoutineLoadTaskScheduler#add() 方法。Kafka的分区数如果小于desired_concurrent_number,也只会创建分区数的任务,如果大于desired_concurrent_number,就会创建desired_concurrent_number对应数量的RoutineLoadTaskInfo(一个消费者多个分区)加入到这个队列里。
-
RoutineLoadTaskScheduler#runAfterCatalogReady() 方法调用了RoutineLoadTaskScheduler#proces()方法。
-
RoutineLoadTaskScheduler#process()方法首先调用 RoutineLoadTaskScheduler#updateBackendSlotIfNecessary()执行更新BE的最大slot (执行routine load的任务槽),存到Map中,key是BE id,value是每个BE可执行的最大routine load任务数,取max_routine_load_task_num_per_be 配置值,默认为5。
-
调用RoutineLoadManager#getClusterIdleSlotNum()方法获取空闲的任务槽,用上面得到的最大MAP和当前每个BE执行多少个任务的MAP遍历相同的BE id并进行互减,如果得到的数值为0,直接睡眠10s,然后return。
-
如果有可用任务槽,调用NeedScheduleTasksQueue#take()方法,如果没有routineLoadTaskInfo,是空的队列,会阻塞线程,直到取到routineLoadTaskInfo。Put()是队尾进,take() 是队头取。Put() 满了会阻塞,add() 满了会抛异常。
10. 取到routineLoadTaskInfo后,检查当前时间与该routineLoadTaskInfo被调度的最后时间的差距是否小于超时时间,超时时间为配置 max_batch_interval (最大执行时间)的两倍。如果小于,这个值,就先放回队列(目前设置最大间隔20s,也就是一个routineLoadTaskInfo没有调度成功,被放回队列中的话,需要40s才能再次被调度?)。
11. 符合条件的话,会调用RoutineLoadTaskScheduler#scheduleOneTask()方法,第一步就是设置最后调度时间为当前时间。
12. 调用RoutineLoadManager#checkTaskInJob()方法检查routineLoadTaskInfo是否已经被kill或者取消掉等等,如果routineLoadTaskInfo已经没了,就直接return。
13. 检查当前topic分区的最大offset是否比之前的缓存的offset大,如果不大,就重新放回队列,return。
14. 再次检查是否有可分配的BE slot,如果之前这个routineLoadTaskInfo被分配的BE还有空闲,直接返回这个BE的ID,否则返回新的ID。如果没有空闲的,则routineLoadTaskInfo重新返回队列,return。
15. 尝试分配一个事务,事务超时时间与第10点一致。检查事务限制时,如果是routine load,就不检查了,一般直接取于第7点的限制。如果事务分配不成功,就重新放回队列,return。
16. 根据上面获取的资源配置和本身的属性值,routineLoadTaskInfo 实例调用RoutineLoadTaskInfo#createRoutineLoadTask()创建一个真正执行的任务。创建异常依然会把routineLoadTaskInfo放回队列,return。
17. 成功创建后调用RoutineLoadTaskScheduler#submitTask() 方法提交任务,这是一个同步操作,如果提交失败,则设置错误信息,然后等待事务自己超时。超时后的任务信息会被RoutineLoadScheduler#process()方法扫描到,重新返回队列。
18. 提交成功后,设置routineLoadTaskInfo的开始执行时间为当前时间。
19. 总结:
调度是按照并发任务数来的,而并发任务数受限于分区和配置。由于kafka的一个分区只能由一个消费者来消费,所以任务数要么小于kafka分区数,要么等于分区数。而任务是一个一个被调度过去的,而任务数量受限于BE的可执行routine load任务数的限制。所以,建议,如果数据量小的topic,使用一个并发,消费所有分区,数据量大的topic,并发数等于分区数。
调度有个比较关键的时间参数,max_batch_interval既是一个routine load能执行的最大时间,超过就不再继续拉数据,又是调度时间间隔的二分之一。因此,如果这个值设置为10s,那么如果kafka此时已经有很多数据,那么BE就持续拉,直到时间到或者数据量达到max_batch_rows or max_batch_size 就停止。那下一次消费这个分区的任务就需要再等10s。之前延迟很高的问题找到了,由于是不连续的手动发送,运气好遇上刚好到时间调度就延迟很低,运气不好就是10s后,而且还是配的20s…。当调为1s时,发送时间与查到数据的延迟就都在1s左右了。
最后,当间隔缩短,并发数提高时,一定要提高
BE: routine_load_thread_pool_size 每个BE的线程池,默认为10, FE: max_routine_load_task_num_per_be 每个BE分配的最大任务, BE: routine_load_consumer_pool_size routine load 所使用的 data consumer 的缓存数量 默认为10这几个参数。