于9月25日,参加云栖大会elasticsearch专场后对阿里云的优化思路进行整理总结。行文分为两个部分,一个部分是对原生elasticsearch的解决方案进行大致介绍,再对阿里云的优化进行整理。

原生架构

在elasticsearch当中index是大家最熟悉的组织结构,它就像传统nosql数据库中的collection,组织着一批有相同数据结构的数据。当一个index中的数据量越来越大,就会很自然的将index进行分片,分到多个shard当中。这样通过水平扩展就能解决数据膨胀的问题,然后同时也引入了副本来提高整个集群的可用性。所以有两个index,每个index有3个shard,每个shard有2个replica的集群就是下面这个样子👇:
a6-1
index A有3个shard:A1A2A3A1’A1的副本,以此类推。

在这样的架构中,如果想向index A中写入文档,会先通过路由算法分配到一个primary所在的节点(比如A1)进行写入,完成以后会到该primary的副本A1’中进行写入,完成以后再返回成功。可以看出这样的流程中有几个瓶颈所在:

  • 如果副本的数量一旦较多,写请求的延时就会成倍增长
  • 副本会保存一份完整的数据,所以集群的存储成本也在成倍地增长
  • 如果副本数量超过了分片数量,会出现有节点只是用来做副本的情况,只能被读,浪费了资源
  • 当主节点出现故障时,会从新在副本当中选举主节点,并新启动一个节点,从新的主节点当中全量拷贝数据。全量拷贝的延时较长,在这一段时间内新副本节点是不可用的,所有的流量会打到主节点和其他副本节点上,可能出现性能问题

优化架构

先来看阿里云的架构图👇:
a6-2
使用了计算与存储分离的架构,通过底层的云储存来提供共享存储。这样主副之间的数据只用存在一个地方即可,而节点上只提供计算能力,由云存储来提供数据的多副本机制以保证数据的可靠性。如果要完整的理解上面这张图,我们需要再介绍一些概念:

在elasticsearch中,一个shard是由多个segment组成的,并对应一个translog文件。那么当写入一个文档时,会先在translong中进行相应的记录,并将其写入到primary节点的buffer当中。然后在primary节点上是有一个每秒执行一次的refresh操作,该操作会把buffer当中的所有document写入到内存中的一个新的segment当中,这时候文档变成了可被检索的状态,所以一个segment实际就是一个倒排索引。最后会有一个flush操作,该操作也会把buffer中的数据写入到一个新的segment中,并进一步将内存中的所有segment冲洗到硬盘上,并清空该shard上的translog。

在对整个索引过程解释了一遍以后就可以清晰的理解上图,NFS表示阿里云提供的分布式云存储。当一个index请求进来的时候,primary节点会先在translog中添加一条记录,并将该文档写入buffer当中。每隔一段时间,primary节点会进行refresh操作将buffer中的所有数据写入到nfs的refresh segment中进行保存。然后会由 nrt segment synchronizer进程定期的复制refresh segment中的数据到临时目录当中。最后会由flush操作,将refresh segment中的数据写入到commit segment中,并删除refresh segment,临时目录,以及translog。而读请求如果从primary节点读会读到refresh segment + commit segment的内容,如果从replica中读会读到临时目录 + commit segment的内容。

这种架构解决了原生架构的一些问题:

  • 降低了写请求的延时,因为现在的refresh segment到临时目录的复制没有通过网络传输,直接是同一文件系统中的复制,所以大大降低了主备延时。(据阿里所说从分钟级降低到了毫秒级,但是原生架构也没有分钟级这么夸张吧…)
  • commit segment只会保存一份,所以不会因为副本数量过多而导致集群的存储成本上升
  • 因为计算与存储分离,当出现主节点故障需要主副切换时,不需要长时间的拷贝全量数据,一个新的副节点启动以后,只要指向原来的临时目录即可

