Elasticsearch 为何如此之快?深度剖析其性能奥秘

嘿,大家好!现在这数据量啊,简直是爆炸式增长,想从海量信息里头快速找到点啥,那可真是个大挑战。不过呢,有这么个家伙,叫Elasticsearch,它可是个搜索和分析的“高手”,速度快得惊人,而且还能做到“准实时”分析,怪不得全世界的程序员都爱它!你是不是也好奇,为啥它处理起PB级的数据还能像闪电一样快呢?

今天啊,我就以一个Elasticsearch“magic”的身份,带你好好扒一扒它速度飞快的秘密,从最基础的原理,到它怎么跑起来的,再到那些能让它更快的“小技巧”,咱们由浅入深,一步步聊清楚!

一:速度的“秘密武器”——倒排索引

要说Elasticsearch为啥这么快,它藏着的核心“秘密武器”就是——倒排索引(Inverted Index)

想想看,你手头有本特别厚的书,想赶紧找到所有提到“人工智能”的页码。你会咋办?最简单的法子就是直接翻到书后面,找那个“索引”部分,找到“人工智能”这词儿,然后它就会告诉你所有出现这词儿的页码列表。倒排索引的工作原理,嘿,跟这简直一模一样!

1.1 倒排索引:全文搜索的“魔法”

跟咱们平时用的那种关系型数据库不一样(它们是按行或记录ID来建索引的),倒排索引呢,它把“词儿”(Term)和包含这些词儿的“文档列表”给对应起来。当文档被“塞”进索引的时候,Elasticsearch会先对它进行一堆文本分析,比如:

  • 分词: 把一句话拆成一个个独立的词儿。

  • 小写转换: 把所有字母都变成小写,这样“Apple”和“apple”就被看成一个词啦。

  • 词干提取: 把单词还原成它的“老家”,比如“running”、“ran”和“runs”最后都会变成“run”。

这些处理完的词儿,还有它们在文档里的位置、出现的次数,都会被好好地存到倒排索引里头。

倒排索引 vs. B-Tree 索引:谁更牛?

为了让你更明白倒排索引有多厉害,咱们拿它跟传统数据库里常用的B-Tree索引比一比:

特性倒排索引 (Inverted Index)B-Tree 索引 (B-Tree Index)

主要用途

全文搜索

精确查找和范围查询

数据结构

词儿直接指向文档ID列表

平衡树结构,数据排得整整齐齐

全文搜索速度

简直了!直接通过词儿就能找到文档

慢点儿,得扫描或者用复杂的匹配方式

精确查找速度

需要额外处理,没B-Tree那么直接高效

超快!顺着树就能找到

组合能力

强!能独立索引好几个字段,运行时组合也灵活

弱点儿,多列查询可能需要复合索引,而且顺序还挺重要

Elasticsearch就是靠着这种“词儿直接到文档”的映射方式,省去了传统数据库在全文搜索时需要扫描一大堆文档的麻烦,所以才能像“谷歌”一样,搜索起来嗖嗖的快!

代码小例子:倒排索引怎么用?

下面就是Elasticsearch怎么用倒排索引来存文档和做全文搜索的简单例子:

# 1. 新建一个索引,就像新建一个文件夹一样
PUT /my_blog_posts

# 2. 往里头放两篇博客文章
PUT /my_blog_posts/_doc/1
{
  "title": "Elasticsearch Speed Secrets",
  "content": "Elasticsearch uses inverted index for lightning-fast full-text search."
}

PUT /my_blog_posts/_doc/2
{
  "title": "Understanding Inverted Index",
  "content": "The inverted index is a fundamental concept in modern search engines."
}

# 3. 来个全文搜索查询,看看效果!
# 搜搜看有没有包含 "search" 的文章
GET /my_blog_posts/_search
{
  "query": {
    "match": {
      "content": "search"
    }
  }
}
# 猜猜看结果?会把文档1和文档2都找出来,因为它们的content里都有"search"相关的词儿。

# 再搜搜看有没有包含 "speed" 的文章
GET /my_blog_posts/_search
{
  "query": {
    "match": {
      "title": "speed"
    }
  }
}
# 这次呢?只会返回文档1哦。

