小姐姐味道【ID:xjjdog】
作者:十年架构,日百亿流量经验,与你分享。
强烈建议先读一下《也浅谈下分布式存储要点》,对ES会有更好的认识。ES融合了倒排索引、行存、列存的诸多特点,已经不再是一个简单的全文搜索引擎。
ES是从厨房里走出来的,原型是Shay Banon
给妻子做的食谱搜索引擎,一开始便充满了爱的味道。
儿女情长什么的,最影响程序员发挥了!
最终,ElasticSearch
斩断情愫,成为一个基于Lucene
的分布式存储。安装后访问其页面,会显示:”You Know, for Search“
,这句平凡的语句无意中透露的自信,深于一切怀疑和平淡。
几点说明
几点说明 |
---|
索引中的Type定义会引起诸多歧义,已经在6.0版本废弃 |
作为搜索时,是类实时性系统,写入到读取之间存在延迟,一般为1秒 ;但当作为存储时,是RT的 |
ES索引的分片数一旦确定不可改变,既成事实的Mapping也是 |
你优化了写入,就可能干扰了查询和可靠性,大多时候不可兼得 |
你会经常碰到OOM,显然ES是内存大户,缺乏这方面的保护 |
ES没有事务 |
基础概念
ES是典型的分布式系统。包含一到多个内存型节点。有分片,也有副本,可靠性高,自适应能力强。ES采用多机并行能力来进行扩展,是建立在一系列层级数据结构上的。这些抽象作为一个全局的路由表,存在于每个运行的实例上,给了ES强大的功能和扩展能力。
名词 | 解释 |
---|---|
Cluster | 集群,有着统一的名称。包含一个master节点,和落干其他类型节点。一般为对等节点 |
Node | 节点,ES运行实例,指定集群中的任意一台即可加入 |
Index | 索引,逻辑命名空间。类比DBMS中的库 |
Shard | 分片,每个索引包含一个或多个分片,用来将数据分布在不同实例上 |
Replica | 每个分片又分为主副本和从副本,用来保证HA和Failover。每一个replica为具体lucene实例 |
Segment | 段,倒排索引的子集,会不断的合并以减少资源占用 |
Document | 文档,为具体存放的某条记录,比如某条订单信息 |
Feild | 字段,文档中的某个字段,可以有不同的类型 |
索引类型
倒排索引
很多同学有一个误解,以为ES是一个全文搜索引擎,那么就只有倒排索引这一种索引类型,那是错的。数据在写入es时,会产生多份数据用于不同查询方式,使用的索引结构也不相同。
ES默认是对所有字段进行索引的(也就是倒排索引),如果不需要,可以在mapping中将index
属性设置为no
;如果字段需要精确查找,则设置为not_analyzed
。
为了增加倒排索引的Term查找速度,ES还专门做了Term index
,它的本质是一棵Trie(前缀)树(使用FST技术压缩)。
_all
是一个特殊的字段,可以根据某个关键词,搜索整个文档内容(而不是某个字段),这个默认是关闭的。
列式存储
按照以上的倒排索引结构,查找包含某个term的文档是非常迅捷的。如果要对这个字段进行排序的话,倒排索引就捉襟见肘了,需要使用其他的存储结构进行索引。
ES使用冗余的方式进行解决这个问题,它存储了另一份数据,也就是Doc Values
。可以说Doc Values
是一个列式存储结构,适合排序、聚合操作等。放在内存中的fielddata
功能和它类似,但没有内存容量的限制,大数据量优先使用。
到此为止,ES已经默认按照不同的结构存储了两份数据了。但如果你不需要,还是可以禁用的。同样是在mapping映射中,给字段赋予属性"doc_values":false
即可。
假如你用的是ELKB系列,倒排索引根本就没用到。
行
式存储
而作为行存
的_source
字段,以json方式存储了原始文档。一般是不需要关闭的。但如果你的文档字段比较多,根据搜索后查出列表,再根据列表的数据到其他存储获取,那么就可以将_source
关掉。类似的,设置"_source"{"enabled":false}
即可。
如果只想要几个字段被存储,可以使用include。
"_source":{ "includes":["field1","field2"] }
ES有太多的这种细化的自定义,不再详叙。
写入过程
找到分片
某个分片具体在哪个节点上,由ES自行决定。每个节点都缓存了这些路由信息,所以,你的请求发送到任何一个ES节点上,都可以执行。
ES选择的分片路由算法是Hash,这决定了它的分片数一旦确定,不可更改。因为一旦变了,路由的数据就完全不正确了。
如果Request中指定了路由条件,则直接使用Request中的Routing,否则使用Mapping中配置的,如果Mapping中无配置,则使用默认的_id字段值。
默认参与Hash计算的字段是_id
,使用ES自带的生成器能较好的平均数据,使用自定义的id可能会产生数据倾斜。
shard = hash(routing) % number_of_primary_shards
剩下的,就是单机索引的事了。
单机Shard的写入过程
ES的写入性能可以很高(尤其是批量写入),取决于你的配置。一个文档要写入索引,直到读可见,要经过一系列的缓冲和合并。我们拿ES官方博客的一张图来说明。
ES的底层存储是Lucene,包含一系列的反向索引。这样的一批索引的信息就是上面提到的段(segment)。但记录不会直接写入段,而是先写入一个缓冲区。
当缓冲区满了,或者在缓冲区呆的够久,达到了刷新时间(划重点),会一次性将缓冲区的内容写进段中。这也是为什么refresh_interval
属性的配置会严重的影响性能。如果你不要很高的实时性,不妨将其配置的大一点。
缓冲区默认使用堆空间的10%,最小值为48mb(针对于分片的)。如果你的索引多且写入重,这部分内存的占用是可观的,可以适当加大。
问题是Segment是不可变的,删除、更新操作,并不能在原来的段上进行。ES对待所有的操作都是相似的,并不区别对待,删除和更新,有着和写入一样的mege过程。这会生成大量的段,每个段会占用一个文件句柄,会浪费大量资源。ES有专门的进程负责段的自动合并,我们不需要手动干涉。
段的合并会浪费大量的I/O和CPU资源,有tiered
(默认)、log_byte_size
、log_doc
三种合并策略,每种策略都有各自的配置参数。可惜的是,索引一旦确定,策略就不能更改了。调整这些参数,大多情况下效果显著。
常用的配置参数是执行归并的线程数,max_bytes_per_sec已经不再使用了。
index.merge.scheduler.max_thread_count
如果你的I/O过重,可以适量减少此值的大小。
即然是先写到缓冲区,就有丢的可能,比如突然断电。为了解决此问题,ES在写Buffer的同时,也将数据写入一个叫做translog的文件。这个文件是顺序写,所以速度比较快。translog是故障恢复时,回放故障发生前夕数据的唯一途径。这些数据,没有机会能够写入到Lucene中。
将translog的写入周期改成async的,而不是基于请求的,会显著减少I/O占用。
操作系统在将磁盘写入文件时,也会有相应的buffer cache,这与其他DB如MySQL、PG的工作方式是一样的,使用fsync保证文件能够刷到磁盘上,不多描述。
节点类型
ES按照不同的用途和场景,划分了不同的节点类型。
Master Node 有资格被选择为主节点,然后控制整个集群
Data Node 该节点能够保存数据和执行操作
Tribe Node 部落节点,可以连接多个集群,对外提供统一的入口
Ingest Node 定义一个pipeline来处理数据,可以替代logstash中的某些功能
客户端节点 不保存数据也不协调集群,仅响应用户请求,将其发送到其他节点
End
ES通过冗余多份数据达到不同的用途,开箱即用。开箱即用的意思也就是认识成本低,学习成本大,先把你吸引进门再说。
ES不是一匹好驯服的野马。通常,它并不像官方和其他PPT宣传的那样无所不能,你可能经常在OOM和分片移动中度日。
希望在使用ES之前,能够了解它的一些底层设计结构。这样,在遇到一些瓶颈的限制以后,能够了解到它为什么有这样或者那样的反应;另外在做一些解决方案的时候,能够多给自己一点底气。
不过话说回来,上得了厅房,下得了厨房的女人,大家还是都喜欢~
共同学习,写下你的评论
评论加载中...
作者其他优质文章