转载

写了一个分布式名字服务JCM

之前在公司里维护了一个名字服务,这个名字服务日常管理了近4000台机器,有4000个左右的客户端连接上来获取机器信息,由于其基本是一个单点服务,所以某些模块接近瓶颈。后来倒是有重构计划,详细设计做了,代码都写了一部分,结果由于某些原因重构就被终止了。

JCM 是我业余时间用Java重写的一个版本,功能上目前只实现了基础功能。由于它是个完全分布式的架构,所以理论上可以横向扩展,大大增强系统的服务能力。

名字服务

在分布式系统中,某个服务为了提升整体服务能力,通常部署了很多实例。这里我把这些提供相同服务的实例统称为集群( cluster ),每个实例称为一个节点( Node )。一个应用可能会使用很多cluster,每次访问一个cluster时,就通过名字服务获取该cluster下一个可用的node。那么,名字服务至少需要包含的功能:

  • 根据cluster名字获取可用的node
  • 对管理的所有cluster下的所有node进行健康度的检测,以保证始终返回可用的node

有些名字服务仅对node管理,不参与应用与node间的通信,而有些则可能作为应用与node间的通信转发器。虽然名字服务功能简单,但是要做一个分布式的名字服务还是比较复杂的,因为数据一旦分布式了,就会存在同步、一致性问题的考虑等。

What’s JCM

JCM围绕前面说的名字服务基础功能实现。包含的功能:

  • 管理cluster到node的映射
  • 分布式架构,可水平扩展以实现管理10,000个node的能力,足以管理一般公司的后台服务集群
  • 对每个node进行健康检查,健康检查可基于HTTP协议层的检测或TCP连接检测
  • 持久化cluster/node数据,通过zookeeper保证数据一致性
  • 提供JSON HTTP API管理cluster/node数据,后续可提供Web管理系统
  • 以库的形式提供与server的交互,库本身提供各种负载均衡策略,保证对一个cluster下node的访问达到负载均衡

项目地址 git jcm

JCM主要包含两部分:

  • jcm.server,JCM名字服务,需要连接zookeeper以持久化数据
  • jcm.subscriber,客户端库,负责与jcm.server交互,提供包装了负载均衡的API给应用使用

架构

基于JCM的系统整体架构如下:

写了一个分布式名字服务JCM

cluster本身是不需要依赖JCM的,要通过JCM使用这些cluster,只需要通过JCM HTTP API注册这些cluster到jcm.server上。要通过jcm.server使用这些cluster,则是通过jcm.subscriber来完成。

使用

可参考 git READMe.md

需要jre1.7+

  1. 启动zookeeper
  2. 下载jcm.server git jcm.server-0.1.0.jar
  3. jcm.server-0.1.0.jar 目录下建立 config/application.properties 文件进行配置,参考 config/application.properties
  4. 启动jcm.server

     java -jar jcm.server-0.1.0.jar 
  5. 注册需要管理的集群,参考cluster描述: doc/cluster_sample.json ,通过HTTP API注册:

     curl -i -X POST http://10.181.97.106:8080/c -H "Content-Type:application/json" --data-binary @./doc/cluster_sample.json 

部署好了jcm.server,并注册了cluster后,就可以通过jcm.subscriber使用:

// 传入需要使用的集群名hello9/hello,以及传入jcm.server地址,可以多个:127.0.0.1:8080 Subscriber subscriber = new Subscriber( Arrays.asList("127.0.0.1:8080"), Arrays.asList("hello9", "hello")); // 使用轮询负载均衡策略 RRAllocator rr = new RRAllocator(); subscriber.addListener(rr); subscriber.startup(); for (int i = 0; i < 2; ++i) {   // rr.alloc 根据cluster名字获取可用的node   System.out.println(rr.alloc("hello9", ProtoType.HTTP)); } subscriber.shutdown();

JCM实现

JCM目前的实现比较简单,参考模块图:

写了一个分布式名字服务JCM

  • model,即cluster/node这些数据结构的描述,同时被jcm.server和jcm.subscriber依赖
  • storage,持久化数据到zookeeper,同时包含jcm.server实例之间的数据同步
  • health check,健康检查模块,对各个node进行健康检查

以上模块都不依赖Spring,基于以上模块又有:

  • http api,使用spring-mvc,包装了一些JSON HTTP API
  • Application,基于spring-boot,将各个基础模块组装起来,提供standalone的模式启动,不用部署到tomcat之类的servlet容器中

jcm.subscriber的实现更简单,主要是负责与jcm.server进行通信,以更新自己当前的model层数据,同时提供各种负载均衡策略接口:

  • subscriber,与jcm.server通信,定期增量拉取数据
  • node allocator,通过listener方式从subscriber中获取数据,同时实现各种负载均衡策略,对外统一提供 alloc node 的接口

接下来看看关键功能的实现

数据同步

既然jcm.server是分布式的,每一个jcm.server instance(实例)都是支持数据读和写的,那么当jcm.server管理着一堆cluster上万个node时,每一个instance是如何进行数据同步的?jcm.server中的数据主要有两类:

  • cluster本身的数据,包括cluster/node的描述,例如cluster name、node IP、及其他附属数据
  • node健康检查的数据

