Lucene
lucene的学习目的主要是为了更好的理解ES的原理,重点理解两个知识点:
1.倒排索引
2.数据分段
倒排索引
搜索的核心需求是全文检索,全文检索简单来说就是要在大量文档中找到包含某个单词出现的位置,在传统关系型数据库中,数据检索只能通过 like 来实现,例如需要在员工数据中查询姓王的的员工,需要通过如下 sql 实现:
select * from hotel_table where hotel_name like '王%';
这种实现方式实际会存在很多问题:
1.无法使用数据库索引,需要全表扫描,性能差
2.搜索效果差,只能首尾位模糊匹配,无法实现复杂的搜索需求
3.无法得到文档与搜索条件的相关性
搜索的核心目标实际上是保证搜索的效果和性能,elasticsearch使用一种称为倒排索引的数据结构,用于快速的全文搜索。一个倒排索引有文档中所有不重复词的列表构成,每个词都有一个包含它的文档列表。
倒排索引是区别于正排索引的概念:
正排索引:
是以文档对象的唯一 ID 作为索引,以文档内容作为记录的结构。
倒排索引:
Inverted index,指的是将文档内容中的单词作为索引,将包含该词的文档 ID 作为记录的结构。
参考如下:
当有一组员工数据的时候,可以存在两个倒排数据 一个是基于年领倒排,一个是基于性别倒排。
下面通过一个例子来说明下倒排索引的生成过程。
假设目前有以下两个文档内容:
苏州街维亚大厦
桔子酒店苏州街店
其处理步骤如下:
1、正排索引给每个文档进行编号,作为其唯一的标识。
a.首先要对字段的内容进行分词,分词就是将一段连续的文本按照语义拆分为多个单词,这里两个文档包含的关键词有:苏州街、维亚大厦.....
b.然后按照单词来作为索引,对应的文档 id 建立一个链表,就能构成上述的倒排索引结构。
有了倒排索引,能快速、灵活地实现各类搜索需求。整个搜索过程中我们不需要做任何文本的模糊匹配。
例如,如果需要在上述两个文档中查询 苏州街桔子 ,可以通过分词后通过 苏州街 查到 1、2,通过 桔子 查到2,然后再进行取交取并等操作得到最终结果。
倒排索引的结构
根据倒排索引的概念,我们可以用一个 Map来简单描述这个结构。这个 Map 的 Key 的即是分词后的单词,这里的单词称为 Term,这一系列的 Term 组成了倒排索引的第一个部分 —— Term Dictionary (索引表,可简称为 Dictionary)。
倒排索引的另一部分为 Postings List(记录表),也对应上述 Map 结构的 Value 部分集合。
记录表由所有的 Term 对应的数据(Postings) 组成,它不仅仅为文档 id 信息,可能包含以下信息:
文档 id(DocId, Document Id),包含单词的所有文档唯一 id,用于去正排索引中查询原始数据。
词频(TF,Term Frequency),记录 Term 在每篇文档中出现的次数,用于后续相关性算分。
位置(Position),记录 Term 在每篇文档中的分词位置(多个),用于做词语搜索(Phrase Query)。
偏移(Offset),记录 Term 在每篇文档的开始和结束位置,用于高亮显示等。
Lucene 倒排索引实现
全文搜索引擎在海量数据的情况下是需要存储大量的文本,所以面临以下问题:
Dictionary 是比较大的(比如我们搜索中的一个字段可能有上千万个 Term)
Postings 可能会占据大量的存储空间(一个Term多的有几百万个doc)
因此上面说的基于 Map 的实现方式几乎是不可行的。
在海量数据背景下,倒排索引的实现直接关系到存储成本以及搜索性能。
为此,Lucene 引入了多种巧妙的数据结构和算法。其倒排索引实现拥有以下特性:
以较低的存储成本存储在磁盘 (索引大小大约为被索引文本的20-30%)
快速读写
下面将根据倒排索引的结构,按 Posting List 和 Terms Dictionary 两部分来分析 Lucene 中的实现。
Posting List 实现
PostingList 包含文档 id、词频、位置等多个信息,这些数据之间本身是相对独立的,因此 Lucene 将 Postings List 被拆成三个文件存储:
.doc后缀文件:记录 Postings 的 docId 信息和 Term 的词频
.pay后缀文件:记录 Payload 信息和偏移量信息
.pos后缀文件:记录位置信息
基本所有的查询都会用 .doc 文件获取文档 id,且一般的查询仅需要用到 .doc 文件就足够了,只有对于近似查询等位置相关的查询则需要用位置相关数据。
三个文件整体实现差不太多,这里以.doc 文件为例分析其实现。
.doc 文件存储的是每个 Term 对应的文档 Id 和词频。每个 Term 都包含一对 TermFreqs 和 SkipData 结构。
其中 TermFreqs 存放 docId 和词频信息,SkipData 为跳表信息,用于实现 TermFreqs 内部的快速跳转。
Term Dictionary 实现
Terms Dictionary(索引表)存储所有的 Term 数据,同时它也是 Term 与 Postings 的关系纽带,存储了每个 Term 和其对应的 Postings 文件位置指针。
倒排查询逻辑
在介绍了索引表和记录表的结构后,就可以得到 Lucene 倒排索引的查询步骤:
1.通过 Term Index 数据(.tip文件)中的 StartFP 获取指定字段的 FST
2.通过 FST 找到指定 Term 在 Term Dictionary(.tim 文件)可能存在的 Block
3.将对应 Block 加载内存,遍历 Block 中的 Entry,通过后缀(Suffix)判断是否存在指定 Term
4.存在则通过 Entry 的 TermStat 数据中各个文件的 FP 获取 Posting 数据
5.如果需要获取 Term 对应的所有 DocId 则直接遍历 TermFreqs,如果获取指定 DocId 数据则通过 SkipData 快速跳转
数据分段
在早期的全文检索中为整个文档集合建立了一个很大的倒排索引,并将其写入磁盘中,如果索引有更新,就需要重新全量创建一个索引来替换原来的索引。
这种方式在数据量很大时效率很低,并且由于创建一次索引的成本很高,所以对数据的更新不能过于频繁,也就不能保证时效性。
现在,在搜索中引入了段的概念(将一个索引文件拆分为多个子文件,则每个子文件叫作段),每个段都是一个独立的可被搜索的数据集,并且段具有不变性,一旦索引的数据被写入硬盘,就不可再修改。
写操作
在分段的思想下,对数据写操作的过程如下。
新增:
当有新的数据需要创建索引时,由于段的不变性,所以选择新建一个段来存储新增的数据。
删除:
当需要删除数据时,由于数据所在的段只可读,不可写,所以Lucene在索引文件下新增了一个.del的文件,用来专门存储被删除的数据id。当查询时,被删除的数据还是可以被查到的,只是在进行文档链表合并时,才把已经删除的数据过滤掉。被删除的数据在进行段合并时才会真正被移除。
更新:
更新的操作其实就是删除和新增的组合,先在.del文件中记录旧数据,再在新段中添加一条更新后的数据。
段不变性的优点
1.不需要锁。因为数据不会更新,所以不用考虑多线程下的读写不一致情况。
2.可以常驻内存。段在被加载到内存后,由于具有不变性,所以只要内存的空间足够大,就可以长时间驻存,大部分查询请求会直接访问内存,而不需要访问磁盘,使得查询的性能有很大的提升。
3.缓存友好。在段的声明周期内始终有效,不需要在每次数据更新时被重建。
4.增量创建。分段可以做到增量创建索引,可以轻量级地对数据进行更新,由于每次创建的成本很低,所以可以频繁地更新数据,使系统接近实时更新。
段不变性的缺点
1.当对数据进行删除时,旧数据不会被马上删除,而是在.del文件中被标记为删除。而旧数据只能等到段更新时才能真正被移除,这样会有大量的空间浪费。
2.更新。更新数据由删除和新增这两个动作组成。若有一条数据频繁更新,则会有大量的空间浪费。
3.由于索引具有不变性,所以每次新增数据时,都需要新增一个段来存储数据。当段的数量太多时,对服务器的资源(如文件句柄)的消耗会非常大,查询的性能也会受到影响。
4.在查询后需要对已经删除的旧数据进行过滤,这增加了查询的负担。
准实时的搜索引擎
为了提升写的性能,Lucene并没有每新增一条数据就增加一个段,而是采用延迟写的策略,每当有新增的数据时,就将其先写入内存中,然后批量写入磁盘中。若有一个段被写到硬盘,就会生成一个提交点,提交点就是一个用来记录所有提交后的段信息的文件。一个段一旦拥有了提交点,就说明这个段只有读的权限,失去了写的权限;相反,当段在内存中时,就只有写数据的权限,而不具备读数据的权限,所以也就不能被检索了。从严格意义上来说,Lucene或者Elasticsearch并不能被称为实时的搜索引擎,只能被称为准实时的搜索引擎。
写索引的流程如下
-
新数据被写入时,并没有被直接写到硬盘中,而是被暂时写到内存中。Lucene默认是一秒钟,或者当内存中的数据量达到一定阶段时,再批量提交到磁盘中,当然,默认的时间和数据量的大小是可以通过参数控制的。通过延时写的策略,可以减少数据往磁盘上写的次数,从而提升整体的写入性能。
在达到触发条件以后,会将内存中缓存的数据一次性写入磁盘中,并生成提交点。
-
清空内存,等待新的数据写入。
段合并策略
虽然分段比每次都全量创建索引有更高的效率,但由于在每次新增数据时都会新增一个段,所以经过长时间的积累,会导致在索引中存在大量的段,当索引中段的数量太多时,不仅会严重消耗服务器的资源,还会影响检索的性能。
因为索引检索的过程是:
查询所有段中满足查询条件的数据,然后对每个段里查询的结果集进行合并,所以为了控制索引里段的数量,我们必须定期进行段合并操作。
但是如果每次合并全部的段,则将造成很大的资源浪费,特别是“大段”的合并。
所以Lucene现在的段合并思路是:
根据段的大小先将段进行分组,再将属于同一组的段进行合并。但是由于对超级大的段的合并需要消耗更多的资源,所以Lucene会在段的大小达到一定规模,或者段里面的数据量达到一定条数时,不会再进行合并。
所以Lucene的段合并主要集中在对中小段的合并上,这样既可以避免对大段进行合并时消耗过多的服务器资源,也可以很好地控制索引中段的数量。
段合并的主要参数
mergeFactor:每次合并时参与合并的段的最少数量,当同一组的段的数量达到此值时开始合并,如果小于此值则不合并,这样做可以减少段合并的频率,其默认值为10。
SegmentSize:指段的实际大小,单位为字节。
minMergeSize:小于这个值的段会被分到一组,这样可以加速小片段的合并。
maxMergeSize:若一个段的文本数量大于此值,就不再参与合并,因为大段合并会消耗更多的资源。
段合并相关的动作
- 对索引中的段进行分组,把大小相近的段分到一组,主要由LogMergePolicyl类来处理。
- 将属于同一分组的段合并成一个更大的段。
在段合并前对段的大小进行了标准化处理,通过logMergeFactorSegmentSize
计算得出,其中,MergeFactor表示一次合并的段的数量,Lucene默认该数量为10;SegmentSize表示段的实际大小。通过上面的公式计算后,段的大小更加紧凑,对后续的分组更加友好。
段分组的步骤
根据段生成的时间对段进行排序,然后根据上述标准化公式计算每个段的大小并且存放到段信息中,后面用到的描述段大小的值都是标准化后的值。
-
在数组中找到最大的段,然后生成一个由最大段的标准化值作为上限,减去LEVEL_LOG_SPAN(默认值为0.75)后的值作为下限的区间。小于等于上限并且大于下限的段,都被认为是属于同一个组的段,可以合并。
-
在确定一个分组的上下限值后,就需要查找属于这个分组的段了,具体过程是:创建两个指针(在这里使用指针的概念是为了更好地理解)start和end,start指向数组的第1个段,end指向第start+MergeFactor个段,然后从end逐个向前查找落在区间的段,当找到第1个满足条件的段时,则停止,并把当前段到start之间的段统一分到一个组,无论段的大小是否满足当前分组的条件。
这样做的好处如下:
1)增加段合并的概率,避免由于段的大小参差不齐导致段难以合并。
2)简化了查找的逻辑,使代码的运行效率更高。
在分组找到后,需要排除不参加合并的“超大”段,然后判断剩余的段是否满足合并的条件,mergeFactor=5,而找到的满足合并条件的段的个数为4,所以不满足合并的条件,暂时不进行合并,继续寻找下一个分组的上下限。
由于在第4步并没有找到满足段合并的段的数量,所以这一分组的段不满足合并的条件,继续进行下一分组段的查找。具体过程是:将start指向end,在剩下的段(从end指向的元素开始到数组的最后一个元素)中寻找最大的段,在找到最大的值后再减去LEVEL_LOG_SPAN的值,再生成一个分组的区间值;然后把end指向数组的第start+MergeFactor个段,逐个向前查找第1个满足条件的段;重复第3步和第4步。
-
如果一直没有找到满足合并条件的段,则一直重复第5步,直到遍历完整个数组。
ELK
ELK是三个开源软件的缩写,分别表示:Elasticsearch , Logstash, Kibana , 它们都是开源软件。新增了一个FileBeat,它是一个轻量级的日志收集处理工具(Agent),Filebeat占用资源少,适合于在各个服务器上搜集日志后传输给Logstash,官方也推荐此工具。
Elasticsearch是个开源分布式搜索引擎,提供搜集、分析、存储数据三大功能。它的特点有:分布式,零配置,自动发现,索引自动分片,索引副本机制,restful风格接口,多数据源,自动搜索负载等。主要负责将日志索引并存储起来,方便业务方检索查询。
Logstash 主要是用来日志的搜集、分析、过滤日志的工具,支持大量的数据获取方式。一般工作方式为c/s架构,client端安装在需要收集日志的主机上,server端负责将收到的各节点日志进行过滤、修改等操作在一并发往elasticsearch上去。是一个日志收集、过滤、转发的中间件,主要负责将各条业务线的各类日志统一收集、过滤后,转发给 Elasticsearch 进行下一步处理。
Kibana 也是一个开源和免费的工具,Kibana可以为 Logstash 和 ElasticSearch 提供的日志分析友好的 Web 界面,可以帮助汇总、分析和搜索重要数据日志。
Filebeat隶属于Beats。目前Beats包含四种工具:
Packetbeat(搜集网络流量数据)
Topbeat(搜集系统、进程和文件系统级别的 CPU 和内存使用情况等数据)
Filebeat(搜集文件数据)
Winlogbeat(搜集 Windows 事件日志数据)
Filebeat工作原理
Filebeat由两个主要组件组成:prospectors 和 harvesters。这两个组件协同工作将文件变动发送到指定的输出中。
1、Harvester(收割机):负责读取单个文件内容。每个文件会启动一个Harvester,每个Harvester会逐行读取各个文件,并将文件内容发送到制定输出中。Harvester负责打开和关闭文件,意味在Harvester运行的时候,文件描述符处于打开状态,如果文件在收集中被重命名或者被删除,Filebeat会继续读取此文件。所以在Harvester关闭之前,磁盘不会被释放。默认情况filebeat会保持文件打开的状态,直到达到close_inactive(如果此选项开启,filebeat会在指定时间内将不再更新的文件句柄关闭,时间从harvester读取最后一行的时间开始计时。若文件句柄被关闭后,文件发生变化,则会启动一个新的harvester。关闭文件句柄的时间不取决于文件的修改时间,若此参数配置不当,则可能发生日志不实时的情况,由scan_frequency参数决定,默认10s。Harvester使用内部时间戳来记录文件最后被收集的时间。例如:设置5m,则在Harvester读取文件的最后一行之后,开始倒计时5分钟,若5分钟内文件无变化,则关闭文件句柄。默认5m)。
2、Prospector(勘测者):负责管理Harvester并找到所有读取源。
Prospector会找到/apps/logs/*目录下的所有info.log文件,并为每个文件启动一个Harvester。Prospector会检查每个文件,看Harvester是否已经启动,是否需要启动,或者文件是否可以忽略。若Harvester关闭,只有在文件大小发生变化的时候Prospector才会执行检查。只能检测本地的文件。
ES用于安全
在传统安全领域,企业通常会借助防火墙,杀毒软件等为企业构造起一套固若金汤的安全防御体系,然而即使在如此严密的防护之下,仍然无法完全保证内部数据的安全,尤其是当面临内部威胁时。这时,根据已有安全数据进行安全分析,及时发现并处理威胁就显得尤为重要。
然而,现代企业的安全数据已随着日益蓬勃发展的信息网络技术而迅速膨胀,对海量安全数据的采集,处理,存储,查询等正日益困扰着企业安全分析团队。
而ES正是为应对海量数据的采集和检索而生的,将ES应用于安全分析领域可以非常便捷高效地解决安全分析领域海量数据的存储和检索问题。
1.数据采集
数据采集是安全分析的第一步,也是至关重要的一步。安全分析所需的数据来源多种多样,有网络数据,也有文件数据,有本机数据还有云端数据。Elastic Stack提供的Beat工具包含了丰富的数据采集工具,可以方便地应用于各种数据采集场景。下表是安全分析中需要采集的各种数据以及ES下对应的采集方式
2.数据标准化
安全分析的数据来源多种多样,不同来源的数据中表示相同含义的字段在名称,类型上各不相同,这就导致了在进行数据检索分析时,为了检索不同数据源中的同类数据,可能要兼容性地写多个查询条件,这给数据分析带来了不小的麻烦。
在实际的安全分析中为了兼容来自不同数据源的数据。
为了解决这个问题,Elastic建立ECS(Elastic Common Schema)项目,采用专业的分类方法,对字段进行统一命名,且Elastic生态相关组件均遵循这一命名规格,使得对不同数据源的检索得以简化。
3.数据增强
除了对数据进行标准化以外, Elastic Stack中的Beat、Logstash、Elasticsearch等组件都可以对数据进行增强,根据现有环境或数据,通过数据处理衍生出一些相关数据,为安全分析提供更为直接详细的信息。
Beats https://www.elastic.co/guide/en/beats/filebeat/6.4/filtering-and-enhancing-data.html
Logstash https://www.elastic.co/guide/en/logstash/6.4/filter-plugins.html
Elasticsearch https://www.elastic.co/guide/en/elasticsearch/reference/master/ingest-processors.html