Minecraft服务端并行化利器:Folia

Folia简介

Folia

Folia是PaperMC组织下的一个项目,旨在为Minecraft服务器提供真正的多线程和区域化的区块更新机制。这个项目由著名的Minecraft优化专家Spottedleaf发起,开发始于2020年8月。Folia通过一系列的前置补丁,如Starlight、玩家区块地图(player chunkmap)以及区块重写(chunk rewrite),解决了长期以来限制服务器扩展的一些问题。

在Folia中,邻近的已加载区块被组合成independent region(独立区域),每个region都有自己的更新循环,以常规的Minecraft更新速率(20 TPS)运行。不同region的更新循环在一个可配置的线程池中并行执行。

Folia已经成功在一些大型服务器上进行了测试,(如2b2t和DonutSMP)。2b2t升级到1.19版本后,由于采用了Folia而运行得非常流畅;DonutSMP则部署了多个Folia实例来支持其庞大的SMP网络,每天能承载数千名玩家在线,处理超过46万个实体。

想要更深入地了解Folia,可以阅读它的官方仓库

这是一篇详细介绍Folia的文章:https://www.paper-chan.moe/folia/,但是没有涉及太多的技术细节。

Paper Chan hideout

关键概念——独立区域

在Folia中,邻近的已加载区块被组合成所谓的independent region(独立区域)。这一机制是Folia实现真正多线程的关键部分。独立区域是指一组相邻已加载区块位置以及与之相关的唯一数据对象。我们可以得出其重要性质:不会同时有两个活跃区域持有同一个区块。

如果两个独立区域的边界接近,则它们可能会合并成为一个更大的区域。反之,如果一个大区域内区块之间的距离增加,那么该区域可能会分裂成更小的独立区域。

每个独立区域有自己的更新循环(tick loop),按照Minecraft的标准更新速率(20 TPS)进行更新,并且每个区域独立维护自己的下一次tick的时间点。这些更新循环是在一个多线程池中并行执行的,因此不存在一个统一的“主线程”来处理所有区块的更新。每个独立区域不是拥有一个独立的线程,而是共享一个多线程池中的线程资源。

对于玩家空间分布较广的服务器,Folia就可以创建许多分散的独立区域,并且并发更新这些区域,这比原版中一次更新一个世界的顺序方法要高效多了。在CPU核心数充足的情况下(Folia在至少拥有16个物理核心的CPU上运行效果最佳),这种设计可以带来显著的性能提升。

Folia区域示意图1

Folia区域示意图2

实现细节

Folia核心架构图

为了便于表述,本章节以下出现的所有“区域”一词,均指代“独立区域”。

独立区域的创建、合并和分离是由Regioniser根据基础的区域化逻辑自动处理的。我们可以把活动的区域想象成气泡,当两个气泡靠近时,它们会合并成一个更大的气泡。每个世界都拥有自己的Regioniser,其主要职责是创建、维护和销毁区域。维护过程主要有三个:合并附近的区域、标记哪些区域可以进行tick处理、将某些区域分裂成更小的独立区域。

Regioniser提供的保证:

  • 第一不变量:不会同时有两个活跃区域持有同一个区块。
  • 第二不变量:对于区域内的每一个区块x,其周围一定范围内的区块都属于同一个区域,以确保一个区域在执行tick时不会受到其他区域的影响。
  • 第三不变量:正在执行tick的区域不能在这个过程中扩展其所拥有的区块范围。
  • 第四不变量:区域只能处于四种状态之一:“暂存”、“就绪”、“tick中”或者“死亡”(”transient”, “ready”, “ticking”, or “dead”)。

在具体的世界分块过程中,Regioniser会将世界分割成多个区域,每个区域为一个NxN的区块网格,其中N是2的幂。例如,当N=16时,区域区块坐标(0,0)包含了所有x在[0,15]和z在[0,15]范围内的区块。

对于一个非tick中的非死亡区域x,可以向tick中的区域y发起稍后合并请求。该操作记录在x和y的待合并集合中。当y完成tick处理后,会执行所有待合并的操作。

任何正在进行tick处理的区域必须最初拥有一个小的外围缓冲区,这些额外的区块保证了在tick处理过程中,该区域不会与其他区域产生直接的相邻关系,以满足第二不变量,从而避免竞态条件。而且区域不得在有相邻的直接邻居区域的情况下开始tick处理,可能也会影响彼此的数据一致性。