作者介绍
郭银枫:2013年加入去哪儿网,一直深耕GDS领域。在系统高并发、高可用、高性能积累了较多的实践经验。
一、前言
为了让大家更好的理解本文内容,我们先来介绍几个名词:
- agentId:中航信为每个代理人分配的编号。
- terminalNo:中航信为代理人分配的一个终端编号,每个代理人可以申请多个 terminalNo 终端,即一个 agentId 有多个 terminalNo;每个 terminalNo 终端和中航信系统保持着 tcp 长连接。工作人员通过 terminalNo 终端输入各种指令,完成机票售卖的全流程。
在国内,所有进行机票售卖的平台、代理商都离不开 terminalNo 系统,这个系统的稳定性十分的重要,支撑着机票最基础、也最核心的功能。下面让我们一起来看一下,qunar 在这个平台的探索之路。
二、单机版的诞生
为了将营业员的手工操作流程搬到线上,支撑业务的快速增长,我们需要对 terminalNo 终端进行服务化。有如下几个问题需要思考:
- 请求如何分发,业务上都是按照代理人维度进行配置的,为了和业务保持一致,我们也制定了请求按代理人(agentId)维度进行分发;但由于代理人都有多个 terminalNo,每个 terminalNo 都有一定的流量限制,所以还需要考虑流量平衡问题;否则会由于流量达到上限,terminalNo 就会被停用,影响代理人整体业务的处理能力。
- terminalNo 的指令执行都是串行的,指令间都有因果关系,所以要完成一组指令的操作,我们需要对 terminalNo 进行锁定,操作完成后,再进行释放。
- terminalNo 管理:如连接、登录、退出、添加、修改、删除等。
- terminalNo 的状态管理:如(已连接|连接失败)、(已登录|登录失败),已退出等状态;只有状态在已登录时才能够进行正常的指令操作。
经过综合考虑,为了项目快速上线,我们决定先实现单机版,减少一些开发周期,在后期不断的进行优化和完善。为了技术方案尽量的简单,我们把同一个 agentId 的所有 terminalNo 都部署在一台服务器上,这样请求分发问题、流量平衡问题,terminalNo 锁定问题等都在单个 JVM 内,程序的复杂性就降低了。
low 版本小集群:为了实现小规模的部署,我们实现了一个简单的 client,client 通过轮训方式,检测 agentId 在哪台服务器上,然后再将请求转发到该服务器。
单机版在特定的历史条件下,为了实现业务快速线上自动化,贡献也就非常大的;我们把人工操作实现了机器自动化,大大的提升了机票售卖效率,但其自身的缺点也是十分的明显的,主要有如下两点:
- 无法集群部署,这种靠轮训方式只能少量部署还可以,随着业务的增长,已经不能满足 qunar 的业务要求。
- 容灾能力差,当一台服务器 down 掉之后,无法快速恢复,在这台服务器上的代理也就下线了。
三、注册中心版
针对单机版的问题,我们主要从两个方面入手,来进行方案的设计。
1.路由问题,也就是如何知道 agentId 在哪台服务器上,我们进行了如下的方案对比。
- db 存储 agentId/server 对应关系:该方案实现简单,但由于 terminalNo 服务在机票的各系统中有大量的使用,服务器众多,直接暴露数据库,会造成有大量的连接直连数据库,大量连接的创建会对数据库稳定性有较大的影响,之前出现过这种问题,我们吸取这个经验教训,pass 掉了这个方案。
- redis 保存 agentId/server 的对应关系
a.服务器启动后,将 agentId/server 的对应关系存储在 redis 上。
b.client 采用定时轮训方式进行数据同步。
c.当服务正常关闭时,删除 redis 对应的 agentId/server 数据。
d.当服务非正常关闭时,只能等 redis 的缓存时间过期了,会造成短时间内的业务失败。
- zookeeper 作为注册中心:
a.服务器启动后,将 agentId/server 对应关系注册到zk上。
b.发布订阅机制,client 同步 agentId/server 数据,相比 redis 轮训方案,数据同步及时,同时也减少大量的无效查询。
c.当服务关闭或者异常 kill 掉后,zk 能够及时清理掉数据,但 redis 只能等待数据过期了。
通过如上方案的对比,我们最终选定 zookeeper 作为注册中心方案。
2.容灾问题:当服务器出现 down 机、网络故障时,如何快速的进行服务器切换,减少对业务的影响,是十分重要的。为此我们进行了两个方案的对比。
哨兵机制 vs zookeeper 选举:两者都是优秀的故障转移方案。当发生故障时,除了需要对服务器进行切换外,我们的服务还需要特有的处理流程,如 terminalNo 连接、登录等操作。这一点哨兵不能够定制。但 zk 的选举提供了良好的功能支持,当一台服务器被选举成为 leader 后,我们可以定制诸多功能,如数据准备、资源准备、服务器切换通知等。所以我们选择 zookeeper 选举作为我们的容灾方案。
3.系统架构:根据我们上面的调研结果,我们制定了如下的系统架构方案。
- 引入 zookeeper 作为注册中心,各服务器启动时将 agentId/server 的对应关系在 zk 上注册;在变更时将zk上的信息及时修改。
- 每组服务由一台主机和一台备机组成,部署在不同的机房,通过 leader 选举决定服务提供者和切换决策。
- client 监听 agentId 和 server 的对应关系的变更通知,在本地进行缓存。当准备向集群发送请求时,查找本地缓存,获取 agentId 对应的服务器信息,然后直接将请求发送到对应的服务器。
注册中心版解决了我们最关心的集群扩展、请求路由、容灾等关键问题,该版本为 qunar 服务了多年,支撑了 qunar 业务的快速发展,但还是有几个问题需要我们做进一步的提升。
- 集群有一半的机器是备机,正常情况下是不提供服务的,对资源造成了浪费。
- 同一个 agentId 必须部署在一台服务器上,当机器出现问题的时候,要切到备机上,这会造成在分钟级别该代理的业务全部失败。
- 当单个 agentId 有大量的 terminalNo 时,在业务量高峰时,单机性能会出现瓶颈。
四、一致性 hash 版
对于有状态的服务,要想分散部署和处理,一般都会按某种条件进行 hash 运算,将服务分散到集群中的某个节点。为此我们对比了两种技术方案。简单的 hash%N 方案和一致性 hash 算法。
hash%N vs 一致性 hash
- 服务器变更:hash 算法会引起集群全部的资源进行重新分配;一致性 hash 只会影响变动的节点。
- 数据倾斜问题:简单的 hash 算法很容易产生数据的倾斜;但一致性hash 只要节点多,就不会出现这个问题。
- 热点问题:和数据倾斜类似,一致性 hash 只要节点多,就不会出现这个问题。
经过简单的方案对比,一致性 hash 就完胜了。一致性 hash 在分布式系统中应用广泛,最典型的就是 redis 了,redis 利用该算法,防止缓存出现雪崩,数据倾斜都起到了关键的作用,完全符合我们的要求。
下面让我们来看下这次优化几个重点的技术点:
- terminalNo 和服务器绑定,使用一致性 hash 部署,当服务器变更时,terminalNo 会自动从下一个服务器启动,继续提供服务。其它节点不受影响。虚拟节点数我们没有进行实验,直接使用 double 的的数值。部署示意图如下:
- 由于同一 agentId 的 terminalNo 被分散到了集群中的不同节点,就需要一个集中存储管理 terminalNo 状态流转的公共设施,我们使用 redis 多队列实现 terminalNo 的状态的流转,这样就能实现 terminalNo 状态的过滤,流量平衡,terminalNo 锁定等功能。示例图如下:
一致 hash 版,在 qunar 已经稳定运行了二年多,在集群的扩展性、容灾能力提升、分散热点数据都有非常不错的表现,主要特点如下:
- 扩展性:集群做到了可以根据业务情况,做到横向无限扩展。
- 容灾能力:出现服务器 down 机后,terminalNo 会在几秒钟从下一节点启动,继续对外提供服务。
- 热点访问:将热点访问均匀分散到集群中的节点,不会造成单机负载过高。
五、总结
本文详细介绍了 qunar 后台团队对于有状态服务部署的探索之路,涉及了热点数据处理、集群扩展性、容灾能力提升等,希望对大家有所帮助。qunar 后台团队会一直关注业界先进的技术方案和实践效果,来优化我们的系统,更好的服务于业务。