当你敲下match查询的时候,Elasticsearch会立马用倒排索引,飞快地定位到那些包含“search”或“speed”这些词儿的文档,根本不用一篇篇地去翻所有文章,是不是很酷?

二:性能“加速器”——Lucene段与准实时搜索

Elasticsearch之所以这么牛,可不光是靠倒排索引,它底层依赖的Apache Lucene库设计得也超赞,特别是它那个“段”的概念和“准实时”搜索的实现,简直是神来之笔!

2.1 Lucene段与“不变性”:写入和搜索的“平衡术”

Lucene把数据存放在一个个叫做“段”(Segments)的、不可更改的文件组里。每个段呢,都是一个独立的、可以搜索的索引,但它只包含了整个索引里头一部分文档。

段的特点:就是“不变”!

一旦一个段被写到硬盘上,它就不能再改了。这意味着:

  • 更新或删除文档: 你想改或者删文档?它不会直接去动那些已经存在的段。Elasticsearch会创建新的段来记录这些变化,或者在老段里把旧文档标记成“已删除”(就是逻辑上删掉,但物理文件还在)。

  • 缓存效率高: 因为段是“不变”的嘛,它的数据就可以放心地被缓存起来,不用担心数据会过期。Elasticsearch能特别好地利用操作系统的文件系统缓存,来加速数据访问,棒呆了!

  • 准实时搜索: 新创建的段可以马上被“打开”并用于搜索,不用等那些耗时的大规模重新索引操作,是不是很方便?

段合并策略:优化和“垃圾回收”

随着数据不断地被索引,小段会越来越多。不过呢,在太多小段里头搜索效率可就不高了。为了让段的数量保持在一个合理的范围,并且优化搜索性能,Elasticsearch会在后台悄悄地把那些小段合并成更大的段。在这个合并过程中,那些被标记为“已删除”的文档占用的空间就会被“回收”掉,这样既能释放磁盘空间,又能提高搜索效率,一举两得!

图解:Lucene段合并过程

2.2 准实时搜索(NRT):刷新机制的“魔力”

Elasticsearch之所以能提供“准实时”的搜索能力,全靠它那个独特的刷新(Refresh)机制

新索引的文档呢,会先跑到内存缓冲区里,同时这些操作也会被记录到事务日志(translog)里,这样就能保证数据不会丢。刷新操作会定期地(默认每秒一次)把内存缓冲区里的文档写到一个新的Lucene段里,然后这个新段就能马上被搜索到了。这个新段是“轻量级”的,可以立刻被“打开”并用于搜索,不用等整个数据都完全提交到磁盘。

这种机制让数据在被索引之后几乎能立刻被搜到,简直是“准实时”的体验!当然啦,刷新操作也是有开销的,所以Elasticsearch允许你调整刷新间隔,来平衡搜索的“新鲜度”和索引数据的“吞吐量”。

代码小例子:调整刷新间隔

# 调整索引的刷新间隔
# 如果你的写入操作特别多,可以把刷新间隔调大一点,比如设成30秒,这样能减少索引的开销。
PUT /my_log_index/_settings
{
  "index.refresh_interval": "30s"
}

# 如果你在进行大量数据的批量索引,可以暂时把自动刷新关掉,这样能把写入速度提到最高!
PUT /my_bulk_index/_settings
{
  "index.refresh_interval": "-1"
}

# 等批量索引搞定后,手动触发一次刷新,让所有数据都能被搜到。
POST /my_bulk_index/_refresh

三:横向扩展和高可用——分布式架构,真香!

Elasticsearch的另一个关键优势就是它天生就是个“分布式”的架构,这让它能轻松地进行横向扩展,而且还特别“皮实”,不容易挂掉!

2.3 分片 (Shards):数据分散着放,大家一起干活!

Elasticsearch把一个大索引切成了好多个小块块,每个小块块都叫一个分片(shards),每个分片都是一个独立的Lucene索引实例。分片啊,就是Elasticsearch实现数据“分散着放”和“并行处理”的基石!

