Elasticsearch 进阶实战
索引生命周期管理(ILM)
Hot-Warm-Cold-Frozen 阶段架构
索引生命周期管理(Index Lifecycle Management, ILM)是 Elasticsearch 内置的索引自动化管理机制,支持根据索引的时效性自动在不同存储阶段之间流转。典型的 ILM 包含四个阶段:
| 阶段 | 名称 | 硬件建议 | 典型操作 | 说明 |
|---|---|---|---|---|
| Hot | 热阶段 | SSD/NVMe | rollover | 承载高频写入和查询,数据实时性最高 |
| Warm | 温阶段 | SATA SSD | shrink, forcemerge | 查询频率降低,不再写入,合并段减少内存开销 |
| Cold | 冷阶段 | HDD | searchable snapshot | 极少查询,可接受秒级延迟,降低存储成本 |
| Frozen | 冻结阶段 | 对象存储(S3/MinIO) | searchable snapshot | 几乎不查询,仅归档保留,可手动恢复 |
Rollover 滚动索引
rollover 是 ILM 的核心触发机制,当索引满足指定条件(文档数、大小、时长)时自动创建新索引,并将别名指向新索引。
PUT /_ilm/policy/logs_policy
{
"policy": {
"phases": {
"hot": {
"min_age": "0ms",
"actions": {
"rollover": {
"max_size": "50GB",
"max_age": "1d",
"max_docs": 10000000
},
"set_priority": {
"priority": 100
}
}
},
"warm": {
"min_age": "7d",
"actions": {
"shrink": {
"number_of_shards": 1
},
"forcemerge": {
"max_num_segments": 1
},
"allocate": {
"number_of_replicas": 1
}
}
},
"cold": {
"min_age": "30d",
"actions": {
"searchable_snapshot": {
"snapshot_repository": "my_backup"
},
"set_priority": {
"priority": 0
}
}
},
"frozen": {
"min_age": "90d",
"actions": {
"searchable_snapshot": {
"snapshot_repository": "my_backup"
},
"set_priority": {
"priority": 0
}
}
}
}
}
}Shrink 收缩索引
shrink 操作将索引的主分片数减少为原有因数(如 9→3、8→4),以减少集群分片总数和资源开销。
POST /my_index/_shrink/my_index_shrunk
{
"settings": {
"index.number_of_shards": 2,
"index.number_of_replicas": 1,
"index.codec": "best_compression"
},
"aliases": {
"my_index": {}
}
}前置条件:索引必须只读(index.blocks.write=true),且所有分片必须位于同一节点。
Force Merge 强制段合并
Lucene 内部以小段(Segment)方式存储数据,段数过多会导致查询性能下降和文件句柄泄漏。forcemerge 将段合并为指定数量。
POST /my_index/_forcemerge?max_num_segments=1建议在 Warm 阶段执行,并且先在非高峰时段将索引设置为只读。
ILM 策略与索引模板关联
PUT /_index_template/logs_template
{
"index_patterns": ["logs-*"],
"template": {
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1,
"index.lifecycle.name": "logs_policy",
"index.lifecycle.rollover_alias": "logs"
},
"mappings": {
"properties": {
"@timestamp": { "type": "date" },
"message": { "type": "text" },
"level": { "type": "keyword" }
}
}
}
}初始化引导索引:
PUT /logs-000001
{
"aliases": {
"logs": {
"is_write_index": true
}
}
}ILM 状态查看与管理
# 查看所有 ILM 策略
GET /_ilm/policy
# 查看某个索引的当前生命周期状态
GET /logs-000001/_ilm/explain
# 手动重试失败的 ILM 操作
POST /logs-000001/_ilm/retry
# 为索引移除 ILM 策略
PUT /logs-000001/_settings
{
"index.lifecycle.name": null
}分词器
内置分词器
Elasticsearch 内置了多种分词器,适用于不同的场景:
| 分词器 | 中文支持 | 特点 | 适用场景 |
|---|---|---|---|
standard | 单字拆分 | 按 Unicode 文本分割,小写转换 | 英文通用文本 |
keyword | 不拆分 | 整个输入作为一个词条 | ID、精确匹配、枚举值 |
whitespace | 不拆分 | 仅按空格分割,不做小写转换 | 原始空格分割需求 |
simple | 单字拆分 | 按非字母字符分割,小写转换 | 纯英文简单文本 |
stop | 单字拆分 | 类似 simple,额外去除停用词 | 需过滤常见词的英文搜索 |
pattern | 取决于正则 | 按正则表达式分割 | 特定格式文本 |
Standard 分词器示例:
POST /_analyze
{
"analyzer": "standard",
"text": "Elasticsearch is a distributed, RESTful search engine"
}Keyword 分词器示例:
POST /_analyze
{
"analyzer": "keyword",
"text": "order_20241001_001"
}IK 分词器安装与配置
IK 分词器是目前 Elasticsearch 生态中最流行的中文分词插件,支持两种粒度模式。
安装方式:
# 方式一:使用 elasticsearch-plugin 安装(版本需严格匹配)
./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v8.12.0/elasticsearch-analysis-ik-8.12.0.zip
# 方式二:离线安装
# 1. 下载对应版本的 zip 包
wget https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v8.12.0/elasticsearch-analysis-ik-8.12.0.zip
# 2. 解压到 plugins 目录
unzip elasticsearch-analysis-ik-8.12.0.zip -d plugins/analysis-ik
# 3. 重启 Elasticsearch 节点两种分词模式:
# ik_max_word:最细粒度拆分,召回率高
POST /_analyze
{
"analyzer": "ik_max_word",
"text": "中华人民共和国国歌"
}
# ik_smart:粗粒度拆分,精准率高
POST /_analyze
{
"analyzer": "ik_smart",
"text": "中华人民共和国国歌"
}自定义词典配置:
在 plugins/analysis-ik/config/ 目录下创建 my_custom.dic 文件:
# my_custom.dic(每行一个词条)
奥格瑞玛
暴风城
铁炉堡
暗影国度修改 IKAnalyzer.cfg.xml:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
<comment>IK Analyzer 扩展配置</comment>
<entry key="ext_dict">my_custom.dic</entry>
<entry key="ext_stopwords">custom_stopword.dic</entry>
<entry key="remote_ext_dict">http://your-server/custom_dict.dic</entry>
<entry key="remote_ext_stopwords">http://your-server/stopword.dic</entry>
</properties>热更新 IK 词典时,remote_ext_dict 指向的远程文件需返回 Last-Modified 头,IK 每 5 分钟轮询一次。
自定义分析器
自定义分析器由三个组件构成:Character Filters → Tokenizer → Token Filters。
PUT /my_index
{
"settings": {
"analysis": {
"char_filter": {
"html_strip_filter": {
"type": "html_strip"
},
"my_mapping_filter": {
"type": "mapping",
"mappings": ["& => and", "| => or"]
}
},
"tokenizer": {
"my_comma_tokenizer": {
"type": "pattern",
"pattern": ","
}
},
"filter": {
"my_synonym": {
"type": "synonym",
"synonyms": [
"elastic, elasticsearch",
"logstash, ls",
"kibana, kb"
]
},
"my_ngram": {
"type": "ngram",
"min_gram": 2,
"max_gram": 4
}
},
"analyzer": {
"my_custom_analyzer": {
"type": "custom",
"char_filter": ["html_strip_filter", "my_mapping_filter"],
"tokenizer": "my_comma_tokenizer",
"filter": ["lowercase", "my_synonym", "my_ngram"]
}
}
}
},
"mappings": {
"properties": {
"content": {
"type": "text",
"analyzer": "my_custom_analyzer",
"search_analyzer": "standard"
}
}
}
}分词测试与调试
使用 _analyze API 可以快速测试分词结果,验证分析器配置是否达到预期。
# 测试已有索引的字段分词器
POST /my_index/_analyze
{
"field": "content",
"text": "Elasticsearch & Logstash 是 ELK 栈的核心组件"
}
# 测试自定义分析器
POST /my_index/_analyze
{
"analyzer": "my_custom_analyzer",
"text": "Elasticsearch & Logstash 是 ELK 栈的核心组件"
}
# 查看 token 的详细位置信息
POST /_analyze
{
"tokenizer": "standard",
"filter": ["lowercase", "stop"],
"text": "The quick brown fox jumps over the lazy dog",
"explain": true,
"attributes": ["position", "offset"]
}聚合分析进阶
Bucket 聚合
Bucket 聚合将文档分组到不同的桶中,每个桶代表一组符合条件的文档。
Date Histogram 按时间柱状聚合
GET /logs-*/_search
{
"size": 0,
"aggs": {
"requests_over_time": {
"date_histogram": {
"field": "@timestamp",
"fixed_interval": "1h",
"format": "yyyy-MM-dd HH:mm:ss",
"time_zone": "Asia/Shanghai",
"min_doc_count": 0,
"extended_bounds": {
"min": "2024-01-01",
"max": "2024-01-02"
}
},
"aggs": {
"status_codes": {
"terms": {
"field": "status",
"size": 10
}
}
}
}
}
}calendar_interval 与 fixed_interval 的区别:
| 参数 | 示例值 | 是否受日历影响 | 适用场景 |
|---|---|---|---|
calendar_interval | month, quarter, year | 是,考虑月份天数差异 | 日历对齐的报表 |
fixed_interval | 1h, 5m, 7d | 否,严格固定时长 | 监控时序数据 |
Range 范围聚合
GET /products/_search
{
"size": 0,
"aggs": {
"price_ranges": {
"range": {
"field": "price",
"ranges": [
{ "key": "0-100", "from": 0, "to": 100 },
{ "key": "100-500", "from": 100, "to": 500 },
{ "key": "500-1000", "from": 500, "to": 1000 },
{ "key": "1000+", "from": 1000 }
]
},
"aggs": {
"avg_price": { "avg": { "field": "price" } }
}
}
}
}Terms 聚合与排序
GET /logs-*/_search
{
"size": 0,
"aggs": {
"top_ips": {
"terms": {
"field": "client_ip",
"size": 5,
"order": { "request_count": "desc" },
"min_doc_count": 100,
"shard_size": 100
},
"aggs": {
"request_count": { "value_count": { "field": "client_ip" } }
}
}
}
}Terms 聚合的 size 与 shard_size 规则:
size:最终返回的桶数shard_size:每个分片返回的候选桶数(默认size × 1.5 + 10)- 当
size较大时(如 > 1000),需要手动调大shard_size以保证准确性
Metric 聚合
Cardinality 基数估算
cardinality 基于 HyperLogLog++ 算法估算唯一值的数量,内存消耗固定。
GET /logs-*/_search
{
"size": 0,
"aggs": {
"unique_visitors": {
"cardinality": {
"field": "user_id",
"precision_threshold": 40000
}
}
}
}precision_threshold | 精度误差 | 内存消耗 |
|---|---|---|
| 1000 | ~1% | ~1.5KB |
| 10000 | ~1% | ~12KB |
| 40000(默认) | ~1% | ~48KB |
| >40000 | 逐步增大 | 固定上限 |
Percentiles 百分位聚合
GET /nginx-logs/_search
{
"size": 0,
"aggs": {
"latency_percentiles": {
"percentiles": {
"field": "response_time",
"percents": [50, 90, 95, 99, 99.9],
"keyed": true
}
}
}
}对于超大规模数据,可使用 tdigest 压缩(默认)或 hdr 高动态范围直方图:
{
"percentiles": {
"field": "response_time",
"percents": [50, 95, 99],
"hdr": {
"number_of_significant_value_digits": 3
}
}
}Stats 统计聚合
GET /orders/_search
{
"size": 0,
"aggs": {
"order_stats": {
"stats": { "field": "amount" }
},
"extended_order_stats": {
"extended_stats": { "field": "amount" }
}
}
}extended_stats 额外返回 sum_of_squares、variance、std_deviation、std_deviation_bounds。
Pipeline 聚合
Pipeline 聚合基于其他聚合的结果进行二次计算,不在原始文档上执行。
Bucket Script 桶脚本
计算占位比:
GET /logs-*/_search
{
"size": 0,
"aggs": {
"status_counts": {
"terms": {
"field": "status",
"size": 10
}
},
"error_ratio": {
"bucket_script": {
"buckets_path": {
"total": "_count",
"errors": "status_counts['5*']>_count"
},
"script": "params.errors / params.total * 100"
}
}
}
}注意:bucket_script 必须在父聚合的平级或子聚合中使用,且 buckets_path 引用路径使用 > 分隔层级。
Derivative 导数聚合
计算时序数据的增长率:
GET /metrics/_search
{
"size": 0,
"aggs": {
"requests_per_hour": {
"date_histogram": {
"field": "@timestamp",
"fixed_interval": "1h"
},
"aggs": {
"total_requests": { "value_count": { "field": "request_id" } },
"requests_derivative": {
"derivative": { "buckets_path": "total_requests" }
}
}
}
}
}其他常用 Pipeline 聚合
| 聚合类型 | 功能 | 常见用途 |
|---|---|---|
avg_bucket | 计算桶中 metric 的平均值 | 平均每小时请求数 |
sum_bucket | 计算桶中 metric 的和 | 总销售额 |
max_bucket / min_bucket | 查找最大/最小值的桶 | 找出流量高峰时段 |
cumulative_sum | 累计求和 | 累计用户数 |
moving_avg | 移动平均值 | 平滑趋势线(8.x 后推荐 moving_fn) |
bucket_selector | 按条件过滤桶 | 过滤出 doc_count > 1000 的桶 |
聚合性能优化
PUT /_settings
{
"index": {
"number_of_shards": 5,
"number_of_replicas": 0,
"mapping": {
"total_fields": { "limit": 2000 }
}
}
}优化原则:
- 设置合理的
shard_size:Terms 聚合默认shard_size = size × 1.5 + 10,大 size 时手动调高 - 避免在 text 字段上聚合:text 字段需要
fielddata=true,极其消耗内存;改用keyword子字段 - 使用
size: 0:聚合查询中设置size: 0避免返回无用的文档内容 - 提前过滤:使用
query/filter先行缩小数据范围,减少聚合处理量 - 利用
search_type=dfs_query_then_fetch:提高 Terms 聚合的全局准确性(代价是增加一次预查询)
# 使用 DFS 查询类型提高聚合精度
GET /my_index/_search?search_type=dfs_query_then_fetch集群调优
分片分配策略
自定义路由分配
通过 _routing 控制文档到分片的映射,保证相关文档落入同一分片,从而支持单分片内的高效聚合和关联查询。
# 索引时指定 routing
PUT /orders/_doc/1001?routing=user_42
{
"user_id": 42,
"order_date": "2024-01-15",
"amount": 299.00
}
# 查询时指定 routing(跳过分片广播)
GET /orders/_search?routing=user_42
{
"query": {
"term": { "user_id": 42 }
}
}分片分配过滤
# 节点启动时打标签
# elasticsearch.yml
node.attr.rack: rack-1
node.attr.tier: hot
# 按 attribute 分配分片
PUT /my_index/_settings
{
"index.routing.allocation.require.rack": "rack-1",
"index.routing.allocation.include.tier": "hot,warm",
"index.routing.allocation.exclude.tier": "frozen"
}分片平衡控制
# 动态调整平衡参数
PUT /_cluster/settings
{
"persistent": {
"cluster.routing.allocation.balance.shard": 0.45,
"cluster.routing.allocation.balance.index": 0.55,
"cluster.routing.allocation.disk.watermark.low": "85%",
"cluster.routing.allocation.disk.watermark.high": "90%",
"cluster.routing.allocation.disk.watermark.flood_stage": "95%"
}
}| 参数 | 默认值 | 说明 |
|---|---|---|
balance.shard | 0.45 | 分片总数平衡权重 |
balance.index | 0.55 | 单个索引分片平衡权重 |
balance.threshold | 1.0 | 平衡阈值,低于此值不触发 |
disk.watermark.low | 85% | 磁盘使用率超过此值不再分配分片 |
disk.watermark.high | 90% | 超过此值将分片迁移到其他节点 |
disk.watermark.flood_stage | 95% | 超过此值强制设置索引只读 |
慢查询分析
Search Slow Log
PUT /my_index/_settings
{
"index.search.slowlog.threshold.query.warn": "10s",
"index.search.slowlog.threshold.query.info": "5s",
"index.search.slowlog.threshold.query.debug": "2s",
"index.search.slowlog.threshold.query.trace": "500ms",
"index.search.slowlog.threshold.fetch.warn": "1s",
"index.search.slowlog.threshold.fetch.info": "800ms",
"index.search.slowlog.threshold.fetch.debug": "500ms",
"index.search.slowlog.threshold.fetch.trace": "200ms",
"index.search.slowlog.level": "info"
}日志文件位置:logs/<cluster_name>_index_search_slowlog.json
Index Slow Log
PUT /my_index/_settings
{
"index.indexing.slowlog.threshold.index.warn": "10s",
"index.indexing.slowlog.threshold.index.info": "5s",
"index.indexing.slowlog.threshold.index.debug": "2s",
"index.indexing.slowlog.threshold.index.trace": "500ms",
"index.indexing.slowlog.threshold.index.level": "info",
"index.indexing.slowlog.source": 1000
}index.indexing.slowlog.source 控制记录源文档的前 N 个字符,以便定位慢写入的具体数据。
慢日志轮转配置
# log4j2.properties
appender.slow_log_rolling.type = RollingFile
appender.slow_log_rolling.name = slow_log_rolling
appender.slow_log_rolling.fileName = ${sys:es.logs.base_path}${sys:file.separator}${sys:es.logs.cluster_name}_index_search_slowlog.log
appender.slow_log_rolling.layout.type = PatternLayout
appender.slow_log_rolling.layout.pattern = [%d{ISO8601}][%-5p][%-25c{1.}] %marker%.-10000messagen
appender.slow_log_rolling.policies.type = Policies
appender.slow_log_rolling.policies.size.type = SizeBasedTriggeringPolicy
appender.slow_log_rolling.policies.size.size = 256MB
appender.slow_log_rolling.strategy.type = DefaultRolloverStrategy
appender.slow_log_rolling.strategy.max = 10强制段合并
# 在非高峰时段手动触发
POST /my_index/_forcemerge?max_num_segments=1
# 限制合并的线程数,避免 I/O 打满
PUT /_cluster/settings
{
"persistent": {
"indices.store.throttle.max_bytes_per_sec": "100mb"
}
}段合并的影响:
| 方面 | 合并前 | 合并后 |
|---|---|---|
| 段数量 | 数百至数千 | 1(或指定数量) |
| 查询性能 | 需打开所有段 | 仅打开一个段 |
| 文件句柄 | 大量 | 显著减少 |
| 磁盘空间(临时) | — | 合并期间需额外 1 倍空间 |
Translog 调优
Translog 是 Elasticsearch 的预写日志(WAL),用于防止节点宕机时的数据丢失。
PUT /my_index/_settings
{
"index": {
"translog.durability": "async",
"translog.sync_interval": "5s",
"translog.flush_threshold_size": "1024mb"
}
}Translog 配置说明:
| 参数 | 默认值 | 说明 |
|---|---|---|
translog.durability | request | request 每次写请求都刷盘;async 异步刷盘 |
translog.sync_interval | 5s | 异步模式下 fsync 间隔 |
translog.flush_threshold_size | 512mb | translog 超过此大小触发 flush |
translog.retention.size | 512mb | 保留的 translog 大小(7.x 后不再需要) |
性能/安全权衡:
async+ 大 threshold 可提升 30-50% 的写入吞吐,但节点宕机会丢失最多sync_interval内的数据。
Mapping 优化
字段类型选择
核心字段类型对比:
| 类型 | 适用场景 | 索引方式 | 是否支持聚合 |
|---|---|---|---|
text | 全文搜索 | 倒排索引 | 需要 fielddata |
keyword | 精确匹配、排序、聚合 | 倒排索引(不分析) | 是 |
integer / long | 整型数值 | Lucene IntPoint/LongPoint | 是 |
float / double | 浮点数值 | Lucene FloatPoint/DoublePoint | 是 |
date | 日期时间 | Lucene LongPoint(时间戳) | 是 |
boolean | 布尔值 | Lucene IntPoint(0/1) | 是 |
ip | IP 地址 | Lucene InetAddressPoint | 是 |
geo_point | 地理坐标 | BKD 树 | 是 |
nested | 对象数组独立查询 | 单独隐藏文档 | 是(限定) |
join | 父子关系 | 存储父子关系 | 有限 |
Keyword vs Text 最佳实践
PUT /articles
{
"mappings": {
"properties": {
"title": {
"type": "text",
"analyzer": "ik_max_word",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 512
},
"pinyin": {
"type": "text",
"analyzer": "pinyin"
}
}
},
"status": {
"type": "keyword"
},
"tags": {
"type": "keyword"
}
}
}
}选择原则:
- 需要 全文搜索、中文分词 →
text+ 合适的分词器 - 需要 精确匹配、排序、聚合、Term 查询 →
keyword - 一个字段两种都需要 →
text+keyword子字段(multi-fields) - 长度超过 32766 字节的 keyword → 设置
ignore_above,超出部分不索引 - 不分词的字段绝不用
text,避免意外分析导致的搜索不准
Nested vs Join
Nested 嵌套对象
PUT /orders
{
"mappings": {
"properties": {
"order_id": { "type": "keyword" },
"items": {
"type": "nested",
"properties": {
"product_id": { "type": "keyword" },
"quantity": { "type": "integer" },
"price": { "type": "float" }
}
}
}
}
}Nested 查询语法:
GET /orders/_search
{
"query": {
"nested": {
"path": "items",
"inner_hits": {},
"query": {
"bool": {
"must": [
{ "term": { "items.product_id": "P001" } },
{ "range": { "items.quantity": { "gte": 2 } } }
]
}
}
}
}
}Nested 的代价: 每个嵌套对象作为一个隐蔽文档存储,查询需额外层级的 nested 查询。写入性能约为普通文档的 1/3-1/5。
Join 父子关系
PUT /questions
{
"mappings": {
"properties": {
"text": { "type": "text" },
"my_join_field": {
"type": "join",
"relations": {
"question": "answer"
}
}
}
}
}插入父文档:
PUT /questions/_doc/q1
{
"text": "Elasticsearch 如何优化写入性能?",
"my_join_field": "question"
}插入子文档:
PUT /questions/_doc/a1?routing=q1
{
"text": "使用 bulk 批量写入、调整 refresh_interval。",
"my_join_field": {
"name": "answer",
"parent": "q1"
}
}Has Child 查询:
GET /questions/_search
{
"query": {
"has_child": {
"type": "answer",
"query": {
"match": { "text": "bulk" }
}
}
}
}Nested vs Join 选择矩阵:
| 维度 | Nested | Join |
|---|---|---|
| 关系深度 | 单层(对象数组) | 多层(父子) |
| 查询性能 | 快(同文档内) | 慢(跨文档 join) |
| 写入性能 | 慢(对象展开为隐式文档) | 中(需指定 routing) |
| 更新父文档 | 直接更新 | 需同时指定 routing |
| 子文档独立查询 | 否 | 是 |
| 关系上限 | 建议 < 10000 个嵌套对象/文档 | 建议 < 1000 个子文档/父文档 |
推荐: 能用 Nested 就不用 Join。Join 仅在父文档极少更新且需要子文档独立查询和维护时才使用。
Doc Values 与 Fielddata
Doc Values 是面向列的磁盘数据结构,用于排序、聚合和脚本访问。正向索引(倒排索引的反面)——_term => doc_id 变为 doc_id => _term_value。
PUT /my_index
{
"mappings": {
"properties": {
"title": {
"type": "text",
"analyzer": "ik_max_word"
},
"price": {
"type": "float",
"doc_values": true
},
"description": {
"type": "text",
"analyzer": "standard",
"fielddata": true
}
}
}
}| 特性 | Doc Values | Fielddata |
|---|---|---|
| 存储位置 | 磁盘(列式存储) | 堆内存 |
| 索引阶段 | 索引时构建 | 查询时按需加载 |
| 启用方式 | 默认启用(除 text) | 显式设置 fielddata: true |
| 适用字段 | keyword、数值、date、ip | text |
| 内存开销 | 低 | 高,容易 OOM |
| 推荐程度 | ✅ 强烈推荐 | ❌ 尽量避免 |
黄金法则: 始终依赖 Doc Values 进行聚合和排序。不要启用
fielddata,除非你完全了解其在堆内存中的开销。
Source Filter 源过滤
_source 字段存储原始 JSON 文档,占用存储空间。通过 _source 过滤和 includes/excludes 可以精确控制存储内容。
PUT /logs
{
"mappings": {
"_source": {
"includes": ["@timestamp", "level", "message", "service.name"],
"excludes": ["internal.*", "debug_info"]
},
"properties": {
"@timestamp": { "type": "date" },
"level": { "type": "keyword" },
"message": { "type": "text" },
"service": {
"properties": {
"name": { "type": "keyword" }
}
},
"internal_request_id": { "type": "keyword" },
"debug_info": {
"type": "object",
"enabled": false
}
}
}
}禁用 _source(谨慎使用):
PUT /my_index
{
"mappings": {
"_source": { "enabled": false }
}
}禁用 _source 的场景:纯时序指标、聚合分析不需要原始数据的场景。禁用后无法使用 update、reindex 等依赖 _source 的功能。
查询性能优化
Filter vs Query 缓存
Elasticsearch 的查询上下文分为 query(评分查询)和 filter(过滤查询),两者在缓存行为上有本质区别。
GET /products/_search
{
"query": {
"bool": {
"must": [
{ "match": { "title": "手机" } }
],
"filter": [
{ "term": { "status": "上架" } },
{ "range": { "price": { "gte": 1000, "lte": 5000 } } }
]
}
}
}| 特性 | Query(查询上下文) | Filter(过滤上下文) |
|---|---|---|
| 评分 | 计算 _score | 不计算,_score = 0 |
| 缓存 | 不缓存 | 结果缓存到节点级缓存 |
| 性能 | 相对慢 | 快(无需评分且可缓存) |
| 适用场景 | 全文搜索、相关性排序 | 精确匹配、范围过滤 |
缓存机制:
- Filter 缓存是 节点级别的 LRU 缓存,默认存活 256 个查询
- 缓存的 key 是整个 filter DSL + 被过滤的段集合
- 段合并后,缓存自动失效并重建
- 高频 filter(如
term: {"status": "active"})可极大受益于缓存
Search After 深度分页
from + size 深度分页(超过 10000 条)会导致协调节点内存泄露和性能急剧下降。search_after 利用前一页的排序值进行游标翻页。
# 第一页:正常查询,记录排序值
GET /products/_search
{
"size": 10,
"sort": [
{ "price": { "order": "desc" } },
{ "product_id": { "order": "asc" } }
],
"query": {
"range": { "price": { "gte": 100 } }
}
}
# 第二页:使用上一页最后一条的 sort 值
GET /products/_search
{
"size": 10,
"sort": [
{ "price": { "order": "desc" } },
{ "product_id": { "order": "asc" } }
],
"search_after": [999.00, "P00042"],
"query": {
"range": { "price": { "gte": 100 } }
}
}search_after 要点:
- 必须为
sort字段确保唯一性(否则可能丢失数据),通常加上_id作为 tiebreaker - 不支持
from参数,不可随机跳页 - 适合"加载更多"场景(如移动端列表、控制台日志流)
- 性能在任意深度保持恒定
Scroll API 全量遍历
scroll 用于一次性导出大量数据(如 reindex、数据备份),维护一个上下文快照。
# 初始化 scroll,设置有效期 3 分钟
POST /products/_search?scroll=3m
{
"size": 1000,
"query": { "match_all": {} }
}
# 从返回结果中获取 _scroll_id,逐页读取
POST /_search/scroll
{
"scroll": "3m",
"scroll_id": "FGluY2x1ZGVfY29udGV4dF91dGlkDnF1ZXJ5..."
}
# 使用完毕后清理
DELETE /_search/scroll
{
"scroll_id": "FGluY2x1ZGVfY29udGV4dF91dGlkDnF1ZXJ5..."
}
# 批量清理
DELETE /_search/scroll/_allScroll vs Search After 对比:
| 维度 | Scroll | Search After |
|---|---|---|
| 机制 | 维持搜索上下文快照 | 基于前页排序值游标 |
| 资源开销 | 高(快照占用堆内存) | 低(无额外状态) |
| 实时性 | 快照时间点的数据 | 实时数据 |
| 随机跳页 | 否 | 否 |
| 超时 | 需 ttl 控制,超时自动释放 | 无超时 |
| 适用场景 | reindex、数据导出、快照 | 实时深度分页 |
Scoll 注意事项: 8.x 中 scroll 仍然可用,但 Elasticsearch 官方逐渐推荐使用
search_after+point in time (PIT)替代。
Profile API 分析
Profile API 是诊断查询性能瓶颈的核心工具。
GET /products/_search
{
"profile": true,
"query": {
"bool": {
"must": [
{ "match": { "title": "手机" } }
],
"filter": [
{ "term": { "brand_id": "B001" } }
]
}
}
}返回解读要点:
{
"profile": {
"shards": [
{
"id": "[node-1][products][0]",
"searches": [
{
"query": [
{
"query_type": "TermQuery",
"lucene": "brand_id:B001",
"time": "0.123456ms",
"breakdown": {
"score": 0,
"build_scorer_count": 1,
"match_count": 0,
"create_weight": 8923,
"next_doc": 0,
"match": 0,
"create_weight_count": 1,
"set_min_competitive_score_count": 0,
"compute_max_score": 0,
"set_min_competitive_score": 0,
"advance": 0,
"advance_count": 0,
"build_scorer": 12345,
"next_doc_count": 1,
"compute_max_score_count": 0
}
}
],
"rewrite_time": 1234,
"collector": [
{
"name": "SimpleTopScoreDocCollector",
"time": "0.234567ms"
}
]
}
]
}
]
}
}Profile 分析清单:
| 指标 | 关注点 | 优化方向 |
|---|---|---|
create_weight 耗时高 | 查询重写开销大 | 简化查询 DSL,减少 bool 嵌套层数 |
build_scorer 耗时高 | 构建评分器慢 | 减少 filter 非缓存查询→转为 cached filter |
next_doc 耗时高 | 匹配文档多,遍历慢 | 增加过滤条件,缩小查询范围 |
advance 耗时高 | 跳块(block)慢 | 检查字段是否启用 doc_values |
rewrite_time 长 | 查询重写次数多 | 避免通配符 prefix 查询,使用精确 term |
索引速度优化
PUT /my_index/_settings
{
"index": {
"refresh_interval": "30s",
"number_of_replicas": 0,
"translog.durability": "async",
"translog.sync_interval": "5s",
"translog.flush_threshold_size": "1024mb",
"indexing.slowlog.threshold.index.warn": "5s"
}
}批量写入优化:
# 动态调整 bulk 大小的最佳实践
POST /_bulk
{"index": {"_index": "logs", "_id": "1"}}
{"@timestamp": "2024-01-01T00:00:00Z", "message": "log entry 1", "level": "info"}
{"index": {"_index": "logs", "_id": "2"}}
{"@timestamp": "2024-01-01T00:00:01Z", "message": "log entry 2", "level": "warn"}写入优化对照表:
| 配置项 | 建议值 | 原理 |
|---|---|---|
refresh_interval | 30-60s 或 -1(批量导入后恢复) | 减少 refresh 次数,降低段合并频率 |
number_of_replicas | 0(批量导入后恢复) | 避免写入时同步副本 |
translog.durability | async | 异步刷盘,减少 fsync 阻塞 |
| bulk size | 5-15MB(建议 5000-10000 条) | 单个 bulk 不宜过大,避免协调节点 GC 压力 |
| 分片数 | 每个分片 20-50GB | 分片过小导致段多,过大导致恢复慢 |
index.codec | best_compression | 启用 LZ4 替代默认压缩 |
集群监控
Cat API
Cat API 以文本形式输出集群信息,适合快速查看集群状态。
# 集群健康度概览
GET /_cat/health?v&pretty
# 节点列表
GET /_cat/nodes?v&h=id,ip,heap.percent,ram.percent,cpu,load_1m,node.role,master
# 索引概况
GET /_cat/indices?v&s=docs.count:desc
# 分片分布
GET /_cat/shards?v&h=index,shard,prirep,state,docs,store,node&s=index
# 段信息
GET /_cat/segments?v&h=index,shard,segment,size,committed,search,version,generation
# 线程池队列
GET /_cat/thread_pool?v&h=id,name,active,queued,rejected,completed
# 挂起任务
GET /_cat/pending_tasks?v
# 主节点
GET /_cat/master?vCluster Health
# 集群健康状态
GET /_cluster/health
# 详细健康(包含分片级别)
GET /_cluster/health?level=shards
# 等待中的节点
GET /_cluster/health?wait_for_nodes=5&timeout=30s
# 等待状态变绿
GET /_cluster/health?wait_for_status=green&timeout=50s健康状态解读:
| 状态 | 含义 | 处理方式 |
|---|---|---|
green | 所有主分片和副本分片正常运行 | 无需处理 |
yellow | 主分片正常,部分副本分片未分配 | 检查节点数是否 >= number_of_replicas + 1 |
red | 存在未分配的主分片 | 立即检查节点状态、磁盘、网络 |
Node Stats
# 所有节点统计
GET /_nodes/stats
# 指定节点、指定指标
GET /_nodes/node-1,node-2/stats/indices,os,process,jvm,fs,transport,http,breakers
# JVM 内存相关
GET /_nodes/stats/jvm?h=gc.collection_count,gc.collection_time_in_millis,mem.heap_used_percent,mem.heap_committed_in_bytes
# 线程池拒绝情况
GET /_nodes/stats/thread_pool?h=search.rejected,bulk.rejected,index.rejected
# 断路器状态
GET /_nodes/stats/breakers?h=name,estimated_size_in_bytes,overhead,tripped,limit_size_in_bytes关键监控指标与告警阈值:
| 指标 | 告警阈值 | 说明 |
|---|---|---|
| JVM Heap 使用率 | > 85% | 接近 GC 频繁触发,检查是否有 OOM 风险 |
| GC 次数 | young GC > 10/s 或 old GC > 1/s | 内存压力大,检查缓存设置 |
| thread_pool.search.rejected | > 0 | 搜索线程池已满,增加节点或优化查询 |
| thread_pool.bulk.rejected | > 0 | 写入超过集群容量,限流或扩展 |
| FS 磁盘使用率 | > 85% | 触发 watermark 迁移,自动降级 |
| Circuit Breaker tripped | > 0 | 请求过大导致内存保护触发,检查聚合或请求体 |
| 节点 CPU | > 80% | 检查是否有热点查询或合并压力 |
Elasticsearch Exporter + Grafana
Prometheus + elasticsearch-exporter 监控方案:
# docker-compose.yml
version: '3.8'
services:
elasticsearch:
image: elasticsearch:8.12.0
environment:
- node.name=es-node-1
- cluster.name=my-cluster
- xpack.security.enabled=false
- discovery.type=single-node
elasticsearch-exporter:
image: quay.io/prometheuscommunity/elasticsearch-exporter:v1.7.0
command:
- '--es.uri=http://elasticsearch:9200'
- '--es.all'
- '--es.indices'
- '--es.indices_settings'
- '--es.shards'
ports:
- "9114:9114"
prometheus:
image: prom/prometheus:v2.51.0
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
ports:
- "9090:9090"
grafana:
image: grafana/grafana:10.3.0
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin
ports:
- "3000:3000"Prometheus 配置:
# prometheus.yml
scrape_configs:
- job_name: 'elasticsearch'
scrape_interval: 15s
static_configs:
- targets: ['elasticsearch-exporter:9114']Grafana 推荐面板:
| 面板 ID | 名称 | 特性 |
|---|---|---|
| 6483 | Elasticsearch Overview | 集群健康、节点统计、索引指标 |
| 2322 | Elasticsearch Exporter | 分片级别详细监控 |
| 1621 | Prometheus ES | JVM、GC、线程池、搜索/写入性能 |
Elasticsearch 安全
X-Pack 安全
X-Pack 是 Elastic Stack 的安全扩展模块,在 7.x 之后捆绑在 Elasticsearch 发行版中。8.x 默认启用安全功能。
用户认证
# 设置内置用户密码(交互式)
./bin/elasticsearch-setup-passwords interactive
# 设置内置用户密码(自动生成)
./bin/elasticsearch-setup-passwords auto内置用户:
| 用户名 | 角色 | 说明 |
|---|---|---|
elastic | superuser | 最高权限管理员 |
kibana_system | kibana_system | Kibana 连接 ES 使用 |
logstash_system | logstash_system | Logstash 监控使用 |
beats_system | beats_system | Beats 监控使用 |
apm_system | apm_system | APM 系统使用 |
remote_monitoring_user | remote_monitoring_user | 远程监控 |
创建和管理用户
# 创建用户
POST /_security/user/zhangsan
{
"password": "StrongP@ssw0rd!",
"roles": ["logstash_writer", "monitoring_user"],
"full_name": "张三",
"email": "zhangsan@example.com",
"metadata": {
"department": "engineering"
},
"enabled": true
}
# 修改密码
PUT /_security/user/zhangsan/_password
{
"password": "NewP@ssw0rd!"
}
# 禁用用户
PUT /_security/user/zhangsan
{
"enabled": false
}
# 删除用户
DELETE /_security/user/zhangsanTLS 传输加密
生成证书
# 使用 elasticsearch-certutil 生成 CA
./bin/elasticsearch-certutil ca --pem --out ca.zip
# 解压 CA 文件
unzip ca.zip
# 得到: ca/ca.crt, ca/ca.key
# 为每个节点生成证书
./bin/elasticsearch-certutil cert --pem --ca-cert ca/ca.crt --ca-key ca/key.key --dns node1.example.com --ip 192.168.1.10 --out node1.zip
# 使用实例证书(简化多节点)
./bin/elasticsearch-certutil cert --ca-key ca/ca.key --ca-cert ca/ca.crt --pem --multiple
# 会进入交互式配置界面,可为每个节点指定名称、DNS、IP节点间加密通信
# elasticsearch.yml
xpack.security.enabled: true
xpack.security.transport.ssl.enabled: true
xpack.security.transport.ssl.verification_mode: certificate
xpack.security.transport.ssl.client_authentication: required
xpack.security.transport.ssl.keystore.path: certs/node1.p12
xpack.security.transport.ssl.truststore.path: certs/truststore.p12HTTP 客户端加密
# elasticsearch.yml
xpack.security.http.ssl.enabled: true
xpack.security.http.ssl.keystore.path: certs/node1.p12
xpack.security.http.ssl.client_authentication: optionalJava 客户端配置 TLS
// Java API Client 配置 TLS
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.json.jackson.JacksonJsonpMapper;
import co.elastic.clients.transport.ElasticsearchTransport;
import co.elastic.clients.transport.rest_client.RestClientTransport;
import org.apache.http.Header;
import org.apache.http.HttpHost;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.ssl.SSLContextBuilder;
import org.elasticsearch.client.RestClient;
import javax.net.ssl.SSLContext;
import java.io.File;
import java.io.FileInputStream;
import java.security.KeyStore;
// 加载信任证书
KeyStore truststore = KeyStore.getInstance("PKCS12");
try (FileInputStream fis = new FileInputStream("certs/truststore.p12")) {
truststore.load(fis, "changeit".toCharArray());
}
SSLContext sslContext = SSLContextBuilder.create()
.loadTrustMaterial(truststore, null)
.build();
BasicCredentialsProvider creds = new BasicCredentialsProvider();
creds.setCredentials(
AuthScope.ANY,
new UsernamePasswordCredentials("elastic", "password")
);
RestClient restClient = RestClient.builder(
new HttpHost("localhost", 9200, "https"))
.setHttpClientConfigCallback(builder ->
builder.setSSLContext(sslContext)
.setDefaultCredentialsProvider(creds))
.build();
ElasticsearchTransport transport = new RestClientTransport(restClient, new JacksonJsonpMapper());
ElasticsearchClient client = new ElasticsearchClient(transport);RBAC 权限控制
角色定义
# 创建只读角色
POST /_security/role/logs_readonly
{
"cluster": ["monitor"],
"indices": [
{
"names": ["logs-*"],
"privileges": ["read", "view_index_metadata"],
"field_security": {
"grant": ["@timestamp", "level", "message", "service"],
"except": ["internal_debug"]
},
"query": {
"term": { "level": "info" }
}
}
],
"run_as": [],
"applications": [
{
"application": "kibana-.kibana",
"privileges": ["read"],
"resources": ["*"]
}
]
}权限级别
| 级别 | 典型权限 | 说明 |
|---|---|---|
cluster | monitor, manage, all | 集群级别操作 |
indices | read, write, delete, manage | 索引级别操作,支持 DLS/FLS |
applications | Kibana Space 权限 | 应用权限(如 Kibana) |
run_as | 模拟其他用户 | 服务账户模拟用户操作 |
索引级别安全(DLS/FLS)
- DLS(Document Level Security):通过
query字段限制用户可访问的文档行 - FLS(Field Level Security):通过
field_security控制用户可见的字段列
# 创建市场分析角色——只可查看华东区的销售数据
POST /_security/role/sales_east
{
"indices": [
{
"names": ["sales-*"],
"privileges": ["read"],
"query": {
"bool": {
"filter": [
{ "term": { "region": "east" } }
]
}
},
"field_security": {
"grant": ["order_id", "customer_name", "amount", "product"],
"except": ["customer_phone", "credit_card"]
}
}
]
}角色映射
# 创建角色映射(用于 LDAP/Active Directory)
POST /_security/role_mapping/mapped_logs_admin
{
"roles": ["logs_admin"],
"rules": {
"all": [
{ "field": { "groups": "cn=es-admins,dc=example,dc=com" } },
{ "field": { "realm.name": "ldap1" } }
]
},
"enabled": true,
"metadata": {
"description": "Map LDAP admins group to logs_admin role"
}
}最小权限原则示例
# 应用服务专用角色——仅允许对该服务自己的索引写入
POST /_security/role/app_service
{
"cluster": ["monitor"],
"indices": [
{
"names": ["app-${username}-*"],
"privileges": ["create_index", "write", "read", "view_index_metadata"]
}
],
"applications": [
{
"application": "kibana-.kibana",
"privileges": ["read"],
"resources": ["*"]
}
]
}使用 ${username} 变量可以让角色自动适配创建的用户名,实现每个服务账号只能操作自己命名的索引。