thumbnail
VanillaRT 计算资源限制算法
由 ChatGPT 生成的文章摘要
本文介绍了正在开发的Minecraft服务器插件VanillaRT。该插件是一个体素模型光线追踪引擎。在测试过程中发现,复杂的渲染任务可能导致服务器崩溃,因此需要对光线追踪的资源进行限制,从而避免主线程卡顿。为了实现这一目标,文章提出了一种算法来限制Minecraft线程池中运行的任务数,并同时最大化资源的利用。该算法依靠监测服务器的TPS(每秒滴答数)来感知资源使用情况,确保TPS保持在一个设定的阈值以上。算法分为三个阶段:试探阶段,快速增加最大并发任务数;卡顿避免阶段,在TPS下降时调整增加速度;最终,当TPS连续降低时,迅速降低任务数以恢复服务器状态。该算法受TCP拥塞控制机制启发,旨在以动态方式维持资源使用率,同时避免服务器卡顿和崩溃。

一、背景

VanillaRT 是我们最近正在开发的一款 Minecraft 服务器插件,是一个体素模型光线追踪引擎。这是一张测试时的渲染效果:

最近测试时,发现有些复杂渲染任务会导致服务器崩溃。这是因为渲染场景时需要大量的运算,而 Minecraft 服务器有一个主线程(Server Thread),主线程卡顿会导致玩家体验大幅下降,甚至因为不能及时喂狗(feed)而触发服务器崩溃。因此我们需要保证主线程不卡顿的同时,尽可能大地使用服务器资源。

二、分析

服务器状态通过检测服务器的 TPS(Tick Per Second)感知到。一般地,TPS = 20 时服务器的运行状态良好,TPS < 18 时资源就有些告急,玩家可以感受到卡顿。所以,我们的算法应当保证 TPS 不能降得太低,以避免主线程卡顿和崩溃。

我们通过一种类似 TCP 拥塞控制的算法动态地根据当前 TPS 调整某个限制光线追踪计算资源的变量,我们先将它称为资源限制变量。我们的算法大致的思路是——在提交新任务时,先把需要计算的任务放入一个就绪队列,再调用一个检查并在此时可以提交此任务的函数。此外,在资源限制变量变大,或任意任务结束时,我们也调用上述函数尝试提交新的任务。

三、算法

这个变量应该是什么呢?直觉告诉我们同时计算的任务过多可能导致卡顿,因此我们先限制并发数。

(一)限制并发数

我们将那些已经提交给 CoroutineScope 且尚未退出的协程数定义为并发数,其统计方式类似下面这样:

private val coroutineScope: CoroutineScope
private val concurrentUnits: AtomicInteger

private fun submit(action: suspend CoroutineScope.() -> Unit): Job {
    concurrentUnits.incrementAndGet()
    return coroutineScope.launch {
        try {
            action()
        } finally {
            concurrentUnits.decrementAndGet()
        }
    }
}

当然我们不能简单地把所有提交的任务都通过上述 submit 函数提交,而是需要先放入一个 scheduledUnits 队列里,并在每个任务结束,或并发数提升时检查是否需要提交新的任务。其代码类似下面这样:

private val coroutineScope: CoroutineScope
private val concurrentUnits: AtomicInteger

private fun submit(action: suspend CoroutineScope.() -> Unit): Job {
    concurrentUnits.incrementAndGet()
    return coroutineScope.launch {
        try {
            action()
        } finally {
            concurrentUnits.decrementAndGet()
            trySubmitScheduledUnits()
        }
    }
}

private val scheduledUnits: Deque<suspend CoroutineScope.() -> Unit>

fun schedule(action: suspend CoroutineScope.() -> Unit) {
    scheduledUnits.add(action)
    trySubmitScheduledUnits()
}

fun trySubmitScheduledUnits() {
    while (true) {
        val unit = scheduledUnits.poll() ?: return
        if (canSubmitNow()) {
            submit(unit)
        } else {
            scheduledUnits.addFirst(unit)
            return
        }
    }
}

函数 canSubmitNow 的逻辑我们暂且不谈。让我们注意新增的 trySubmitScheduledUnits,它不断地从 scheduledUnits 取出第一个节点并尝试提交,如果当前资源不足则重新将其加入队列并返回。

值得一提的是,我们实际上不需要同时有多个 trySubmitScheduledUnits 执行,这只会增加队列操作失败重试的次数。所以我们用一个布尔值保证最多只有一个线程调用它:

private val trySubmitScheduledUnitsLock = AtomicBoolean()

fun trySubmitScheduledUnits() {
    if (trySubmitScheduledUnitsLock.compareAndSet(false, true)) {
        // Try submit scheduled units.
        trySubmitScheduledUnitsLock.set(false)
    }
}

(二)限制任务对象数

实际上限制并发数并不能解决服务器卡顿的问题。但是在持续卡顿很长时间后会 OOM,这启发我们卡顿的原因不是并发数太高,而是任务对象占据的资源太多。因此我们打算也限制任务对象数。

具体地,我们希望在任务数足够时 schedule 立即返回,不够时挂起提交任务的协程,直到有任务数时恢复。

TODO

Not by AI

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