滴滴 Elasticsearch 多集群架构实践

服务器

浏览数:69

2019-5-18

出品 | 滴滴技术
作者 |魏子珺

Elasticsearch 是基于 Lucene 实现的分布式搜索引擎,提供了海量数据实时检索和分析能力。Elastic 公司开源的一系列产品组成的Elastic Stack,可以为日志服务、搜索引擎、系统监控等提供简单、易用的解决方案。

滴滴 Elasticsearch 简介
滴滴2016年初开始构建Elasticsearch平台,如今已经发展到超过3500+Elasticsearch实例,超过5PB的数据存储,峰值写入tps超过了2000w/s的超大规模。

Elasticsearch在滴滴有着非常丰富的使用场景,例如线上核心的打车地图搜索,客服、运营的多维度查询,滴滴日志服务等近千个平台用户。

超大的规模和丰富的场景给滴滴Elasticsearch平台带来了极大的挑战,我们在这期间积累了丰富经验,也取得了一些成果。本文给大家分享下滴滴在Elasticsearch多集群架构的实践。

单集群架构瓶颈
介绍单集群架构瓶颈前,先来看下滴滴Elasticsearch单集群的架构。

滴滴Elasticsearch单集群架构

滴滴在单集群架构的时候,写入和查询就已经通过Sink服务和Gateway服务管控起来。

| Sink服务
滴滴几乎所有写入Elasticsearch的数据都是经由kafka消费入到Elasticsearch。kafka的数据包括业务log数据、mysql binlog数据和业务自主上报的数据,Sink服务将这些数据实时消费入到Elasticsearch。

最初设计Sink服务是想对写入Elasticsearch集群进行管控,保护Elasticsearch集群,防止海量的数据写入拖垮Elasticsearch,之后我们也一直沿用了Sink服务,并将该服务从Elasticsearch平台分离出去,成立滴滴Sink数据投递平台,可以从kafka或者MQ实时同步数据到Elasticsearch、HDFS、Ceph等多个存储服务。

有了多集群架构后,Elasticsearch平台可以消费一份MQ数据写入多个Elasticsearch集群,做到集群级别的容灾,还能通过MQ回溯数据进行故障恢复。

| Gateway服务
所有业务的查询都是经过Gateway服务,Gateway服务实现了Elasticsearch的http restful和tcp协议,业务方可以通过Elasticsearch各语言版本的sdk直接访问Gateway服务,Gateway服务还实现了SQL接口,业务方可以直接使用SQL访问Elasticsearch平台。

Gateway服务最初提供了应用权限的管控,访问记录,限流、降级等基本能力,后面随着平台演进,Gateway服务还提供了索引存储分离、DSL级别的限流、多集群灾备等能力。

| Admin服务
整个Elasticsearch平台由Admin服务统一管控起来。Admin服务提供了索引的生命周期管理,索引容量自动规划,索引健康分,集群监控等丰富的平台能力,以及为Sink、Gateway服务提供索引、权限等元数据信息。

Elasticsearch单集群瓶颈
随着滴滴Elasticsearch平台规模的快速发展,Elasticsearch集群越来越大,最大的时候,是由几百台物理机组成集群,当时集群共 3000+ 的索引,超过了 50000 个 shard,集群总容量达到了PB级别。超大的Elasticsearch集群面临了很大的稳定性风险,这些风险主要来自于以下三个方面:

  • Elasticsearch架构瓶颈
  • 索引资源共享风险
  • 业务场景差异大

Elasticsearch架构瓶颈
Elasticsearch架构在集群变大到一定的规模会遇到瓶颈,瓶颈主要跟Elasticsearch任务处理模型有关。

Elasticsearch看起来是p2p架构,但实际上,仍然是中心化的分布式架构。整个集群只有一个active master。master负责整个集群的元数据管理。集群的所有元数据保存在ClusterState对象中,主要包括全局的配置信息、索引信息和节点信息。只要元数据发生修改,都得由master完成。

Elasticsearchmaster的任务处理是单线程完成的,每次处理任务,涉及到ClusterState的改动,都会将最新的ClusterState对象publish给集群的全部节点,并阻塞等待全部节点接受到变更消息,处理完变更任务后,才完成本次任务。

