时隔3年重新开源,这些 ElasticSearch 应用技能运维必会?

2024-11-08 14:11:37 Jinyu
图片

2021年1月15日,Elastic 创始人兼 CEO Shay Bannon 宣布改变 ElasticSearch 和 Kibana 的开源协议,由 Apache 2.0 变更为 SSPL 和 Elastic License。
SSPL 是一种针对服务器端软件的开源许可证,由 MongDB 公司创建,用户可以在自己的项目中使用 SSPL 的项目许可,但不能将其作为独立的商业产品出售。
时隔3年,Elasticsearch 与 Kibana 将再次回归开源,AGPL 将作为新的许可选项,与 ELv2 和 SSPL 并列,以此消除人们心中的疑虑与不安。
下面推荐一篇 ElasticSearch 的简介,深入理解功能和应用。

一、ES 的基本使用

在决定使用Elasticsearch的时候首先要考虑的是版本问题,Elasticsearch(排除 0.x 和 1.x)目前有如下常用的稳定的主版本:2.x,5.x,6.x,7.x(current)。
你可能会发现没有3.x和4.x,ES从2.4.6直接跳到了5.0.0。其实是为了 ELK(ElasticSearch,Logstash,Kibana)技术栈的版本统一,免的给用户带来混乱。
在 Elasticsearch是2.x(2.x的最后一版2.4.6的发布时间是July 25, 2017)的情况下,Kibana已经是4.x(Kibana 4.6.5 的发布时间是 July 25, 2017)。
那么在Kibana的下一主版本肯定是5.x了,所以Elasticsearch直接将自己的主版本发布为 5.0.0 了。
统一之后,我们选版本就不会犹豫困惑了,我们选定Elasticsearch的版本后再选择相同版本的Kibana就行了,不用担忧版本不兼容的问题。
Elasticsearch是使用Java构建,所以除了注意ELK技术的版本统一,我们在选择Elasticsearch的版本的时候还需要注意JDK的版本。
每个大版本所依赖的JDK版本也不同,目前7.2版本已经可以支持JDK11。

安装使用

下载和解压Elasticsearch,解压后即可用:

安装目录下运行 bin/elasticsearch 来启动 ES。
默认在 9200 端口运行,请求 curl http://localhost:9200/ 或者浏览器输入 http://localhost:9200,得到一个 JSON 对象,其中包含当前节点、集群、版本等信息。


















{"name" : "U7fp3O9","cluster_name" : "elasticsearch","cluster_uuid" : "-Rj8jGQvRIelGd9ckicUOA","version" : {"number" : "6.8.1","build_flavor" : "default","build_type" : "zip","build_hash" : "1fad4e1","build_date" : "2019-06-18T13:16:52.517138Z","build_snapshot" : false,"lucene_version" : "7.7.0","minimum_wire_compatibility_version" : "5.6.0","minimum_index_compatibility_version" : "5.0.0"},"tagline" : "You Know, for Search"}

集群健康状态

要检查群集运行状况目前有很多监控比如Grafana、Prometheus有集成好的模版、此外还有Cerebro进行集群管理,这里暂时看基本的信息,从Kibana控制台中运行以下命令GET/_cluster/health,得到如下信息:

















{"cluster_name" : "lzj","status" : "yellow","timed_out" : false,"number_of_nodes" : 1,"number_of_data_nodes" : 1,"active_primary_shards" : 9,"active_shards" : 9,"relocating_shards" : 0,"initializing_shards" : 0,"unassigned_shards" : 5,"delayed_unassigned_shards" : 0,"number_of_pending_tasks" : 0,"number_of_in_flight_fetch" : 0,"task_max_waiting_in_queue_millis" : 0,"active_shards_percent_as_number" : 64.28571428571429}

