目录

如何平滑切换线上的ElasticSearch索引

前言

哈喽,大家好,我是asong,今天与大家聊一聊如何平滑切换线上的ES索引。使用过ES的朋友们都知道,修改索引真的是一件费时又费力的工作,所以我们应该在创建索引的时候就尽量设计好索引能够满足需求,当然这几乎是不可能的,毕竟存在着万恶的产品经理,所以掌握"平滑切换线上的ES索引"就很必要,接下来我们就来看一看如何实现!

前置条件

能够平滑切换线上的ES索引需要有两个先决条件,只有满足了这两个条件才能去执行接下来的平滑切换操作,否则一切操作都是白费。

前置条件之使用别名访问索引

重建索引的问题是必须更新应用中的索引名称,索引别名就是用来解决这个问题的。索引别名就像一个快捷方式或软连接,可以指向一个或多个索引,也可以给任何一个需要索引名的API来使用。别名 带给我们极大的灵活性,允许我们做下面这些:

  • 在运行的集群中可以无缝的从一个索引切换到另一个索引
  • 给多个索引分组
  • 给索引的一个子集创建 视图

索引与索引别名的关系,我们画个图来说一下:

https://song-oss.oss-cn-beijing.aliyuncs.com/golang_dream/article/static/%E6%88%AA%E5%B1%8F2021-04-02%20%E4%B8%8B%E5%8D%889.12.01.png

上图中user_index就是索引别名,user_index_v1user_index_v2user_index_v3分别是三个索引,这里索引别名user_indexuser_index_v1进行了关联,所以我们搜索的时候使用索引别名,也就是去索引user_index_v1上查询。假设现在我们不想使用索引user_index_v1了,想使用索引user_index_v2,那么直接使用_aliases操作执行原子操作(后面介绍具体使用),将索引别名user_index与索引user_index_v2进行关联,现在使用索引别名user_index搜索的就是索引user_index_v2的数据了。

前置条件之足够空间

既然要重建ES索引,就一定保证你有足够的空间存储数据,可以使用如下指令查看ES每个节点的可用磁盘空间:

1
curl http://localhost:9200/_cat/allocation\?v

获得结果如下:

https://song-oss.oss-cn-beijing.aliyuncs.com/golang_dream/article/static/%E6%88%AA%E5%B1%8F2021-04-02%20%E4%B8%8B%E5%8D%889.32.20.png

如何平滑切换

因为大家使用的ES场景不同,所以平滑切换的步骤会稍有偏差,但是都离不开这几个步骤:

  1. 创建新索引
  2. 同步数据/数据迁移到新索引
  3. 切换索引

先介绍一下数据迁移和切换索引使用什么指令操作:

  • 数据迁移

使用ES中提供的reindex api就可以将数据copy到新索引中,比如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
curl --location --request POST 'http://localhost:9200/_reindex' \
--header 'Content-Type: application/json' \
--data-raw '{
  "conflicts": "proceed",
  "source": {
    "index": "user_index_v1"
  },
  "dest": {
    "index": "user_index_v2",
    "op_type": "create",
    "version_type": "external"
  }
}'

介绍一下上面几个字段的意义:

  • "source":{"index": "user_index_v1"}:这里代表我们要迁移数据的源索引;
  • "dest":{"index": "user_index_v2"}:这里代表我们要迁移的目标索引;
  • "conflicts": "proceed":默认情况下,版本冲突会导致_reindex操作终止,可以设置这个字段使该请求遇到冲突时不会终止,而是统计冲突数量;
  • "version_type": "extrenal":这个字段介绍起来比较复杂,且听我细细道来。_reindex指令会生成源索引的快照,它的目标索引必须是一个不同的索引[新索引],以便避免版本冲突。如果不设置version_type字段,默认为internalES会直接将文档转存储到目标索引中(dest index),直接覆盖任何具有相同类型和iddocument,不会产生版本冲突。如果把version_type设置为extertral,那么ES会从源索引(source index)中读取version字段,当遇到具有相同类型和iddocument时,只会保留new version,即最新的version对应的数据。此时可能会有冲突产生,比如当把op_tpye设置为create,对于产生的冲突现象,返回体中的 failures 会携带冲突的数据信息【类似详细的日志可以查看】。
  • op_typeop_type 参数控制着写入数据的冲突处理方式,如果把 op_type 设置为 create【默认值】,在 _reindex API 中,表示写入时只在 dest index 中添加不存在的 doucment,如果相同的 document 已经存在,则会报 version confilct 的错误,那么索引操作就会失败。【这种方式与使用 _create API 时效果一致】。