这样的架构模型导致在集群规模变大的时候出现很严重的稳定性风险。

  • 如果有节点假死,比如jvm内存被打满,进程还存活着,响应master任务时间会很长,影响单个任务的完成时间。
  • 有大量恢复任务的时候,由于master是单线程处理的,所有任务需要排队处理,产生大量的pending_tasks。恢复时间变得很长。
  • Elasticsearch的任务分了优先级,例如put-mapping任务优先级低于创建、恢复索引,如果一些业务上低优先级索引在恢复,正常索引有新字段写入时会被阻塞。
  • master任务处理模型,在任务执行完成后,会回调大量listener处理元数据变更。其中有些回调逻辑在索引、shard膨胀后,会出现处理缓慢的问题,当shard膨胀到5-6w时,一些任务处理需要8-9s的时间,严重影响了集群的恢复能力。

针对这些问题,Elasticsearch也在不断优化,针对相同类型的任务,比如put-mapping任务,master会一次性处理所有堆积在队列里的相同任务。ClusterState对象只传递diff内容,优化回调listener模块的处理耗时环节等等。

但是由于整个集群的任务都集中在一个master的一个线程中处理,在线程中需要同步元数据变更给集群的每个节点,并阻塞等待全部节点同步完成。这个模型在集群规模不断膨胀时,稳定性会不断下降。

| 索引资源共享风险
Elasticsearch索引是由多个shard组成,master会动态给这些shard分配节点资源。不同的索引会存在资源混部的情况。

Elasticsearch通过Shard Allocation Awareness的设计,可以将集群的节点按集合划分成不同的rack。在分配索引时可以指定rack列表,这样索引就只会分配在指定rack对应的节点列表中,从而做到物理资源的隔离。

但是实际使用中,很多容量小的索引由于占用资源有限,会混部在一些节点中。这种情况下,会因为个别索引的查询、写入量飙升,而影响到其他索引的稳定性。如果出现了节点故障,就会影响到整个集群的稳定性。

整个集群master、clientnode资源是共享的,master风险前面已经单独提及,clientnode共享带来的gc、抖动、异常问题都会影响到集群内的全部索引。

| 业务场景差异大
Elasticsearch适用的业务场景差异特别大。

  • 针对线上核心的入口搜索,一般按城市划分索引后,索引容量不大,数据没有实时写入或者实时写入tps很小,比如地图poi数据采用离线更新的方式,外卖商家、菜品写入量也很小。但是查询的qps很高,查询对rt的平均时间和抖动情况要求很高。
  • 针对日志检索的的场景,实时写入量特别大,有些索引甚至超过了100w/s的tps,该场景对吞吐量要求很高,但对查询qps和查询rt要求不高。
  • 针对binlog数据的检索,写入量相比日志会小很多,但是对查询的复杂度、qps和rt有一定的要求。
  • 针对监控、分析类的场景,聚合查询需求会比较多,对Elasticsearch内存压力较大,容易引起节点的抖动和gc。

这些场景各异,稳定性、性能要求各不相同的场景,一个Elasticsearch集群即使使用各种优化手段,很难全部满足需求,最好的方式还是按业务场景划分Elasticsearch集群。

多集群挑战
正是单集群面临了非常大的稳定性风险,我们开始规划多集群的架构。我们在设计多集群方案的时候,期望对业务方是零感知的。

写入还是经过kafka,Sink服务可以将不同topic的数据入到不同的Elasticsearch集群。查询继续通过Gateway服务,而且业务方仍然像之前一样传递索引名称,而无需感知到平台内部的索引分布。所有的索引在不同集群的分布细节,均由Gateway服务屏蔽。

整个改造最大的挑战在于查询方式的兼容。Elasticsearch查询索引的方式非常灵活,可以支持*号作为通配符匹配。这样一个索引query可能查询的是多个索引,比如有如下3个索引:

  • index_a
  • index_b
  • index_c

