Ketika satu server web melayani jutaan request per hari, grep pada access.log tidak lagi cukup untuk menjawab pertanyaan operasional dan...
Ketika satu server web melayani jutaan request per hari, grep pada access.log tidak lagi cukup untuk menjawab pertanyaan operasional dan keamanan. Contohnya: endpoint mana yang tiba-tiba memuntahkan error 5xx, IP mana yang memindai /wp-login.php ratusan kali per menit, atau berapa latensi p95 per upstream lima menit terakhir. Jawabannya harus muncul dalam hitungan detik, bukan setelah log dirotasi.
Pola arsitektur yang semakin populer untuk kebutuhan ini adalah log sebagai event stream: log dikirim ke broker streaming lalu dimaterialisasi ke database OLAP kolumnar. Di artikel ini kita memakai Redpanda, broker yang kompatibel penuh dengan API Kafka, berupa satu binary C++ tanpa ZooKeeper/JVM. Pasangannya adalah ClickHouse, OLAP kolumnar yang mampu memindai ratusan juta baris per detik. Karena Redpanda berbicara protokol Kafka, Kafka table engine bawaan ClickHouse bisa langsung mengonsumsinya tanpa konektor tambahan.
1. Alur Data
- Nginx menulis access log dalam format JSON.
- Vector (shipper ringan berbasis Rust) membaca file log dan mem-publish setiap baris ke topik Redpanda
nginx-logs. - Redpanda menyimpan event secara durable dan terpartisi, sekaligus menjadi buffer saat ClickHouse sedang maintenance.
- ClickHouse: tabel engine
Kafkabertindak sebagai consumer, lalu materialized view menulis hasil transformasi ke tabelMergeTree.
2. Menjalankan Redpanda dan ClickHouse dengan Docker Compose
# docker-compose.yml
services:
redpanda:
image: docker.redpanda.com/redpandadata/redpanda:latest
command:
- redpanda start
- --mode dev-container
- --smp 1
- --kafka-addr internal://0.0.0.0:9092,external://0.0.0.0:19092
- --advertise-kafka-addr internal://redpanda:9092,external://localhost:19092
ports: ["19092:19092", "9644:9644"]
volumes: ["redpanda-data:/var/lib/redpanda/data"]
clickhouse:
image: clickhouse/clickhouse-server:latest
environment:
CLICKHOUSE_USER: analyst
CLICKHOUSE_PASSWORD: ${CH_PASSWORD:?set CH_PASSWORD}
ports: ["8123:8123", "9000:9000"]
ulimits: { nofile: { soft: 262144, hard: 262144 } }
volumes: ["ch-data:/var/lib/clickhouse"]
volumes:
redpanda-data:
ch-data:
export CH_PASSWORD='ganti-dengan-password-kuat'
docker compose up -d
# Buat topik dengan 6 partisi (sesuaikan dengan jumlah consumer ClickHouse)
docker compose exec redpanda rpk topic create nginx-logs -p 6 -r 1
docker compose exec redpanda rpk cluster health
--mode dev-container mematikan beberapa jaminan durabilitas (seperti fsync agresif) demi kemudahan lab. Untuk produksi, jalankan minimal 3 broker dengan -r 3, disk NVMe lokal (bukan NFS), dan aktifkan SASL/SCRAM + TLS pada listener Kafka.
3. Format Log Nginx JSON dan Vector Shipper
3.1 Log Format JSON di Nginx
# /etc/nginx/conf.d/json_log.conf
log_format json_analytics escape=json
'{"ts":"$time_iso8601","host":"$host","ip":"$remote_addr",'
'"method":"$request_method","uri":"$uri","status":$status,'
'"bytes":$body_bytes_sent,"rt":$request_time,'
'"upstream_rt":"$upstream_response_time","ua":"$http_user_agent"}';
access_log /var/log/nginx/access.json json_analytics;
3.2 Konfigurasi Vector
# /etc/vector/vector.yaml
sources:
nginx:
type: file
include: ["/var/log/nginx/access.json"]
transforms:
parse:
type: remap
inputs: ["nginx"]
source: |
. = parse_json!(.message)
sinks:
redpanda:
type: kafka
inputs: ["parse"]
bootstrap_servers: "10.10.10.50:19092"
topic: "nginx-logs"
encoding:
codec: json
compression: zstd
sudo systemctl enable --now vector
# Verifikasi pesan masuk ke topik
docker compose exec redpanda rpk topic consume nginx-logs --num 3
4. Skema ClickHouse: Kafka Engine, Tabel Target, Materialized View
4.1 Tabel Consumer (Kafka Engine)
Tabel ini tidak menyimpan data. Ia adalah consumer group yang membaca topik. Pengaturan kafka_handle_error_mode = 'stream' mencegah satu pesan JSON rusak menghentikan seluruh ingest; pesan rusak tersedia di kolom virtual _error.
CREATE DATABASE IF NOT EXISTS weblogs;
CREATE TABLE weblogs.nginx_queue
(
ts String, host String, ip String, method String, uri String,
status UInt16, bytes UInt64, rt Float32, upstream_rt String, ua String
)
ENGINE = Kafka
SETTINGS kafka_broker_list = 'redpanda:9092',
kafka_topic_list = 'nginx-logs',
kafka_group_name = 'ch-nginx-logs',
kafka_format = 'JSONEachRow',
kafka_num_consumers = 3,
kafka_thread_per_consumer = 1,
kafka_handle_error_mode = 'stream';
4.2 Tabel Penyimpanan MergeTree
Pemilihan ORDER BY menentukan performa. Urutkan dari kolom berkardinalitas rendah yang paling sering difilter (host, status) ke kolom waktu. LowCardinality mengompresi string berulang secara drastis, dan TTL menghapus data lama secara otomatis.
CREATE TABLE weblogs.nginx
(
ts DateTime64(3, 'Asia/Makassar'),
host LowCardinality(String),
ip IPv4,
method LowCardinality(String),
uri String,
status UInt16,
bytes UInt64,
rt Float32,
ua String
)
ENGINE = MergeTree
PARTITION BY toYYYYMMDD(ts)
ORDER BY (host, status, ts)
TTL toDateTime(ts) + INTERVAL 30 DAY;
CREATE MATERIALIZED VIEW weblogs.nginx_mv TO weblogs.nginx AS
SELECT parseDateTime64BestEffort(ts, 3) AS ts,
host, toIPv4OrDefault(ip) AS ip, method, uri, status, bytes, rt, ua
FROM weblogs.nginx_queue
WHERE length(_error) = 0;
-- Simpan pesan rusak untuk investigasi
CREATE TABLE weblogs.nginx_errors (raw String, error String, at DateTime DEFAULT now())
ENGINE = MergeTree ORDER BY at TTL at + INTERVAL 7 DAY;
CREATE MATERIALIZED VIEW weblogs.nginx_errors_mv TO weblogs.nginx_errors AS
SELECT _raw_message AS raw, _error AS error FROM weblogs.nginx_queue
WHERE length(_error) > 0;
ip menjadi IPv6 dan gunakan toIPv6OrDefault(ip); alamat IPv4 akan disimpan dalam bentuk IPv4-mapped.
5. Query Analitik dan Deteksi Serangan
5.1 Error Rate dan Latensi p95 per Menit
SELECT toStartOfMinute(ts) AS menit, host,
count() AS req,
round(countIf(status >= 500) / count() * 100, 2) AS err5xx_pct,
round(quantile(0.95)(rt) * 1000) AS p95_ms
FROM weblogs.nginx
WHERE ts >= now() - INTERVAL 15 MINUTE
GROUP BY menit, host
ORDER BY menit DESC, err5xx_pct DESC;
5.2 Deteksi Scanner dan Brute Force
Query berikut menemukan IP yang menghasilkan banyak 404 ke path sensitif atau mencoba login berulang. Hasilnya bisa diumpankan ke firewall (nftables set, CrowdSec bouncer, atau ModSecurity) untuk pemblokiran otomatis.
SELECT ip,
count() AS hits,
countIf(status = 404) AS not_found,
uniqExact(uri) AS distinct_uri,
countIf(match(uri, '(?i)(wp-login|xmlrpc|\\.env|phpmyadmin|/\\.git)')) AS sensitive
FROM weblogs.nginx
WHERE ts >= now() - INTERVAL 5 MINUTE
GROUP BY ip
HAVING sensitive > 5 OR (not_found > 100 AND distinct_uri > 50)
ORDER BY hits DESC
LIMIT 20;
5.3 Mengukur Lag End-to-End
-- Lag consumer group di sisi Redpanda
docker compose exec redpanda rpk group describe ch-nginx-logs
-- Lag ingest di sisi ClickHouse (detik)
SELECT max(now64(3) - ts) AS lag_detik FROM weblogs.nginx WHERE ts >= now() - INTERVAL 1 MINUTE;
-- Status consumer Kafka engine
SELECT database, table, consumer_id, num_messages_read, last_exception
FROM system.kafka_consumers;
SELECT langsung pada nginx_queue di produksi: offset akan maju dan data itu hilang dari materialized view. Untuk mengubah logika MV, lakukan DETACH TABLE weblogs.nginx_mv, ubah, lalu ATTACH. Pastikan juga jumlah partisi topik ≥ total kafka_num_consumers di seluruh replika.
6. Tips Produksi
- Batching: naikkan
kafka_max_block_sizedan aturkafka_flush_interval_ms(misalnya 1000 ms). ClickHouse lebih efisien dengan insert besar yang jarang daripada insert kecil yang sering. - Agregasi pra-hitung: tambahkan MV kedua ke tabel
AggregatingMergeTreeper menit agar dashboard Grafana tidak memindai data mentah. - Keamanan: gunakan named collection untuk kredensial SASL Redpanda, batasi user ClickHouse dengan
readonly=1untuk dashboard, dan jangan ekspos port 8123/9000 ke internet. - Lakehouse: Redpanda mendukung fitur Iceberg Topics di edisi tertentu, dan ClickHouse dapat membaca tabel Iceberg. Data panas tetap di ClickHouse, sedangkan histori jangka panjang bisa diarsipkan ke lakehouse S3.
Kesimpulan
Redpanda + ClickHouse membentuk pipeline log real-time yang sederhana tetapi bertenaga: satu binary broker, satu database kolumnar, dan transformasi berbasis SQL. Hasilnya, tim ops dan keamanan mendapatkan visibilitas error rate, latensi, dan pola serangan dalam hitungan detik. Kapasitasnya pun mudah diskalakan dengan menambah partisi, consumer, atau shard tanpa merombak arsitektur.
COMMENTS