RustCon Asia 实录 | Distributed Actor System in Rust

做者介绍: Zimon Dai,阿里云城市大脑 Rust 开发工程师。前端

本文根据 Zimon 在首届 RustCon Asia 大会上的演讲整理。编程

你们好,我今天分享的是咱们团队在作的 Distributed Actor System。首先我想说一下这个 Talk 「不是」关于哪些内容的,由于不少人看到这个标题的时候可能会有一些误解。安全

<center>图 1</center>服务器

第一点,咱们不会详细讲一个完整的 Actor System 是怎么实现的,由于 Actor System 有一个很完善的标准,好比说像 Java 的 Akka, Rust 的 Actix 这些都是很成熟的库,在这里讲没有特别大的意义。第二,咱们也不会去跟别的流行的 Rust 的 Actor System 作比较和竞争。可能不少人作 Rust 开发的一个缘由是 Rust 写的服务器在 Techpower 的 benchmark 上排在很前面,好比微软开发的 Actix,咱们以为 Actix 确实写的很好,而咱们也没有必要本身搞一套 Actix。第三,咱们不会介绍具体的功能,由于这个库如今并无开源,但这也是咱们今年的计划。网络

这个 Talk 主要会讲下面几个方向(如图 2),就是咱们在作一个 Actor System 或者你们在用 Actor System 相似想法去实现一个东西的时候,会遇到的一些常见的问题架构

<center>图 2</center>异步

首先我会讲一讲 Compilation-stable 的 TypeId 和 Proc macros,而后分享一个目前尚未 Stable 的 Rust Feature,叫作 Specialization, 最后咱们会介绍怎么作一个基于 Tick 的 Actor System,若是你是作游戏开发或者有前端背景的话会比较了解 Tick 这个概念,好比作游戏的话,有 frame rate,你要作 60 帧,每帧大概就是 16 毫秒,大概这样是一个 Tick;前端的每个 Interval 有一个固定的时长,好比说 5 毫秒,这就是一个 Tick。分布式

1. The TypeId Problem

<center>图 3</center>ide

首先讲一下 TypeId。如图 3 ,好比说咱们如今已经有了两个Actor,它们多是在分布式系统里面的不一样的节点上,要进行网络传输。这个时候你能想到一个很简单的方式:Actor A 经过机器的 Broker A 发了一个消息,这个消息经过网络请求到达了另外一个 Broker B,经过这个 Broker B,把这个 Buffer 变成一个 Message 给了目标 Actor B,这是一个常见的网络通讯。fetch

<center>图 4</center>

可是这里面会有一个问题,好比,咱们要进行网络通信的时候,咱们其实是把他编译成了一个没有信息的 Buffer,就是一个 Vec<u8>,Message 自己是有 Type 的(由于Rust 是强类型的语言,Rust 中全部东西都是有类型的)。怎么把这个信息抹掉,而后当到了目标 Actor 的时候,再把这个类型恢复回来?这是咱们今天要讲 TypeId 的问题。

1.1 常见的解决办法

有一个很常见的解决方法,就是给每个 message 的消息头里加上这个 message 的类型描述,你们能够看下图是一段我写的伪代码:

<center>图 5</center>

最重要的就是第一个 field,叫作 type_uid,这个 Message 里 payload 具体是什么类型。若是咱们给 Actor System 里每个消息类型都赋予一个独特的 TypeId,那么就能够根据  TypeId 猜出来这个 Message 的 payload 具体是什么东西。第二个  field 就是 receiver,其实就是一个目标的 address。 第三个是一个 Buffer,是经过 serialization 的 Buffer。

如今咱们把这个问题聚焦到一个更小的具体问题上:咱们怎么给每一个消息类型赋予一个独特的 TypeId?恰好 Rust 有一个东西能够作这个事情——std::any::Any(图 6)。

<center>图 6</center>

