记一次Elasticsearch的升级
笔者所在公司前段时间对线上Elasticsearch进行了一次比较大的升级,版本由1.6.1升级到了5.5.0,由于版本跨度较大,在升级的过程中踩了不少坑,所以将整个过程记录下来,希望能够给需要的同学提供一点帮助。整个升级过程大概分为了下面几步:
- 安装新的ES集群
- 对Mapping和程序代码进行改造
- 迁移数据并进行双写
接下来就逐条说明。
安装并配置Elasticsearch集群
ES的安装很简单,在每台服务器上执行下面的命令:
wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-5.5.0.tar.gz
# 解压
tar zxvf elasticsearch-5.5.0.tar.gz
# 将解压后的目录移到/opt下
mv ./elasticsearch-5.5.0 /opt
下载并解压5.5.0的包,然后直接运行bin目录下的elasticsearch命令即可正常启用ES。如果我们不做任何配置,这样运行起来的ES其实是处于测试环境的,我们这里需要在正式环境使用ES,因此还需要做一些配置。其中比较重要的配置有如下几个:
1、 cluster.name
指定集群的名称,如果某个集群想添加新的节点,那么这个节点所配置的集群名称必须和集群中其他节点的名称保持一致。
2、 node.name
节点名称,方便我们在管理节点的时候区分当前是哪个节点,所以只要取一个有意义的名字即可,比如可以利用当前机器的IP来命名:node-159,159是这台机器的IP。
3、 path.data
和 path.logs
这两个参数默认情况下指向的是/{ES_HOME}/data
和/{ES_HOME}/logs
目录,但是不排除以后ES升级了将原目录删除的可能,如果数据文件和日志文件都在默认路径下就有被误删的风险。因此,这里建议将两者单独存放,比如,数据放在/data/elasticsearch
目录下,日志可以放在/var/log/elasticsearch
目录下。
4、 bootstrap.memory_lock
这个配置在正式环境中非常重要,通过将其设为true
,可以对jvm的内存进行锁定,从而避免了物理内存中数据的换入/换出,而且在正式环境里,ES启动的时候也会做一个检查,如果发现不能锁定内存,将导致程序启动失败。
5、 network.host
默认情况下这个值是127.0.0.1,这就意味着我们只能通过本机来请求ES的rest api,并且,如果你希望搭建集群,那么也只能发现本机的其他节点。 因此,在正式环境,需要将其指定为本机的IP。
6、 http.port
默认是9200,一般也不需要改。
7、 discovery.zen.ping.unicast.hosts
Zen 发现机制是ElasticSearch中默认的用来发现新节点的功能模块,在1.6的版本中,默认采用多播的模式来寻找其他节点,但是在比较大的集群里,这种多播的模式会产生很多不必要的流量开销,因此,2.0之后的版本只能使用单播模式。单播模式是这样的,新加入的节点会发送一个ping请求到事先设置好的地址中,来通知集群它已经准备好加入到集群中了,这个事先设置好的地址就是这个参数指定的地址。
8、 discovery.zen.minimum_master_nodes
这个参数主要的作用就是避免脑裂
现象的发生,那什么是脑裂
现象呢?由于某些节点的失效,部分节点的网络连接会断开,并形成一个与原集群一样名字的集群,这种情况就称为脑裂(split-brain)现象。脑裂现象最大的问题在于,新形成的两个集群会同时索引和修改集群的数据。而这个参数的含义是说:需要多少个节点才能选举一个Master节点,默认值是1。根据经验这个值一般设置成 N/2 + 1,N是集群中节点的数量,例如一个有3个节点的集群,minimum_master_nodes
应该被设置成 3/2 + 1 = 2(向下取整)。由于节点数不满足选举新的mater的条件,因此也就一定程度避免了脑裂现象。
除了上面的这几条配置,我们比较关心的还有就是Jvm的一些参数,这里主要是对内存的大小做一个优化。默认情况下,ES指定了2G的内存空间,由于现在服务器的配置一般都比较高(比如笔者使用的机器是32G的内存),因此,这里可以多分配一些内存给es。当然,这个内存也不是越大越好,单个es实例内存分配不要超过32G,一般的建议是一半的内存给ES另一半则留给给Lucene使用。在配置的时候要注意的是,-Xms
和 -Xmx
一定要一致,像下面这样:
-Xms10g
-Xmx10g
这些工作都做完后,ES就适用于线上环境了。为了方便管理,接下来是安装一些配套的工具。
安装elasticsearch-head和Kibana
虽然我们就可以通过一系列的rest api来访问ES中的数据、查看节点状态等,但是每次都要构造rest请求还是有点麻烦,elasticsearch-head便是解决这个问题的一个很好的工具。elasticsearch-head在Github的地址如下:https://github.com/mobz/elasticsearch-head,安装也不复杂,由于它是一个前端的工具,因此需要我们预先安装了node和npm,之后执行下面的步骤:
git clone git://github.com/mobz/elasticsearch-head.git
cd elasticsearch-head
npm install
安装完成后,运行命令npm run start
就可以了。但是这个时候还不能通过http://{your ip}:9100/
来访问ES,原因就在于ES默认不支持跨域,为了开启这个功能,在ES的配置文件中添加下面两行:
http.cors.enabled: true
http.cors.allow-origin: "*"
重启ES,再次访问http://{your ip}:9100/
就好了。
除了elasticsearch-head,另一个需要安装的应用是Kibana,Kibana可以对数据可视化,但这里主要是配合x-pack
对ES进行保护并监控其运行状况。
Kibana的安装跟ES比较类似,也是下载tar包,然后解压即可:
wget https://artifacts.elastic.co/downloads/kibana/kibana-5.5.2-linux-x86_64.tar.gz
sha1sum kibana-5.5.0-linux-x86_64.tar.gz
tar -xzf kibana-5.5.0-linux-x86_64.tar.gz
# 将解压后的目录移到/opt下
mv ./kibana-5.5.0 /opt
解压完毕后,对Kibana做一些简单的配置,这里就不做过多介绍了,需要的同学可以去查看下官方文档,也不是很复杂。最后在Kibana上安装x-pack这个插件,进到kibana的目录,执行下面的语句:
bin/kibana-plugin install x-pack
为了收集各个节点的运行信息,还需要在每个节点的ES上安装x-pack插件,命令跟上面的有点类似:
cd /{ES_HOME}
bin/elasticsearch-plugin install x-pack
安装完毕后重启ES,这个时候就可以实时查看每台机器的运行情况了,例如下面这图:
Mapping及查询语句的改造
ES升级后,最直接的影响是之前的一些Mapping和查询语句需要做出一定的调整。根据业务需要,我目前总结出了下面的几条:
1、文档中不能包含_id
字段,这个字段是ES的元数据字段,可以使用id
来代替,否则在写入操作的时候会报下面的错误:
"cause": {
"type": "mapper_parsing_exception",
"reason": "Field [_id] is a metadata field and cannot be added inside a document. Use the index API request parameters."
}
2、 在1.6.1版本中,我们可以使用string
来表示一个字符串,但是在5.5中已经废弃,需要改成keyword
或text
,这两者的区别就是:如果需要分词处理就使用text
,否则就用keyword
。
3、我们使用的分词插件是ik
,这个插件在5.0中移除了名为 ik
的analyzer和tokenizer,应当分别使用 ik_smart
和 ik_max_word
,它们的区别是ik_max_word
会将文本做最细粒度的拆分而ik_smart
会做最粗粒度的拆分 ,因此这里推荐的配置是:
// 在索引文档的时候使用更细的粒度
"analyzer": "ik_max_word",
// 查询的时候用户一般希望按照输入的完整单词查询,不希望再拆分,所以粒度要大一些
"search_analyzer": "ik_smart",
4、在5.5.0中,nested
类型的参数只保留了dynamic
、include_in_all
、properties
,在1.6.1中使用的include_in_parent
需要删除。
5、在1.6.1中,会这样定义一个字段:
{
"some_field": {
"type": "string",
"index": "not_analyzed"
}
}
但是在5.5.0中,index这个选项只接受true or false,默认为true意味着该字段可以被搜索,反之则不可搜索。
除了Mapping的差异外,在查询语句上同样存在一些区别:
1、在5.5.0中missing查询已经移除,需要改为
{
"query": {
"bool": {
"must_not": {
"exists": {
"field": "some_field"
}
}
}
}
}
2、在1.6.1中,nested查询可以不使用完整的path,例如:
{
"query": {
"nested": {
"path": "stages",
"query": {
"bool": {
"must": [
{
"term": {
"stage": "BASE_SINGLE_INPUT"
}
}
]
}
}
}
}
}
但是在5.x中,nested中虽然指定了path,但是在条件中也要使用完整的路径名:
{
"query": {
"nested": {
"path": "stages",
"query": {
"bool": {
"must": [
{
"term": {
"stages.stage": "BASE_SINGLE_INPUT"
}
}
]
}
}
}
}
}
3、在5.5.0中,废除了filtered查询,需要换成bool/must/filter,否则会报如下错误
{
"error": {
"root_cause": [{
"type": "parsing_exception",
"reason": "no [query] registered for [filtered]",
"line": 1,
"col": 55
}],
"type": "parsing_exception",
"reason": "no [query] registered for [filtered]",
"line": 1,
"col": 55
},
"status": 400
}
目前,我发现的主要的一些差异就是上面这些,如果后期发现了新的也会继续补充。
数据迁移
在一切都准备好之后,最后一步就是要迁移数据,官方给出的方案是reindex,但是笔者这里遇到的问题是,我们的一个index同时被其他业务的同学在读操作,而我们两方又无法保证同时进行升级,因此,除了reindex外,我们还采用了双写的方案来进行兼容。
第一步,在5.5.0中创建对应的的index以及mapping,然后将 refresh_interval
置为-1,number_of_replicas
置为0 来加快索引的速度:
curl -XPUT {some ip}:9200/{index}/_settings -d '{
"index": {
"refresh_interval": -1,
"number_of_replicas": 0
}
}'
之后查看索引信息如下:
第二步,通过reindex-from-remote来同步数据
curl -XPOST http://10.0.1.158:9200/_reindex?pretty&wait_for_completion=false -d '{
"conflicts": "proceed",
"source": {
"remote":{
"host":"http://10.0.1.17:9200"
},
"index":"task_es",
"size":1000
},
"dest": {
"index": "task_es_v1",
"version_type": "external"
}
}'
注意到这里使用了参数wait_for_completion=false
,上面的调用会返回一个taskid,可以通过这个id查看同步进度
第三步,同步完成后将 refresh_interval
、number_of_replicas
分别设置回"30s"和1。
遇到的问题及解决方案
在整个过程中,遇到了不少的问题,本人觉得值得记录的问题有下面两个:
第一个是reindex的时候报错
这个问题在于:Lucene中一个term的字节长度限制为32766,如果超过这个大小就会报上面的错误。在ES中的Keyword类型有个参数是ignore_above
,默认值是2147483647,这个参数的含义是:如果录入的长度超过其指定的值将不会被索引,我们只需要将这个值缩小至Lucene能接受的范围就可以正常索引,而ignore_above
的单位是character,Lucene是字节,对于UTF8,这里给定32766 / 3 = 10922即可。
第二个问题是对nested中的字段作用exists
查询。对于非nested字段,比如有一个user
字段,我们执行下面的查询:
GET /_search
{
"query": {
"exists" : { "field" : "user" }
}
}
这个查询会匹配到下面这些文档:
{ "user": "jane" }
{ "user": "" }
{ "user": "-" }
{ "user": ["jane"] }
{ "user": ["jane", null ] }
而另一些不会被匹配到:
{ "user": null }
{ "user": [] }
{ "user": [null] }
{ "foo": "bar" }
反过来,如果想查询不存在这个字段的文档可以使用must_not
:
GET /_search
{
"query": {
"bool": {
"must_not": {
"exists": {
"field": "user"
}
}
}
}
}
假如现在user
是嵌套在address
下的字段,我们第一感觉是将上面的查询改成下面这样:
GET /_search
{
"query": {
"nested": {
"path": "address",
"query": {
"bool": {
"must_not": {
"exists": {
"field": "address.user"
}
}
}
}
}
}
}
遗憾的是,如果我们执行这个语句会发现它返回的是空,但是我们发现如果将上面的must_not
改成must
是可以正常返回的,怎么反过来就没用了呢?正确的方法是将must_not
放在最外层:
GET /_search
{
"query": {
"bool": {
"must_not": {
"nested": {
"path": "address",
"query": {
"bool": {
"must": [
{
"exists": {
"field": "address.user"
}
}
]
}
}
}
}
}
}
}
上一篇: 分布式追踪系统概述及主流开源系统对比
下一篇: 记一次动画使用总结