Loki Architecture: เข้าใจการออกแบบระบบ Log Aggregation

Loki เป็น log aggregation system ที่ Grafana Labs ออกแบบมาให้ประหยัดต้นทุนและ scale แบบ horizontal ได้ดีกว่าเดิมมาก ต่างจากระบบเก็บ log แบบเก่าที่ index ทุกฟิลด์ Loki เลือก index เฉพาะ label ทำให้ storage footprint เล็กกว่าและ operate ง่ายขึ้น บทความนี้อธิบาย architecture ของระบบ — component ย่อยแต่ละตัวทำหน้าที่อะไร, flow ของ log ตั้งแต่ client ถึง storage และเหตุผลเชิงออกแบบที่ทำให้ระบบนี้ต่างจาก Elasticsearch อย่างชัดเจน

Design Philosophy — ทำไมต้อง Index เฉพาะ Label

ในระบบเก็บ log แบบ full-text ทุกคำถูก tokenize และสร้าง inverted index ผลคือต้องใช้ RAM และ disk IOPS สูง เพราะ index ใหญ่มาก ทีมที่มี log วันละ TB มักจ่ายค่า compute และ storage หลักหลายหมื่น USD/เดือน ทีมงานวิศวกรออกแบบระบบใหม่จึงมองว่า query log จริง ๆ ส่วนมากเริ่มจากคำถามแบบ “log ของ service X ช่วงเวลา Y” ไม่ใช่ “ค้นคำ foo ในข้อมูลทั้งหมด” ดังนั้นถ้า index แค่ label ที่จำเป็นสำหรับ routing และให้ grep ข้อความเป็น post-filter จะประหยัด resource ได้มหาศาล

หลักการนี้ทำให้ storage ของ Loki เป็นแค่ object storage (S3, GCS, Azure Blob) ที่เก็บ chunk ของ log แบบ compressed ส่วน index เป็นแค่ตาราง label → chunk ที่ขนาดเล็กกว่า data จริง 100-1000 เท่า การ scale จึงใช้ stateless microservice ที่เพิ่ม replica ได้ตาม load โดยไม่ต้อง rebalance shard แบบ Elasticsearch

Deployment Mode — Monolithic vs Microservice

Loki รองรับ 3 mode: monolithic (ทุก component รันใน process เดียว), simple scalable (แยกเป็น 2 roles: read + write), และ microservice mode ที่แยกทุก component เป็น service อิสระ สำหรับ volume ต่ำถึงปานกลาง (ไม่เกิน 500 GB/วัน) monolithic mode เพียงพอและ operate ง่ายที่สุด simple scalable เหมาะกับ mid-size ที่ต้องการ scale read แยกจาก write — เช่นเวลา query เยอะในช่วง incident แต่ ingest rate คงที่

Microservice mode ใช้ใน production ขนาดใหญ่ที่ต้องการ tune แต่ละ path อย่างละเอียด — distributor, ingester, querier, query-frontend, ruler, compactor, index-gateway, query-scheduler แยก deployment และ scale อิสระ mode นี้ต้องการ Kubernetes operator หรือ Helm chart ที่ดูแล dependency เพราะ component มี order boot ที่ชัดเจน

Components หลักและหน้าที่

  • Distributor — รับ log จาก client (Promtail, Fluent Bit, Vector) ตรวจ label validation, rate limit และ forward ไปให้ ingester ตาม consistent hash ของ tenant + label set
  • Ingester — เก็บ log เข้า memory chunk แบบ compressed (gzip/snappy/lz4) เมื่อ chunk เต็มหรือหมดเวลา จะ flush ลง object storage และเขียน index entry ลง backend (BoltDB-shipper, TSDB, Cassandra)
  • Querier — รับ LogQL query ค้น index แล้วดึง chunk จาก storage นำมา decompress และ filter ให้ตรงกับ expression ของ query
  • Query Frontend — แบ่ง query ใหญ่เป็น sub-query, cache ผลลัพธ์ด้วย Memcached และ queue ให้ querier ทำงานเป็น parallel เพื่อลด latency
  • Compactor — merge index file ย่อย ๆ ให้เล็กลงและลบ chunk ที่เกิน retention รวมทั้งเขียน tombstone สำหรับ delete request (GDPR)
  • Ruler — รัน alert rule ที่เขียนด้วย LogQL ส่ง alert ไปยัง Alertmanager เหมือนกับ Prometheus rule

Flow การเดินทางของ Log

Client เช่น Promtail อ่าน log file ของ application, เติม label (service, host, level), แล้ว POST ไปยัง distributor endpoint distributor ตรวจ schema, validate label cardinality, เลือก ingester 3 node (replication factor default = 3) และ forward log ไปด้วย gRPC ingester สร้าง in-memory chunk หนึ่งอันต่อ label set เมื่อ chunk ถึง target size (1.5 MB) หรือ idle time (1 ชั่วโมง) จะ flush ลง S3 พร้อมกับ update index