更多_redinx api使用方法可以移步官方文档学习:https://www.elastic.co/guide/en/elasticsearch/reference/5.6/docs-reindex.html

上面只是举一个简单的例子,具体要在数据迁移中使用哪些参数需要根据场景而定。

什么时候可以选择数据迁移:

  1. 当我们新创建的索引只改变了mapping结构时,例如:删除字段,更新字段的类型,这种场景就可以直接使用_reindex进行数据迁移;
  2. 新创建的索引中添加了新字段,但是新的字段都是由老的字段计算得到的,这种情况,也可以使用_reindex进行数据迁移,api中使用script参数,编写你的脚本即可。

注意:使用_redindex接口时要注意一个问题,接口会在reindex结束后返回,接口超时控制只有30s,如果reindex时间过长,建议加上wait_for_completion=false参数,这样redindex就变成异步任务,返回的是taskID,查看进度可以通过 _tasks API 进行查看。

  • 切换索引

ES中两种方式管理别名:_alias用于单个操作,_aliases用于执行多个原子级操作。

因为我们这里要做的是切换索引,主要分为两个步骤:

  1. 移除当前索引与索引别名的关联
  2. 将新建的索引与索引别名进行关联

所以我们可以选择_alisases执行原子操作:

1
2
3
4
5
6
7
8
curl --location --request POST 'http://localhost:9200/_aliases' \
--header 'Content-Type: application/json' \
--data-raw '{
    "actions": [
        {"remove": {"index": "user_index_v2", "alias": "user_index"}},
        { "add": {"index": "user_index_v1",  "alias": "user_index"}}
    ]
}'

举例子

假设我们有一个user_index_v1,他的mapping结构如下;

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
{
    "mappings":{
        "properties":{
            "id":{
                "type":"byte"
            },
            "Name":{
                "type":"text"
            },
            "Age":{
                "type":"byte"
            }
        }
    }
}

现在这个v1索引中,我们的id字段使用的byte类型,显然范围是比较小的,随着数据量增多,id数值的增大,该字段已经不能满足存储需求了,所以需要把它换成long类型,因此可以创建v2索引:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
{
    "mappings":{
        "properties":{
            "id":{
                "type":"long"
            },
            "Name":{
                "type":"text"
            },
            "Age":{
                "type":"byte"
            }
        }
    }
}

现在我们就来考虑一下,如何平滑的进行索引切换。这里假设我们ES中数据同步采用的消息队列推送完成的,所以在切换索引时要考虑数据损失的问题。

这里我们可以列举几种方案如下:

  • 方案一:直接创建v2索引,使用_aliases切换索引,进行数据迁移,优点是直接切换别名和索引的关联,简单方便,缺点是出现问题回退到旧索引,会有数据损失,直接切换到v2索引会导致服务在数据没有迁移完之前不可用。
  • 方案二:创建v2索引,添加v2索引与别名的关联,进行数据迁移,_alias操作解除别名和v2索引的关联。优点是不会造成服务不可用,缺点是在解除别名和v1关联之前,一个别名关联两个索引,单索引操作无法执行,只能搜索,搜索也会出现数据重复,并且也会造成数据损失。
  • 方案三:创建v2索引,添加v2索引与别名的关联,修改代码写入操作使用v2索引,搜索操作使用别名索引,进行数据迁移,解除v1索引与别名的关联,优点是搜索和写入操作分开了,缺点是回退需要修改代码,并且会出现数据损失,如果v2索引不可用了,不能立刻回退索引。
  • 方案四:创建v2索引,进行数据迁移,然后切换索引;优点是同步数据到v2期间搜索功能正常使用,回退无数据损失;缺点是会造成数据丢失。
  • 方案五:创建v2索引,添加两个别名索引readwrite,添加别名readv1索引、v2索引的关联,添加别名writev2索引的关联,进行数据迁移,解除别名readv1索引的关联;优点是搜索和写入分开了,更新索引时只需要创建新索引,数据同步完成后,解除别名read和旧索引关联即可;缺点是数据迁移完成之前,搜索结果会出现重复,回退到旧索引,会有数据损失。