通过把一个索引的文档分散到好几个分片上,再把这些分片分布到集群里不同的机器上,Elasticsearch就能让索引和搜索操作同时进行。比如,一个搜索请求来了,协调节点就会把它分发给所有相关的分片,大家一起并行执行,这样查询速度就蹭蹭地往上涨啦!

2.4 副本 (Replicas):不怕挂,还能多读!

副本分片呢,就是主分片的“克隆体”,它们主要有两个用处:

  1. 高可用性: 要是某个机器或者主分片不幸“挂掉”了,Elasticsearch会自动把一个副本分片“扶正”成新的主分片,保证数据一直都在,不会因为某个点坏了就全完蛋。

  2. 读扩展: 搜索请求可以在主分片和它所有的副本分片上同时执行。这意味着,你多加几个副本,就能有效地提升集群的读取能力,让更多人同时搜索,速度还是一样快!

图解:分片和副本怎么分布?

图清楚地展示了Elasticsearch集群里分片和副本是怎么分布的。主分片分散在不同的节点上,而它们的副本呢,则被放在了跟对应主分片不同的节点上,这样就能保证数据有备份,而且还不容易挂掉!

代码小例子:创建带分片和副本的索引

# 来,创建一个叫 'my_products_index' 的索引,给它3个主分片,再加1个副本分片
PUT /my_products_index
{
  "settings": {
    "number_of_shards": 3, # 主分片数量
    "number_of_replicas": 1 # 副本分片数量
  },
  "mappings": {
    "properties": {
      "name": { "type": "text" },
      "price": { "type": "float" },
      "category": { "type": "keyword" }
    }
  }
}

四:高级性能优化——精细控制,榨干性能!

懂了底层原理是基础,但要想真正把Elasticsearch的性能发挥到极致,你还得掌握一些高级的优化“小窍门”!

4.1 索引数据流:高效写入的“独门秘籍”

  • 文档路由 (_routing): 你可以给文档自定义一个_routing值,这样就能把那些相关的文档(比如,同一个用户的所有数据)都强制地“塞”到同一个分片上。这在多租户的场景里特别有用,能大大减少搜索时的“扇出”(fan-out)操作,因为查询可以直接发给包含相关数据的特定分片,查询效率自然就提升了!

  • Bulk API: 如果你要写入大量数据,Elasticsearch提供了Bulk API,它能让你在一个请求里搞定好几个索引、创建、删除和更新操作。这能极大地提高数据写入的速度,还能分摊网络来回跑的开销和硬盘读写成本,简直是批量操作的福音!

代码小例子:自定义路由和Bulk API怎么用?

自定义路由索引文档:

# 1. 创建一个要求自定义路由的索引,就像给数据分个区
PUT /my_tenant_data
{
  "settings": {
    "number_of_shards": 3,
    "number_of_replicas": 1
  },
  "mappings": {
    "_routing": {
      "required": true # 强制要求你提供路由值哦!
    },
    "properties": {
      "tenant_id": { "type": "keyword" },
      "content": { "type": "text" }
    }
  }
}

# 2. 索引文档的时候,指定好自定义路由值(比如,用 tenant_id 作为路由键)
PUT /my_tenant_data/_doc/doc1?routing=tenantA
{
  "tenant_id": "tenantA",
  "content": "This document belongs to Tenant A."
}

# 3. 搜索的时候也指定路由值,这样就能把搜索范围限制到特定的分片上,更快!
GET /my_tenant_data/_search?routing=tenantA
{
  "query": {
    "match": {
      "content": "document"
    }
  }
}

Bulk API使用:

POST /_bulk
{ "index": { "_index": "my_logs", "_id": "1" } }
{ "timestamp": "2024-01-01T10:00:00Z", "message": "User login successful" }
{ "create": { "_index": "my_logs", "_id": "2" } }
{ "timestamp": "2024-01-01T10:01:00Z", "message": "Application started" }
{ "update": { "_index": "my_logs", "_id": "1" } }
{ "doc": { "status": "processed" } }

4.2 查询执行流:高效检索的“艺术”

Elasticsearch的查询速度可不光取决于索引结构,还得看它那高效的查询执行流程!