对于cluster数据,因为cluster对node的管理是一个两层的树状结构,而对cluster有增删node的操作,所以并不能在每一个instance上都提供真正的数据写入,这样会导致数据丢失。假设同一时刻在instance A和instance B上同时对cluster c1添加节点N1和N2,那么instance A写入c1(N1),而instance B还没等到数据同步就写入c1(N2),那么c1(N1)就被覆盖为c1(N2),从而导致添加的节点N1丢失。

所以,jcm.server instance是分为 leaderfollower 的,真正的写入操作只有leader进行,follower收到写操作请求时转发给leader。leader写数据优先更新内存中的数据再写入zookeeper,内存中的数据更新当然是需要加锁互斥的,从而保证数据的正确性。

写了一个分布式名字服务JCM

leader和follower是如何确定角色的?这个很简单,标准的利用zookeeper来进行主从 选举的实现 。

jcm.server instance数据间的同步是基于zookeeper watch机制的。这个可以算做是一个JCM的一个瓶颈,每一个instance都会作为一个watch,使得实际上jcm.server并不能无限水平扩展,扩展到一定程度后,watch的效率就可能不足以满足性能了,参考 zookeeper节点数与watch的性能测试 (那个时候我就在考虑对我们系统的重构了) 。

jcm.server中对node健康检查的数据采用同样的同步机制,但node健康检查数据是每一个instance都会写入的,下面看看jcm.server是如何通过分布式架构来分担压力的。

健康检查

jcm.server的另一个主要功能的是对node的健康检查,jcm.server集群可以管理几万的node,既然已经是分布式了,那么显然是要把node均分到多个instance的。这里我是以cluster来分配的,方法就是简单的使用一致性哈希。通过一致性哈希,决定一个cluster是否属于某个instance负责。每个instance都有一个server spec,也就是该instance对外提供服务的地址(IP+port),这样在任何一个instance上,它看到的所有instance server spec都是相同的,从而保证在每一个instance上计算cluster的分配得到的结果都是一致的。

健康检查按cluster划分,可以简化数据的写冲突问题,在正常情况下,每个instance写入的健康检查结果都是不同的。

写了一个分布式名字服务JCM

健康检查一般以1秒的频率进行,jcm.server做了优化,当检查结果和上一次一样时,并不写入zookeeper。写入的数据包含了node的完整key (IP+Port+Spec),这样可以简化很多地方的数据同步问题,但会增加写入数据的大小,写入数据的大小是会影响zookeeper的性能的,所以这里简单地对数据进行了压缩。

健康检查是可以支持多种检查实现的,目前只实现了HTTP协议层的检查。健康检查自身是单个线程,在该线程中基于异步HTTP库,发起异步请求,实际的请求在其他线程中发出。

jcm.subscriber通信

jcm.subscriber与jcm.server通信,主要是为了获取最新的cluster数据。subscriber初始化时会拿到一个jcm.server instance的地址列表,访问时使用轮询策略以平衡jcm.server在处理请求时的负载。subscriber每秒都会请求一次数据,请求中描述了本次请求想获取哪些cluster数据,同时携带一个cluster的version。每次cluster在server变更时,version就变更(时间戳)。server回复请求时,如果version已最新,只需要回复node的状态。

subscriber可以拿到所有状态的node,后面可以考虑只拿正常状态的node,进一步减少数据大小。

压力测试

目前只对健康检查部分做了压测,详细参考 test/benchmark.md 。在A7服务器上测试,发现写zookeeper及zookeeper的watch足以满足要求,jcm.server发起的HTTP请求是主要的性能热点,单jcm.server instance大概可以承载20000个node的健康监测。

网络带宽:

Time              ---------------------traffic-------------------- Time               bytin  bytout   pktin  pktout  pkterr  pktdrp 01/07/15-21:30:48   3.2M    4.1M   33.5K   34.4K    0.00    0.00 01/07/15-21:30:50   3.3M    4.2M   33.7K   35.9K    0.00    0.00 01/07/15-21:30:52   2.8M    4.1M   32.6K   41.6K    0.00    0.00

CPU,通过jstack查看主要的CPU消耗在HTTP库实现层,以及健康检查线程:

  PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND 13301 admin     20   0 13.1g 1.1g  12m R 76.6  2.3   2:40.74 java         httpchecker 13300 admin     20   0 13.1g 1.1g  12m S 72.9  2.3   0:48.31 java 13275 admin     20   0 13.1g 1.1g  12m S 20.1  2.3   0:18.49 java

代码中增加了些状态监控:

checker HttpChecker stat count 20 avg check cost(ms) 542.05, avg flush cost(ms) 41.35

表示平均每次检查耗时542毫秒,写数据因为开启了cache没有参考价值。

虽然还可以从我自己的代码中做不少优化,但既然单机可以承载20000个节点的检测,一般的应用远远足够了。

总结

名字服务在分布式系统中虽然是基础服务,但往往承担了非常重要的角色,数据同步出现错误、节点状态出现瞬时的错误,都可能对整套系统造成较大影响,业务上出现较大故障。所以名字服务的健壮性、可用性非常重要。实现中需要考虑很多异常情况,包括网络不稳定、应用层的错误等。为了提高足够的可用性,一般还会加多层的数据cache,例如subscriber端的本地cache,server端的本地cache,以保证在任何情况下都不会影响应用层的服务。

正文到此结束
Loading...