Bạn đã tới đây. Bài 28, bài cuối của series Kibana từ A đến Z. Cluster bạn đã có log shipper ổn, dashboard versioned trong git, Terraform manage role và alert, runbook recovery cho disk full. Còn một câu hỏi: làm sao tune cluster cho performance tối ưu mà không tốn x2 node?

Performance tuning ES là một rabbit hole. Có người dành cả sự nghiệp chỉ làm việc này. Bài này không cố cover tất cả, mà tập trung vào 5 dial có tỉ lệ leverage cao nhất: JVM heap, GC, field caps cache, merge throttling, refresh interval. Sau đó là circuit breaker để bạn không OOM khi user gõ aggregation điên rồ.

Mục tiêu bài:

  • Sizing JVM heap đúng cho workload
  • Hiểu G1GC vs CMS, khi nào cần đổi
  • Tune field caps cache cho Kibana Discover nhanh
  • Throttle merge để không kill ingest
  • Set circuit breaker tránh OOM

Phần 1: JVM heap

Heap là biến số quan trọng nhất. Sai một bước, cả cluster hành xử kỳ lạ.

Quy tắc vàng

ES guideline:

  • Tối đa 50% RAM của machine, nhường nửa còn lại cho OS file system cache (Lucene đọc data qua FS cache).
  • Tối đa 31 GB (chính xác là 30.5-31 GB tuỳ JVM build). Trên 32 GB, JVM mất compressed OOPs, tốn ~30% RAM cho cùng object count.

Bảng tham khảo:

Total RAMHeapOS cache
16 GB8 GB8 GB
32 GB16 GB16 GB
64 GB30 GB34 GB
128 GB30 GB98 GB

Trên 64 GB: nếu workload là search heavy, không tăng heap quá 31 GB. RAM dư hơn nhường hết cho FS cache để search nhanh.

Cách set

/etc/elasticsearch/jvm.options.d/heap.options:

-Xms16g
-Xmx16g

XmsXmx luôn bằng nhau. Khác = JVM resize heap on the fly, tốn CPU và có thể trigger GC pauses.

Verify compressed OOPs

Check log ES startup:

heap size [16gb], compressed ordinary object pointers [true]

Nếu [false] = mất compressed OOPs, hoặc heap quá lớn, hoặc JVM version cũ. Giảm heap.

Pattern: monitoring heap usage

curl -sS "http://es:9200/_cat/nodes?v&h=name,heap.percent,heap.current,heap.max"
name  heap.percent heap.current heap.max
es-1  68           10.8gb       16gb
es-2  72           11.5gb       16gb
es-3  65           10.4gb       16gb

70% bình thường. Nếu thấy 90%+ kéo dài = sắp OOM. Đo old-gen riêng:

curl -sS "http://es:9200/_nodes/stats/jvm?pretty" \
  | jq '.nodes[] | .name, .jvm.mem.pools.old'

Old-gen 90%+ chronic = leak hoặc workload thực sự lớn, cần scale.

Phần 2: GC tuning

ES 7+ mặc định dùng G1GC (Garbage First). Tốt cho heap lớn.

Khi G1GC vẫn không đủ

Triệu chứng:

  • Log ES báo gc overhead limit exceeded
  • _nodes/hot_threads thấy GC time cao
  • Query latency spike đều đặn mỗi 30-60 giây

Tune G1GC qua jvm.options.d/g1.options:

-XX:MaxGCPauseMillis=200
-XX:InitiatingHeapOccupancyPercent=30
-XX:G1ReservePercent=25
  • MaxGCPauseMillis: target pause time. 200ms là default ES, không nên thấp hơn 100ms.
  • InitiatingHeapOccupancyPercent: bắt đầu concurrent cycle khi heap đạt N%. Giảm để GC chạy sớm hơn, tránh full GC.
  • G1ReservePercent: reserve để tránh promotion failure.

Log GC ra file

-Xlog:gc*,gc+age=trace,safepoint:file=/var/log/elasticsearch/gc.log:utctime,pid,tags:filecount=32,filesize=64m

GC log là vàng khi debug performance. Phân tích bằng tool như GCEasy hoặc GCViewer.

Anti-pattern: thay đổi GC ngầm

Đừng thử ZGC hoặc Shenandoah trên ES production trừ khi đã benchmark kỹ. ES không qualify với GC này officially. Một số plugin và internal code assume G1 behavior.

Phần 3: Field caps cache

Field caps API trả về schema của index (field names + types). Kibana Discover gọi liên tục mỗi khi user open data view. Khi cluster có 1000+ index, mỗi gọi tốn 5-15 giây.

Tune cache

curl -X PUT "http://es:9200/_cluster/settings" \
  -H 'Content-Type: application/json' \
  -d '{
    "persistent": {
      "indices.field_caps.cache.enabled": true,
      "indices.field_caps.cache.size": "10%"
    }
  }'

size: 10% = 10% heap dành cho cache field caps. Default 1-2%, tăng lên cho heavy Kibana traffic.

Verify cache hit