ตอน query ผู้ใช้เปิด Grafana Explore กด query เช่น {app="api"} |= "error" — query frontend แบ่ง time range เป็น 15 นาที × N ส่งต่อให้ querier แต่ละตัว ไป query จาก ingester (สำหรับ log ล่าสุดที่ยัง buffer อยู่) และจาก object storage (สำหรับ log เก่า) ผลลัพธ์ถูก merge, deduplicate ด้วย sample hash และ return กลับพร้อม aggregation

Index Backend — BoltDB-shipper, TSDB, Cassandra

เวอร์ชันแรกของ Loki ใช้ Cassandra หรือ DynamoDB เป็น index เพราะต้องการ scalable key-value store แต่ operational cost สูงมาก ทีมจึงสร้าง BoltDB-shipper — ingester เขียน BoltDB file ลง local แล้ว sync ขึ้น object storage ตาม interval querier ดึง file นั้นลงมา mmap อ่าน ทำให้ index เป็นแค่ object ใน S3 ไม่ต้องมี database เพิ่ม

เวอร์ชันปัจจุบันแนะนำ TSDB index ที่ port มาจาก Prometheus — compact กว่า, query เร็วกว่า, รองรับ label cardinality สูงได้ดีขึ้น โครงการใหม่ทั้งหมดควรเริ่มด้วย TSDB ส่วน BoltDB-shipper ยังรองรับ legacy deployment

ตัวอย่าง Config Microservice Mode

# loki-config.yaml - microservice mode ย่อ
target: distributor  # เปลี่ยนตาม role: ingester, querier, etc.

auth_enabled: true   # multi-tenancy ผ่าน X-Scope-OrgID

server:
  http_listen_port: 3100
  grpc_listen_port: 9095

common:
  replication_factor: 3
  path_prefix: /loki
  storage:
    s3:
      endpoint: s3.amazonaws.com
      bucketnames: my-loki-logs
      region: ap-southeast-1

memberlist:
  join_members:
    - loki-gossip.monitoring.svc.cluster.local

schema_config:
  configs:
    - from: 2024-01-01
      store: tsdb
      object_store: s3
      schema: v13
      index:
        prefix: index_
        period: 24h

limits_config:
  retention_period: 720h  # 30 วัน
  max_query_series: 5000
  ingestion_rate_mb: 10

Cardinality — ศัตรูตัวฉกาจของ Loki

เนื่องจาก index เก็บตาม label set ถ้า label มี value หลากหลายมาก (เช่น user_id, request_id, session_id) index จะระเบิดและ ingester จะใช้ memory หนักมาก จนอาจ crash ทีมต้องวาง label design ให้ low cardinality เท่านั้น — service, app, environment, level, pod เหมาะที่จะเป็น label ส่วน user_id, trace_id ควรอยู่ใน body log ใช้ | json pipeline extract ตอน query แทน

Rule of thumb: จำนวน active stream ต่อ tenant ไม่ควรเกิน 100,000 ในช่วงเวลาเดียว ถ้าเกินต้องรวม label ที่คล้ายกันหรือย้ายออกจาก label เป็น log content

Multi-tenancy และ Isolation

Loki รองรับ multi-tenancy แบบ native — ทุก request ต้องมี header X-Scope-OrgID ระบุ tenant distributor, ingester, querier จะ route data แยกกันตาม tenant id และ storage มี path prefix เช่น s3://bucket/tenant-a/index/... ใช้ได้ดีกับ managed service ที่ให้ลูกค้าหลายรายใช้ cluster เดียวกัน

Limit ระดับ tenant ตั้งใน limits_config ที่ override ด้วย per_tenant_override_config — แต่ละลูกค้าได้ ingest rate, retention และ query concurrency ต่างกันได้ เหมาะกับ billing แบบ pay-per-use

สรุป

Architecture ของ Loki ออกแบบมาให้ประหยัด storage และ operate ง่ายบน Kubernetes — ใช้ object storage แทน disk, index เฉพาะ label, scale แยก read/write ตามโหลด การรู้จัก component ย่อย (distributor, ingester, querier, compactor) ช่วยให้ debug ปัญหาและ tune performance ได้ตรงจุด ทีมที่เริ่มใช้ควรตั้งต้นจาก simple scalable mode ก่อนแล้วค่อยย้ายเป็น microservice เมื่อ workload โต

การระวัง label cardinality เป็นสิ่งสำคัญที่สุดในการใช้ระบบนี้ — ถ้า schema ดี ระบบจะ scale ได้ถึง petabyte ต่อเดือนในราคาเศษเสี้ยวของ Elasticsearch แต่ถ้า schema ผิด ingester จะเจ๊ง memory ก่อนที่ log จะถึง storage ด้วยซ้ำ ทีมที่ย้ายจาก ELK ต้องปรับ mindset เรื่อง label design ให้ต่างจากการ index full-text เดิม