Akka分布式游戏后端开发3 Actor设计

154 阅读6分钟

本专栏的项目代码在Github上,如果感兴趣的话,麻烦点点Star,谢谢啦。


在 Classic Akka 中,Akka 为我们提供了基础的 Actor 类 AbstractActor 供我们继承使用,还有其衍生的类型 AbstractActorWithXXX UntypedAbstractActor,带有 With 类型的,一般是混合了某种特质,例如 Stash,Timers,而 Untyped则是抹去了消息类型,提供了一个统一的接口 onReceive来供我们接收消息,消息的类型需要我们自己去处理。

在我们的设计中,Actor 既需要有 Stash 功能,也要有 Timers 的功能,因为 Actor 启动的时候,需要把消息暂存起来,然后等数据加载完成之后,才处理消息。定时器是必不可少的功能,许多业务都需要使用定时器。同时我们也要使 Actor 具备执行外部脚本的功能,这个非常重要,因为线上经常遇到打补丁、修玩家数据的情况。

但是 Akka 为我们提供的只有 With 单一特质的抽象 Actor,没有提供类似于 AbstractActorWithStashAndTimers 这种类型,所以需要我们自己再封装一下。

Kotlin 协程支持

在实际的业务中,我们经常会给不同的 Actor 发送 RPC 请求,为了避免回调地狱的出现,我们在 Actor 中引入了 Kotlin 的协程来避免这一种情况。

data class ActorCoroutineRunnable(val runnable: Runnable) : Message, Runnable by runnable

fun ActorRef.safeActorCoroutineScope(): TrackingCoroutineScope {
    val dispatcher = Executor { tell(ActorCoroutineRunnable(it)) }.asCoroutineDispatcher()
    return TrackingCoroutineScope(dispatcher + SupervisorJob())
}

class TrackingCoroutineScope(context: CoroutineContext) : CoroutineScope {
    private val job = Job(context[Job])
    override val coroutineContext: CoroutineContext = context + job

    private val jobs = ConcurrentHashMap.newKeySet<Job>()

    fun launch(
        context: CoroutineContext = EmptyCoroutineContext,
        start: CoroutineStart = CoroutineStart.DEFAULT,
        block: suspend CoroutineScope.() -> Unit
    ): Job {
        val newJob = (CoroutineScope::launch)(this, context, start, block)
        jobs.add(newJob)
        newJob.invokeOnCompletion { jobs.remove(newJob) }
        return newJob
    }

    fun getAllJobs(): Set<Job> = jobs
}

以上,我们会在 Actor 中启动一个 CoroutineScope ,里面默认的协程调度器就是 Actor 本身,我们向自己的 Actor 发送 Runnable 消息来驱动协程中不同状态的运行,这样就避免了 Actor 的线程安全问题。不过,我们无法避免一个 Actor 中的数据在挂起函数返回之后,仍然满足业务需要的这种情况,下面的代码会给一个类似的例子:

player.launch {
    val playerEntity = player.manager.get<PlayerMem>().player
    check(playerEntity.level == 10)//必须是要玩家等级为10才做某些操作
    /**
     *向World询问一些额外的状态,此函数流程被挂起
     */
    player.askWorld<XXXResp>(XXXAction(playerEntity.worldId))
    /**
     * 再次检查玩家等级为10级,才继续往下做操作,因为玩家等级可能在挂起的过程中改变
     */
    check(playerEntity.level == 10)
} 

为什么要这么设计?

有的人会说,在协程启动的时候,暂存其它类型的消息,协程执行完成之后,再处理其它消息不就好了。这确实是一种解决方案,但是会有一些致命的问题,例如因为 RPC 调用接收方因为异常没有给调用方回包,那么这个 Actor 就会一直卡住直到协程超时,这个时候从客户端的表现来说可能就是转圈,如果玩家等得不耐烦了,可能会选择重启游戏,但是这样并不能解决问题,因为这个 Actor 还是因为协程卡住的状态,所以玩家也无法登录。再如 Actor 会有一些后台逻辑,不只是处理玩家的请求,如果在启动协程的时候,整个 Actor 卡住,那么这些后台逻辑就不能得到执行,这样也是不行的。综上,我们选择不阻塞 Actor 执行的方式来启动协程,坏处是在挂起函数返回之后,一些业务逻辑判断可能需要再次判断一下。

基于以上思路,我们在 Actor 中封装两个用于启动协程的方法,与 Kotlin 自带的有所区别,加入了一个默认的超时时间,在实际的业务处理中,一般都要处理 RPC 超时这种情况。