查询协调节点与分发:大家一起找!

Elasticsearch的搜索请求通常会经历一个两阶段的过程,非正式地叫做“散播-收集”(scatter-gather)模式:

  1. 散播阶段: 你的请求会先发给集群里的任意一个节点(这个节点就是“协调节点”),协调节点会把查询“广播”给所有相关的分片,让它们并行执行。

  2. 收集阶段: 各个分片返回一些轻量级的结果后,协调节点会把这些结果收集起来,进行聚合、排序、合并,然后向那些最终拥有这些结果文档的分片发送请求,把完整的文档内容取回来,最后再返回给你。

图姐:查询执行流程

查询上下文 vs. 过滤上下文:性能和相关性,我都要!

Elasticsearch的查询语言允许你在两种“上下文”里执行查询:

  • 查询上下文 (Query Context): 这个是用来找相关文档,并且计算“相关性分数”的(比如,match查询)。它会告诉你这个文档有多符合你的查询。

  • 过滤上下文 (Filter Context): 这个就简单粗暴了,只回答“文档符不符合?”这种是/否的问题,不计算什么相关性分数。过滤查询在性能上可是有大优势的:它们跑得更快、会自动缓存,而且还更省CPU资源!所以啊,对于那些不需要评分的条件(比如日期范围、精确值匹配),咱们就优先用过滤上下文,性能杠杠的!

Doc Values和Fielddata:排序和聚合的“内存魔法”

为了支持排序和聚合操作,Elasticsearch需要快速访问字段的值。

  • Doc Values: 这是一种在索引的时候就建好的、基于磁盘的“列式数据结构”。它把数据按列存起来,对于排序、聚合和脚本访问来说效率超高,大多数字段类型(除了text字段)默认都是开启的。

  • Fielddata: 以前呢,text字段要排序和聚合就得靠它。但是!它会把数据加载到JVM的堆内存里,对于那些“值”特别多的text字段,可能会占用大量内存,甚至直接把内存搞爆!所以啊,除非万不得已,一般不建议对text字段开启fielddata,最好还是考虑用keyword类型,然后利用它的doc_values

代码小例子:Query DSL里怎么用Filter Context,Doc Values怎么配?

# Query DSL里怎么用Filter Context:
GET /products_v2/_search
{
  "query": {
    "bool": {
      "must": [
        { "match": { "description": "wireless headphones" } } # 查询上下文:这个会算相关性分数哦!
      ],
      "filter": [
        { "range": { "price": { "gte": 50, "lte": 200 } } }, # 过滤上下文:不计算分数,可以缓存,更快!
        { "term": { "category.keyword": "electronics" } } # 过滤上下文:不计算分数,可以缓存,更快!
      ]
    }
  }
}

# Doc Values配置:
PUT /my_logs_index
{
  "mappings": {
    "properties": {
      "timestamp": { "type": "date" },
      "session_id": { "type": "keyword", "index": false }, # 不创建倒排索引,但 doc_values 默认是开着的
      "event_type": { "type": "keyword" }
    }
  }
}

4.3 映射(Mapping)优化:数据结构决定性能!

Elasticsearch的映射啊,就是定义了文档里每个字段的数据类型,还有它怎么被索引和存储,这直接影响着搜索和分析的速度!

  • Text vs Keyword:

    • text字段呢,是用来存全文内容的,它会被分析(就是分词、小写那些),特别适合全文搜索。

    • keyword字段呢,是用来存结构化内容的(比如ID、标签),它不会被分析,特别适合精确匹配、过滤、排序和聚合。

    • 如果想同时支持全文搜索和精确匹配,可以用多字段 (Multi-fields),把同一个字段既索引成text类型(用来全文搜索),又索引成keyword子字段(用来精确匹配和聚合),比如product_nameproduct_name.keyword

  • 显式映射 (Explicit Mapping): 强烈建议在生产环境里用这个!通过提前定义好每个字段的数据类型和索引方式,就能精确控制数据怎么被解释和索引,避免动态映射可能带来的性能问题。

代码小例子:显式映射定义,Text/Keyword多字段怎么玩?

