作者:Simon,腾讯后台开发高级工程师
WeTest导读
分布式系统理念渐渐成为了后台架构技术的重要选择,本文介绍了作者在手游领域对分布式系统进行的种种尝试,并在尝试中制定了对服务的定义、整体框架的构建以及服务内部拆分的流程。
前言
业务规模不断扩大,对稳定性、扩展性的要求不断提高,推动了后台架构技术的不断革新。面对日益复杂的需求,分布式系统的理念也逐渐深入到后台开发者的骨髓。2013年,借着手游热潮我对分布式系统开始尝试。在近三年的摸爬滚打中,踩过不少坑,也从业界技术发展中吸取一些经验,逐渐形成了目前的设计思路。这里和大家分享点心得,不敢奢谈有多大参考价值,权当抛砖引玉吧。
1. 失败的首次尝试
最初考虑使用分布式的出发点很简单:解决端游开发时单点结构导致容灾、扩容困难的问题。一种朴素的想法就是将相同功能的进程作为一个整体对外提供服务。这里简要描述下基本框架:
这种架构提供了三个基本组件:
Client API, 服务请求者API:
Server API, 服务提供者API:
Cluster Center Server, 集群中心进程:
这种架构具备了集群的基本雏形,可以满足容灾扩容的基本需求,大家应该也发现不少问题,我这里总结几点:
1. 服务发现的蹩脚实现 Cluster Center Server 的实现是单点,出现故障时Client请求会异常;没有提供监控机制,Client只能通过定时请求来获取服务的最新状况。
2. CS采用Request/Response的通信方式不灵活 现实应用中,服务往往存在相互请求,一应一答远远不够,全双工 是必须要支持的。
3. 有瑕疵的保活机制 Server对Client定期单边心跳,有两个问题:不同Client对保活要求可能不同,有些5s,有些可能1s,如果心跳发起全部在Server,无法满足差异化要求;服务端作为被动方,承担监控请求者存活的责任不明智。
4. 架构设计的层次不清晰
对架构的层次、模块划分没有作出很好的规划,比如通信底层、服务发现、集群探测与保活等等没有清晰定义接口,导致相互耦合,替换、维护较为困难。
2. 看看外面的世界
上述问题,归根结底还是眼界狭窄,自己闷头造轮子没跟上业界技术发展的步伐。近几年微服务架构发展迅速,相比传统面向服务架构不再过分强调企业服务总线,而是深入到单个业务系统内部的组件化。这里我介绍下自己的调研结果。
2.1 服务协同
服务协同是分布式系统一个核心组成部分,概述为:多个进程节点作为整体对外提供服务,服务可以相互发现,服务关注者可以及时获取被关注者的变化以完成协作。具体运行过程包括:服务注册 和 服务发现。在实现上涉及以下方面:
业界中较为成熟的实现如下表所示:
2.2 消息中间件
亦称消息队列,在分布式系统广泛使用,在需要进行网络通信的节点间建立通道,高效可靠地进行平台无关的数据交流。架构上主要分为两种:Broker-Based(代理),和 Brokerless(无代理)。前者需要部署一个消息转发的中间层,提供二次处理和可靠性保证。后者轻量级,直接在内嵌在通信节点上。业界较为成熟的实现如下表所示:
2.3 通信协议数据格式
服务间通信,需要将数据结构/对象和传输过程中的二进制流做相互转化,一般称为 序列化/反序列化 。不同编程语言或应用场景,对数据结构/对象的定义和实现是不同的。在选择时需要考虑以下方面:
实现 序列化/反序列化 的组件一般包含:IDL(Interface Description Language), IDL Compiler, Stub/Skeleton。业界目前比较流行的序列化协议有:XML, JSON, ProtoBuf, Thrift, Avro等。关于这几种协议的实现以及比较,可以参考文章 《序列化和反序列化》。这里将原文中的选型结论摘录给大家:
3. 重整旗鼓
调研周边后,2015年开搞第二款手游,吸取之前的教训,这次设计的基本原则是:
下面首先对服务定义,然后介绍整体框架和服务内部拆分。
3.1 服务定义
举个手游的例子,看图说话:
上图中,Service Instance 完整路径可描述为:/AppID/Area/Platform/WorldID/GroupID/ClusterName/InstanceName。有以下特点:
3.2 服务发现基本流程
先抽象几个基本操作,不同服务发现组件的API可能略有差异,但应该有对应功能:
Service Instance 每次在启动时,按照下面的流程处理:
Service Instance 在关闭时,按照下面的流程处理:
根据上面的抽象可以定义 服务发现 的基本接口,接口的具体实现可以针对不同的组件开发不同的wrapper,但可以和业务解耦。
3.3 服务架构
所有的架构归根结底还是需要具体到进程层次实现的。目前我们项目开发的分布式架构组件称之为 DMS(Distributed Messaging System),以 DMS Library 的形式提供,集成该库即可实现面向服务的分布式通信。下面是 DMS 设计的总体结构:
关于Serialize/DeSerialize, APP业务的选择自由度较高,下面介绍其它Layer的具体实现:
3.3.1 Message Middleware
消息中间件前面介绍有很多选择。DMS 使用的是 ZeroMQ,出发点是:轻量级、性能强大、偏底层所以灵活而且可控性较高。由此带来的成本是,高级应用场景需要做不少二次开发,而且长达80多页的资料也需要不少时间。介绍ZeroMQ的文章太多,这里不打算科普,所以直接给出设计方案。
通信模式的选择
ZeroMQ的Socket有多种类型,不同组合可以形成不同的通信模式,列举几种常见的:
看到这里,大家可能会觉得选择PUB/SUB和DEALER/ROUTER应该可以满足绝大部分应用场景吧。实际上DMS只使用了一种socket类型,那就是ROUTER,通信模式只有一种ROUTER/ROUTER。一种socket,一种通信模式,听起来很简单,但真可以满足要求吗?
3.3.2 DMS Protocol
消息结构
DMS的协议实现集群管理,消息转发等基本功能。ZeroMQ的消息可以由 Frame 组成,一个Frame可以为空也可以是一段字节流,一个完整的消息可以包含多个Frame,称为Multipart Message。基于这种特点,在DMS定义协议,可以将内容拆分为不同的基本单元,每个单元用一个Frame描述,通过单元组合表示不同的含义。这与传统方式:一条协议就是一个结构体,不同单元组合需要定义为一个结构体的方式相比更加灵活。
下面来看看DMS Protocol的基本组成。首帧一定是对端ID。对端接收后也一定会获取信息发送端的ID。第二帧包含DMS控制信息。第三、第四帧等全部是业务自定义的传输信息,仅对REQ-REP有效:
PIDF有两层含义:所在服务集群的标记,自身的实例标记。这些标记与Service Discovery关于节点key的定义保持一致,有两种形式 字符串 与 整型,前者可读方便理解,后者是前者的Hash,提高传输效率。使用伪代码来描述PIDF,大概是下面的样子:
PIDF中的 ClusterID 和 InstanceID 各种取值,会有不同的通信行为:
在连接首次建立时,还需要将可读的服务路径传输给对端:
协议命令字
DMS协议全部在每个消息的第二帧即Control Frame中实现。命令字定义为:
通信流程——建立连接
通过 Service Discovery 找到server后不要立即连接,而是发送探测包。原因有以下几点:
通信流程——业务消息发送
路由 和 广播 是可以混合使用的。上述过程 DMS 自动完成,业务不必参与,但可以截获干预。
通信流程——保活机制
建立连接后,请求者会持续按照自己的间隔向服务者发送探测包。如果请求者连续若干次没有收到服务者的PONG回包,则请求者认为与服务者的连接已经断开。 如果服务者收到请求者的任何数据包,认为请求者存活,如果超出一定时间没有收到(含PING),则认为请求者掉线。这个超时时间包含在READY协议中,由请求者告知服务者。
通信流程——连接断开
任何一方收到 DISCONNECT 后,即认为对方主动断开连接,不要再主动向对方进行任何形式的通信。
3.3.3 DMS Kernel
下面介绍 DMS Kernel 如何根据 DMS Protocol 实现相关逻辑,并如何与业务交互。
SERVICE MANAGER
ROUTER MANAGER
每个服务实例在主动成功连接对端服务后,通过 SERVICE MANAGER 将连接以边的形式写入到 SERVICE DISCOVERY 中,这样就会以 邻接边 的形式生成一张完整的图结构,也就是routing table。比如: Service 1 和 Service 2,Service 3,Service 4 均有连接,那么将边(1,2),(1,3),(1,4) 记录下来。SERVICE DISCOVERY 关于路由邻接链表的记录可以使用公共的key,比如: /AppID/Area/Platform/routing_table 。然后所有的服务实例都可以更新、访问该路径以便获得一致的路由表。基础功能有两个:
CONNECTION MANAGER
管理 Frontends 即前端请求进入的连接,和 Backends 即向后端主动发起的连接。Backends的目标来源于 Service Manager。
3.3.4 DMS Interface
DMS API 是DMS对业务提供的服务接口,可以管理服务、通信等基本功能;
DMS APP Interface 是DMS要求业务必须实现的接口比如:Dispatcher 的负载均衡策略,对端服务状态变化通知,以及业务自定义 路由算法 等等。
3.4 应用场景
下面罗列DMS三大类典型应用场景,其它场景应该可以通过这三个例子组合实现:
最基础的通信方式——两个集群之间的 Instance 全连接,适合服务数量不多、逻辑不复杂的简单业务。
对于一个内部聚合的子系统,可能包含N个服务,这些服务之间相互存在较强的交互行为。如果使用无Broker模式可能有两个问题:链路过多:通信层的内存占用较大;运维维护困难;服务没有解耦,直接依赖于对端的存在; 这时Broker集群可以承担消息中转的作用,而且可以完成一些集中式逻辑处理。注意这里Broker只是一个名字,通过 DMS Library 可以直接实现。
多个子系统相互通信,估计没有设计者愿意把内部细节完全暴露给对方,这时两个Broker集群就相当于门户:首先可以实现内部子系统相互通信,以及集中逻辑;其次,可以作为所处子系统的对外接口,屏蔽细节。这样不同子系统只需通过各自的Broker集群对外提供服务即可。
总结
本文主要介绍了 DMS 的几个基础结构:服务发现、消息中间件以及通信架构。基本思想是:框架分层、层级之间接口清晰定义,以便在不同场景下使用不同的具体实现进行替换。其中 zookeeper,ZeroMQ 只是举例说明当前的一种实现方式,在不同场景下可以选择不同组件,只要满足接口即可。
关于腾讯WeTest
腾讯WeTest是腾讯游戏官方推出的一站式游戏测试平台,用十年腾讯游戏测试经验帮助广大开发者对游戏开发全生命周期进行质量保障。 腾讯WeTest提供:兼容适配测试;云端真机调试;安全测试;耗电量测试;服务器压力测试;舆情监控等服务。