做者介绍: Zimon Dai,阿里云城市大脑 Rust 开发工程师。前端
本文根据 Zimon 在首届 RustCon Asia 大会上的演讲整理。编程
你们好,我今天分享的是咱们团队在作的 Distributed Actor System。首先我想说一下这个 Talk 「不是」关于哪些内容的,由于不少人看到这个标题的时候可能会有一些误解。安全
第一点,咱们不会详细讲一个完整的 Actor System 是怎么实现的,由于 Actor System 有一个很完善的标准,好比说像 Java 的 Akka, Rust 的 Actix 这些都是很成熟的库,在这里讲没有特别大的意义。第二,咱们也不会去跟别的流行的 Rust 的 Actor System 作比较和竞争。可能不少人作 Rust 开发的一个缘由是 Rust 写的服务器在 Techpower 的 benchmark 上排在很前面,好比微软开发的 Actix,咱们以为 Actix 确实写的很好,而咱们也没有必要本身搞一套 Actix。第三,咱们不会介绍具体的功能,由于这个库如今并无开源,但这也是咱们今年的计划。服务器
这个 Talk 主要会讲下面几个方向(如图 2),就是咱们在作一个 Actor System 或者你们在用 Actor System 相似想法去实现一个东西的时候,会遇到的一些常见的问题。网络
首先我会讲一讲 Compilation-stable 的 TypeId 和 Proc macros,而后分享一个目前尚未 Stable 的 Rust Feature,叫作 Specialization, 最后咱们会介绍怎么作一个基于 Tick 的 Actor System,若是你是作游戏开发或者有前端背景的话会比较了解 Tick 这个概念,好比作游戏的话,有 frame rate,你要作 60 帧,每帧大概就是 16 毫秒,大概这样是一个 Tick;前端的每个 Interval 有一个固定的时长,好比说 5 毫秒,这就是一个 Tick。架构
首先讲一下 TypeId。如图 3 ,好比说咱们如今已经有了两个Actor,它们多是在分布式系统里面的不一样的节点上,要进行网络传输。这个时候你能想到一个很简单的方式:Actor A 经过机器的 Broker A 发了一个消息,这个消息经过网络请求到达了另外一个 Broker B,经过这个 Broker B,把这个 Buffer 变成一个 Message 给了目标 Actor B,这是一个常见的网络通讯。异步
可是这里面会有一个问题,好比,咱们要进行网络通信的时候,咱们其实是把他编译成了一个没有信息的 Buffer,就是一个 Vec,Message 自己是有 Type 的(由于Rust 是强类型的语言,Rust 中全部东西都是有类型的)。怎么把这个信息抹掉,而后当到了目标 Actor 的时候,再把这个类型恢复回来?这是咱们今天要讲 TypeId 的问题。分布式
有一个很常见的解决方法,就是给每个 message 的消息头里加上这个 message 的类型描述,你们能够看下图是一段我写的伪代码:ide
最重要的就是第一个 field,叫作 type_uid,这个 Message 里 payload 具体是什么类型。若是咱们给 Actor System 里每个消息类型都赋予一个独特的 TypeId,那么就能够根据 TypeId 猜出来这个 Message 的 payload 具体是什么东西。第二个 field 就是 receiver,其实就是一个目标的 address。 第三个是一个 Buffer,是经过 serialization 的 Buffer。fetch
如今咱们把这个问题聚焦到一个更小的具体问题上:咱们怎么给每一个消息类型赋予一个独特的 TypeId?恰好 Rust 有一个东西能够作这个事情——std::any::Any(图 6)。
Rust 里面全部的类型都实现了 Any 这个 Trait, 它有一个核心方法,叫作 get _type_id,这个方法刚刚在上周 stable。对任何一个类型调用这个方法的话,就能获得一个独特的 TypeId,它里面是一个 64 位的整数。
有了 TypeId 以后,你们能够想一下对 TypeId 会有什么样的要求?下图中我列举了一些最重要的事情:
首先,这个 TypeId 要对全部的节点都是一致的。好比你有一个消息类型, TypeId 是 1,但在另外一个节点里面 1 这个整数可能表示的是另外一个消息类型,若是按照新的消息类型去解码这个消息的话,会出现解码错误。因此咱们但愿这个 TypeId 是在整个 Network 里面都是稳定的。这就致使咱们并不可使用 std 提供的 TypeId。由于很不幸的是 std 的 TypeId 是跟编译的流程绑定的,在你每次编译时都会生成新的 TypeId,也就是说若是整个网络里部署的软件正好是来自两次不一样的 Rust 编译的话,TypeId 就会有 mismatch。
这样就会致使一个问题:即使是更新了一个小小的组件,也可能要从新编译整个网络,这是很夸张的。因此咱们如今是利用 Proc Macro 来得到一个稳定的 TypeId 从而解决这个问题。
其实这也是社区里面一个很长久的问题,大概从 2015 年左右就有人开始问,特别是不少作游戏编程的人,由于游戏里 identity 都须要固定的 TypeId。
这个问题怎么解决呢?很简单,用一个很粗暴的方式:若是咱们可以知道每个消息名字 name,就能够给每个 name 分一个固定的整数 id,而后把这个组合存到一个文件里,每次编译的时候都去读这个文件,这样就能够保证每次生成的代码里面是固定的写入一个整数,这样 TypeId 就是固定的。
咱们怎么作到在编译的时候去读一个文件呢?其实如今几乎是惟一的方法,就是去用 Proc Macro 来作这事。咱们看一下这边咱们定义了(图 9)一个本身的 TypeId 的类型:
UniqueTypeId 这个 Trait 只有一个方法,就是获取 Type-uid,至关于 std 的 Any; struct TypeId 内部只有一个 field,一个整数 t, TypeId 就至关于 std 的 TypeId。
图 10 上半部分有一个 Message 叫作 StartTaskRequest,这是咱们要使用的消息。而后咱们在上面写一个 customer derive。图 10 下半部分就是咱们真正去实现它的时候写的 Proc Macro,你们能够看到,咱们是用的 quote,里面是真正去实现前面咱们讲的 UniqueTypeId 的这个 Trait。而后里面这个 type_uid 方法他返回的 TypeId,其实是固定写死的。这个 t 的值是 #id,#id 能够在 customer derive 写的过程当中从文件中固定读出来的一个变量。
经过这种方法,咱们就能够固定的生成代码,每次就写好这个 Type,就是这个 integer,不少的 customer derive 可能只是为了简化代码,可是固定 TypeId 是不用 Proc macro 和 Customer derive 绝对作不到的事情。
而后咱们只须要在本地指定一个固定的文件,好比 .toml (图 10 右下角),让里面每个 message 类型都有一个固定的 TypeId,就能够解决这个问题。
得到固定的 TypeId 以后,就能够用来擦除 Rust 中的类型。能够经过 serde 或者 Proto Buffer 来作。把 TypeId 序列化成一个 Buffer,再把 Buffer 反序列化成一个具体的 Type。
前面讲了一种方法,根据 Buffer header 的 signature 猜 Type 类型。这个方法总体感受很像 Java 的 Reflection,就是动态判断一个 Buffer 的具体类型。具体判断可能写这样的代码依次判断这个 message 的 TypeId 是什么(如图 12),好比先判断它是不是 PayloadA 的 TypeId,若是不是的话再判断是不是 PayloadB 的 TypeId……一直往下写,可是你这样也会写不少不少代码,并且须要根据全部的类型去匹配。怎么解决这个问题呢?咱们仍是要用 Proc Macro 来作这个事情。
如图 13,咱们在 Actor 里定义一个 message 叫作 handle_message,它内部实际上是一个 Macro,这个 Macro 会根据你在写这个 Actor 时注册的全部的消息类型把这些 if else 的判断不停的重复写完。
最后咱们会获得一个很是简单的 Actor 的架构(如图 14)。咱们这里好比说写一个 Sample Actor,首先你须要 customer derive Actor,它会帮你实现 Actor 这个 Trait。接下来要申明接收哪几种消息,#[Message(PayloadA, PayloadB)] 表示 SampleActor 接收的是 PayloadA 和 PayloadB,而后在实现 Actor 这个 Trait 时,customer derive 就会把 if else 类型匹配所有写彻底,而后只须要实现一个 Handler 的类把消息处理的方法再写一下。这样下来整个程序架构会很是清晰。
总的来讲,经过 Proc Macro 咱们能够获得一个很是干净的、有 self-explaining 的 Actor Design,同时还能够把 Actor 的声明和具体的消息处理的过程彻底分割开,最重要的是咱们能够把不安全的 type casting 所有都藏在背后,给用户一个安全的接口。并且这个运行损耗会很是低,由于是在作 integer comparison。
第二个议题是介绍一下 Specialization,这是 Rust 的一个尚未进入 Stable 的 Feature,不少人可能还不太了解,它是 Trait 方向上的一个重要的 Feature。
图 16 中有一个特殊的问题。若是某个消息是有多种编码模式,好比 Serde 有一个很流行的编码叫 bincode(把一个 struct 编码成一个 Buffer),固然也有不少人也会用 Proto-buffer,那么若是 Message 是来自不一样的编码模式,要怎么用一样的一种 API 去解码不一样的消息呢?
这里须要用到一个很新的 RFC#1212 叫作 Specialization,它主要是提供两个功能:第一个是它可让 Trait 的功能实现互相覆盖,第二个是它容许 Trait 有一个默认的实现。
好比说咱们先定义了一个 Payload(如图 18),这个 Payload 必须支持 Serde 的 Serialization 和 Deserialization, Payload 的方法也是常规的方法,Serialize 和 Deserialize。最重要的是默认的状况下,若是一个消息只支持 Serde 的编码解码,那咱们就调用 bincode。
这样咱们就能够写一个实现(图 19),前面加一个 Default,加了 Default 以后,若是一个 struct 有这几个 Trait 的支持,那他就会调用 Default。若是多了一个 Trait 的话,就会用多出来的 Trait 的那个新方法。这样你们就能够不断的去经过限制更多的范围来支持更多 Codec。
Specialization 这个 feature,如今只有 nightly 上有,而后只须要开一个 #![feature(specialization)] 就能够用。
下面来介绍一下 Tick-based actor system,就是咱们怎么在一个基于 Tokio 的 actor system 上面实现Tick,你们都知道 Tokio 是异步的架构,可是咱们想作成基于 Tick 的。
Tick 有哪些好处呢?首先 Tick 这个概念会用在不少的地方,而后包括好比说游戏设计、Dataflow、Stream computation(流式计算),还有 JavaScript 的 API,也有点 Tick 的 感受。若是整个逻辑是基于 Tick 的话,会让逻辑和等待机制变得更加简单,同时也能够作 event hook。
具体作法其实很简单。咱们能够设计一个新的 struct,好比图 21 中的 WaitForOnce,首先声明一个 deadline,意思是在多少个 Tick 以内我必须得收到一个消息,而后能够提交这个消息的 signature。咱们在使用 Tokio 来进行 Network IO 时就能够生成一个 stream,把 stream 每次输出时 Tick 加 1,咱们就只须要维护一个 concurrent 的 SkipMap,而后把每个 Tick 的 waits 所有注册进来。当到达这个 Tick 时,若是该 Tick 全部的 waits 都已经覆盖到了,那你就能够 release 这个 feature,解决掉。
另外,经过 Tick 也能够去作一些 actor system 这个 spec 里面没有的东西。
好比在图 22 中列举的,第一点 actor system 不多会容许等待别的 actor,可是基于 Tick 的架构是能够作的,好比设置 deadline 等于 1,表示在下一个 Tick 执行以前,必须得收到这个消息,实际上就实现了一种 actor 之间互相依赖消息的设置。第二个,咱们还能够作 pre-fetch,好比如今要去抓取一些资源作预存,不会马上用这个资源,这样当我真正使用这些资源的时候他能够很快获得,那么能够设置一个比较“遥远”可是没有那么“遥远”的 deadline,好比设置 1000 个 tick 以后,必须拿到一个什么东西,实际上这个消息的 fetch 会有比较大的时间容错。
最后总结一下咱们的 Distributed Actor System 的一些特性,首先它是基于 Tick 的,而且能够经过 Specialization 支持多种不一样的 codecs,而后咱们能够经过 TypeId 实现相似 reflection 的效果。最后咱们计划在 2019 年左右的时候开源这个 actor system。其实咱们有不少系统和线上的业务都是基于 Rust 的,咱们也会逐渐的公开这些东西,但愿可以在从今年开始跟社区有更多的互动,有更多的东西能够和你们交流。
RustCon Asia 2019 年 4 月 23 日,由秘猿科技和 PingCAP 主办的 首届 RustCon Asia 在北京圆满落幕,300 余位来自中国、美国、加拿大、德国、俄罗斯、印度、澳大利亚等国家和地区的 Rust 爱好者参加了本次大会。做为 Rust 亚洲社区首次「大型网友面基 Party」,本届大会召集了 20 余位海内外顶尖 Rust 开发者讲师,为你们带来一天半节奏紧凑的分享和两天 Workshop 实操辅导,内容包括 Rust 在分布式数据存储、安全领域、搜索引擎、嵌入式 IoT、图像处理等等跨行业、跨领域的应用实践。