本文分为如下几个部分:
背景:当日志服务需要承载2TB每日的量时 优化前的准备:你所要了解的ES分片机制以及存储机制 如何优化:ES的index配置以及ES本身的配置如何调优 优化的成果:检验我们做的工作是否真的能够降本提效 后续的思考:像资本家一样贪婪——通过架构升级榨干服务的资源
背景
当日志服务需要承载2TB每日的量时,我们需要怎么做?今年年初,我司开始实行降本,日志服务作为非核心服务,必须要压缩成本。目前,日志服务我们采用的方案是业界流行的ELK技术栈。整体的架构如下如所示

用过这套技术栈的同学应该明白,这一套下来其实非常烧机器的资源,我们需要单独部署logstash集群,es集群等,kafka集群。而且在我接手日志服务的时候,发现了一个问题便是很多时候日志写入延时,甚至出现丢日志的现象。而且目前日志保存时间太短,只有3天 这套架构整体没有问题,可以可靠应对很高的日志量的写入。略微从数据流向的角度思考一下,我得出了两个可能的原因
从filebeat到logstash多了一个投递kafka的过程,多了一跳网络消耗,从而增加了丢包和延时的风险 logstash集群数量的问题,可能是logstash集群数量太少,消费kafka消息不及时。采用了以下的两种方式来应对 filebeat直连logstash(没啥效果,后来切回去了) logstash拆分,将高配机器拆分成多个低配机器。在以上的尝试均不理想的情况下,我瞄准了ES,应该是ES写入的问题导致问题的出现。
优化前的准备
既然我们要优化ES,那么我们必须先做三件事:
确定日志服务提供的SLA,这个很重要,会直接关系到我们是否采用一些极端的优化手段 知己知彼百战不殆,你所要了解的ES读写机制。 确认ES的版本,不同版本的优化方式不同,目前使用的ES版本为5.4.3
所有优化工作我们都要确定服务优化后的目标与SLA。优化的目标很明确,就是解决日志写入延时和丢日志的现象。SLA的制定需要拉业务方一起来,日志服务的业务方是广大的开发同学。经过讨论之后,确定SLA如下:
延时不超过1分钟 日志存储时间为7天 可以接受日志服务短暂性的不可用 可以接受日志少部分缺失
OK,明白了日志服务的SLA之后,我们还需要了解一下ES的基础知识。
ES读写机制
ES作为一个分布式存储,采用的是分片方式组织数据(这种在分布式存储中很常见),这种机制可以理解为其实就是数据库的水平分库分表操作。如果把分片理解为分库分表,那么就很简单了,分库可以独立提供读写服务的。在创建ES的index的时候,我们会指定默认的分片数量,与此同时我们也就指定了路由的规则,ES根据hash的方式将同一hash值的id分发到同一个索引中,这也就是为什么index一旦创建就无法更改其分片的数量了。
ES为了避免数据的丢失,提供了副本的机制,一个分片可以拥有0到N-1个分片(N为ES数据节点的机器数量)。对doc的创建,索引,删除的请求都必须要在主分片上进行然后并发地写到副本中。当所有的副本都报告写成功之后,再返回写成功。这一点其实和数据库是很像的,数据库是所有的写操作全都在主库主表中进行,然后异步同步到其他分库分表中。写操作的可以用下图来表示:

其实我们发现这样的效率其实并没有很高,写入的延时就等于
latency = latency(Primary Write) + MAX(latency(Replica Write))
但是副本存在的意义也是很明显的,可以避免单点问题以及数据丢失的问题。在数据完整性与高性能的权衡下,一般来说这个天平会倒向数据的完整性,除非你面对的场景允许丢失部分数据。那我们写入的数据马上就可以被搜索到吗?答案是否定的。我们知道ES是基于Lucene实现的,内部通过Lucene完成索引的创建写入和搜索查询的。当添加一份doc的时候,Lucene进行分词处理,然后将文档索引写入内存中并将本次操作写入事物日志(transLog)中,transLog类似于数据库的binlog,用于数据恢复使用。这里肯定有小朋友会问:阿摸老师,为什么ES是先写内存再写日志啊。这里先不展开解释,后续我会专门写文章进行讨论。在默认的配置下,Lucene每隔refresh_interval(默认配置下为1s)秒的时间将内存中的数据refresh到文件系统的缓存中,这玩意称之为segment(段)。只有生成segment并且这个segment reopen之后,这个doc才可以被检索到。好了,回答完ES为啥不是实时搜索这个问题之后,肯定又有小朋友要问了:阿摸老师,那ES什么时候会把segment刷进磁盘呢?随着时间的推移,生成的segment越来越多,默认配置中,Lucenne每隔30分钟或者存在大于512m的segment持久化落盘,并且删掉对应的translog。整体流程如下:

介绍完了写,再简单介绍一下读。 前面介绍了,ES的写入是,是等待分片+所有副本写成功之后再返回的,换言之就是ES任何一个分片以及它的副本上的数据都是一致的,再换一句更加通俗的话来说就是,可以随便从那个地方读都是一样的。那我们就可以简单得出一个推论就是一个index的副本数目设的适当的多,那么这个index的读性能就会非常好。
如何优化
ES的index配置以及ES本身的配置如何调优
index配置优化
从上文的ES知识提示中,我们已经可以粗略的得出一些结论:性能优化主要是在优化索引的配置。试想一下,如果我们使用数据库进行数据的存储,当数据达到一定规模的时候我们会怎么做?按照目前流行的做法就是会进行分库分表,如果分库分表之后数据依然还是很大,那么是不是会在做冷热数据分离存储?ES的索引配置一样如此。当ES的数据量大到开始影响性能的时候,我们就必须考虑一件事:目前索引的配置是否是当前的最优解。 日志服务中的数据有一个很明显的特点就是他是一个时间序列的数据,那么我们自然而然会选择将索引分成按天设置,接下来我们就是看索引的分片配置是否合理。先看机器资源和当前的分片配置:
6台8核32G机器。集群情况为3master node + 6 data node(data node承担master的工作) 分片设置为 5分片 * (2副本 + 1主分片)
不难看出,目前的分片数量其实比较多,为5个主分片+10个副分片。日志服务是典型的写多读少的应用场景,所以我们应当适当减少分片的数量。结合上之前确定的SLA,可以忍受日志服务的短暂不可用与少量的缺失。**我们可以激进地将分片的副本数量全部设置为0,只保留主分片即可。**目前机器一共有6台,可以将分片数量设置成6个以提高读写的效率。虽然这样不一定符合ES权威指南给的建议,但是后面测试过来发现效果还是很不错的。生产环境调优切勿墨守陈规,还是需要灵活一些。
分片的调整只是一部分,后面还有相关的index的配置调优 直接放出调优前后的index-template配置 调优前的index-template配置:
{
"order": 5,
"template": "*-json-*",
"settings": {
"index": {
"codec": "best_compression",
"routing": {
"allocation": {
"require": {
"box_type": "hot"
}
}
},
"number_of_shards": "6",
"number_of_replicas": "0"
}
},
"mappings": {
"log": {
"properties": {
"level": {
"type": "text",
"fields": {
"keyword": {
"ignore_above": 256,
"type": "keyword"
}
}
},
"ip": {
"type": "text",
"fields": {
"keyword": {
"ignore_above": 256,
"type": "keyword"
}
}
},
"logger": {
"type": "text",
"fields": {
"keyword": {
"ignore_above": 256,
"type": "keyword"
}
}
},
"source": {
"type": "text",
"fields": {
"keyword": {
"ignore_above": 256,
"type": "keyword"
}
}
},
"thread": {
"type": "text",
"fields": {
"keyword": {
"ignore_above": 256,
"type": "keyword"
}
}
},
"message": {
"type": "text",
"fields": {
"keyword": {
"ignore_above": 256,
"type": "keyword"
}
}
},
"type": {
"type": "text",
"fields": {
"keyword": {
"ignore_above": 256,
"type": "keyword"
}
}
},
"content": {
"type": "text",
"fields": {
"keyword": {
"ignore_above": 256,
"type": "keyword"
}
}
},
"tags": {
"type": "text",
"fields": {
"keyword": {
"ignore_above": 256,
"type": "keyword"
}
}
},
"hostname": {
"type": "text",
"fields": {
"keyword": {
"ignore_above": 256,
"type": "keyword"
}
}
},
"@timestamp": {
"type": "date"
},
"appid": {
"type": "text",
"fields": {
"keyword": {
"ignore_above": 256,
"type": "keyword"
}
}
},
"time": {
"type": "text",
"fields": {
"keyword": {
"ignore_above": 256,
"type": "keyword"
}
}
}
}
}
},
"aliases": {}
}
调优前的index-template配置:
{
"order": 5,
"template": "*-json-*",
"settings": {
"index": {
"codec": "best_compression",
"routing": {
"allocation": {
"require": {
"box_type": "hot"
}
}
},
"refresh_interval": "60s",
"number_of_shards": "6",
"translog": {
"flush_threshold_size": "1024mb",
"sync_interval": "60s",
"durability": "async"
},
"number_of_replicas": "0"
}
},
"mappings": {
"log": {
"_all": {
"enabled": false
},
"properties": {
"level": {
"norms": false,
"type": "text",
"fields": {
"keyword": {
"ignore_above": 256,
"type": "keyword"
}
},
"index_options": "docs"
},
"ip": {
"norms": false,
"type": "text",
"fields": {
"keyword": {
"ignore_above": 256,
"type": "keyword"
}
},
"index_options": "docs"
},
"logger": {
"norms": false,
"type": "text",
"fields": {
"keyword": {
"ignore_above": 256,
"type": "keyword"
}
},
"index_options": "docs"
},
"source": {
"norms": false,
"type": "text",
"fields": {
"keyword": {
"ignore_above": 256,
"type": "keyword"
}
},
"index_options": "docs"
},
"thread": {
"norms": false,
"type": "text",
"fields": {
"keyword": {
"ignore_above": 256,
"type": "keyword"
}
},
"index_options": "docs"
},
"message": {
"norms": false,
"type": "text",
"fields": {
"keyword": {
"ignore_above": 256,
"type": "keyword"
}
},
"index_options": "docs"
},
"type": {
"norms": false,
"type": "text",
"fields": {
"keyword": {
"ignore_above": 256,
"type": "keyword"
}
},
"index_options": "docs"
},
"content": {
"norms": false,
"type": "text",
"fields": {
"keyword": {
"ignore_above": 256,
"type": "keyword"
}
},
"index_options": "docs"
},
"tags": {
"norms": false,
"type": "text",
"fields": {
"keyword": {
"ignore_above": 256,
"type": "keyword"
}
},
"index_options": "docs"
},
"hostname": {
"norms": false,
"type": "text",
"fields": {
"keyword": {
"ignore_above": 256,
"type": "keyword"
}
},
"index_options": "docs"
},
"@timestamp": {
"type": "date"
},
"appid": {
"norms": false,
"type": "text",
"fields": {
"keyword": {
"ignore_above": 256,
"type": "keyword"
}
},
"index_options": "docs"
},
"time": {
"norms": false,
"type": "text",
"fields": {
"keyword": {
"ignore_above": 256,
"type": "keyword"
}
},
"index_options": "docs"
}
}
}
},
"aliases": {}
}
对比不难发现做了如下的调整:
refresh_interval: 默认为1s,调整成60s。用延迟60s的代价换来segment的消耗降低 translog: 调整了flush的大小,默认为512m,而且将translog的写入方式变成异步写入,写入间隔为60s,进一步提升写请求性能 _all: 禁用这个字段,这个字段会被分析和索引,_all字段在查询时占用更多的CPU和占用更多的磁盘空间,如果确实不需要它可以完全的关闭它或者基于每字段定制。注意, ES 6.0+以上的版本不再支持_all字段,不需要设置 norms: 用于在搜索时计算该doc的_score(代表这条数据与搜索条件的相关度),如果不需要评分,可以将其关闭。这个字段会占用1byte的空间 index_options: 控制倒排索引中包括哪些信息(docs、freqs、positions、offsets)。对于不太注重score/highlighting的使用场景,可以设为 docs来降低内存/磁盘资源消耗
至此,索引相关的调优暂时结束
ES配置优化
当然,调优ES本身的配置也可以带来很大的性能提升。下面是我调整的ES配置的列表以及理由:
关闭交换分区,防止内存置换降低性能。将/etc/fstab 文件中包含swap的行注释掉
sed -i '/swap/s/^/#/' /etc/fstabswapoff -a
磁盘挂载选项 noatime:禁止记录访问时间戳,提高文件系统读写性能 data=writeback:不记录data journal,提高文件系统写入性能 barrier=0:barrier保证journal先于data刷到磁盘,上面关闭了journal,这里的barrier也就没必要开启了 nobh:关闭buffer_head,防止内核打断大块数据的IO操作
mount -o noatime,data=writeback,barrier=0,nobh /dev/sda /es_data
适当增大写入buffer和bulk队列长度,提高写入性能和稳定性
indices.memory.index_buffer_size: 15%thread_pool.bulk.queue_size: 1024 4. 计算disk使用量时,不考虑正在搬迁的shard
在规模比较大的集群中,可以防止新建shard时扫描所有shard的元数据,提升shard分配速度。
cluster.routing.allocation.disk.include_relocations: false
做完这些操作,接下来我们看看优化的成果吧
优化的成果
检验我们做的工作是否真的能够降本提效。如何衡量我们做的优化是否有价值
首先开头说的三个问题:
日志的延迟30分钟 日志丢失 日志保存时间为3天 这些问题已经全部解决了,目前的性能水平如下: 日志延迟1分钟,对于日志服务来说,这个时间可以接受,甚至绰绰有余 日志丢失,这个可能会出现,但是目前尚未出现。后续可以再优化做到保证数据可用 日志保存时间从原来的3天改为7天 kibana的搜索性能从原来的10s下降为2s,这个在使用体感上非常好
除此之外,可以看到下图机器水平由原来的全线飘红到蓝油油的一片,粗略估计还可以再支持增长30%吞吐量。完美的做到了降本提效。

后续的思考
优化到当前的状态是最好的吗?是不是还有空间可以继续优化?我们该如何做?
其实目前看来还有主要还是两个比较值得优化的地方的
日志规范化,目前来说日志服务的降本提效不能只在日志服务层面做工作,还需要推动业务方改造日志,现在看来乱打日志,乱用日志级别,日志过长,业务主键缺失等问题还是很明显的 架构升级,采用ELK这一套的日志解决方案其实并没有将机器水平压榨到很高。目前看来ES做的最大的工作是只是检索(说了一句重要的废话),我们在日常使用日志服务的时候,用于检索的绝大部分都是使用订单号,userId之类的业务主键进行查询。在ES上可以增加一个数据清洗层,这将更好的利用ES,关于这个可以从下面的简单的架构图中窥见一斑

ps:顺着这次机会,可以好好研究一下ES,出一些ES相关的文章
参考文章
Optimizing Elasticsearch: How Many Shards per Index?
我在 Elasticsearch 集群内应该设置多少个分片?
Elasticsearch权威指南
ES调优实践
ES官方调优指南




