Elasticsearch bên trong: index, shard, mapping

4 câu hỏi phỏng vấn~30 phútTừ khóa: index, document, shard, primary, replica, segment, refresh, near real-time, translog, merge, node roles, master, data, ingest, coordinating, cluster health, mapping, dynamic mapping, index template, data stream, backing index, scatter-gather

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 đề

3. Giải pháp: các khái niệm

Khái niệmGiải thích ngắnSo với PostgreSQL
DocumentMột dòng log JSONMột row
IndexTập document cùng mappingMột table
MappingKiểu của từng field (keyword, text, date, long…)Schema
ShardMột mảnh của index; thực chất là một Lucene indexPartition
Primary shardBản gốc nhận writePrimary
Replica shardBản sao trên node khác, phục vụ read và chịu lỗiReplica
SegmentFile 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:

RoleLàm gìGợi ý shop-app
masterQuản lý cluster state: index nào, shard ở đâu. Cần số lẻ (3) để bầuCả 3 node đều master-eligible
data (data_hot, data_warm, data_cold)Giữ shard, chạy query, indexingCả 3 node
ingestChạy ingest pipelineCả 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ĩaLàm gì
greenMọi primary và replica đã gánKhông
yellowPrimary đủ, thiếu replicaallocation/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ƯuNhược
Index mỗi ngày, 5 primaryĐơn giảnNgày ít log → shard 1GB, quá nhiều shard
Data stream, 1 primary, rollover theo 50GBShard đều kích thướcCần ILM (bài 19.11)
Data stream, 3 primary (bằng số data node)Chia tải ghi đều 3 nodeChỉ đáng khi ghi nhiều

7. Khi nào nên dùng / không nên dùng

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:

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


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).