fangpsh's blog

ElasticSearch 用于日志记录

EdgeofSanity

  • 原文地址
    • http://edgeofsanity.net/article/2012/12/26/elasticsearch-for-logging.html
  • 已获得原文作者翻译许可,本文遵循原文许可协议。
  • 原文发表时间:2012-12-26

在我工作中,我们在Web 前端搜索上使用ElasticSearch。性能至关重要,对我们而言,大多数的数据是静态的。
我们每天更新搜索索引,不过用旧的索引跑几个星期也没问题。这个集群大部分的流量是搜索;这是一个“重读”的集群。一开始的时候我们有一些性能上的小问题,
不过我们通过和ElasticSearch 的Shay Bannon 密切合作解决了这些问题。现在我们的前端集群非常可靠,灵活,和快速。

我目前的工作是部署一个符合规范,同时也非常有用的中心化的日志基础服务。这个日志基础服务的目标是尽可能的实现Splunk 的功能。我之前写的一篇关于日志记录的文章解释了我们为什么决定不用Splunk

评估了一些选择之后,我决定使用ElasticSearch 作为这个系统的后端存储。这个集群的类型和我们之前部署的用于大量搜索的集群非常不同

译文
EveryCloud 贡献的一篇俄语译文

索引设计

两个流行的开源日志分发系统是Graylog2LogStash。在写这篇文章的时候,稳定版本的Graylog2 只支持从单一的索引读/写。正如送之前文章中指出的,Graylog2 存在很多严重的问题。0.10.0 版的Graylog2 将包含联合多个索引的能力。然而,我已经在LogStash 的索引上有了一些经验,因为之前它是唯一的选择。

为了尽可能得充分利用ElasticSearch 用来做日志记录,你需要使用多个索引。当你滚动更新索引的时候有非常多种方式,不过LogStash 默认的每天滚动更新的方式是最合理的。这样,你会看到下列内容:

  • logstash-2012.12.19
  • logstash-2012.12.20
  • logstash-2012.12.21
  • logstash-THE_WORLD_HAS_ENDED

你可以跟踪每个索引中有多少个文本。在百万或者千万个文本之后滚动更新,或者其他你随意设定的数值之后,但是这样你会给你自己以后带来更多的工作内容。在某些少见的情况下其他的索引方式可能会更高效,但是对于大多数用户来说,一天一个索引是最简单的,也能最有效的利用你的资源。

认真点,不然就回家待着

LogStash 和Graylog2 都附带内置的ElasticSearch。这对示范演示和开发来说非常方便。用于真正用途的时候不要使用内置的服务器!我非常惊奇有大量的LogStash 和Graylog2 用户使用了内置的ElasticSearch 存储引擎,最后在irc.freenode.org 的 #elasticsearch 频道里对服务崩溃感到奇怪。

请运行一个独立的ElasticSearch 集群!

你将需要为集群划划分硬件。像LogStash和ElasticSearch 这样的Java 应用是内存和磁盘缓存密集型的。保证一批硬件是用来作为日志处理的箱子,然后划分这些箱子给ElasticSearch 集群。Java 有一些奇怪的内存问题。我们发现你不会想把超过32GB内存给ElasticSearch,预留至少8GB 给操作系统的文件系统缓存。

在我的开发环境中,我的集群每天处理大约60GB 的日志资料,集群一共有三个搜索节点,每个节点24GB 内存,毫无压力。这引发下一个问题,我的集群需要多少台服务器?开始先在你的ElasticSearch 集群中部署3台服务器。这样你可以灵活的关闭一台服务器,维持你的集群能充分得被使用。你可以随时添加更多的硬件!

安装ElasticSearch

我不打算覆盖安装ElasticSearch 的所有内容,你可以在文档站点上读到更多关于这方面的内容。你可能决定通过PuppetChef 利用 .deb 包或rpm 包的方式创建一份ElasticSearch 的部署脚本。关于安装我会说的唯一一件事是,无论有多不爽,最好还是用Sun JVM 运行ElasticSearch。这是ElasticSearch 的开发人员运行ElasticSearch 的方式,所以你也应该这样。

ElasticSearch 配置:OS 和 Java

