同一批快递全堆在一个大格口里,谁来取都得先排队开锁;换个做法,给每个收件人单独一个格口,钥匙只有他自己有,取件这个动作就再也不需要锁了。Seastar 的内存分配器走的是后一条路,而且极端到把 shard 编号直接刻进了指针的高位。代价当然存在。这套做法把并发问题从分配器内部赶了出去,赶到了free()的入口处:小对象分配路径上一条原子指令都没有,跨 shard 释放却要走一条 CAS 慢路,对象还会滞留在别人的链表里,直到对方主动来收。这两件事是同一个决策的正反面,谁想抄这套设计,抄的应该是那个决策,不是 slab 的挂载规则。我们从一次跨 shard 的delete开始拆,一路拆到伙伴分配器里那行看不懂的位运算、小对象 slab 的挂载与归还规则、mbind的 first-touch 语义,以及一个没写进任何文档、由一个uint8_t悄悄定死的 span 上限。拆完你手里会多出三条能直接搬走的设计约束,也会看清这套分配器在什么场景下反而是负优化——包括我自己最不放心的那一处。这一段路不算好走,涉及地址空间编码、伙伴算法的位运算推导,还有几处得盯着汇编看的细节。一步一步来,每个机制我都会先让它跑起来、把结果摆出来,再回头解释为什么。1. 一次跨 shard 的 delete,把整个设计的前提抖了出来1.1 先制造一次意外我们不从概念开始,先写一段应该出事的代码,看它到底怎么出事。看看如下范例:
把 Shard 编号刻进指针高位!拆解 Seastar 零原子指令内存分配器
同一批快递全堆在一个大格口里,谁来取都得先排队开锁;换个做法,给每个收件人单独一个格口,钥匙只有他自己有,取件这个动作就再也不需要锁了。Seastar 的内存分配器走的是后一条路,而且极端到把 shard 编号直接刻进了指针的高位。代价当然存在。这套做法把并发问题从分配器内部赶了出去,赶到了free()的入口处:小对象分配路径上一条原子指令都没有,跨 shard 释放却要走一条 CAS 慢路,对象还会滞留在别人的链表里,直到对方主动来收。这两件事是同一个决策的正反面,谁想抄这套设计,抄的应该是那个决策,不是 slab 的挂载规则。我们从一次跨 shard 的delete开始拆,一路拆到伙伴分配器里那行看不懂的位运算、小对象 slab 的挂载与归还规则、mbind的 first-touch 语义,以及一个没写进任何文档、由一个uint8_t悄悄定死的 span 上限。拆完你手里会多出三条能直接搬走的设计约束,也会看清这套分配器在什么场景下反而是负优化——包括我自己最不放心的那一处。这一段路不算好走,涉及地址空间编码、伙伴算法的位运算推导,还有几处得盯着汇编看的细节。一步一步来,每个机制我都会先让它跑起来、把结果摆出来,再回头解释为什么。1. 一次跨 shard 的 delete,把整个设计的前提抖了出来1.1 先制造一次意外我们不从概念开始,先写一段应该出事的代码,看它到底怎么出事。看看如下范例: