Elasticsearch bên trong: index, shard, mapping
Tiếp đi: Elasticsearch bên trong. Mình dựng 3 node ES trên VM cho shop-app, log vào được rồi, nhưng Kibana cứ báo cluster "yellow". Vẫn tìm log bình thường mà, có sao không nhỉ?
Yellow nghĩa là dữ liệu vẫn đủ nhưng đang thiếu bản sao. Chưa mất gì, nhưng nếu mất thêm một node là có thể mất log. Để hiểu vì sao, cần nắm index, shard và replica. Phần inverted index và analyzer đã có ở bài 6.11, ở đây mình tập trung vào phần cluster và log.
1. Bài toán
es-1 (10.0.2.21) es-2 (10.0.2.22) es-3 (10.0.2.23) mỗi máy 16GB RAM, 500GB SSD
log shop-app: ~40GB/ngày, giữ 14 ngày
GET _cluster/health → "status": "yellow", "unassigned_shards": 6
2. Vấn đề
- Không biết shard nào chưa được gán, vì sao.
- Đặt số shard tùy hứng (mặc định cũ là 5, giờ là 1) → hoặc shard quá to, hoặc quá nhiều shard nhỏ.
- Field mới trong log tự sinh mapping sai kiểu (ví dụ
orderIdthànhlong, hôm sau có giá trị "ORD-1042" thì bị từ chối).
3. Giải pháp: các khái niệm
| Khái niệm | Giải thích ngắn | So với PostgreSQL |
|---|---|---|
| Document | Một dòng log JSON | Một row |
| Index | Tập document cùng mapping | Một table |
| Mapping | Kiểu của từng field (keyword, text, date, long…) | Schema |
| Shard | Một mảnh của index; thực chất là một Lucene index | Partition |
| Primary shard | Bản gốc nhận write | Primary |
| Replica shard | Bản sao trên node khác, phục vụ read và chịu lỗi | Replica |
| Segment | File bất biến bên trong shard | (không có tương đương) |
Near real-time: ghi vào ES không tìm thấy ngay.
index doc ──► in-memory buffer + translog (ghi đĩa, chống mất khi crash)
│
│ refresh (mặc định 1s, chỉ khi index có người search)
▼
segment mới (tìm được)
│
│ flush: fsync segment, cắt translog
│ merge: gộp segment nhỏ thành to, xóa doc đã delete
▼
segment lớn trên đĩa
Bên trong segment là gì → bài 6.12. Ở đây chỉ cần trade-off cho ingest: với log, tăng refresh_interval lên 30s giảm tải ghi rất nhiều, và log trễ 30s là chấp nhận được.
Node roles:
| Role | Làm gì | Gợi ý shop-app |
|---|---|---|
master | Quản lý cluster state: index nào, shard ở đâu. Cần số lẻ (3) để bầu | Cả 3 node đều master-eligible |
data (data_hot, data_warm, data_cold) | Giữ shard, chạy query, indexing | Cả 3 node |
ingest | Chạy ingest pipeline | Cả 3 node |
| coordinating-only (roles rỗng) | Nhận request, chia việc, gộp kết quả | Thêm khi cluster lớn, nhiều dashboard nặng |
Cluster lớn (>10 node) nên tách 3 dedicated master nhỏ, để query nặng không làm master chết.
4. Code từng bước
Bước 1: elasticsearch.yml trên es-1 (es-2, es-3 tương tự)
cluster.name: shop-logs
node.name: es-1
node.roles: [ master, data_hot, data_content, ingest ]
network.host: 10.0.2.21
discovery.seed_hosts: ["10.0.2.21", "10.0.2.22", "10.0.2.23"]
cluster.initial_master_nodes: ["es-1", "es-2", "es-3"] # chỉ lần khởi tạo đầu, rồi xóa
xpack.security.enabled: true
xpack.security.transport.ssl.enabled: true # chứng chỉ chi tiết ở 19.11
Bước 2: index template + data stream cho log
PUT _index_template/logs-shop
{
"index_patterns": ["logs-shop-*"],
"data_stream": {},
"priority": 200,
"template": {
"settings": {
"number_of_shards": 1,
"number_of_replicas": 1,
"refresh_interval": "30s",
"index.lifecycle.name": "logs-shop-policy"
},
"mappings": {
"dynamic": "false",
"properties": {
"@timestamp": { "type": "date" },
"message": { "type": "match_only_text" },
"log.level": { "type": "keyword" },
"service.name": { "type": "keyword" },
"service.environment": { "type": "keyword" },
"trace.id": { "type": "keyword" },
"orderId": { "type": "keyword" },
"error.type": { "type": "keyword" },
"error.stack_trace": { "type": "text", "index": false },
"http.response.status_code": { "type": "short" },
"event.duration": { "type": "long" }
}
}
}
}
dynamic: false: field lạ vẫn được lưu trong _source, chỉ không index. Không bị "nổ mapping" (chi tiết lỗi field limit đã có ở 17.11).
Data stream là một tên duy nhất (logs-shop-prod) đứng trước nhiều backing index ẩn (.ds-logs-shop-prod-2026.10.14-000001…). Chỉ cho phép append, rollover tự động. Rất hợp cho log.
Bước 3: Java ghi thử document (để hiểu, thực tế dùng shipper ở 19.9)
// build.gradle: implementation 'co.elastic.clients:elasticsearch-java:8.15.3'
@Bean
ElasticsearchClient esClient(@Value("${ES_URL}") String url,
@Value("${ES_API_KEY}") String apiKey) {
RestClient rest = RestClient.builder(HttpHost.create(url))
.setDefaultHeaders(new Header[]{ new BasicHeader("Authorization", "ApiKey " + apiKey) })
.build();
return new ElasticsearchClient(new RestClientTransport(rest, new JacksonJsonpMapper()));
}
record LogEvent(@JsonProperty("@timestamp") Instant timestamp,
@JsonProperty("service.name") String service,
String message) {}
// data stream chỉ nhận op_type=create
esClient.create(c -> c.index("logs-shop-dev").id(UUID.randomUUID().toString())
.document(new LogEvent(Instant.now(), "payment-service", "test")));
Bước 4: kiểm tra bằng curl
AUTH="Authorization: ApiKey $ES_API_KEY"
curl -s -H "$AUTH" "$ES_URL/_cluster/health?pretty"
curl -s -H "$AUTH" "$ES_URL/_cat/nodes?v&h=name,node.role,heap.percent,disk.used_percent"
curl -s -H "$AUTH" "$ES_URL/_cat/shards/logs-shop-*?v&s=state"
curl -s -H "$AUTH" "$ES_URL/_data_stream/logs-shop-prod?pretty"
curl -s -H "$AUTH" "$ES_URL/_cluster/allocation/explain?pretty" # vì sao shard chưa gán
5. Cách hoạt động: shard được gán và health
index logs-shop-prod, 1 primary + 1 replica, 3 node
es-1: P0 (.ds-...-000003) R0 (.ds-...-000002)
es-2: R0 (.ds-...-000003) P0 (.ds-...-000001)
es-3: P0 (.ds-...-000002) R0 (.ds-...-000001)
es-2 chết ──► master nâng R0 trên node còn lại thành primary (vẫn ghi được)
──► replica thiếu → yellow; sau 1 phút (delayed_timeout) tạo replica mới ở es-1/es-3
Search gửi tới mọi shard liên quan rồi gộp kết quả, nên càng nhiều shard càng nhiều việc con. Chi tiết cấu trúc dữ liệu làm search nhanh (FST, posting list, doc values, BKD, BM25) xem → bài 6.12.
Cluster health:
| Màu | Ý nghĩa | Làm gì |
|---|---|---|
| green | Mọi primary và replica đã gán | Không |
| yellow | Primary đủ, thiếu replica | allocation/explain; thường do node chết, đĩa vượt watermark, hoặc replica > số node - 1 |
| red | Ít nhất một primary chưa gán → thiếu dữ liệu, ghi vào index đó lỗi | Đưa node lại, khôi phục snapshot; phương án cuối là allocate_empty_primary (chấp nhận mất) |
6. So sánh: số shard
| Cách | Ưu | Nhược |
|---|---|---|
| Index mỗi ngày, 5 primary | Đơn giản | Ngày ít log → shard 1GB, quá nhiều shard |
| Data stream, 1 primary, rollover theo 50GB | Shard đều kích thước | Cần ILM (bài 19.11) |
| Data stream, 3 primary (bằng số data node) | Chia tải ghi đều 3 node | Chỉ đáng khi ghi nhiều |
7. Khi nào nên dùng / không nên dùng
- Dùng data stream + template cho mọi log/metric theo thời gian.
- Dùng index thường cho dữ liệu cần update theo id (catalog sản phẩm, bài 6.11).
- Không để dynamic mapping tự do trên production log.
- Không chạy 2 node master-eligible (mất 1 là mất quorum); dùng 1 hoặc 3.
Mình chạy thử ES một node ở môi trường staging, mà vừa tạo index log là yellow luôn. Có phải cấu hình sai không?
Không sai, đây là hành vi đúng. ES không bao giờ đặt replica cùng node với primary của nó, vì như thế không chịu lỗi được. Một node + number_of_replicas: 1 → replica không có chỗ → yellow mãi.
PUT logs-shop-staging/_settings
{ "index.number_of_replicas": 0 }
Hoặc để trong template của staging. Nhớ: replica 0 là chấp nhận mất log nếu đĩa node đó hỏng; với staging thường ổn, production thì không.
Có bạn bảo mình: "Muốn ES nhanh thì cứ tăng replica lên 2, 3 cho chắc." Vậy có đúng không nhỉ?
Đúng một nửa. Replica giúp đọc và chịu lỗi, nhưng hại cho ghi:
- Mỗi doc phải được index lại trên mọi replica → replica 2 = CPU ghi gấp 3.
- Đĩa gấp 3: 40GB/ngày × 14 ngày × 3 = 1.68TB.
- 3 node mà replica 2 thì mỗi node giữ đủ một bản; mất node không còn chỗ để bù.
Log là workload ghi nhiều, đọc ít. Replica 1 là đủ cho dữ liệu hot. Dữ liệu cũ có thể giảm về 0 nếu đã có snapshot (bài 19.11).
Phỏng vấn hỏi về Elasticsearch bên trong thì trả lời sao?
Q1. Shard, replica là gì; vì sao cluster yellow hoặc red?
🇻🇳 Tiếng Việt:
Index chia thành các primary shard, mỗi shard là một Lucene index; replica là bản sao trên node khác để chịu lỗi và phục vụ đọc. Yellow là đủ primary nhưng thiếu replica, ví dụ cluster một node hoặc đĩa vượt watermark. Red là thiếu primary, dữ liệu không đủ và ghi vào index đó sẽ lỗi. Tôi dùng allocation explain để xem lý do, rồi đưa node lại, giải phóng đĩa hoặc khôi phục snapshot.
🇬🇧 English:
An index is split into primary shards, each a Lucene index, and replicas are copies on other nodes for fault tolerance and read throughput. Yellow means all primaries are assigned but some replicas are not, for example on a single-node cluster or when disk watermarks are exceeded. Red means a primary is missing, so data is incomplete and writes to that index fail. I use the allocation explain API to find the reason, then bring the node back, free disk, or restore from a snapshot.
Q2. Vì sao Elasticsearch là near real-time?
🇻🇳 Tiếng Việt:
Document được ghi vào buffer trong RAM và translog, chỉ tìm được sau khi refresh tạo segment mới, mặc định mỗi giây. Translog đảm bảo không mất khi crash, flush ghi segment xuống đĩa, merge gộp segment nhỏ. Với log, tôi tăng refresh interval lên 30 giây để giảm tải ghi.
🇬🇧 English:
Documents go into an in-memory buffer and the translog, and become searchable only after a refresh creates a new segment, every second by default. The translog protects against crashes, flush persists segments, and merges combine small segments. For logs I raise the refresh interval to around 30 seconds to cut indexing overhead.
Q3. Data stream là gì, sao hợp với log?
🇻🇳 Tiếng Việt:
Data stream là một tên ghi duy nhất đứng trước nhiều backing index ẩn, chỉ cho phép thêm mới, cần field @timestamp và rollover tự động theo ILM. Nó hợp với log vì log chỉ ghi thêm, query theo thời gian và xóa theo tuổi bằng cách xóa cả backing index, rất rẻ so với delete từng document.
🇬🇧 English:
A data stream is a single write name in front of hidden backing indices; it is append-only, requires a @timestamp field, and rolls over automatically through ILM. It fits logs because logs are append-only, queried by time, and expired by dropping whole backing indices, which is far cheaper than deleting documents one by one.
Cách nhớ ngắn gọn
- Index → shard (Lucene) → segment bất biến; refresh mới tìm được.
- Replica không bao giờ cùng node với primary; 1 node + replica 1 = yellow.
- Yellow thiếu bản sao, red thiếu bản gốc; hỏi
allocation/explain. - Node chết: replica lên primary, cluster yellow rồi tự bù replica.
- Log: data stream + template +
dynamic: false.
Tự kiểm tra
Vì sao log vừa ghi chưa thấy trong Kibana?
Chưa refresh; mặc định 1s, log thường đặt 30s.
Cluster 2 node master-eligible có vấn đề gì?
Mất 1 node là mất đa số để bầu master. Dùng 1 hoặc 3.
Red có nghĩa là gì với việc ghi?
Primary của index đó chưa gán, ghi vào sẽ lỗi; dữ liệu có thể đã mất nếu không có bản nào.
dynamic: false khác strict chỗ nào?
false: field lạ được giữ trong _source nhưng không index. strict: từ chối cả document.
Data stream nhận lệnh index theo id để update được không?
Không, chỉ op_type=create (append).