es是哪里来的?
问题:当数据量达到了亿级,兆级,我们将会面临哪些问题?
- 如何存储?用什么数据库?关系型还是非关系型?
- 如何保证数据的安全性?如何保证服务的可用性?
- 如何搜索?
- 如何统计分析?
es 来了 解决方案:
- 建立集群,分担数据存储压力,应对高并发,提高系统可用性。
- 多点存储,提高数据安全性。
- 高效索引,提高查询速度和统计分析能力。
- 压缩数据,降低存储成本。
简单介绍一下es 答:
es是一个基于lucence的开源的全文检索引擎,高扩展,分布式。它的数据读写数据都很快,尤其是读,搜索速度近乎实时。扩展性也很强大,可以扩展成上百台服务器的集群。es还使用RESTful API来隐藏实现上的复杂性,使得通过任何语言使用起来都变得非常简单
两个核心功能 检索和统计
es节点类型
es为了实现集群的各种能力,集群中的各个节点可能会分别承担不同的角色。主节点:通过node.master=true的配置,可以把一个节点设置为候选主节点。候选主节点经过集群选举,如果成功,就会成为主节点。主节点负责索引的创建和删除,分配分片,追踪集群中的节点状态等工作。数据节点:通过node.data=true的配置,可以把一个节点设置为数据节点。数据节点主要负责数据的读写相关操作,如CRUD,搜索,聚合等。客户端节点:如果某个节点的node.master和node.data都为false,那么,该节点将既不参与竞选主节点,也不会读写数据。那么,它在集群中还有用吗?有。它也可以处理客户的请求,只是处理的方式是:把收到的请求分发到相关节点,并把得到的结果汇总返回。这种节点,称之为客户端节点。协调节点:协调节点不是一个具体的节点类型,它更像是一个节点所扮演的角色。如上面提到的客户端节点,就是一个协调节点的角色,客户端节点的工作内容也正是承担了协调节点角色的工作内容。当然了,主节点,候选主节点或者数据节点,都可以承担起这样的角色。
为什么要有主节点?
这个问题其实很简单,任何一个组织都需要有一个核心领导团队,就像公司有Boss一样。做为一个集群,也需要有一个核心领导力。这个核心领导力,可能来自于某些第三方中间件,也可能像es这样,从内部产生一个master。主节点,主要是针对分布式系统下的CAP,提供解决方案,CAP,即一致性,可用性,分区容忍性。后面我们聊到主节点的具体工作时,就会对这部分有更深入的理解。
主节点的选举
es的主节点的选举可以简单归纳为:有人竞选,有人投票,票多者得。选举的目标是:选出唯一的一个主节点。
谁可以竞选?
所以node.master=true的节点都可以参与主节点的竞选。
谁可以投票?
es集群中的每个节点都可以投票
如何投票?
每个节点都把自己所知道的节点进行一次排序,排第一就是自己要选的。源码如下:
public DiscoveryNode electMaster(Iterable nodes){ List sortedNodes = sortedMasterNodes(nodes);if (sortedNodes == null || sortedNodes.isEmpty()) {returnnull; }return sortedNodes.get(0); }
排序的依据是节点的id:
private static classNodeComparatorimplementsComparator{@Overridepublicintcompare(DiscoveryNode o1, DiscoveryNode o2){if (o1.masterNode() && !o2.masterNode()) {return -1; }if (!o1.masterNode() && o2.masterNode()) {return1; }return o1.id().compareTo(o2.id()); } }
如何当选?
某个候选主节点得到了大于全部候选主节点数量一半以上的票数,并且自己先选了自己,它们,该节点就当选为主节点。假设某集群有9个节点,其中,候选主节点有3个,其中一个是A,那么,A当选是条件就是包含A在内,至少有两个节点投票A为主节点。
选举脑裂
观察投票时,节点排序的源码,我们可以看到,es是简单的通过节点的id来排序的。而节点的id,一旦加入到集群中,就是固定的,所以,一般情况下,大家对于选举其实是有共识的。这种共识,可以保证选择的成功。但是,在发生了分区的情况下,情况就变得复杂了。
图 1
如上图,在发生分区的情况下,由于每个节点都只在(也只能在)自己知道的节点中进行选举,我们假设5个节点都是候选主节点。那么,在上图的情形下,左分区节点1会当选,右分区节点2会当选。这样,当两个分区再联通时,就会产生两个主节点,这种情况,我们称之为脑裂。那么,该如何避免这种情况发生呢?看如下源码:
publicbooleanhasEnoughMasterNodes(Iterable nodes){if (minimumMasterNodes < 1) {returntrue; }int count = 0;for (DiscoveryNode node : nodes) {if (node.masterNode()) { count++; } }return count >= minimumMasterNodes; }
上述方法会在选举进行前执行,当返回true时,选举正常进行,当返回false时,选择不进行。方法很简单,就是通过比较当前集群中的候选主节点数量和minimumMasterNodes,如果候选主节点数量大于minimumMasterNodes,则可进行选举,否则,不可选举。minimumMasterNodes来源于我们的配置。如图1,我们可以通过将minimumMasterNodes设置为3,来避免脑裂的发生。这是因为,当minimumMasterNodes为3时,左分区由于候选主节点不足3个,所以无法进行选举,只能等待分区的联通。而右分区可以正常选举。于是,当分区恢复后,节点3当选成为了唯一的主节点。
discovery.zen.minimum_master_nodes=3通常,对于一个有n个候选主节点的集群,minimumMasterNodes可以设置为 n/2 + 1。这样可以避免脑裂。需要注意的是,假设有一个集群,有两个候选主节点,那么minimumMasterNodes设置为2就可以避免脑裂,但是,这样会导致,一旦集群发生分区,使得两个候选主节点无法联通时,集群将变得不可用。而minimumMasterNodes设置为1,可以保证可用,但是又无法避免脑裂。所以es推荐,集群中的候选主节点数量至少为3。
es 写数据过程
- 客户端选择一个 node 发送请求过去,这个 node 就是 coordinating node (协调节点)。
- coordinating node 对 document 进行路由,将请求转发给对应的 node(有 primary shard)。
- 实际的 node 上的 primary shard 处理请求,然后将数据同步到 replica node 。
- coordinating node 如果发现 primary node 和所有 replica node 都搞定之后,就返回响应结果给客户端。
es 读数据过程
可以通过 doc id 来查询,会根据 doc id 进行 hash,判断出来当时把 doc id 分配到了哪个 shard 上面去,从那个 shard 去查询。
- 客户端发送请求到任意一个 node,成为 coordinate node 。
- coordinate node 对 doc id 进行哈希路由,将请求转发到对应的 node,此时会使用 round-robin 随机轮询算法,在 primary shard 以及其所有 replica 中随机选择一个,让读请求负载均衡。
- 接收请求的 node 返回 document 给 coordinate node 。
- coordinate node 返回 document 给客户端。
es 搜索数据过程
es 最强大的是做全文检索,就是比如你有三条数据:
java真好玩儿啊
java好难学啊
j2ee特别牛
你根据 java 关键词来搜索,将包含 java 的 document 给搜索出来。es 就会给你返回:java 真好玩儿啊,java 好难学啊。
客户端发送请求到一个 coordinate node 。
协调节点将搜索请求转发到所有的 shard 对应的 primary shard 或 replica shard ,都可以。
query phase:每个 shard 将自己的搜索结果(其实就是一些 doc id )返回给协调节点,由协调节点进行数据的合并、排序、分页等操作,产出最终结果。
fetch phase:接着由协调节点根据 doc id 去各个节点上拉取实际的 document 数据,最终返回给客户端。
写请求是写入 primary shard,然后同步给所有的 replica shard;读请求可以从 primary shard 或 replica shard 读取,采用的是随机轮询算法。
写数据底层原理
先写入内存 buffer,在 buffer 里的时候数据是搜索不到的;同时将数据写入 translog 日志文件。
如果 buffer 快满了,或者到一定时间,就会将内存 buffer 数据 refresh 到一个新的 segment file 中,但是此时数据不是直接进入 segment file 磁盘文件,而是先进入 os cache 。这个过程就是 refresh 。
每隔 1 秒钟,es 将 buffer 中的数据写入一个新的 segment file ,每秒钟会产生一个新的磁盘文件 segment file ,这个 segment file 中就存储最近 1 秒内 buffer 中写入的数据。
但是如果 buffer 里面此时没有数据,那当然不会执行 refresh 操作,如果 buffer 里面有数据,默认 1 秒钟执行一次 refresh 操作,刷入一个新的 segment file 中。
操作系统里面,磁盘文件其实都有一个东西,叫做 os cache ,即操作系统缓存,就是说数据写入磁盘文件之前,会先进入 os cache ,先进入操作系统级别的一个内存缓存中去。只要 buffer 中的数据被 refresh 操作刷入 os cache 中,这个数据就可以被搜索到了。
为什么叫 es 是准实时的?NRT ,全称 near real-time 。默认是每隔 1 秒 refresh 一次的,所以 es 是准实时的,因为写入的数据 1 秒之后才能被看到。可以通过 es 的 restful api 或者 java api ,手动执行一次 refresh 操作,就是手动将 buffer 中的数据刷入 os cache 中,让数据立马就可以被搜索到。只要数据被输入 os cache 中,buffer 就会被清空了,因为不需要保留 buffer 了,数据在 translog 里面已经持久化到磁盘去一份了。
重复上面的步骤,新的数据不断进入 buffer 和 translog,不断将 buffer 数据写入一个又一个新的 segment file 中去,每次 refresh 完 buffer 清空,translog 保留。随着这个过程推进,translog 会变得越来越大。当 translog 达到一定长度的时候,就会触发 commit 操作。
commit 操作发生第一步,就是将 buffer 中现有数据 refresh 到 os cache 中去,清空 buffer。然后,将一个 commit point 写入磁盘文件,里面标识着这个 commit point 对应的所有 segment file ,同时强行将 os cache 中目前所有的数据都 fsync 到磁盘文件中去。最后清空 现有 translog 日志文件,重启一个 translog,此时 commit 操作完成。
这个 commit 操作叫做 flush 。默认 30 分钟自动执行一次 flush ,但如果 translog 过大,也会触发 flush 。flush 操作就对应着 commit 的全过程,我们可以通过 es api,手动执行 flush 操作,手动将 os cache 中的数据 fsync 强刷到磁盘上去。
translog 日志文件的作用是什么?你执行 commit 操作之前,数据要么是停留在 buffer 中,要么是停留在 os cache 中,无论是 buffer 还是 os cache 都是内存,一旦这台机器死了,内存中的数据就全丢了。所以需要将数据对应的操作写入一个专门的日志文件 translog 中,一旦此时机器宕机,再次重启的时候,es 会自动读取 translog 日志文件中的数据,恢复到内存 buffer 和 os cache 中去。
translog 其实也是先写入 os cache 的,默认每隔 5 秒刷一次到磁盘中去,所以默认情况下,可能有 5 秒的数据会仅仅停留在 buffer 或者 translog 文件的 os cache 中,如果此时机器挂了,会丢失 5 秒钟的数据。但是这样性能比较好,最多丢 5 秒的数据。也可以将 translog 设置成每次写操作必须是直接 fsync 到磁盘,但是性能会差很多。
实际上你在这里,如果面试官没有问你 es 丢数据的问题,你可以在这里给面试官炫一把,你说,其实 es 第一是准实时的,数据写入 1 秒后可以搜索到;可能会丢失数据的。有 5 秒的数据,停留在 buffer、translog os cache、segment file os cache 中,而不在磁盘上,此时如果宕机,会导致 5 秒的数据丢失。
总结一下,数据先写入内存 buffer,然后每隔 1s,将数据 refresh 到 os cache,到了 os cache 数据就能被搜索到(所以我们才说 es 从写入到能被搜索到,中间有 1s 的延迟)。每隔 5s,将数据写入 translog 文件(这样如果机器宕机,内存数据全没,最多会有 5s 的数据丢失),translog 大到一定程度,或者默认每隔 30mins,会触发 commit 操作,将缓冲区的数据都 flush 到 segment file 磁盘文件中。
数据写入 segment file 之后,同时就建立好了倒排索引。
删除/更新数据底层原理
如果是删除操作,commit 的时候会生成一个 .del 文件,里面将某个 doc 标识为 deleted 状态,那么搜索的时候根据 .del 文件就知道这个 doc 是否被删除了。
如果是更新操作,就是将原来的 doc 标识为 deleted 状态,然后新写入一条数据。
buffer 每 refresh 一次,就会产生一个 segment file ,所以默认情况下是 1 秒钟一个 segment file ,这样下来 segment file 会越来越多,此时会定期执行 merge。每次 merge 的时候,会将多个 segment file 合并成一个,同时这里会将标识为 deleted 的 doc 给物理删除掉,然后将新的 segment file 写入磁盘,这里会写一个 commit point ,标识所有新的 segment file ,然后打开 segment file 供搜索使用,同时删除旧的 segment file 。
底层 lucene
简单来说,lucene 就是一个 jar 包,里面包含了封装好的各种建立倒排索引的算法代码。我们用 Java 开发的时候,引入 lucene jar,然后基于 lucene 的 api 去开发就可以了。
通过 lucene,我们可以将已有的数据建立索引,lucene 会在本地磁盘上面,给我们组织索引的数据结构。
下一篇 倒排索引
本文转载自: 掘金