Architecture Mastery • Phase 2
Redis Fundamentals, History & RAM-First Internals
The Paradigm Shift: From Disk Buffers to In-Memory Event Loop, Memory Encodings, and High-Performance Persistence
1. The "Why" & Origins: Breaking the Disk Bottleneck
ในปี 2009 วิศวกรชาวซิซิลีชื่อ Salvatore Sanfilippo (antirez) กำลังพัฒนาระบบ Real-time Web Log Analytics ชื่อว่า LLOOGG ปัญหาใหญ่ที่เขาเผชิญคือ: Relational Database (MySQL) รับโหลดการเขียน Log วิเคราะห์แบบ Real-time ไม่ไหว
แม้จะพยายามปรับแต่ง Index และรันบนเซิร์ฟเวอร์ราคาแพง Disk I/O Bottleneck ก็ยังคงเกิดขึ้น เพราะทุกๆ การอัปเดตตัวเลขสถิติ (Pageview Counter, Unique Visitors) RDBMS ต้องทำ Disk-bound operations, B+ Tree updates, และ Lock contention ข้าม Transactions
ทำไม Memcached ถึงยังตอบโจทย์ไม่สุด?
ในยุคนั้น Memcached เป็น In-Memory Key-Value Cache ยอดนิยม แต่มีข้อจำกัดเชิงสถาปัตยกรรมที่สำคัญ:
- Data Structure-Blind: Memcached เก็บข้อมูลเป็นเพียง "Opaque Blob (Raw Bytes/String)" หากต้องการอัปเดตสมาชิกใน Array หรือนับยอดเงิน ต้องดึง Blob ทั้งก้อนผ่าน Network มา Deserialize บน Application แก้ไข แล้ว Serialize ส่งกลับไป Set ทับ (เกิด Network NIC saturation และ Race condition มหาศาล)
- No Built-in Persistence: ข้อมูลระเหยหายหมด 100% หาก Process ดับหรือ Restart
- No Server-side Computation: ไม่สามารถคำนวณเซต intersection, union, หรือ range query ภายในหน่วยความจำของ Cache Server ได้
การถือกำเนิดของ RE.D.I.S. (REmote DIctionary Server):
Antirez จึงสร้าง Data Structure Server ที่เปลี่ยนมุมมองใหม่: ไม่ใช่แค่ "Cache เก็บสตริง" แต่เป็น "Remote Data Structure Engine" ที่ให้ Client ส่งคำสั่งเชิงตรรกะมาประมวลผลตรงๆ ใน RAM ของเซิร์ฟเวอร์แบบ Atomic
2. RAM-First Architecture: สถาปัตยกรรมที่ทิ้ง Disk ออกจาก Critical Path
เมื่อเปรียบเทียบกับ PostgreSQL (Disk-First) ที่เราศึกษาใน Phase 1 สถาปัตยกรรมของ Redis แตกต่างกันอย่างสิ้นเชิงในระดับรากฐาน:
DISK-FIRST ARCHITECTURE (e.g. PostgreSQL)
=========================================
Client Query ---> [ Optimizer ] ---> [ Buffer Pool Lookup ]
|
(Buffer Miss) | (Slow Page Fault)
v
[ OS Page Cache / NVMe Disk ]
(Latency: 10,000 - 50,000 ns)
|
v
[ shared_buffers ]
|
[ WAL Flush (fsync) ]-+-> (Ack Client)
RAM-FIRST ARCHITECTURE (Redis)
==============================
Client Command -> [ Network Socket ] -> [ Multiplexing (epoll) ]
|
v
[ SINGLE-THREAD EVENT LOOP ]
|
(Direct RAM Memory Access)
(Latency: 50 - 100 ns)
|
+-> [ Reply Buffer ] -> Client Ack
:
: (Asynchronous / Non-blocking)
v
[ RDB Fork / AOF Write ] (Background/Separate)
| มิติทางสถาปัตยกรรม |
Disk-First (RDBMS / PostgreSQL) |
RAM-First (Redis) |
| Primary Data Home |
Physical Disk Blocks (Tablespace 8KB Pages) |
DRAM Main Memory (Direct Pointers / Allocators) |
| บทบาทของ RAM |
Cache ชั่วคราว (Buffer Pool / shared_buffers) |
Single Source of Truth ในการประมวลผล |
| Critical Path Latency |
~500 µs – 10 ms (ผูกกับ Disk I/O & WAL fsync) |
~10 – 100 µs (RAM Speed ไร้ Disk Wait) |
| Data Layout ใน RAM |
Fixed-size 8KB Pages จำลองโครงสร้างดิสก์ |
Native C Data Structures (SkipList, Dict, SDS) |
| Concurrency Model |
Multi-process / Multi-thread + Heavy Row/Table Locks |
Single-Threaded Event Loop ไร้ Lock Contention |
3. The Single-Threaded Event Loop & I/O Multiplexing
คำถามยอดฮิตระดับ Lead Architect: "ในยุค Multi-Core CPU ทำไม Redis ถึงใช้ Single Thread ในการประมวลผลคำสั่ง แล้วทำไมมันถึงเร็วกว่าระบบ Multi-threaded RDBMS ทั่วไป?"
+-------------------------------------------------------------------------+
| REDIS PROCESS ARCHITECTURE |
| |
| Clients (10,000+ Sockets) |
| | | | |
| v v v |
| +------------------------+ |
| | OS Kernel epoll/kqueue | (I/O Multiplexing: Monitor socket readiness)|
| +-----------+------------+ |
| | Ready File Events (Read/Write) |
| v |
| +-------------------------------------------------------------------+ |
| | aeEventLoop (Single Main Thread) | |
| | | |
| | +------------------+ +-------------------+ +--------------+ | |
| | | Read QueryBuf |-->| Execute Command |-->| Write Output | | |
| | | (Parse RESP) | | (RAM Dictionary) | | (Send reply) | | |
| | +------------------+ +-------------------+ +--------------+ | |
| | ^ | |
| | | Time Events (ServerCron: Eviction, Expiration, Stats) | |
| +-----------+-------------------------------------------------------+ |
| |
| (Background BIO Threads: Async AOF fsync, Lazy Freeing / UNLINK) |
+-------------------------------------------------------------------------+
3 เหตุผลทางฟิสิกส์ฮาร์ดแวร์ที่ Single Thread ชนะ
- No Context Switching Overhead: การสลับ Thread ของ OS บังคับให้ CPU ต้อง Save/Restore Registers, สลับ Kernel/User Mode, และที่ร้ายแรงที่สุดคือทำลาย CPU L1/L2 Cache (Cache Pollution / Thrashing) การรัน Single Thread ต่อเนื่องทำให้ CPU Instruction Cache ทำงานได้อย่างเต็มประสิทธิภาพสูงสุด
- No Concurrency Control Overhead: ไม่ต้องมี Mutex, Spinlock, Semaphore หรือ Read-Write Locks ในการปกป้อง Data Structures ระดับ RAM ไม่มีความเสี่ยง Deadlock และไม่มี CPU Cycles สูญเปล่าไปกับการแย่ง Lock
- CPU Is Not the Bottleneck: Bottleneck ของ In-Memory Store คืองาน Memory Bandwidth และ Network Bandwidth (Packet processing) ไม่ใช่ CPU Speed โดย 1 Core ของ CPU ยุคใหม่สามารถ Execute Redis Memory Operations ได้ทะลุ 100,000–200,000 Ops/Sec สบายๆ
กฎเหล็กของ Single-Threaded Event Loop (Zero Blocking Rule):
เนื่องจากคำสั่งถูกรันต่อคิวกันบน Thread เดียว หากมีคำสั่งใดคำสั่งหนึ่งกินเวลา CPU นาน (Time Complexity $O(N)$ เช่น KEYS *, HGETALL บนแฮชยักษ์, หรือ FLUSHALL) ทุก Client ในระบบจะถูก Block ทั้งหมดทันที! นี่คือจุดกำเนิดของข้อห้ามใช้งานคำสั่งอันตรายบน Production
4. Data Structures & Memory Encodings (Under The Hood)
หัวใจที่ทำให้ Redis ประหยัด RAM มหาศาลและเข้าถึงข้อมูลระดับ Sub-microsecond คือการเลือกใช้ Internal Memory Encodings ที่ปรับเปลี่ยนตามขนาดข้อมูลโดยอัตโนมัติ
Logical Type Internal Compact Encoding Threshold Triggered
--------------------------------------------------------------------------------
String -------> int / embstr / raw (44 bytes boundary)
List -------> quicklist (Listpack/Ziplist) (list-max-listpack-size)
Hash -------> listpack (ziplist) -> hashtable (hash-max-listpack-entries)
Set -------> intset -> hashtable (set-max-intset-entries)
Sorted Set -------> listpack -> skiplist + dict (zset-max-listpack-entries)
1. Simple Dynamic String (SDS) vs C-String
Redis ไม่ใช้ C-String แบบดั้งเดิม (char* สิ้นสุดด้วย