2.3 健壮性架构
接下来我们来看看在集群健壮性方面的架构,前面是讲的扩展性,下面我们来看看健壮性。ES 是一个典型的两层的查询结构,整体上来看的话,ES 的查询包含三大类:大查询、高并发的点查询、高并发的写入。
大查询对于内存资源要求很高,所以很容易打垮单个节点的内存,进而逐步压垮集群中的每一个节点;
高并发点查询对于计算和 I/O 资源要求比较高,很容易导致 I/O 或者是计算资源的争抢,引起长尾、拒绝;
高并发的写入对于计算和内存要求比较高,可能引起拒绝、雪崩,当然大家常见的一个分布式集群里面的硬件故障、网络抖动之类的问题,也会引起服务的抖动。
那么从整体的资源的角度上来看的话,整个主要的瓶颈实际上内存的瓶颈最为明显,计算和硬盘的瓶颈其次,怎么去解决这方面的问题?
Elastic 的设计其实默认提供了一套漏斗限流,但这漏斗限流实现的比较初级,不能满足需求。我们的实现的一个思路是,首先通过服务限流来保证整个集群的稳定,然后通过一个异常容忍的这种架构设计来提供稳定的服务,平响保持比较平稳。
2.3.1 服务限流
下面我们来看看健壮性架构方面的服务限流,用来保证集群稳定性方面的工作。
其实在服务限流这块,业界典型的实现方案有两类:
一类是类似代理这种限流方式,在代理里面通过规则引擎去限流,这种方式比较适合高并发点查询的模式,适用于微服务这种场景,但是对于 ES 这种大查询的场景就不太满足。
第二类是在大数据场景下,大家通常看到的以 MapReduce 或者 Hadoop 场景下为代表的任务容器的方式的限流,以 Yarn 来实现,每一个任务在执行前会去生成一个容器,在容器内部去执行这个任务。这种方式的好处在于,可以精准地控制每一个任务的资源占用,不至于导致影响其他的请求。 但是问题在于整个容器的分配非常耗时,对 ES 里面的小查询不太适合。另外一个是这种方式,对计算资源的利用率通常不高。
所以我们在 ES 里面实现了一套自己的方案,这套方案的主要的设计逻辑是全内存的熔断限流,再加上弹性漏斗。
全内存的熔断限流是指,我们在请求的入口协调层以及执行层都做了内存的熔断,整体上限制每一个进程的总体内存以及每一个请求级别的内存的限制,
另外通过引入弹性队列,我们可以一定程度上来缓解 CPU 跟 I/O 的抢占,并且通过弹性队列的支持,我们可以做到容忍一定的读写的毛刺。
但这种实现方案有一定的关键点,
第一在于如何精准的统计大量中小查询的内存。首先我们在 Java 语言里可以通过 JOM 来获取到进程总体的内存,而对于部分查询,我们可以在关键路径上去统计大块的内存分配。
第二个是服务限流,如何保证高吞吐呢?我们做的一个方式是通过平滑限流来保证,无限流情况下,整个集群的吞吐仍然接近于一个最高的水准。
第三个是如何来避免大查询导致的 OOM,这个过程中你一定要及时地去检测。我们的一个做法,例如在聚合的过程中,我们会深入到聚合分桶的一个生成的过程中,去做一个梯度的熔断限流。
2.3.2 异常容忍
前面介绍完了集群健壮性架构方面的东西,下面我们来介绍一下异常容忍方面。集群的健壮性架构能保证集群的稳定可靠,而异常容忍方面能让我们在一个不可靠的集群里面提供可靠的服务。
具体的一个线上的问题的样例,比如说在日志和时序场景下,这种百万级 TPS 的场景容易触发拒绝,而我们观察的一个现象是,集群的资源利用率并不高。另一个是对于搜索等场景十万级 QPS,我们观察到,长尾查询非常明显,有很多查询达到了秒级甚至更高的一个标准,对用户体验非常不好,这个原因是什么?
其实在于分布式场景下,一方面异常难以消除,包含机器异常、网络的抖动、GC、后台任务对资源的抢占这些等等,其实都是分布式系统里面的异常。而对于这些异常的消除是很难彻底性去解决的,对于分布式系统里面高扇出的,分布式的读写会放大异常的影响,比如说我们一个请求访问到 100 个分片,只要其中一个分片是慢节点,那么整个请求都需要等待长尾,导致的服务的体验比较弱。
我们在这里做的一个工作是,
- 对于写入的场景我们实现了一个分组路由,比如说原来写入是每个写入请求下发到 100 个分片,我们对分片进行分组,每一定的分片放在一个分组里,一次写入随机路由到一个分组内部。然后在这个分组里面,再按照数据本身的特性,去做 Hash 的路由,这样的话我们做到的情况是大大降低了扇出,可以来避免这种长尾的影响。同时不同批次的写入,写入到不同的分组里面,这样来利用整个分布式集群的能力。
- 对于查询方面的话,一方面我们引入了这种自适应路由。通常情况下,大多数分布式系统的实现是,一个请求会随机地选择数据副本去发送请求,我们做的一个方式是去做了近期时间窗口的统计,根据统计信息来把请求发到最合适的一个副本上。
- 另外我们引入了一个对冲请求,比如说一个请求花了 100 个分片,很难避免有个别分片成为慢请求、慢查询,我们做的一个方式是对于尾部的少量的副本的访问,我们可以做到发起一个对冲请求、备份请求,谁先回来,谁作为一个响应的结果,这样的话大大降低长尾的影响。
最终的实现的效果是对于写入,我们的 TPS 提升了一倍,资源利用率有稳步的提升;对于查询来说,我们的毛刺降低了 10 倍,降低到 100 毫秒以内。
其实这里是提供了我们在不可靠环境下,去构建可靠服务的一个经验。
2.成本优化
介绍完可用性之后,我们来看看成本方面的优化。
2.1 解决方案
成本方面我们的主要瓶颈前面介绍过,一方面在于内存、存储、计算这几个维度,所以我们做的一些工作是
- 首先在内存层面我做的内存的优化,提高内存的利用效率;
- 其次,对于硬盘存储方面,我们做了冷热分层,数据上卷这些东西来降低用户的成本;
- 在计算方面的话,我们通过日志即数据库这种方案来降低写入的开销,同时通过分组路由来提高资源的利用率;
- 当然我们还通过一些资源调度方面,多租户、弹性伸缩这些东西来提高整体的资源利用率情况。
最终的实现的效果是,我们在日志场景下仅通过软件架构可以降低成本 70%,结合这种硬件选型、资源调优这些,可以降低一个数量级的成本。
2.2 内存优化
下面我们来展开给大家介绍一些成本方面的优化工作。第一个是内存方面的优化,ES 的内存消耗很高,这主要是在于 ES 早期是做搜索场景的,所以它非常考虑性能,把索引都常驻于内存。而在时序跟日志场景下这就出现了不同,历史数据很少访问,而且部分字段可能也比较少访问,把所有的索引都放在内存里面,数据量这么大,实际上对内存的开销非常高。
所以这里一个很重要的点实际上是,提高内存的利用效率,但是要注意保持住查询的性能。ES 社区在这方面做了一定的工作,它做的一个方案是,写入查询的过程中,索引是按需加载的,放置在系统缓存里。但是这种方案的不足点是在于,系统缓存它不会区分索引跟数据,所以当出现一个大查询把系统缓存冲刷之后,整个后续会引起查询的抖动。
我们在这里实现了自己的方案,采用独立的 Cache 来完成这个工作,一方面我们做了精准的 Cache 命中率方面的优化,比如说主键索引不访问,我们可以不放入 Cache,Merge 的过程中,我们会主动地把历史数据的索引剔除掉,来提高整个索引的内存利用效率。其次对索引的使用性能这块,我们也做了优化。比如说多次 Cache 的查找,可以在多个查询之间进行共用,然后对于 Cache 的放置位置,我们把它移到了堆外,在 Java 语言里放置堆外可以降低 Java GC 的影响,提升性能。
所以最终的效果是,我们内存利用率提升了 80%的同时,性能基本上保持不变,Cache 的命中率在 99%+。主要还是原生的 ES 在内存利用率这块使用得相对较弱。
2.3 数据上卷:计算置换存储
第二块是成本优化在于数据上卷。其实在监控这种场景下,这种需求很自然,就是对历史数据、监控场景需要保存半年以上的数据,同时又要求查询性能很高,怎么去做呢?典型的一个做法是通过预计算来换取成本,我们可以把数据提前按照时间,或者是按照其他的维度做了一个聚合,聚合之后成本大幅下降。
这里面我们介绍一些典型的方案的对比,一些典型的方案比如说依赖于这种 Hadoop 的离线计算这种方式,依赖复杂而且计算成本高;其他的一些方式,比如聚合查询,聚合查询在大量这种基数的情况下容易导致内存的打爆;再有比如说 Merge 任务这种方式,底层在 Merge 的过程中把数据的力度,完成这种数据上卷的一个计算的过程,但这种方式存在一个冗余表的存储。
我们采用的方案是,对于 ES 中的底层数据采用流式任务调度的方式,去进行多路归并来完成这个过程,不存在冗余表的写入。整个计算也只有写入的 10% 内存,非常可控,而且数据的延迟相当于只有实时数据的基础上增加了 5 分钟,对用户体验非常好。
2.4 日志即数据库
日志即数据库这一块。日志即数据库实际上是一个很有创新的设计,大多数分布式系统中都存在主从副本的一个设计,主从副本多数情况下是对等的,而且副本之间通过全同步或者 Quorum 机制来保证数据的一致性。但是在日志和时序这种场景下,实际上存在着一定的不同,因为日志和时序场景下,它对于数据的时效性这种一致性要求相对较低。
所以我们这里可以做一个非常好的优化就是日志即数据库的概念。对于主分片我们通常情况下,分布式系统设计保持一致,而对于从分片的话,我们是写入日志来保证高可靠、周期性的从主分片,来拉取数据文件来提供读取。这样的话可以降低一倍的写入,整个写入的吞吐可以提升一倍,有非常好的一个效果。
3.性能优化
最后我们来看看性能优化方面。性能优化方面我们做的工作其实也比较丰富,从整个 ES 的存储层到执行器,到优化器都做了一些工作。
3.1 解决方案
存储层的话我们主要做的比如说时序的 Merge 来减少底层的小文件、优化 I/O,前面也说到了,这种数据上卷方面的工作来提升性能。
执行器的方面的话,我们比如说前面提到的对冲查询、文件的裁剪等等,这里面也有部分 Patch 我们已经反馈给开源社区。
优化器这一块方面的话,比如说,RBO(Rule -Based Optimization)的这种优化,然后分区的裁剪等。
总体的最终的效果是,我们对于这种搜索场景,我们的性能可以提升一倍,毛刺可以降低一个数量级,同时整个集群的线性扩展非常好。
3.2 时序 Merge
这里我们展开介绍一下 Merge 策略这一块。这里我们其实在实际场景下引入了一种新的 Merge,叫时序 Merge。ES 传统的 Merge 实际上是典型的考虑大小、考虑效率的 Merge,会尽量把大小相似的文件放到一起,并且限制单个文件的大小上限。这种方式有一个不好的地方在于底层文件还是相对较多,有 30 个文件,所以大家分析的时候涉及到大量的 I/O。
然后另一个方面的话,因为底层数据文件的时间上面并不是连续的,一个文件可能包含了 1 月份整月的数据跟 3 月 1 号的数据,所以这个时候如果大家去查询 3 月初的数据的话,一个问题是底层大量文件都需要扫描,不利于裁减。开源社区里面有很多这种 Merge 方案,典型的以 LevelDB 为例,LevelDB 底层也实现了自己的 Merge 策略,或者叫 Compact 策略。数据在产生的时候放在 level 0 层,然后数据按照这种 Level 0 层,或者下面层次的文件的个数,从上层往下去 Merge,最终来实现整个数据的管理。
这种模式非常适合这种点查询或者是 QV 系统,内存中因为保存了这些文件的一定的索引信息,我们进行点查询的时候,它根据索引信息可以做快速的裁剪,最终只需要大家去扫描少量几个文件就可以返回结果。但是对 ES 实际上不太适合的,因为 ES 它通常需要去读取较多的数据,那么就涉及到底层这么多文件的扫描,小文件太多了,所以我们引入了一个时序的 Merge 的方案。
大概的思路是,**首先我们会在 ES 这种分层的 Merge 基础上引入时间序的一个概念,相当于是同一个文件内部的数据,时间上尽量是连续的,方便大家裁剪。另一个方面是我们会做冷数据的 Merge,日志、时序数据,很多天以前的数据其实不太访问了,所以我们可以把它的文件数做一个收敛来提高性能。**最终我们在时序的 Merge 的效果是,基本上对产品的性能可以提升一倍。
四、总结及未来规划
前面介绍完了这么多内容之后,我们做一个简单的总结。
1.现状总结
从目前的状况来看,整体上我们对 ES 的可用性、成本、性能这些方面都做了很多的优化,在整个社区里面也处于一个领先的地位。然后在与集群的管控托管这一块,其实我们也提供了一个成熟的托管平台,包含集群的管控操作、监控、巡检这种丰富的平台。
另外我们目前业务发展也达到了一个非常快速的阶段,支持了日志实时分析、全文检索、时序处理等过程,而且我们与官方的 Elastic 生态有很好的兼容。
2.未来发展
接下来看看未来的一个规划,其实前面说了,在日志跟时序场景下,成本和数据的价值始终是一个矛盾,所以我们会从这两个维度着手去解决。成本方面,我们会持续地从资源调度、系统架构方面持续地去优化。然后对于业务价值方面,我们采用的措施是,一方面会去和大数据生态打通,方便大家来利用大数据生态里面的数据;另一方面其实大家对数据的利用典型的划分,可以理解成数据工程类型的利用,比如说流批的处理、数据的搜索分析(搜索分析比如说大家去做一些交互式的运营分析)。
第三类是在线的 App,而在搜索分析这个领域里面包含了非常多的系统、非常多的软件,比如说搜索领域的 ES Solr,分析领域里面的 ClickHouse、Doris 等各类系统,里面的系统非常多,所以我们希望基于 ES 去扩展交互式分析领域,在一套系统里面来提供 PB 级日志的闭环的解决方案,帮助用户去进一步地挖掘数据的价值,同时减少这种维护的系统的数量,降低维护成本。
3.开源协同
最后一块的话,我们来看一看开源协同的部分。整个开源协同这一块,我们向社区做了大量的贡献,包含源码 Patch 上的贡献,我们是整个亚太地区 ES 贡献最大的一个团队;其次在社区活动方面,我们也积极参加社区里面的这种技术峰会 Meetup,然后技术文稿也在持续地输出。
当然对开源社区的贡献,一方面有利于社区的发展,另一方面对于我们个人、对于团队也有非常好的收益。技术层面的话,首先可以降低我们的版本维护成本,我们的分支上的功能特性相对于开源社区会保持跟进得比较紧密,随时了解开源社区的动态;另一方面人才培养这块的话,开源社区这种开放透明、高效的这种开发模式,也有利于大家培养一个好的开发习惯;在影响力方面,我们也得到了包含 Elastic 公司的 CEO 开发者的认可、社区组织的认可,也有利于我们去吸引人才,去助力产品的发展。最后还是倡议大家一起投入到这种开源社区的反馈的过程中,最终实现开源社区、企业、个人共赢的过程。
案例复盘
需要注意的陷阱是,ES 的可用性方面有些问题。一些典型的陷阱,比如说集群的扩展性,如果大家使用开源的 ES 的话,我们建议集群里面的表的数量不要超过 1 万,然后集群的节点数不要超过 100 个,来保证比较平稳的运行。当然如果大家有更高的需求的话,需要参考社区去调优,做一些最佳实践的参考,这是这一个方面的陷阱。
另一个方面的一些陷阱,如果大家在遇到一些 ES 使用方面的问题,或者是资源不足方面的一些假象的时候,尽量从机器资源这些角度层面,从根本原因上进行细致的分析,这样的话来找到问题,而避免盲目地去扩容解决问题。