(图片源自网络)
2 架构描述
简单架构
从之前的描述,已经可以看出我们会采用RPC over MQ的方式做底层实现,类似方法调用的通信语义会在client和server两端的库中作封装。
从后端实现来说,我们用三套后端来满足不同的场景:
1、对大中型分布式系统环境,rabbitmq是非常非常好的支撑。本来以为需要自己做很多工作,但深入了解rabbitmq,尤其是其支持的amqp协议,发现其实前人在很多思路方面已经栽好树了,比如一致性hash和跨机房等功能,都有相应的插件支撑。
所以,rabbitmq成为babel的第一选择,可以实现我们规划功能的全集,我们的SAAS平台都是使用的rabbitmq。
2、对少量机器而言,redis提供了非常轻量级的队列支持,可以提供有限但必要的功能。
redis没有类似amqp这样的协议,需要手动作些封装。我们在单机环境使用redis,尽可能减少部署和运维的开销。
3、对性能有苛刻要求的可以用zeromq后端去做tcp直连。前两种mq的方式毕竟会多几跳中转,但在路由的灵活性和通讯语义的提供更丰富的选择,而且在大数据量的处理上,吞吐量和平均延时并不会比直连差很多。
但为了满足特殊环境的需要,我们预留了zeromq的实现选择,最近由于新的需求,正在准备完成这块拼图。zeromq的缺点在于需要中央配置系统来帮忙完成路由功能。
每种后台实现对使用者透明,可以通过配置进行透明切换,但是有些高级通信语义redis和zeromq不支持。
如果对应到web service 三要素:
UDDI:传统的rpc或者SOA都是去注册中心发现远端对象,然后客户端主动推送数据到服务端。mq的方式帮我们省却了自注册(订阅实现)和服务发现(mq自己路由)的问题。
WSDL:目前我们通过json的方式来描述rpc的service端,包括机房所在地,持久化,超时等等。
SOAP:目前使用json的方式,我们定义了一个统一的Event对象来封装一些固定属性,其他都在一个map中。由业务代码自己去打包拆包。当然这种方式在大团队中不适合。
通讯语义封装
大量的工作可以利用mq来实现,我们的工作主要体现在通讯语义的封装。
❶ client端访问模式语义
queue语义(消息有去无回):传统的数据输送。
简单rpc(消息一去一回):传统的rpc和soa都适用于此场景。
轮询rpc(消息一去多回):一个request出去,多个response回来,适合于轮询下游节点的场景。
分布式存储rpc(一个request消息,只要有最小条件的response消息就返回):适合于分布式场景下的读写。例如三个拷贝,需要至少两份读成功或者至少两份写成功,等等。目前此方式我们还没有用到。
❷ 消息分发语义(实际上这里的行为参考了storm的部分功能)
Shuffle:一个消息,会有多个接收者,这些接收者根据自己的资源情况去抢占同一来源的消息,达到load balance的目的。实际上我们通过shuffle来做集群功能,省掉了LB的引入。而且性能强的拉多点,性能弱的拉少点,变相的实现了根据消费者的性能来做分发。
Sharding:与shuffle类似,也是多个consumer来分享消息,不过根据消息的key,保证在拓扑环境不发生改变的情况下,同一个key始终指向同一个消费者,为后续分布式系统的搭建打下基础
topic语义:所有消费者都会得到消息的一个拷贝。常见的mq语义
topic+shffule:一组消费者作为一个整体来订阅topic,得到所有的消息,每个订阅团体内部通过shuffle的形式去分摊。这种非常适合用大数据环境下,有不同类型的数据消费者,每一个类型的消费者有各自的实例数。
topic+sharding:一组消费者作为一个整体来订阅topic,得到所有的消息,每个订阅团体内部通过sharding的形式去分摊。类似于topic shuffle,只是换用了sharding这种更严格的语义。
❸ 数据的封装语义。用于指定babel上承载数据的特征,例如:
batch operation:用于指定是否进行批处理传递。
Security:暂无使用。
Compressing:指定payload压缩方法,目前只做了gzip。
机房:指定了机房所在地,框架会根据生产者和消费者的不同自动做跨机房的处理。
持久化:指定在无消费者的情况下,是否需要持久化存储,以及最大大小。
超时:指定消息的最大有效时间,超过的消息将会被丢弃。
其他。
对于以上的通讯语义,首先需要去底层的mq基础里面找到相对应的设施来做封装,比如对于queue语义作个简单举例:
而对于像rpc,轮询,以及其他功能,则需要相应的代码来支撑,比如:
response的返回可以通过client监听queue来实现
response和request的串联可以通过自定义的requestid来实现
轮询可以通过client 端等待多个消息返回,可以用condition来做同步
……
这里有不少细节,暂不在本文中进行展开了。
跨语言
由于几种mq都有python和java的客户端,所以我们工作会轻松很多,只是同样的逻辑需要写两份,好处还是很明显的,使得我们的系统语言无关,方便根据当前人员的技能情况来分配开发任务。
不过这里不得不吐槽python的并发,虽然有心理准备,没想到是如此之差。当使用多线程的时候,性能下降的厉害,比java要差两个数量级,所以我们python版做了同步(多线程),异步(协程)两个版本。异步版本的性能尚可接受;我们已经准备在build自己的异步python框架,来覆盖我们的应用程序。
跨机房通信
Babel的一大特色是跨机房通信,来帮助我们解决不同数据中心的通信问题,使得业务开发人员只用关心其所负责的业务即可。跨机房的通信和本机房的通信有所不同:
本地机房的通信讲究高吞吐量,rpc类访问会要求低延时。
跨机房通信必须应对复杂的网络情况,要求数据不丢,rpc类通信可以接受相对较高的延时。
实现上,我们利用了federation插件,当rpc框架发现存在跨机房访问时,会自动启用相应的路由,下图是同事画的两种情况下的路由,绿线是本地调用,红线是跨机房调用。
对于业务应用而言,使用上是基本透明的,借助于mq的中转,在多机房环境下它也可以玩转除数据推送外的RPC类访问语义。
3 实战举例
实例
1 分布式数据计算平台
首先是我们的私有化大数据平台warden。warden集数据采集、转换、分发、实时分析和展示等功能于一体,希望从客户的原始网络流量中找出异常点和风险事件。
此图是一个warden分布式版本的草图:
采集的数据通过topic sharding类分配给不同类型的消费者,比如ES writer,Mysql writer,实时分析引擎;每种消费者可以有不同的实例数。
实时计算引擎通过sharding来分摊流量,达到scale out的效果。
rule引擎需要数据的时候同样通过简单的sharding的rpc就可以获得相应的数据了。
规则引擎的结果可以通过topic来进行再分发。
目前只有实时引擎是java的,因为性能要求苛刻,其他模块采用python开发。
上图只是个例子,来简要说明babel是如何支撑一个分布式数据计算系统的。实际的系统使用了更多的语义,也更加的复杂。不过借助于Babel的协助,整个系统在实现和运维上已经很大程度上减轻了复杂程度。
2 水平扩展的web系统
第二个例子是我们曾经做过的SAAS平台私有化案例,是我们早期SAAS平台的极简版本。
图画的略凌乱了些:
系统主架构是用haproxy做负载均衡,发到我们的两台主机上。
两台主机内部完全相同,右边主机内部组件没有画全。
每台主机有内部的nginx,load balance到本机器内部的诸多python web server 上。
python web server直接读取本地的nosql数据库。
写数据时,由于写请求sharding到两台机器上,所以我们有个topic的service来处理nosql数据写入,保证每个写入操作都写到两台机器上,每台机器的nosql始终存有全量的最新数据。
由于客户要求落地关系型数据库,所以通过shuffle再将写请求分散开,统一写入mysql中。
在这个系统中,我们成功的利用babel建立了自己的一致性框架,从而避免了去使用db做数据一致性;同时由于对等的服务器架构,在部署维护上省掉了很多事情。
框架运维
整个框架,我们都准备了统一的metrics体系去做监控和报警(实际上metrics系统本身的跨机房属性反而是通过babel来实现),详尽的监视了RPC的某个环节,之前有过我们监控的文章,这里就不重复了。