curl -sS "http://es:9200/_nodes/stats/indices/field_data?pretty"

Sau khi enable + 1 ngày traffic, cache hit ratio nên 80%+. Nếu thấp = cache size chưa đủ.

Pattern: consolidate data view

Nếu Kibana có 50 data view với pattern overlap (app-logs-team1-*, app-logs-team2-*…), Discover sẽ resolve field caps cho mỗi cái. Consolidate thành app-logs-* với filter trong saved search, giảm số call field caps.

Phần 4: Merge throttling

ES merge segment liên tục để giảm số file Lucene. Merge nặng CPU + IO, có thể kill ingest nếu không throttle.

Default behavior

ES tự throttle dựa trên ingest rate. Nhưng trong burst ingest (deploy mới, log spike), default không đủ.

Setting per index:

curl -X PUT "http://es:9200/app-logs-*/_settings" \
  -H 'Content-Type: application/json' \
  -d '{
    "index.merge.scheduler.max_thread_count": 1,
    "index.merge.scheduler.max_merge_count": 4,
    "index.merge.policy.max_merge_at_once": 5,
    "index.merge.policy.segments_per_tier": 5
  }'
  • max_thread_count: 1: với SSD nhanh, 1 thread là đủ. HDD chậm, set 2-3.
  • max_merge_count: 4: số merge chờ tối đa trước khi block ingest.
  • segments_per_tier: 5: số segment cùng size cho phép. Giảm = merge sớm hơn, ổn định search latency.

Indexing pressure

ES 7.9+ có circuit breaker cho indexing:

curl -X PUT "http://es:9200/_cluster/settings" \
  -H 'Content-Type: application/json' \
  -d '{
    "persistent": {
      "indexing_pressure.memory.limit": "10%"
    }
  }'

Khi memory dành cho indexing vượt 10% heap, ES reject thêm bulk request. Filebeat/Vector sẽ retry sau, tránh OOM.

Phần 5: Refresh interval

Refresh = make data searchable. Default 1 giây. Với heavy ingest, 1 giây là expensive.

Tăng cho daily index:

curl -X PUT "http://es:9200/app-logs-*/_settings" \
  -H 'Content-Type: application/json' \
  -d '{"index.refresh_interval": "30s"}'

Trade-off: data hiện trong Kibana chậm hơn 30 giây. OK cho log (không ai chờ log thấy ngay trong 1 giây). Không OK cho transactional search.

Cho index read-only (rolled-over): tắt hẳn refresh:

curl -X PUT "http://es:9200/app-logs-2026.05.10/_settings" \
  -H 'Content-Type: application/json' \
  -d '{"index.refresh_interval": "-1"}'

-1 = không refresh nữa. Sau đó force merge để tối ưu segment count.

Pattern: dynamic refresh

Hot index (đang ingest): 30s refresh. Warm index (đã rollover): -1 (no refresh). Cold index (searchable snapshot): N/A.

ILM policy có thể set tự động.

Phần 6: Circuit breaker

ES có circuit breaker để abort query nguy hiểm trước khi OOM:

BreakerDefaultPurpose
indices.breaker.total.limit95% heapTổng
indices.breaker.request.limit60% heapPer request (aggregation, fetch)
indices.breaker.fielddata.limit40% heapField data cache
network.breaker.inflight_requests.limit100% heapIn-flight network buffer

Khi vượt, query abort với CircuitBreakingException. Đây là feature, không phải bug.

Tune nếu workload đặc thù:

curl -X PUT "http://es:9200/_cluster/settings" \
  -H 'Content-Type: application/json' \
  -d '{
    "persistent": {
      "indices.breaker.request.limit": "50%",
      "indices.breaker.fielddata.limit": "30%"
    }
  }'

Giảm threshold = sớm abort, bảo vệ cluster. Tăng = cho phép query lớn hơn, risk OOM cao hơn.

Pattern: query nguy hiểm

Một dev viết:

{
  "size": 0,
  "aggs": {
    "all_users": {
      "terms": {"field": "user_id", "size": 1000000}
    }
  }
}

size: 1000000 cho terms agg = build dictionary 1 triệu entry. Circuit breaker abort. Dev complain “ES bị bug”. Không, breaker đang bảo vệ ES.

Pattern fix: cardinality high field nên dùng composite agg với pagination, không terms size lớn.

Phần 7: Một câu chuyện thực tế

Cuối 2025 tôi tham gia tune một cluster ELK 8 node ingest 50k EPS, search latency P99 = 2.5 giây. Sau khi check qua các bài trước, không phải query hay shard imbalance. Tune theo thứ tự:

Tuần 1: heap

Heap mỗi node 24 GB trong khi RAM 64 GB. Tăng heap lên 30 GB. Sai. Search latency tệ hơn vì FS cache giảm.

Revert heap về 24 GB. Lesson: hơn 50% là quá nhiều với search workload.

Tuần 2: refresh interval

Default 1s. Đổi thành 5s cho hot index. P99 ingest latency giảm 30%. Không ảnh hưởng search vì Kibana mặc định auto-refresh dashboard mỗi 30s.

