Loggly 服务底层的很多核心功能都使用了 Elasticsearch 作为搜索引擎。就像 Jon Gifford(译者注:Loggly 博客作者之一)在他近期关于“Elasticsearch vs Solr”的文章中所述,日志管理在搜索技术方面产生了一些苛刻的需求,为满足这些需求,必须能够:
在超大规模数据集上可靠地进行准实时索引 - 在我们的案例中,每秒有超过 100,000 个日志事件
与此同时,在该索引上可靠高效地处理超大量的搜索请求
当时我们正在构建 Gen2 日志管理服务,想保证使用的所有 Elasticsearch 配置信息,可以获得最优的索引和搜索性能。悲剧的是,我们发现想在 Elasticsearch 文档里找到这样的信息非常困难,因为它们不只在一个地方。本文总结了我们的学习经验,可作为一个配置属性的参考检查单(checklist) 用于优化你自己应用中的ES。
小贴士1:规划索引、分片 以及集群增长情况
ES 使得创建大量索引和超大量分片非常地容易,但更重要的是理解每个索引和分片都是一笔开销。如果拥有太多的索引或分片,单单是管理负荷就会影响到 ES 集群的性能,潜在地也会影响到可用性方面。这里我们专注于管理负荷,但运行大量的索引 / 分片依然会非常显著地影响到索引和检索性能。
我们发现影响管理负荷的最大因素是集群状态数据的大小,因为它包含了集群中每个索引的所有 mapping 数据。我们曾经一度有单个集群拥有超过 900MB 的集群状态数据。该集群虽然在运行但并不可用。
让我们通过一些数据来了解到底发生了什么 。。。。。。
假如有一个索引包含 50k 的 mapping 数据(我们当时是有 700 个字段)。如果每小时生成一个索引,那么每天将增加 24 x 50k 的集群状态数据,或者 1.2MB。如果需要在系统中保留一年的数据,那么集群状态数据将高达 438MB(以及 8670 个索引,43800 个分片)。如果与每天一个索引(18.25MB,365 个索引,1825 个分片)作比较,会看到每小时的索引策略将会是一个完全不同的境况。
幸运的是,一旦系统中有一些真实数据的话,实际上非常容易做这些预测。我们应当能够看到集群必须处理多少状态数据和多少索引 / 分片。在上到生产环境之前真的应该演练一下,以便防止凌晨 3:00 收到集群挂掉的电话告警。
在配置方面,我们完全可以控制系统中有多少索引 (以及有多少分片),这将让我们远离危险地带。
小贴士 2:在配置前了解集群的拓扑结构
Loggly 通过独立的 master 节点和 data 节点来运行 ES。这里不讨论太多的部署细节(请留意后续博文),但为了做出正确的配置选择,需要先确定部署的拓扑结构。
另外,我们为索引和搜索使用单独的 ES client 节点。这将减轻 data 节点的一些负载,更重要的是,这样我们的管道就可以和本地客户端通信,从而与集群的其他节点通信。
可通过设置以下两个属性的值为 true 或 false 来创建 ES 的 data 节点和 master 节点:
Master node: node.master:true node.data:false Data node: node.master:false node.data:true Client node: node.master:false node.data:false
以上是相对容易的部分,现在来看一些值得关注的 ES 高级属性。对大多数部署场景来说默认设置已经足够了,但如果你的 ES 使用情况和我们在 log 管理中遇到的一样难搞,你将会从下文的建议中受益良多。
小贴士 3: 内存设置
Linux 把它的物理 RAM 分成多个内存块,称之为分页。内存交换(swapping)是这样一个过程,它把内存分页复制到预先设定的叫做交换区的硬盘空间上,以此释放内存分页。物理内存和交换区加起来的大小就是虚拟内存的可用额度。
内存交换有个缺点,跟内存比起来硬盘非常慢。内存的读写速度以纳秒来计算,而硬盘是以毫秒来计算,所以访问硬盘比访问内存要慢几万倍。交换次数越多,进程就越慢,所以应该不惜一切代价避免内存交换的发生。
ES 的 mlockall 属性允许 ES 节点不交换内存。(注意只有 Linux/Unix 系统可设置。)这个属性可以在 yaml 文件中设置:
bootstrap.mlockall: true
在 5.x 版本中,已经改成了 bootstrap.memory_lock: true.
mlockall 默认设置成 false,即 ES 节点允许内存交换。一旦把这个值加到属性文件中,需要重启 ES 节点才可生效。可通过以下方式来确定该值是否设置正确:
curl http://localhost:9200/_nodes/process?pretty
如果你正在设置这个属性,请使用 -DXmx 选项或 ES_HEAP_SIZE 属性来确保 ES 节点分配了足够的内存。
小贴士 4:discovery.zen 属性控制 ElasticSearch 的发现协议
Elasticsearch 默认使用服务发现 (Zen discovery) 作为集群节点间发现和通信的机制。Azure、EC2 和 GCE 也有使用其他的发现机制。服务发现由 discovery.zen.* 开头的一系列属性控制。
在 0.x 和 1.x 版本中同时支持单播和多播,且默认是多播。所以要在这些版本的 ES 中使用单播,需要设置属性 discovery.zen.ping.multicast.enabled 为 false。
从 2.0 开始往后服务发现就仅支持单播了。
首先需要使用属性 discovery.zen.ping.unicast.hosts 指定一组通信主机。方便起见,在集群中的所有主机上为该属性设置相同的值,使用集群节点的名称来定义主机列表。
属性 discovery.zen.minimum_master_nodes 决定了有资格作为 master 的节点的最小数量,即一个应当“看见”集群范围内运作的节点。如果集群中有 2 个以上节点,建议设置该值为大于 1。一种计算方法是,假设集群中的节点数量为 N,那么该属性应该设置为 N/2+1。
Data 和 master 节点以两种不同方式互相探测:
通过 master 节点 ping 集群中的其他节点以验证他们处于运行状态通过集群中的其他节点 ping master 节点以验证他们处于运行状态或者是否需要初始化一个选举过程
节点探测过程通过 discover.zen.fd.ping_timeout 属性控制,默认值是 30s,决定了节点将会等待响应多久后超时。当运行一个较慢的或者拥堵的网络时,应该调整这个属性;如果在一个慢速网络中,将该属性调大;其值越大,探测失败的几率就越小。
Loggly 的 discovery.zen 相关属性配置如下:
discovery.zen.fd.ping_timeout: 30s discovery.zen.minimum_master_nodes: 2 discovery.zen.ping.unicast.hosts: [“esmaster01″,”esmaster02″,”esmaster03″]
以上属性配置表示节点探测将在 30 秒内发生,因为设置了 discovery.zen.fd.ping_timeout 属性。另外,其他节点应当探测到最少两个 master 节点(我们有 3 个 master)。我们的单播主机是 esmaster01、 esmaster02、esmaster03。
小贴士 5:当心 DELETE _all
必须要了解的一点是,ES 的 DELETE API 允许用户仅仅通过一个请求来删除索引,支持使用通配符,甚至可以使用 _all 作为索引名来代表所有索引。例如:
curl -XDELETE ‘http://localhost:9200/*/’
这个特性非常有用,但也非常危险,特别是在生产环境中。在我们的所有集群中,已通过设置 action.destructive_requires_name:true 来禁用了它。
这项配置在 1.0 版本中开始引用,并取代了 0.90 版本中使用的配置属性 disable_delete_all_indices。
小贴士 6:使用 Doc Values
2.0 及以上版本默认开启 Doc Values 特性,但在更早的 ES 版本中必须显式地设置。当进行大规模的排序和聚合操作时,Doc Values 相比普通属性有着明显的优势。本质上是将 ES 转换成一个列式存储,从而使 ES 的许多分析类特性在性能上远超预期。
为了一探究竟,我们可以在 ES 里比较一下 Doc Values 和普通属性。
当使用一个普通属性去排序或聚合时,该属性会被加载到属性数据缓存中。一个属性首次被缓存时,ES 必须分配足够大的堆空间,以便能保存每一个值,然后使用每个文档的值逐步填充。这个过程可能会耗费一些时间,因为可能需要从磁盘读取他们的值。一旦这个过程完成,这些数据的任何相关操作都将使用这份缓存数据,并且会很快。如果尝试填充太多的属性到缓存,一些属性将被回收,随后再次使用到这些属性时将会强制它们重新被加载到缓存,且同样有启动开销。为了更加高效,人们会想到最小化或淘汰,这意味着我们的属性数量将受限于此种方式下的缓存大小。
相比之下,Doc Values 属性使用基于硬盘的数据结构,且能被内存映射到进程空间,因此不影响堆使用,同时提供实质上与属性数据缓存一样的性能。当这些属性首次从硬盘读取数据时仍然会有较小的启动开销,但这会由操作系统缓存去处理,所以只有真正需要的数据会被实际读取。
Doc Values 因此最小化了堆的使用(因为垃圾收集),并发挥了操作系统文件缓存的优势,从而可进一步最小化磁盘读操作的压力。
小贴士 7:Elasticsearch 配额类属性设置指南
分片分配就是分配分片到节点的过程,可能会发生在初始化恢复、副本分配、或者集群再平衡的阶段,甚至发生在处理节点加入或退出的阶段。
属性 cluster.routing.allocation.cluster_concurrent_rebalance 决定了允许并发再平衡的分片数量。这个属性需要根据硬件使用情况去适当地配置,比如 CPU 个数、IO 负载等。如果该属性设置不当,将会影响 ES 的索引性能。
cluster.routing.allocation.cluster_concurrent_rebalance:2
默认值是 2,表示任意时刻只允许同时移动 2 个分片。最好将该属性设置得较小,以便压制分片再平衡,使其不影响索引。
另一个分片分配相关的属性是 cluster.routing.allocation.disk.threshold_enabled。如果该属性设备为 true(默认值),在分配分片到一个节点时将会把可用的磁盘空间算入配额内。关闭该属性会导致 ES 可能分配分片到一个磁盘可用空间不足的节点,从而影响分片的增长。
当打开时,分片分配会将两个阀值属性加入配额:低水位和高水位。
低水位定义 ES 将不再分配新分片到该节点的磁盘使用百分比。(默认是 85%)高水位定义分配将开始从该节点迁移走分片的磁盘使用百分比。(默认是 90%)
这两个属性都可以被定义为磁盘使用的百分比(比如“80%”表示 80% 的磁盘空间已使用,或者说还有 20% 未使用),或者最小可用空间大小(比如“20GB”表示该节点还有 20GB 的可用空间)。
如果有很多的小分片,那么默认值就非常保守了。举个例子,如果有一个 1TB 的硬盘,分片是典型的 10GB 大小,那么理论上可以在该节点上分配 100 个分片。在默认设置的情况下,只能分配 80 个分片到该节点上,之后 ES 就认为这个节点已经满了。
为得到适合的配置参数,应该看看分片到底在变多大之后会结束他们的生命周期,然后从这里反推,确认包括一个安全系数。在上面的例子中,只有 5 个分片写入,所以需要一直确保有 50GB 的可用空间。对于一个 1TB 的硬盘,这个情形会变成 95% 的低水位线,并且没有安全系数。额外的,比如一个 50% 的安全系数,意味着应该确保有 75GB 的可以空间,或者一个 92.5% 的低水位线。
小贴士 8:Recovery 属性允许快速重启
ES 有很多恢复相关的属性,可以提升集群恢复和重启的速度。最佳属性设置依赖于当前使用的硬件(硬盘和网络是最常见的瓶颈),我们能给出的最好建议是测试、测试、还是测试。
想控制多少个分片可以在单个节点上同时恢复,使用:
cluster.routing.allocation.node_concurrent_recoveries
恢复分片是一个 IO 非常密集的操作,所以应当谨慎调整该值。在 5.x 版本中,该属性分为了两个:
cluster.routing.allocation.node_concurrent_incoming_recoveries cluster.routing.allocation.node_concurrent_outgoing_recoveries
想控制单个节点上的并发初始化主分片数量,使用:
cluster.routing.allocation.node_initial_primaries_recoveries
想控制恢复一个分片时打开的并行流数量,使用:
indices.recovery.concurrent_streams
与流数量密切相关的,是用于恢复的总可用网络带宽:
indices.recovery.max_bytes_per_sec
除了所有这些属性,最佳配置将依赖于所使用的硬件。如果有 SSD 硬盘以及万兆光纤网络,那么最佳配置将完全不同于使用普通磁盘和千兆网卡。
以上所有属性都将在集群重启后生效。
小贴士 9:线程池属性防止数据丢失
Elasticsearch 节点有很多的线程池,用于提升一个节点中的线程管理效率。
在 Loggly,索引时使用了批量操作模式,并且我们发现通过 threadpool.bulk.queue_size 属性为批量操作的线程池设置正确的大小,对于防止因批量重试而可能引起的数据丢失是极其关键的。
threadpool.bulk.queue_size: 5000
这会告诉 ES, 当没有可用线程来执行一个批量请求时,可排队在该节点执行的分片请求的数量。该值应当根据批量请求的负载来设置。如果批量请求数量大于队列大小,就会得到一个下文展示的 RemoteTransportException 异常。
正如上文所述,一个分片包含一个批量操作队列,所以这个数字需要大于想发送的并发批量请求的数量与这些请求的分片数的乘积。例如,一个单一的批量请求可能包含 10 个分片的数据,所以即使只发送一个批量请求,队列大小也必须至少为 10。这个值设置太高,将会吃掉很多 JVM 堆空间(并且表明正在推送更多集群无法轻松索引的数据),但确实能转移一些排队情况到 ES,简化了客户端。
既要保持属性值高于可接受的负载,又要平滑地处理客户端代码的 RemoteTransportException 异常。如果不处理该异常,将会丢失数据。我们模拟使用一个大小为 10 的队列来发送大于 10 个的批处理请求,获得了以下所示异常。
RemoteTransportException[[<Bantam>][inet[/192.168.76.1:9300]][bulk/shard]]; nested: EsRejectedExecutionException[rejected execution (queue capacity 10) on org.elasticsearch.action.support.replication.TransportShardReplicationOperationAction$AsyncShardOperationAction$1@13fe9be];
为 2.0 版本以前的用户再赠送一个小贴士:最小化 Mapping 刷新时间
如果你仍在使用 2.0 版本以前的 ES,且经常会更新属性 mapping,那么可能会发现集群的任务等待队列有一个较大的 refresh_mappings 请求数。对它自身来说,这并不坏,但可能会有滚雪球效应严重影响集群性能。
如果确实遇到这种情况,ES 提供了一个可配置参数来帮助应对。可按下述方式使用该参数:
indices.cluster.send_refresh_mapping: false
那么,这是怎么个意思,为什么可以奏效?
当索引中出现一个新的属性时,添加该属性的数据节点会更新它自己的 mapping,然后把新的 mapping 发送给主节点。如果这个新的 mapping 还在主节点的等待任务队列中,同时主节点发布了自己的下一个集群状态,那么数据节点将接收到一个过时的旧版本 mapping。通常这会让它发送一个更新 mapping 的请求到主节点,因为直到跟该数据节点有关,主节点一直都拥有错误的 mapping 信息。这是一个糟糕的默认行为——该节点应该有所行动来保证主节点上拥有正确的 mapping 信息,而重发新的 mapping 信息是一个不错的选择。
但是,当有很多的 mapping 更新发生,并且主节点无法持续坚持时,会有一个乱序聚集 (stampeding horde) 效应,数据节点发给主节点的刷新消息就可能泛滥。
参数 indices.cluster.send_refresh_mapping 可以禁用掉默认行为,因此消除这些从数据节点发送到主节点的 refresh_mapping 请求,可以让主节点保持最新。即时没有刷新请求,主节点也最终会看到最初的 mapping 变更,并会发布一个包含该变更的集群状态更新。
总结:Elasticsearch 的可配置属性是其弹性的关键
对 Loggly 来讲 Elasticsearch 可深度配置的属性是一个巨大的优势,因为在我们的使用案例中已经最大限度发挥了 Elasticsearch 的参数威力 (有时更甚)。如果在你自己应用进化的当前阶段 ES 默认配置工作得足够好了,请放心,随着应用的发展你还会有很大的优化空间。
译者介绍:杨振涛(Gentle Yang),搜索引擎架构师。现就职于 vivo 移动互联网,负责搜素引擎相关产品和系统的架构设计与开发实施。目前专注于互联网系统架构特别是实时分布式系统的设计与工程实现,关注大数据的存储、检索及可视化。之前曾参与创业,先后负责开发母婴 B2C、移动 IM、智能手表等产品;在此之前就职于华大基因,从事基因组学领域的科研工作,专注于基因组数据的存储、检索和可视化。审阅了《Circos Data Visualization How-to》一书,参与社区协作翻译了《Elasticsearch 权威指南》一书,待出版。
阅读英文原文: 9 Tips on ElasticSearch Configuration for High Performance 。已获授权。
感谢杜小芳对本文的审校。
给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ , @丁晓昀),微信(微信号: InfoQChina )关注我们。
评论