性能调优(基于 Elasticsearch 8.x)
学习目标
- 深入掌握 Elasticsearch 8.x 的写入性能优化技巧和最佳实践
- 深入掌握查询性能优化技巧和评分优化
- 理解内存、磁盘、网络的全面优化策略
- 能够系统地分析和解决各类性能问题
- 掌握性能测试和基准测试方法
- 理解生产环境的性能调优实战经验
知识结构思维导图
性能调优
写入优化
批量写入
刷新策略
副本策略
线程池调优
索引缓冲区
查询优化
Filter优化
分页优化
字段优化
缓存优化
路由优化
聚合优化
范围限制
桶数量控制
执行策略
预聚合
内存优化
堆内存设置
字段数据缓存
查询缓存
断路器
磁盘优化
SSD选择
段合并
压缩策略
水位线设置
JVM调优
GC策略
堆大小
GC日志
参数调优
分片优化
分片数量
分片大小
分片分配
路由策略
一、性能问题诊断
1.1 常见性能问题
写入慢:
- 刷新间隔太短
- 副本数量过多
- 分片数量不合理
查询慢:
- 深度分页
- 聚合数据量大
- 没有使用 filter
- 分片数量过多
内存不足:
- 堆内存设置不当
- 字段数据缓存过大
- 查询缓存过大
1.2 性能监控指标
bash
# 查看节点统计
GET /_nodes/stats
# 查看索引统计
GET /products/_stats
# 查看慢查询日志
GET /_cat/thread_pool?v
# 查看任务
GET /_tasks
二、写入性能优化
2.1 批量写入
使用 Bulk API:
java
// ✗ 错误:单条写入
for (Product product : products) {
client.index(i -> i.index("products").document(product));
}
// ✓ 正确:批量写入
BulkRequest.Builder br = new BulkRequest.Builder();
for (Product product : products) {
br.operations(op -> op.index(idx -> idx
.index("products")
.document(product)
));
}
client.bulk(br.build());
批量大小建议:
单次批量:1000-5000 条
单次大小:5-15 MB
根据实际测试调整
2.2 调整刷新间隔
json
// 默认 1s,增大可提升写入性能
PUT /products/_settings
{
"refresh_interval": "30s"
}
// 批量导入时临时禁用
PUT /products/_settings
{
"refresh_interval": "-1"
}
// 导入完成后恢复
PUT /products/_settings
{
"refresh_interval": "1s"
}
POST /products/_refresh
2.3 减少副本数
json
// 导入前减少副本
PUT /products/_settings
{
"number_of_replicas": 0
}
// 导入后恢复副本
PUT /products/_settings
{
"number_of_replicas": 1
}
2.4 禁用 _source
json
// 如果不需要返回原始文档
{
"mappings": {
"_source": {
"enabled": false
}
}
}
2.5 使用自动生成 ID
java
// ✗ 慢:指定 ID(需要检查是否存在)
client.index(i -> i.index("products").id("1").document(product));
// ✓ 快:自动生成 ID
client.index(i -> i.index("products").document(product));
2.6 调整线程池
yaml
# elasticsearch.yml
thread_pool:
write:
size: 30
queue_size: 1000
2.7 写入性能测试
java
@Test
public void testBulkPerformance() {
int totalDocs = 100000;
int batchSize = 5000;
long startTime = System.currentTimeMillis();
for (int i = 0; i < totalDocs; i += batchSize) {
List<Product> batch = generateProducts(batchSize);
bulkIndex(batch);
}
long endTime = System.currentTimeMillis();
long duration = endTime - startTime;
double tps = (double) totalDocs / (duration / 1000.0);
System.out.println("总耗时: " + duration + "ms");
System.out.println("TPS: " + tps);
}
三、查询性能优化
3.1 使用 filter 代替 query
json
// ✗ 慢:使用 query(计算评分)
{
"query": {
"bool": {
"must": [
{ "term": { "category": "手机" }},
{ "range": { "price": { "lte": 5000 }}}
]
}
}
}
// ✓ 快:使用 filter(不计算评分,可缓存)
{
"query": {
"bool": {
"filter": [
{ "term": { "category": "手机" }},
{ "range": { "price": { "lte": 5000 }}}
]
}
}
}
3.2 避免深度分页
json
// ✗ 慢:深度分页
{
"from": 10000,
"size": 10
}
// ✓ 快:使用 search_after
{
"size": 10,
"search_after": [1000, "product-1000"],
"sort": [
{ "price": "asc" },
{ "_id": "asc" }
]
}
3.3 限制返回字段
json
// ✗ 慢:返回所有字段
{
"query": { "match_all": {} }
}
// ✓ 快:只返回需要的字段
{
"query": { "match_all": {} },
"_source": ["name", "price", "category"]
}
3.4 使用 keyword 字段排序
json
// ✗ 慢:text 字段排序
{
"sort": [{ "name": "asc" }]
}
// ✓ 快:keyword 字段排序
{
"sort": [{ "name.keyword": "asc" }]
}
3.5 预加载字段数据
json
PUT /products/_mapping
{
"properties": {
"category": {
"type": "keyword",
"eager_global_ordinals": true
}
}
}
3.6 使用路由
java
// 指定路由,减少查询的分片数
client.search(s -> s
.index("products")
.routing("category-1")
.query(q -> q.matchAll(m -> m))
);
3.7 禁用评分
json
{
"query": {
"bool": {
"filter": [
{ "term": { "category": "手机" }}
]
}
},
"track_scores": false
}
四、聚合性能优化
4.1 限制聚合范围
json
// ✗ 慢:全量聚合
{
"size": 0,
"aggs": {
"categories": {
"terms": { "field": "category" }
}
}
}
// ✓ 快:先过滤再聚合
{
"query": {
"range": {
"createTime": { "gte": "now-7d" }
}
},
"size": 0,
"aggs": {
"categories": {
"terms": { "field": "category" }
}
}
}
4.2 限制桶数量
json
{
"aggs": {
"categories": {
"terms": {
"field": "category",
"size": 10 // 只返回前10个
}
}
}
}
4.3 使用 composite 聚合
json
// 用于大量桶的分页聚合
{
"aggs": {
"my_buckets": {
"composite": {
"size": 100,
"sources": [
{ "category": { "terms": { "field": "category" }}}
]
}
}
}
}
五、内存优化
5.1 堆内存设置
yaml
# jvm.options
# 设置为物理内存的一半,不超过 32GB
-Xms16g
-Xmx16g
为什么不超过 32GB?
内存 ≤ 32GB:
- 使用压缩指针
- 内存利用率高
内存 > 32GB:
- 无法使用压缩指针
- 内存利用率低
- 反而浪费内存
5.2 字段数据缓存
yaml
# elasticsearch.yml
indices.fielddata.cache.size: 30%
5.3 查询缓存
yaml
indices.queries.cache.size: 10%
5.4 断路器设置
yaml
# 防止 OOM
indices.breaker.total.limit: 70%
indices.breaker.fielddata.limit: 40%
indices.breaker.request.limit: 40%
六、磁盘优化
6.1 使用 SSD
HDD:
- 随机读写慢
- 适合冷数据
SSD:
- 随机读写快
- 适合热数据
- 推荐用于生产环境
6.2 合并段
bash
# 合并段,减少段数量
POST /products/_forcemerge?max_num_segments=1
# 注意:
# 1. 只对不再写入的索引执行
# 2. 会占用大量 CPU 和 I/O
# 3. 建议在低峰期执行
6.3 磁盘水位设置
yaml
# elasticsearch.yml
cluster.routing.allocation.disk.watermark.low: 85%
cluster.routing.allocation.disk.watermark.high: 90%
cluster.routing.allocation.disk.watermark.flood_stage: 95%
七、分片优化
7.1 合理设置分片数
分片数量建议:
- 单分片大小:20-50GB
- 单节点分片数:不超过 20 个
- 总分片数:节点数 × 20
计算公式:
分片数 = 数据总量 / 单分片目标大小
7.2 分片分配策略
json
PUT /_cluster/settings
{
"transient": {
"cluster.routing.allocation.enable": "all",
"cluster.routing.rebalance.enable": "all",
"cluster.routing.allocation.cluster_concurrent_rebalance": 2,
"cluster.routing.allocation.node_concurrent_recoveries": 2
}
}
八、慢查询分析
8.1 启用慢查询日志
json
PUT /products/_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"
}
8.2 查看慢查询日志
bash
# 日志位置
tail -f /var/log/elasticsearch/my-cluster_index_search_slowlog.log
8.3 使用 Profile API
json
GET /products/_search
{
"profile": true,
"query": {
"match": { "name": "手机" }
}
}
响应包含详细的执行信息:
json
{
"profile": {
"shards": [{
"searches": [{
"query": [{
"type": "TermQuery",
"description": "name:手机",
"time_in_nanos": 123456,
"breakdown": {
"create_weight": 1000,
"build_scorer": 2000,
"next_doc": 100000
}
}]
}]
}]
}
}
九、实战案例
案例 1:优化商品搜索
优化前:
java
// 响应时间:500ms
SearchResponse<Product> response = client.search(s -> s
.index("products")
.query(q -> q
.bool(b -> b
.must(m -> m.match(mt -> mt.field("name").query("手机")))
.must(m -> m.term(t -> t.field("category").value("电子产品")))
.must(m -> m.range(r -> r.field("price").lte(JsonData.of(5000))))
)
)
.from(0)
.size(20)
.sort(so -> so.field(f -> f.field("sales").order(SortOrder.Desc)))
);
优化后:
java
// 响应时间:50ms
SearchResponse<Product> response = client.search(s -> s
.index("products")
.query(q -> q
.bool(b -> b
.must(m -> m.match(mt -> mt.field("name").query("手机")))
.filter(f -> f.term(t -> t.field("category").value("电子产品")))
.filter(f -> f.range(r -> r.field("price").lte(JsonData.of(5000))))
)
)
.from(0)
.size(20)
.sort(so -> so.field(f -> f.field("sales").order(SortOrder.Desc)))
.source(src -> src.filter(flt -> flt
.includes("name", "price", "imageUrl")
))
);
优化点:
1. 使用 filter 代替 must(精确匹配)
2. 限制返回字段
3. 确保 category 和 price 字段有索引
案例 2:优化聚合查询
优化前:
java
// 响应时间:2s
SearchResponse<Product> response = client.search(s -> s
.index("products")
.size(0)
.aggregations("categories", a -> a
.terms(t -> t.field("category").size(100))
.aggregations("avg_price", sub -> sub
.avg(avg -> avg.field("price"))
)
)
);
优化后:
java
// 响应时间:200ms
SearchResponse<Product> response = client.search(s -> s
.index("products")
.query(q -> q
.range(r -> r.field("createTime").gte(JsonData.of("now-30d")))
)
.size(0)
.aggregations("categories", a -> a
.terms(t -> t
.field("category")
.size(10)
.executionHint("map")
)
.aggregations("avg_price", sub -> sub
.avg(avg -> avg.field("price"))
)
)
);
优化点:
1. 先过滤再聚合(减少数据量)
2. 限制桶数量(100 → 10)
3. 使用 execution_hint
4. 确保 category 字段启用 eager_global_ordinals
十、性能测试与基准测试
10.1 使用 esrally 进行基准测试
安装 esrally:
bash
pip3 install esrally
运行标准测试:
bash
# 使用内置的 http_logs 测试集
esrally race --track=http_logs --target-hosts=localhost:9200
# 使用 geonames 测试集
esrally race --track=geonames --target-hosts=localhost:9200
# 指定客户端数量
esrally race --track=http_logs --target-hosts=localhost:9200 --client-options="timeout:60"
自定义测试:
bash
# 创建自定义测试集
esrally create-track --track=my-track --target-hosts=localhost:9200 --indices="products"
# 运行自定义测试
esrally race --track-path=~/.rally/benchmarks/tracks/my-track --target-hosts=localhost:9200
10.2 自定义性能测试框架
java
/**
* Elasticsearch 性能测试工具
*/
public class ESPerformanceTest {
private final ElasticsearchClient client;
private final ExecutorService executor;
/**
* 写入性能测试
*/
public void testIndexPerformance(int totalDocs, int batchSize, int concurrency) {
CountDownLatch latch = new CountDownLatch(concurrency);
AtomicLong successCount = new AtomicLong(0);
AtomicLong failCount = new AtomicLong(0);
long startTime = System.currentTimeMillis();
for (int i = 0; i < concurrency; i++) {
final int threadId = i;
executor.submit(() -> {
try {
int docsPerThread = totalDocs / concurrency;
for (int j = 0; j < docsPerThread; j += batchSize) {
List<Product> batch = generateProducts(batchSize, threadId, j);
try {
bulkIndex(batch);
successCount.addAndGet(batch.size());
} catch (Exception e) {
failCount.addAndGet(batch.size());
log.error("批量索引失败", e);
}
}
} finally {
latch.countDown();
}
});
}
try {
latch.await();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
long endTime = System.currentTimeMillis();
long duration = endTime - startTime;
printIndexResults(totalDocs, duration, successCount.get(), failCount.get());
}
/**
* 查询性能测试
*/
public void testSearchPerformance(int totalRequests, int concurrency) {
CountDownLatch latch = new CountDownLatch(concurrency);
AtomicLong successCount = new AtomicLong(0);
AtomicLong failCount = new AtomicLong(0);
List<Long> latencies = new CopyOnWriteArrayList<>();
long startTime = System.currentTimeMillis();
for (int i = 0; i < concurrency; i++) {
executor.submit(() -> {
try {
int requestsPerThread = totalRequests / concurrency;
for (int j = 0; j < requestsPerThread; j++) {
long reqStart = System.currentTimeMillis();
try {
search("手机");
successCount.incrementAndGet();
} catch (Exception e) {
failCount.incrementAndGet();
log.error("查询失败", e);
}
long reqEnd = System.currentTimeMillis();
latencies.add(reqEnd - reqStart);
}
} finally {
latch.countDown();
}
});
}
try {
latch.await();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
long endTime = System.currentTimeMillis();
long duration = endTime - startTime;
printSearchResults(totalRequests, duration, successCount.get(),
failCount.get(), latencies);
}
/**
* 打印索引测试结果
*/
private void printIndexResults(int total, long duration, long success, long fail) {
double tps = (double) success / (duration / 1000.0);
double successRate = (double) success / total * 100;
System.out.println("========== 索引性能测试结果 ==========");
System.out.println("总文档数: " + total);
System.out.println("成功数: " + success);
System.out.println("失败数: " + fail);
System.out.println("成功率: " + String.format("%.2f%%", successRate));
System.out.println("总耗时: " + duration + "ms");
System.out.println("TPS: " + String.format("%.2f", tps));
System.out.println("=====================================");
}
/**
* 打印查询测试结果
*/
private void printSearchResults(int total, long duration, long success,
long fail, List<Long> latencies) {
double qps = (double) success / (duration / 1000.0);
double successRate = (double) success / total * 100;
Collections.sort(latencies);
long p50 = latencies.get((int) (latencies.size() * 0.5));
long p90 = latencies.get((int) (latencies.size() * 0.9));
long p95 = latencies.get((int) (latencies.size() * 0.95));
long p99 = latencies.get((int) (latencies.size() * 0.99));
double avg = latencies.stream().mapToLong(Long::longValue).average().orElse(0);
System.out.println("========== 查询性能测试结果 ==========");
System.out.println("总请求数: " + total);
System.out.println("成功数: " + success);
System.out.println("失败数: " + fail);
System.out.println("成功率: " + String.format("%.2f%%", successRate));
System.out.println("总耗时: " + duration + "ms");
System.out.println("QPS: " + String.format("%.2f", qps));
System.out.println("平均延迟: " + String.format("%.2fms", avg));
System.out.println("P50 延迟: " + p50 + "ms");
System.out.println("P90 延迟: " + p90 + "ms");
System.out.println("P95 延迟: " + p95 + "ms");
System.out.println("P99 延迟: " + p99 + "ms");
System.out.println("=====================================");
}
}
10.3 性能基准参考
写入性能基准:
硬件配置:
- CPU: 16核
- 内存: 64GB
- 磁盘: SSD
- 网络: 10Gbps
单节点性能:
- 单线程: 5,000-10,000 docs/s
- 多线程: 20,000-50,000 docs/s
3节点集群性能:
- 批量写入: 50,000-100,000 docs/s
查询性能基准:
简单查询(term/match):
- P50: < 10ms
- P95: < 50ms
- P99: < 100ms
复杂查询(bool + 聚合):
- P50: < 50ms
- P95: < 200ms
- P99: < 500ms
QPS:
- 单节点: 1,000-5,000 QPS
- 3节点集群: 5,000-15,000 QPS
十一、JVM 调优详解
11.1 GC 策略选择
G1GC(ES 8.x 默认):
bash
# jvm.options
-XX:+UseG1GC
-XX:G1ReservePercent=25
-XX:InitiatingHeapOccupancyPercent=30
G1GC 优势:
✓ 可预测的停顿时间
✓ 适合大堆内存
✓ 并发标记和清理
✓ 自动调整
G1GC 参数调优:
bash
# 目标停顿时间(默认 200ms)
-XX:MaxGCPauseMillis=200
# 并发 GC 线程数
-XX:ConcGCThreads=4
# 并行 GC 线程数
-XX:ParallelGCThreads=8
# G1 区域大小(1-32MB)
-XX:G1HeapRegionSize=16m
11.2 GC 日志配置
bash
# jvm.options (ES 8.x)
-Xlog:gc*,gc+age=trace,safepoint:file=/var/log/elasticsearch/gc.log:utctime,pid,tags:filecount=32,filesize=64m
GC 日志分析:
bash
# 查看 GC 统计
jstat -gcutil <pid> 1000 10
# 输出示例
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
0.00 50.00 75.00 30.00 95.00 90.00 100 1.500 5 2.000 3.500
# 字段说明:
# S0/S1: Survivor 区使用率
# E: Eden 区使用率
# O: Old 区使用率
# M: Metaspace 使用率
# YGC: Young GC 次数
# YGCT: Young GC 总时间
# FGC: Full GC 次数
# FGCT: Full GC 总时间
11.3 堆内存调优
堆大小设置原则:
1. Xms = Xmx(避免堆大小调整)
2. 不超过物理内存的 50%
3. 不超过 32GB(压缩指针阈值)
4. 留足够内存给操作系统缓存
示例(64GB 物理内存):
-Xms31g
-Xmx31g
为什么不超过 32GB?
内存 ≤ 32GB:
使用压缩指针(Compressed Oops)
对象引用占用 4 字节
实际可用内存:~31GB
内存 > 32GB:
无法使用压缩指针
对象引用占用 8 字节
实际可用内存:~30GB(浪费了 10GB!)
结论:
31GB 堆内存 > 40GB 堆内存
11.4 GC 问题排查
频繁 Young GC:
原因:
- Eden 区太小
- 对象创建速率过高
解决:
1. 增加堆内存
2. 优化代码,减少对象创建
3. 使用对象池
频繁 Full GC:
原因:
- Old 区空间不足
- 内存泄漏
- 大对象直接进入 Old 区
解决:
1. 增加堆内存
2. 排查内存泄漏
3. 优化查询,减少内存使用
4. 清理缓存
GC 停顿时间过长:
原因:
- 堆内存过大
- Old 区碎片化
- 并发线程数不足
解决:
1. 减少堆内存(不超过 32GB)
2. 调整 G1 参数
3. 增加并发线程数
十二、网络优化
12.1 网络配置优化
yaml
# elasticsearch.yml
# 网络线程池
network.tcp.no_delay: true
network.tcp.keep_alive: true
network.tcp.reuse_address: true
# 连接超时
http.tcp.keep_alive: true
http.tcp.keep_idle: 300
http.tcp.keep_interval: 60
# 最大连接数
http.max_content_length: 100mb
http.max_initial_line_length: 4kb
http.max_header_size: 8kb
12.2 压缩传输
yaml
# 启用 HTTP 压缩
http.compression: true
http.compression_level: 3
# 节点间通信压缩
transport.compress: true
12.3 连接池优化
java
// Java 客户端连接池配置
RestClientBuilder builder = RestClient.builder(
new HttpHost("localhost", 9200, "http"))
.setRequestConfigCallback(requestConfigBuilder ->
requestConfigBuilder
.setConnectTimeout(5000)
.setSocketTimeout(60000)
.setConnectionRequestTimeout(5000))
.setHttpClientConfigCallback(httpClientBuilder ->
httpClientBuilder
.setMaxConnTotal(100)
.setMaxConnPerRoute(10)
.setKeepAliveStrategy((response, context) -> 300000));
十三、索引设计优化
13.1 字段类型选择
json
{
"mappings": {
"properties": {
// ✓ 正确:根据用途选择类型
"id": { "type": "keyword" }, // 精确匹配
"name": {
"type": "text", // 全文搜索
"fields": {
"keyword": { "type": "keyword" } // 排序/聚合
}
},
"price": { "type": "double" }, // 数值计算
"createTime": { "type": "date" }, // 日期范围
"description": {
"type": "text",
"index": false // 不需要搜索
},
"imageUrl": {
"type": "keyword",
"index": false, // 不需要搜索
"doc_values": false // 不需要排序/聚合
}
}
}
}
13.2 禁用不需要的功能
json
{
"mappings": {
"_source": {
"enabled": true,
"excludes": ["largeField"] // 排除大字段
},
"properties": {
"field1": {
"type": "keyword",
"index": false, // 不需要搜索
"doc_values": false // 不需要排序/聚合
},
"field2": {
"type": "text",
"norms": false, // 不需要评分归一化
"index_options": "freqs" // 只存储词频
}
}
}
}
13.3 使用数值类型代替字符串
json
// ❌ 不好:使用字符串
{
"status": { "type": "keyword" } // "pending", "processing", "completed"
}
// ✓ 好:使用数值
{
"status": { "type": "byte" } // 0, 1, 2
}
// 节省存储空间,提升查询性能
十四、实战优化案例
案例 1:电商搜索系统优化
优化前的问题:
- 查询响应时间:500-1000ms
- QPS:500
- 堆内存使用率:85%
- 频繁 Full GC
优化措施:
java
// 1. 索引设计优化
PUT /products_v2
{
"settings": {
"number_of_shards": 6,
"number_of_replicas": 1,
"refresh_interval": "5s",
"codec": "best_compression"
},
"mappings": {
"properties": {
"name": {
"type": "text",
"analyzer": "ik_max_word",
"fields": {
"keyword": { "type": "keyword" }
}
},
"category": {
"type": "keyword",
"eager_global_ordinals": true
},
"price": { "type": "scaled_float", "scaling_factor": 100 },
"sales": { "type": "integer" },
"rating": { "type": "half_float" },
"description": {
"type": "text",
"index_options": "offsets"
},
"images": {
"type": "keyword",
"index": false,
"doc_values": false
}
}
}
}
// 2. 查询优化
SearchRequest request = SearchRequest.of(s -> s
.index("products")
.query(q -> q
.bool(b -> b
.must(m -> m
.multiMatch(mm -> mm
.query("手机")
.fields("name^3", "description")
.type(TextQueryType.BestFields)
))
.filter(f -> f.term(t -> t.field("onSale").value(true)))
.filter(f -> f.range(r -> r.field("price").lte(JsonData.of(5000))))
))
.source(src -> src.filter(f -> f
.includes("name", "price", "imageUrl", "rating")))
.size(20)
.trackTotalHits(t -> t.enabled(false))
);
// 3. JVM 优化
// jvm.options
-Xms31g
-Xmx31g
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:G1HeapRegionSize=16m
// 4. 系统配置优化
// elasticsearch.yml
indices.queries.cache.size: 15%
indices.fielddata.cache.size: 30%
thread_pool.search.size: 30
thread_pool.search.queue_size: 2000
优化后的效果:
- 查询响应时间:50-100ms(提升 5-10 倍)
- QPS:3000(提升 6 倍)
- 堆内存使用率:60%
- Full GC 频率:从每小时 10 次降到每天 2 次
案例 2:日志系统优化
优化前的问题:
- 写入速度:10,000 docs/s
- 磁盘使用:1TB/天
- 查询慢(历史数据)
优化措施:
json
// 1. 使用 ILM 管理索引生命周期
PUT /_ilm/policy/logs_policy
{
"policy": {
"phases": {
"hot": {
"actions": {
"rollover": {
"max_size": "50GB",
"max_age": "1d"
}
}
},
"warm": {
"min_age": "3d",
"actions": {
"shrink": { "number_of_shards": 1 },
"forcemerge": { "max_num_segments": 1 }
}
},
"cold": {
"min_age": "7d",
"actions": {
"freeze": {}
}
},
"delete": {
"min_age": "30d",
"actions": {
"delete": {}
}
}
}
}
}
// 2. 索引模板优化
PUT /_index_template/logs_template
{
"index_patterns": ["logs-*"],
"template": {
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1,
"refresh_interval": "30s",
"codec": "best_compression",
"index.lifecycle.name": "logs_policy"
},
"mappings": {
"properties": {
"timestamp": { "type": "date" },
"level": { "type": "keyword" },
"message": {
"type": "text",
"index_options": "docs"
},
"fields": {
"type": "object",
"enabled": false
}
}
}
}
}
// 3. 批量写入优化
BulkRequest.Builder bulkBuilder = new BulkRequest.Builder()
.refresh(Refresh.False); // 禁用立即刷新
for (LogEntry log : logs) {
bulkBuilder.operations(op -> op
.index(idx -> idx
.index("logs")
.document(log)));
}
client.bulk(bulkBuilder.build());
优化后的效果:
- 写入速度:50,000 docs/s(提升 5 倍)
- 磁盘使用:400GB/天(节省 60%)
- 查询速度:提升 3 倍
- 自动清理旧数据
学习检查清单
- 深入掌握写入性能优化的所有技巧
- 深入掌握查询性能优化的所有技巧
- 理解聚合性能优化策略
- 掌握内存和磁盘的全面优化
- 掌握 JVM 调优和 GC 优化
- 能够系统地分析慢查询
- 能够进行完整的性能测试
- 掌握网络和索引设计优化
- 能够解决实际生产环境的性能问题
- 完成所有实战优化案例
下一步
下一节我们将学习监控与运维,确保 Elasticsearch 集群稳定高效运行。
十五、缓存优化详解
15.1 查询缓存(Query Cache)
查询缓存机制:
缓存内容:
- filter 查询的结果
- 聚合的结果
不缓存:
- query 查询(因为需要计算评分)
- 脚本查询
缓存策略:
- LRU(最近最少使用)
- 基于段(segment)级别
配置查询缓存:
yaml
# elasticsearch.yml
indices.queries.cache.size: 10% # 堆内存的 10%
查看缓存统计:
bash
GET /_stats/query_cache
# 响应示例
{
"indices": {
"products": {
"total": {
"query_cache": {
"memory_size_in_bytes": 104857600,
"total_count": 1000,
"hit_count": 800,
"miss_count": 200,
"cache_size": 500,
"cache_count": 500,
"evictions": 50
}
}
}
}
}
优化查询缓存使用:
java
// ✓ 使用 filter,可以被缓存
SearchRequest request = SearchRequest.of(s -> s
.index("products")
.query(q -> q
.bool(b -> b
.filter(f -> f.term(t -> t.field("category").value("手机")))
.filter(f -> f.range(r -> r.field("price").lte(JsonData.of(5000))))
))
);
// ✗ 使用 must,不会被缓存(因为需要计算评分)
SearchRequest request = SearchRequest.of(s -> s
.index("products")
.query(q -> q
.bool(b -> b
.must(m -> m.term(t -> t.field("category").value("手机")))
.must(m -> m.range(r -> r.field("price").lte(JsonData.of(5000))))
))
);
清除查询缓存:
bash
# 清除所有索引的查询缓存
POST /_cache/clear?query=true
# 清除指定索引的查询缓存
POST /products/_cache/clear?query=true
15.2 字段数据缓存(Fielddata Cache)
字段数据缓存用途:
用于:
- text 字段的排序
- text 字段的聚合
- 脚本中访问字段值
注意:
- 占用大量堆内存
- 建议使用 keyword 字段代替
配置字段数据缓存:
yaml
# elasticsearch.yml
indices.fielddata.cache.size: 30% # 堆内存的 30%
监控字段数据缓存:
bash
GET /_stats/fielddata
# 响应示例
{
"indices": {
"products": {
"total": {
"fielddata": {
"memory_size_in_bytes": 314572800,
"evictions": 10
}
}
}
}
}
优化字段数据缓存:
json
// ✗ 不好:对 text 字段排序(需要加载 fielddata)
{
"sort": [{ "name": "asc" }]
}
// ✓ 好:对 keyword 字段排序(使用 doc_values)
{
"sort": [{ "name.keyword": "asc" }]
}
// 如果必须对 text 字段排序,启用 fielddata
PUT /products/_mapping
{
"properties": {
"name": {
"type": "text",
"fielddata": true,
"fielddata_frequency_filter": {
"min": 0.001,
"max": 0.1,
"min_segment_size": 500
}
}
}
}
清除字段数据缓存:
bash
# 清除所有索引的字段数据缓存
POST /_cache/clear?fielddata=true
# 清除指定索引的字段数据缓存
POST /products/_cache/clear?fielddata=true
15.3 请求缓存(Request Cache)
请求缓存机制:
缓存内容:
- size=0 的搜索请求结果
- 聚合结果
缓存键:
- 完整的请求 JSON
- 索引名称
失效条件:
- 索引刷新(refresh)
- 手动清除
配置请求缓存:
yaml
# elasticsearch.yml
indices.requests.cache.size: 1% # 堆内存的 1%
启用/禁用请求缓存:
json
// 启用请求缓存(默认启用)
GET /products/_search?request_cache=true
{
"size": 0,
"aggs": {
"categories": {
"terms": { "field": "category" }
}
}
}
// 禁用请求缓存
GET /products/_search?request_cache=false
{
"size": 0,
"aggs": {
"categories": {
"terms": { "field": "category" }
}
}
}
监控请求缓存:
bash
GET /_stats/request_cache
# 响应示例
{
"indices": {
"products": {
"total": {
"request_cache": {
"memory_size_in_bytes": 10485760,
"evictions": 5,
"hit_count": 500,
"miss_count": 100
}
}
}
}
}
15.4 节点查询缓存(Node Query Cache)
节点查询缓存配置:
yaml
# elasticsearch.yml
indices.queries.cache.size: 10%
indices.queries.cache.count: 10000
缓存命中率优化:
java
/**
* 缓存优化工具类
*/
public class CacheOptimizer {
/**
* 分析缓存命中率
*/
public void analyzeCacheHitRate(String indexName) throws IOException {
IndicesStatsResponse stats = client.indices().stats(s -> s.index(indexName));
IndexStats indexStats = stats.indices().get(indexName);
QueryCacheStats queryCache = indexStats.total().queryCache();
long hitCount = queryCache.hitCount();
long missCount = queryCache.missCount();
long totalCount = hitCount + missCount;
double hitRate = totalCount > 0 ? (double) hitCount / totalCount * 100 : 0;
System.out.println("========== 查询缓存分析 ==========");
System.out.println("索引: " + indexName);
System.out.println("缓存命中次数: " + hitCount);
System.out.println("缓存未命中次数: " + missCount);
System.out.println("缓存命中率: " + String.format("%.2f%%", hitRate));
System.out.println("缓存大小: " + queryCache.memorySizeInBytes() / 1024 / 1024 + "MB");
System.out.println("缓存条目数: " + queryCache.cacheSize());
System.out.println("缓存驱逐次数: " + queryCache.evictions());
System.out.println("===================================");
// 建议
if (hitRate < 50) {
System.out.println("⚠️ 缓存命中率较低,建议:");
System.out.println("1. 检查查询是否适合缓存(使用 filter)");
System.out.println("2. 增加缓存大小");
System.out.println("3. 减少查询的多样性");
}
}
/**
* 预热缓存
*/
public void warmupCache(String indexName, List<String> commonQueries) {
System.out.println("开始预热缓存...");
for (String query : commonQueries) {
try {
client.search(s -> s
.index(indexName)
.query(q -> q.queryString(qs -> qs.query(query)))
.requestCache(true)
);
System.out.println("预热查询: " + query);
} catch (Exception e) {
System.err.println("预热失败: " + query + ", " + e.getMessage());
}
}
System.out.println("缓存预热完成");
}
}
十六、线程
池优化
16.1 线程池类型
Elasticsearch使用多种线程池处理不同类型的操作:
线程池类型及用途:
1. search 线程池
- 用途:处理搜索请求
- 默认大小:int((核心数 * 3) / 2) + 1
- 队列大小:1000
2. write 线程池
- 用途:处理索引、删除、更新请求
- 默认大小:核心数
- 队列大小:10000
3. get 线程池
- 用途:处理GET请求
- 默认大小:核心数
- 队列大小:1000
4. analyze 线程池
- 用途:处理分析请求
- 默认大小:1
- 队列大小:16
5. management 线程池
- 用途:处理集群管理任务
- 默认大小:5
- 队列大小:无界
16.2 线程池配置优化
yaml
# elasticsearch.yml
# 搜索线程池配置
thread_pool:
search:
size: 30 # 线程数
queue_size: 2000 # 队列大小
write:
size: 16
queue_size: 10000
get:
size: 16
queue_size: 1000
配置建议:
- search线程池:根据查询负载调整,避免过大导致内存压力
- write线程池:根据写入负载调整,通常设置为CPU核心数
- 队列大小:适当增加可以缓冲突发流量,但过大会导致请求延迟
- 监控拒绝次数:如果频繁拒绝,需要增加线程数或队列大小
16.3 线程池监控
java
/**
* 线程池监控工具
*/
public class ThreadPoolMonitor {
private final ElasticsearchClient client;
/**
* 获取线程池统计信息
*/
public void monitorThreadPools() throws IOException {
NodesStatsResponse stats = client.nodes().stats();
stats.nodes().forEach((nodeId, nodeStats) -> {
System.out.println("节点: " + nodeStats.name());
// 遍历所有线程池
nodeStats.threadPool().forEach((poolName, poolStats) -> {
System.out.println("\n线程池: " + poolName);
System.out.println(" 活跃线程数: " + poolStats.active());
System.out.println(" 线程数: " + poolStats.threads());
System.out.println(" 队列任务数: " + poolStats.queue());
System.out.println(" 已完成任务数: " + poolStats.completed());
System.out.println(" 拒绝次数: " + poolStats.rejected());
// 告警检查
if (poolStats.rejected() > 0) {
System.out.println(" ⚠️ 警告:线程池有拒绝请求!");
}
if (poolStats.queue() > 500) {
System.out.println(" ⚠️ 警告:队列积压较多!");
}
});
});
}
/**
* 分析线程池瓶颈
*/
public void analyzeThreadPoolBottleneck() throws IOException {
NodesStatsResponse stats = client.nodes().stats();
System.out.println("=== 线程池瓶颈分析 ===\n");
stats.nodes().forEach((nodeId, nodeStats) -> {
nodeStats.threadPool().forEach((poolName, poolStats) -> {
long rejected = poolStats.rejected();
long queue = poolStats.queue();
long active = poolStats.active();
long threads = poolStats.threads();
// 计算线程池使用率
double utilization = (double) active / threads * 100;
if (rejected > 0 || queue > 1000 || utilization > 80) {
System.out.println("节点: " + nodeStats.name() + ", 线程池: " + poolName);
System.out.println(" 线程使用率: " + String.format("%.2f%%", utilization));
System.out.println(" 队列积压: " + queue);
System.out.println(" 拒绝次数: " + rejected);
// 优化建议
System.out.println(" 优化建议:");
if (rejected > 0) {
System.out.println(" - 增加线程池大小");
System.out.println(" - 增加队列大小");
System.out.println(" - 优化查询性能");
}
if (utilization > 80) {
System.out.println(" - 线程池接近饱和,考虑扩容");
}
System.out.println();
}
});
});
}
}
16.4 线程池调优实战
场景:搜索请求频繁被拒绝
java
/**
* 搜索线程池调优
*/
public class SearchThreadPoolTuning {
/**
* 步骤1:检查当前配置
*/
public void checkCurrentConfig() {
System.out.println("当前搜索线程池配置:");
System.out.println("GET /_nodes/stats/thread_pool");
System.out.println("GET /_cluster/settings");
}
/**
* 步骤2:调整线程池大小
*/
public void adjustThreadPoolSize(ElasticsearchClient client) throws IOException {
// 动态调整(临时生效)
String settings = """
{
"transient": {
"thread_pool.search.size": 30,
"thread_pool.search.queue_size": 2000
}
}
""";
System.out.println("调整搜索线程池配置:");
System.out.println("PUT /_cluster/settings");
System.out.println(settings);
// 永久配置需要修改 elasticsearch.yml
System.out.println("\n永久配置(elasticsearch.yml):");
System.out.println("thread_pool:");
System.out.println(" search:");
System.out.println(" size: 30");
System.out.println(" queue_size: 2000");
}
/**
* 步骤3:监控调整效果
*/
public void monitorAdjustmentEffect() {
System.out.println("监控指标:");
System.out.println("1. 拒绝次数是否下降");
System.out.println("2. 队列积压是否减少");
System.out.println("3. 响应时间是否改善");
System.out.println("4. CPU使用率是否合理");
}
}
十七、集群扩容策略
17.1 何时需要扩容
扩容信号:
1. 性能指标
- CPU使用率持续 > 80%
- 内存使用率持续 > 85%
- 磁盘使用率 > 80%
- 查询响应时间持续增长
2. 容量指标
- 数据量快速增长
- 分片数量过多
- 单个分片过大(> 50GB)
3. 稳定性指标
- 频繁出现GC
- 线程池拒绝增多
- 节点频繁离线
17.2 扩容方案选择
┌─────────────────────────────────────────────────────────────┐
│ 扩容方案对比 │
├─────────────┬─────────────┬─────────────┬──────────────────┤
│ 方案 │ 适用场景 │ 优点 │ 缺点 │
├─────────────┼─────────────┼─────────────┼──────────────────┤
│ 垂直扩容 │ 单节点性能 │ 实施简单 │ 成本高,有上限 │
│ (升级配置) │ 不足 │ 无需迁移 │ │
├─────────────┼─────────────┼─────────────┼──────────────────┤
│ 水平扩容 │ 整体容量 │ 成本低 │ 需要数据迁移 │
│ (增加节点) │ 不足 │ 可线性扩展 │ 实施复杂 │
├─────────────┼─────────────┼─────────────┼──────────────────┤
│ 混合扩容 │ 综合场景 │ 灵活性高 │ 规划复杂 │
└─────────────┴─────────────┴─────────────┴──────────────────┘
17.3 水平扩容实施步骤
java
/**
* 集群扩容管理器
*/
public class ClusterScalingManager {
private final ElasticsearchClient client;
/**
* 步骤1:评估扩容需求
*/
public void assessScalingNeeds() throws IOException {
System.out.println("=== 扩容需求评估 ===\n");
// 1. 检查集群健康状态
HealthResponse health = client.cluster().health();
System.out.println("集群状态: " + health.status());
System.out.println("节点数: " + health.numberOfNodes());
System.out.println("数据节点数: " + health.numberOfDataNodes());
// 2. 检查节点资源使用情况
NodesStatsResponse stats = client.nodes().stats();
stats.nodes().forEach((nodeId, nodeStats) -> {
System.out.println("\n节点: " + nodeStats.name());
// CPU使用率
if (nodeStats.os() != null && nodeStats.os().cpu() != null) {
System.out.println(" CPU使用率: " + nodeStats.os().cpu().percent() + "%");
}
// 内存使用率
if (nodeStats.jvm() != null && nodeStats.jvm().mem() != null) {
long used = nodeStats.jvm().mem().heapUsedInBytes();
long max = nodeStats.jvm().mem().heapMaxInBytes();
double memPercent = (double) used / max * 100;
System.out.println(" 堆内存使用率: " + String.format("%.2f%%", memPercent));
}
// 磁盘使用率
if (nodeStats.fs() != null && nodeStats.fs().total() != null) {
long total = nodeStats.fs().total().totalInBytes();
long available = nodeStats.fs().total().availableInBytes();
double diskPercent = (double) (total - available) / total * 100;
System.out.println(" 磁盘使用率: " + String.format("%.2f%%", diskPercent));
}
});
// 3. 检查分片分布
System.out.println("\n分片统计:");
System.out.println(" 活跃分片数: " + health.activeShards());
System.out.println(" 主分片数: " + health.activePrimaryShards());
System.out.println(" 迁移中分片数: " + health.relocatingShards());
System.out.println(" 未分配分片数: " + health.unassignedShards());
}
/**
* 步骤2:准备新节点
*/
public void prepareNewNodes() {
System.out.println("\n=== 准备新节点 ===\n");
System.out.println("1. 安装Elasticsearch(版本与集群一致)");
System.out.println("2. 配置elasticsearch.yml:");
System.out.println(" cluster.name: my-cluster");
System.out.println(" node.name: node-4");
System.out.println(" network.host: 192.168.1.14");
System.out.println(" discovery.seed_hosts: [\"192.168.1.11\", \"192.168.1.12\"]");
System.out.println(" cluster.initial_master_nodes: [\"node-1\", \"node-2\", \"node-3\"]");
System.out.println("\n3. 配置JVM参数(与现有节点保持一致)");
System.out.println("4. 启动节点");
}
/**
* 步骤3:控制分片迁移速度
*/
public void controlShardRebalancing(ElasticsearchClient client) {
System.out.println("\n=== 控制分片迁移 ===\n");
String settings = """
{
"transient": {
"cluster.routing.allocation.node_concurrent_recoveries": 2,
"indices.recovery.max_bytes_per_sec": "40mb",
"cluster.routing.allocation.cluster_concurrent_rebalance": 2
}
}
""";
System.out.println("调整迁移速度配置:");
System.out.println("PUT /_cluster/settings");
System.out.println(settings);
System.out.println("\n配置说明:");
System.out.println("- node_concurrent_recoveries: 每个节点同时恢复的分片数");
System.out.println("- max_bytes_per_sec: 每秒传输的最大字节数");
System.out.println("- cluster_concurrent_rebalance: 集群同时重平衡的分片数");
}
/**
* 步骤4:监控扩容过程
*/
public void monitorScaling() throws IOException {
System.out.println("\n=== 监控扩容过程 ===\n");
HealthResponse health = client.cluster().health();
System.out.println("集群状态: " + health.status());
System.out.println("迁移中分片数: " + health.relocatingShards());
System.out.println("初始化中分片数: " + health.initializingShards());
System.out.println("未分配分片数: " + health.unassignedShards());
// 监控恢复进度
System.out.println("\n查看恢复进度:");
System.out.println("GET /_cat/recovery?v&active_only=true");
// 监控节点负载
System.out.println("\n查看节点负载:");
System.out.println("GET /_cat/nodes?v&h=name,cpu,heap.percent,disk.used_percent");
}
/**
* 步骤5:验证扩容效果
*/
public void verifyScalingEffect() throws IOException {
System.out.println("\n=== 验证扩容效果 ===\n");
// 1. 检查分片分布
System.out.println("1. 检查分片分布是否均衡");
System.out.println("GET /_cat/shards?v");
// 2. 检查性能指标
System.out.println("\n2. 检查性能指标");
System.out.println("- 查询响应时间是否降低");
System.out.println("- CPU使用率是否下降");
System.out.println("- 内存使用率是否下降");
// 3. 检查稳定性
System.out.println("\n3. 检查稳定性");
System.out.println("- 是否还有线程池拒绝");
System.out.println("- GC频率是否降低");
System.out.println("- 节点是否稳定");
}
}
17.4 扩容最佳实践
扩容前准备:
1. 备份数据
- 创建快照备份
- 验证备份可用性
2. 评估容量需求
- 计算未来3-6个月的数据增长
- 预留30%的冗余空间
3. 选择扩容时机
- 业务低峰期
- 避免重要活动期间
- 预留足够的时间窗口
4. 准备回滚方案
- 记录当前配置
- 准备快速回滚步骤
扩容中注意事项:
1. 控制迁移速度
- 避免对现有业务造成影响
- 监控网络带宽使用
- 监控磁盘IO
2. 分批次扩容
- 一次增加1-2个节点
- 等待分片重平衡完成
- 验证稳定后再继续
3. 实时监控
- 集群健康状态
- 节点资源使用
- 业务指标
扩容后验证:
1. 功能验证
- 搜索功能正常
- 写入功能正常
- 聚合功能正常
2. 性能验证
- 响应时间符合预期
- 吞吐量提升
- 资源使用合理
3. 稳定性验证
- 运行24小时无异常
- 无频繁GC
- 无线程池拒绝
十八、性能测试与压测
18.1 性能测试工具
java
/**
* Elasticsearch性能测试工具
*/
public class PerformanceTestTool {
private final ElasticsearchClient client;
private final ExecutorService executor;
public PerformanceTestTool(ElasticsearchClient client) {
this.client = client;
this.executor = Executors.newFixedThreadPool(10);
}
/**
* 写入性能测试
*/
public void testWritePerformance(String indexName, int totalDocs, int concurrency) {
System.out.println("=== 写入性能测试 ===");
System.out.println("索引: " + indexName);
System.out.println("文档数: " + totalDocs);
System.out.println("并发数: " + concurrency);
long startTime = System.currentTimeMillis();
CountDownLatch latch = new CountDownLatch(totalDocs);
AtomicInteger successCount = new AtomicInteger(0);
AtomicInteger failCount = new AtomicInteger(0);
// 分批写入
int batchSize = totalDocs / concurrency;
for (int i = 0; i < concurrency; i++) {
final int start = i * batchSize;
final int end = (i == concurrency - 1) ? totalDocs : (i + 1) * batchSize;
executor.submit(() -> {
for (int j = start; j < end; j++) {
try {
// 创建测试文档
Map<String, Object> doc = new HashMap<>();
doc.put("id", j);
doc.put("title", "Test Document " + j);
doc.put("content", "This is test content for document " + j);
doc.put("timestamp", System.currentTimeMillis());
// 写入文档
client.index(idx -> idx
.index(indexName)
.id(String.valueOf(j))
.document(doc)
);
successCount.incrementAndGet();
} catch (Exception e) {
failCount.incrementAndGet();
} finally {
latch.countDown();
}
}
});
}
try {
latch.await();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
long endTime = System.currentTimeMillis();
long duration = endTime - startTime;
// 输出测试结果
System.out.println("\n测试结果:");
System.out.println("总耗时: " + duration + "ms");
System.out.println("成功数: " + successCount.get());
System.out.println("失败数: " + failCount.get());
System.out.println("TPS: " + (successCount.get() * 1000.0 / duration));
System.out.println("平均延迟: " + (duration * 1.0 / successCount.get()) + "ms");
}
/**
* 查询性能测试
*/
public void testSearchPerformance(String indexName, List<String> queries, int iterations) {
System.out.println("\n=== 查询性能测试 ===");
System.out.println("索引: " + indexName);
System.out.println("查询数: " + queries.size());
System.out.println("迭代次数: " + iterations);
List<Long> latencies = new ArrayList<>();
AtomicInteger successCount = new AtomicInteger(0);
AtomicInteger failCount = new AtomicInteger(0);
long startTime = System.currentTimeMillis();
for (int i = 0; i < iterations; i++) {
for (String query : queries) {
long queryStart = System.currentTimeMillis();
try {
client.search(s -> s
.index(indexName)
.query(q -> q.queryString(qs -> qs.query(query)))
, Map.class);
long queryEnd = System.currentTimeMillis();
latencies.add(queryEnd - queryStart);
successCount.incrementAndGet();
} catch (Exception e) {
failCount.incrementAndGet();
}
}
}
long endTime = System.currentTimeMillis();
long duration = endTime - startTime;
// 计算统计指标
Collections.sort(latencies);
long minLatency = latencies.get(0);
long maxLatency = latencies.get(latencies.size() - 1);
long avgLatency = latencies.stream().mapToLong(Long::longValue).sum() / latencies.size();
long p50 = latencies.get((int) (latencies.size() * 0.5));
long p95 = latencies.get((int) (latencies.size() * 0.95));
long p99 = latencies.get((int) (latencies.size() * 0.99));
// 输出测试结果
System.out.println("\n测试结果:");
System.out.println("总耗时: " + duration + "ms");
System.out.println("成功数: " + successCount.get());
System.out.println("失败数: " + failCount.get());
System.out.println("QPS: " + (successCount.get() * 1000.0 / duration));
System.out.println("\n延迟统计:");
System.out.println("最小延迟: " + minLatency + "ms");
System.out.println("最大延迟: " + maxLatency + "ms");
System.out.println("平均延迟: " + avgLatency + "ms");
System.out.println("P50延迟: " + p50 + "ms");
System.out.println("P95延迟: " + p95 + "ms");
System.out.println("P99延迟: " + p99 + "ms");
}
/**
* 混合负载测试
*/
public void testMixedWorkload(String indexName, int duration) {
System.out.println("\n=== 混合负载测试 ===");
System.out.println("索引: " + indexName);
System.out.println("持续时间: " + duration + "秒");
AtomicInteger writeCount = new AtomicInteger(0);
AtomicInteger searchCount = new AtomicInteger(0);
AtomicBoolean running = new AtomicBoolean(true);
// 写入线程
Thread writeThread = new Thread(() -> {
int id = 0;
while (running.get()) {
try {
Map<String, Object> doc = new HashMap<>();
doc.put("id", id++);
doc.put("content", "Test content " + id);
client.index(idx -> idx
.index(indexName)
.document(doc)
);
writeCount.incrementAndGet();
Thread.sleep(10); // 控制写入速度
} catch (Exception e) {
// 忽略错误
}
}
});
// 查询线程
Thread searchThread = new Thread(() -> {
while (running.get()) {
try {
client.search(s -> s
.index(indexName)
.query(q -> q.matchAll(m -> m))
, Map.class);
searchCount.incrementAndGet();
Thread.sleep(50); // 控制查询速度
} catch (Exception e) {
// 忽略错误
}
}
});
// 启动测试
long startTime = System.currentTimeMillis();
writeThread.start();
searchThread.start();
// 等待指定时间
try {
Thread.sleep(duration * 1000L);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
// 停止测试
running.set(false);
try {
writeThread.join();
searchThread.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
long endTime = System.currentTimeMillis();
long actualDuration = (endTime - startTime) / 1000;
// 输出测试结果
System.out.println("\n测试结果:");
System.out.println("实际运行时间: " + actualDuration + "秒");
System.out.println("写入总数: " + writeCount.get());
System.out.println("查询总数: " + searchCount.get());
System.out.println("写入TPS: " + (writeCount.get() / actualDuration));
System.out.println("查询QPS: " + (searchCount.get() / actualDuration));
}
public void shutdown() {
executor.shutdown();
}
}
18.2 使用Rally进行压测
Rally是Elasticsearch官方的压测工具。
安装Rally:
bash
# 使用pip安装
pip3 install esrally
# 验证安装
esrally --version
运行基准测试:
bash
# 使用内置的测试数据集
esrally race --distribution-version=8.0.0 --track=geonames
# 指定Elasticsearch集群
esrally race --pipeline=benchmark-only \
--target-hosts=localhost:9200 \
--track=geonames
# 自定义测试参数
esrally race --pipeline=benchmark-only \
--target-hosts=localhost:9200 \
--track=geonames \
--challenge=append-no-conflicts \
--client-options="timeout:60"
查看测试结果:
bash
# 列出所有测试
esrally list races
# 查看测试详情
esrally compare --baseline=<race-id-1> --contender=<race-id-2>
18.3 压测场景设计
java
/**
* 压测场景管理器
*/
public class LoadTestScenario {
/**
* 场景1:日常负载测试
*/
public void dailyLoadTest() {
System.out.println("=== 日常负载测试 ===");
System.out.println("目标:模拟正常业务负载");
System.out.println("写入TPS: 1000");
System.out.println("查询QPS: 5000");
System.out.println("持续时间: 1小时");
System.out.println("并发用户: 100");
}
/**
* 场景2:峰值负载测试
*/
public void peakLoadTest() {
System.out.println("\n=== 峰值负载测试 ===");
System.out.println("目标:模拟业务高峰期负载");
System.out.println("写入TPS: 5000");
System.out.println("查询QPS: 20000");
System.out.println("持续时间: 30分钟");
System.out.println("并发用户: 500");
}
/**
* 场景3:压力测试
*/
public void stressTest() {
System.out.println("\n=== 压力测试 ===");
System.out.println("目标:找到系统极限");
System.out.println("策略:逐步增加负载直到系统崩溃");
System.out.println("起始TPS: 1000");
System.out.println("每分钟增加: 500");
System.out.println("观察指标:");
System.out.println("- 响应时间");
System.out.println("- 错误率");
System.out.println("- 资源使用率");
}
/**
* 场景4:稳定性测试
*/
public void stabilityTest() {
System.out.println("\n=== 稳定性测试 ===");
System.out.println("目标:验证长时间运行的稳定性");
System.out.println("写入TPS: 2000");
System.out.println("查询QPS: 10000");
System.out.println("持续时间: 24小时");
System.out.println("观察指标:");
System.out.println("- 内存泄漏");
System.out.println("- GC频率");
System.out.println("- 性能衰减");
}
}
十九、监控告警体系
19.1 关键监控指标
┌─────────────────────────────────────────────────────────────┐
│ 监控指标体系 │
├──────────────┬──────────────────┬──────────────────────────┤
│ 类别 │ 指标 │ 告警阈值 │
├──────────────┼──────────────────┼──────────────────────────┤
│ 集群健康 │ 集群状态 │ 非GREEN状态 │
│ │ 节点数量 │ 减少 │
│ │ 未分配分片 │ > 0 │
├──────────────┼──────────────────┼──────────────────────────┤
│ 性能指标 │ 查询延迟 │ P99 > 1000ms │
│ │ 索引延迟 │ P99 > 500ms │
│ │ 查询QPS │ 下降 > 20% │
├──────────────┼──────────────────┼──────────────────────────┤
│ 资源使用 │ CPU使用率 │ > 80% │
│ │ 内存使用率 │ > 85% │
│ │ 磁盘使用率 │ > 80% │
│ │ JVM堆使用率 │ > 85% │
├──────────────┼──────────────────┼──────────────────────────┤
│ 线程池 │ 拒绝次数 │ > 0 │
│ │ 队列积压 │ > 1000 │
│ │ 活跃线程比例 │ > 80% │
├──────────────┼──────────────────┼──────────────────────────┤
│ GC │ GC频率 │ > 10次/分钟 │
│ │ GC耗时 │ > 1秒 │
│ │ Full GC次数 │ > 0 │
└──────────────┴──────────────────┴──────────────────────────┘
19.2 监控系统集成
java
/**
* Prometheus监控集成
*/
public class PrometheusMonitoring {
/**
* 配置Prometheus监控
*/
public void configurePrometheus() {
System.out.println("=== Prometheus监控配置 ===\n");
System.out.println("1. 安装Prometheus Exporter插件");
System.out.println("bin/elasticsearch-plugin install prometheus-exporter");
System.out.println("\n2. 配置Prometheus抓取");
String prometheusConfig = """
scrape_configs:
- job_name: 'elasticsearch'
static_configs:
- targets: ['localhost:9200']
metrics_path: '/_prometheus/metrics'
""";
System.out.println(prometheusConfig);
System.out.println("\n3. 重启Elasticsearch");
System.out.println("\n4. 验证指标暴露");
System.out.println("curl http://localhost:9200/_prometheus/metrics");
}
/**
* 常用Prometheus查询
*/
public void commonPrometheusQueries() {
System.out.println("\n=== 常用Prometheus查询 ===\n");
System.out.println("1. 查询QPS");
System.out.println("rate(es_search_query_total[5m])");
System.out.println("\n2. 查询延迟P99");
System.out.println("histogram_quantile(0.99, rate(es_search_query_time_seconds_bucket[5m]))");
System.out.println("\n3. JVM堆使用率");
System.out.println("es_jvm_mem_heap_used_percent");
System.out.println("\n4. CPU使用率");
System.out.println("es_os_cpu_percent");
System.out.println("\n5. 磁盘使用率");
System.out.println("(es_fs_total_total_in_bytes - es_fs_total_available_in_bytes) / es_fs_total_total_in_bytes * 100");
}
}
19.3 告警规则配置
yaml
# Prometheus告警规则
groups:
- name: elasticsearch_alerts
interval: 30s
rules:
# 集群状态告警
- alert: ElasticsearchClusterNotHealthy
expr: elasticsearch_cluster_health_status{color="red"} == 1
for: 5m
labels:
severity: critical
annotations:
summary: "Elasticsearch集群状态异常"
description: "集群{{ $labels.cluster }}状态为RED"
# 节点离线告警
- alert: ElasticsearchNodeDown
expr: up{job="elasticsearch"} == 0
for: 2m
labels:
severity: critical
annotations:
summary: "Elasticsearch节点离线"
description: "节点{{ $labels.instance }}已离线"
# CPU使用率告警
- alert: ElasticsearchHighCPU
expr: es_os_cpu_percent > 80
for: 10m
labels:
severity: warning
annotations:
summary: "Elasticsearch CPU使用率过高"
description: "节点{{ $labels.node }}的CPU使用率为{{ $value }}%"
# 内存使用率告警
- alert: ElasticsearchHighMemory
expr: es_jvm_mem_heap_used_percent > 85
for: 10m
labels:
severity: warning
annotations:
summary: "Elasticsearch内存使用率过高"
description: "节点{{ $labels.node }}的堆内存使用率为{{ $value }}%"
# 磁盘使用率告警
- alert: ElasticsearchHighDiskUsage
expr: (es_fs_total_total_in_bytes - es_fs_total_available_in_bytes) / es_fs_total_total_in_bytes * 100 > 80
for: 5m
labels:
severity: warning
annotations:
summary: "Elasticsearch磁盘使用率过高"
description: "节点{{ $labels.node }}的磁盘使用率为{{ $value }}%"
# 查询延迟告警
- alert: ElasticsearchHighQueryLatency
expr: histogram_quantile(0.99, rate(es_search_query_time_seconds_bucket[5m])) > 1
for: 5m
labels:
severity: warning
annotations:
summary: "Elasticsearch查询延迟过高"
description: "P99查询延迟为{{ $value }}秒"
# 线程池拒绝告警
- alert: ElasticsearchThreadPoolRejections
expr: rate(es_threadpool_rejected_count[5m]) > 0
for: 2m
labels:
severity: warning
annotations:
summary: "Elasticsearch线程池拒绝请求"
description: "节点{{ $labels.node }}的{{ $labels.name }}线程池正在拒绝请求"
# GC频率告警
- alert: ElasticsearchFrequentGC
expr: rate(es_jvm_gc_collection_seconds_count[5m]) > 10
for: 5m
labels:
severity: warning
annotations:
summary: "Elasticsearch GC频率过高"
description: "节点{{ $labels.node }}的GC频率为{{ $value }}次/分钟"
二十、性能优化检查清单
20.1 索引设计检查
索引设计优化检查清单:
□ 分片数量
□ 主分片数量是否合理(建议:分片大小20-50GB)
□ 副本数量是否合理(建议:至少1个副本)
□ 是否存在过度分片问题
□ 映射设计
□ 字段类型是否正确
□ 是否禁用了不需要的功能(_source、doc_values)
□ 是否使用了合适的分词器
□ 数值字段是否使用了正确的类型
□ 索引设置
□ refresh_interval是否合理(写入密集型可设置为30s或-1)
□ number_of_replicas是否合理
□ translog设置是否优化
□ 索引模板
□ 是否使用了索引模板
□ 索引模板配置是否合理
□ 是否使用了索引生命周期管理
20.2 查询优化检查
查询优化检查清单:
□ 查询语句
□ 是否使用了filter而非query(可缓存)
□ 是否避免了深度分页
□ 是否使用了合适的查询类型
□ 是否限制了返回字段(_source_includes)
□ 聚合优化
□ 是否使用了合适的聚合类型
□ 是否限制了聚合的bucket数量
□ 是否使用了doc_values
□ 是否考虑了预聚合
□ 缓存利用
□ 是否启用了查询缓存
□ 是否启用了请求缓存
□ 缓存命中率是否合理
□ 并发控制
□ 是否控制了并发查询数
□ 是否使用了连接池
□ 是否设置了合理的超时时间
20.3 写入优化检查
写入优化检查清单:
□ 批量写入
□ 是否使用了bulk API
□ bulk大小是否合理(建议:5-15MB)
□ 是否控制了并发写入数
□ 索引配置
□ refresh_interval是否调大
□ 副本数是否临时设置为0
□ translog是否优化
□ 文档设计
□ 文档大小是否合理
□ 是否避免了嵌套文档
□ 字段数量是否合理
□ 硬件资源
□ 磁盘IO是否充足
□ 网络带宽是否充足
□ CPU资源是否充足
20.4 集群配置检查
集群配置检查清单:
□ JVM配置
□ 堆内存大小是否合理(不超过32GB)
□ GC算法是否合适
□ GC参数是否优化
□ 系统配置
□ 文件描述符是否足够
□ 虚拟内存是否禁用
□ swap是否禁用
□ 网络配置
□ 网络带宽是否充足
□ 网络延迟是否低
□ 是否配置了合理的超时时间
□ 磁盘配置
□ 是否使用了SSD
□ RAID配置是否合理
□ 磁盘空间是否充足
□ 线程池配置
□ 线程池大小是否合理
□ 队列大小是否合理
□ 是否监控拒绝次数
20.5 监控告警检查
监控告警检查清单:
□ 监控指标
□ 是否监控集群健康状态
□ 是否监控节点资源使用
□ 是否监控查询性能
□ 是否监控写入性能
□ 是否监控GC情况
□ 告警规则
□ 是否配置了集群状态告警
□ 是否配置了资源使用告警
□ 是否配置了性能告警
□ 是否配置了错误告警
□ 日志管理
□ 是否收集了Elasticsearch日志
□ 是否分析了慢查询日志
□ 是否分析了GC日志
□ 定期检查
□ 是否定期检查集群健康
□ 是否定期检查性能指标
□ 是否定期检查磁盘空间
□ 是否定期检查索引大小
练习题
练习1:诊断慢查询
场景:某个查询的P99延迟达到5秒,需要优化。
任务:
- 使用Profile API分析查询性能
- 找出性能瓶颈
- 提出优化方案
- 验证优化效果
提示:
- 检查是否使用了合适的查询类型
- 检查是否有深度分页
- 检查字段是否建立了索引
- 检查是否可以使用filter代替query
练习2:优化写入性能
场景:需要导入1亿条数据,当前速度为1000 TPS,需要提升到10000 TPS。
任务:
- 分析当前写入瓶颈
- 制定优化方案
- 实施优化
- 验证优化效果
提示:
- 使用bulk API
- 调整refresh_interval
- 临时关闭副本
- 增加写入并发
练习3:解决内存问题
场景:集群频繁出现Full GC,影响服务稳定性。
任务:
- 分析GC日志
- 找出内存泄漏原因
- 制定解决方案
- 验证解决效果
提示:
- 检查堆内存配置
- 检查fielddata使用
- 检查查询复杂度
- 检查聚合操作
练习4:集群扩容
场景:集群CPU使用率持续在90%以上,需要扩容。
任务:
- 评估扩容需求
- 制定扩容方案
- 实施扩容
- 验证扩容效果
提示:
- 评估当前资源使用
- 计算需要增加的节点数
- 控制分片迁移速度
- 监控扩容过程
练习5:性能压测
场景:新集群上线前需要进行性能压测。
任务:
- 设计压测场景
- 准备测试数据
- 执行压测
- 分析测试结果
提示:
- 模拟真实业务负载
- 测试不同并发下的性能
- 找出系统瓶颈
- 制定优化建议
学习检查清单
完成本章学习后,你应该能够:
基础能力:
- 理解Elasticsearch的性能瓶颈来源
- 掌握性能问题的诊断方法
- 了解各种性能优化手段
写入优化:
- 掌握bulk API的使用
- 理解refresh机制及优化方法
- 掌握translog的配置优化
- 能够优化索引设置提升写入性能
查询优化:
- 掌握Profile API分析查询性能
- 理解查询缓存机制
- 掌握filter和query的区别
- 能够优化慢查询
聚合优化:
- 理解聚合的性能影响因素
- 掌握聚合优化技巧
- 能够使用预聚合提升性能
内存优化:
- 理解Elasticsearch的内存使用
- 掌握JVM堆内存配置
- 能够优化fielddata使用
- 能够分析和解决内存问题
磁盘优化:
- 理解磁盘IO对性能的影响
- 掌握磁盘配置优化方法
- 能够监控磁盘使用情况
JVM调优:
- 掌握JVM参数配置
- 理解GC算法的选择
- 能够分析GC日志
- 能够解决GC问题
分片优化:
- 理解分片大小对性能的影响
- 掌握分片数量的计算方法
- 能够优化分片分配策略
慢查询分析:
- 掌握慢查询日志的配置
- 能够分析慢查询原因
- 能够优化慢查询
实战案例:
- 能够诊断和解决实际性能问题
- 能够制定性能优化方案
- 能够进行性能测试和压测
缓存优化:
- 理解各种缓存的作用
- 掌握缓存配置方法
- 能够监控缓存效果
线程池优化:
- 理解线程池的作用
- 掌握线程池配置方法
- 能够监控线程池状态
集群扩容:
- 能够评估扩容需求
- 掌握扩容实施步骤
- 能够监控扩容过程
性能测试:
- 掌握性能测试工具的使用
- 能够设计压测场景
- 能够分析测试结果
监控告警:
- 掌握关键监控指标
- 能够配置监控系统
- 能够配置告警规则
综合能力:
- 能够进行全面的性能优化
- 能够制定性能优化检查清单
- 能够指导团队进行性能优化
- 能够在面试中清晰描述性能优化经验