Tuần 3: merge throttling

Burst ingest sau deploy gây merge backlog. Set max_thread_count: 2 (HDD setup), max_merge_count: 8. Ingest stable hơn, không drop event nữa.

Tuần 4: field caps cache

Cluster có 800+ data view. Field caps mỗi gọi mất 8s. Bật cache với size: 15%. Sau 2 ngày cache warm, latency Discover open giảm từ 8s xuống 600ms.

Tuần 5: circuit breaker

Vẫn có 2-3 lần/tuần một dev gõ query agg lớn làm node OOM. Set request.limit: 50%. Query bị abort sớm, không kill node. Dev thấy error message rõ ràng, biết phải refactor query.

Tổng kết: search P99 từ 2.5s xuống 700ms. Ingest stability tăng. Cluster có chu kỳ dev-friendly hơn.

Bài học từ tuần 1: đừng tune một dial mà không hiểu trade-off. Heap to không phải lúc nào cũng nhanh hơn. Đo trước, đo sau, đo lại sau 1 tuần.

Phần 8: Cheatsheet tuning

Tham sốDefaultKhi nào tune
JVM heap1 GBSizing theo workload, max 31 GB
MaxGCPauseMillis200msGC pause > 500ms
indices.field_caps.cache.size1%Kibana Discover slow open
index.refresh_interval1sIngest >10k EPS
index.merge.scheduler.max_thread_count1 (SSD)HDD, set 2-3
indexing_pressure.memory.limit10%Tránh OOM khi burst
indices.breaker.request.limit60%Bảo vệ khỏi agg nguy hiểm
indices.breaker.fielddata.limit40%Bảo vệ field data
VerifyCommand
Heap usageGET /_cat/nodes?h=name,heap.percent
GC statsGET /_nodes/stats/jvm
Cache hitGET /_nodes/stats/indices/query_cache,request_cache
Merge statsGET /_nodes/stats/indices/merges
Breaker tripGET /_nodes/stats/breaker
Pending tasksGET /_cluster/pending_tasks

Tổng kết toàn series

28 bài, từ “Kibana là cái gì” đến “JVM heap sizing”. Đi qua:

  • Part 1: Foundation (bài 1-3): KQL, ES|QL, Discover. Dev cầm vào dùng được.
  • Part 2: Visualization (bài 4-7): Lens, Canvas, Maps, pitfall agg. Báo cáo đẹp và đúng.
  • Part 3: Alerts (bài 8-11): rule, connector, SLO, throttling. Reactive thành proactive.
  • Part 4: Security (bài 12-15): RBAC, Space, API key, audit. Multi-team an toàn.
  • Part 5: Production (bài 16-20): ILM, snapshot, reverse proxy, TLS, upgrade. Vận hành 24/7.
  • Part 6: Integration (bài 21-24): log shipper, NDJSON workflow, API automation, Terraform. Treat Kibana như code.
  • Part 7: Troubleshooting (bài 25-28): debug load, query chậm, disk full, performance tuning. Khi sự cố tới.

Mục tiêu ban đầu: build một series để developer backend và DevOps không phải Google “kibana how to …” lặp đi lặp lại. Tin là series này đã làm được phần lớn việc đó.

Lời kết và lời cảm ơn

Viết series này mất hơn một năm. Nhiều bài là extract từ runbook nội bộ, postmortem incident và những cuộc tranh luận nửa đêm với teammates. Không phải tất cả pattern đều đúng cho mọi cluster. Có chỗ bạn sẽ thấy quan điểm khác, có chỗ workload bạn cần pattern khác.

Đó là điều mình mong nhận lại nhất: feedback từ bạn đọc.

  • Có bài nào bạn thấy thiếu pitfall quan trọng?
  • Có pattern nào trong bài không khớp với production của bạn?
  • Có topic nào về Kibana/ES bạn vẫn chưa tìm thấy resource tốt?

Drop comment, gửi email, hoặc reply trên các kênh social mà bạn tới đây. Nếu series này đã tiết kiệm cho bạn vài đêm thức, mình rất vui. Nếu có sai sót, mình muốn fix ngay.

Lần đầu mình chạm vào Kibana là năm 2018, làm consulting cho một fintech, được giao “debug log stack giúp anh em backend”. Mở Kibana ra, gõ level:error, không ra kết quả nào, sợ hỏng. Phải Google cả buổi mới biết Serilog dùng Level chữ hoa. Bài 1 của series này thực ra là bài mình ước có thời điểm đó.

Series khép lại nhưng Kibana không. Phiên bản mới ra liên tục, feature mới được thêm. Khi nào có gì xứng đáng cho một bài bonus, mình sẽ viết tiếp dưới tag kibana. Subscribe tag đó, hoặc bookmark blog là cách tốt nhất để theo dõi.

Cảm ơn các bạn đã đi cùng đến bài 28. Hẹn gặp ở những series khác. Trong khi đó, cluster của bạn green status, dashboard versioned trong git, alert kêu khi cần và silent khi không. Đó là điều quan trọng nhất.