主机系统上有一些地方你真的需要配置一下。我假定你的主机系统上运行的是Linux 系统。你应该使用非特权用户来运行ElasticSearch。我们的集群是用'elasticsearch' 用户来运行的,另外我们调整了/etc/security/limits.conf中内核对进程和内存上的限制:

# Ensure ElasticSearch can open files and lock memory!
elasticsearch   soft    nofile          65536
elasticsearch   hard    nofile          65536
elasticsearch   -       memlock         unlimited

你也应该把ElasticSearch 最小和最大的内存池设置为一样的值。这样使得启动时就能分配到总内存,这样线程需要更多的内存的时候不需要等待内核分配。我在一个RedHat 系统上部署了ElasticSearch,/etc/sysconfig/elasticsearch中的配置会在daemon 进程启动时设置好环境变量:

# Allocate 14 Gigs of RAM
ES_MIN_MEM=14g
ES_MAX_MEM="$ES_MIN_MEM"

这份文件由Puppet 管理,并设置内存大小等于50% 的RAM 大小,写2遍。这没什么难的,在每份ElasticSearch 优化指南中都会提到。

ElasticSearch 配置:elasticsearch.yml

elasticsearch.yml 文件中有一些调优的事情我们可以做,可以极大的改进重写的节点的性能。首先是设置bootstrap.mlockall 为 true。这会强制JVM 立即分配所有的ES_MIN_MEM。这意味着Java 在启动时就有了所有所需的内存!重写的集群的另外一个让人担心的点是indexing/bulk 引擎分配内存时不均衡的问题。

ElasticSearch 是假定你将大量使用搜索,所以分配了大部分的内存用于保障搜索。这不是我的集群的使用情况,所以调整indices.memory.index_buffer_size到50%,从而恢复在我们使用方式下内存分配的平衡。在我的设置里,我还提高了刷新间隔和日志刷新的传输总数。否则,ElasticSearch 将需要接近每分钟都刷新传输日志。

另外一个需要我们调整以避免灾难性的失败的地方是线程池的设置。ElasticSearch 会做它认为能获得最好性能的事情。我们发现,在生产环境中,这意味着生成成千上万的线程来处理传入的请求。在高负载下,你的集群很快会被压垮。为了避免这种情况,我们分别设置了search,index 和bulk 线程池的最大值。我们主要的操作是bulk,所以给bulk 设置了60个线程,其他的操作设置了20个线程。我们也设置了bulk 队列能处理的最大请求数为200,其他的各为100。这样一来,如果集群过载,它会拒绝新的请求,不过它会留给你足够的文件描述数和PID 来ssh 到主机中找出问题所在。

总结以上内容,这是我的配置文件

##################################################################
# /etc/elasticsearch/elasticsearch.yml
#
# Base configuration for a write heavy cluster
#

# Cluster / Node Basics
cluster.name: logng

# Node can have abritrary attributes we can use for routing
node.name: logsearch-01
node.datacenter: amsterdam

# Force all memory to be locked, forcing the JVM to never swap
bootstrap.mlockall: true

## Threadpool Settings ##

# Search pool
threadpool.search.type: fixed
threadpool.search.size: 20
threadpool.search.queue_size: 100

# Bulk pool
threadpool.bulk.type: fixed
threadpool.bulk.size: 60
threadpool.bulk.queue_size: 300

# Index pool
threadpool.index.type: fixed
threadpool.index.size: 20
threadpool.index.queue_size: 100

# Indices settings
indices.memory.index_buffer_size: 30%
indices.memory.min_shard_index_buffer_size: 12mb
indices.memory.min_index_buffer_size: 96mb

# Cache Sizes
indices.fielddata.cache.size: 15%
indices.fielddata.cache.expire: 6h
indices.cache.filter.size: 15%
indices.cache.filter.expire: 6h

# Indexing Settings for Writes
index.refresh_interval: 30s
index.translog.flush_threshold_ops: 50000

# Minimum nodes alive to constitute an operational cluster
discovery.zen.minimum_master_nodes: 2

# Unicast Discovery (disable multicast)
discovery.zen.ping.multicast.enabled: false
discovery.zen.ping.unicast.hosts: [ "logsearch-01", "logsearch-02", "logsearch-03" ]

ElasticSearch 配置: 索引模板

