Elasticsearch 7 — Tối ưu hiệu năng & vận hành
Elasticsearch 7 — Tối ưu hiệu năng & vận hành
Sáu bài trước dựng nền: từ tổng quan, mô hình dữ liệu, Query DSL, aggregations, đến nạp dữ liệu và cluster/scaling. Bài này là phần mà đội vận hành sống chung mỗi ngày: khi cluster chậm hoặc chuyển đỏ thì làm gì. Bối cảnh xuyên suốt là một cụm log giao dịch của NCB đột nhiên latency tìm kiếm tăng vọt, _cluster/health báo yellow rồi red — chúng ta sẽ đi qua tối ưu tìm kiếm, mapping, bộ nhớ, giám sát và các sự cố điển hình theo đúng thứ tự một kỹ sư sẽ chẩn đoán.
Tối ưu tìm kiếm
Đa số vấn đề hiệu năng đến từ truy vấn viết ẩu, không phải phần cứng. Vài nguyên tắc tạo khác biệt lớn.
Filter context để cache
Query DSL có hai ngữ cảnh: query context tính điểm liên quan (_score) và filter context chỉ trả lời có/không (khớp hay không), không tính điểm. Filter context vừa nhanh hơn (bỏ scoring) vừa được ES cache kết quả (node query cache) — cực hợp cho các điều kiện lọc lặp lại như trạng thái, khoảng ngày, mã chi nhánh.
GET /transactions-2026.06/_search
{
"query": {
"bool": {
"must": [ { "match": { "note": "chuyển khoản nhầm" } } ],
"filter": [
{ "term": { "status": "SUCCESS" } },
{ "range": { "created_at": { "gte": "2026-06-01", "lt": "2026-07-01" } } }
]
}
}
}
must (tính điểm) chỉ nên chứa phần cần liên quan văn bản; mọi điều kiện lọc đúng/sai bỏ vào filter. Đây là tối ưu "một dòng" hiệu quả nhất trong thực tế.
Tránh wildcard đầu chuỗi và script tốn kém
- Leading wildcard (
*abc,?abc) buộc ES quét gần như toàn bộ từ điển của inverted index — cực chậm. Trailing wildcard (abc*) ổn hơn nhiều. Nếu cần tìm hậu tố, cân nhắc lưu thêm field đảo ngược, hoặc dùngsearch_as_you_type/n-gram thay vì wildcard. scriptquery / script trong sort/agg chạy mỗi document mỗi lần → tốn CPU và không cache tốt. Ưu tiên tính sẵn giá trị lúc index (denormalize) thay vì tính lúc truy vấn.regexp,fuzzybiên độ lớn cũng đắt — dùng có chủ đích, không rải khắp dashboard.
Hạn chế field trả về
Kéo cả _source lớn qua mạng làm chậm cả truy vấn lẫn client. Chỉ lấy field cần:
GET /transactions-2026.06/_search
{
"_source": ["txn_id", "amount", "status", "created_at"],
"query": { "term": { "status": "FAILED" } },
"size": 50
}
Với truy vấn chỉ cần vài field số/keyword, dùng "docvalue_fields" hoặc "stored_fields" còn rẻ hơn vì đọc từ doc_values thay vì parse _source.
keyword cho lọc và aggregation
Field kiểu text được phân tích (analyzed) để full-text; không dùng để lọc chính xác hay gom nhóm. Field kiểu keyword giữ nguyên chuỗi, có doc_values — đó mới là thứ ES đọc cực nhanh cho term filter, sort, và terms/date_histogram aggregation. Mẫu chuẩn là multi-field: status (text để tìm) kèm status.keyword (keyword để lọc/agg). Lọc/agg trên text sẽ báo lỗi hoặc buộc bật fielddata (ăn heap khủng khiếp — tránh tuyệt đối).
Tránh deep pagination → search_after
from + size để phân trang sâu (ví dụ from: 10000) buộc mỗi shard gom from + size kết quả rồi điều phối viên (coordinating node) sắp xếp lại — chi phí tăng tuyến tính theo độ sâu, và ES chặn cứng ở index.max_result_window (mặc định 10.000). Với dữ liệu lớn:
search_after— phân trang không trạng thái bằng cách "tiếp tục sau bản ghi cuối", dựa trên giá trị sort (nên kèm một tie-breaker duy nhất như_idhoặctxn_id):
GET /transactions-2026.06/_search
{
"size": 1000,
"sort": [ { "created_at": "asc" }, { "txn_id": "asc" } ],
"search_after": [ "2026-06-30T09:41:22.000Z", "TXN-2026-0009812" ]
}
- PIT (Point In Time) kết hợp
search_afterđể có ảnh nhất quán khi quét sâu; Scroll API vẫn dùng để export lô lớn nhưng đã bị coi là legacy, ưu tiên PIT +search_after.
Tối ưu mapping
Mapping quyết định cả tốc độ lẫn dung lượng. Vài công tắc quan trọng:
- Tắt index cho field không tìm kiếm. Field chỉ để hiển thị/lưu trữ (payload thô, id kỹ thuật) đặt
"index": false— vẫn nằm trong_source, không tạo cấu trúc tìm kiếm, tiết kiệm đĩa và tốc độ nạp. - doc_values. Bật mặc định cho keyword/số/date; đây là cấu trúc cột giúp sort/agg nhanh. Nếu một field chắc chắn không bao giờ sort/agg, có thể tắt (
"doc_values": false) để tiết kiệm — nhưng cân nhắc kỹ vì bật lại phải reindex. - Cân nhắc
_source. Tắt_source("_source": { "enabled": false }) tiết kiệm đáng kể đĩa cho dữ liệu chỉ-agg, nhưng mất khả năng reindex, update-by-query, xem lại document gốc, và một số tính năng highlight/reindex. Với log ngân hàng cần tra cứu bản ghi gốc → hầu như luôn giữ_source; chỉ tắt cho index metric thuần số có thể tái tạo. - Tắt
norms/doc_valueschọn lọc. Field keyword chỉ dùng filter (không scoring) có thể tắtnorms; nhỏ nhưng cộng dồn trên tỉ document.
PUT /transactions-clean
{
"mappings": {
"properties": {
"txn_id": { "type": "keyword" },
"amount": { "type": "scaled_float", "scaling_factor": 100 },
"status": { "type": "keyword" },
"raw_payload":{ "type": "text", "index": false },
"created_at": { "type": "date" }
}
}
}
Lưu ý: dùng keyword cho id thay vì text, và scaled_float/integer đúng kiểu số thay vì để ES tự đoán ra text+keyword (double mapping tốn đôi).
Tối ưu nạp (nhắc lại ngắn)
Đã nói kỹ ở Indexing & nạp dữ liệu, nhắc lại vì liên quan trực tiếp đến hiệu năng khi backfill:
- Dùng
_bulk, không nạp từng document; lô khởi điểm 5–15 MB / 1.000–5.000 doc. - Tăng
refresh_interval(hoặc-1khi backfill) để bớt tạo segment nhỏ → bớt merge. - Tạm
number_of_replicas: 0khi backfill, bật lại sau khi nạp xong rồi_refresh— tránh nhân đôi công ghi trong lúc nạp nặng. - Nạp song song vừa phải, canh
429 Too Many Requestsđể không làm bão hoà cluster.
Quản lý bộ nhớ
Đây là gốc rễ của phần lớn sự cố "cluster đỏ".
JVM heap
ES chạy trên JVM. Hai quy tắc kinh điển:
- Đặt heap ~50% RAM vật lý của node, phần còn lại để dành cho filesystem cache của hệ điều hành (xem dưới).
- Không vượt ~31 GB (thường khuyến nghị 30–31 GB). Trên ngưỡng này JVM mất compressed ordinary object pointers (compressed oops) — con trỏ phải dùng 64-bit, khiến heap 32 GB thực dụng còn ít hơn 30 GB mà lại tốn RAM hơn. Kiểm tra
XmsvàXmxbằng nhau (tránh JVM resize heap lúc chạy).
Ví dụ: node 64 GB RAM → đặt heap 30–31 GB, để ~33 GB cho fs cache. Node 128 GB RAM → vẫn giữ heap ~31 GB (không tăng lên 64), phần dư dùng cho fs cache; nếu muốn tận dụng RAM lớn hãy chạy nhiều node ES/máy hoặc thêm máy.
Filesystem cache quan trọng
Lucene đọc segment qua page cache của OS. Nếu index (phần "hot") nằm gọn trong fs cache, đọc gần như từ RAM; nếu không, mỗi truy vấn phải chạm đĩa → chậm hàng chục lần. Đây là lý do không dồn hết RAM vào heap: heap to quá thì fs cache còn ít, tưởng tối ưu hoá lại làm chậm search. Dùng SSD/NVMe cho node hot cũng vì thế.
Circuit breaker
Để tránh OOM (out of memory) làm chết node, ES có circuit breaker ước lượng bộ nhớ một thao tác sắp dùng và từ chối trước nếu vượt ngưỡng, ném lỗi CircuitBreakingException thay vì làm sập JVM. Các breaker chính: parent (tổng, mặc định ~95% heap), request (agg/query một request), fielddata (chống agg trên text ăn hết heap), in_flight_requests. Thấy CircuitBreakingException trong log không phải bug — đó là ES tự bảo vệ; tín hiệu để tối ưu truy vấn/giảm shard/tăng heap, không phải để nâng ngưỡng breaker một cách mù quáng.
Giám sát cluster
Không đo thì không tối ưu. Bộ công cụ chẩn đoán cốt lõi:
_cluster/health — nhìn tổng trạng thái
GET /_cluster/health
{
"status": "yellow",
"number_of_nodes": 5,
"active_primary_shards": 640,
"active_shards": 1180,
"unassigned_shards": 100,
"number_of_pending_tasks": 3
}
Ý nghĩa màu: green = mọi primary và replica đều đã gán; yellow = đủ primary nhưng thiếu replica (dữ liệu vẫn đọc/ghi được, chỉ mất dự phòng); red = thiếu primary → một phần dữ liệu không truy cập được, phải xử lý ngay. unassigned_shards cao là manh mối chính; dùng GET /_cluster/allocation/explain để biết vì sao một shard chưa gán được.
_cat APIs — bảng gọn cho con người
GET /_cat/nodes?v&h=name,heap.percent,ram.percent,cpu,load_1m,disk.used_percent
GET /_cat/indices?v&s=store.size:desc
GET /_cat/shards?v&h=index,shard,prirep,state,docs,store,node&s=state
GET /_cat/thread_pool/search,write?v&h=node_name,name,active,queue,rejected
Đọc nhanh: heap.percent các node, index nào chiếm đĩa nhiều nhất, shard nào UNASSIGNED, và rejected ở thread pool (số > 0 nghĩa là queue đầy, request bị từ chối — dấu hiệu quá tải).
_nodes/stats — số liệu sâu
GET /_nodes/stats cho chi tiết theo node: jvm.mem.heap_used_percent, jvm.gc (số lần và thời gian GC — GC young/old kéo dài là cảnh báo), os.cpu, fs (đĩa), thread_pool.*.rejected, indices.search.query_time_in_millis, indices.indexing.index_time_in_millis, breakers.*.tripped (số lần circuit breaker nổ).
Slow log
Bật slow log để bắt truy vấn/nạp chậm ngay tại index, phân ngưỡng theo pha query và fetch:
PUT /transactions-2026.06/_settings
{
"index.search.slowlog.threshold.query.warn": "2s",
"index.search.slowlog.threshold.query.info": "800ms",
"index.search.slowlog.threshold.fetch.warn": "1s",
"index.indexing.slowlog.threshold.index.warn": "2s"
}
Slow log ghi ra file log của node kèm nguyên văn truy vấn — cực quý để truy ra dashboard/nghiệp vụ nào đang tạo truy vấn nặng.
Các chỉ số then chốt
| Chỉ số | Nguồn | Ngưỡng cảnh giác |
|---|---|---|
| Search latency (p95/p99) | indices.search.query_time + số query | Tăng đột biến so với baseline |
| Indexing latency | indices.indexing.index_time | Tăng khi merge/refresh áp lực |
| JVM heap used % | jvm.mem.heap_used_percent | Thường > 75–80% liên tục là lo |
| GC pause | jvm.gc.collectors.old | Old GC thường xuyên/kéo dài |
| Thread pool rejected | thread_pool.*.rejected | Bất kỳ > 0 kéo dài |
| Circuit breaker tripped | breakers.*.tripped | Tăng dần |
| Disk used % | _cat/nodes / fs | Chạm watermark (85/90/95%) |
Luồng chẩn đoán
Khi cluster chậm/đỏ, đi theo lộ trình cố định thay vì đoán mò:
Sự cố thường gặp
Heap cao / GC liên tục. Triệu chứng: heap_used_percent neo cao, old GC thường xuyên, latency giật cục, đôi khi CircuitBreakingException. Nguyên nhân hay gặp: quá nhiều shard (mỗi shard tốn heap cho metadata), fielddata do agg/sort trên text, agg cardinality cực lớn, hoặc bulk quá to. Xử lý: giảm số shard (dưới), sửa agg sang keyword/doc_values, chia nhỏ agg, tăng heap trong giới hạn 31 GB.
Quá nhiều shard (oversharding). Kinh điển với time-series chia index theo ngày mà mỗi index lại đặt nhiều shard: 365 ngày × 5 shard × replica = hàng nghìn shard cho một dataset nhỏ. Quy tắc thực chiến: shard nên ~10–50 GB, và giữ số shard/node không quá ~20 shard mỗi GB heap (ví dụ heap 30 GB → tránh vượt ~600 shard/node). Khắc phục: giảm number_of_shards cho index mới, gộp index cũ bằng _shrink hoặc _reindex vào index tháng, dùng ILM rollover theo kích thước thay vì cứng theo ngày.
Mapping explosion. Dữ liệu log động (mỗi document mang key khác nhau, ví dụ nhét cả object params tự do) khiến ES dynamic mapping sinh ra hàng nghìn field → metadata phình, heap cạn, cluster chậm dần rồi đỏ. Chặn bằng index.mapping.total_fields.limit, đặt "dynamic": "strict" hoặc "false" cho vùng dữ liệu tự do, hoặc dùng kiểu flattened để gói cả object thành một field thay vì bung ra vô số field con.
Hot node / hotspot. Một node (hoặc một shard) gánh phần lớn tải trong khi các node khác nhàn. Nguyên nhân: shard phân bố lệch, routing dồn ghi vào một shard, hoặc index đang-nạp (hot) chỉ nằm trên một node. _cat/nodes thấy một node cpu/load cao bất thường. Xử lý: cân bằng lại allocation, xem lại custom routing, dùng ILM để trải index hot đều các node hot, tách vai trò node (hot/warm) và dùng shard allocation awareness.
Capacity planning
Ước lượng thô nhưng đủ dùng để mua sắm và đặt ILM:
- Dung lượng dữ liệu. Kích thước trên đĩa ≈ kích thước JSON thô × hệ số (thường ~1,1–1,3× cho log có
_source, cao hơn nếu index nhiều field/analyzer nặng), rồi × (1 + số replica). Ví dụ: 1,8 triệu doc/ngày × ~1,5 KB ≈ 2,7 GB/ngày thô → ~3,5 GB/ngày trên đĩa × 2 (1 replica) ≈ 7 GB/ngày; giữ nóng 90 ngày ≈ ~630 GB hot. - Số shard. Chia sao cho mỗi shard rơi vào ~10–50 GB. 3,5 GB/ngày → 1 primary shard/ngày là quá đủ (đừng đặt 5).
- RAM/heap. Bảo đảm phần hot vừa fs cache; heap ≤ 31 GB/node; chừa CPU cho merge/GC. Với đỉnh nạp gấp 4–5 lần trung bình, tính node theo đỉnh chứ không theo trung bình.
- Chừa headroom cho disk watermark (85% low, 90% high, 95% flood-stage → ES chuyển index sang read-only). Đừng bao giờ chạy sát 85%.
Use case thực tế
Bối cảnh. Cụm ES log giao dịch NCB (5 node, đã mô tả kiến trúc nạp ở bài 5) đột nhiên latency tìm kiếm p99 tăng từ ~200 ms lên hơn 4 giây; _cluster/health báo yellow, có lúc red vào giờ cao điểm. Đội vận hành điều tra theo luồng chẩn đoán ở trên.
Điều tra & phát hiện.
_cat/nodescho thấyheap.percentcủa 3/5 node neo ~88–92%,_nodes/statsbáo old GC chạy liên tục vàbreakers.fielddata.trippedtăng._cat/shardsđếm được ~4.100 shard cho ~90 ngày log — do mỗi index ngày đặt 5 primary + 1 replica, trong khi mỗi shard chỉ ~0,7 GB. Kinh điển oversharding.- Slow log lộ một dashboard Giám sát chạy
termsaggregation trên fieldchannelkiểutext(không phảikeyword) → bậtfielddataăn heap khổng lồ; và một truy vấn khác dùng leading wildcard*ACB*trên số tài khoản. redxảy ra khi một node chạm disk watermark 90% khiến shard không gán được.
Xử lý.
- Sửa dashboard sang
channel.keyword(doc_values) và bỏ leading wildcard (dùngterm/prefix) → heap và latency giảm ngay. - Chuyển field lọc/agg sang
filtercontext để tận dụng query cache. - Đặt index mới 1 primary shard/ngày thay vì 5;
_reindexcác index cũ theo tháng; bật ILM rollover theo kích thước 30 GB. Tổng shard giảm từ ~4.100 xuống vài trăm → heap về ~55%. - Chặn mapping explosion: đặt
dynamic: "strict"cho phầnparamstự do, chuyển sang kiểuflattened. - Dọn đĩa (drop backing index quá 90 ngày qua ILM), thêm cảnh báo ở mức 80% để không chạm watermark.
Kết quả. p99 về ~180 ms, cluster ổn định green, heap ổn định dưới 60%, không còn CircuitBreakingException. Bài học lặp lại: phần lớn "ES chậm" là truy vấn/mapping/shard sai, không phải thiếu phần cứng.
Ghi nhớ
- Tìm kiếm: đưa điều kiện lọc vào
filtercontext (nhanh + cache); tránh leading wildcard và script đắt; chỉ lấy field cần (_sourcechọn lọc); dùngkeyword+doc_values cho lọc/sort/agg (không bao giờ agg trêntext/fielddata); thay deep pagination bằngsearch_after(+ PIT). - Mapping:
"index": falsecho field không tìm; giữ doc_values cho field sort/agg; cân nhắc kỹ trước khi tắt_source(log ngân hàng hầu như luôn giữ); chọn đúng kiểu số/keyword để tránh double mapping. - Nạp:
_bulk, tăngrefresh_interval, tạmreplica=0khi backfill, canh429— chi tiết ở bài 5. - Bộ nhớ: heap ~50% RAM và ≤ ~31 GB (compressed oops),
Xms=Xmx; chừa RAM cho filesystem cache (quyết định tốc độ đọc); circuit breaker là cơ chế tự bảo vệ, thấyCircuitBreakingExceptionlà tín hiệu tối ưu chứ không phải nâng ngưỡng. - Giám sát:
_cluster/health(green/yellow/red),_cat/nodes|indices|shards|thread_pool,_nodes/stats(heap, GC, rejected, breakers), slow log; theo dõi search/index latency, GC, heap, thread pool rejection, disk watermark. - Sự cố kinh điển: heap cao (shard nhiều/fielddata/agg nặng), oversharding (shard ~10–50 GB, giảm shard/reindex/rollover), mapping explosion (giới hạn field,
strict/flattened), hot node (cân bằng allocation/routing). - Capacity: đĩa ≈ JSON thô × ~1,1–1,3 × (1+replica); shard ~10–50 GB; tính theo đỉnh tải; chừa headroom dưới watermark 85%.
Bài viết liên quan
CSV/JSON/Parquet/Avro, lưu trữ theo hàng vs cột, nén, và OLTP vs OLAP.
Data Engineering là gì, vai trò trong vòng đời dữ liệu, và bức tranh hệ sinh thái công cụ.
Vì sao xử lý phân tán, mô hình Spark (RDD/DataFrame), lazy evaluation, shuffle và tối ưu.
Đảm bảo chất lượng & minh bạch dữ liệu bằng dbt: data tests dựng sẵn (unique, not_null, relationships, accepted_values), singular test, unit test, và tài liệu tự sinh kèm lineage graph.