导读:Elasticsearch 是广泛使用的一个软件,我们邀请了曾经在高可用架构分享过 ES 的王卫华继续分享在升级 Elasticsearch 过程中的经验。
王卫华,资深开发工程师、架构师,具有 10+ 年互联网从业经验,曾获得微软 2002 – 2009 MVP 荣誉称号。在百姓网近 9 年,负责后端代码开发和 Elasticsearch & Solr 维护工作。现就职于途虎养车。
背景
百姓网使用 Elasticsearch 虽然有用于日志(ELK),但本次分享所涉及 Elasticsearch 升级,是指用于业务系统数据服务的 Elasticsearch 集群。
百姓网是一个分类网站,要提供快速数据查询,我们使用了 Lucene 作为基层的搜索系统,从几年前的 Solr 到 现在使用的 Elasticsearch。为了提供快速的查询响应,我们使用了一个 golang 写的代理系统,代理后面是几个 Elasticsearch 集群,以应对不同查询。
因为集群众多,一次性全部系统升级需要占用一倍的机器,这比较浪费,所以我们采用一个集群一个集群升级,这就需要不同版本的集群同时存在,从 1.0 升级到 1.6/1.7,他们基本查询都相差不大,然而,从 1.x 到 2.x,需要做的事情就很多了。而且很不幸,还有坑。
下面谈谈我们在升级过程所遇到的一些问题和解决之路。
一、Elasticsearch 2.x 变化
1、doc_values
这无疑是 2.x 中最大变化之一,虽然之前也有 doc_values,但是这次是默认开启 doc_values,也说明官方是建议你使用 doc_values 的。
2、Filtered Query
这个已经 deprecated,在 2.x 中你还可以用。但是建议做如下修改
{
“query”: {
“filtered”: {
“query”: {
…….
},
“filter”: {
…….
}
}
}
}
修改为
{
“query”: {
“bool”: {
“must”: {
…….
},
“filter”: {
…….
}
}
}
}
把 query 和 filter 移到 bool 查询的 must 和 filter 参数之中。
3、DeleteByQuery
现在作为一个插件了,而且使用的是 Scroll/Scan & Bulk 来进行安全删除,当然,速度可能慢一些。(./bin/plugin install delete-by-query 安装插件)
4、Facet 已经删除,使用 aggerations 代替。
Aggerations histogram min_doc_count 默认值现在是 0。
5、network.host 默认是 localhost,如果不设置就只能本机访问了。
一般设置为网络设备名称相关,如 eth0,则设置为 _eth0:ipv4,若是 em1,这设置为 _em1:ipv4。
6、Discovery:multicast (组播)因为系统受限的原因,现在从 Elasticsearch 移除。
不过它也可以作为一个插件加入。1.x multicast 默认是启用的,2.x 使用 unicast (单播),需要设置 discovery.zen.ping.unicast.hosts: [“host1:port”, “host2”],以使得集群可以加入相关机器。
7、Store FS: 内存(memory/ram)存储模式被移除。
默认使用 default_fs,是一种 Lucene MMapDirectory 和 NIOFSDirectory 混合的模式,词典文件和 doc values 文件使用 mmap 映射到系统虚拟内存(需要设置 vm.max_map_count=262144),其他的文件(如频率、位置等)使用 nio 文件系统。
8、Mapping 变化
1)同名字段:如果同一个索引中有不同类型的同名字段,那么这两个类型的 mapping 必须一致。并且不能删除 mapping (删了mapping,另一个类型同名字段就没有 mapping 了?)。
2)各种 _ 前缀的名称移除了。
3)dot (点)的各种坑,字段名不要包含 dot。
4)字段名最长 255
5)_routing 只能设置为required : true,没有 path 参数。
6)analyzer 现在可以分开设置 index_analyzer 和 search_analyzer。默认设置 analyzer 即为两者(index、search)同一配置。
9、快照的配置 path.repo 要设置白名单(注:是一个数组)。
10、Scroll:
1)search_type=scan deprecated,你可以在 scroll 查询时使用 sort:”_doc” 来代替,_doc 排序已经进行了优化,因此它的性能和 scan 相同。
2)search_type=count deprecated, 可以设置 size:0 。
11、 Optimize:deprecated。使用 force merge 接口代替。
12、geo_point:percolating 的地理查询移除了。
Percolator docs 是在内存中,不支持 doc_values,而 geo_point(ES 2.2)的一些查询功能需要启用 doc_values。
不过,geo_point 禁用了doc_values,有些一般查询仍然有效。
13、indices.fielddata.cache.expire 配置移除(默认会忽略)。
二、升级之路
1、IO 压力增大
2.0 刚出的时候,我们进行了测试,发现 IO 压力有点大。启用比不启用 doc_values,IO 压力要增加一倍以上(测试磁盘非 SSD)。
2.0 初始版本,Delete 会导致 IO 压力更大,删除操作会有 translog 等诡异问题。
解决办法:建议升级到较高版本,如 2.2 及以上。
2、Index 速度变慢。
在 1.x 时候,我们没有启用 Bulk 接口,而是使用 Index 接口,升级后发现更新速度比较慢。我们改用 Bulk 接口以解决这个问题。
3、Bulk 接口的问题
如果使用 Bulk 接口来进行删除,建议升级到较高版本,因为 2.0 初始版本 Bulk delete 可以不需要提供 routing,但是这样性能也很差。较高版本修复了这个问题,删除一个 DOC,需要提供 routing。
其实要获得 routing 并不困难,2.x 在你查询的时候,提供的结果中,就有 routing 这个数据,这个对于做删除操作还是比较方便的,不需要进行计算,还能保证在 routing 频繁变化后删除干净。
3、Doc Values
Lucene 索引是一种倒排索引,当需要进行排序或者计算时,需要在内存中使用 fielddata cache 进行计算,极端情况下,可能导致 OOM 或者内存泄露。这时候可以考虑启用 doc_values,这个是索引时候已经进行处理的一种非倒排索引。启用 doc_values,性能有一点损失,但是可以设置较小的 heap size,而留下内存给系统缓存 doc_values 索引,性能几乎相当。
1)启用 doc_values 后,Index size 增加近一倍。
2)启用 doc_values ,当进行 aggs,sort 时,减少内存需求,减低 GC 压力。可以设置较小的 heap size。
3)启用 doc_values 后,当 Lucene 索引有效使用系统缓存时,性能几乎相当。
4)2.x 你仍然可以 Disable doc_values,设置一个较大的 heap。只要没有较大的 GC 问题,选择 disable doc_values 是可以的,而且带来的好处是索引较小。这是一个平衡选择,大家可以根据平时使用情况进行调整。我们选择了 disable doc_values 以减少索引大小。
4、GC 各种挂、挂、挂。
在 1.x 升级到 2.x 的过程,基于集群只能滚动式升级,这决定 1.x 和 2.x 集群是同时共存。而在升级过程中,不幸躺着中枪,频繁遇到 GC 问题,几乎导致升级失败。
首先我们尝试了进行 GC 调优,CMS,G1,调整 heap size,heap NEW size ….,各种策略均告失败。调整 thread pool 各项参数,对 query:size 过大数字也进行调整,以减少 GC 压力,这些调整也均失效。
具体表现为,运行一段时间后,集群中某些 Node 的 CPU Usage 会突然上升,最后 JVM 保持在 100% CPU Usage,集群 Node 因为长期下线,被集群踢出,如果运气好,Node 还会回来,大部分情况下它就保持在 100% CPU Usage 不死不活。
检查日志,并无 OOM,而显示 GC 问题很大,在几次 CMS GC (new heap) 后,发生 Full GC,并且 Heap 使用率一直保持 90% 左右,GC 进入死循环。
一开始,判断是 GC 问题,故而一直进行 GC 调优,未果。
当我们遇到 JVM GC 时,很可能并非 GC 策略本身问题,而可能是应用的 BUG。最后,我们不得不另寻出路。
1)对 Cache (Static 配置,需要配置在 elasticsearch.yml 并重启) 和 Circuit breaker 配置进行调整。如下:
Static 配置:
indices.queries.cache.size
indices.cache.filter.size
indices.queries.cache.size
indices.memory.index_buffer_size
Circuit breaker 配置:
indices.breaker.request.limit
indices.breaker.fielddata.limit
indices.breaker.total.limit
前者和后者中相关的配置需要保持前者小于后者。
调整这些数据,未果。
2)Mapping 过大?
我们的 Mapping 确实比较大,因为业务处理逻辑复杂,各种名字的字段没有明确的限制,所以 Mapping 是比较大的。在 Mapping 很大的时候,当一个新的字段进行索引,每个索引都要进行 mapping 更新,可能会导致 OOM。不过我们观察到我们的 GC 问题和索引更新并没有很明显的联系,因为我们在进行索引初始化时,快速 Bulk 索引也只是 LA 比较大,并无 GC 问题,再一个在 1.x mapping 也没有什么问题。
3)Shards 太多?
shards 过多,也是可能导致 GC 问题的。因为每个 shard 的内存使用控制变得复杂。尽管我们某些集群的 shards 数量较多( shards 90 * 2 = 180 个 shard),但尝试调整或合并 Shards,均告无果。
4)Doc_values 和 fielddata cache 选择
因为 GC 这种问题,所以我们尝试减少 JVM 的内存使用,降低 GC 压力。启用 doc_values后,Heap 内存占用变小,但不能解决这个问题。减小 Heap 大小,以减轻 GC 压力,也无法解决这个问题。
5) Filtered Query 兼容之坑
我们对 1.x 和 2.x 集群加上了版本区分。在 2.x 的情况下,我们对查询进行了强制修改。修改办法就是上面提到的 Filtered Query 变更。即取消 filtered 而使用 bool 来进行代替。GC 问题得到缓解。
6)Aggerations histogram
我们经过仔细对比 1.x 和 2.x,对于 aggs histogram 的默认值变化(doc_min_count从1到0),一开始并没有重视,后来显式的设置这个参数为 1。GC问题得到解决。
上面的 5)6) 就是 GC 问题两个很深的坑。
虽然他们算不上是 BUG,然而在 filtered query 只是 deprecated,而不是不能使用的情况下,这也太坑人了,遇到需要多集群滚动式升级的(比如我们),可能就会沿用 filtered query,以便能平滑升级,然后就会掉进深坑而不能自拔。
而 6)也算不上是 BUG,不过对于 doc_min_count = 0,大概率会触发 GC,使用任何 GC 策略都不能正常使用。
三、优化或建议
1、Lucene version 在初期版本要显式的在 mapping::settings 中配置。后来的版本没有问题了。建议升级到较高版本以避免这种问题。
2、 aggerations 尽可能不要用在 analyzed fields,原因是 analyzed fields 是没有 doc_values的,另外 analyzed fields 分词之后,你进行 aggerations 也只能得到 term 的统计结果。
3、如果修改文档是增量的,并且不会带来数据覆盖问题,建议使用 update API(或 bulk update API),可接受部分数据更新,而不需要一个完整文档。
4、thread pool 调整。
如果一台服务器内存较大或者因为多集群原因需要配置多个 Elasticsearch JVM node,建议调整默认的 threadpool.search.size (默认值:int((available_processors * 3) / 2) + 1),比如默认值为 24,此时这台机器有 2 JVM node,可以根据各 node 大致的访问量、访问压力在 24 / 2 = 12 上下调整。如果更配置更多的 JVM 以有效利用 CPU 和内存,需要进行这个调整。否则 JVM 可能奔溃而无法启动。
5、count api (search api with size 0)
Count API 在某种情况下是很有效,比如当你只想获得 Total Count 的时候,可以使用这个 API。
不过,2.1 以后已经使用 search API 并设置 size = 0 来代替了。新版本中 Elasticsearch Java 代码中 Count API 已经去除,但是应用层面 _count 还是保留的。
6、timeout 参数 2.x 必须加上 s ,如 :? timeout = 3s
四、百姓之道
0、基本优化: 包括 硬件(CPU、Memory、SSD)、JVM 及其版本选择(Heap size,GC,JDK8)、系统配置(File Descriptors、VM/Virtual memory、Swap、Swappiness、mlockall)。
我们使用多核服务器和大内存,一定程度上可以弥补非 SSD 磁盘。
一台服务器多个 JVM,版本为 JDK8;Heapsize 一般为 30G 以内,根据不同用途、索引大小和访问压力 ,Heapsize 有 5、10、20、30G 的不同配置,Heap NewSize 配置比较激进,通常大于 Heapsize 的一半;GC 选择 CMS GC。
1、routing : UID, first_category, city + second_category
为了提供快速查询,根据业务特点对集群进行不同搭配,如用户访问(带有 Uid)将指向到 UID 集群;查询一个城市的二手手机将会指向到 city + second_category 集群;指定了类目的查询将指向 city + second_category 集群的 first_category 索引(我们的特点是一级类目基本固定)。
2、fulltext && normal Cluster
我们的信息特点是,信息描述内容比较多,并且需要对描述内容做全文索引。这样会导致集群的索引大小非常大,需要占用的磁盘和内存也就很多。从上文可知,我们根据业务特点划分了不同的集群,如果每个集群都包含了信息描述内容,索引都会很大,带来成本的提高,也增加了维护难度。
我们业务另外一个特点是,全文索引查询占所有类型查询比例较低,所以一个大的集群可以提供全部全文索引查询,那么另外的集群就可以不需要索引“信息描述内容”,索引就大大的减小了。
3、time: week(N百万级),2 month(N千万级),full(N 亿级)
我们还有采用时间来进行区分的集群。基于某些业务对信息新鲜度敏感,所以可以获取一周或二月的信息即可满足需求。大大减少对 Full 类型集群的访问压力,也能提供快速访问。
4、full cluster(N 亿级) && mini Cluster(N千万级)
使用时间划分集群后,还有一个好处,我们可以用二月的信息的集群来作为较小集群,让查询优先访问这个集群,当数据满足条件后,就不需要查询 Full 集群;数据不足继续查询 Full 集群。大集群的访问压力进一步降低。
这时,若查询了较小集群,并且需要准确的 Total Count (默认提供一个 Mini 集群10倍的数字),可以进一步使用 Count API (设置 size:0)去访问 Full 集群。
5、full cluster && other small cluster(N千万级/百万级,业务分拆)
这里和 4)不同的地方在于,上面使用的是时间划分,而这里是业务划分。这个集群只包含了特定数据的集群(比如二手大类目的二级类目手机),主要看相关查询量是否很大,若是这类查询带来压力较大,就有必要分出去。
6、Cloud Query (Cached Query)
我们的一个特点,第一至三页几乎是所有访问的 80% 以上,所以这部分查询我们构造了一个 Cloud Query 池,用于提供快速访问。这个池:
1)使用 DSL 查询,查询方法同 Elasticsearch。
2)初始化数据从 Elasticsearch 获取。
3)保留了几百个左右新鲜数据。
4)不断更新。
5)数据不足,查询指向 Elasticsearh。
6)使用 Redis zset 存储新鲜数据 (Redis Cluster)。
为实现上面的功能,我们使用 golang 语言开发一个 Proxy 类型的服务(代号 4Sea)。
五、后话:Elasticsearch 5.0
0、Lucene 6
“磁盘空间少一半;索引时间少一半;” ,Merge 时间和 JVM Heap 占用都会减少,索引本身的性能也提升。
“查询性能提升25%;IPV6也支持了”。
1、Profile API,可以用来进行查询性能监控和查询优化。不用再对耗时查询两眼一抹黑。
2、翻页利器: Search After。search 接口的一个新实现,使得你可以深度翻页。这个弥补了 scroll 和 search 的不足。
3、Shrink API: 合并 Shards 数,现在不用担心 shard 数字设置不合理,你可以使用这个 API 去合并以减少索引 shards 数量。
4、Reindex,应该是比较令人心动的 API,可惜需要启用 _source。
5、更新数据的 wait_for refresh 特性,可能在某种用户非异步更新时会有好处,让用户(更新接口)等待到更新完成,避免用户得不到数据或者得到老数据。
6、delete_by_query 重回 core !!!但是实现方式优化了。
7、2.x 中 Deprecated 的功能在这个版本大多移除。
还有更多….
结语:
升级 2.x 成功,5.x 还会远吗?看到上面的好处,我想大家都有强烈的升级冲动。
升级工具:
0.90.x /1.x => 2.x
https://github.com/elastic/elasticsearch-migration/tree/1.x
2.x => 5.0
https://github.com/elastic/elasticsearch-migration/tree/2.x
Q&A;
提问:对比下 Elasticsearch 和 Solr?为何贵司选了 ES?
王卫华:当初使用 Solr 的时候,Elasticsearch 还没出现。Elasticsearch 作为一个新出现的开源搜索引擎,有许多新特性,我们从 0.x 就开始使用,当初最看好的是它的管理方便,插件多,接口设计好等比较人性化的特性。
提问:线上集群如何进行不停机 reindex 的,这个过程在有数据不断索引的情况下如何保证原有集群数据同新集群数据一致性?
王卫华:Reindex 是一个高耗操作,所以一般情况下,最好不要提供服务,但是如果索引比较小,这个操作带来的压力一般。索引大,大量的碎片会带来很大的性能问题。所以我们一般对集群每天进行 optimize(force merge)。这样在高峰期可以提供较好的性能。
我们现在因为通过 4Sea 的配置,可以让任何比较清闲的集群承担当前 reindex 索引的查询。
提问:GC 选择 CMS,为何不选择 G1 呢?
王卫华:G1 的性能也很不错。官方目前支持 CMS,认为 G1 在 JDK8 还不算成熟。我们在试验中得出的结论 G1 对比稍差一点,并不落后多少。
不过,如果你设置 heap size 大于 30G,我建议你使用 G1。小于 30G,CMS 比较好。
提问:百姓网的 es 集群是从一开始就切成多个了吗?大体分为几个,为什么如此切分?代理服务器上路由实现是如何进行的?
王卫华:我们一开始也只有一个集群,但是我们对性能有追求,几百毫秒一个查询是不可接受的。随着数据越来越多,有些查询顶不住,需要分而治之。才能提供快速访问(毫秒级别)。
切分的原则,我们上面讲了,大致是:routing,时间,业务,是否提供全文索引。
代理服务器对查询进行分析,然后导引到合适的集群,比如 week,month,业务,并提供不同的routing。
提问:请问,32G 的物理内存,慢慢越来越少,是否正常?怎样做这方面优化?
王卫华:32G 的内存,JVM 会使用一部分内存。Lucene(系统缓存)会使用一部分。
越来越少是因为 Lucene 索引使用了内存,还有一些可能是其他文件缓存。
一般处理原则是 JVM + 索引大小 < 物理内存 即可。
提问:为何选 Golang 做 Proxy,不用 Java?
王卫华:主要看中 Golang 的 goroutine 和编码的简单舒适感,第三方工具包也足够多,使用过程中也没有 GC 性能问题(至少我们使用中没有这个问题)。
题外:我们从 Go 1.4 直接跳到 Go 1.6,解决一些坑(比如升级后锁变化问题),性能有很大的提升。
提问:怎么应对网络不稳定对集群的影响,特别是集群意外断电,导致 shard 的自动迁移,恢复时间长,从而导致集群不稳定,在 2.x 版本对 shard 的均衡分布和自动迁移有没有相关的更新?
王卫华:网络不稳定的情况下,解决办法就是提高 discovery.zen.ping.timeout 的时间,然而这样提供快速查询就比较伤。所以一个集群中保持一个稳定的网络环境还是很重要的。
要加快恢复时间而网络带宽允许的情况下,可以调整 cluster.routing.allocation 和 recovery 各项参数,增加并发,提高同时恢复的 node 数,提高传输速率。
2.x 对 allocation,recovery 进行了不少优化。
原文来自:http://h2ex.com/1303?utm_source=tuicool&utm_medium=referral