本文是数人云“分布式架构的开源组件大选”Meetup的实录分享。分享嘉宾是来自数人云的资深架构师春明。这次的分享带来一些好用的分布式小工具——gRPC、Raft、Actor,如何利用它们更好地做分布式应用开发,快来一睹为快吧!
今天的话题是一款基于Mesos的开发组件。数人云最近研发了一款分布式方面的应用,Mesos调度器的Framework——Swan。这次演讲与大家分享一下数人云用到的一些分布式小工具,以及一些开发上面的最佳实践。
gRPC——分布式RPC的库
gRPC是一款RPC的框架,类似于CS的程序或者是P2P的程序,gRPC非常有帮助。gRPC由两个部分组成,一是协议、定义部分,用Protobuf来做协议的定义;第二是它的通信,目前采用HTTP2,没有采用TCP或者UDP。
gRPC的优点
gRPC有几个优点:
第一,在服务定义方面很简单,可以很轻松地搭建出一个RPC的调度或者是P2P相互之间调度的框架。
第二, gRPC是语言无关、平台无关的,它有很多种不同的实现。如果写好一个Protobuf协议定义,可以轻松地去生成不同语言的协议框架。
第三,它支持双向的流的调用,在做一个分布式系统或者是相互调用的系统时,如果有流的概念是比较容易的。
在使用Protobuf做协议通信之前,许多人倾向于采用一些XML,JSON去定义消息的格式,或者在C语言里面定义struct或java class去定一个协议,它们都有一个缺点,即不能在跨语言之间形成一个非常统一的序列化反序列化。所以Protobuf的出现定义一种消息格式,可以生成不同的语言,使用者再写一个针对不同语言的库去序列化或者反序列化这种结构。
下面为大家展示一个Protobuf的例子,上图中required表明这个字段是必须的,optional表示这个字段可有可无(新的语法在Protobuf3的 optional已经被去掉了)。里面没有体现的是repeated,repeated特别适合把数组放在这里。这个结构只是单层的,Person里面只有string和int32,它实际上还支持自定义的数据结构,把自定义的数据结构嵌起来。大Field后面的1、2、3数字,很难在其他语言里面见到这种写法,但在Protobuf很关键,它根据string占多少、int32占多少,以及这些索引定位字段。
一个游戏Server的例子
接下来我用亲身经历向大家展示在做网络应用时,gRPC使用之前和之后的差别。这个工具虽小,但是对大家的开发非常有帮助。
开发一个CS结构应用或者是P2P的应用,如果有相互之间的请求调用或者是方法调用,首先会考虑几件事情,第一件事是网络通信,选择TCP、UDP还是HTTP,在传输上采用什么协议,XML还是JSON等等。根据每个项目会有不同的选择,比如HTTP更倾向于非常简单的请求应答的场景,一些流的应用更多考虑TCP或者UDP的应用。在很多游戏里面,比如端游UDP采用得比较多,为了更高的性能,能容忍UDP产生的一些乱序的问题或者其它的问题。而传输协议的选择,也有很多选择方面的平衡。
2012年我参与了一款游戏后端的设计,采用的一个技术栈是Protobuf在TCP上的传输,语言和框架是NodeJS,当时没有gRPC非常痛苦。这种类型的游戏特点是许多玩家在同一个屏幕里,不同的玩家有不同的操作,上面很多AI不断的移动、不断的掉落或者是打斗。要把所有的客户端怪物的移动或者是人的移动都传输到后端平台,在另外一个玩家的屏幕上反馈出来,它的特点是移动的包和心跳的包多且频繁,占据了超过50%以上的流量。这一类包体积特别小,只要说明哪一个AI大致在什么位置。另一个特点是心跳包特别频繁,为了保证反馈尽量实时,所以心跳包基本在50毫秒或30毫秒。
当时选择TCP+Protobuf这种形式。如上图所示,首先把一个AI移动的包写成一个Protobuf,然后把它一个一个排在TCP的管道上。要写入这个包有多长,接着序列化之后的Protobuf包放在后面,接下来是第二个包,第三个包,依次排下去。在接收端从内核里拿到TCP的包,它与在发送端的发的包大小未必一致,因为内核会等待用户空间的包足够大再一起发出去,这是Nagle协议决定的。所以这时候在接收端要处理这种场景,把多个TCP的包黏起来。比如期望20个比特,第一个TCP包只来了15个比特,另外5个比特在下一个TCP的包,要把这些TCP的包黏起来,所以这些小细节都是没有用框架的时候需考虑到的。
最常用的做法是把内核上来的一些小Protobuf的包拷贝到程序里面,然后做解析、做相应的处理。这时又带来一个问题,需要频繁地去内存申请小的内存空间来存储从内核里流过来的包,程序在不停地申请,申请一段空间然后再释放一段空间,对GC来说非常有挑战。这时需要考虑一些其它的技术,比如预先申请,在heap里申请空间预留着,尽可能重用。
自从有了gRPC以后,就不再需要去考虑上面这些细节。
gRPC Demo举例
上图展示如何通过gRPC搭建一个CS的程序。前两个message,第一步是发一个request,第二个是response,核心的定义一个gRPC的service在下面。可以看到有rpc的关键字,比如数人云举办会议,想要Apply,参数是MeetupRequest,希望返回值是MeetupResponse。
接下来Protobuf Definition会生成几个部分代码,第一部分,前两段消息结构定义,Go语言会生成两个Go的struct,同时还有一些struct对应的方法。另外,service部分会生成两个代码,一个是server端代码,server需要有一个handler去处理apply的请求,在Client端会发出这个要求。
protoc是一个小工具,能把一个Protobuf的定义转化成不同语言。它支持了非常多的语言,比如Java、PHP、C++、.net等等,它同时支持一些plugin,plugin能解析比如上一步RPC的那个关键字,同时生成一些server框架性的代码。
这是request和response生成以后的Go的代码,刚才那步定义了两个结构体。 Protobuf其相当于协议的定义了,它支持从别的地方import,即能达到一定的重用。一旦能够重用,就可以组合成非常多有意思的东西。
这是Protobuf生成了Client端的代码。核心是apply的方法,它接受了context,context是Golang常见的概念,对一些IO的调用或者是数据库的调用,它可以传一些value进来或者说是从外面cancel处理。
然后看第二个参数,在协议定义里的request。它期望response是一个meetup response,里面做的事情是gRPC调用了server端一个RPC的地址。从这里可以看出来是HTTP的请求,第二点应该是一个Path。这是server生成的一段小的代码, Meetupserver其实是接口,这就提供了一些实现上的扩展性,只要实现apply的方法。
这段例子是server端的小例子, Meetup实现了apply的方法,于是它实现了上一步见到的MeetupServer的接口。最下面的第五行,关键是把写的一个小server注册上,很像一个HTTP的handler。最终搭建了简单的、根据protobuf的定义生成一个小server。
上图是一段client的程序, gRPC很常见,最常见的是net.dial,相当于客户端去连服务端,再返回来连一个connection。后面的参数其实可变长的,这里面有很多选项,可以参考一下文档。
以上演示了一个最简单的gRPC调用小工具,工具虽小,但是可以节省大量的时间。
Raft——分布式Consensus的算法
为什么选择Raft
数人云在做Swan的时候,希望它是高可用的。很多人对Ratf的翻译是“分布式系统一致性”算法,“一致性”并不太精准,一致性对应另外一个词就是consistency。 而Raft是consensus,几个节点通过某种协议达到了一致性,大家对Raft协议本身想了解更多的话,这里两个地址,第一个是Raft的一个论文,第二个是GitHub上Raft动画演示,详细的展示Raft的选举和log replicates步骤。
Raft帮助解决什么问题呢?比如它一部分的服务倒掉了,不会影响整个的服务。众所周知的CAP原则,作为分布系统来说,从用户的角度来看,第一个原则尽可能是一致性,比如说先写一次,紧接着去读它,希望它的结果是我们期望的;第二个是可靠性;第三个Partition tolerance,一部分服务异常,不要影响整个的服务。大家在写分布式的时候尽量满足这3个原则。
写分布式核心的时候程序在一个节点或者是多个节点上执行,实际上是有状态的state machine,尽可能保证对state machine一致性,如果不能保证一致性的话,尽可能保证它的最终一致性。如何保证一致性?state machine通过log entry,把log entry apply到多机的state machine上来,以此保证一致性。
Raft的功能特点
Raft的feature,第一是在整个集群或者集群是一部分能选出一个leader;第二个功能是复制log并且保证log的复制过程中是安全的有序的;第三,如果在新加入节点到一个集群时,非常多日志需要同步到新的节点,它有日志压缩的feature。
Raft目前很多实现,各种语言都有不同的实现,第一个是被认为最广泛的go-raft实现,第二个是Consul的实现,第三个是Etcd的实现。Etcd的实现是go-raft的基础之上完成的。我们最终选的是Etcd Raft的实现。原因如下:第一非常流行,声称目前使用人数最多的也最稳定,它做过很多的优化,代码很多处理非常优雅;第二是因为它的算法部分和周边松耦合,算法的使用者可以实现自己的存储模式和传输上的协议;第三用户群体广泛,SwarmKit, Etcd本身,以及PingCap的TIPV等等。
Raft使用举例
举几个使用Raft的例子。第一个例子由Etcd提供(https://github.com/coreos/etc... ),第二个是SwarmKit的实现(https://github.com/docker/swa... )。
上图展示如何启动一个Raft node的例子,可以看一下关键逻辑是怎么处理的,有些数据是如何存在Ratf cluster并且让它持久化的。第一行说明需要一个storage,Raft算法层面和存储的层面是分离的。从第二行设置启动一个Raft node的一些配置,electionTick表示Raft做选举时Interval是多少。 接下来第三个参数是从Raft一个leader节点到心跳节点,第三个存储,第四个、第五个限制了消息传递,最后一个是如何raft node的节点。第二个参数说明在启动一个集群的时候,另外两个节点ID是多少。Ratf能在运行时添加一个节点。用SwarmKit做的话,可以添加无限多的节点,并且可以把一个node的节点promote成manager节点,在SwarmKit里 manager节点就是参与Raft选举节点,也就是可以在运行时增删一个集群的节点。
上图是一段伪代码,模拟的是一个ratf node起来之后需要做哪些相应的处理。比较关键的是s.node ready,每个term,ready会提供一些数据,包括rd state当前的节点是什么样的状态,第二个说明里面有哪些log entry没有在persist里,需要persist掉。第三个是snapshot,raft的log compaction,做一些log的压缩。send做的操作是从一个节点到其他节点发送数据。对于for循环,rd committed entries是整个的关键,message可以apply到State Machine里。node.advance节点可以进行下一步的操作。
这条语句表明如何把自己的data去放到raft cluster做存储,这个数据其实没有真正的存储,只是异步第一步。
刚才讲到raft cluster在运行时可以增删这个节点,confChange的结构可以有几大类型,节点要增加,移除或者有变化,某节点的增删信息放到cluster进而通知其他的节点。
Actor模式和事件驱动
分布式系统最复杂的部分不在于选择协议、传输,或是哪一个小工具,而是写一段大型的异步、分布式的代码,如何组织代码就需要一个指导来构建架构,这部分我们参考了actor。
过去写代码很多人倾向于写一个网络服务的代码,数据包来了就解包,然后放到不同的线程中,在这个过程中,更多是不停地修改一些共享的存储,如果是面向对象,在object的外面修改field或者通过方法修改它,调来调去发现有点混乱,尤其是更多的人投入进来的时候,大家不知道如何去协调通信,也不知道是否存在锁的问题,非常麻烦。
什么是actor?写一个数据包,封装了一个golang的goroutine或者Erlang的的process,这本身是一段代码,数据也放在那里,通过一个mailbox,以此达到操作数据在不同的object,尽可能的减少互相修改field,减少锁的问题,每个actor接收到发送到自身的消息,做出相应反应。此外actor把数据和operation放在一起,尽量和其他的actor做隔离。
Erlang 的OTP里面的gen-server是actor典型应用之一。这里展示了一个最简单的gen-server。里面最核心的是后面接口部分, call和 cast,call是发一个同步的请求,cast是发一个异步请求。同步的请求是希望actor给 response,cast是发一个message不需要response。
数人云开源项目
下面介绍数人云两个开源项目,分别是容器管理面板Crane和Mesos调度器Swan。Crane是数人云做的Docker UI的项目,如果大家用过Docker UCP,Shipyard都可以考虑一下Crane,除了Docker集群的UI以外,还有Registry集成、账号相关的功能以及周边的feature。数人云之前用了很长时间的Marathon,后来扩展方面有些困难,于是有了Swan的诞生,把一些代理功能、proxy功能、DNS的功能、一容器一IP的需求等都放在了Swan里面,欢迎大家去提一些意见。
今天就给大家介绍到这儿,谢谢大家。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。