@liuhui0803
2016-07-20T10:11:35.000000Z
字数 10502
阅读 2886
未分类
我管理着一个规模可观的ElasticSearch集群。多大?嗯,“大”只是相对而言的。确切地说在ElasticSearch数据节点这部分,目前的运行情况如下:
规模还在增长。个别集群的范围从48TB到PB规模不等,这里所说的“PB规模”也包括这样的集群:
从资源的角度来看,对现代化操作系统来说其实不算很大,但对ElasticSearch的世界来说已经不小了。一年多以来(使用ES 0.90.x - 2.2.x版)管理这些系统的过程中进行了无穷无尽的设计,制定各种运维策略,并获得了不少收获,我觉得有必要和大家分享一下。
本文涉及的每个话题都足以单独写一篇博文,或让读者花费数天甚至数周的时间做实验。如果要包含每个领域涉及的各种细节,本文的长度可能超过100页,因此我的目标是介绍在以某种规模运维ElasticSearch的过程中我认为一些比较重要的高层面结论。
本文会假设你已经多次听人说过:ElasticSearch实际上是一种分布式可复制数据库结构的抽象层,这种技术以Apache Lucene为基础,提供了(丰富的)搜索API。这种技术的索引实际上是Lucene实例。副本又是什么。是否要对专用Master使用仲裁(Quorum)等。我更希望分享一些通用的思维模式,例如如何运维,希望这些想法能对你的解决方案有所帮助。
那么,咱们开始吧!
和通用集群设计
这方面主要围绕两个因素介绍:我有一个索引驱动的工作负载,并且对数据保留有所要求(需要保留数天)。有关Shard数量的讨论往往会归结于妖术或是艺术,但实际情况比这个简单得多((...难道不是吗?)。
基本上,我们可以将Shard看作是一种基本的性能单位,不同时段可能会使用不同的性能描述文件(例如在持续创建索引时,资源需求的增长与合并操作中片段的大小有关,并会在片段大小达到最大值后趋于平稳)。另外取决于具体操作成本也是可预测的(例如大型合并操作会对垃圾回收程序造成极大压力,查询较大规模数据集会更长时间地占用查询线程池,导致查询复杂度增加并需要耗费更多CPU资源等)。最后,每个操作都是在明确的机械量(Mechanical quantity)中进行的。合并、搜索、索引等都需要线程参与。这些线程的数量需要精确定义(并且大部分时候需要可配置)。我们知道可以在每个Shard或每个节点上使用多少线程,而每个类型的操作其资源需求也有不同特征(CPU、磁盘、内存,或这些因素的组合)。这意味着一切都是可度量的。
ElasticSearch或其他任何复杂系统的问题在于,容量规划工作感觉更像是一种妖术或者艺术,因为大部分东西都是不可度量或不可控的。
我们的想法是:度量吧,控制吧。我们的集群是这样做的。
我们的标准数据节点使用了d2.2xlarge实例,这种实例包含8个内核的Xeon E5-2676v3处理器,61GB内存,以及6块2TB直接连接“可能是SATA或近线SAS的”磁盘。磁盘配置为6路LVM条带卷,提供11TB可用存储,(忽略fs和其他开销后)可稳定一致地提供约900MB/s的顺序读写速度*。
*(AWS不会对d2s的存储进行过量提供(Over-provision),整个Hypervisor包含24个主轴(Spindle),可将所有24个主轴分配给最大规模的8xl实例。不同规模下可以二等分访问资源,这样通过2xl便能获得6个磁盘/8个处理器内核/61GB内存。在测试中Hypervisor HBA还支持对所有磁盘同步饱和(Simultaneous saturation),这样用户就可以获得始终如一的性能。)
每个数据节点实际上充当了“性能单位”的“Shard容器”。选择任何一种标准化构建,借此创建单Shard索引并投入运转。随后会发现资源使用率呈现出一致的衰减和流动。举例来说,如果让索引操作满载运行,直到合并后的片段大小达到最大值,此时会看到CPU、存储IO、CPU、存储IO呈现周期性循环。这是片段合并工作进行压缩/解压缩(耗费CPU)以及将新生成的更大的片段保存到磁盘(大量顺序IO)操作造成的。合并操作达到最大化负载后会看到一定数量的处理器核心在满载运行合并线程(如果没有明确定义合并线程池的规模,则可根据CPU的核心数计算。请打开_nodes/_local/hot_threads
,在这里可以看到大量[[index-name][shard-num]: Lucene Merge Thread #xxx线程]。触发写入合并结果的操作后会看到磁盘将在一段固定不变的时间内持续满载。合并、写入、合并、写入。索引就是这样做的。
索引比查询更可预测。在indexing/bulk线程队列接近上限(如果需要监控什么的话,也许就应该从_cat/thread_pool
着手)或批(Bulk)索引所需的时间实在太久(我更关心这个问题)之前,你可以用某一“每秒文档数”速率(文档的大小和结构也真的非常重要)安全地将内容索引到一个Shard中。在我们的例子里,通过每个Shard每秒大约可以搜索2500篇文档。以这样的速率,合并操作会让3个核心满载(假设为了获得可预测性/伸缩能力,我们将每个Shard的合并线程池限制为3个,下文将提到这个问题),并且偶尔会用3-5秒将新生成的片段写入存储。虽然容量规划也要考虑自己的独特需求,但我的工作负载会优先处理索引任务,这样就可以确保5个核心和一个存储系统不会过载。我还可以在这个节点上另外创建一个Shard,实现每个节点5000文档/秒的速率。我们每个节点的索引容量大致就是这样。
限制为3个合并线程,处于峰值使用率下的一个Shard。hot_threads的输出结果显示在横线下方。
这些数字有可能是编造的,但相关概念是真实的。遵循这些建议而不要陷入有关集群规模或Shard数量的武断思维方式,借此可为每个Shard建立性能基线,这种方式很适合用在我们标准化的构建中。随后需要研究打算运行的工作负载并确定这些工作负载所需的Shard数量。最后还需要确定每台机器想要实现的工作负载密度(使用每台机器可实现峰值利用率的Shard数量控制)。需要多少预留余量(Headroom)才能保证节点有能力继续处理其他操作,例如查询、再平衡(Rebalance)、容错,或复制?我们需要通过计算得出所需Shard数量以及决定节点数量的预留余量需求。
只想实现40000文档/秒的速度,但不想过多考虑查询问题?
40000 / 2500 = 每索引16个Shard / 每节点2个Shard = 8 个节点
想要复制?直接将数量翻倍,使用16个节点就行了(不过伸缩并不是完全线性的,还有一些额外的内存和网络开销)。
说实在的,这种做法大部分情况下都可行。通过这样的计算进行估算可以进行粗略的伸缩,随后可以根据所获得的性能特征对伸缩进行微调(例如不同集群的查询性能各异)。我们已经成功地从500文档/秒的集群伸缩至超过150K的速度。
举例来说,如果之前每节点2个Shard的例子在满载的情况下可以实现5000文档/秒的速度,在对达到最大值的片段进行合并(外加大量线程开销*)时,我们最终让6个核心满负荷运行。空闲的2个核心足够处理查询负载吗?也许可以。但如果你希望频繁针对50GB索引数据执行复杂的正则表达式查询那肯定是无法满足的。
更高工作负载密度,更低预留容量:
相同工作负载在密度更低,预留容量更多情况下的表现:
一个重要问题:索引对资源的实际需求取决于很多因素。为了看到上文描述的行为,可能需要对ES的默认值进行大量有针对性的调整。正如上文所述,我的工作负载会优先处理索引。出于性能和伸缩性的考虑,将事件保存到磁盘中并让合并操作尽可能快速的运行,这样的做法更合理。我的资源需求符合这样的模式,但这是特意安排的,主要是因为index.translog.durability
被配置为'async',而indices.store.throttle.type
被配置为'none',此外我更倾向于数量更少但规模更大的批请求(Bulk request)以及更大量的事件。如果使用默认值,使用更小的事件、更多的请求以及更大的并发性,CPU可能会因为太多线程而严重超载,并且需要使用截然不同的伸缩模式。
容量规划工作的目标以及随之而来的集群规格可以帮助我们理解ElasticSearch的通用结构以及它与资源的关系和对资源的争夺。从我的观点看来,至少需要建立每个Shard的性能基线,并构建足够大规模的集群以提供所需数量的“伸缩单位”,并为其他操作提供足够的预留容量。虽然这只是一种想法但依然行之有效,不过你依然需要使用自己的数据进行测试。
可惜这些只是最简单的部分,如果只涉及ElasticSearch的一个功能(例如索引),这样做可以获得不错的效果。但如果需要包含其他用户和集群活动,例如查询、Shard再平衡,以及更长的GC暂停,容量问题将变得更为复杂。上文提到的“其他操作的预留容量”才是最困难的部分,而这个问题最有效的解决方法是对输入进行控制,下文将重点介绍这部分内容。
ElasticSearch稳定性的关键在于对输入的控制。虽然在下文的度量一节我会提到为了应对已知的输入量,例如索引进行的容量规划,但除了索引还有必要妥善管理所有操作的资源需求。否则规格的确定将没有任何意义,除非你才刚刚起步(并且完全不差钱)。
我觉得有3个主要类别的控制需要考虑:
下文将按顺序介绍。
对于索引:我们使用了自行开发的索引服务。我认为索引程序一定要能配置或自行调整速率限制(可以由索引程序自行处理或在上游使用调节服务),并要能对批处理索引的参数进行控制(例如批的大小、超时写入触发器等)。你可能已经听说过批(Bulk)索引的使用,这样做是没错的。另外我还想补充一个可能被很多人忽略的重点:集群中索引程序可以处理的未决(Outstanding)索引请求的总数。如果按照容量规划一节的建议进行配置,每个节点都会对活跃的和队列中的批线程有所限制,未决索引请求的总数不能超过这个限制。
如果要构建一个速度达到60K文档/秒的集群,即可将索引程序的速率限制为60K,如果总数据摄入量超过ES的容量规格,可将上游请求加入队列(当然还要喝杯咖啡然后着手伸缩集群)。索引操作的控制非常重要,但我发现很多人会忽略这个问题,只是一味地投入更多资源。
上一节有关性能的话题中提到了批线程的成本:虽然需要决定批请求的速率和规模,但我更愿意在需要从索引程序中获得的性能和ES数据节点的批线程利用率之间进行权衡。举例来说,如果一切组件都扩展到最高规格,在不影响整体性能的情况下能将索引程序的请求速率回缩到什么程度,这样就可以重新获得ES集群的部分CPU资源。
对于查询:基本是相同的想法。我们使用了自行开发的查询服务,可以像处理索引那样处理查询。索引和查询都是需要占用大量资源的工作负载,对吧!我们还为并发查询的查询队列设置了TTL。这里需要注意的是,在ElasticSearch改进查询运行控制能力之前,如果要提供给外部用户,单一查询实际所能造成的影响可以认为几乎是不可确定的。除非从语句的构成方面阻止这种做法,目前几乎无法阻止用户提交那种可能需要1100个核心满负荷运行40分钟,随后还需要进行大量垃圾回收的查询。但另一方面,查询速率和并发性至少是一种可行的控制点。
对于集群活动:我觉得这一点非常好理解。在将ElasticSearch应用于生产环境的早期阶段(大概是0.90版的时候),会由一些问题造成非常奇怪的服务中断,例如初始状态下主要Shard和可用数据节点之间的失衡。一个数据节点可能达到非常高的使用率,导致Shard针对比较老的索引进行再平衡,但我并未对这些问题采取措施。结合在一起后翻倍的索引负载(在设计上并不针对这种一个节点包含两个主副本的情况)以及Shard的迁移会导致GC彻底暂停并超出集群的超时值,使得节点从集群中退出。
类似的情况我还见过很多,大部分都可以用一行代码解决。挺好笑的对吧:
$ curl -s localhost:9200/_cat/shards | awk '{if($3~/p/) p[$(NF)]++}; {if($3~/r/) r[$(NF)]++} END {for(x in p) if(p[x] >1) print x,p[x]," primaries"} END {for(x in r) if(r[x] >1) print x,r[x]," replicas"}'
xxx-search05 3 primaries
xxx-search06 2 primaries
xxx-search07 6 primaries
xxx-search08 3 primaries
xxx-search05 3 replicas
xxx-search06 2 replicas
xxx-search07 3 replicas
xxx-search08 4 replicas
最实际的解决方法还是对这种行为本身进行控制。很多控制可通过修改设置(例如主要Shard的隔离权重,[但我发现这个功能已经被废弃了])的方式实现,其余部分可以通过我开发的一个名为Sleepwalk的设置调度器服务加以解决。这样我们就可以根据时间安排短暂应用某种集群设置。例如“早九点到晚九点之间允许进行0次再平衡,并对传输速率进行更大程度的限制,其他时间不进行任何限制”。
由于大部分工作负载存在波动的情况,白天高夜间低,很容易就可以建立相应的索引生命周期。我会在索引峰值时期限制常规的集群操作,使用其他工具对索引进行优化和复制(下文详述),随后放开了让集群执行需要的任何操作:
上图中我们允许以几乎不限制传输速率的方式进行5个并发的Shard再平衡。随后当“Sleepwalk运行结束”后,任务也已完成,索引速率重新恢复正常。
当然了,如果打算将集群活动作为一种会造成负载的“输入”进行控制,你会发现对一些东西的监控同样重要,例如Shard的移动或复苏,这一点与GC和存储利用率的做法没什么区别。这些东西一定要监控,追踪Shard的移动/状态能为你提供极大的帮助。
经过一些快速简单的调整,这种方式起到了很好的效果,并成为我们的第三个控制点。实施所有这些措施(并额外用大量时间对不同类型的实例、存储以及集群技术进行测试和调优,JVM实在是太蠢了)后,我们的系统在性能和稳定性方面有了巨大的飞跃。
你可能依然有些想法。“那么我是否该用固态硬盘?”、“是否需要给页面缓存提供128GB内存?”
我不习惯在不了解具体情况的时候针对性能问题给出笼统的建议。为什么?引用一个别人的说法吧:
说“机械硬盘太慢了”,就好像是在说“金属太重了,浮不起来”,结论是不应该用金属造船。
上下文情境和具体规格很重要。按照“搜索x必须在100ms内返回结果”或者“最大尺寸的片段写入操作花费的时间不能超过5秒”这样的方式思考一下需求吧。在开始设计系统时,需要考虑不同组件如何帮你满足这些需求。
当然对于固态硬盘这个例子,无论如何其延迟(当然还有顺序读写IO)都会比机械硬盘出色很多。但这真的重要吗?用我的工作负载为例,完全由索引驱动,数据的保留意味着一切。200TB的ElasticSearch数据恰巧是花大钱或者花小钱的分界线。我们的文档相对来说比较大,每个约2KB+,索引(lz4解压缩/压缩周期,或ES 2.x中的DEFLATE)时会让内核满载。如果用标准的服务器运行索引并让每个内核都满负荷运行,这一过程中磁盘大部分时候都是闲置的:满负荷执行大量顺序写入,闲置,满负荷,闲置… 如果使用固态硬盘作为存储,也许会将写入时间从5秒缩短至2秒,但我真的在意这种改进吗?更何况数据保留期限是我接下来要考虑的问题,就算让存储成本提升数倍,但从可预料的用户体验标准来看,完全无法获得任何实际收益。
你可能还会问到查询。我会考虑一下对延迟容忍度的最高值和平均值(SLO到底是什么呢?)。以我的ES集群作为后端的应用所查询的问题与你使用基于Hadoop的应用查询的问题差不多,因此5秒的查询响应速度已经相当“快”了(一个集群提供超过70GB/s的顺序读取吞吐率总量,这已经很酷很实用啦)。大部分非常长的查询响应通常都涉及复杂的正则表达式,这种查询更依赖CPU而非存储。尽管如此,我也试过最大的i2.2-8xlarge实例。基本上表现不过如此,绝对不值得我们使用,存储和CPU的性能完全失衡了。在使用EBS优化渠道和GP2存储的情况下,索引性能甚至不如c3实例,这主要是文档大小和所进行的lz4操作的数量导致的。但i2也许是最适合你的实例。
但也不要误会我有关硬件的说法。个人而言,我家的所有设备都在使用固态硬盘,闪存实在是太赞了。但实际上,通常来说别人付你钱是为了让你给他们设计系统,而不是让你自己觉得赞。我的建议是只需要考虑系统本身的设计即可,不需要为一些泛化的概念花冤枉钱。
最终归纳来说,您可能需要更多CPU资源,大部分人可能最开始就猜错了。AWS的c4和d2实例是最实用的。如果数据量不大,预算充足或者对查询延迟很敏感,那么就用能买得起的最快速的存储吧。
通用的集群设计中还有一个因素需要考虑,必须让数据节点的规格能够满足跨集群传输对最短时间的要求。所有负责构建系统的人都需要记住一个数字:理想情况下,每1Gb网络吞吐率所能实现的数据移动速度理论最大值是439GB/小时。如果你觉得在1Gb带宽的情况下给每个节点存储20TB数据是一种很好的做法,因为能够满足对索引/查询的带宽需求,还能节约机柜空间,那这就是你自找的。如果某个节点故障需要从中撤出整个数据集,你觉得花费数天时间才能让集群恢复,这种状况会有什么后果?
在我们设计的更大规模的集群中,还需要把存储密度和存储总量与集群聚合存储吞吐率,以及对等网络带宽等特性放在一起进行权衡并作为一个考虑因素。我会考虑类似“如果我必须重新复制这20个索引,而每个索引都存储了300TB数据,我能接受在多久时间里完成?”这样的问题。
以及针对性能的进一步思考
很多情况下可靠性与容量规划非常类似,并且它们之间绝对是有关系的。我将谈到可靠性的两个子类:稳定性和耐久性。
对于稳定性这个词,其实上文已经介绍过了(合适的规模以及对输入进行控制)。我觉得除此之外能对稳定性产生最大影响的因素应该是查询的复杂度和数据量。在我看来查询工作负载可以分为两类:
数量多但规模小是指那些“每秒钟处理xxxxx千个查询”的应用,但查询的复杂度以及时间范围(或者说数据集的范围)相对来说较小。数量小但规模大是指我所要处理的:“咱们用这250亿个文档做一次7层的正则表达式Dip吧”这种情况。针对非常大的数据集执行非常复杂的查询所产生的最大影响在于会耗尽CPU资源,并且随后会产生大量GC操作造成持续的压力。如果无法从语句结构上避免此类问题(且假设真有必要执行如此“狂野”的查询),此时最佳的做法可能是使用上文提到的查询调节方法,并严格限制并发的活跃查询。
如果你真的想要伸缩至更大量的复杂查询或实现非常高速率的小规模查询,通常可以实施多种类型的阶梯式集群节点体系结构。举例来说,如果索引量相对较低,但查询量相对较大,可以构建索引节点池(通过标签定义),为查询节点定义一个单独的池,并准备好所需数量的副本。这样就可以将副本节点视作独立于查询的一批容量(核心,堆(Heap)内存)。需要更多查询容量?将查询池从240核心/960GB的堆扩展至360核心1.44TB的堆,并继续准备相应数量的副本吧。
对于耐久性,ElasticSearch所提供的最主要的控制功能是Shard复制。只要无需考虑银河系会遭受类似拜占庭帝国那样的毁灭,副本策略远不如性能和总容量之类的因素重要。考虑到默认配置,我猜大部分人会使用每个主节点1个副本的默认设置对索引进行拆分。这样做对成本造成的最大影响是存储用量翻倍。此外有个不太容易发现的问题:相比写入非复制的索引,保存到复制的索引位置所能实现性能(明显)差很多。在进行度量之前还有一个不太容易发现的问题:使用复制后的性能一致性很不规则,至少在我看来是这样的。索引延迟和资源用量的波动范围远远超过写入非复制索引时的情况。这个问题重要吗?也许重要,但也许无所谓吧。
但这也让我产生了一些想法。听起来挺疯狂,但你也应该问问自己:“我真的需要对活跃索引进行复制吗?”。如果不是非常必要,不进行复制的效果其实更好。这样集群重启动的速度真的非常快(为了改善恢复机制,这个特性在新版ElasticSearch中的效果大幅下降)并且每个节点都能提供非常高的索引性能。非复制索引实现更可预测的一致性能,这也意味着我可以在更接近预留容量/闲置容量的水平下运行数据节点,而无须担心用量突然激增并抢占其他工作负载的资源。听起来有点危险是吧?当然危险了。其实可以轮换活跃索引并对刚刚停止的索引进行复制,借此建立完善的重放机制(Replay mechanism)和检查点。如果有节点下线,只需要挪用其他索引并接手后续的写入操作,为受影响的索引分配失去的Shard并重放数据。这样即可在后台自行恢复原先的索引。
类似的,你还应该问问自己:“数据变老后是否会失去价值?”。如果会,就无需在索引的整个生命周期内进行复制。集群外存储(例如S3或你自己数据中心内的大容量“温”存储)每GB成本可能更低。如果索引需要保存90天,但95%的查询针对命中的都是最近7天的数据,可以考虑将快照发送至集群之外,并停止对老旧索引的复制。如果能够配合SLO使用,将能大幅降低大集群的痕迹。
最后,这些话题大部分都与Shard的数量有关。但是需要提醒大家注意,Shard不是免费的(哪怕其中并未保存数据),而每个Shard都会占用集群的部分空间。其中保存的元数据主要是集群中每个节点的心跳信号,另外在一些Shard或节点数非常大的集群中我还遇到过有关可靠性的奇怪问题。在考虑任何问题时,请尽量将所用Shard数量降至最低。
以及集群的其他实时突变
大家都喜欢对分布式数据库进行升级。我觉得我会首先问自己团队一个(挺奇怪)的问题:“轮流重启动需要多长时间?我们的集群需要好几天!”。天呐,这个过程慢到离谱我也是醉了。
选项1很明显。如果完全不关心SLA,你可以安排一个停机窗口,执行全面的停止>升级>启动,这个过程大概需要3分钟(这个过程我说得很简单,但实际上还需要进行一些准备工作以及其他运维任务)。
选项2,如果活在现实世界里并以零停机时间为目标,我其实并不喜欢这种轮流升级/重启动的方法。需要执行大量数据移动、协调等工作并要付出大量时间。选项2需要:
1.将新节点接入集群(可将再平衡设置为0,或将新节点从分配中排除在外,这样再平衡过程才不会影响后续操作)。
2.选择一个分配给新节点的新索引,开始将新的传入数据写入这个索引。
3.随后设置排除老索引,并对再平衡进行调节(可使用cluster.routing.allocation.node_concurrent_recoveries
和indices.recovery.max_bytes_per_sec
设置)。
4.源节点的数据排空后将其移除。
新节点可能使用了全新配置的新版本ElasticSearch(但最好不要跨越大版本这样做;)),甚至可能是恢复操作的一部分(我们并没有试着修复节点;在第一次遇到这个故障后我们只是给集群新增了一个节点,并在故障节点上设置了排除)。
顺利完成这一切后的结果实在是不可思议。新的节点逐渐开始根据node_concurrent_recoveries
和recovery.max_bytes_per_sec
的设置运转(或使用Sleepwalk调度),等弃用的节点清空并删除就行了。
ElasticSearch的使用诀窍有很多,但我必须得说这也许是最强大同时也最简单的一个。就我个人来看,这也是不停机多次移动数PB规模ES的最佳方式。
希望本文能在系统体系和各种喧嚣的意见之间提供一种有趣的平衡。借此也可以管窥我所从事的一个炫酷的项目,我觉得我应该尽量从较高层次来介绍,原因有两个。
首先,出于一些原因我很少喜欢那种需要通过撰写“深度文”来探讨的ElasticSearch话题,所以通常我根本不会考虑写这些内容。其次,过去两年来我从事过一些非常大型的项目,这些项目有很多人的参与。我觉得需要通过一篇酷酷的故事向那些参与者表达感谢和称赞:
Chris作为项目经理截至目前也在积极参与我们的每次讨论,甚至经常在凌晨1点还在修复集群遇到的各种问题。负责项目推进的Nathan组建了一个只包含两人的团队(没错,他们也很重要),基本上是在通过一个附带项目(Side-project)管理着PB规模的ElasticSearch项目团队。负责运维/财务事项的Brad对项目组内部的沟通和实现起到了巨大的促进作用。Shaun帮助开发了很多本文没有涉及的自动化工具。Justin、Eric,以及Bill开发了我们的ElasticSearch索引/搜索客户端,经常面对各种艰巨的难题加班到深夜。
当然还要感谢Antoine Girbal、Pius Fung以及ElasticSearch团队其他对ElasticSearch的开发做出贡献的成员。最棘手的ElasticSearch技术问题他们也能非常快速地做出响应,这一点真是举世无双。另外这几位的办公室也非常有趣 ;)。
作者:Jamie Alquiza,阅读英文原文:Field notes - ElasticSearch at petabyte scale on AWS