refresh 和 flush 的区别

refresh:
分片默认每秒一次,是 doc 从 memory 到未被提交的轻量 segment(其实仍然在内存里) 的过程
此时这个轻量级内存 sgment 已经可以被搜索到了。

translog:
上面说的内存中的 segment 如果在断电或者程序崩溃之类的问题发生时,没办法保证数据持久化到磁盘。
所以 elasticsearch 增加了 translog 。translog 中记录了所有还没有被 flush 到磁盘的操作。

flush:
默认每30分钟 flush 一次或者当 translog 太大时触发 flush ,也可以手动调用 api 刷新。
flush 发生时

  1. memory buffer 写到 memory segment
  2. memory buffer 被清空
  3. lucene 执行一次 commit
  4. flush 通过系统的 fsync 进行刷新
  5. flush 之后,内存段被全量提交,translog 被清空

这也就解释了为什么我们在 rolling restart 时,最好停止外部的索引变动,手动执行一下 flush 配合 stop allocation. 通过手动减少 translog 中的操作,让 es 快速恢复。

https://stackoverflow.com/questions/19963406/refresh-vs-flush

https://www.elastic.co/guide/cn/elasticsearch/guide/current/dynamic-indices.html#img-memory-buffer

https://www.elastic.co/cn/blog/found-elasticsearch-from-the-bottom-up

or operation

以下内容基于 Elasticsearch 2.0.1 。

首先是 or filter 已经变成了 bool ,它是专门用来合并 queries ,支持 or、and、not 这类操作符的,大概相当于 ()(?).

当然还涉及到搜索结果评分等,但是这里不提这些。

现在遇到的情景大概就是想搜索多个空格分隔的关键字,任何一个 field 满足任一个关键字则匹配成功,返回所有匹配成功的结果和它们的 highlight 字段。

首先是对搜索内容做一点微不足道的处理:

const queries = 'some keywords'.trim().split(/\s+/).map((query) => query.lowerCase());
const queryString = queries.join(' OR ');

保持所有 query 包裹在 bool 中,把上面的两个变量用在 POST body 中,curl 同理:

{
"index": "index",
"from": 11,
"size": 20,
"_source": false,
"body": {
"query": {
"bool": {
"should": [
{
"terms": {
"tags": queries,
},
},
{
"query_string": {
"query": queryString,
"fields": ["name^5", "name.analyzed^5", "email", "phone", "content"],
"analyzer": "whitespace",
},
},
],
"minimum_should_match": 1,
}
},
"highlight": {
"fields": {
"content": { fragment_size: 18, number_of_fragments: 1 },
"name: {},
"name.analyzed": {},
"tags": {},
"email": {},
"phone": {}
}
}
}
}

note: minimum_should_match may not be available in some version of elasticsearch.

es 版本升级

Download

Migration

Run

从 2.0 -> 5.2 整体来说没有想象中那么困难,当然原本的环境就是单点大法…所以过程比较顺畅,有一些小坑,错误都比较明显。

Referrer: https://www.elastic.co/guide/en/elasticsearch/reference/6.3/setup-upgrade.html

es 从单机到集群

Background

Migration

先说结论,最终是没有实现 zero downtime。参考了几个不停机迁移方案以及现有的业务模型和数据规模,感觉过程实现成本和风险远远大于短暂的停机,所以选择了在凌晨重启服务加入了两个新的节点构成集群并在 data nodes 之间同步数据,修改配置和加入集群期间服务不可用大概一分钟多,新的 data node 同步百G级数据完成大概用了4个小时。这一分钟左右没有出现更新操作(凌晨原因),新增操作在后续通过脚本做了补充。

关于不停机迁移的方法也看了几个现成的方法诸如

https://blog.engineering.ticketbis.com/elasticsearch-cluster-migration-without-downtime/
https://thoughts.t37.net/migrating-a-130tb-cluster-from-elasticsearch-2-to-5-in-20-hours-with-0-downtime-and-a-rollback-39b4b4f29119

多半离不开版本号和更新时间这两个字段,迁移过程区分迁移前的数据和迁移过程中新增的数据,在迁移完毕时把新数据再同步一次,这就要求数据只有 create 没有 update 操作在迁移过程中发生,感觉不太适合除了自增类型的例如日志以外的其他业务逻辑。

关于集群,需要至少3个 master eligible node 以防止脑裂,并把其中两个设置为 data node 承载数据,在磁盘的扩充过程中给每个节点挂载数据盘,方便日后扩容。

以其中一个节点为例,几个比较重要的配置是

discovery.zen.ping.unicast.hosts: ["10.66.90.77:9300", "10.66.91.150:9300", "10.30.55.37:9300"]
network.host: ["10.30.55.37", _local_]

作为有限且小规模的集群,为 discovery module 指定广播发现节点的私有地址列表,同时允许各个节点在本机的访问调试.

Summary

总之如果不是追求绝对的不停机,并且不愿意增加额外的字段或改变自己的逻辑,es提供的加入集群方式来做迁移数据在我看来是相当简便又稳定的一种方式。

后续应该会在众多开源产品中选择一种 monitor 让集群和服务的状态更透明、报警和响应更及时。

script language

从 2.0.1 升级到 5.1.1 有一阵子了,今天发现了一个存在比较久的问题,就是诸如 update 包括 bulk update 操作,不能被正常的执行。问题集中在那些在 body 中使用 script 的 query,而直接全文更新的则没有问题。例如,对 list 类型的 document 进行局部更新:

POST index/type/id/_update
{
"script": "ctx._source.tags -= tag",
"params" : {
"tag" : "blue"
}
}

这样的使用在 2.0.1 中是没问题的,然而在 5.x 中却报错:Variable [tag] is not defined. 无法执行这个 script,后来发现了 default lang 不再是 groovy 而变成了 painless ,而 painless 的取值需要携带 key ,即 params.tag 这样才可以正常找到值。那么 groovy 为什么不再是 default 还被新版本中标记为 deprecated 了呢……要知道 groovy 之前替换掉 mvel 的理由是足够快而且简单……

先说一下什么是 sandboxed language

沙盒是在受限的安全环境中运行应用程序的一种做法,这种做法是要限制授予应用程序的代码访问权限。

像 groovy 和 JavaScript 这类脚本语言它们本身都不是 sandboxed,它们可以做很多系统级别的不止是读写、网络请求的操作,这样就给基于 JAVA 并且在运行中默认开启 The Security Manager 的 elasticsearch 带来很大的安全隐患,比如在脚本中随便加一句 infinite loop ,服务器可能就表现成拒绝访问的状态,所以在之前的版本中 elasticsearch 为 groovy 加入了沙盒控制一些权限,然而后面由于权限限制不够,还是出现了一些问题23333。

虽然 5.x 仍然内置 groovy ,但是考虑到 elasticsearch script 的来源,可以是 inline、store 、还有 file,前两个就不说了,一个是 query 中直接写进去的像我上面的例子,另一个也是以数据的形式存在某个 cluster state 的 _script 节点下,而 file 的形式是配置在 elasticsearch 的 config 文件夹中,所以从安全的角度,elasticsearch 5.x 只对 file script 默认允许执行 groovy 。

至于开头的 query ,如果不考虑安全性,例如默认我们的 elasticsearch 运行与一个相对隔离的环境下,如果还想用 inline groovy ,就可以为 groovy 单独开启一个配置 script.engine.groovy.inline: true 或者更宽泛的,针对所有 inline script 的配置script.inline: true,那么为上面的 script 声明一下 lang 就可以成功执行了(在 Python 和 Node.js 包中拼 dict 和 object 也是一样):

POST index/type/id/_update
{
"script" :{
"inline": "ctx._source.tags -= tag",
"lang": "groovy",
"params" : {
"tag" : "blue"
}
}
}

等 painless 相对稳定了,直接切换过去就可以了,毕竟语法都类似,而且还安全。

upsert element to exist document

一个已经存在的 document 可能有一个 tags 的 element ,它是一个 Array 形态,现在我们想 upsert 某个 tag 进去。

这种 array 的操作通常是用 script 操作的,于是很直观地用到文档中的 upsert:

{
"script": {
"inline": "ctx._source.tags += tag",
"lang": "groovy",
"params": {
"tag": "皮皮虾"
}
},
"upsert": {
"tags": ["皮皮虾"]
}
}

然而 script 总是执行而 upsert 不执行,原因是 document 已经存在了,这个 upsert 只是针对当 document 不存在时,所以还是要把逻辑做在 script 中:

{
"script": {
"inline": "if (ctx._source.tags) {ctx._source.tags += tag;} else {ctx._source.tags = [tag]}",
"lang": "groovy",
"params": {
"tag": "皮皮虾"
}
}
}

重启集群节点

(以下内容出现的部分流程和命令可以在 《ELASTICSEARCH 源码解析与优化实战》中找到详细的解释,部分内容依据我们的业务场景、部署方式、机器规格有额外的修改)

由于机器升级或某个节点出现了问题,需要执行重启的操作。但是直接重启会引发新插入更新的文档和重启节点的版本不匹配触发全局的 allocation 导致部分 indices 在 recovery 过程中长时间不可用。所以比较平滑的重启方法(rolling restart)如下:

  1. 检查集群的状态,不太建议在红色状态下重启,至少是黄绿状态最好 primary shards 都是可用的状态

  2. 停止切片分配

    curl -XPUT localhost:9200/_cluster/settings -d '{"transient":{"cluster.routing.allocation.enable": "none"}}'
  3. 执行 synced flush,解决副本分片恢复慢的问题

    curl -X POST "localhost:9200/_flush/synced"
  4. 重启节点

  5. 调整分片的限速,这一步考虑集群的配置,主要是内存和带宽

    curl -X PUT "localhost:9200/_cluster/settings?flat_settings=true" -H 'Content-Type: application/json' -d'{"transient" : {"indices.recovery.max_bytes_per_sec" : "300mb"}}'
    curl -X PUT "localhost:9200/_cluster/settings?flat_settings=true" -H 'Content-Type: application/json' -d'{"transient" : {"cluster.routing.allocation.node_concurrent_recoveries" : "100"}}'
  6. 开启切片分配

    curl -XPUT localhost:9200/_cluster/settings -d '{"transient":{"cluster.routing.allocation.enable": "all"}}'

等待切片分配完成,期间观察集群的可用状态。

集群水平扩容

扩容比较简单,如果集群是上云的,可以直接用加入新节点的方式平衡 data 的储存分配。

只是加入的时候要注意 除了 elasticsearch.yml 的配置要争取之外,jvm.options 的 heap size 也一定要符合机器的水平并且尽量与集群中其他同角色节点的配置完全相同,否则会出现 heap size 过小频繁触发 full GC 导致节点故障甚至切片分配问题。