1. 首页
  2. 系统面经
  3. 一周速通字节,抖音直播后端面筋分享

一周速通字节,抖音直播后端面筋分享

  • 发布于 2024-11-06
  • 11 次阅读

ES为什么实现近实时搜索?

Elasticsearch(ES)能够实现近实时(Near Real-Time, NRT)搜索的关键在于其高效的数据索引和刷新机制。以下是其核心原理:

1. 写入分段的倒排索引

当数据写入 ES 时,它会首先被写入内存的缓存中,而不是直接存储在磁盘上。这些写入的数据被组织成倒排索引的形式,以便后续可以快速搜索。这种倒排索引结构保证了对关键词的快速定位和搜索效率。

2. 刷新机制(Refresh)

ES 的刷新机制是实现近实时搜索的关键之一。ES 默认每隔 1 秒执行一次“刷新”操作,这个操作会将内存中的数据缓存(也称为 buffer​)写入一个新的磁盘段(segment)文件,并在此时生成一个新的倒排索引。这些 segment 文件一旦写入磁盘,就变成了只读的,并立即可以被查询。正是这种定时的刷新机制,使得新写入的数据能够被快速搜索到。

近实时(NRT)的原因:刷新并不是实时的,因为写入磁盘和生成倒排索引都需要一些时间(默认 1 秒的延迟)。因此,ES 并不是绝对实时,而是“近实时”搜索。

3. Segment 合并

每次刷新都会生成一个新的 segment 文件,长期积累下去会导致文件数量过多。为了保持查询效率,ES 会定期合并这些小 segment 成为更大的文件。这种合并不会影响搜索的实时性,但可以减少文件碎片,提升查询性能。

4. Translog 保证数据安全

在写入数据时,ES 还会将数据写入一个称为 translog​ 的事务日志中,以确保数据的可靠性和持久性。这意味着即使发生崩溃或宕机,ES 也可以从 translog​ 恢复数据。不过,translog​ 并不会影响实时性,因为它是为数据恢复准备的,不参与搜索查询。

5. Lucene 提供的高效索引支持

ES 底层使用 Lucene,Lucene 本身是为全文检索优化的库。Lucene 的索引机制和高效的查询算法,使得在小粒度上快速生成倒排索引成为可能,支持了 ES 的近实时性。

6. 缓存和异步更新

ES 会对一些热数据进行缓存,通过内存缓存提升响应速度。搜索请求的执行会利用这些缓存,因此即使数据在刷新时存在微小延迟,也能在大多数情况下保持极低的查询延迟。异步的刷新和缓存机制帮助 ES 实现了高效的近实时搜索体验。

总结

综上所述,ES 近实时搜索的实现主要依赖于刷新机制、segment 分段索引、异步的 translog 记录、以及 Lucene 提供的高效倒排索引结构。这些设计相互配合,使得 ES 能够在保证数据安全性的同时,提供快速的数据搜索。

Elasticsearch(ES)实现近实时搜索的原因主要有以下几点:

  1. 数据写入与刷新(Refresh):在ES中,数据写入并不是直接添加到现有的倒排索引中。新数据首先被写入内存缓冲区(In-Memory Buffer),然后定期或按需将缓冲区中的数据合并成一个新的索引段(Index Segment),这个过程称为刷新(Refresh)。默认情况下,ES每秒自动执行一次刷新操作,这意味着大部分情况下,数据写入后最多等待1秒即可被搜索到。

  2. 索引段(Segment)与倒排索引:Elasticsearch基于Lucene,而Lucene使用segment(分段)来存储数据。一条记录要被搜索到,必须写入到segment中。由于新插入的数据首先写入内存缓冲区,然后通过刷新操作写入到segment,这就导致了数据不是立即可见,而是在下一次刷新后变得可搜索,因此ES是近实时的。

  3. Translog(事务日志)与可靠性:为了保证数据的可靠性,ES使用translog来记录所有的操作,我们称之为write-ahead-log。新增了一条记录时,ES会把数据写到translog和in-memory buffer中。translog是实时fsync的,也即写入ES的数据,其对应的translog内容是实时写入磁盘的,并且是以顺序append文件的方式,所以写磁盘的性能很高。只要数据写入translog了,就能保证其原始信息已经落盘,进一步就保证了数据的可靠性。

  4. 提交点(Commit Point)与数据可见性:在Lucene中,一个索引由一堆Segment的集合加上一个提交点组成。提交点记录着所有已知的Segment。对于一个Lucene index的组成,只有当文档变更到达文件系统缓存中,并且有一个更新的提交点之后,这些变更才可以体现在搜索结果中。

  5. Flush与数据持久化:ES中还有一个操作叫做flush,它会清空translog,并且将虚拟文件系统中的段文件fsync刷盘,这个过程会将内存中的数据持久化到磁盘上,进一步确保数据的安全性。

  6. 近实时搜索的权衡:ES通过内存缓冲区和文件系统缓存的引入,实现了批量变更到文件系统缓存,批操作可以提高性能。文件系统缓存的引入是为了解决fsync的高昂代价(随机I/O),如果每次索引一个文档都去执行一次fsync(随机I/O)的话会造成很大的性能问题,比如:文档变更更加耗时,以及需要更长时间才能体现在搜索结果中。

