当前位置: 首页 > 科技观察

请不要再问我关于Elasticsearch的问题了!

时间:2023-03-19 22:51:49 科技观察

如今,越来越多的企业在业务场景中使用Elasticsearch(以下简称ES)存储非结构化数据。图片来自Pexels。例如电商业务实现站内商品搜索、数据索引分析、日志分析等。ES作为传统关系型数据库的补充,提供了一些关系型数据库所没有的能力。ES首先进入大众视野的是它能够实现全文搜索。也是基于Lucene的实现,里面有一个倒排索引的数据结构。本文作者将介绍ES的分布式架构和ES的存储索引机制。本文不会详细介绍ES的API,而是从整体架构层面进行分析。什么是倒排索引要弄清楚什么是倒排索引,首先我们来梳理一下索引,比如一本书,书的目录页,里面有章节,章节名,我们想看哪一章,我们使用目录页查看对应的章节和页码,可以定位到具体的章节内容。在目录页通过章节名找到章节页码,然后就可以看到章节内容了。这个过程就是一个索引过程,那么什么是倒排索引呢?比如查询这本书《java 编程思想》的文章,可以读到目录页,记录章节名和章节地址页码。通过查询“继承”章节名,可以定位到“继承”章节的具体地址。查看文章内容后,我们可以看到文章内容中有很多“物”字。那么如果我们要查询本书中所有包含“物”字的文章,应该怎么办呢?按照目前的索引方式,毫无疑问大海捞针。假设我们有一个“对象”--→文章的映射关系,这不就可以了吗?像这样的倒排索引称为倒排索引。如图1所示,文章分词后得到关键词,根据关键词建立倒排索引。将关键词构造成一个字典,字典中存储每一个词条(关键词),每个关键词都有一个列表与之对应。图1中的列表是一个发帖列表,里面存储了章节文档编号、词频等信息,发帖列表中的每个元素都是一个发帖项。最后可以看出,整个倒排索引就像一部新华字典,所有单词的倒排列表往往是顺序存储在磁盘上的某个文件中。该文件称为倒排文件。字典和倒排文件是Lucene的两种基本数据结构,只是存储方式不同。字典存储在内存中,而倒排文件存储在磁盘中。本文不会介绍分词、tf-idf、BM25、向量空间相似度等构造倒排索引和查询倒排索引的技术。读者只需对倒排索引有一个基本的了解即可。ES集群架构集群节点一个ES集群可以由多个节点组成,一个节点就是一个ES服务实例,通过配置集群名称cluster.name加入到集群中。那么节点如何通过配置相同的集群名称加入集群呢?要理解这个问题,首先要理解节点在ES集群中的作用。如果ES中的节点有不同的角色,在配置文件conf/elasticsearch.yml中进行如下配置来设置角色。node.master:true/falsenode.data:true/false集群中的单个节点可以是候选主节点,也可以是数据节点。通过以上配置,可以两两组合,形成四类:只有候选主节点才是候选主节点,同时也是数据节点。它只是一个数据节点。既不是候选主节点也不是数据节点候选主节点:只有候选主节点可以参与选举投票,也只有候选主节点可以被选为主节点。主节点:负责添加和删除索引,跟踪哪些节点是集群的一部分,分配分片,收集集群中每个节点的状态等。稳定的主节点对集群的健康非常重要。数据节点:负责数据的增删改查、聚合等操作。数据节点负责查询和存储数据。对机器的CPU、IO、内存要求比较高。一般选择配置高的机器。作为数据节点。另外,还有一个节点角色叫协调节点,不是通过设置分配的。用户请求可以随机发送到任意一个节点,由节点负责分发请求、收集结果等,不需要主节点转发。这种节点可以称为协调器节点,集群中的任何一个节点都可以充当协调器节点。每个节点将保持相互联系。图2DiscoveryMechanism上面说到节点可以通过设置集群名加入集群,那么ES是怎么做到的呢?这里要说的是ZenDiscovery,ES的一种特殊的发现机制。ZenDiscovery是ES内置的发现机制。它提供两种发现方法:单播和组播。它的主要职责是发现集群中的节点并选举Master节点。Multicast也叫组播,意思是一个节点可以向多台机器发送请求。ES不建议在生产环境中使用该方法。对于大型集群,多播会产生大量不必要的通信。单播,当一个节点加入一个现有的集群,或者形成一个新的集群时,一个请求被发送到一台机器。当一个节点联系单播列表中的成员时,它会得到整个集群中所有节点的状态,然后它会联系Master节点并加入集群。只有在同一台机器上运行的节点才会自动形成集群。ES默认配置为使用单播发现。单播列表不需要包含集群中的所有节点,它只需要足够的节点。当一个新节点联系其中一个节点并进行通信时,就足够了。如果将主备选节点作为单播列表,只需要列出三个即可。这个配置在elasticsearch.yml文件中:discovery.zen.ping.unicast.hosts:["host1","host2:port"]集群信息收集阶段使用Gossip协议,上面的配置相当于a种子节点,Gossip协议这里不再赘述。ES官方建议配置unicast.hosts为所有候选master节点,ZenDiscovery会在每个ping_interval(配置项)时ping通。每次超时为discovery.zen.ping_timeout(配置项),如果3次ping不通(ping_retries配置项),则认为该节点宕机。在宕机的情况下,会触发故障转移,进行分片重新分配、复制等操作。如果宕机节点不是Master,Master会更新集群的meta信息,Master节点会将最新的集群meta信息发布给其他节点。其他节点回复Ack,Master节点收到discovery.zen.minimum_master_nodes值-1候选master节点的回复,然后向其他节点发送Apply消息,集群状态更新。如果宕机节点是Master,则其他候选Master节点将开始Master节点的选举过程。①Master选举在Master选举过程中,需要保证只有一个Master。ES使用一个参数quorumrepresentativemajoritythreshold来保证选出的master至少被quorum候选master节点认可,从而保证只有一个master。主选举的发起由候选主节点发起。当前候选master节点发现自己不是master节点,ping其他节点发现无法联系到master节点。而包括我在内已经有超过minimum_master_nodes个节点联系不上master节点,所以此时发起master选举。选择master的流程图如下:图3选择master时,根据集群节点的参数排序后的第一个节点就是Master节点。当一个候选主节点发起选举时,它会根据上述排序策略选出一个它认为的主节点。②脑裂指的是分布式系统中master的选择。不免要提一下脑裂现象。什么是裂脑?如果集群中选举了多个Master节点,就会出现数据更新不一致的情况。这种现象叫做脑裂。简而言之,集群中不同节点在Master的选择上意见不一,多个Master相互竞争。一般来说,脑裂问题可能是由以下几种原因引起的:网络问题:集群之间的网络延迟导致部分节点无法访问Master,以为Master宕机了,但实际上Master并没有宕机,以及选出一个新的。Master,并将Master上的shards和replicas标记为红色,并分配一个新的primaryshard。节点负载:主节点的角色既是Master又是Data。当有大量访问时,可能会导致ES停止响应(假死状态),造成大面积延迟。此时其他节点得不到主节点的响应,认为主节点挂了,重新选举主节点。内存回收:主节点的角色既是Master又是Data。当Data节点上的ES进程占用大量内存时,会引起JVM的大规模内存回收,导致ES进程失去响应。如何避免脑裂:基于以上原因,我们可以做优化措施:适当增加响应超时时间,减少误判。通过参数discovery.zen.ping_timeout设置节点ping超时时间,默认3s,可适当调整。要触发选举,我们需要在候选节点的配置文件中设置参数discovery.zen.munimum_master_nodes的值。该参数表示选举主节点时需要参选的候选主节点的个数。默认值为1,官方推荐值为(master_eligibel_nodes/2)+1,其中master_eligibel_nodes为候选主节点数。这样既可以防止脑裂现象的发生,又可以最大化集群的高可用,因为只要不少于discovery.zen.munimum_master_nodes个候选节点存活下来,选举就可以正常进行。小于该值时,无法触发选举行为,无法使用集群,也不会造成分片混乱。角色分离是指候选主节点与上述数据节点之间的角色分离,可以减轻主节点的负担,防止主节点假死,减少对主节点宕机的误判。如何写索引写索引的原理①ShardedES支持PB级的全文搜索。通常,当我们的数据量很大时,查询性能会越来越慢。我们可以想到的一种方法是将数据分散到不同的地方进行存储。ES也是如此。ES将数据在一个索引上进行拆分,通过水平拆分的方式分布到不同的数据块中。拆分后的数据库块称为分片,很像MySQL的分库分表。不同的primaryshard分布在不同的node上,那么multi-shardindex应该把数据写到哪里呢?一定不能乱写,否则查询时无法快速检索到对应的数据,这就需要一个route策略来决定写到哪个shard,下面会介绍如何路由。创建索引时需要指定分片数,分片数一旦确定,就不能修改。②Replica副本是一个分片的副本。每个主分片都有一个或多个副本分片。当主分片异常时,副本可以提供数据查询等操作。主分片和对应的副本分片不会在同一个节点上,避免数据丢失。当一个节点宕机时,也可以通过副本查询数据。复制分片的最大数量为N-1(其中N是节点数)。对doc的新建、索引、删除请求都是写操作,这些写操作必须在primaryshard上完成,然后才能复制到对应的replica。为了提高ES的编写能力,将进程并发编写。同时,为了解决并发写入过程中数据冲突的问题,ES采用乐观锁的方式进行控制。每个文档都有一个版本号。当文档被修改时,版本号增加。一旦所有副本分片都报告成功,它们就向协调节点报告成功,协调节点向客户端报告成功。图4③Elasticsearch的索引写入流程上面提到索引只能写入主分片,然后同步到副本分片。如图4所示,有四个主分片:S0、S1、S2、S3,一条数据是按照什么策略写入指定分片的?为什么将这段索引数据写入S0而不是S1或S2呢?这个过程是根据下面的公式来确定的:shard=hash(routing)上面公式的值%number_of_primary_shards是0和number_of_primary_shards-1之间的余数,也就是数据文件所在shard的位置。路由通过Hash函数生成一个数,然后用这个数除以number_of_primary_shards(primaryshards的个数)得到余数。routing是一个变量值,默认为文档的_id,也可以设置为自定义值。写入请求发送到某个节点后,该节点将作为上述协调节点,根据路由公式计算写入哪个分片。当前节点拥有所有其他节点的分片信息。如果对应的分片在其他节点上,则请求转发到该分片的主分片节点。在ES集群中,每个节点通过上面的公式就知道数据在集群中的存储位置,因此每个节点都有接收读写请求的能力。那么为什么在创建索引的时候就确定了primaryshards的个数,并且不能修改呢?因为如果数字发生变化,之前路由计算出来的所有值都会失效,再也找不到数据了。图5如上图5所示,通过路由计算公式得到的当前数据的值为shard=hash(routing)%4=0。具体过程如下:向node1节点发送数据写入请求,通过路由计算得到的值为1,那么对应的数据应该在主分片S1上。node1节点将请求转发给S1主分片所在的节点node2,node2接受请求并写入磁盘。将数据并发复制到三个副本分片R1,通过乐观并发控制数据冲突。一旦所有的replicashards都报告成功,node2就会向node1报告成功,node1再向client报告成功。这种模式下,只要有replicas,最小的写延迟就是两个单shard写的总和,效率会很低。但是这样做的好处也是显而易见的。为避免写入后单机硬件故障导致数据丢失,从数据完整性和性能上来说,一般优先选择数据,除非有一些特殊场景允许数据丢失。ES中为了减少磁盘IO,保证读写性能,一般会每隔一段时间(比如30分钟)将数据写入磁盘进行持久化。对于写入内存但还没有刷入磁盘的数据,如果机器死机或者掉电,内存中的数据也会丢失。如何确保这一点?对于这类问题,ES借鉴了数据库中的处理方式,在ES中添加了CommitLog模块,称为transLog,在后面的ES存储原理中会介绍。上面的存储原理介绍了ES内部的索引写入过程。数据写入分片和副本后,数据当前在内存中。为保证数据掉电不丢失,需要持久化到磁盘。我们知道ES是基于Lucene实现的,在内部,索引的创建、写入和搜索查询都是通过Lucene完成的。Lucene的工作原理如下图所示。当有新文档加入时,Lucene进行分词等预处理,然后将文档索引写入内存,并将此操作写入事务日志(transLog)。transLog类似于MySQL的binlog,用于宕机后恢复内存数据,保存未持久化数据的操作日志。默认情况下,Lucene每隔1s(refresh_interval配置项)将内存中的数据刷新到文件系统缓存中,称为一个段(segment)。一旦刷新到文件系统缓存中,该段就可以用于检索,并且在此之前无法检索。所以refresh_interval决定了ES数据的实时性,所以ES是一个准实时系统。该段在磁盘上不可修改,因此避免了对磁盘的随机写入,并且所有随机写入都在内存中执行。随着时间的推移,细分越来越多。默认情况下,每隔30分钟或者segment空间大于512M,Lucene会将缓存中的segment持久化到磁盘,称为commitpoint,此时删除对应的transLog。我们在测试写操作的时候,可以手动刷新,保证数据能够及时取回,但是在生产环境中,不要每次索引文档时都手动刷新,因为刷新操作会有一定的性能开销。一般业务场景不需要每秒刷新一次。可以通过增加Settings中refresh_interval="30s"的值来降低每个索引的刷新频率。设置值时需要注意后面的时间单位,否则默认为毫秒。当refresh_interval=-1时,表示关闭索引的自动刷新。图6索引文件是分段存储的,不能修改,那么如何增、改、删呢?添加,添加好办,因为数据是新的,所以只需要在当前文档中添加一个段即可。.删除,因为不能修改,所以对于删除操作,不会从旧段中删除文件,而是会添加一个新的.del文件。这些被删除文件的段信息将列在该文件中。这被标记为删除。该文档仍然可以被查询匹配,但在返回最终结果之前它将从结果集中删除。update,不能修改旧段来更新文档。其实更新就相当于删除和添加两个动作。旧文档在.del文件中被标记为删除,文档的新版本被索引到一个新的部分。文档的两个版本可能会被单个查询匹配,但在返回结果集之前,将删除已删除文档的旧版本。段设置为不可变的有一定的优点和缺点。优点:不需要锁。如果您从不更新索引,则无需担心多个进程同时修改数据。一旦索引被读入内核的文件系统缓存,由于其不变性,它会保留在那里。只要文件系统缓存中有足够的空间,大多数读取请求将直接进入内存而不会到达磁盘。这提供了很大的性能提升。其他缓存(如过滤器缓存)在索引的生命周期内始终有效。不需要每次数据更改时都重建它们,因为数据不会更改。写入单个大型倒排索引允许压缩数据,减少磁盘I/O和需要缓存在内存中的索引的使用。缺点:删除旧数据时,不会立即删除旧数据,而是在.del文件中标记为已删除。旧的数据只有在段更新时才能被移除,这样会造成大量的空间浪费。如果有一条数据经常更新,每次更新都加新标记旧的,会浪费很多空间。每次添加新数据时,都需要一个新的段来存储数据。当段数过多时,对文件句柄等服务器资源的消耗会非常大。在查询结果中包含所有的结果集,需要排除标记为删除的旧数据,增加了查询的负担。①段合并由于每次刷新都会创建一个新的段(segment),这样会导致段数在短时间内急剧增加,段数过多会带来更大的麻烦。大量的段会影响数据读取性能。每个段都消耗文件句柄、内存和CPU周期。而且,每次搜索请求都要依次检查每个段,然后合并查询结果,所以段越多,搜索越慢。所以,Lucene会按照一定的策略合并段,合并的时候,那些旧的删除文件会从文件系统中清除。删除的文档不会复制到新的大段。合并过程中不会中断索引和查找,倒排索引的数据结构使得合并文件更加容易。段合并在索引和搜索期间自动发生。合并过程选择一小组大小相似的段,并在后台将它们合并成更大的段,这些段可以是未提交的也可以是已提交的。.合并完成后,旧段被删除,新段被刷入磁盘,并写入一个包含新段且不包括旧段和较小段的新提交点,并打开新段进行搜索。segmentmerging的计算量巨大,同时也会消耗大量的磁盘I/O,而且segmentmerging会拖累writerate,放任不管会影响搜索性能。默认情况下,ES会对合并过程的资源进行限制,所以搜索性能可以得到保证。图7写在最后。本文介绍了ES的架构原理以及索引的存储和写入机制。ES的整体架构体系比较巧妙。我们在设计系统时可以借鉴其设计思想。本文只介绍ES的整体架构。作者:官网商城开发组编辑:陶家龙来源:转载自公众号vivo互联网科技(ID:vivoVMIC)