PUT /products_v2
{
  "mappings": {
    "properties": {
      "product_name": {
        "type": "text", # 这个用来全文搜索
        "fields": {
          "keyword": {
            "type": "keyword", # 这个用来精确匹配和聚合
            "ignore_above": 256 # 超过256个字符的关键字值就忽略掉
          }
        }
      },
      "description": {
        "type": "text",
        "analyzer": "english" # 用英文分析器来处理英文内容
      },
      "price": { "type": "float" },
      "category": { "type": "keyword" },
      "created_at": { "type": "date" }
    }
  }
}

# 用 keyword 子字段来精确匹配或聚合
GET /products_v2/_search
{
  "query": {
    "term": {
      "category.keyword": "Electronics" # 精确匹配 "Electronics"
    }
  },
  "aggs": {
    "products_by_category": {
      "terms": {
        "field": "category.keyword",
        "size": 10
      }
    }
  }
}

4.4 缓存机制:少算点,更快点!

Elasticsearch用了好几种缓存机制来加速操作,比如:

  • 节点请求缓存 (Node Request Cache): 这个会缓存filter上下文里查询的结果。

  • 文件系统缓存 (File System Cache): 这是操作系统层面的缓存,会把那些经常访问的Lucene段文件保存在内存里,Elasticsearch特别依赖它来提升性能。

所以啊,合理利用过滤上下文、优化时间过滤器,还有监控缓存的使用情况,都是提升性能的有效办法哦!

4.5 别名(Aliases)和生命周期管理(ILM):灵活又高效!

  • 别名 (Aliases): 别名呢,就是一个或多个索引的“小名儿”。它能让你的应用程序通过一个别名,无缝地从一个索引切换到另一个索引,实现数据版本升级或者重新索引的时候,完全不用停机!

  • 索引生命周期管理 (ILM): ILM就是自动化管理索引“寿命”的工具,对时间序列数据特别管用。它能让你定义一些策略,自动执行索引的创建、滚动、收缩、强制合并和删除等操作,这样就能优化性能,还能省存储成本,把数据从“热”阶段(高性能硬件)移到“温”、“冷”阶段(低成本存储),棒不棒!

代码小例子:别名怎么操作?

# 1. 把别名 'my_data_alias' 指向 my_data_v1
POST /_aliases
{
  "actions": [
    { "add": { "index": "my_data_v1", "alias": "my_data_alias" } }
  ]
}

# 2. 假设数据已经从 my_data_v1 重新索引到 my_data_v2 了,现在来个“原子切换”别名!
POST /_aliases
{
  "actions": [
    { "remove": { "index": "my_data_v1", "alias": "my_data_alias" } }, # 把旧的别名关系移除
    { "add": { "index": "my_data_v2", "alias": "my_data_alias" } } # 添加新的别名关系
  ]
}
# 你的应用程序根本不用改代码,继续查询 /my_data_alias 就行,但现在访问的已经是 my_data_v2 里的数据啦!

总结:

Elasticsearch之所以能跑得这么快,那可不是随便说说的!它靠的是倒排索引这个核心数据结构,还有基于Lucene的段机制(包括它的“不变性”、刷新和合并策略)带来的准实时能力,再加上分布式架构(分片和副本)提供的横向扩展和高可用性,还有3级缓存,如页 缓存(OS)、分页请求缓存,查询缓存,这些东西加起来,才有了它今天的速度!

除此之外呢,自定义路由Bulk API查询上下文和过滤上下文的区分Doc Values的应用、映射优化缓存机制,以及**别名和索引生命周期管理(ILM)**这些高级策略,都给用户提供了更精细的控制和持续优化的空间。这些机制通力合作,确保了Elasticsearch在高并发、大数据量的场景下,依然能提供又快又稳的搜索和分析服务。

随着数据量继续“蹭蹭”地往上涨,大家对实时分析的需求也越来越高,Elasticsearch的这些核心设计原则肯定会继续支撑它在搜索、日志分析、安全智能这些领域里保持领先地位。所以啊,深入理解这些底层原理,绝对是你构建高性能、可伸缩Elasticsearch解决方案的关键!

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

nextera-void

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值