这篇文章是2017年第一篇,也是服务端系列的最后一篇。
上篇文章,我们讨论了RPC框架,从应用层的角度描述了服务端的不同节点之间如何交互。
回顾一下。
假设有App1、App2。
App1提供了一组服务Service(Service.Method1、Service.Method2等)。
App2调用Service.Method1。可以直接在代码中这样写:
Service.Method1(a,b,c);
如此一来,就把「打包」、「发消息」这两个流程,合并成了一个普通的「函数调用」。
在应用层,我们可以用比较统一的形式进行服务请求,而完全不需要关注底层通信细节,
假设服务端的所有通信情景都只涉及两方——也就是App2调用App1提供的服务的前提是,App2与App1建立了直接连接,那么只有RPC这一种「规范」性质的概念就足够了。
但是,小说君在之前的文章中提到过「基础设施抽象」的概念,比如「网关」, 比如「消息队列」,比如「数据服务」, 比如「分布式协调组件」。
通常的设计中,业务节点并不会与其他业务节点(或者客户端)直连。
因此,我们也不太可能用一套统一的RPC规范就适应一切通信问题。
比如,节点A借助网关向固定数量的客户端组播消息,节点B借助分布式协调组件向多个节点推送消息。
诸如此类应用情景,我们都需要另一种与RPC平行的抽象来特化RPC的定义形式。
抛开形而上的讨论,其实我们需要的是一种分类方式,能区分不同类型的RPC——不同的定义方式,以及不同的调用方式。
小说君借用一个比较通俗的词来描述这种抽象:「pattern」。
什么是pattern?
程序员最开始接触的「pattern」往往都是「design pattern」,设计模式。pattern的含义正如「design pattern」中的pattern——描述的是一些业界经验性质的针对特定问题的解决方案。
ZeroMQ guide的全书都在强调「pattern」,而且书的开头就介绍了ZeroMQ的四种built-in patterns:
Request-reply
Pub-sub
Pipeline
Exclusive pair
其中除了第四种是用来描述线程间通信的,与今天的主题关联不是特别大,其余三种都是服务端程序员的老朋友了。
zguide描述的是message patterns,而每个RPC事实上也是一个message,因此我们在做pattern的划分时,也可以借鉴zguide的message pattern划分。
接下来,小说君就根据不同的业务情景,定义不同的pattern。
首先是最常见的业务情景,两个节点的RPC调用。
不论是客户端调用服务端的服务,还是服务端之间互调服务,RPC都与本地方法调用类似,有时关注方法的执行结果,有时不关注方法的执行结果。
对于前者,无论方法是否有返回值,都是语义上的同步执行。也就是如果将逻辑分为两部分,逻辑2的开始执行,是依赖于方法调用的执行完成的。
而对于后者,则是语义上的异步执行。执行图变为:
同步语义,就是最典型的Request-reply pattern。调用方的一次request,必然对应唯一的一次服务方的reply。
RPC层面可以做一些处理,收到reply之后,在应用层还原request时的执行现场。
这种语义的应用范围非常广泛,从在线编辑的一次保存,搜索引擎到一次搜索,到游戏中的一次抽卡,都是Request-reply。
异步语义,命名多种多样,小说君习惯称之为Sync,表示一次数据同步。
数据同步通常可以区分为有源和无源,业务情景是两个节点,那数据同步自然是有源的。
这种语义的应用范围较之Request-reply,就少了不少。web中可能业务情景局限于服务端间调用,而在游戏中的应用反而要比Request-reply多。比如使用了个物品,单播的使用效果就是一次Sync调用。
然后是稍微复杂一些的业务情景,涉及多个节点的RPC调用。
我们之前引入的两种pattern:Request-reply和Sync,都有一个特点,那就是服务提供方定义服务接口,服务调用方调用接口。
但是有些时候,需求正相反:服务调用方需要定义服务接口,服务提供方选择性实现接口。
举个例子,服务端的行为采集系统。同一个节点,既要触发行为事件,又要定义事件源的类型(服务定义)。而事件触发了之后如何记录log,是服务提供方的服务实现逻辑。
这种pattern小说君习惯称为Notify,当然其本质就是pub-sub,服务调用方调用RPC,而不关注后续是否会被处理、会如何被路由——如果有节点实现了该服务,就做处理;如果没有,消息就被废弃。
pub-sub在实现上会dup每条消息,也就是服务调用方的RPC会被复制,每个服务提供方都会收到相同的RPC。
因此我们还需要有不做dup的版本,小说君称之为Distribute。
Distribute与Notify的唯一区别就在于是否对消息做dup,在具体业务应用中,Distribute可以实现定制化的分发策略,从最简单的round-robin,到稍微复杂些的一致性哈希,都能按需实现。
现在,我们初步定义了四种pattern:
Request-reply
Sync
Notify
Distribute
由于zguide确实太经典,我们的这几种pattern其实并没有跳出zguide的框架,如Notify之于pub-sub,Distribute之于pipeline。
但是,如前所述,这四种pattern的作用是用来描述服务的类型——pattern与RPC,共同描述了一个服务应该如何声明,如何调用。
借助这四种pattern定义,我们确定了客户端与服务、服务与服务的有限种交互形式。
现在,有了pattern和RPC,我们就能定义各种基于各种基础设施抽象的服务。
不同的基础设施抽象可以实现不同的pattern子集,如果需要新增加一类基础设施,我们可以看它的功能分别可以映射到哪几种pattern上,而对于应用层,只需要关注pattern,而无须关注某个服务底层需要经过哪个基础设施抽象。
继续举例子,网关和消息队列实现了不同的pattern集合。
虽然网关本质上也是一种消息队列,但是由于我们对两者的定位不同,所以限定了各自支持的pattern。
比如网关不支持Notify和Distribute,消息队列不支持Sync。
同时,基础设施抽象由于其底层机制,即使是同样支持某种pattern,可能实际的支持特性也不尽相同。
比如网关和消息队列同样都支持Request-reply,消息队列都能提供QoS1的支持,网关由于性能考量通常只支持到QoS0。
同理如基于redis做的数据服务与消息队列,两者都能实现Notifty的pattern,但是前者也是只能支持QoS0,后者可以支持QoS1。
有些组件,比如分布式协调器,虽然实现上可以支持各种pattern,但是这样做既难以理解又没什么收益,所以只要支持Notify这一种特定的pattern就能发挥相当大的作用了,比如处理很多无状态服务的服务发现和配置下发。
服务端系列文章告一段落,各篇链接:
服务的设计模式(本篇)
个人订阅号:gamedev101「说给开发游戏的你」,聊聊服务端,聊聊游戏开发。