I. Thu thập chỉ số thiết bị Linux
1. Node Exporter
1.1. Giới thiệu
Là bộ phận được cài trên mục tiêu thu thập các thông tin chỉ số phần cứng và hệ điều hành từ các hệ thống Linux, những thông tin này có thể là mức sử dụng CPU, RAM, đĩa và chỉ số mạng. [1]1.2. Cấu hình
Sau khi tải phiên bản mới nhất của Prometheus Node Exporter [2], chạy tệp với cấu hình xuất dữ liệu tại localhost ở cổng 9100. [3]
Sass:
./node_exporter --web.listen-address="127.0.0.1:9100" &
Kiểm tra bằng cách truy cập tại đường dẫn sau
Sass:
curl http://localhost:9100/metrics
Hình 1. Kiểm tra trạng thái của Node
2. Prometheus
Prometheus là nơi lưu trữ các chỉ số này và thực hiện truy vấn, bằng cách:- Cấu hình thêm đoạn lệnh sau trong /etc/prometheus/prometheus.yml, trong phần scrape_configs
- Cấu hình remote write để đẩy chỉ số đến cổng 8500 với tần suất lên đến 5000 sample mỗi lần gửi, 10 luồng và tối đa 10 shard trong bộ nhớ.
YAML:
scrape_configs:
- job_name: "linux-node"
static_configs:
- targets: ['localhost:9100']
remote_write:
- url: "http://localhost:8500/write"
queue_config:
max_samples_per_send: 5000
max_shards: 10
capacity: 10000
Sau đó có thể truy vấn sử dụng promql để truy vấn
Hình 2. Truy vấn thử Promql
II. Vận chuyển và trực quan hóa chỉ số thiết bị Linux
1. Bộ vận chuyển Vector
Là công cụ nhẹ, tốc độ cao có chức năng thu thập, chuẩn hóa và vận chuyển log và chỉ số.Trong ngữ cảnh này, Vector sẽ là thành phần vận chuyển các chỉ số thô đến OpenSearch và cấu hình trên tệp /etc/vector/vector.yaml.
Phần source sẽ thu thập dữ liệu từ endpoint được cấu hình của Prometheus đã nêu trên.
YAML:
sources:
prom_receiver:
type: "prometheus_remote_write"
address: "0.0.0.0:8500"
path: "/write"
Đối với phần transform, trong trường hợp Prometheus nhận dữ liệu từ nhiều nguồn khác nhau mà Vector nhận cùng một nguồn, bằng các phân chia luồng dựa trên trường "name". [4]
YAML:
transforms:
route_by_source_prometheus:
type: "route"
inputs:
- prom_receiver
route:
linux_metrics: 'starts_with(string!(.name), "node_")'
vcenter_metrics: 'starts_with(string!(.name), "vsphere_")'
Phần sinks sẽ chuyển chỉ số về OpenSearch với tên index là "linux_node_exporter-metrics-"
YAML:
sinks:
opensearch_linux_node_metric:
type: "elasticsearch"
inputs:
- route_by_source_prometheus.linux_metrics
endpoints:
- "https://127.0.0.1:9200"
tls:
verify_certificate: false
auth:
strategy: "basic"
user: "admin"
password: "Th3adm1n@"
api_version: "v7"
bulk:
index: "linux_node_exporter-metrics-%Y.%m.%d"
2. Thành phần trực quan OpenSearch
2.1. Định nghĩa
Là một công cụ tìm kiếm và phân tích phân tán, hỗ trợ nhiều trường hợp sử dụng khác nhau, từ việc triển khai tìm kiếm trên trang web cho đến phân tích dữ liệu bảo mật nhằm phát hiện các mối đe dọa.2.2. Tạo index chỉ số
Để kiểm tra và khám phá dữ liệu đã đến OpenSearch, cần tạo một index pattern.Kết quả sau khi tạo trong Discover
Hình 3. Kết quả chỉ số Linux trên Discover
2.3. Tạo quy tắc tính toán
Trước khi tạo một bộ quy tắc để tính toán trước khi trực quan, cần lựa chọn các phương pháp tạo quy tắc.| Tiêu chí | Vector.dev | Ingest Pipelines | Index Transforms |
| Thời điểm xử lý | Khi nhận được dữ liệu từ Prometheus | Khi nhận được dữ liệu từ Vector | Chạy định kỳ trên những dữ liệu đã index trong OpenSearch |
| Xử lý dữ liệu | Chỉ xử lý stream dữ liệu thời gian thực | Chỉ xử lý stream dữ liệu thời gian thực | Quét và tổng hợp trực tiếp trên các index hiện có |
| Phép toán nhiều bước | Khá phức tạp | Khá phức tạp | Đơn giản |
| Tác động hiệu suất | Tốn tài nguyên ít | Gây chậm tốc độ ingest | Tốn thêm dung lượng bộ nhớ lưu trữ |
| Trực quan hóa | Phải cấu hình biểu đồ dựa trên cấu trúc JSON mới. | Phải cấu hình biểu đồ dựa trên cấu trúc JSON mới. | Tạo ra một index tinh gọn, biểu đồ load nhanh |
Tạo một bộ rule tính toán, kết quả sẽ ghi vào một index mới "summary-linux-metric" với các thông tin chỉ số sau:
- CPU_Load_1m: sử dụng "node_load1" đo lường số lượng khối lượng công việc tính toán mà bộ xử lý đảm nhận, đơn vị %.
- CPU_Usage_Avg: sử dụng "node_cpu_seconds_total" đo lường mức sử dụng CPU thông qua số "idle" CPU, đơn vị %.
- Memory_Available_Bytes: sử dụng "node_memory_MemAvailable_bytes" để tính số RAM đang sẵn sàng trong máy chủ, đơn vị Byte.
- Memory_Usage_Avg: sử dụng "node_memory_MemTotal_bytes" chứa tổng số bộ nhớ RAM với giá trị RAM đang sẵn sàng để tính toán mức RAM sử dụng, đơn vị %.
- Disk_Usage_Avg: sử dụng "node_filesystem_size_bytes" chứa tổng số dung lượng bộ nhớ và "node_filesystem_avail_bytes" chứa dung lượng còn tại để tính toán mức dung lượng đĩa đã sử dụng, đơn vị %.
- Disk_Write_Bytes_Per_Sec và Disk_Read_Bytes_Per_Sec: sử dụng "node_disk_write_bytes_total" và "node_disk_read_bytes_total" để đo lường tổng số byte ghi và đọc trên đĩa, chuyển sang đơn vị GB.
- Disk_Read_Latency_Ms: sử dụng "node_disk_read_time_seconds_total" chứa tổng số thời gian mà tất cả các thao tác đọc đã tiêu tốn và "node_disk_reads_completed_total" chứa tổng số thao tác đọc đĩa thành công để tính toán độ trễ đọc, đơn vị ms.
- Disk_Write_Latency_Ms: Tương tự như Read như với "node_disk_write_time_seconds_total" và "node_disk_writes_completed_total" để tính toán độ trễ ghi, đơn vị ms.
- Network_In_Bytes_Per_Sec và Network_Out_Bytes_Per_Sec: "node_network_receive_bytes_total" và "node_network_transmit_bytes_total" lần lượt chứa tổng số Byte sử dụng cho thao tác nhận và truyền tải dữ liệu dưới đơn vị Byte/s. [6]
Sass:
PUT _plugins/_transform/linux_master_transform_rule
{
"transform": {
"enabled": true,
"continuous": true,
"description": "Linux metric master transform rule",
"source_index": "linux_node_exporter-metrics-*",
"target_index": "summary-linux-metric",
"schedule": {
"interval": {
"period": 5,
"unit": "Minutes"
}
},
"page_size": 1000,
"groups": [
{
"terms": {
"source_field": "tags.instance.keyword",
"target_field": "Host"
}
}
],
"aggregations": {
"@timestamp": {
"scripted_metric": {
"init_script": "state.max_time = 0L",
"map_script": "if (doc['timestamp'].size() > 0) { long t = doc['timestamp'].value.toInstant().toEpochMilli(); if (t > state.max_time) { state.max_time = t; } }",
"combine_script": "return state.max_time;",
"reduce_script": """
long max_t = 0L;
for (t in states) {
if (t > max_t) { max_t = t; }
}
if (max_t == 0L) return null;
return Instant.ofEpochMilli(max_t).toString();
"""
}
},
"CPU_Load_1m": {
"scripted_metric": {
"init_script": "state.total = 0.0; state.count = 0;",
"map_script": """
if (doc['name.keyword'].value == 'node_load1') {
state.total += doc['gauge.value'].value;
state.count++;
}
""",
"combine_script": "return state;",
"reduce_script": """
double total = 0; long count = 0;
for (s in states) {
total += s.total;
count += s.count;
}
return count == 0 ? 0.0 : total / count;
"""
}
},
"CPU_Usage_Avg": {
"scripted_metric": {
"init_script": "state.total_idle = 0.0; state.total_cpu = 0.0;",
"map_script": """
if (doc['name.keyword'].value == 'node_cpu_seconds_total') {
if (doc['tags.mode.keyword'].value == 'idle') {
state.total_idle += doc['gauge.value'].value;
}
state.total_cpu += doc['gauge.value'].value;
}
""",
"combine_script": "return state;",
"reduce_script": """
double total_idle = 0;
double total_cpu = 0;
for (s in states) {
total_idle += s.total_idle;
total_cpu += s.total_cpu;
}
if (total_cpu == 0) return 0.0;
return ((total_cpu - total_idle) / total_cpu) * 100.0;
"""
}
},
"Memory_Available_Bytes": {
"scripted_metric": {
"init_script": "state.total = 0.0; state.count = 0;",
"map_script": """
if (doc['name.keyword'].value == 'node_memory_MemAvailable_bytes') {
state.total += doc['gauge.value'].value;
state.count++;
}
""",
"combine_script": "return state;",
"reduce_script": """
double total = 0; long count = 0;
for (s in states) {
total += s.total;
count += s.count;
}
return count == 0 ? 0.0 : total / count;
"""
}
},
"Memory_Usage_Avg": {
"scripted_metric": {
"init_script": """
state.total = 0.0;
state.avail = 0.0;
""",
"map_script": """
if (doc['name.keyword'].value == 'node_memory_MemTotal_bytes') {
state.total = doc['gauge.value'].value;
} else if (doc['name.keyword'].value == 'node_memory_MemAvailable_bytes') {
state.avail = doc['gauge.value'].value;
}
""",
"combine_script": "return state;",
"reduce_script": """
double total = 0;
double avail = 0;
for (s in states) {
if (s.total > 0) total = s.total;
if (s.avail > 0) avail = s.avail;
}
if (total == 0) return 0;
return ((total - avail) / total) * 100;
"""
}
},
"Disk_Usage_Avg": {
"scripted_metric": {
"init_script": """
state.total = 0.0;
state.avail = 0.0;
""",
"map_script": """
if (doc['name.keyword'].value == 'node_filesystem_size_bytes') {
state.total = doc['gauge.value'].value;
} else if (doc['name.keyword'].value == 'node_filesystem_avail_bytes') {
state.avail = doc['gauge.value'].value;
}
""",
"combine_script": "return state;",
"reduce_script": """
double total = 0;
double avail = 0;
for (s in states) {
if (s.total > 0) total = s.total;
if (s.avail > 0) avail = s.avail;
}
if (total == 0) return 0;
return ((total - avail) / total) * 100;
"""
}
},
"Disk_Read_Bytes_Per_Sec": {
"scripted_metric": {
"init_script": """
state.min = null;
state.max = null;
""",
"map_script": """
if (doc['name.keyword'].value == 'node_disk_read_bytes_total') {
double val = doc['gauge.value'].value;
if (state.min == null || val < state.min) state.min = val;
if (state.max == null || val > state.max) state.max = val;
}
""",
"combine_script": "return state;",
"reduce_script": """
double min = -1;
double max = -1;
for (s in states) {
if (s.min != null && (min == -1 || s.min < min)) min = s.min;
if (s.max != null && (max == -1 || s.max > max)) max = s.max;
}
if (max == -1 || min == -1 || max == min) return 0;
return (max - min) / 64424509440.0;
"""
}
},
"Disk_Write_Bytes_Per_Sec": {
"scripted_metric": {
"init_script": """
state.min = null;
state.max = null;
""",
"map_script": """
if (doc['name.keyword'].value == 'node_disk_written_bytes_total') {
double val = doc['gauge.value'].value;
if (state.min == null || val < state.min) state.min = val;
if (state.max == null || val > state.max) state.max = val;
}
""",
"combine_script": "return state;",
"reduce_script": """
double min = -1;
double max = -1;
for (s in states) {
if (s.min != null && (min == -1 || s.min < min)) min = s.min;
if (s.max != null && (max == -1 || s.max > max)) max = s.max;
}
if (max == -1 || min == -1 || max == min) return 0;
return (max - min) / 64424509440.0;
"""
}
},
"Disk_Read_Latency_Ms": {
"scripted_metric": {
"init_script": """
state.time_min = null;
state.time_max = null;
state.ops_min = null;
state.ops_max = null;
""",
"map_script": """
double val = doc['gauge.value'].value;
if (doc['name.keyword'].value == 'node_disk_read_time_seconds_total') {
if (state.time_min == null || val < state.time_min) state.time_min = val;
if (state.time_max == null || val > state.time_max) state.time_max = val;
} else if (doc['name.keyword'].value == 'node_disk_reads_completed_total') {
if (state.ops_min == null || val < state.ops_min) state.ops_min = val;
if (state.ops_max == null || val > state.ops_max) state.ops_max = val;
}
""",
"combine_script": "return state;",
"reduce_script": """
double time_min = -1; double time_max = -1;
double ops_min = -1; double ops_max = -1;
for (s in states) {
if (s.time_min != null && (time_min == -1 || s.time_min < time_min)) time_min = s.time_min;
if (s.time_max != null && (time_max == -1 || s.time_max > time_max)) time_max = s.time_max;
if (s.ops_min != null && (ops_min == -1 || s.ops_min < ops_min)) ops_min = s.ops_min;
if (s.ops_max != null && (s.ops_max > ops_max)) ops_max = s.ops_max;
}
double time_diff = (time_max > time_min) ? (time_max - time_min) : 0.0;
double ops_diff = (ops_max > ops_min) ? (ops_max - ops_min) : 0.0;
if (ops_diff == 0) return 0.0;
return (time_diff / ops_diff) * 1000.0;
"""
}
},
"Disk_Write_Latency_Ms": {
"scripted_metric": {
"init_script": """
state.time_min = null;
state.time_max = null;
state.ops_min = null;
state.ops_max = null;
""",
"map_script": """
double val = doc['gauge.value'].value;
if (doc['name.keyword'].value == 'node_disk_write_time_seconds_total') {
if (state.time_min == null || val < state.time_min) state.time_min = val;
if (state.time_max == null || val > state.time_max) state.time_max = val;
} else if (doc['name.keyword'].value == 'node_disk_writes_completed_total') {
if (state.ops_min == null || val < state.ops_min) state.ops_min = val;
if (state.ops_max == null || val > state.ops_max) state.ops_max = val;
}
""",
"combine_script": "return state;",
"reduce_script": """
double time_min = -1; double time_max = -1;
double ops_min = -1; double ops_max = -1;
for (s in states) {
if (s.time_min != null && (time_min == -1 || s.time_min < time_min)) time_min = s.time_min;
if (s.time_max != null && (time_max == -1 || s.time_max > time_max)) time_max = s.time_max;
if (s.ops_min != null && (ops_min == -1 || s.ops_min < ops_min)) ops_min = s.ops_min;
if (s.ops_max != null && (s.ops_max > ops_max)) ops_max = s.ops_max;
}
double time_diff = (time_max > time_min) ? (time_max - time_min) : 0.0;
double ops_diff = (ops_max > ops_min) ? (ops_max - ops_min) : 0.0;
if (ops_diff == 0) return 0.0;
return (time_diff / ops_diff) * 1000.0;
"""
}
},
"Network_In_Bytes_Per_Sec": {
"scripted_metric": {
"init_script": """
state.min = null;
state.max = null;
""",
"map_script": """
if (doc['name.keyword'].value == 'node_network_receive_bytes_total') {
double val = doc['gauge.value'].value;
if (state.min == null || val < state.min) state.min = val;
if (state.max == null || val > state.max) state.max = val;
}
""",
"combine_script": "return state;",
"reduce_script": """
double min = -1;
double max = -1;
for (s in states) {
if (s.min != null && (min == -1 || s.min < min)) min = s.min;
if (s.max != null && (max == -1 || s.max > max)) max = s.max;
}
if (max == -1 || min == -1 || max == min) return 0;
return (max - min) / 60.0;
"""
}
},
"Network_Out_Bytes_Per_Sec": {
"scripted_metric": {
"init_script": """
state.min = null;
state.max = null;
""",
"map_script": """
if (doc['name.keyword'].value == 'node_network_transmit_bytes_total') {
double val = doc['gauge.value'].value;
if (state.min == null || val < state.min) state.min = val;
if (state.max == null || val > state.max) state.max = val;
}
""",
"combine_script": "return state;",
"reduce_script": """
double min = -1;
double max = -1;
for (s in states) {
if (s.min != null && (min == -1 || s.min < min)) min = s.min;
if (s.max != null && (max == -1 || s.max > max)) max = s.max;
}
if (max == -1 || min == -1 || max == min) return 0;
return (max - min) / 60.0;
"""
}
}
}
}
}
Bật rule
Sass:
POST _plugins/_transform/linux_cpu_transform_rule/_start
Sau khi chạy thành công vào Dashboard Management > Index patterns để tạo pattern mới cho index mới tạo.
Hình 4. Tạo index patterns
Sau khi tạo pattern này người dùng có thể vào Discover để xem
Hình 5. Tạo index patterns
2.4. Tạo Dashboard trực quan
Sau đó tạo ra một dashboard giám sát:- Tài nguyên sử dụng: CPU, RAM, dung lượng đĩa dưới đơn vị %
- Độ trễ đĩa dưới đơn vị ms
- Tốc độ đọc và ghi của đĩa dưới đơn vị Byte/s
- Tốc độ đường truyền mạng dưới đơn vị GB/s
Hình 6. Dashboard giám sát chỉ số của máy chủ Linux
Nguồn tham khảo
[1] Soe Wai Lin, "Monitoring system resources using with node-exporter and prometheus", Medium. Avaliable: https://medium.com/@09soewailynn/mo...ith-node-exporter-and-prometheus-20e84941d180[2] Prometheus, "Node Exporter 1.12.1 / 2026-07-14", Github. Avaliable: https://github.com/prometheus/node_exporter/releases
[3] Prometheus, "Monitoring Linux host metrics with the Node Exporter", Prometheus Documents. Avaliable: https://prometheus.io/docs/guides/node-exporter/
[4] Vector, "Transforms Route", Vector Documents Configuration. Avaliable: https://vector.dev/docs/reference/configuration/transforms/route/
[5] OpenSearch, "Index transforms", OpenSearch documents. Avaliable: https://docs.opensearch.org/latest/im-plugin/index-transforms/index/
[6] OpenSearch, "Scripted metric aggregation", OpenSearch documents. Avaliable: https://docs.opensearch.org/latest/aggregations/metric/scripted-metric/