fun launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    timeout: Duration? = 3.minutes,
    block: suspend CoroutineScope.() -> Unit
): Job {
    return if (timeout == null) {
        coroutineScope.launch(context, start, block)
    } else {
        coroutineScope.launch(context, start) {
            withTimeout(timeout, block)
        }
    }
}

fun <T> async(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> T
): Deferred<T> {
    return coroutineScope.async(context, start, block)
}

定时器设计

由于我们只能在 Actor 中混入一种特质,我们选择了 AbstractActorWithStash ,我们单独在这个 Actor 中启动一个子 Actor,继承 AbstractActorWithTimers 来实现定时器的功能。

data class TimersInteraction(val block: (TimerScheduler) -> Unit)

class TimersActor : AbstractActorWithTimers() {
    companion object {
        fun props(): Props = Props.create(TimersActor::class.java)
    }

    override fun createReceive(): Receive {
        return receiveBuilder()
            .match(TimersInteraction::class.java) { it.block(timers) }
            .matchAny { context.parent.tell(it, self) }
            .build()
    }
}

基于以上的需求,我们再次封装一次后的 Actor 如下

abstract class StatefulActor : AbstractActorWithStash() {
    val logger = actorLogger()
    val coroutineScope = self.safeActorCoroutineScope()

    private lateinit var timers: ActorRef

    override fun preStart() {
        super.preStart()
        timers = context.actorOf(TimersActor.props(), "timers")
    }

    fun cancel(key: Any) {
        val interaction = TimersInteraction { it.cancel(key) }
        timers.tell(interaction)
    }

    fun cancelAll() {
        val interaction = TimersInteraction { it.cancelAll() }
        timers.tell(interaction)
    }

    suspend fun isTimerActive(key: Any): Boolean {
        val channel = Channel<Boolean>(1)
        val interaction = TimersInteraction { channel.trySend(it.isTimerActive(key)) }
        timers.tell(interaction)
        return channel.receive()
    }

    fun startSingleTimer(key: Any, msg: Any, timeout: Duration) {
        val interaction = TimersInteraction { it.startSingleTimer(key, msg, timeout) }
        timers.tell(interaction)
    }

    fun startTimerAtFixedRate(key: Any, msg: Any, initialDelay: Duration, interval: Duration) {
        val interaction = TimersInteraction { it.startTimerAtFixedRate(key, msg, initialDelay, interval) }
        timers.tell(interaction)
    }

    fun startTimerAtFixedRate(key: Any, msg: Any, interval: Duration) {
        val interaction = TimersInteraction { it.startTimerAtFixedRate(key, msg, interval) }
        timers.tell(interaction)
    }

    fun startTimerWithFixedDelay(key: Any, msg: Any, initialDelay: Duration, delay: Duration) {
        val interaction = TimersInteraction { it.startTimerWithFixedDelay(key, msg, initialDelay, delay) }
        timers.tell(interaction)
    }

    fun startTimerWithFixedDelay(key: Any, msg: Any, delay: Duration) {
        val interaction = TimersInteraction { it.startTimerWithFixedDelay(key, msg, delay) }
        timers.tell(interaction)
    }
}

在主 Actor 中,封装一下 Timers 中的方法,转发给子 Actor 进行定时操作,子 Actor 定时完成之后,直接把消息发送给它的父 Actor,这样就能触发定时了。

Actor 脚本支持

我们不希望每增加一种类型的 Actor,都要去处理如何在 Actor 中执行脚本的问题,我们希望直接把这一部分封装好,新加的 Actor 直接继承这个抽象的 Actor,就可以获得这个功能了。为此,我们需要重写 Actor 中的部分方法,对于执行脚本部分的消息,直接拦截掉。

override fun aroundReceive(receive: PartialFunction<Any, BoxedUnit>?, msg: Any?) {
    when (msg) {
        is ActorCoroutineRunnable -> {
            handleRunnable<ActorCoroutineRunnable> { msg.run() }
        }

        is ActorNamedRunnable -> {
            handleRunnable<ActorNamedRunnable> { msg.block() }
        }

        is ExecuteActorScript -> {
            node.scriptActor.forward(CompileActorScript(msg.uid, msg.script, self), context)
        }

        is ExecuteActorFunction -> {
            try {
                msg.function.invoke(this, msg.extra)
                sender.tell(ExecuteScriptResult(msg.uid, true), self)
            } catch (e: Exception) {
                sender.tell(ExecuteScriptResult(msg.uid, false), self)
                logger.error(e, "{} failed to execute actor function", self)
            }
        }

        else -> {
            super.aroundReceive(receive, msg)
        }
    }
}
  • ExecuteActorScript 二进制的脚本文件,可以是jar或者groovy,需要先发送到 ScriptActor 进行处理,得到可执行的 Function,才能在 Actor 中进行执行。
  • ExecuteActorFunction 处理完之后的脚本 Function,在目标 Actor 中执行。
  • ActorCoroutineRunnable 协程支持部分,无关