综上所述,Elasticsearch通过这些机制实现了近实时搜索,即在数据写入后极短的时间内(通常在毫秒至秒级别)使其可被搜索到,虽然不是绝对意义上的实时,但对于大多数应用而言已经足够接近实时,从而满足了对快速响应和数据更新即时可见性的需求。

  1. 什么是 ES 的近实时搜索

    • Elasticsearch(ES)是一个分布式、RESTful 风格的搜索和数据分析引擎。它的近实时搜索是指在索引文档后,文档几乎可以立刻被搜索到。通常来说,从文档被索引到能够被搜索到,会有一个短暂的延迟(通常是一秒以内)。

  2. ES 实现近实时搜索的原理

    • 数据写入机制

      • 内存缓冲区(Buffer):当数据写入 ES 时,首先会进入内存缓冲区。这个缓冲区就像是一个临时的存储区域,新数据会在这里累积。例如,当有大量的日志数据源源不断地写入 ES 时,它们会先暂存在这个缓冲区。

      • 刷新(Refresh)操作:ES 会定期将内存缓冲区的数据刷新到文件系统缓存(OS Cache)中,这个过程被称为刷新操作。默认情况下,ES 每隔 1 秒自动执行一次刷新操作。这就像是一个定时的 “搬运工”,把内存缓冲区的数据搬运到文件系统缓存。这样,数据一旦被刷新到文件系统缓存,就可以被搜索到了,这是实现近实时搜索的关键步骤。

    • 数据读取机制与查询缓存

      • 查询缓存(Query Cache):ES 还会对经常执行的查询进行缓存。当相同的查询再次被执行时,它可以直接从缓存中获取结果,而不需要重新执行一遍查询操作。这进一步加快了搜索的速度,提升了近实时搜索的用户体验。例如,如果有一个查询是统计过去一小时内某个网站的访问量,这个查询被频繁执行,ES 会缓存这个查询的结果,当下次还需要这个统计数据时,直接从缓存中获取,减少了查询的响应时间。

    • 分布式架构优势

      • ES 是一个分布式系统,它由多个节点组成。数据在这些节点之间进行分布式存储和处理。这种架构使得搜索请求可以在多个节点上并行处理。例如,当一个大型的电商网站进行商品搜索时,搜索请求可以同时在多个 ES 节点上进行处理,每个节点负责一部分数据的搜索,然后将结果汇总。这样可以利用多个节点的计算资源,快速返回搜索结果,实现近实时搜索的高效性。

  3. 应用场景体现近实时搜索的必要性

    • 日志分析:在大型系统的日志分析中,运维人员需要及时了解系统的运行状态。例如,当系统出现故障时,通过近实时搜索,可以快速在大量的日志数据中找到相关的错误信息,及时定位和解决问题。如果搜索不是近实时的,可能会导致问题发现和解决的延迟,影响系统的稳定性。

    • 实时监控:对于一些实时监控系统,如网站流量监控、服务器性能监控等。通过 ES 的近实时搜索,可以实时获取最新的数据指标并进行分析。比如,网站管理员可以通过近实时搜索查看当前网站的访问人数、访问来源等信息,以便及时调整网站的运营策略。

