31个稳定版本
新版本 2.0.7 | 2024年8月24日 |
---|---|
2.0.3 | 2024年7月20日 |
1.18.22 | 2024年8月9日 |
1.18.20 | 2024年7月24日 |
0.0.0 | 2023年12月1日 |
#638 in 神奇豆
9,046 个月下载量
在 16 个包中使用(通过 solana-unified-scheduler-…)
635KB
13K SLoC
统一调度器的任务(交易)调度代码
高级API和设计
最重要的类型是 SchedulingStateMachine
。它接收新任务(=交易)并在保持账户只读/可写锁规则的情况下,通过 ::schedule_task()
返回它们,如果它们是可运行的。那些返回的可运行任务是保证可以并行执行的。最后,SchedulingStateMachine
应该通过 ::deschedule_task()
通知执行完成,以便可以将冲突任务从 ::schedule_next_unblocked_task()
返回为新的可运行任务。
该包(solana-unified-scheduler-logic
)的设计原则是关注点的分离。它只通过少数公共API与solana-unified-scheduler-pool
交互。这个包对银行、槽位、solana-runtime、线程、crossbeam-channel一无所知。正因为如此,它是确定性的、易于单元测试的,并且它的性能影响得到了很好的理解。它真正专注于它的单一任务:按可执行顺序排序交易。
算法
该算法可以理解为基于每个地址的FIFO队列,每当有新任务到来(即称为调度)或者可运行的任务完成(即称为取消调度)时,这些队列都会被更新。
对于非冲突调度的情况,过程非常简单;它只需要记住所有访问的地址都已经被写锁或读锁,并且锁的数量与活跃任务(即当前已调度但尚未取消调度的)的数量相同。相应地,取消调度执行相反的账本管理过程,无论完成的任务是否发生冲突。
对于冲突调度的情况,它记住每个非冲突地址,就像上面提到的非冲突情况一样。至于冲突地址,每个任务都会记录到附加到(冲突的)地址的相应FIFO队列中。重要的是,还要记住冲突任务的冲突地址数量。
最后缺失的部分是,调度器在取消调度时,除了上述账本管理处理外,还会尝试重新调度之前被阻塞的任务。也就是说,当给定的地址因为取消一个任务而准备好新的锁(即写锁被释放或读锁计数达到零)时,它就会从该地址的FIFO阻塞任务队列中弹出第一个元素。然后,它立即将该地址标记为重新锁定。它还会减少弹出任务的冲突地址数量。最后一步是,如果数量达到零,这意味着任务已经完全锁定了所有地址,可以直接进入可运行状态。最后,如果阻塞任务队列的下一个第一个元素正在尝试锁定该地址,就像弹出的那个一样,这种重新调度会重复进行,作为提高任务执行并行性的优化。
换句话说,这个算法试图在尽可能不同的时间逐步锁定所有任务地址,同时不偏离原始任务摄入顺序的执行顺序。这表明通常没有锁定重试,这是非线性性能退化的主要来源。
根据对mainnet-beta
验证器的常规CPU的合成微基准测试,调度和取消调度包含10个账户的交易大约需要100纳秒。包含100个账户的交易需要1微秒。请注意,这不包括所有交叉beam通信开销。也就是说,如果交易执行没有成为瓶颈,那么整个统一的调度器总体上达到100k-1m tps是现实的。
运行时性能特性和数据结构排列
它的算法在高速率下非常快,在低延迟下是实时的。整个统一的调度器架构是从零开始设计的,以支持此调度代码的最快执行。为此,统一的调度器预先加载所有交易账户的地址特定锁定状态数据结构(称为UsageQueue
),以便从调度器线程卸载工作。这种预加载是在create_task()
中完成的。这样,任务调度的计算复杂度基本上降低到调度器线程中的几个字大小的加载和存储(即常量;没有分配也没有系统调用),而与给定交易中的地址数量成比例。请注意,无论是否存在冲突,这个陈述都是成立的。这是因为预加载还预先分配了一些备用区(blocked_usages_from_tasks
)来存储被阻塞的地址。因此,冲突只会引起一些额外的固定数量的内存存储,在常量复杂度的误差范围内。如果发生这种不寻常的事件,备用内存分配也可以说是摊销的。
使用 [Arc
] 来实现预加载机制,因为 UsageQueue
在访问相同账户的任务之间共享,以及在预加载的情况下在线程之间共享。此外,还需要内部可变性。然而,SchedulingStateMachine
不使用传统的 RwLock 锁。由于它是最唯一的状态可变独占线程,它使用 UnsafeCell
,并由一个定制的包装器 TokenCell
包装。通过 Rust 类型系统强加过度限制的别名规则来维持内存安全。TokenCell
通过将任何同步局部化到消息传递,调度代码本身实现了可能的最大单线程执行,而不会在任何地方停滞 CPU 流水线,只受限于内存访问延迟,同时高效地利用充满 UsageQueue
的 L1-L3 CPU 缓存。
缓冲区膨胀的无足轻重
调度代码本身并不关心可能出现在统一调度器中的缓冲区膨胀问题,其中大量线性化和阻塞的任务运行可能会因为大量并发可运行任务而严重受损。原因仍然是关注点的分离。这是可以接受的,因为如上所述的描述和上面提到的基准测试所验证的,调度代码本身并不容易受到缓冲区膨胀问题的困扰。因此,这个问题应该在别处解决,特别是在调度池中。
依赖项
~12–20MB
~293K SLoC