最终 Actor 设计

abstract class StatefulActor<N>(val node: N) : AbstractActorWithStash() where N : Node {
    val logger = actorLogger()
    val coroutineScope = self.safeActorCoroutineScope()

    private lateinit var timers: ActorRef

    override fun preStart() {
        super.preStart()
        timers = context.actorOf(TimersActor.props(), "timers")
    }

    override fun aroundReceive(receive: PartialFunction<Any, BoxedUnit>?, msg: Any?) {
        when (msg) {
            is ActorCoroutineRunnable -> {
                handleRunnable<ActorCoroutineRunnable> { msg.run() }
            }

            is ActorNamedRunnable -> {
                handleRunnable<ActorNamedRunnable> { msg.block() }
            }

            is ExecuteActorScript -> {
                node.scriptActor.forward(CompileActorScript(msg.uid, msg.script, self), context)
            }

            is ExecuteActorFunction -> {
                try {
                    msg.function.invoke(this, msg.extra)
                    sender.tell(ExecuteScriptResult(msg.uid, true), self)
                } catch (e: Exception) {
                    sender.tell(ExecuteScriptResult(msg.uid, false), self)
                    logger.error(e, "{} failed to execute actor function", self)
                }
            }

            else -> {
                super.aroundReceive(receive, msg)
            }
        }
    }

    private inline fun <reified T> handleRunnable(runnable: () -> Unit) {
        try {
            runnable()
        } catch (e: Exception) {
            logger.error(e, "{} failed to execute {}", self.path(), T::class.java)
        }
    }

    fun cancel(key: Any) {
        val interaction = TimersInteraction { it.cancel(key) }
        timers.tell(interaction)
    }

    fun cancelAll() {
        val interaction = TimersInteraction { it.cancelAll() }
        timers.tell(interaction)
    }

    suspend fun isTimerActive(key: Any): Boolean {
        val channel = Channel<Boolean>(1)
        val interaction = TimersInteraction { channel.trySend(it.isTimerActive(key)) }
        timers.tell(interaction)
        return channel.receive()
    }

    fun startSingleTimer(key: Any, msg: Any, timeout: Duration) {
        val interaction = TimersInteraction { it.startSingleTimer(key, msg, timeout) }
        timers.tell(interaction)
    }

    fun startTimerAtFixedRate(key: Any, msg: Any, initialDelay: Duration, interval: Duration) {
        val interaction = TimersInteraction { it.startTimerAtFixedRate(key, msg, initialDelay, interval) }
        timers.tell(interaction)
    }

    fun startTimerAtFixedRate(key: Any, msg: Any, interval: Duration) {
        val interaction = TimersInteraction { it.startTimerAtFixedRate(key, msg, interval) }
        timers.tell(interaction)
    }

    fun startTimerWithFixedDelay(key: Any, msg: Any, initialDelay: Duration, delay: Duration) {
        val interaction = TimersInteraction { it.startTimerWithFixedDelay(key, msg, initialDelay, delay) }
        timers.tell(interaction)
    }

    fun startTimerWithFixedDelay(key: Any, msg: Any, delay: Duration) {
        val interaction = TimersInteraction { it.startTimerWithFixedDelay(key, msg, delay) }
        timers.tell(interaction)
    }

    fun launch(
        context: CoroutineContext = EmptyCoroutineContext,
        start: CoroutineStart = CoroutineStart.DEFAULT,
        timeout: Duration? = 3.minutes,
        block: suspend CoroutineScope.() -> Unit
    ): Job {
        return if (timeout == null) {
            coroutineScope.launch(context, start, block)
        } else {
            coroutineScope.launch(context, start) {
                withTimeout(timeout, block)
            }
        }
    }

    fun <T> async(
        context: CoroutineContext = EmptyCoroutineContext,
        start: CoroutineStart = CoroutineStart.DEFAULT,
        block: suspend CoroutineScope.() -> T
    ): Deferred<T> {
        return coroutineScope.async(context, start, block)
    }

    fun execute(name: String, block: () -> Unit) {
        self tell ActorNamedRunnable(name, block)
    }

    fun fireEvent(event: Event) {
        self tell event
    }
}

结尾

对于 ScriptActor 中的脚本加载和执行部分,后面会单独进行说明。