Elasticsearch(ES)作为NOSQL+搜索引擎的有机结合体,不仅有近实时的查询能力,还具有强大的聚合分析能力。因此在全文检索、日志分析、监控系统、数据分析等领域ES均有广泛应用。而完整的Elastic Stack体系(Elasticsearch、Logstash、Kibana、Beats),更是提供了数据采集、清洗、存储、可视化的整套解决方案。
本文基于ES 5.6.4,从性能和稳定性两方面,从linux参数调优、ES节点配置和ES使用方式三个角度入手,介绍ES调优的基本方案。当然,ES的调优绝不能一概而论,需要根据实际业务场景做适当的取舍和调整,文中的疏漏之处也随时欢迎批评指正。
Elastic Stack
sed -i '/swap/s/^/#/' /etc/fstab
swapoff -a
mount -o noatime,data=writeback,barrier=0,nobh /dev/sda /es_data
echo noop > /sys/block/sda/queue/scheduler
indices.memory.index_buffer_size: 15%
thread_pool.bulk.queue_size: 1024
在规模比较大的集群中,可以防止新建shard时扫描所有shard的元数据,提升shard分配速度。
cluster.routing.allocation.disk.include_relocations: false
ES底层使用Lucene存储数据,主要包括行存(StoreFiled)、列存(DocValues)和倒排索引(InvertIndex)三部分。
大多数使用场景中,没有必要同时存储这三个部分,可以通过下面的参数来做适当调整:
# 关闭 _source
PUT my_index
{
"mappings": {
"my_type": {
"_source": {
"enabled": false
}
}
}
}
# _source只存储部分字段,通过includes指定要存储的字段或者通过excludes滤除不需要的字段
PUT my_index
{
"mappings": {
"_doc": {
"_source": {
"includes": [
"*.count",
"meta.*"
],
"excludes": [
"meta.description",
"meta.other.*"
]
}
}
}
}
PUT my_index
{
"mappings": {
"my_type": {
"properties": {
"session_id": {
"type": "keyword",
"doc_values": false
}
}
}
}
}
PUT my_index
{
"mappings": {
"my_type": {
"properties": {
"session_id": {
"type": "keyword",
"index": false
}
}
}
}
}
PUT /my_index
{
"mapping": {
"my_type": {
"_all": {
"enabled": false
}
}
}
}
PUT /my_index
{
"mapping": {
"my_type": {
"_field_names": {
"enabled": false
}
}
}
}
对于打开了上述_source字段的index,可以通过下面的命令来把lucene适用的压缩算法替换成 DEFLATE,提高数据压缩率。
PUT /my_index/_settings
{
"index.codec": "best_compression"
}
写入数据时尽量使用下面的bulk接口批量写入,提高写入效率。每个bulk请求的doc数量设定区间推荐为1k~1w,具体可根据业务场景选取一个适当的数量。
POST _bulk
{ "index" : { "_index" : "test", "_type" : "type1" } }
{ "field1" : "value1" }
{ "index" : { "_index" : "test", "_type" : "type1" } }
{ "field1" : "value2" }
默认情况下,translog的持久化策略是,对于每个写入请求都做一次flush,刷新translog数据到磁盘上。这种频繁的磁盘IO操作是严重影响写入性能的,如果可以接受一定概率的数据丢失(这种硬件故障的概率很小),可以通过下面的命令调整 translog 持久化策略为异步周期性执行,并适当调整translog的刷盘周期。
PUT my_index
{
"settings": {
"index": {
"translog": {
"sync_interval": "5s",
"durability": "async"
}
}
}
}
写入Lucene的数据,并不是实时可搜索的,ES必须通过refresh的过程把内存中的数据转换成Lucene的完整segment后,才可以被搜索。默认情况下,ES每一秒会refresh一次,产生一个新的segment,这样会导致产生的segment较多,从而segment merge较为频繁,系统开销较大。如果对数据的实时可见性要求较低,可以通过下面的命令提高refresh的时间间隔,降低系统开销。
PUT my_index
{
"settings": {
"index": {
"refresh_interval" : "30s"
}
}
}
ES的一个index由多个shard组成,而一个shard其实就是一个Lucene的index,它又由多个segment组成,且Lucene会不断地把一些小的segment合并成一个大的segment,这个过程被称为merge。默认值是Math.max(1, Math.min(4, Runtime.getRuntime().availableProcessors() / 2)),当节点配置的cpu核数较高时,merge占用的资源可能会偏高,影响集群的性能,可以通过下面的命令调整某个index的merge过程的并发度:
PUT /my_index/_settings
{
"index.merge.scheduler.max_thread_count": 2
}
当用户显示指定_id写入数据时,ES会先发起查询来确定index中是否已经有相同_id的doc存在,若有则先删除原有doc再写入新doc。这样每次写入时,ES都会耗费一定的资源做查询。如果用户写入数据时不指定doc,ES则通过内部算法产生一个随机的_id,并且保证_id的唯一性,这样就可以跳过前面查询_id的步骤,提高写入效率。
所以,在不需要通过_id字段去重、update的使用场景中,写入不指定_id可以提升写入速率。腾讯云CES技术团队的测试结果显示,无_id的数据写入性能可能比有_id的高出近一倍,实际损耗和具体测试场景相关。
# 写入时指定_id
POST _bulk
{ "index" : { "_index" : "test", "_type" : "type1", "_id" : "1" } }
{ "field1" : "value1" }
# 写入时不指定_id
POST _bulk
{ "index" : { "_index" : "test", "_type" : "type1" } }
{ "field1" : "value1" }
对于数据量较大的index,一般会配置多个shard来分摊压力。这种场景下,一个查询会同时搜索所有的shard,然后再将各个shard的结果合并后,返回给用户。对于高并发的小查询场景,每个分片通常仅抓取极少量数据,此时查询过程中的调度开销远大于实际读取数据的开销,且查询速度取决于最慢的一个分片。开启routing功能后,ES会将routing相同的数据写入到同一个分片中(也可以是多个,由index.routing_partition_size参数控制)。如果查询时指定routing,那么ES只会查询routing指向的那个分片,可显著降低调度开销,提升查询效率。
routing的使用方式如下:
# 写入
PUT my_index/my_type/1?routing=user1
{
"title": "This is a document"
}
# 查询
GET my_index/_search?routing=user1,user2
{
"query": {
"match": {
"title": "document"
}
}
}
PUT my_index
{
"mappings": {
"my_type": {
"properties": {
"title": {
"type": "text",
"norms": false,
"index_options": "docs",
"fields": {
"raw": {
"type": "keyword"
}
}
}
}
}
}
}
# 1. 通过mapping指定 tags 字段为keyword类型
PUT my_index
{
"mappings": {
"my_type": {
"properties": {
"tags": {
"type": "keyword"
}
}
}
}
}
# 2. 通过template,指定my_index*类的index,其所有string字段默认为keyword类型
PUT _template/my_template
{
"order": 0,
"template": "my_index*",
"mappings": {
"_default_": {
"dynamic_templates": [
{
"strings": {
"match_mapping_type": "string",
"mapping": {
"type": "keyword",
"ignore_above": 256
}
}
}
]
}
},
"aliases": {}
}
默认情况下,ES通过一定的算法计算返回的每条数据与查询语句的相关度,并通过_score字段来表征。但对于非全文索引的使用场景,用户并不care查询结果与查询条件的相关度,只是想精确的查找目标数据。此时,可以通过query-bool-filter组合来让ES不计算_score,并且尽可能的缓存filter的结果集,供后续包含相同filter的查询使用,提高查询效率。
# 普通查询
POST my_index/_search
{
"query": {
"term" : { "user" : "Kimchy" }
}
}
# query-bool-filter 加速查询
POST my_index/_search
{
"query": {
"bool": {
"filter": {
"term": { "user": "Kimchy" }
}
}
}
}
写入ES的数据最好通过某种方式做分割,存入不同的index。常见的做法是将数据按模块/功能分类,写入不同的index,然后按照时间去滚动生成index。这样做的好处是各种数据分开管理不会混淆,也易于提高查询效率。同时index按时间滚动,数据过期时删除整个index,要比一条条删除数据或delete_by_query效率高很多,因为删除整个index是直接删除底层文件,而delete_by_query是查询-标记-删除。
举例说明,假如有module_a,moduleb两个模块产生的数据,那么index规划可以是这样的:一类index名称是module_a + {日期},另一类index名称是module_b+ {日期}。对于名字中的日期,可以在写入数据时自己指定精确的日期,也可以通过ES的ingest pipeline中的index-name-processor实现(会有写入性能损耗)。
# module_a 类index
- 创建index:
PUT module_a@2018_01_01
{
"settings" : {
"index" : {
"number_of_shards" : 3,
"number_of_replicas" : 2
}
}
}
PUT module_a@2018_01_02
{
"settings" : {
"index" : {
"number_of_shards" : 3,
"number_of_replicas" : 2
}
}
}
...
- 查询数据:
GET module_a@*/_search
# module_b 类index
- 创建index:
PUT module_b@2018_01_01
{
"settings" : {
"index" : {
"number_of_shards" : 3,
"number_of_replicas" : 2
}
}
}
PUT module_b@2018_01_02
{
"settings" : {
"index" : {
"number_of_shards" : 3,
"number_of_replicas" : 2
}
}
}
...
- 查询数据:
GET module_b@*/_search
分片(shard):一个ES的index由多个shard组成,每个shard承载index的一部分数据。
副本(replica):index也可以设定副本数(number_of_replicas),也就是同一个shard有多少个备份。对于查询压力较大的index,可以考虑提高副本数(number_of_replicas),通过多个副本均摊查询压力
shard数量(number_of_shards)设置过多或过低都会引发一些问题:shard数量过多,则批量写入/查询请求被分割为过多的子写入/查询,导致该index的写入、查询拒绝率上升;对于数据量较大的inex,当其shard数量过小时,无法充分利用节点资源,造成机器资源利用率不高 或 不均衡,影响写入/查询的效率。
对于每个index的shard数量,可以根据数据总量、写入压力、节点数量等综合考量后设定,然后根据数据增长状态定期检测下shard数量是否合理。腾讯云CES技术团队的推荐方案是:
# 修改系统资源限制
# 单用户可以打开的最大文件数量,可以设置为官方推荐的65536或更大些
echo "* - nofile 655360" >>/etc/security/limits.conf
# 单用户内存地址空间
echo "* - as unlimited" >>/etc/security/limits.conf
# 单用户线程数
echo "* - nproc 2056474" >>/etc/security/limits.conf
# 单用户文件大小
echo "* - fsize unlimited" >>/etc/security/limits.conf
# 单用户锁定内存
echo "* - memlock unlimited" >>/etc/security/limits.conf
# 单进程可以使用的最大map内存区域数量
echo "vm.max_map_count = 655300" >>/etc/sysctl.conf
# TCP全连接队列参数设置, 这样设置的目的是防止节点数较多(比如超过100)的ES集群中,节点异常重启时全连接队列在启动瞬间打满,造成节点hang住,整个集群响应迟滞的情况
echo "net.ipv4.tcp_abort_on_overflow = 1" >>/etc/sysctl.conf
echo "net.core.somaxconn = 2048" >>/etc/sysctl.conf
# 降低tcp alive time,防止无效链接占用链接数
echo 300 >/proc/sys/net/ipv4/tcp_keepalive_time
-Xms和-Xmx设置为相同的值,推荐设置为机器内存的一半左右,剩余一半留给系统cache使用。
ES集群的元信息管理、index的增删操作、节点的加入剔除等集群管理的任务都是由master节点来负责的,master节点定期将最新的集群状态广播至各个节点。所以,master的稳定性对于集群整体的稳定性是至关重要的。当集群的节点数量较大时(比如超过30个节点),集群的管理工作会变得复杂很多。此时应该创建专有master节点,这些节点只负责集群管理,不存储数据,不承担数据读写压力;其他节点则仅负责数据读写,不负责集群管理的工作。这样把集群管理和数据的写入/查询分离,互不影响,防止因读写压力过大造成集群整体不稳定。
将专有master节点和数据节点的分离,需要修改ES的配置文件,然后滚动重启各个节点。
# 专有master节点的配置文件(conf/elasticsearch.yml)增加如下属性:
node.master: true
node.data: false
node.ingest: false
# 数据节点的配置文件增加如下属性(与上面的属性相反):
node.master: false
node.data: true
node.ingest: true
上面提到,ES的元信息由master节点管理,定期同步给各个节点,也就是每个节点都会存储一份。这个元信息主要存储在cluster_state中,如所有node元信息(indices、节点各种统计参数)、所有index/shard的元信息(mapping, location, size)、元数据ingest等。
ES在创建新分片时,要根据现有的分片分布情况指定分片分配策略,从而使各个节点上的分片数基本一致,此过程中就需要深入遍历cluster_state。当集群中的index/shard过多时,cluster_state结构会变得过于复杂,导致遍历cluster_state效率低下,集群响应迟滞。腾讯云CES技术团队曾经在一个20个节点的集群里,创建了4w+个shard,导致新建一个index需要60s+才能完成。
当index/shard数量过多时,可以考虑从以下几方面改进:
前面提到,ES底层采用Lucene做存储,而Lucene的一个index又由若干segment组成,每个segment都会建立自己的倒排索引用于数据查询。Lucene为了加速查询,为每个segment的倒排做了一层前缀索引,这个索引在Lucene4.0以后采用的数据结构是FST (Finite State Transducer)。Lucene加载segment的时候将其全量装载到内存中,加快查询速度。这部分内存被称为SegmentMemory,常驻内存,占用heap,无法被GC。
前面提到,为利用JVM的对象指针压缩技术来节约内存,通常建议JVM内存分配不要超过32G。当集群的数据量过大时,SegmentMemory会吃掉大量的堆内存,而JVM内存空间又有限,此时就需要想办法降低SegmentMemory的使用量了,常用方法有下面几个:
腾讯云CES技术团队在此基础上,对FST部分进行了优化,释放高达40%的Segment Memory内存空间。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。