Rust 里面全部的类型都实现了 Any 这个 Trait, 它有一个核心方法,叫作 get _type_id,这个方法刚刚在上周 stable。对任何一个类型调用这个方法的话,就能获得一个独特的 TypeId,它里面是一个 64 位的整数。

有了 TypeId 以后,你们能够想一下对 TypeId 会有什么样的要求?下图中我列举了一些最重要的事情

<center>图 7</center>

首先,这个 TypeId 要对全部的节点都是一致的。好比你有一个消息类型, TypeId 是 1,但在另外一个节点里面 1 这个整数可能表示的是另外一个消息类型,若是按照新的消息类型去解码这个消息的话,会出现解码错误。因此咱们但愿这个 TypeId 是在整个 Network 里面都是稳定的。这就致使咱们并不可使用 std 提供的 TypeId。由于很不幸的是 std 的 TypeId 是跟编译的流程绑定的,在你每次编译时都会生成新的 TypeId,也就是说若是整个网络里部署的软件正好是来自两次不一样的 Rust 编译的话,TypeId 就会有 mismatch。

这样就会致使一个问题:即使是更新了一个小小的组件,也可能要从新编译整个网络,这是很夸张的。因此咱们如今是利用 Proc Macro 来得到一个稳定的 TypeId 从而解决这个问题。

1.2 Proc Macro

其实这也是社区里面一个很长久的问题,大概从 2015 年左右就有人开始问,特别是不少作游戏编程的人,由于游戏里 identity 都须要固定的 TypeId。

<center>图 8</center>

这个问题怎么解决呢?很简单,用一个很粗暴的方式:若是咱们可以知道每个消息名字 name,就能够给每个 name 分一个固定的整数 id,而后把这个组合存到一个文件里,每次编译的时候都去读这个文件,这样就能够保证每次生成的代码里面是固定的写入一个整数,这样 TypeId 就是固定的。

咱们怎么作到在编译的时候去读一个文件呢?其实如今几乎是惟一的方法,就是去用 Proc Macro 来作这事。咱们看一下这边咱们定义了(图 9)一个本身的 TypeId 的类型:

<center>图 9</center>

UniqueTypeId 这个 Trait 只有一个方法,就是获取 Type-uid,至关于 std 的 Any; struct TypeId 内部只有一个 field,一个整数 t, TypeId 就至关于 std 的 TypeId。

<center>图 10</center>

图 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,就能够解决这个问题。

<center>图 11</center>

得到固定的 TypeId 以后,就能够用来擦除 Rust 中的类型。能够经过 serde 或者 Proto Buffer 来作。把 TypeId 序列化成一个 Buffer,再把 Buffer 反序列化成一个具体的 Type。

<center>图 12</center>

前面讲了一种方法,根据 Buffer header 的 signature 猜 Type 类型。这个方法总体感受很像 Java 的 Reflection,就是动态判断一个 Buffer 的具体类型。具体判断可能写这样的代码依次判断这个 message 的 TypeId 是什么(如图 12),好比先判断它是不是 PayloadA 的 TypeId,若是不是的话再判断是不是 PayloadB 的 TypeId……一直往下写,可是你这样也会写不少不少代码,并且须要根据全部的类型去匹配。怎么解决这个问题呢?咱们仍是要用 Proc Macro 来作这个事情。

<center>图 13</center>

如图 13,咱们在 Actor 里定义一个 message 叫作 handle_message,它内部实际上是一个 Macro,这个 Macro 会根据你在写这个 Actor 时注册的全部的消息类型把这些 if else 的判断不停的重复写完。

<center>图 14</center>

最后咱们会获得一个很是简单的 Actor 的架构(如图 14)。咱们这里好比说写一个 Sample Actor,首先你须要  customer derive Actor,它会帮你实现 Actor 这个 Trait。接下来要申明接收哪几种消息,#[Message(PayloadA, PayloadB)] 表示 SampleActor 接收的是 PayloadA 和 PayloadB,而后在实现 Actor 这个 Trait 时,customer derive 就会把 if else 类型匹配所有写彻底,而后只须要实现一个 Handler 的类把消息处理的方法再写一下。这样下来整个程序架构会很是清晰。