一开始的时候,我是基于LogStash 来部署集群,由于那时Graylog2 部署上的缺点。这一节内容主要关于“logstash”,但同样也适用于Graylog2 或其他自定义的索引映射(译注:Mapping)。

因为我们决定一天创建一个索引,这就有两种方式配置每个索引的映射和特性。我们可以按照我们的需求配置明确得生成索引,也可以使用一个包含我们需要的配置和特性的模板来生成每个索引!模板在这种情况下最方便,我们可以在运行的集群上创建它们!

我的模板配置如下:

{
    "template": "logstash-*",
    "settings" : {
        "index.number_of_shards" : 3,
        "index.number_of_replicas" : 1,
        "index.query.default_field" : "@message",
        "index.routing.allocation.total_shards_per_node" : 2,
        "index.auto_expand_replicas": false
    },
    "mappings": {
        "_default_": {
        "_all": { "enabled": false },
        "_source": { "compress": false },
        "dynamic_templates": [
            {
                "fields_template" : {
                    "mapping": { "type": "string", "index": "not_analyzed" },
                    "path_match": "@fields.*"
                }
            },
            {
                "tags_template" : {
                    "mapping": { "type": "string", "index": "not_analyzed" },
                    "path_match": "@tags.*"
                }
            }
        ],
        "properties" : {
            "@fields": { "type": "object", "dynamic": true, "path": "full" },
            "@source" : { "type" : "string", "index" : "not_analyzed" },
            "@source_host" : { "type" : "string", "index" : "not_analyzed" },
            "@source_path" : { "type" : "string", "index" : "not_analyzed" },
            "@timestamp" : { "type" : "date", "index" : "not_analyzed" },
            "@type" : { "type" : "string", "index" : "not_analyzed" },
            "@message" : { "type" : "string", "analyzer" : "whitespace" }
        }
        }
    }
}

要在集群上应用配置,我们可以通过一个PUT 来创建和更新模板:

curl -XPUT 'http://localhost:9200/_template/template_logstash/' -d @logstash-template.json

将模板设置为logstash-* ,意味着配置将应用到所以'logstash-' 开头新建的索引。我通过禁用对_all 字段进行搜索并将默认的属性修改为@message,覆盖了原来的默认搜索行为(译注:默认会对全部字段进行搜索,作者禁用了这种行为,修改为只对message 字段进行搜索)。这个字段(译注:指message 字段)将包含原生的syslog 消息。这也是唯一一个禁止分析的字段。不要不安。这能节省空间和建立索引的时间。这意味着搜索文档中其他字段时只能使用精确搜索,而不能使用模糊搜索,不过这是OK 的。我们依然可以对@message 字段进行模糊搜索!这将极大的降低存储空间。

在之前的文章中,ElasticSearch 0.19 之前,你可能见过设置了"_source": { "compress": true } 。这对日志类的数据是不推荐的。这个属性决定了每份文档(日志消息)是否进行压缩存储。由于这些文件往往非常小,压缩并不能真正的节省很多的空间。在进行索引和检索的时,这会带来额外的时间成本。对记录日志的集群来说最好禁用压缩。我们elasticsearch.yml中启用的压缩是使用块级别的压缩方式,更加高效。

索引配置

这份索引的配置是为一个3个节点的集群调优的。我们可以修改配置中的任何项,但是如果我们需要扩容或者缩容时,index.number_of_shards需要立即修改。这份配置不是绝对完美的,因为我们有时最后会有一些孤儿(未分配)的分片。通过ElasticSearch API 来移动分片可以很方便的纠正这些错误。

我们随着添加节点而提升存储能力,而不是复制整个索引到整个集群。这样我们有一个类似“RAID” 的分配分片的设置。我有一个三个节点的集群,每个索引我创建3个分片。这意味着主分片或者“写”分片可以分摊到每一个节点。为了冗余,我设置副本数为一。这意味着每个索引有6个分片。每个节点只需要有每个索引的2个分片。

你需要进行实验来找到你需要的配置。在失去功能前,考虑一下损失多少个节点是你能够承受的。你需要基于这个调整副本数量。我采用了一个简单的做法,即用1个分片备份。这意味着我一次只能从集群中下掉一个节点。到目前为止,我发现number_of_replicas 等于 (2/3 * 节点数) -1 是一个不错的数字,每个人的情况可能不同。