集群状态通过 绿,黄,红 来标识:

  • 绿色:集群健康完好,一切功能齐全正常,所有分片和副本都可以正常工作。
  • 黄色:预警状态,所有主分片功能正常,但至少有一个副本是不能正常工作的。此时集群是可以正常工作的,但是高可用性在某种程度上会受影响。
  • 红色:集群不可正常使用。某个或某些分片及其副本异常不可用,这时集群的查询操作还能执行,但是返回的结果会不准确。对于分配到这个分片的写入请求将会报错,最终会导致数据的丢失。当集群状态为红色时,它将会继续从可用的分片提供搜索请求服务,但是你需要尽快修复那些未分配的分片。

二、ES 机制原理

ES的基本概念和基本操作介绍完了之后,我们可能还有很多疑惑

  • 它们内部是如何运行的?

  • 主分片和副本分片是如何同步的?

  • 创建索引的流程是什么样的?

  • ES如何将索引数据分配到不同的分片上的?

    以及索引数据是如何存储的?

  • 为什么说ES是近实时搜索引擎而文档的CRUD(创建-读取-更新-删除) 操作是实时的?

  • 以及ES是怎样保证更新被持久化在断电时也不丢失数据?

  • 为什么删除文档不会立刻释放空间?

带着这些疑问我们进入接下来的内容。

写索引原理