<center>图 15</center>

总的来讲,经过 Proc Macro 咱们能够获得一个很是干净的、有 self-explaining 的 Actor Design,同时还能够把 Actor 的声明和具体的消息处理的过程彻底分割开,最重要的是咱们能够把不安全的 type casting 所有都藏在背后,给用户一个安全的接口。并且这个运行损耗会很是低,由于是在作 integer comparison。

2. Specialization

第二个议题是介绍一下 Specialization,这是 Rust 的一个尚未进入 Stable 的 Feature,不少人可能还不太了解,它是 Trait 方向上的一个重要的 Feature。

<center>图 16</center>

图 16 中有一个特殊的问题。若是某个消息是有多种编码模式,好比 Serde 有一个很流行的编码叫 bincode(把一个 struct 编码成一个 Buffer),固然也有不少人也会用 Proto-buffer,那么若是 Message 是来自不一样的编码模式,要怎么用一样的一种 API 去解码不一样的消息呢?

<center>图 17</center>

这里须要用到一个很新的 RFC#1212 叫作 Specialization,它主要是提供两个功能:第一个是它可让 Trait 的功能实现互相覆盖,第二个是它容许 Trait 有一个默认的实现。

<center>图 18</center>

好比说咱们先定义了一个 Payload(如图 18),这个 Payload 必须支持 Serde 的 Serialization 和 Deserialization, Payload 的方法也是常规的方法,Serialize 和 Deserialize。最重要的是默认的状况下,若是一个消息只支持 Serde  的编码解码,那咱们就调用 bincode。

<center>图 19</center>

这样咱们就能够写一个实现(图 19),前面加一个 Default,加了 Default 以后,若是一个 struct 有这几个 Trait 的支持,那他就会调用 Default。若是多了一个 Trait 的话,就会用多出来的 Trait 的那个新方法。这样你们就能够不断的去经过限制更多的范围来支持更多 Codec。

Specialization 这个 feature,如今只有 nightly 上有,而后只须要开一个 #![feature(specialization)] 就能够用。

3. Tick-based actor system

<center>图 20</center>

下面来介绍一下 Tick-based actor system,就是咱们怎么在一个基于 Tokio 的 actor system 上面实现Tick,你们都知道  Tokio  是异步的架构,可是咱们想作成基于 Tick 的。

Tick 有哪些好处呢?首先 Tick 这个概念会用在不少的地方,而后包括好比说游戏设计、Dataflow、Stream computation(流式计算),还有 JavaScript 的 API,也有点 Tick 的 感受。若是整个逻辑是基于 Tick 的话,会让逻辑和等待机制变得更加简单,同时也能够作 event hook。

<center>图 21</center>

具体作法其实很简单。咱们能够设计一个新的 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 里面没有的东西。

<center>图 22</center>

好比在图 22 中列举的,第一点 actor system 不多会容许等待别的 actor,可是基于 Tick 的架构是能够作的,好比设置 deadline 等于 1,表示在下一个 Tick 执行以前,必须得收到这个消息,实际上就实现了一种 actor 之间互相依赖消息的设置。第二个,咱们还能够作 pre-fetch,好比如今要去抓取一些资源作预存,不会马上用这个资源,这样当我真正使用这些资源的时候他能够很快获得,那么能够设置一个比较“遥远”可是没有那么“遥远”的 deadline,好比设置 1000 个 tick 以后,必须拿到一个什么东西,实际上这个消息的 fetch 会有比较大的时间容错。

4. 总结

<center>图 23</center>

最后总结一下咱们的 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、图像处理等等跨行业、跨领域的应用实践。

大会 Talk 视频合集

相关文章
相关标签/搜索