自动扩充副本

最好也禁用ElasticSearch 默认会基于集群中有多少节点就扩充多少个副本的行为。我们假定手工管理这个能提高性能,特别是当我们需要停止或重启一个集群中的节点时。自动扩充是一个很不错的特性,对于重搜索的、有着小到中量的数据的索引来说。无需配置,添加一个几点就能提高性能。然而,如果你的索引有着大量的数据,并启用了这个特性,当一个节点重启时,将会发生下列事情:

  • 一切正常。副本(replica)数 = 1
  • 节点A 关闭
  • 集群注意到节点下线,状态变成黄色(译注:elasticsearch 集群健康状态用三种颜色表示,红、黄、绿,具体可参考Elasticsearch "Yellow" cluster status explained
    • 副本数 = 0, 期望值为1
    • 目前节点数为1
    • 目前副本数期望 = 0
  • 集群健康状态上升为绿色,一切完美
  • 节点A 重新上线
  • 集群发送期望的副本数和实际的索引个数
  • 节点A 它的副本是不必要的,于是删除了数据
  • 集群增加了节点数,期望的副本数 = 1,实际 = 0
  • 节点A 被通知它的副本数不满足要求
  • 节点A 通过网络复制所有副本加到它的索引中

正如你看到的,这不太理想,特别是在一个非常繁忙的集群里。请小心你的生产环境中的操作,当你从集群中添加/删除节点时请注意你的网络设备监控图。如果你看到尖峰,你可能需要手动进行处理。你失去了一些魔法,不过你可能发现这些是黑魔法。禁用副本的自动扩充,会出现下列情况:

  • 一切正常
  • 节点A 关闭
  • 集群注意到节点下线,状态变成黄色
  • 集群健康状态未恢复,期望的副本数 != 实际副本数
  • 节点A 重新上线
  • 集群发送期望的副本数和实际上所有的索引个数
  • 节点A 通知集群它有分片的拷贝
  • 集群期望和实际上的副本数相等,健康状态变绿
  • 集群校验分片,和复制任何过期的分片给节点A

这才是大多数人民期望集群默认该做的,但是判定集群状态之间的逻辑关系让这很难实现。再说一次,auto_expand_replicas的魔法动作在大多数情况下是有用的,不过不适用与我们的情况。

维护和监控

我为工作在生产环境中的ElasticSearch写了一些脚本 。它包含传送标准值给Graphite 和Cacti。这儿也有一份Nagios 的监控检测脚本,非常容易配置。我们使用这些工具来跟踪我们多个集群的性能和健康状态,包括日志记录集群。过几天,我会更新一下,将包含我们logstash 索引维护的脚本。

当你将数据写入日志集群时,ElasticSearch 同时在后台创建Lucence 索引。收到的文档有一个缓冲区,基于你的设置,数据会被刷新到一个Lucene 索引。Lucene 索引新建/更新 的成本非常高,但是搜索非常。这意味这单个分片可能包含上百个Lucence 索引,常常被称为段。这些段可以被很快速的搜索,但是每个段只能被一个线程处理。这可能对性能有负面影响。我们已经观察到20多个段的索引,有10%的性能下降。

幸运的是,ElasticSearch 提供了一个API 优化Lucene段。你不应该优化一个正在索引数据的索引。在这些分片上,新数据将会生成更多的段。那么我们如何知道已经写完一个索引了呢?如果你还记得,我建议按天建立索引。这意味着,你可以运行一个每天的cron 任务(或每小时)来检查昨天或者更久之前的索引,确保他们已经被优化(或max_num_segments = 1)。如果你已经为创建索引的名字选择了一些其他模式,那么你刚刚给自己增加加了很多工作。

未来的探索

这篇文章比我想象的长很多。我只是对设计和实践用于日志记录的ElasticSearch 集群做了表面的研究。我的集群很快会从开发环境迁移到生产环境(尽管它正在提供生产环境的功能)。当我这样做的时候,我将面对更多的挑战,我有一个笔记本写满了关于如何构建索引的方法,关于集群如何处理负载,和其他当你突然提供简单、快速的访问大量的数据时候出现的一些隐私相关的问题。