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移动互联网,负责搜素引擎相关产品和系统的架构设计与开发实施,在厂商移动互联网领域有4年多的经验。热爱开源,乐于分享,目前专注于互联网系统架构特别是实时分布式系统的设计与工程实现,关注大数据的存储、检索及可视化。之前曾参与创业,先后负责开发母婴B2C、移动IM、智能手表等产品;在此之前就职于华大基因,从事基因组学领域的科研工作,专注于基因组数据的存储、检索和可视化。审阅了《Circos Data Visualization How-to》一书,参与社区协作翻译了《ElasticSearch权威指南》一书,待出版。
原文地址:http://www.infoq.com/cn/news/2017/01/ElasticSearch-9
.NET社区新闻,深度好文,微信中搜索dotNET跨平台或扫描二维码关注