声明:本文来自腾讯质量开放平台WeTest原创投稿文章,未经许可禁止任何形式的转载。
作者: Simon,腾讯后台开发高级工程师。
责编: 钱曙光,关注架构和算法领域,寻求报道或者投稿请发邮件qianshg@csdn.net,另有「CSDN 高级架构师群」,内有诸多知名互联网公司的大牛架构师,欢迎架构师加微信qshuguang2008申请入群,备注姓名+公司+职位。
分布式系统理念渐渐成为了后台架构技术的重要选择,本文介绍了作者在手游领域对分布式系统进行的种种尝试,并在尝试中制定了对服务的定义、整体框架的构建以及服务内部拆分的流程。
业务规模不断扩大,对稳定性、扩展性的要求不断提高,推动了后台架构技术的不断革新。面对日益复杂的需求,分布式系统的理念也逐渐深入到后台开发者的骨髓。2013年,借着手游热潮我对分布式系统开始尝试。在近三年的摸爬滚打中,踩过不少坑,也从业界技术发展中吸取一些经验,逐渐形成了目前的设计思路。这里和大家分享点心得,不敢奢谈有多大参考价值,权当抛砖引玉吧。
最初考虑使用分布式的出发点很简单:解决端游开发时单点结构导致容灾、扩容困难的问题。一种朴素的想法就是将相同功能的进程作为一个整体对外提供服务。这里简要描述下基本框架:
这种架构提供了三个基本组件:
Client API,服务请求者API:
Server API,服务提供者API:
Cluster Center Server,集群中心进程:
这种架构具备了集群的基本雏形,可以满足容灾扩容的基本需求,大家应该也发现不少问题,我这里总结几点:
上述问题,归根结底还是眼界狭窄,自己闷头造轮子没跟上业界技术发展的步伐。近几年微服务架构发展迅速,相比传统面向服务架构不再过分强调企业服务总线,而是深入到单个业务系统内部的组件化。这里我介绍下自己的调研结果。
服务协同是分布式系统一个核心组成部分,概述为:多个进程节点作为整体对外提供服务,服务可以相互发现,服务关注者可以及时获取被关注者的变化以完成协作。具体运行过程包括:服务注册 和 服务发现。在实现上涉及以下方面:
业界中较为成熟的实现如下表所示:
亦称消息队列,在分布式系统广泛使用,在需要进行网络通信的节点间建立通道,高效可靠地进行平台无关的数据交流。架构上主要分为两种:Broker-Based(代理),和 Brokerless(无代理)。前者需要部署一个消息转发的中间层,提供二次处理和可靠性保证。后者轻量级,直接在内嵌在通信节点上。业界较为成熟的实现如下表所示:
服务间通信,需要将数据结构/对象和传输过程中的二进制流做相互转化,一般称为 序列化/反序列化。不同编程语言或应用场景,对数据结构/对象的定义和实现是不同的。在选择时需要考虑以下方面:
调研周边后,2015年开搞第二款手游,吸取之前的教训,这次设计的基本原则是:
下面首先对服务定义,然后介绍整体框架和服务内部拆分。
举个手游的例子,看图说话:
上图中,Service Instance 完整路径可描述为:/AppID/Area/Platform/WorldID/GroupID/ClusterName/InstanceName。有以下特点:
先抽象几个基本操作,不同服务发现组件的API可能略有差异,但应该有对应功能:
Service Instance每次在启动时,按照下面的流程处理:
Service Instance在关闭时,按照下面的流程处理:
根据上面的抽象可以定义 服务发现 的基本接口,接口的具体实现可以针对不同的组件开发不同的wrapper,但可以和业务解耦。
所有的架构归根结底还是需要具体到进程层次实现的。目前我们项目开发的分布式架构组件称之为 DMS(Distributed Messaging System),以 DMS Library 的形式提供,集成该库即可实现面向服务的分布式通信。下面是 DMS 设计的总体结构:
关于 Serialize/DeSerialize ,APP业务的选择自由度较高,下面介绍其它Layer的具体实现:
3.3.1 Message Middleware
消息中间件前面介绍有很多选择。DMS 使用的是 ZeroMQ,出发点是:轻量级、性能强大、偏底层所以灵活而且可控性较高。由此带来的成本是,高级应用场景需要做不少二次开发,而且长达80多页的资料也需要不少时间。介绍ZeroMQ的文章太多,这里不打算科普,所以直接给出设计方案。
通信模式的选择
ZeroMQ的Socket有多种类型,不同组合可以形成不同的通信模式,列举几种常见的:
REQ/REP 一应一答,有请求必须等待回应
PUB/SUB 发布订阅
PUSH/PULL 流水线式处理,上游推数据,下游拉数据
DEALER/ROUTER 全双工异步通信
看到这里,大家可能会觉得选择 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后不要立即连接,而是发送探测包。原因有以下几点:
a. 服务发现虽然可以反映节点是否存活,但一般有延迟,所以从服务发现获取的节点仅仅是候选节点。b. 网络底层机制差异较大,有些基于连接,比如raw socket,有些没有连接,比如shared memory。最好在高层协议中解决连接是否成功。这就好比声纳,投石问路,有回应说明可以连接,没有回应说明目前连接不可用。
a. 普通消息:若PIDF表示对端实例和当前进程直接连接,那么发送消息;
b. 路由消息:若PIDF表示对端实例和当前进程没有直接连接,那么可以通过直连的实例转发。路由机制 后文会介绍;
c. 广播消息:若PIDFInstanceID为负数,则向指定集群内所有实例广播。
路由和 广播 是可以混合使用的。上述过程DMS自动完成,业务不必参与,但可以截获干预。
建立连接后,请求者会持续按照自己的间隔向服务者发送探测包。如果请求者连续若干次没有收到服务者的PONG回包,则请求者认为与服务者的连接已经断开。
如果服务者收到请求者的任何数据包,认为请求者存活,如果超出一定时间没有收到(含PING),则认为请求者掉线。这个超时时间包含在READY协议中,由请求者告知服务者。
任何一方收到DISCONNECT后,即认为对方主动断开连接,不要再主动向对方进行任何形式的通信。
3.3.3 DMS Kernel
下面介绍 DMS Kernel 如何根据 DMS Protocol 实现相关逻辑,并如何与业务交互。
a. self 确定自身 服务路径,实现服务注册,以及与目标通信链路的注册,供路由表使用
b. targets 获取并监控目标服务的数据以及运行状态
c. ACL 访问控制管理
d. 对服务发现层接口进行封装,不同的 SERVICE DISCOVERY 功能可能有所不同
每个服务实例在主动成功连接对端服务后,通过 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 。然后所有的服务实例都可以更新、访问该路径以便获得一致的路由表。基础功能有两个:
a. Updater 用于向路由表中添加边,删除边,设置边的属性(比如权重),并对边的变化进行监控b. Calculator 根据邻接边形成的 图结构 计算路由,出发点是当前实例,给定目标点判断目标是否可达,如果可达确定路径并传输给下一个节点转发。默认选择 Dijkstra 算法,业务可以定制。
管理 Frontends 即前端请求进入的连接,和 Backends 即向后端主动发起的连接。Backends的目标来源于 Service Manager。
a. Sentinel 对前端发起的连接,通过 READY 协议,可以获取该连接的失活标准,并通过前端主动包来判断进入连接是否存活。如果失活,将该连接置为断开状态,不再向对应前端主动发包。
b. Prober 对后端服务进行连接建立和连接保活。
c. Dispatcher 消息发送时用于确定通信对端实例。连接是基于实例的,但是业务一般都是面向服务集群的,所以Dispathcer 需要实现一定的分配机制,将消息转发给 服务集群中的某个 具体实例 。注意这里仅只存在直接连接的单播。分配时应考虑 负载均衡 默认使用一致性哈希算法,业务完全可以根据具体应用场景自定义。
3.3.4 DMS Interface
DMS API 是DMS对业务提供的服务接口,可以管理服务、通信等基本功能;DMS APP Interface 是DMS要求业务必须实现的接口比如:Dispatcher 的负载均衡策略,对端服务状态变化通知,以及业务自定义 路由算法 等等。
下面罗列DMS三大类典型应用场景,其它场景应该可以通过这三个例子组合实现:
最基础的通信方式——两个集群之间的 Instance 全连接,适合服务数量不多、逻辑不复杂的简单业务。
对于一个内部聚合的子系统,可能包含N个服务,这些服务之间相互存在较强的交互行为。如果使用无Broker模式可能有两个问题:链路过多:通信层的内存占用较大;运维维护困难;服务没有解耦,直接依赖于对端的存在;
这时Broker集群可以承担消息中转的作用,而且可以完成一些集中式逻辑处理。注意这里Broker只是一个名字,通过 DMS Library 可以直接实现。
多个子系统相互通信,估计没有设计者愿意把内部细节完全暴露给对方,这时两个Broker集群就相当于门户:首先可以实现内部子系统相互通信,以及集中逻辑;其次,可以作为所处子系统的对外接口,屏蔽细节。这样不同子系统只需通过各自的Broker集群对外提供服务即可。
本文主要介绍了 DMS 的几个基础结构:服务发现、消息中间件以及通信架构。基本思想是:框架分层、层级之间接口清晰定义,以便在不同场景下使用不同的具体实现进行替换。其中 zookeeper,ZeroMQ 只是举例说明当前的一种实现方式,在不同场景下可以选择不同组件,只要满足接口即可。