🍊 集群架构设计
为了提高Elasticsearch服务的整体可用性,需要合理的部署集群架构。Elasticsearch集群采用主节点、数据节点和协调节点分离的架构,即将主节点和数据节点分开布置,同时引入协调节点,以实现负载均衡。在5.x版本以后,数据节点还可进一步细分为Hot-Warm的架构模式。
🎉 主节点和数据节点
在Elasticsearch配置文件中,有两个非常重要的参数,分别是node.master和node.data。这两个参数配合使用,可以提高服务器的性能。主节点配置node.master:true和node.data:false,表示该节点只作为一个主节点,不存储任何索引数据。推荐每个集群运行3个专用的Master节点,以提高集群的弹性。在使用时,还需设置discovery.zen.minimum_master_nodes参数为2,以避免脑裂的情况。因为三个主节点仅负责集群的管理,不包含数据、不进行搜索和索引操作,因此它们的CPU、内存和磁盘配置可以比数据节点少很多。
数据节点配置node.master:false和node.data:true,只作为一个数据节点,专门用于存储索引数据,实现功能单一,降低资源消耗率。Hot-Warm架构是将数据节点分成热节点和暖节点,热节点只保存最新的数据,暖节点则保存旧的数据,以实现不同数据的不同存储需求。
引入协调节点可以实现负载均衡,减少节点间的通信压力,提升服务的整体性能。在协调节点上可以运行诸如Kibana、Logstash、Beats等工具,以进行数据可视化、数据采集等操作。在配置协调节点时,还需设置discovery.zen.minimum_master_nodes参数为2,避免脑裂的情况。
总之,合理的部署Elasticsearch集群架构,可以提高服务的整体可用性,减少节点间的通信负担,降低资源消耗率,优化服务的整体性能。
🎉 hot节点和warm节点
hot节点主要是用于存储索引数据并保存最近频繁被查询的索引。由于索引是一项CPU和IO密集型操作,因此建议使用SSD磁盘类型来保持高性能的写入操作。同时,为了保证高可用性,建议至少部署3个最小化的hot节点。如果需要增加性能,可以增加服务器数量。要将节点设置为hot类型,elasticsearch.yml应该包含以下配置:node.attr.box_type:hot。对于针对指定索引操作,可以通过设置index.routing.allocation.require.box_type:hot使其将索引写入hot节点。
warm节点主要是处理大量不经常访问的只读索引的设计。由于这些索引是只读的,因此warm节点倾向于挂载大量普通磁盘来替代SSD。内存和CPU的配置应该与hot节点保持一致,节点数量一般也应该大于或等于3。要将节点设置为warm类型,elasticsearch.yml应该包含以下配置:node.attr.box_type:warm。同时,也可以在elasticsearch.yml中设置index.codec:best_compression以保证warm节点的压缩配置。当索引不再频繁查询时,可以使用index.routing.allocation.require.box_type:warm将索引标记为warm,从而确保索引不写入hot节点,以便将SSD磁盘资源用于处理更为关键的操作。一旦设置了该属性,Elasticsearch会自动将索引合并到warm节点。
🎉 协调节点
协调(coordinating)节点是分布式系统中的一个节点,用于协调多个分片或节点返回的数据,进行整合后返回给客户端。该节点不会被选作主节点,也不会存储任何索引数据。在Elasticsearch集群中,所有的节点都有可能成为协调节点,但可以通过设置node.master、node.data、node.ingest都为false来专门设置协调节点。协调节点需要具备较好的CPU和较高的内存。在查询时,通常涉及从多个node服务器上查询数据,并将请求分发到多个指定的node服务器,对各个node服务器返回的结果进行汇总处理,最终返回给客户端。因此,协调节点在查询负载均衡方面发挥了重要的作用。除此之外,可以通过设置node.master和node.data的值来特别指定节点的功能类型,如:node.master:false和node.data:true,该节点仅用于数据存储和查询,node.master:true和node.data:false,该节点仅用于协调请求等。设置节点的功能类型可以使其功能更加单一,从而降低其资源消耗率,提高集群的性能。
🎉 关闭data节点服务器中的http功能
在Elasticsearch集群中,关闭data节点服务器中的http功能是一种有效的保障数据安全和提升服务性能的方法。具体实现方式是对所有的数据节点进行http.enabled:false的配置参数设置,同时不安装head、bigdesk、marvel等监控插件,这样data节点服务器只需处理索引数据的创建、更新、删除和查询等操作。而http服务可以在非数据节点服务器上开启,相关监控插件也可以安装到这些服务器上,用于监控Elasticsearch集群的状态和数据信息。通过这种方法,可以在保证数据安全的前提下,提升Elasticsearch集群的服务性能。
🎉 一台服务器上只部署一个node
在一台物理服务器上,可以通过设置不同的启动端口来启动多个node服务器节点。然而,由于服务器的CPU、内存、硬盘等资源是有限的,当在同一台服务器上启动多个node节点时,会导致资源的竞争和争夺,从而影响服务器性能。因此,建议在进行服务器节点部署时,将不同的node节点部署在不同的物理服务器上,从而实现资源的充分利用,提高服务器性能和可靠性。同时,为了确保多个node节点之间的通信和协调,可以使用负载均衡器等技术手段来实现节点间的负载均衡和故障转移,从而保障应用程序的稳定运行。
🎉 集群分片设置
在Elasticsearch中,一旦创建好索引后,就不能再调整分片的设置。由于一个分片对应于一个Lucene索引,而Lucene索引的读写会占用大量的系统资源,因此分片数不能设置过大。因此,在创建索引时,合理配置分片数是非常重要的。一般来说,应当遵循以下原则:
(1)控制每个分片占用的硬盘容量不超过Elasticsearch的最大JVM堆空间设置,通常不应超过32GB。因此,如果索引的总容量在500GB左右,分片大小应在16个左右。当然,最好同时考虑原则2。
(2)考虑节点数量。通常情况下,每个节点对应一台物理机。如果分片数过多,即大大超过了节点数,很可能会导致在某个节点上存在多个分片。一旦该节点发生故障,即使保持了一个以上的副本,仍然有可能导致数据丢失,从而无法恢复整个集群。因此,一般都设置分片数不超过节点数的三倍。
🍊 可用性优化
在可用性方面,Elasticsearch原生版本存在三个问题:
(1)系统健壮性不足:系统在面对意外情况时无法保持正常运行,具体表现为系统容易导致集群雪崩和节点OOM。这种情况在大流量的情况下尤为明显,此时系统的负载会变得非常高,容易导致节点内存耗尽,甚至导致集群崩溃。造成这种情况的主要原因是内存资源不足和负载不均。具体来说,内存资源不足可能是由于系统中存在内存泄漏或者内存管理不当等引起的。而负载不均则可能是由于集群中的节点在处理任务时,存在一些节点负载过高,而另一些节点负载过轻的情况。为了解决这个问题,需要采取一些优化措施,以提升系统的健壮性和稳定性。具体来说,可以优化服务限流和节点均衡策略。限流策略的作用是控制系统的访问量,防止系统因为大量请求而导致崩溃或者响应变得异常缓慢。节点均衡策略则是通过对任务进行分配,以使得每个节点的负载均衡,避免一些节点负载过高,而另一些节点负载过轻的情况。这些优化措施可以有效增强系统的健壮性,使得系统更加稳定可靠。
(2)容灾方案欠缺:尽管Elasticsearch自身提供了副本机制,以确保数据的安全性,但是对于涉及多个可用区的容灾策略,需要云平台额外实现。此外,即使在存在副本机制和跨集群复制的情况下,仍然需要提供低成本的备份回滚能力,以应对可能存在的误操作和数据删除的风险。针对这些问题,建议采取以下措施:首先,对于多可用区容灾方案的实现,可以考虑采用云平台提供的跨可用区副本和快照备份功能,以确保数据可靠性和可用性。其次,可以部署多个集群,实现数据的跨集群复制,以进一步提高数据的安全保障。同时,需要在数据备份和回滚方面做好充分的准备,确保在发生误操作或数据删除时能够迅速恢复数据。
(3)内核Bug:Elasticsearch是一种开源搜索引擎,其内核存在一些Bug,可能会影响其可用性。为了解决这些问题,Elasticsearch修复了一系列与内核可用性相关的问题,包括Master任务堵塞、分布式死锁、滚动重启速度慢等问题。此外,为了确保用户能够及时获得修复后的版本,Elasticsearch及时提供了新版本给用户升级。这些措施充分展示了Elasticsearch对用户可用性的关注,并且以负责任的方式解决问题。这些改进将有助于提升Elasticsearch的性能和可靠性,进一步满足用户在搜索领域的需求。
接下来,将针对用户在可用性层面常遇到的两类问题展开分析:
(1)当高并发请求过多时,会导致集群崩溃的问题。为了解决这个问题,可以采用一些方法来提升集群的吞吐能力。其中,可以优化集群的配置,例如增加硬件资源、提升网络带宽、调整线程池大小等。另外,可以采用异步I/O方式来提高请求的处理效率,从而缓解集群压力。此外,负载均衡技术也是一个值得推荐的方法,通过将请求分配到不同的节点上,可以避免某些节点过载而导致集群崩溃的情况发生。总之,需要采取多种方法综合应对高并发请求的问题,从而提升集群的稳定性和吞吐能力。
(2)当进行单个大查询时,很容易出现节点因负载过大而崩溃的情况。为了解决这个问题,可以采用一些优化措施。首先是数据分片和副本,这可以将数据分散到多个节点上,减少单个节点的负载,同时保证数据的可靠性和高可用性。其次是搜索建议,它可以根据用户输入的关键词提供相关的搜索建议,减少用户不必要的查询请求。最后是聚合结果优化,它可以对查询结果进行聚合,减少不必要的数据传输和计算,提高查询效率和稳定性。通过这些优化措施,可以有效地减轻单个查询对节点的负载,提升系统的查询效率和稳定性,达到更好的用户体验和服务质量。
🍊 高并发请求压垮集群
高并发请求是一种常见的场景,可能会导致集群崩溃。例如,早期内部的一个日志集群,其中写入量在一天内突然增加了5倍,导致集群中的多个节点的Old GC卡住而脱离集群,集群变成了RED状态,写入操作停止了。这个场景可能会对集群造成很大的损失。对于挂掉的节点,进行内存分析后发现,大部分内存都被反序列化前后的写入请求所占用。这些写入请求是堆积在集群的接入层位置上的。接入层是指用户的写入请求先到达其中一个数据节点,称之为数据节点。然后由该协调节点将请求转发给主分片所在节点进行写入,主分片写入完毕再由主分片转发给从分片写入,最后返回给客户端写入结果。从内存分析结果看,这些堆积的位置导致了节点的崩溃,因此根本原因是协调节点的接入层内存被打爆。
经过问题原因的分析,制定了针对高并发场景下的优化方案。这个方案包括两个关键点:加强对接入层的内存管理和实现服务限流。为了避免集群崩溃,需要确保接入层内存不会被输入请求打爆,因此需要加强内存管理。在实现服务限流的方面,需要一个能够控制并发请求数量,并且能够精准地控制内存资源的方案。这个方案还要具有通用性,能够作用于各个层级实现全链限流。
🍊 服务限流
一般情况下,数据库的限流策略是从业务端或者独立的代理层配置相关的业务规则,进行资源预估等方式进行限流。但是这种方式适应能力较弱、运维成本高、业务端很难准确地预估资源消耗。原生版本本身也有限流策略,但是单纯地基于请求数的限流不能控制资源使用量,而且只作用于分片级子请求的传输层,对于接入层无法起到有效的保护作用。
因此,优化方案是基于内存资源的漏桶策略。将节点JVM内存作为漏桶的资源,当内存资源足够的时候,请求可以正常处理,当内存使用量到达一定阈值的时候分区间阶梯式平滑限流,处理中的请求和merge操作都可以得到保证,从而保证节点内存的安全性。这个方案不仅可以控制并发数,还可以控制资源使用量并且具有通用性,可以应用于各个层级实现全链限流。
在限流方案中,一个重要的挑战是如何实现平滑限流。采用单一的阈值限流很容易出现请求抖动的情况,例如请求一上来就会立即触发限流,因为内存资源不足,而稍微放开一点请求量又会迅速涌入,使内存资源再次极度紧张。因此,通过设置高低限流阈值区间、基于余弦变换实现请求数和内存资源之间的平滑限流方案。在该区间中,当内存资源足够时,请求通过率达到100%;当内存到达限流区间时,请求通过率逐步下降。而当内存使用量下降时,请求通过率也会逐步上升,而不是一下子放开。经过实际测试,平滑的区间限流能够在高压力下保持稳定的写入性能。平滑限流方案是对原生版本基于请求数漏桶策略的有效补充,作用范围更广泛,能覆盖协调节点、数据节点的接入层和传输层。但需要说明的是,该方案并不会替代原生的限流方案,而是对其进行有效的补充。
🍊 单个大查询打挂节点
在某些分析场景中,需要进行多层嵌套聚合,这可能导致返回的结果集非常大,因此可能导致某个请求将节点打挂。在这种聚合查询流程中,请求首先到达协调节点,然后被拆分为分片级子查询请求,然后发送给目标分片所在的数据节点进行子聚合。最后,协调节点将所有分片结果收集并进行归并、聚合、排序等操作。然而,这个过程中存在两个主要问题点。
第一个问题是,当协调节点大量汇聚结果并反序列化之后,可能会导致内存膨胀。这可能是由于结果集太大,或者节点内存不足等原因造成的。
第二个问题是,二次聚合可能会产生新的结果集,这可能导致内存爆炸。
为了解决上述单个大查询的问题,可以采用以下五个优化方案。
首先,针对内存开销问题,可以通过增加节点内存大小或将查询结果进行分批处理来优化。这样能够在降低内存使用率的同时,提高查询效率。
接着,针对内存浪费严重的写入场景,优化方案主要是实现弹性的内存buffer,并对于读写异常的请求及时进行内存回收。要注意,这里所提到的内存回收策略并不是指GC策略。JVMGC债务管理主要评估JVMOldGC时长和正常工作时长的比例来衡量JVM的健康情况,特殊情况下会重启JVM以防止长时间hang死,这与内存回收策略是两个不同的方面。通过内存利用率优化,整个公有云的Elasticsearch集群的可用性得到了提升,达到了4个9。内存利用率提升了30%,在高压力场景下节点稳定性更强,基本能保证节点不会OOM,集群也不会雪崩。总的来说,内存利用率,内存回收策略以及JVMGC债务管理都是优化内存利用率的重要方面,它们能够提高系统吞吐量,减少节点OOM的发生,保障系统的稳定性和可用性。通过对这些方面的优化,能够有效地提高系统的性能表现,使系统更加健康和稳定。
其次,在进行聚合操作时,需要特别注意减少中间结果的存储和传输。对于大规模数据集的查询,优先考虑使用分布式计算框架,如Apache Spark等。
然后,在数据库管理中,单个查询内存限制是一个非常有用的功能。当一个查询过于庞大时,其会占用大量的内存资源,从而影响其他所有请求的响应时间。通过设置单个查询内存限制,可以有效地控制查询的内存使用量,从而保证整个数据库系统的正常运行。除此之外,滚动重启速度优化也是一个非常实用的功能。尤其是在大规模集群环境下,单个节点的重启时间往往较长,如果要重启整个集群,可能会导致整个系统长时间处于不可用状态。通过优化滚动重启速度,可以将单个节点的重启时间从10分钟降至1分钟以内,大幅缩短了重启时间,从而提高了系统的可用性。值得一提的是,这个优化已经在7.5版本中被合并了,因此用户不需要再自己手动进行配置。如果遇到大集群滚动重启效率问题,可以关注此功能,以提高数据库系统的可靠性和稳定性。
最后,第三个优化方案的重点是内存膨胀预估加流式检查。该方案主要分为两个阶段:第一阶段在协调节点接收数据节点返回的响应结果反序列化之前做内存膨胀预估,并在内存使用量超过阈值时直接熔断请求;第二阶段在协调节点reduce过程中,流式检查桶数,每增加固定数量的桶检查一次内存,如果超限则直接熔断。这样用户不再需要关心最大桶数,只要内存足够就能最大化地满足业务需求。不足之处是大请求还是被拒掉了,但是可以通过官方已有的batch reduce的方式缓解,即每收到部分子结果就先做一次聚合,这样能降低单次聚合的内存开销。该方案已经提交给官方并合并了,将在最近的7.7.0版本中发布。
🍊 性能优化
性能优化的场景可以分为写入和查询两个部分。在写入方面,主要包括海量时序数据场景,如日志和监控,通常能够实现千万级别的吞吐。带有id的写入会导致性能衰减一倍,因为需要首先查询记录是否存在。在查询方面,主要包括搜索场景和分析场景。搜索服务需要高并发并且具有低延迟;而聚合分析主要涉及大型查询,需要大量的内存和CPU开销。
从性能影响面的角度来看,硬件资源和系统调优通常是直接可控的,例如资源不足时可以进行扩容,调整参数深度来进行调优等。然而,存储模型和执行计划通常涉及内核优化,因此普通用户难以直接进行调整。接下来,将重点介绍存储模型和执行计划的优化。
存储模型的优化是一个关键问题。Elasticsearch底层Lucene基于LSM Tree的数据文件。原生默认的合并策略是按文件大小相似性合并,一次性固定合并10个文件,采用近似分层合并。这种合并方式最大的优点是效率高,可以快速降低文件数。但是,文件不连续会导致查询时的文件裁剪能力较弱,例如查询最近1小时的数据,有可能会将1小时的文件拆分到几天前的文件中,进而增加了必须检索的文件数量。业界通常采用解决数据连续性的合并策略,例如基于时间窗口的合并策略,如以Cassandra、HBase为代表的策略。其优点在于数据按时间序合并,查询效率高,还可以支持表内TTL。缺点是仅适用于时序场景,并且文件大小可能不一致,从而影响合并效率。另一类策略由LevelDB、RocksDB为代表的分层合并策略构成,一层一组有序,每次抽取部分数据向下层合并,优点在于查询高效。但如果相同的数据被合并多次,这将影响写入吞吐。
最后是优化合并策略,其目标是提高数据连续性、收敛文件数量,提升文件裁剪的能力以提高查询性能。实现策略是按时间序分层合并,每层文件按创建时间排序。除了第一层外,所有层次都按照时间序和目标大小进行合并,而不是固定每次合并文件数量,保证了合并效率。对于少量未合并的文件和冷分片文件,采用持续合并策略,将超过默认5分钟不再写入的分片进行持续合并,并控制合并并发和范围,以降低合并成本。
🍊 执行引擎的优化
在Elasticsearch中,有一种聚合叫做Composite聚合,它支持多字段的嵌套聚合,类似于MySQL的group by多个字段,同时也支持流式聚合,即以翻页的形式分批聚合结果。使用Composite聚合时,只需要在查询时聚合操作下面指定composite关键字,并指定一次翻页的长度和group by的字段列表,每次拿到的聚合结果会伴随着一个after key返回,下一次查询可以拿着这个after key查询下一页的结果。
Composite聚合的实现原理是利用一个固定size的大顶堆,size就是翻页的长度,全量遍历一把所有文档迭代构建这个基于大顶堆的聚合结果,最后返回这个大顶堆并将堆顶作为after key。第二次聚合时,同样的全量遍历一把文档,但会加上过滤条件排除不符合after key的文档。然而,这种实现方式存在性能问题,因为每次拉取结果都需要全量遍历一遍所有文档,并未实现真正的翻页。
为了解决这个问题,提出了一种优化方案,即利用index sorting实现after key跳转以及提前结束(earlytermination)。通过index sorting,可以实现数据的有序性,从而实现真正的流式聚合,大顶堆仍然保留,只需要按照文档的顺序提取指定size的文档数即可快速返回。下一次聚合时,可以直接根据请求携带的afterkey做跳转,直接跳转到指定位置继续向后遍历指定size的文档数即可返回。这种优化方案可以避免每次翻页全量遍历,大幅提升查询性能。
在Elasticsearch7.6版本中,已经实现了覆盖数据顺序和请求顺序不一致的优化场景。该版本在性能层面进行了全面优化,从底层的存储模型、执行引擎、优化器到上层的缓存策略都有相应的提升。具体来说,该版本在存储模型方面采用了更加高效的数据结构和算法,以减少磁盘I/O、内存消耗等问题,提高读写性能。在执行引擎方面,优化了查询和聚合操作的执行过程,使其能快速响应请求并返回数据。而在优化器方面,针对不同的查询场景进行了优化,以减少计算量,提高查询效率。最后,在缓存策略方面,采用了更加智能的缓存机制,以加速常用请求的响应速度。以上这些优化措施的整合,实现了对覆盖数据顺序和请求顺序不一致的场景的优化,并带来了更高效、更可靠的Elasticsearch体验。
🍊 成本优化
在大规模数据场景下,优化成本是一个非常重要的问题。在此过程中,需要重点关注集群的 CPU、内存和磁盘三个方面。根据实际情况,这三个方面的成本占比一般为1比4比8。也就是说,磁盘和内存成本占比相对较高,需要着重考虑。举个例子,一般的16核64GB,2-5TB磁盘节点的成本占比也大致如此。因此,在成本优化过程中,主要的瓶颈就在于磁盘和内存的使用。在实际操作中,可通过对磁盘和内存的使用进行优化,以降低成本,提高效率。
成本优化的主要目标是存储成本和内存成本。
🎉 存储成本优化
Elasticsearch单个集群能够处理千万级别的写入操作,但实现千万每秒的写入量需要考虑多方面因素,如硬件配置、索引设计、数据量和查询复杂度等,而业务需要保留至少半年的数据供查询。
假设单集群平均写入速度为1000万OPS,意味着在半年的时间内,共有60 * 60 * 24 * 180 = 15552000秒。每个文档大小为50Byte,且基于高可用需要2个副本,因此计算公式为:1000万(OPS) * 86400(秒) * 180(天) * 50Byte(平均文档大小) * 2(副本)等于14PB。即此集群需要14PB的存储空间。假设每台物理机的内存和硬盘都能够完全用于Elasticsearch存储,那么:1PB = 1024TB,14PB = 14 * 1024 = 14336TB。然而,一个物理机可以存储多少数据,取决于该数据的复杂度、索引方式、查询频率等因素。假设平均每台物理机可以存储800GB数据,则此集群需要的物理机数量为:14336TB ÷ 0.8TB/台 = 17920台。这么多的物理机数量的成本远远超出了业务成本预算。因此,需要采用其他方式,在不牺牲性能的情况下减少存储需求,以适应业务预算。
为了提高对Elasticsearch系统的效率和成本效益,可以采取多种优化措施。首先,可以通过调研业务数据访问频率,将历史数据进行冷热分离,将冷数据放入HDD中来降低存储成本。同时,索引生命周期管理可用于数据搬迁,将冷数据盘利用多盘策略提高吞吐和数据容灾能力。此外,超冷数据可以通过冷备到腾讯云对象存储COS中来降低成本。 其次,通过分析数据访问特征,可以采用Rollup方案降低历史数据的精度并降低存储成本。Rollup方案利用预计算来释放原始细粒度数据,例如将秒级数据聚合成小时级和天级,以方便展示跨度较长的跨度报表。Rollup方案还显著降低存储成本和提高查询性能。Rollup优化方案主要基于流式聚合加查询剪枝结合分片级并发来实现高效性。分片级并发可以通过添加routing来实现,让相同的对象落到相同的分片内,并能实现分片级并发。此外,通过对Rollup任务资源预估,并感知集群的负载压力来自动控制并发度,从而避免对集群整体的影响。综上所述,通过冷热分离、索引生命周期管理、多盘策略、对象存储和Rollup方案等手段,可以从架构层对Elasticsearch进行优化,实现同时满足业务需求和成本效益。
🎉 内存成本优化
目前,很多情况下堆内存使用率过高,而磁盘使用率相对较低。FST是一种倒排索引,它通常常驻内存且占用较大的内存比例。为了节省内存空间并保持快速访问,FST使用了自适应前缀编码技术,但这也导致了在查询时需要解压缩FST,从而占用大量的堆内存空间。因此,将FST移至堆外(off-heap)并按需加载FST可以显著提高堆内内存利用率并降低垃圾回收开销,从而提高单个节点对磁盘的管理能力。
具体来说,在每10TB的磁盘中,FST需要10GB到15GB的内存来存储索引。为了减小内存占用,可以将FST从堆内存中移至堆外,这种方式可以单独管理并减轻对JVM垃圾回收的影响。除此之外,这种优化方案还可以显著降低堆内内存中FST的占用比例,提高堆内内存利用率,并降低GC开销。同时,使用off-heap内存还可以降低GC的次数和持续时间,从而提高整个系统的性能和稳定性。
因此,需要在使用FST索引时考虑内存占用的问题,并将FST移至堆外并按需加载FST,以提高系统的性能和稳定性。这个优化方案可以显著提高堆内内存利用率,降低GC开销,并提升单个节点管理磁盘的能力。
原生版本实现 off-heap 的方式,将 FST 对象放到 MMAP 中管理。虽然这种方式实现简单,但是有可能会被系统回收掉,导致读盘操作,进而带来性能的损耗。HBase 2.0 版本中 off-heap 的实现方式,则是在堆外建立了 cache,但是索引仍然在堆内,而且淘汰策略完全依赖于 LRU 策略,冷数据不能及时清理。而在堆外建立 cache,可以保证 FST 的空间不受系统影响,实现更精准的淘汰策略,提高内存使用率,同时采用多级 cache 的管理模式来提升性能。虽然这种方式实现起来比较复杂,但是收益很明显。因此,读者可以根据实际需求选择合适的 off-heap 方案。
为了优化访问FST的效率,可以考虑采用一种综合方案,即LRUcache+零拷贝+两级cache。在这种方案中,LRUcache被建立在堆外,并且当堆内需要访问FST时,会从磁盘加载到LRUcache中。由于Lucene默认的访问FST的方式使用一个堆内的buffer,直接从堆外拷贝到堆内的buffer会占用大量的时间和资源。因此,对Lucene访问FST的方式进行了改造,将buffer不直接存放FST,而是存放指向堆外对象的指针,这样就实现了堆内和堆外之间的零拷贝。
需要注意的是,这里的零拷贝和操作系统中的用户态和内核态的零拷贝是两个不同的概念。但是,根据key去查找堆外对象的过程也会损耗一部分性能,例如计算hash、数据校验等。为了进一步优化性能,可以利用Java的弱引用建立第二层轻量级缓存。弱引用指向堆外的地址,只要有请求使用,这个key就不会被回收,可以重复利用而无需重新获取。一旦不再使用,这个key就会被GC回收掉,并回收掉堆外对象指针。但是,堆外对象指针回收之后需要清理堆外内存,不能浪费一部分内存空间。为了解决这个问题,最好的办法是在堆内对象地址回收的时候直接回收堆外对象。然而,Java没有析构的概念,可以利用弱引用的ReferenceQueue,在对象要被GC回收时将对象指向的堆外内存清理掉,这样就可以完美解决堆外内存析构的问题,并提高内存利用率。
为了帮助读者更好地理解上述内容,本节将采用以下故事的形式对上述内容进行再次解析:
曾经有一个叫做小明的程序员,他在开发一个搜索引擎的项目中遇到了一个问题:搜索引擎的堆内存占用率过高,而磁盘的使用率却相对较低。他研究了一番后发现,这是由于搜索引擎中使用的倒排索引(FST)需要很大的内存比例才能常驻内存,而在查询时需要解压缩FST,占用了大量的堆内存空间。
小明急于解决这个问题,因此他开始了一段冒险之旅。他发现,将FST移至堆外并按需加载FST可以显著提高堆内内存利用率并降低垃圾回收开销,从而提高单个节点对磁盘的管理能力。他做了一些实验后,发现将FST放到MMAP中管理的方式实现简单,但是有可能会被系统回收掉,导致读盘操作,进而带来性能的损耗。而在堆外建立cache,可以保证FST的空间不受系统影响,实现更精准的淘汰策略,提高内存使用率,同时采用多级cache的管理模式来提升性能。
在这个过程中,小明还研究了一种综合方案:LRUcache+零拷贝+两级cache。他把LRUcache建立在堆外,并且当堆内需要访问FST时,会从磁盘加载到LRUcache中。同时,他利用Java的弱引用建立第二层轻量级缓存,指向堆外的地址,只要有请求使用,这个key就不会被回收,可以重复利用而无需重新获取。一旦不再使用,这个key就会被GC回收掉,并回收掉堆外对象指针。最后,小明成功地实现了从堆内移动FST到堆外的优化方案,并大幅提高了系统的性能和稳定性。他心满意足地把这个方案分享给了同事们,为搜索引擎的开发作出了重大贡献。
🍊 扩展性优化
Elasticsearch中的元数据管理模型是由Master节点来管理元数据,并同步给其他节点。以建索引流程为例,首先Master节点会分配分片,并产生差异的元数据,这些元数据会发送到其他节点上。当大多数Master节点返回元数据后,Master节点会发送元数据应用请求,其他节点开始应用元数据,并根据新旧元数据推导出各自节点的分片创建任务。在这个过程中有一些瓶颈点,主要有以下几点:
首先,Mater节点在分配分片时需要进行正反向转换。由于路由信息是由分片到节点的映射,而在做分片分配时需要节点到分片的映射,因此需要知道每个节点上的分片分布。因此,分片分配完毕后还需要将节点到分片的映射转换回来。这个转换过程涉及多次的全量遍历,这在大规模分片的情况下会存在性能瓶颈。其次,在每次索引创建的过程中,会涉及多次元数据同步。在大规模的节点数场景下,会出现同步瓶颈,由于节点数量过多,可能会出现某些节点一些网络抖动或Old GC等问题,导致同步失败。为了解决以上问题,可以从三个方面进行优化:首先,采用任务下发的方式,定向下发分片创建任务,避免了多次全节点元数据同步,从而优化分片创建导致的元数据同步瓶颈。其次,针对分配分片过程中多次正反向遍历的问题,采用增量化的数据结构维护的方式,避免了全量的遍历,从而优化分配分片的性能问题。最后,为了优化统计接口的性能,采用缓存策略避免多次重复的统计计算,大幅降低资源开销。
为了帮助读者更好地理解上述内容,本节将采用以下故事的形式对上述内容进行再次解析:
在一个遥远的星系中,有一个由许多机器人组成的世界。这些机器人可以相互通信,进行各种任务。其中,有一个机器人被选为主节点,负责管理所有机器人的信息。这个主节点的职责很重要,因为它需要协调所有机器人的工作,尤其是在建索引流程中。每当有任务需要建立索引时,主节点会分配分片,并产生差异的元数据,这些元数据会发送到其他机器人上。但是,这个过程并不总是顺利的。主节点需要在分配分片时进行正反向转换,这样每个机器人才能知道自己需要建立哪些索引。这个转换过程需要多次的全量遍历,如果分片数量很多,这个过程会非常耗时。另外,每次索引创建的过程中,也会涉及多次元数据同步。而在大规模的机器人场景下,可能会出现同步瓶颈,导致同步失败。
为了解决这些问题,机器人们开始探索各种优化方案。首先尝试了任务下发的方式,定向下发分片创建任务,避免了多次全节点元数据同步,从而优化了分片创建导致的元数据同步瓶颈。接着,采用增量化的数据结构维护的方式,避免了全量的遍历,从而优化了分配分片的性能问题。最后,采用了缓存策略来优化统计接口的性能,避免多次重复的统计计算,大幅降低了资源开销。这些优化措施让机器人们的工作更加顺利,他们可以更快地完成任务,并且不再遇到瓶颈问题。同时,这些措施也为未来的工作提供了一个优化的思路,让机器人们可以不断地改进和进步。
🍊 分析性能问题
分析性能问题的路径可以遵循以下步骤:首先,明确性能问题后,进行流量录制,以获取一个用于后续基准压测的测试集合。随后,使用相关的性能分析工具,首先明确是否存在CPU热点或IO问题。对于Java技术栈,可以使用Scaple、JProfiler、Java Flight Recorder、Async Profiler、Arthas、perf等工具进行性能分析。
利用火焰图进行分析,配合源代码进行数据分析和验证。此外,在Elasticsearch中,也可以使用Kibana的Search Profiler协助定位问题。接下来,进行录制大量的流量,抽样分析后,以场景为例,通过Profiler分析,发现TermInSetQuery占用了一半以上的耗时。明确问题后,从索引和检索链路两侧进行分析,评估问题并设计多种解决方案,并利用Java Microbenchmark Harness(JMH)代码基准测试工具验证解决方案的有效性。最后,集成验证解决方案的最终效果。
🌟 7.熟练使用设计模式
不同营销策略的切换场景:策略模式; 对象的创建和管理场景:工厂模式; 奖励分配和活动参与场景:责任链模式;实时消息推送、互动交流场景:发布-订阅模式; 用户的行为响应和推送通知功能场景:观察者模式; 支付场景:策略模式 + 工厂模式 + 门面模式 + 单例模式; 业务投放场景:责任链模式; 平台积分红包发放场景:装饰者模式; 订单状态场景:状态模式+观察者模式; 开具增值税发票场景: 建造者模式 + 原型模式; 商品多级分类目录场景:组合模式+访问者模式; 记录核心审计日志场景: 模板方法模式; 查询ElasticSearch大量数据场景:迭代器模式;
🌟 8.抢购系统落地
以高并发、高性能、高可用的技术作为基础保障,重点突破库存与限购、防刷与风控、数据一致、热点隔离、动静分离、削峰填谷、数据兜底、限流与降级、流控与容灾等核心技术问题。抢购系统所涉及到的最核心的技术内容:
缓存设计:多级缓存与库存分割
分离策略:主从分离与动静分离
流量策略:负载均衡与加权处理
数据库优化:合理选择字段类型、索引设计、查询性能、分库分表(垂直拆分与水平拆分)
异步优化:系统异步化、缓存队列、缓冲队列
代码异步化处理:Servlet3异步化、使用MQ异步化、自定义异步化策略
使用多级缓存:本地缓存、Redis缓存、数据库缓存
缓存问题:缓存击穿、缓存穿透、缓存雪崩
合理使用锁:注意锁粒度、锁的获取与释放、锁超时
池化技术:线程池、连接池、对象池、缓冲区
SQL优化:尽量走索引、尽量减少关联查询、查询数据量尽量少
物理机极致优化:CPU模式优化、操作系统参数优化、套接字缓冲区优化、频繁接收大文件优化、网卡层面优化、TCP连接优化、Nginx优化、网关优化
单机Java进程极致优化:JVM优化、Tomcat优化、线程模型优化、Servlet3异步化、RPC框架调优、资源静态化、Vertx异步化
隔离策略:线程隔离、连接隔离、业务隔离、系统隔离、数据隔离、热点隔离(动态热点与静态热点)流量隔离、逻辑隔离、物理隔离
流量控制:预约设计、缓存设计、动态感知、把控参与人数、设置人数上限
削峰与限流:验证码、问答题、异步消息、分层过滤、服务网关限流、业务网关限流、应用层限流(线程池限流与API限流)
服务降级:读服务降级、写服务降级、简化系统功能、舍弃非核心功能、数据兜底
热点数据:读热点与写热点
服务容灾:同机房多部署、多机房部署、同城双活、异地多活
库存扣减设计:下单减库存、付款减库存、与扣减库存、库存扣减问题解决方案、秒杀系统扣减库存方案、Redis实现扣减库存、Redis+Lua解决超卖、Redis分割库存、商品维度限购、库存防超卖
限购规则:商品维度限购与个人维度限购
防刷策略:Nginx条件限流、Token机制防刷、布隆过滤器校验、黑名单机制
风控策略:完善用户画像、丰富业务场景、不断优化算法
可复用于任何需要支撑高并发、大流量的业务场景
🌟 9.工作经验
能独立或带领团队Java工程师成员完成服务端代码的研发工作,结合业务需求给出合理的技术解决方案,改进现有模块功能,提高系统的可扩展性,封装性,稳定性。深入挖掘业务需求,可0-1设计高可用、高并发、高伸缩的分布式项目架构,环境搭建、自动化部署、服务器环境线上排查、性能评估相关经验。拥有产品需求讨论、项目开发计划制定、控制项目风险、开发团队组建、技术小组日常管理、进度检验、成本管理、开发部署问题梳理、任务分配、负责指导、培训普通开发工程师、代码Review、审核开发工程师的设计与研发质量等经验。
🌟 10.项目经验
项目的业务背景、解决的问题、实现的效果和带来的价值(提高了公司效率、降低成本、增加收益)、在项目中的角色和能力、项目的整体架构和技术栈(使用的框架、数据库、服务器、使用什么设计模式、优化技巧、性能调优)、自己在项目中的角色和贡献(参与的模块、负责的功能、解决的问题、在项目中的领导能力、带领团队完成项目、项目管理经验)、给出真实的数据和指标(使用的数据量、用户量、处理速度、项目的规模)、项目的演示链接和作品代码演示链接。