这里总共列举了5种方案,我也不推荐具体使用那个方案比较好,各有利弊,大家可以根据自己的业务场景来进行选择。

这里以选择方案四为例子,给出我的脚本数据,作为样例;

  • 创建user_index_v2索引:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#!/bin/bash

url=$1
index=$2


echo `curl --location --request GET ${url}/${index}`

echo `curl --location --request PUT ${url}/${index} \
--header 'Content-Type: application/json' \
--data-raw '{
    "mappings":{
        "properties":{
            "id":{
                "type":"long"
            },
            "Name":{
                "type":"text"
            },
            "Age":{
                "type":"byte"
            }
        }
    }
}'`

echo `curl --location --request GET ${url}/${index}`

运行指令:./create_index.sh http://localhost:9200 user_index_v2

  • 进行数据迁移(数据量比较大时建议分批and异步处理)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#!/bin/bash

url=$1



echo `curl --location --request POST ${url}/'_reindex?wait_for_completion=false' \
--header 'Content-Type: application/json' \
--data-raw '{
  "conflicts": "proceed",
  "source": {
    "index": "user_index_v1"
  },
  "dest": {
    "index": "user_index_v2",
    "op_type": "create",
    "version_type": "external"
  }
}'`

运行指令:./reindex.sh http://localhost:9200

  • 切换索引
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#!/bin/bash

url=$1
aliasIndex=$2
oldIndex=$3
newIndex=$4

echo `curl --location --request GET ${url}/${aliasIndex}`

echo `curl --location --request POST ${url}/_aliases --header 'Content-Type: application/json' --data-raw '{"actions": [{"remove": {"index": "'$oldIndex'", "alias": "'$aliasIndex'"}},{ "add": {"index": "'$newIndex'",  "alias": "'$aliasIndex'"}}]}'`

echo `curl --location --request GET ${url}/${aliasIndex}`

运行指令:./aliases.sh http://localhost:9200 user_index user_index_v1 user_index_v2

总结

本文例举了几种平滑切换ES索引的方案,可以看出修改索引真不是一件容易的事情,要考虑的事情比较多,所以最好在第一次创建索引的时候就多考虑一下以后的使用场景,确定好字段和类型,这样就可以避免重建ES索引。当然随着产品的需求变更,重建ES索引也是不可避免的,上面几种仅供大家参考,根据自己的场景去选择就好啦。

好啦,这篇文章就到这里啦,素质三连(分享、点赞、在看)都是笔者持续创作更多优质内容的动力!

创建了一个Golang学习交流群,欢迎各位大佬们踊跃入群,我们一起学习交流。入群方式:加我vx拉你入群,或者公众号获取入群二维码

结尾给大家发一个小福利吧,最近我在看[微服务架构设计模式]这一本书,讲的很好,自己也收集了一本PDF,有需要的小伙可以到自行下载。获取方式:关注公众号:[Golang梦工厂],后台回复:[微服务],即可获取。

我翻译了一份GIN中文文档,会定期进行维护,有需要的小伙伴后台回复[gin]即可下载。

翻译了一份Machinery中文文档,会定期进行维护,有需要的小伙伴们后台回复[machinery]即可获取。

**我是asong,一名普普通通的程序猿,让我们一起慢慢变强吧。欢迎各位的关注,我们下期见~~~**

https://song-oss.oss-cn-beijing.aliyuncs.com/golang_dream/article/static/%E6%89%AB%E7%A0%81_%E6%90%9C%E7%B4%A2%E8%81%94%E5%90%88%E4%BC%A0%E6%92%AD%E6%A0%B7%E5%BC%8F-%E7%99%BD%E8%89%B2%E7%89%88.png

推荐往期文章: