ELK 性能(4) — 大规模 Elasticsearch 集群性能的最佳实践
ELK 性能(4) — 大规模 Elasticsearch 集群性能的最佳实践
介绍
集群规模
-
集群数:6
-
整体集群规模:
- 300 Elasticsearch 实例
- 141 物理服务器
- 4200 CPU 核心
- 38TB RAM
- 1.5 Pb 存储
-
索引日志:
- 100 亿/天
- 400k/秒
内容
开场白
健康提示
-
将 Elasticsearch 集群的名称 “elasticsearch” 进行重命名。当网络内有两个以上的集群时,就会发现这样做所带来的好处。
-
为了防止误删除,设置参数
action.destructive_requires_name=true
-
始终使用 SSD 。这并不是可选的。
-
需要至少 10G 的带宽。
-
采用监护人制度,开发并发布自己的版本。
扩展
扩展 Elasticsearch 集群
影响到 Elasticsearch 集群的因素
-
CPU
- 核心数 > 时钟速度
-
内存
- 文档的数量
- 分片的数量
-
磁盘 I/O
- SSD 持续写的速率
-
网络带宽
- 至少 10G 的带宽保证快速恢复与重新索引
影响到集群内存的因素
- 段内存(segment memory):~4b RAM/文档 = ~4Gb/10亿行日志
- 字段数据内存(field data memory):几乎与段内存相当
- 过滤器缓存(filter cache):~1/4 到 1/2 的段内存,取决于搜索的内容
- 剩下的所有(50% 的系统内存)用作操作系统文件的缓存
- 无法获得足够的内存
影响到集群I/O的因素
- SSD 持续写速率
- 计算片恢复的速度(假设一个节点失败):
- 片大小(Shard Size)=(日存储量 / 分片的数量)
- (每个节点上分片的数量 * 片大小)/ (磁盘写速度 / 节点分片的数量)
- 例如:30Gb 分片,每个节点 2 个分片,250Mbps 的写速度:
- (2 * 30Gb)/ 125Mbps = 8 mintues
- 恢复弹性所能忍受的时间
- 可以忍受失去多少节点
- 一台服务器多个节点会增加恢复所需的时间
影响到网络的因素
- 10G 至少
- 10 分钟恢复 vs 50+ 分钟恢复
- 1G 瓶颈:网络上线
- 10G 瓶颈:磁盘速度
扩展 Logstash 集群
扩展 Logstash 的 CPU
- 规则 1:买所能承受的尽可能快的 CPU 核心
- 规则 2:参见第一条
- 更多的过滤 = 更多的 CPU
监控
Marvel | 自研 |
---|---|
易用 | 需要花时间开发 |
数据存入 ES | 与自己的系统集成 |
很多分析度量 | |
没有集成 | 重复造轮子 |
成本高 | 免费 |
监控 Elasticsearch
-
度量在多个地方都有暴露:
-
_cat API
包括了大多数度量,易读
-
_stats API,_nodes API
涵盖所有,JSON格式,易于解析
-
-
发送到 Graphite
-
创建 dashboards
监控系统
-
SSD 性能
-
监控 Logstash 报管道阻塞的频率,并找出原因
-
动态的磁盘空间阀值
-
((服务器的数量 - 失败的数量)/ 服务器的数量)- 15%
-
100 服务器
-
最多允许 6 个失败
-
磁盘空间预警的阀值 =((100 - 6)/ 100)- 15%
磁盘空间预警的阀值 = 79%
-
-
根据集群增加与移除节点的数量配置并管理系统
-
额外的 15% 是用来提供申请并准备更多节点的时间
扩展 Logstash
影响 Logstash 性能的因素
-
日志行的长度
-
Grok 模式的复杂度 - 正则表达式非常慢
-
插件的使用
-
GC
- 增加的堆大小
-
超线程
- 度量,并关闭
重复测量
将日志以 JSON 格式输出并没有带来很大的好处,除非不使用 grok,kv 等。Logstash 还是需要将字符串转换成为 ruby 的 hash
GC 垃圾回收
-
缺省配置通常是可以的
-
确保记录了 GC 的图
-
Ruby 会很容易的创建很多对象:在做伸缩扩展时需要监控 GC
-
在写插件时需要时刻记住 GC
-
不好的:1_000_000.times
| | user | system | total | real
| ------------------------- | ------------------------|
| time | 0.130000 | 0.000000 | 0.130000 | ( 0.132482) -
好用法:foo = 'This is a string'; 1_000_000.times
| | user | system | total | real
| ------------------------- | ------------------------|
| time | 0.060000 | 0.000000 | 0.060000 | ( 0.055005)
-
插件性能基准
-
如何建立基准
-
度量某些过滤器
-
度量更多的过滤器
-
计算每个过滤器的成本
-
社区提供的过滤器只是在大多数情况下适用
- 对于特殊的场景需要自己开发
- 易于使用
-
在测评时执行至少 5 分钟的时间,使用大数据集
-
建立基准的吞吐量:Python,StatsD,Graphite
-
Logstash 简单配置,10m 行 apache 日志,没有过滤:
input { file { path => "/var/log/httpd/access.log" start_position => "beginning" } } output { stdout { codec => "dots" } }
-
Python 脚本将 Logstash 输出到 statsd :
sudo pip install statsd #!/usr/bin/env python import statsd, sys c = statsd.StatsClient('localhost', 8125) while True: sys.stdin.read(1) c.incr('logstash.testing.throughput', rate=0.001)
-
为什么我们不用 statsd 输出插件?它会降低输出的速度!
-
放在一起
logstash -f logstash.conf | pv -W | python throughput.py ![](http://images2015.cnblogs.com/blog/613455/201612/613455-20161201113003006-798605241.png)
插件性能 Grok
-
增加一个简单的 Grok
grok { match => [ "message", "%{ETSY_APACHE_ACCESS}" ] }
-
在只有一个 worker 时,性能下降 80%
-
增加 worker 的数量,吞吐量仍然下降了 33%:65k/s -> 42k/s
-w <num_cpu_cores>
插件性能 kv
-
加一个 kv 过滤器
kv { field_split => "&" source => "qs" target => "foo" }
-
吞吐量基本不变,有 10% 的下降(40k/s)
-
吞吐量变化较大主要因为 GC 的压力
-
kv 很慢,以下是一个用来查询字符串的
splitkv
插件kvarray = text.split(@field_split).map { |afield| pairs = afield.split(@value_split) if pairs[0].nil? || !(pairs[0] =~ /^[0-9]/).nil? || pairs[1].nil? || (pairs[0].length < @min_key_length && !@preserve_keys.include?(pairs[0])) next end if !@trimkey.nil? # 2 if's are faster (0.26s) than gsub (0.33s) #pairs[0] = pairs[0].slice(1..-1) if pairs[0].start_with?(@trimkey) #pairs[0].chop! if pairs[0].end_with?(@trimkey) # BUT! in-place tr is 6% faster than 2 if's (0.52s vs 0.55s) pairs[0].tr!(@trimkey, '') if pairs[0].start_with?(@trimkey) end if !@trimval.nil? pairs[1].tr!(@trimval, '') if pairs[1].start_with?(@trimval) end pairs } kvarray.delete_if { |x| x == nil } return Hash[kvarray]
splitkv
之前的 CPU 占用率是 100% ,之后的占用率是 33% 。
Elasticsearch 的输出
- Logstash 的输出设置直接影响了 Logstash 所在机器的 CPU
- 将 flush_size 从 500 改到 5000 ,或更多
- 将 idle_flush_time 从 1s 改到 5s ,
- 增加输出线程 workers
- 结果受日志行的影响
- 调整,等待 15 分钟,然后观察
当使用缺省的 500 flush_size 时,Logstash 集群的峰值会达到 50% ,处理能力在每秒 ~40k 日志行。将这个值改到 10k 时,同时增加 idle_flush_time 到 5s 。处理能力在每秒 ~150k 日志行,同时 CPU 占用会下降到 25% 。
Pipeline 管道性能
-
Logstash 2.3 之前
…/vendor/…/lib/logstash/pipeline.rb SizedQueue.new(20) -> SizedQueue.new(500)
-
Logstash 2.3 之后
—pipeline-batch-size=500
最好在调优最后改变这个参数。管道的性能受输出插件性能的影响。
测试配置变更
增加上下文
-
发现管道的延迟
mutate { add_field => [ "index_time", "%{+YYYY-MM-dd HH:mm:ss Z}" ] }
-
logstash 服务器处理日志行
mutate { add_field => [ "logstash_host", "<%= node[:fqdn] %>" ] }
-
对日志行进行哈希,实现重放
hashid
插件可以避免重复行 -
~10% 下降
服务器上的配置
describe package('logstash'),
:if => os[:family] == 'redhat' do
it { should be_installed }
end
describe command('chef-client') do
its(:exit_status) { should eq 0 }
end
describe command('logstash -t -f ls.conf.test') do
its(:exit_status) { should eq 0 }
end
describe command('logstash -f ls.conf.test') do
its(:stdout) { should_not match(/parse_fail/) }
end
describe command('restart logstash') do
its(:exit_status) { should eq 0 }
end
describe command('sleep 15') do
its(:exit_status) { should eq 0 }
end
describe service('logstash'),
:if => os[:family] == 'redhat' do
it { should be_enabled }
it { should be_running }
end
describe port(5555) do
it { should be_listening }
end
Input
input {
generator {
lines => [ '<Apache access log>' ]
count => 1
type => "access_log"
}
generator {
lines => [ '<Application log>' ]
count => 1
type => "app_log"
}
}
Filter
filter {
if [type] == "access_log" {
grok {
match => [ "message", "%{APACHE_ACCESS}" ]
tag_on_failure => [ "parse_fail_access_log" ]
}
}
if [type] == "app_log" {
grok {
match => [ "message", "%{APACHE_INFO}" ]
tag_on_failure => [ "parse_fail_app_log" ]
}
}
}
Output
output {
stdout {
codec => json_lines
}
}
小结
-
更快的 CPU
CPU 核心数 > CPU 时钟速度
-
增加管道的大小
-
更多内存
18Gb+ 防止频繁 GC
-
横向扩展
-
为日志行添加上下文
-
编写自己的插件
-
对所有的东西进行性能评测
扩展 Elasticsearch
默认基准
Logstash 输出: 默认选项 + 4 workers
Elasticsearch: 默认选项 + 1 shard, no replicas
影响索引的因素
- 日志行的长度与分析,默认映射
- doc_values - 必须
- 使用更多的 CPU 时间
- 索引时使用更多的磁盘空间,磁盘 I/O
- 有助于降低内存的使用
- 如果发现 fielddata 使用过多内存,定位占用最多的,然后将它们移到 doc_values
- 为恢复保留足够的带宽
- CPU
-
分析
-
映射
默认映射会创建大量 .raw 字段
-
doc_values
-
合并
-
恢复
-
- 内存
- 索引的缓冲
- GC
- 段(segment)数量和未优化的索引
- 网络
-
恢复的速度
更快的网络 == 更短的恢复延迟
-
影响内存的因素
-
以 32Gb 堆为例的分布情况:
- Field data: 10%
- Filter cache: 10%
- Index buffer: 500Mb
- Segment cache (~4 bytes per doc):
每个节点可存储的文档数
-
32Gb - ( 32G / 10 ) - ( 32G / 10 ) - 500Mb = ~25Gb (段内存)
-
25Gb / 4b = 6.7bn 个文档(所有片的总和)
-
10bn docs / day, 200 shards = 50m docs/shard
- 1 daily shard per node: 6.7bn / 50m / 1 = 134 days
- 5 daily shards per node: 6.7bn / 50m / 5 = 26 days
Doc Values
-
Doc values 可以降低内存开销
-
Doc values 会消耗 CPU 和存储
-
部分字段使用 doc_values:
1.7G Aug 11 18:42 logstash-2015.08.07/7/index/_1i4v_Lucene410_0.dvd
-
所有字段使用 doc_values:
106G Aug 13 20:33 logstash-2015.08.12/38/index/_2a9p_Lucene410_0.dvd
-
-
不要盲目地为所有字段开启 doc_values
- 找到使用最频繁的字段,然后将它们转换成 Doc Values
- curl -s 'http://localhost:9200/_cat/fielddata?v' | less -S
-
示例
total request_uri _size owner ip_address 117.1mb 11.2mb 28.4mb 8.6mb 4.3mb 96.3mb 7.7mb 19.7mb 9.1mb 4.4mb 93.7mb 7mb 18.4mb 8.8mb 4.1mb 139.1mb 11.2mb 27.7mb 13.5mb 6.6mb 96.8mb 7.8mb 19.1mb 8.8mb 4.4mb 145.9mb 11.5mb 28.6mb 13.4mb 6.7mb 95mb 7mb 18.9mb 8.7mb 5.3mb 122mb 11.8mb 28.4mb 8.9mb 5.7mb 97.7mb 6.8mb 19.2mb 8.9mb 4.8mb 88.9mb 7.6mb 18.2mb 8.4mb 4.6mb 96.5mb 7.7mb 18.3mb 8.8mb 4.7mb 147.4mb 11.6mb 27.9mb 13.2mb 8.8mb 146.7mb 10mb 28.7mb 13.6mb 7.2mb
内存小结
-
实例使用 128Gb 或 256Gb RAM
-
根据硬件配置优化 RAM
Haswell/Skylake Xeon CPUs 有 4 个内存通道
-
Elasticsearch 多个实例
为每个实例分配自己的名称 node.name
CPU
-
CPU 密集型操作
- 索引:分析,合并,压缩
- 搜索:计算,解压缩
-
写压力
- CPU 核心数受并发的索引操作影响
- 核心数 优于 CPU 频率值
基准
为什么这么慢?
[logstash-2016.06.15][0] stop throttling indexing:
numMergesInFlight=4, maxNumMerges=5
合并
第一步:将分片数从 1 提升到 5
第二步:禁用 merge throttling(ES < 2.0)
index.store.throttle.type: none
拆分 Hosts
当 CPU 接近最大时,需要加入更多节点
在不同 Hosts 上运行 Elasticsearch 以及 Logstash
吞吐量有 50% 的提升:13k/s -> 19k/s
超线程 Hyperthreading
超线程可以提升 20% 的性能
CPU 治理
~15-30% 的性能提升。
# echo performance | tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor
存储
磁盘 I/O
建议
- 使用 SSD
- RAID 0
- 软 RAID 足够
更多的建议
-
好的 SSD 非常重要
廉价 SSD 会大大降低性能
-
不要使用多个数据路径,使用 RAID 0
大量的 translog 写磁盘操作会是瓶颈
-
如果有大量段合并,但是 CPU 和 磁盘 I/O 还有空闲:
可以尝试提升值
index.merge.scheduler.max_thread_count
-
降低间隔(Durability)
index.translog.durability: async
Translog fsync() 值为 5s ,足够
-
集群的恢复会吃掉大量磁盘 I/O
需要在恢复前后调整相应的参数
indices.recovery.max_bytes_per_sec: 300mb cluster.routing.allocation.cluster_concurrent_rebalance: 24 cluster.routing.allocation.node_concurrent_recoveries: 2
-
任何的持续 I/O 等待都意味着存在一个次优状态
SSD 的选择
-
消费级
- 慢速写
- 廉价
- 低耐久性,每天相对较少的写次数
-
企业级
- 快速写
- 昂贵
- 高耐久性,每天相对较高的写次数
-
大量读
- 低耐久性,1-3 DWPD
- 低速读,代价小
-
混合使用
- 中度耐久性,10 DWPD
- 平衡读写,中等价位
-
大量写
- 高耐久性,25 DWPD
- 高速写,代价高
基准
降低间隔后,基本仍然维持在 ~20-25k,但更平滑
为什么提升很小?Merging
$ curl -s 'http://localhost:9200/_nodes/hot_threads?threads=10' | grep %
73.6% (367.8ms out of 500ms) 'elasticsearch[es][bulk][T#25]'
66.8% (334.1ms out of 500ms) 'elasticsearch[es][[logstash][1]: Lucene Merge Thread #139]'
66.3% (331.6ms out of 500ms) 'elasticsearch[es][[logstash][3]: Lucene Merge Thread #183]'
66.1% (330.7ms out of 500ms) 'elasticsearch[es][[logstash][1]: Lucene Merge Thread #140]'
66.1% (330.4ms out of 500ms) 'elasticsearch[es][[logstash][4]: Lucene Merge Thread #158]'
62.9% (314.7ms out of 500ms) 'elasticsearch[es][[logstash][3]: Lucene Merge Thread #189]'
62.4% (312.2ms out of 500ms) 'elasticsearch[es][[logstash][2]: Lucene Merge Thread #160]'
61.8% (309.2ms out of 500ms) 'elasticsearch[es][[logstash][1]: Lucene Merge Thread #115]'
57.6% (287.7ms out of 500ms) 'elasticsearch[es][[logstash][0]: Lucene Merge Thread #155]'
55.6% (277.9ms out of 500ms) 'elasticsearch[es][[logstash][2]: Lucene Merge Thread #161]'
分层存储
- 将更多访问的索引放在更多的服务器上,并分配更多的内存以及更快的 CPU
- 将 “冷” 索引独立存储(SSD下仍然需要这么做)
- 设置 index.codec: best_compression
- 移动索引,重新优化
- 构建 elasticsearch-curator 可以让事情变得简单
为什么默认的配置 Merging 如此多?
$ curl 'http://localhost:9200/_template/logstash?pretty'
看到了吗?
"string_fields" : {
"mapping" : {
"index" : "analyzed", // <--- see?
"omit_norms" : true,
"type" : "string",
"fields" : {
"raw" : {
"ignore_above" : 256, // <--- see?
"index" : "not_analyzed", // <--- see?
"type" : "string" // <--- see?
}
}
},
"match_mapping_type" : "string",
"match" : "*"
}
使用自定义映射
"string_fields" : {
"mapping" : {
"index" : "not_analyzed",
"omit_norms" : true,
"type" : "string"
},
"match_mapping_type" : "string",
"match" : "*"
}
有那么一点帮助
索引的性能
-
增加 bulk 线程池可以控制索引的爆发
但同时也要注意,这会隐藏性能的问题
-
增加索引的缓冲
-
增加刷新的时间,1s 到 5s
-
将索引请求发送到多个 hosts
-
增加 worker 直到没有明显的性能提升为止
num_cpu / 2
-
增加 flush_size 知道没有明显的性能提升为止
10,000
-
磁盘 I/O 性能
-
索引协议
- HTTP
- Node
- Transport
-
Transport 仍然是性能最好的,但是 HTTP 已经非常接近了
-
Node 基本上不会使用
-
自定义映射模板
- 默认模板为每个字段额外生成 not_analyzed.raw 字段
- 分析每个字段会占用 CPU
- 额外的字段会吃掉更多磁盘空间
- 动态字段和 Hungarian 标记
-
使用开启了动态字段的自定义映射模板,但是将它们设置为 non_analyzed 剔除 .raw 字段,除非真的需要它。
-
这可以将 Elasticsearch 集群的 CPU 的使用率从 28% 降到 15%
-
消息的复杂度也十分相关
加 20k 的新行与平均 1.5k 的索引速率
-
截断
ruby { code => "if event['message'].length > 10240 then event['message'] = event['message'].slice!(0,10240) end" }
-
让 Logstash 做更多的事情
索引的大小
-
按索引来调优分片
num_shards = (num_nodes - failed_node_limit) / (number_of_replicas + 1)
50 个节点,并允许最多 4 个节点失败,replication 为 1x:
num_shards = (50 - 4) / (1 + 1) = 23
-
如果分片大于 25Gb ,需要相应增加分片数
-
调优 indices.memory.index_buffer_size
index_buffer_size = num_active_shards * 500Mb
其中“active_shards”:指任何 5 分钟内更新的分片
-
调试 refresh_interval
-
默认 1s - 过于频繁
-
增加到 5s
-
更高的值会导致磁盘抖动
-
目标:将磁盘里的缓冲尽可能的移储
例如:Samsung SM863 SSDs
-
DRAM buffer: 1Gb
-
Flush speed: 500Mb/sec
-
-
参考
参考来源:
2016.6 ELK: Moose-ively scaling your log system