使用index*查询的时候,可以同时查询到index_a、index_b、index_c三个索引。 Elasticsearch这种实现方式非常简单,由于一次query最终查询的是多个shard的数据,所以无论对于具体的索引,还是模糊的索引,都是先根据索引名称得到shard列表,再将多个shard的query结果merge到一起返回。

这样的使用方式,对于多集群方案就会遇到问题,比如index_a在A集群,index_b在B集群、index_c在C集群,对于index*的query,就无法在一个集群上完成。

tribenode介绍

经过调研,我们发现Elasticsearchtribenode特性可以很好的满足多集群查询的特性。tribenode的实现非常巧妙。org.elasticsearch.tribe包下只有三个文件,核心类是TribeService。tribenode的核心原理就是merge每个集群的ClusterState对象成一个公共的ClusterState对象,ClusterState包含了索引、shard和节点数据分布表。而Elasticsearch的工作逻辑都是基于ClusterState元数据驱动的,所以对外看起来就是一个包含全部索引的的clientnode。

tribenode通过配置多个Elasticsearch集群地址,然后以clientnode角色分别连接每个集群,每个集群看起来会多了一个clientnode。tribenode通过该clientnode角色获取到集群的ClusterState信息,并绑定listener监听ClusterState变化。tribenode将获取的所有集群的ClusterState信息merge到一起,形成一个对外部访问使用的ClusterState对象,对外提供服务。tribenode除了注册listener和merge ClusterState,其他的所有逻辑都是复用了clientnode的代码。

可以看到tribenode的优点:

  • 能够满足多集群访问的需求,对外使用是透明的。
  • 实现的简单、优雅,可靠性有保证。

同时tribenode有些不足的地方:

  • tribenode必须以clientnode加入到每个Elasticsearch集群,master的变更任务必须等待tribenode的回应才能继续,可能影响到原集群的稳定性。
  • tribenode不会持久化ClusterState对象,重启时需要从每个Elasticsearch集群获取元数据。而在获取元数据期间,tribenode就已经能够提供访问,会导致查询到还在初始化中的集群索引访问失败。
  • tribenode连接的集群多了,初始化会变得很慢。针对该缺陷,我们平台在重启某个tribenode集群时,将Gateway访问该集群的全部流量切到备份tribenode集群解决。

如果多个集群有相同的索引名称,tribenode只能设置一种perfer规则:随机、丢弃、prefer指定集群。这可能带来查到不符合预期的异常。滴滴Elasticsearch平台通过统一管控索引,避免了同一个索引名称出现在tribenode连接的多个集群中。

正是tribenode有了这些瑕疵,Elasticsearch在高版本引入了Cross ClusterSearch的设计,Cross Cluster不会以节点的形式连接到其他集群,只是将请求代理。目前我们还在评估Cross Cluster的方案,这里不展开介绍。

多集群架构拓扑
最终改造后,我们的集群架构拓扑如下:

按照不同的应用场景,平台将Elasticsearch集群划分成四种类型,Log集群、Binlog集群、文档数据集群、独立集群。公共集群一般最多100台datanode为基准组成一个集群。我们利用滴滴云实现了集群的自动化部署和弹性扩缩容,可以很方便的水平扩展集群。

Elasticsearch集群前面是多组tribenode集群,主要是为了解决tribenode的稳定性问题。

Gateway会同时连接tribenode集群和Elasticsearch集群,根据应用访问的索引列表,配置应用访问的集群名称,Gateway根据集群名称,将请求代理到指定集群访问,如果访问的是tribenode集群,则该应用可以访问到多个集群的索引。

Admin服务则管控了所有的Elasticsearch集群,以及索引和集群的对应关系。一系列功能都针对多集群做了改造。

Sink服务已经从Elasticsearch平台分离出去,成立DSink数据投递平台,DSink Manager负责管理DSink节点,DSink Manager从Elasticsearch Admin服务获取索引的元数据信息,下发给对应的DSink节点。

多集群架构实践总结

