此文档只针对 ES 5.x 版本。
0 疑惑
1、当一个索引别名同时对应多个真实索引,此时使用索引别名读索引将会发生什么?
猜测:请求会被分发给所有索引的所有分片上,如果按条件过滤后读取到的文档 id 并不相同,则将符合条件的文档返回,如果文档 id 发生冲突,要么其中一个覆盖另一个,要么直接发生异常(或者 ES 还提供了其他可配置化方案?)。
1 什么时候需要重建索引?
对于已经定义好结构并且正在线上运行的索引来说,原生 ES 并不支持其字段属性的修改或者某个字段的删除,如果强制进行字段属性的修改或者某个字段的删除,将会破坏索引结构,造成索引不可用。
如果你有这样的需求,往往只有一个办法:重建索引。
2 重建索引
重建索引,顾名思义:重新建立一份与源索引结构相似并且满足新需求的目标索引。
重建索引往往面临这样一个问题:线上的源索引正在运行,如果要重建索引,如何实现零停机更换索引?也就是说,源索引与目标索引的切换过程是对用户透明的、平滑的,不会造成一段时间内业务不可用这种情况的发生。
这便是本文要讲的重点了,reIndex API 以及别名切换可以帮助你完美的完成索引间的平滑迁移。
2.1 建立新索引
重建索引的第一步便是建立与源索引相似的目标索引,此过程比较简单,不再赘述。
2.2 reIndex API
reIndex 是 ES 原生的 API,它不会设置目标索引,也不会复制源索引的设置信息,目标索引的分片数,副本等仍然需要你手动设置,它能够帮你做的只是快速的将一个索引中的数据导入另一个索引中。reIndex 的实现原理并不复杂,ES 会将 reIndex 置为一个后台任务,然后使用 scroll api 将源数据批量查询出来,最后重新用 bulk api 写入新索引中。
这里有两个问题需要注意:
1、 重建索引的目的如果是为了删除源索引中的某个字段(假设为字段 A),而字段 A 上还有数据,reIndex 目标索引并不会将字段删除成功。源索引中所有字段上的数据都会被 reIndex 到目标索引上,包括字段 A,即使目标索引中没有定义字段 A。
2、 目标索引 reIndex 的过程中需要停止源索引上的写流量,并尽量保证在业务低峰期执行。目的是为了保证在目标索引 reIndex 的过程中,源索引不会发生修改,保证数据一致性,否则不一致的数据将很难被发现并更正。如果没有办法停止源索引的写流量,那情况将更加复杂,此时也许需要考虑使用双写(博主目前还没有碰到如此复杂的场景,便不再赘述了)。当然,如果有办法在不停止写流量的情况下最终也能保证 reIndex 后源索引与目标索引的数据一致性,并且对业务无影响,那么是否停止源索引的写流量便无所谓了。
对于第一点中描述的问题通常并没有好的解决方法,目前我摸索到的也就两个方案:
1、 如果是删除字段的话,可以选择直接废弃这个字段,不用它就行了,也不用搞什么索引重建、别名迁移,这种方案方便、安全且快捷,只有一个缺点,不完美,对于强迫症患者来说很难受,索引中定义的字段却没有用,很不爽;
2、 如果你是强迫症患者,发誓必须要将源索引中无用的字段给删了,那么可以试试这种方案(未验证,但我觉得可行):既然是删除某个字段,那么在索引全量更新时理应不会再对这个字段进行读写操作,此时请使用你的新代码对源索引进行全量更新,这样会使源索引中所有文档上的这个字段值为空。最后使用你的目标索引(目标索引未定义需要被删除字段)进行 reIndex 操作,便可在数据迁移的同时完美删掉无用字段。
通过第二点的描述后大家可以发现,在索引设计之初就应该尽量避免给用户能将数据直接写到索引上的能力。随着业务的快速变化,索引重建往往是一个很常见的操作,如果索引设计之初留有缺陷,那将给以后的维护带来巨大的麻烦,这便得不偿失了。
一般来说,ES 提供的能力主要是查询,用户能够控制的数据写操作一般都面向数据库,而不会直接面向索引。此外,索引一般都能够通过数据库进行全量刷新(就是速度有点慢),因此在索引 reIndex 的过程中需要开双写的场景还是挺少的。
2.3 别名切换
应用索引别名可以实现零停机进行索引切换。在读写索引时,应尽可能使用索引别名。索引别名就像一个快捷方式或软连接,可以指向一个或多个索引。
ES 服务端在接受客户端请求时,首先会进行别名查找,如果没有匹配到相应别名,才会进行真实索引名的匹配。
在设置索引别名时有如下几点需要注意:
1、 设置的索引别名不能与集群中的任何一个真实索引名重名,否则会设置失败;
2、 一个索引别名可以对应多个索引,但并不建议这样做,原因是在写文档时如果一个别名同一时间映射了多个索引,默认是不能直接使用别名进行写文档的,因为 ES 不知道文档该发往哪个索引;
3、 网上某些博客说使用 Java API 操作 ES 中有别名的索引不能再指定类型名,否则会操作失败,我在实践后发现并未如上所述,使用索引别名同样需要设置类型名;
4、 有两种方式管理别名:_alias 用于单个操作,_aliases 用于执行多个原子级操作。当我们将别名指向目标索引时,通常还需要删除别名原来到旧索引的指向。这个改变需要是原子的,因此在源索引已有索引别名的情况下要进行别名切换只能用 _aliases。
关于上述第二点,可以使用 is_write_index 属性为一个别名下的其中一个索引指定为写索引,此时则可以直接使用别名进行写 api 的调用。
关于上述第四点有如下示例:
POST /_aliases
{
"actions": [
{ "remove": { "index": "my_index_v1", "alias": "my_index" }},
{ "add": { "index": "my_index_v2", "alias": "my_index" }}
]
}