背景
背景是设计一个实时数据接入的模块,负责接收客户端的实时数据写入(如日志流,点击流),数据支持直接下沉到HBase上(后续提供HBase上的查询),或先持久化到Kafka里,方便后续进行一些计算和处理,再下沉到文件系统或做别的输出。
在设计中,对于客户端和服务端有这么些目标。
客户端需要支持多语言(Java,C++),做得尽量轻量级,只要连上服务端的ip:port,以RPC的形式调用简单的write就可以把数据写出去。客户端不承担任何逻辑的处理,服务端的负载均衡对客户端是透明的。
服务端想要做的是一个去中心化的节点集群,节点之间汇报各自的负载情况,每个节点能知道全局的负载情况,在接收客户端的连接请求的时候,能返回负载合适的节点让客户端往目标节点写。另外,服务端能容忍高并发的写入操作,某节点挂掉后要能让客户端选择新的低负载节点,不影响客户端的数据写入。服务端对业务系统的接入配置记录在DB里,能选择合适的处理逻辑,把数据写到HBase或kafka里。
设计
客户端与服务端使用Thrift来通信,由.thrift来约定写数据的接口和结构,客户端所要做的是从初始配置里选择一个可用服务节点,向其询问目前负载最低的节点进行数据写入,或者客户端本身选择服务节点的时候就是以偏随机和均衡的方式。这两种方式在后面讨论服务端实现的时候都会涉及到。
下面说说服务端的几种方案。
其实在规划的时候,服务端是往p2p大量节点的方向设计的,后续我又提出了些自己的看法,做了一些讨论。所以下面先介绍比较复杂些的实现方案。
一致性哈希+p2p广播 方案
如上图所示,绿色FeedsGroup是每个服务节点,具体是一个JVM进程,一台实体机器上可以部署多个。主要思路是通过Chord算法来维护和管理网络结构,通过类gossip的实现来广播节点的负载信息。Chord是一致性哈希的一种实现,是p2p里比较重要的一个算法。一致性哈希保证了整个网络结构和可扩展性,且客户端可以通过hash的方式比较随机分散地分布到整个哈希环上,让各个节点上的连接数比较平均。此外,Chord算法的每个节点维护了一个全局的Finger Table,这个Finger表是每个节点对整个网络的"全局视图",本身是用来做类二分的节点查找的,我们在这把它当作一个路由表,除了客户端可以通过访问一个节点找到自己落在环上的位置外,这个路由表还用来让节点们做gossip通信。gossip是p2p网络里在无中心情况下广播自己信息和获得其他节点信息的协议。实现方式有很多,在下面介绍纯gossip的实现方式的时候再具体介绍。关于chord算法的一些内容,可以参考我之前写的这篇文章。
后半部分是不同Sink目标,通过DB配置的方式为不同业务系统绑定不同的处理逻辑和数据下沉方案,包括具体schema的设计。
纯gossip广播 方案
纯gossip方案实现参考了Cassandra的实现方式,为每个FG进程配置了几个种子节点,即每个FG起来之后都会与种子节点交换自己的信息和其他节点的信息,然后通过种子节点维护的节点列表,再随机选取两个或三个节点进行通信。种子节点的设定避免的信息孤岛。Cassandra内gossip通信协议的实现说明可以参考DataStax的文章。
其实纯gossip实现和上面第一种实现相比,少了DHT(Distributed Hash Table)这一块,对于客户端来说,每次连接需要节点返回一个全局负载最低节点给他来进行数据写入。
小规模集群 基于zk实现方案
以上实现方式,归根到底是为了能承受客户端高并发的连接和写入需求,并且要做到可扩展,负载均衡和去中心化。且上面两种方案在成千上万个节点下面可能会比较有优势,因为chord和gossip本身的一些网络开销和信息维护都是适合于p2p网络下面大量节点的管理和维护。
在实际开发中,可能我们最多起到上百个节点,甚至几十个节点。我考虑了简化的实现方式,不过本身也存在争议 :)。
最简单的情况下,其实我们只有数据的写,没有第二次来同节点进行查询的请求。所以场景并不太像一致性哈希本身的数据存取场景,要简单很多。单纯是一个服务节点列表,不需要任何哈希环,也可以解决我们的写入需求。不过这样可能就没啥设计可言了。
我考虑在节点数目比较少的情况下,可以把节点在哈希环上的大整数值记录在zk上,如此就会在zk上维护一个类似哈希环的节点列表,且按顺序排列。节点之间维护一致哈希环也不需要额外的互相通信开销,而是直接从zk上进行读取操作,或者在zk上设置watch通知已有节点变更情况。以此换来的代价是,在zk上会有比较多的读请求,写请求非常少,zk充当了一个可靠的节点列表存储的地方,把网络通信的开销转移到了zk上面。在节点数目比较少的情况下,通过zk维护全局列表的方式可以完成哈希环的管理、客户端对节点的选取、节点之间互相广播通信的事情。
总结
在设计之初,我们也有考虑直接拿kafka来做写入的事情,客户端做个producer,让kafka来承担并发写和负载的事情也完全可以其实。不过考虑到实时接入场景的多样性,本文的实现满足的是直接同步写入的场景以及先持久化后计算的场景。
第一种方案在设计上比较优雅,而且会很适合大量节点数目的场景。
第二种方案产生的原因其实是,我们的写需求其实并不符合一致性哈希的场景,所以没有环这个东西也没啥问题,所以纯广播的方式照样是可以保证的。
第三种方案是我自己YY的,不过也是比较鸡肋,比较奇怪,既然写zk了,就算没有环这个概念,直接把存活节点记录在上面,通过让客户端取余或别的随机方案选择目标节点也完全可以,好像也没有必要搞成一个哈希值什么的了。我感觉如果单纯是一致性哈希算法的实现的话,基于zk上的这种实现方式在节点规模比较小的情况下应该还是可行的,只是通信量级会与节点数目成正比,在合理的设计下,感觉还是简单可行的一种实现方案。
全文完 :)
一个轻客户端,多语言支持,去中心化,自动负载,可扩展的实时数据写服务的实现方案讨论,布布扣,bubuko.com