2、在查询页表前面一步是什么?程序运行到内存的过程是什么?

在计算机系统中,程序运行到内存的过程可以分为几个主要步骤,其中查询页表是虚拟内存管理中的一部分。以下是程序从磁盘加载到内存并执行的一般过程:

  1. 编译:源代码(如C/C++代码)被编译器编译成机器码或中间代码。

  2. 链接:编译后生成的目标文件(.o文件)通过链接器链接成可执行文件(.exe文件或ELF文件等)。

  3. 加载:操作系统的加载器(Loader)将可执行文件加载到内存中。这个过程中,可执行文件被映射到进程的地址空间。

  4. 重定位:如果可执行文件中包含动态链接库(DLLs/SOs)的引用,操作系统会解析这些引用,并在内存中定位这些库。

  5. 内存分配:操作系统为程序分配内存空间,包括代码段、数据段、堆和栈。

  6. 页表创建:操作系统为每个进程创建页表,用于将虚拟地址映射到物理地址。

  7. 查询页表:在程序执行过程中,每当访问内存时,CPU都会使用页表将虚拟地址转换为物理地址。

  8. 缺页处理:如果请求的页面不在物理内存中,会触发缺页中断,操作系统会从磁盘中加载所需的页面到内存中,并更新页表。

  9. 指令执行:CPU开始执行加载到内存中的指令。

  10. I/O操作:程序可能需要进行输入输出操作,如读取文件或显示图形界面。

  11. 程序结束:程序执行完毕后,操作系统会清理分配的资源,包括内存、文件描述符等。

在查询页表的前一步通常是内存访问,即程序尝试访问内存中的某个地址。这个地址是虚拟地址,CPU通过查询页表将这个虚拟地址转换为物理地址,然后访问物理内存中的数据。如果该页面不在物理内存中,就会发生缺页,操作系统需要从磁盘中加载该页面到内存中。这个过程是虚拟内存管理的核心部分,它允许程序使用比物理内存更大的地址空间,并且有效地管理内存资源。

在理解程序运行到内存的过程以及页表查询之前,需要先了解程序加载的基本过程。以下是从程序运行到进入内存的详细步骤:

1. 程序编译与生成可执行文件

编写的源代码(如 C/C++)需要经过编译器编译,生成可执行文件(如 ELF 文件)。这个可执行文件包含了程序的代码段、数据段、堆栈段等信息,编译器在生成这些段时已经考虑了程序的逻辑地址(虚拟地址)。

2. 加载器(Loader)加载程序

操作系统中的加载器负责将可执行文件加载到内存。加载器会读取可执行文件的各个段信息,并在操作系统分配的地址空间中为其分配相应的虚拟地址,同时在页表中初始化相应的映射关系。

3. 分配虚拟地址空间

当程序被加载时,操作系统会为其创建一个独立的虚拟地址空间。在现代操作系统中,每个进程都有自己的独立虚拟地址空间,保证进程之间的隔离。虚拟地址空间被划分为代码段、数据段、堆段和栈段等部分,这些部分的虚拟地址是逻辑地址,尚未映射到物理内存。

4. 页表创建

虚拟地址空间需要通过页表映射到物理内存。当进程启动时,操作系统会为该进程分配一个空的页表结构,但不会立即为所有虚拟地址分配物理内存,而是采用按需分配(Lazy Allocation)的策略。只有当进程真正访问某一部分内存时,才会分配实际的物理页并更新页表。

5. CPU 访问虚拟地址,触发页表查询

当程序执行指令或访问内存数据时,CPU 会生成一个虚拟地址。首先,通过 CPU 的内存管理单元(MMU),该虚拟地址会被翻译为物理地址。在这个过程中,MMU 会查找页表,以确定该虚拟地址是否有对应的物理地址。

6. TLB 缓存:页表查询的优化

在查询页表之前,MMU 会先查找翻译后备缓冲区(Translation Lookaside Buffer, TLB),这是一个小型的缓存,用于存储最近访问的虚拟地址到物理地址的映射。若 TLB 命中,则无需查询页表,直接得到物理地址。