但是也引入了一些新的问题:

  • 由于现在是共享内存,所以refresh操作多了传输成本,并且在NFS的速度也低于原本机内存中的速度,所以这里的成本有提高。不过应该是小于主从复制的性能提升。
  • 引入了一些传统共享内存型分布式的问题,比如脑裂,双写等。不过在PPT中也看到阿里也实现了IO fencing来避免主从切换时带来的双写问题。

下面再简单介绍一下演讲中讲到的主从复制的优化,下面是给出的示意图👇:
a6-3
阿里说是使用了luence-replicator框架进行实现,我现在还没仔细阅读,有兴趣的可以看看Code Reduction: The Replicator
所以我下面的解释主要基于他们的演讲以及一些猜测:
在主从复制过程当中,主节点会定时的对meta进行快照,比如生成了图中的snapshot4,然后对它增加引用计数,再发送给从节点。从节点会和自己的快照进行比对,找到落后了多少个版本。将缺少的segment复制到临时目录当中,复制完成以后就可以通知primary节点复制结束,从而减少同步前从节点快照版本的引用计数,删除引用计数为0的文件。

其中还提到了一些针对segment merge的优化,那么还是先介绍一些什么是segment merge:
因为每秒都会进行refresh操作,生成一个小的segment文件,这些小的文件对内存的利用率是非常低的,而且每次query请求来的时候都会轮训这些小的segment文件,所以文件数量越多性能越差。elasticsearch会在后台进行异步的合并操作,从小的segment合并成大的segment,并且在合并阶段处理文件的删除和更新。

那么优化的部分是什么呢?就是在合并成大的segment以后会立即进行flush操作,来保证大的segment不会出现在主从复制当中。从而进一步的对主从复制进行提速。

优化索引速度

离线

离线全量写入一直是一个痛点问题,传统解决办法是维护两个elasticsearch集群,正常使用A集群,然后收集A集群的translog,并将全量快照写入B集群,完成以后回放translog,保证两边数据一致。完成后从A集群切到B集群,这样的维护成本是非常高的,并且全量快照写入的时间又非常的长。

阿里云针对这个点进行的优化是取消了translog,使用blink checkpoint天生的at least once的语义来保证故障恢复,相当于减少了一半的写工作量。但是这样相当于在elasticsearch外面套上了一个blink,并且需要和外部的blink进行通信,系统复杂度又上升了一个等级(不过反正也是云服务,又不要我们自己做DBA)。第二个是在segment合并上增加了一个limit,只有合并后达到一定的大小才会flush到磁盘当中,这样可以减少磁盘里的小segment再被读入到内存中进行反复的合并,减小IO次数。

前两个我觉得都是较小的优化,第三个优化比较大,如下图👇:
a6-4
优化的核心点是利用更高的并发提前计算好索引,直接让线上的elasticsearch集群来load索引。具体怎么做的呢,就是利用blink的流计算能力来取代client node的计算数据分片能力,然后模拟process,build,merge三个操作并将其分布式化,让这三个操作都可以进行水平扩展,让索引能力可以随着计算资源的提升而提升。原来如果想提高索引的计算能力,是需要对elasticsearch集群的资源进行拓展,但是现在将索引计算的步骤分离了出来,转变成了一个经典分布式计算框架能解决的问题。最后放到OSS当中,让线上的模型进行load。类似的优化之前也有人已经尝试过,有兴趣可以看看基于Hadoop快速构建离线elasticsearch索引

在线

在线增量导入的优化也是将client node路由计算的能力放到了blink上,因为他们发现在大数据增量导入的时候,瓶颈出现在了CPU上,所以将计算部分外移,并且blink可以更好的进行针对计算的弹性升缩。

参考材料:
从Elasticsearch来看分布式系统架构设计 - 知乎
Elasticsearch写入原理深入详解 - 铭毅天下(公众号同名) - CSDN博客
Elasticsearch 之 commit point | Segment | refresh | flush 索引分片内部原理 - 舒哥的blog - CSDN博客

Comments

⬆︎TOP