下图描述了3个节点的集群,共拥有12个分片,其中有4个主分片(S0、S1、S2、S3和8个副本分片(R0、R1、R2、R3),每个主分片对应两个副本分片,节点1是主节点(Master节点)负责整个集群的状态。

图片

写索引是只能写在主分片上,然后同步到副本分片。这里有四个主分片,一条数据 ES 是根据什么规则写到特定分片上的呢?

首先这肯定不会是随机的,否则将来要获取文档的时候我们就不知道从何处寻找了。

实际上,这个过程和之前写的Hash分片的路由方法,实际根据下面这个公式决定的:

shard = hash(routing) % number_of_primary_shards

Routing是一个可变值,默认是文档的_id,也可以设置成一个自定义的值。Routing通过Hash函数生成一个数字,然后这个数字再除以number_of_primary_shards(主分片的数量)后得到余数。

这个在0到number_of_primary_shards-1之间的余数,就是我们所寻求的文档所在分片的位置。

这就解释了为什么我们要在创建索引的时候就确定好主分片的数量并且永远不会改变这个数量:因为如果数量变化了,那么所有之前路由的值都会无效,文档也再也找不到了。

由于在 ES 集群中每个节点通过上面的计算公式都知道集群中的文档的存放位置,所以每个节点都有处理读写请求的能力。

在一个写请求被发送到某个节点后,该节点即为前面说过的协调节点,协调节点会根据路由公式计算出需要写到哪个分片上,再将请求转发到该分片的主分片节点上。

图片

  1. 客户端向 ES1 节点(协调节点)发送写请求,通过路由计算公式得到值为 0,则当前数据应被写到主分片 S0 上。
  2. ES1 节点将请求转发到 S0 主分片所在的节点 ES3,ES3 接受请求并写入到磁盘。
  3. 并发将数据复制到两个副本分片 R0 上,其中通过乐观并发控制数据的冲突。
    一旦所有的副本分片都报告成功,则节点 ES3 将向协调节点报告成功,协调节点向客户端报告成功。

存储原理

上面介绍了在 ES 内部索引的写处理流程,这个流程是在ES的内存中执行的,数据被分配到特定的分片和副本上之后,最终是存储到磁盘上的,这样在断电的时候就不会丢失数据。
具体的存储路径可在配置文件 ../config/elasticsearch.yml中进行设置,默认存储在安装目录的 Data 文件夹下。建议不要使用默认值,因为若 ES 进行了升级,则有可能导致数据全部丢失:


path.data: /path/to/data//索引数据path.logs: /path/to/logs//日志记录

分段存储

索引文档以段的形式存储在磁盘上,何为段?索引文件被拆分为多个子文件,则每个子文件叫作段,每一个段本身都是一个倒排索引,并且段具有不变性,一旦索引的数据被写入硬盘,就不可再修改。

在底层采用了分段的存储模式,使它在读写时几乎完全避免了锁的出现,大大提升了读写性能。

段被写入到磁盘后会生成一个提交点,提交点是一个用来记录所有提交后段信息的文件。

一个段一旦拥有了提交点,就说明这个段只有读的权限,失去了写的权限。相反,当段在内存中时,就只有写的权限,而不具备读数据的权限,意味着不能被检索。

段的概念提出主要是因为:在早期全文检索中为整个文档集合建立了一个很大的倒排索引,并将其写入磁盘中。

如果索引有更新,就需要重新全量创建一个索引来替换原来的索引。这种方式在数据量很大时效率很低,并且由于创建一次索引的成本很高,所以对数据的更新不能过于频繁,也就不能保证时效性。

索引文件分段存储并且不可修改,那么新增、更新和删除如何处理呢?

  • 新增

很好处理,由于数据是新的,所以只需要对当前文档新增一个段就可以。

  • 删除

由于不可修改,所以对于删除操作,不会把文档从旧的段中移除而是通过新增一个 .del 文件,文件中会列出这些被删除文档的段信息。这个被标记删除的文档仍然可以被查询匹配到, 但它会在最终结果被返回前从结果集中移除。

  • 更新

不能修改旧的段来进行反映文档的更新,其实更新相当于是删除和新增这两个动作组成。会将旧的文档在.del文件中标记删除,然后文档的新版本被索引到一个新的段中。可能两个版本的文档都会被一个查询匹配到,但被删除的那个旧版本文档在结果集返回前就会被移除。

段被设定为不可修改具有一定的优势也有一定的缺点,优势主要表现在:不需要锁。如果你从来不更新索引,你就不需要担心多进程同时修改数据的问题。

一旦索引被读入内核的文件系统缓存,便会留在哪里,由于其不变性。只要文件系统缓存中还有足够的空间,那么大部分读请求会直接请求内存,而不会命中磁盘。这提供了很大的性能提升。

其它缓存(像Filter缓存),在索引的生命周期内始终有效。它们不需要在每次数据改变时被重建,因为数据不会变化。

写入单个大的倒排索引允许数据被压缩,减少磁盘 I/O 和需要被缓存到内存的索引的使用量。

段的不变性的缺点如下:

  • 当对旧数据进行删除时,旧数据不会马上被删除,而是在.del文件中被标记为删除。
    而旧数据只能等到段更新时才能被移除,这样会造成大量的空间浪费。
  • 若有一条数据频繁的更新,每次更新都是新增新的标记旧的,则会有大量的空间浪费。
  • 每次新增数据时都需要新增一个段来存储数据。
    当段的数量太多时,对服务器的资源例如文件句柄的消耗会非常大。
  • 在查询的结果中包含所有的结果集,需要排除被标记删除的旧数据,这增加了查询的负担。

延迟写策略

介绍完了存储的形式,那么索引写入到磁盘的过程是怎样的?是否是直接调 Fsync物理性地写入磁盘?

答案是显而易见的,如果是直接写入到磁盘上,磁盘的 I/O 消耗上会严重影响性能。那么当写数据量大的时候会造成 ES 停顿卡死,查询也无法做到快速响应。如果真是这样 ES 也就不会称之为近实时全文搜索引擎了。

为了提升写的性能,ES并没有每新增一条数据就增加一个段到磁盘上,而是采用延迟写的策略。每当有新增的数据时,就将其先写入到内存中,在内存和磁盘之间是文件系统缓存。

当达到默认的时间(1 秒钟)或者内存的数据达到一定量时,会触发一次刷新(Refresh),将内存中的数据生成到一个新的段上并缓存到文件缓存系统上,稍后再被刷新到磁盘中并生成提交点。

这里的内存使用的是ES的JVM内存,而文件缓存系统使用的是操作系统的内存。

新的数据会继续的被写入内存,但内存中的数据并不是以段的形式存储的,因此不能提供检索功能。

由内存刷新到文件缓存系统的时候会生成新的段,并将段打开以供搜索使用,而不需要等到被刷新到磁盘。

在Elasticsearch中,写入和打开一个新段的轻量的过程叫做 Refresh(即内存刷新到文件缓存系统)。

默认情况下每个分片会每秒自动刷新一次。这就是为什么我们说Elasticsearch 是近实时搜索,因为文档的变化并不是立即对搜索可见,但会在一秒之内变为可见。

我们也可以手动触发 Refresh:



POST/_refresh //刷新所有索引POST/nba/_refresh //刷新指定的索引

Tips:尽管刷新是比提交轻量很多的操作,它还是会有性能开销。当写测试的时候,手动刷新很有用,但是不要在生产>环境下每次索引一个文档都去手动刷新。而且并不是所有的情况都需要每秒刷新。

可能你正在使用Elasticsearch索引大量的日志文件,可能想优化索引速度而不是>近实时搜索。
这时可以在创建索引时在Settings中通过调大如下参数refresh_interval = "30s"的值 , 降低每个索引的刷新频率,设值时需要注意后面带上时间单位,否则默认是毫秒。当refresh_interval=-1 时表示关闭索引的自动刷新。
虽然通过延时写的策略可以减少数据往磁盘上写的次数提升了整体的写入能力,但是我们知道文件缓存系统也是内存空间,属于操作系统的内存,只要是内存都存在断电或异常情况下丢失数据的危险。
为了避免丢失数据,Elasticsearch添加了事务日志(Translog),事务日志记录了所有还没有持久化到磁盘的数据。

图片

索引写流程

添加了事务日志后整个写索引的流程如上图所示:一个新文档被索引之后,先被写入到内存中,但是为了防止数据的丢失,会追加一份数据到事务日志中。
不断有新的文档被写入到内存,同时也都会记录到事务日志中。这时新数据还不能被检索和查询。
当达到默认的刷新时间或内存中的数据达到一定量后,会触发一次  Refresh,将内存中的数据以一个新段形式刷新到文件缓存系统中并清空内存。这时虽然新段未被提交到磁盘,但是可以提供文档的检索功能且不能被修改。
随着新文档索引不断被写入,当日志数据大小超过 512M 或者时间超过 30 分钟时,会触发一次Flush。
内存中的数据被写入到一个新段同时被写入到文件缓存系统,文件系统缓存中数据通过Fsync刷新到磁盘中,生成提交点,日志文件被删除,创建一个空的新日志。
通过这种方式当断电或需要重启时,ES不仅要根据提交点去加载已经持久化过的段,还需要工具Translog里的记录,把未持久化的数据重新持久化到磁盘上,避免了数据丢失的可能。3. 段合并 由于自动刷新流程每秒会创建一个新的段 ,这样会导致短时间内的段数量暴增。而段数目太多会带来较大的麻烦。
每一个段都会消耗文件句柄、内存和 CPU 运行周期。更重要的是,每个搜索请求都必须轮流检查每个段然后合并查询结果,所以段越多,搜索也就越慢。
Elasticsearch通过在后台定期进行段合并来解决这个问题。小的段被合并到大的段,然后这些大的段再被合并到更大的段。
段合并的时候会将那些旧的已删除文档从文件系统中清除。被删除的文档不会被拷贝到新的大段中。合并的过程中不会中断索引和搜索。

图片

segment合并

段合并在进行索引和搜索时会自动进行,合并进程选择一小部分大小相似的段,并且在后台将它们合并到更大的段中,这些段既可以是未提交的也可以是已提交的。
合并结束后老的段会被删除,新的段被 Flush 到磁盘,同时写入一个包含新段且排除旧的和较小的段的新提交点,新的段被打开可以用来搜索。
段合并的计算量庞大, 而且还要吃掉大量磁盘 I/O,段合并会拖累写入速率,如果任其发展会影响搜索性能。
Elasticsearch在默认情况下会对合并流程进行资源限制,所以搜索仍然有足够的资源很好地执行。

六、ES 的性能优化

存储设备

磁盘在现代服务器上通常都是瓶颈。Elasticsearch重度使用磁盘,你的磁盘能处理的吞吐量越大,你的节点就越稳定。这里有一些优化磁盘 I/O 的技巧:

  • 使用SSD。就像其他地方提过的,他们比机械磁盘优秀多了。

  • 使用RAID10/RAID5。条带化RAID会提高磁盘I/O,同时增加数据可靠性

  • 使用多块硬盘,并允许Elasticsearch通过多个 path.data目录配置把数据条带化分配到它们上面。

  • 不要使用远程挂载的存储,比如NFS或者SMB/CIFS。这个引入的延迟对性能来说完全是背道而驰的。

  • 如果你用的是 EC2,当心EBS。即便是基于SSD的EBS,通常也比本地实例的存储要慢。

内部索引优化

图片

索引优化

Elasticsearch为了能快速找到某个Term,先将所有的Term排个序,然后根据二分法查找Term,时间复杂度为logN,就像通过字典查找一样,这就是Term Dictionary。
现在再看起来,似乎和传统数据库通过B-Tree的方式类似。但是如果Term 太多,Term Dictionary 也会很大,放内存不现实,于是有了 Term Index。
就像字典里的索引页一样,A开头的有哪些Term,分别在哪页,可以理解 Term Index是一棵树。
这棵树不会包含所有的Term,它包含的是Term的一些前缀。通过Term Index可以快速地定位到Term Dictionary的某个Offset,然后从这个位置再往后顺序查找。
在内存中用FST方式压缩Term Index,FST以字节的方式存储所有的Term,这种压缩方式可以有效的缩减存储空间,使得Term Index足以放进内存,但这种方式也会导致查找时需要更多的CPU 资源。
对于存储在磁盘上的倒排表同样也采用了压缩技术减少存储所占用的空间。

调整配置参数

调整配置参数建议如下:

  • 给每个文档指定有序的具有压缩良好的序列模式ID,避免随机的UUID-4 这样的ID,这样的ID压缩比很低,会明显拖慢Lucene。

  • 对于那些不需要聚合和排序的索引字段禁用Doc values。

    Doc Values 是有序的、基于document=>field value的映射列表。

  • 不需要做模糊检索的字段使用Keyword类型代替Text类型,这样可以避免在建立索引前对这些文本进行分词。

  • 如果你的搜索结果不需要近实时的准确度,考虑把每个索引的index.refresh_interval改到30s。

  • 如果你是在做大批量导入,导入期间你可以通过设置这个值为-1 关掉刷新,还可以通过设置 index.number_of_replicas: 0 关闭副本。别忘记在完工的时候重新开启它。

  • 避免深度分页查询建议使用Scroll进行分页查询。普通分页查询时,会创建一个from+size的空优先队列,每个分片会返回from+size条数据,默认只包含文档ID和得分Score给协调节点。

  • 如果有N个分片,则协调节点再对(from+size)×n条数据进行二次排序,然后选择需要被取回的文档。当from很大时,排序过程会变得很沉重,占用CPU资源严重。减少映射字段,只提供需要检索,聚合或排序的字段。其他字段可存在其他存储设备上,例如HBase,在ES中得到结果后再去HBase 查询这些字段。

  • 创建索引和查询时指定路由Routing值,这样可以精确到具体的分片查询,提升查询效率。路由的选择需要注意数据的分布均衡。

JVM 调优

JVM 调优建议如下:
确保堆内存最小值( Xms )与最大值( Xmx )的大小是相同的,防止程序在运行时改变堆内存大小。
ES默认安装后设置的堆内存是 1GB。可通过 ../config/jvm.option文件进行配置,但是最好不要超过物理内存的50%和超过32GB。
GC 默认采用 CMS 的方式,并发但是有 STW 的问题,可以考虑使用 G1 收集器。
ES 非常依赖文件系统缓存(Filesystem Cache),快速搜索。一般来说,应该至少确保物理上有一半的可用内存分配到文件系统缓存。


我要咨询