7. 页表查询

如果 TLB 未命中,MMU 将会查询页表。页表是一个层级结构(如二级页表或四级页表),用于将虚拟地址映射到物理地址。在查询页表时,MMU 会分多级解析页表项,直到找到与该虚拟地址对应的物理页。

8. 缺页异常(Page Fault)

若页表中没有该虚拟地址的物理页记录,操作系统会触发缺页异常。在缺页异常处理过程中,操作系统会为该虚拟地址分配新的物理页,并更新页表,同时刷新 TLB,以便后续可以直接命中。

9. 物理地址访问

在页表成功查询并获得物理地址后,MMU 将其提供给 CPU,CPU 可以访问物理内存上的数据。

总结

  • 查询页表前的步骤包括程序编译、加载到虚拟地址空间、页表创建、TLB 查找等。

  • 页表查询本身则是通过 MMU 从虚拟地址空间到物理地址空间的映射过程,通常在 TLB 缓存未命中的情况下触发。

这套机制使得程序可以在独立的虚拟地址空间中运行,而操作系统通过页表和 TLB 确保内存的有效利用和安全性。

在查询页表前面一步通常是内存管理单元(MMU)接收来自处理器的虚拟地址。具体过程如下:

  1. 程序运行时,处理器产生的是虚拟地址,因为在使能了分页机制的处理器中,处理器不能直接访问物理地址,只能访问虚拟地址空间。

  2. 这个虚拟地址随后被发送给内存管理单元(MMU),MMU 负责将虚拟地址转换为物理地址。而要完成这个转换,就需要查询页表来获取虚拟页帧号(VPN)对应的物理页帧号(PFN)。所以 MMU 接收虚拟地址是查询页表的前一步骤。

程序运行到内存的过程主要包括以下步骤:

  1. 加载(Loading):

    • 映射 Elf 文件到内存:操作系统的加载器读取可执行程序(如 Elf 文件格式的程序)的头部信息,确定程序各个段(如代码段、数据段等)的位置和大小,然后将这些段映射到内存中相应的位置。每个段都有特定的起始地址和长度。

    • 重定位(Relocation):由于程序在编译时所使用的地址与实际加载到内存中的地址可能不同,所以需要进行重定位。加载器会根据程序中的重定位表更新相应的地址,使得程序中的指令和数据能够正确地指向内存中的目标位置。

    • 设置内存保护:根据程序文件中各个段的属性,加载器为相应的内存区域设置保护属性。例如,代码段通常设置为只读,以防止程序意外修改代码;数据段设置为可读写,以便程序能够正常地读写数据。

  2. 链接(Linking):

    • 查找动态库:如果程序依赖于动态库,动态链接器会根据程序文件中的动态链接表找到所需的动态库,并将它们加载到内存中。

    • 重定位动态库中的符号:动态链接器更新程序中的符号引用,使其指向动态库中的正确位置,以便程序能够正确地调用动态库中的函数和访问数据。

    • 解决依赖关系:确保程序所依赖的所有动态库都被正确加载,并且解决动态库之间的依赖关系,例如一个动态库可能依赖于另一个动态库,需要保证依赖关系的正确建立。

  3. 初始化(Initialization):

    • 初始化全局变量:操作系统加载器将数据段中已初始化的全局变量加载到内存中,并将未初始化的全局变量所在的内存区域清零。

    • 执行构造函数:如果程序使用了面向对象编程语言,并且在程序中有全局构造函数(如 C++ 中的静态构造函数),这些构造函数会在程序启动时被执行,以完成对象的初始化工作。

    • 执行初始化代码:程序中可能包含一些特定的初始化代码块,这些代码块会在程序启动时被执行,用于完成程序的一些初始化操作,例如设置初始状态、打开文件等。

  4. 执行(Execution):

    • 跳转到程序入口点:加载器将程序的执行指针设置到程序的入口点,例如 _start​ 函数(在 C 和 C++ 程序中)或其他指定的入口函数。

    • 执行主程序:程序从入口点开始执行,然后进入主函数(如 C 和 C++ 中的 main​ 函数),主函数中的代码开始被执行,程序正式开始运行。