| 多集群架构收益
Elasticsearch多集群架构改造给Elasticsearch平台带来了如下收益:

  • Elasticsearch平台的隔离性可以从物理节点级别上升到Elasticsearch集群级别。对于核心的线上应用,可以使用独立的Elasticsearch集群支持。
  • 不同类型的数据按集群划分,避免相互影响,减小了故障的影响面,对平台稳定性带来极大的提升。
  • Elasticsearch平台的扩展能力进一步提升,通过新增集群可以很好的做到水平扩展。
  • 多集群架构最终做到了对业务方无感知,业务看起来,Elasticsearch平台就像一个无限大的Elasticsearch集群,而无需感知索引真实的集群分布。

| 多集群架构实践经验
滴滴Elasticsearch平台多集群的架构已经演进了一年半时间,这期间也遇到一些多集群架构带来的挑战。
tribenode稳定性挑战:

  • 随着集群数量越来越多,前面提到的tribenode不足越来越明显,比如初始化的时间越来越长等等。我们采取的应对策略是部署多组tribenode集群,有几组连接全量的集群,互为灾备,有几组只连接核心的一些集群,用作更为重要的跨集群访问场景。
  • tribenode的ClusterState元数据包含了太多的索引和shard,Elasticsearch的search逻辑在有些case处理下容易出现耗时过长的情况。Elasticsearch在client接收到search请求时,是在netty的io线程中完成请求转发给每个shard的,低版本的Elasticsearch还没有限制一次query的shard数量,在一些复杂的模糊索引匹配shard的逻辑中,以及给每个shard发送query请求时,会出现较高的耗时,可能有超过1-2s的case,这会影响到该netty worker上的其他的请求,造成部分响应飙高的情况。我们优化了tribenode search流程中一些索引、shard膨胀之后的耗时逻辑,解决了该问题。

多集群配置、版本统一的挑战:

  • 在只有一个集群的时候,平台只用维护一份集群的配置和版本。当集群数量增多后,不同集群间的_cluster
    settings信息会出现部分差异,这些差异,可能会导致集群间的负载不均,恢复速度过快或者过慢等问题,每个集群还有一份基础的索引模板配置,这里面也出现了部分差异。这个问题目前我们还在解决中,我们计划将Admin服务分离成索引管理服务和集群管理服务,集群管理会专注于集群版本、配置、部署、扩容、监控等方面对Elasticsearch集群进行更全面的管控。
  • 我们做的一些Elasticsearch源码优化,会先后在部分集群上线,这样导致了集群间的版本混乱的问题。我们的解决方案是在Elasticsearch和Lucene内增加内部的版本号,通过公司内部的发布系统,发布Elasticsearch的更新,后续集群管理服务会将集群的版本管理起来。

多集群间容量均衡的挑战:

  • 我们主要从跨集群索引迁移和容量规划解决集群间容量均衡的挑战,在单Elasticsearch集群的时候,数据迁移可以依赖Elasticsearch的rebalance能力完成。在使用多集群架构后,平台内部的Elasticsearch集群会出现资源分配不均的问题,例如有些索引容量增长的很快,导致所在集群的资源紧张,有些索引数据减少,不需要占用太多资源,导致集群资源空闲。于是产生了索引跨集群迁移的需求。针对这个需求,我们通过给索引添加版本号,解决了索引跨集群迁移问题。之后我们有文章会详细的介绍该方案。
  • 滴滴Elasticsearch平台实现了索引容量的自动规划,解决了集群间的容量均衡。Elasticsearch平台可以动态的规划索引的容量。当一个集群容量规划不足时,平台可以动态的迁移一部分索引到空闲的集群中。新的索引接入需求会优先接入在空闲的集群资源中。滴滴Elasticsearch平台是如何实现索引容量的自动规划,也请期待后续的分享。

总结
滴滴的多集群架构,最初是为了解决Elasticsearch单集群架构的瓶颈。为了支持多集群架构,后面的很多组件都需要考虑连接多个集群的场景,给平台架构带来了一定的复杂性。但是多Elasticsearch集群带来的稳定性和隔离性的提升,它所带来的收益远远大于架构的复杂性。改造成多集群架构后,我们扛住了Elasticsearch平台规模爆炸式增长,Elasticsearch平台的规模翻了5倍多,多集群架构很好的支撑了业务的快速发展。

作者:滴滴技术