RDB and AOF
ทดสอบบน Redis 8.10.2 (
redis:8.10.2-alpine), ioredis 6.0.0, Node 24 บน Mac (Apple silicon) transcript แบบ# cli/...ถูก harness ของคอร์สรันซ้ำ ส่วนบล็อกconsoleคือ sessiondocker execจริงกับ container ชั่วคราวของผมที่มี volume บน/data(มันเปลี่ยนไฟล์ จึงไม่ถูกรันซ้ำ) เอกสารอ้างอิง persistence: Redis persistence docs
RDB — snapshot เป็นระยะ
หัวข้อที่มีชื่อว่า “RDB — snapshot เป็นระยะ”RDB เขียน snapshot ของ dataset ทั้งหมด ณ ช่วงเวลาหนึ่งลงไฟล์ไบนารีกะทัดรัดหนึ่งไฟล์ (dump.rdb) ตอน startup Redis โหลดมัน ทำให้ RDB เป็นวิธีที่เร็วที่สุดในการกลับมาหลัง restart
แบบ manual: SAVE เขียนไฟล์ บน command thread (ทุกคนรอ); BGSAVE fork child ให้เขียนขณะที่ parent ยังให้บริการ แบบอัตโนมัติ: กฎ save <seconds> <changes> ระบบทำ snapshot เมื่อมี key เปลี่ยนอย่างน้อย <changes> ตัวภายใน <seconds> วินาที นี่คือค่าเริ่มต้นของ image ทางการจากเซิร์ฟเวอร์โดยตรง พร้อมคำตอบที่ตัดสินหัวข้อถัดไป:
127.0.0.1:6379> CONFIG GET save1) "save"2) "3600 1 300 100 60 10000"127.0.0.1:6379> CONFIG GET appendonly1) "appendonly"2) "no"127.0.0.1:6379> CONFIG GET aof-use-rdb-preamble1) "aof-use-rdb-preamble"2) "yes"127.0.0.1:6379> CONFIG GET dbfilename1) "dbfilename"2) "dump.rdb"127.0.0.1:6379> SET greeting helloOK127.0.0.1:6379> BGSAVEBackground saving started127.0.0.1:6379> LASTSAVE(integer) 1790750454image ใหม่จึง snapshot ตามกฎ 3600 1 300 100 60 10000 ไม่มี AOF (appendonly no) และจะใช้ RDB preamble ถ้าคุณเปิด AOF BGSAVE ตอบทันทีด้วย Background saving started; การบันทึกเสร็จทีหลัง ดังนั้นให้ตรวจว่าเสร็จแล้วแทนที่จะสมมติ:
// @runimport { Redis } from 'ioredis';
const redis = new Redis(process.env.REDIS_URL!);
// INFO returns "field:value" lines; turn a section into an objectasync function info(section: string) { const out: Record<string, string> = {}; for (const line of (await redis.info(section)).split('\r\n')) { const i = line.indexOf(':'); if (i > 0) out[line.slice(0, i)] = line.slice(i + 1); } return out;}
await redis.set('greeting', 'hello');const before = Number((await info('persistence')).rdb_saves);console.log(await redis.bgsave()); // Background saving started
// BGSAVE only STARTS the save: poll until it finished, then check the statuslet p = await info('persistence');while (p.rdb_bgsave_in_progress === '1' || Number(p.rdb_saves) === before) { await new Promise((r) => setTimeout(r, 50)); p = await info('persistence');}console.log('rdb_last_bgsave_status:', p.rdb_last_bgsave_status); // okconsole.log('changes since last save:', p.rdb_changes_since_last_save); // 0console.log('LASTSAVE matches INFO:', (await redis.lastsave()) === Number(p.rdb_last_save_time)); // true
await redis.quit();Output จริง (Mac เครื่องนี้):
Background saving startedrdb_last_bgsave_status: okchanges since last save: 0LASTSAVE matches INFO: trueLASTSAVE เป็น Unix timestamp หน่วย วินาที ดังนั้นสอง save ในวินาทีเดียวกันแยกไม่ออก loop ข้างบนจึงดู rdb_saves และ rdb_bgsave_in_progress จาก INFO persistence แทน
save 3600 1save 300 100save 60 10000dbfilename dump.rdbdir /dataสามกฎ save นั้นหมายถึง: ทุกชั่วโมงถ้ามี key เปลี่ยนอย่างน้อย 1, ทุก 5 นาทีถ้าเปลี่ยน 100, ทุกนาทีถ้าเปลี่ยน 10,000 save "" ปิด snapshot อัตโนมัติ ต้นทุนของ RDB คือช่วงเวลา: อะไรที่เขียนหลัง snapshot ล่าสุดจะหายเมื่อ crash
SAVE บล็อก, BGSAVE fork
หัวข้อที่มีชื่อว่า “SAVE บล็อก, BGSAVE fork”ผมโหลด key เล็กๆ 1,000,000 ตัว รัน SAVE จาก client หนึ่งและส่ง PING จากอีก client 20 ms ต่อมา:
// @runimport { Redis } from 'ioredis';
const a = new Redis(process.env.REDIS_URL!);const b = new Redis(process.env.REDIS_URL!);await a.flushall();await a.eval("for i=1,1000000 do redis.call('SET','k:'..i,'value-'..i) end return 1", 0); // 1M small keysawait b.ping();
// SAVE runs on the command thread: a second client's PING waits for itconst t0 = performance.now();const saving = a.save();await new Promise((r) => setTimeout(r, 20));const t1 = performance.now();await b.ping();const waited = performance.now() - t1;await saving;console.log(`SAVE took ${(performance.now() - t0).toFixed(0)} ms; a PING sent 20 ms later waited ${waited.toFixed(0)} ms`);
// BGSAVE forks: the reply is immediate and other clients keep being servedawait new Promise((r) => setTimeout(r, 300));const t2 = performance.now();console.log(await a.bgsave(), `in ${(performance.now() - t2).toFixed(1)} ms`);const t3 = performance.now();await b.ping();console.log(`PING right after BGSAVE: ${(performance.now() - t3).toFixed(1)} ms`);
await new Promise((r) => setTimeout(r, 500));await a.flushall();a.disconnect();b.disconnect();Output จริง (Mac เครื่องนี้):
SAVE took 259 ms; a PING sent 20 ms later waited 236 msBackground saving started in 7.3 msPING right after BGSAVE: 1.2 msขณะที่ SAVE ทำงาน PING ของ client ที่สองรอ 236 ms ให้เซิร์ฟเวอร์ที่ยุ่งกับการเขียน BGSAVE ตอบกลับในไม่กี่มิลลิวินาทีและ PING ถัดมาใช้ 1.2 ms การ fork ไม่ฟรี (มันคัดลอก page table และเพิ่ม latency spike บน dataset ใหญ่มากได้ ดู latency docs) แต่การเขียนเองอยู่นอก command thread
AOF — append-only file
หัวข้อที่มีชื่อว่า “AOF — append-only file”เมื่อ appendonly yes Redis ต่อท้ายทุกคำสั่งเขียนในรูปแบบ wire ลง log และเล่นซ้ำตอน restart appendfsync ควบคุมว่าบอกให้ OS flush ลงดิสก์บ่อยแค่ไหน: always (หลังทุกคำสั่ง; ปลอดภัยสุด ช้าสุด), everysec (วินาทีละครั้ง; อาจเสียราวหนึ่งวินาที; ค่าเริ่มต้น) และ no (OS ตัดสินใจ)
appendonly yesappendfilename "appendonly.aof"appendfsync everysecaof-use-rdb-preamble yesบนดิสก์มีอะไร
หัวข้อที่มีชื่อว่า “บนดิสก์มีอะไร”ตั้งแต่ Redis 7.0 AOF คือ ไดเรกทอรีของหลายไฟล์ (multi-part AOF) ไม่ใช่ไฟล์เดียว ผมเปิด AOF บน container ที่รันอยู่ เขียนหนึ่ง key และรัน rewrite:
$ docker exec redis-b2-persist ls -A /data(nothing yet: no snapshot, no AOF)$ docker exec redis-b2-persist redis-cli SET greeting helloOK$ docker exec redis-b2-persist redis-cli CONFIG SET appendonly yesOK$ docker exec redis-b2-persist ls -l /data/appendonlydir-rw------- 110 appendonly.aof.1.base.rdb-rw------- 0 appendonly.aof.1.incr.aof-rw------- 102 appendonly.aof.manifest$ docker exec redis-b2-persist cat /data/appendonlydir/appendonly.aof.manifestfile appendonly.aof.1.base.rdb seq 1 type bfile appendonly.aof.1.incr.aof seq 1 type i startoffset 0$ docker exec redis-b2-persist head -c 9 /data/appendonlydir/appendonly.aof.1.base.rdbREDIS0015$ docker exec redis-b2-persist redis-cli SET more 1OK$ docker exec redis-b2-persist cat /data/appendonlydir/appendonly.aof.1.incr.aof*2$6SELECT$10*3$3SET$4more$11$ docker exec redis-b2-persist redis-cli BGREWRITEAOFBackground append only file rewriting started$ docker exec redis-b2-persist ls /data/appendonlydirappendonly.aof.2.base.rdbappendonly.aof.2.incr.aofappendonly.aof.manifest$ docker exec redis-b2-persist cat /data/appendonlydir/appendonly.aof.manifestfile appendonly.aof.2.base.rdb seq 2 type bfile appendonly.aof.2.incr.aof seq 2 type i startoffset 1อ่านรายการแบบนี้:
appendonly.aof.1.base.rdbคือ base: snapshot ของ dataset ณ ตอนที่เปิด AOF ขึ้นต้นด้วย byteREDIS0015มันจึงเป็นไฟล์ RDBappendonly.aof.1.incr.aofคือ log แบบ incremental ของคำสั่งตั้งแต่ base ในรูปแบบ wire ธรรมดาที่แสดง (*3 $3 SET $4 more $1 1คือSET more 1)appendonly.aof.manifestระบุส่วนต่างๆ และเลขลำดับBGREWRITEAOFไม่ได้เขียนไฟล์ยักษ์ใหม่: มันเขียน base ใหม่ (2) เริ่มไฟล์ incr ใหม่ (2) และอัปเดต manifest
redis-check-aof (มีใน image) ตรวจทั้งชุด และ redis-check-rdb ทำเช่นเดียวกันกับ snapshot:
$ docker exec redis-b2-persist redis-check-aof /data/appendonlydir/appendonly.aof.manifest (trimmed)Start checking Multi Part AOFStart to check BASE AOF (RDB format).[offset 0] Checking RDB file /data/appendonlydir/appendonly.aof.4.base.rdb[offset 143] Checksum OKRDB preamble is OK, proceeding with AOF tail...BASE AOF appendonly.aof.4.base.rdb is validStart to check INCR files.INCR AOF appendonly.aof.5.incr.aof is validAll AOF files and manifest are valid(เลขไฟล์สูงขึ้นที่นี่เพราะตอนนั้น container รัน rewrite และ backup มากขึ้นแล้ว)
Hybrid persistence คือสิ่งที่คุณได้อยู่แล้ว
หัวข้อที่มีชื่อว่า “Hybrid persistence คือสิ่งที่คุณได้อยู่แล้ว”“Hybrid” หมายถึง base ของ AOF ถูกเขียนในรูปแบบ RDB (โหลดเร็ว) และมีเพียง tail ที่เป็น command log การตั้งค่าคือ aof-use-rdb-preamble และมันเป็น yes เป็นค่าเริ่มต้นตั้งแต่ Redis 5.0: redis.conf ที่ส่งมาให้ระบุ aof-use-rdb-preamble no ใน 4.0 (พร้อมคอมเมนต์ว่ามันจะ “at some point be used as the default”) และ yes ใน 5.0.0 บน 8.10.2 CONFIG GET aof-use-rdb-preamble เป็น yes และ .base.rdb ข้างบนคือผลลัพธ์ มันใช้ทุกครั้งที่ AOF เปิดอยู่ ไม่จำเป็นต้องเปิด snapshot RDB ควบคู่ (ข้อความเดิมของบทนี้บอกว่า 7.0; 7.0 เปลี่ยนแค่โครงสร้างไฟล์เป็น multi-part)
BACKUP: backup ออนไลน์ (8.10+)
หัวข้อที่มีชื่อว่า “BACKUP: backup ออนไลน์ (8.10+)”การคัดลอก dump.rdb หรือ appendonlydir ด้วยมือเป็นเรื่องยุ่งเสมอ (ต้องหยุด rewrite หรือคัดลอกให้ถูกจังหวะ) Redis 8.10.0 เพิ่มกลุ่ม BACKUP (docs): BACKUP START สร้าง base ใหม่ลงใน backupdirname (ค่าเริ่มต้น backupdir ภายใต้ dir), BACKUP LIST แสดงไฟล์ที่ pin ไว้, BACKUP SEAL แช่แข็งส่วน incremental และเขียน manifest, BACKUP CLEANUP ปล่อยไฟล์, BACKUP STATUS / BACKUP ABORT ตรวจและยกเลิก ทำงานได้ขณะที่การเขียนดำเนินต่อ:
127.0.0.1:6379> BACKUP STATUS1) "state"2) "idle"3) "error"4) ""5) "start_time"6) (integer) 07) "end_time"8) (integer) 0127.0.0.1:6379> BACKUP STARTOK127.0.0.1:6379> BACKUP LIST1) "/data/backupdir/appendonly.aof.4.base.rdb"127.0.0.1:6379> SET written-during-backup 1OK127.0.0.1:6379> BACKUP SEALOK127.0.0.1:6379> BACKUP LIST1) "/data/backupdir/appendonly.aof.4.base.rdb"2) "/data/backupdir/appendonly.aof.5.incr.aof"3) "/data/backupdir/appendonly.aof.manifest"127.0.0.1:6379> BACKUP CLEANUPOK127.0.0.1:6379> BACKUP STATUS1) "state"2) "idle"3) "error"4) ""5) "start_time"6) (integer) 07) "end_time"8) (integer) 0SET ที่เขียนหลัง START อยู่ใน backup ที่ seal แล้ว (ไฟล์ incr ปรากฏใน LIST ครั้งที่สอง) หลัง SEAL คุณคัดลอกสามไฟล์ที่ระบุไปยังที่เก็บ แล้ว CLEANUP เพื่อ restore ให้ชี้ preload-file aof:/path/to/appendonly.aof.manifest (ตั้งได้ตอน startup เท่านั้น) ไปที่ manifest ต้องใช้ 8.10 ขึ้นไป; บนเซิร์ฟเวอร์รุ่นเก่ากว่าให้ใช้ BGSAVE แล้วคัดลอกไฟล์ หรือขั้นตอน AOF ที่เอกสารระบุ “ปิด rewrite, คัดลอก, เปิดใหม่” ผมไม่ได้ทดสอบ preload-file
Reading AI-generated code
หัวข้อที่มีชื่อว่า “Reading AI-generated code”AI เพิ่ม endpoint สำหรับแอดมิน “เพื่อ persist Redis เดี๋ยวนี้”:
🐛 หา bug ในโค้ดนี้
app.post('/admin/persist', async (_req, res) => {await redis.save(); // write dump.rdb nowres.json({ ok: true });});เฉลย
SAVE รันบน command thread กับ 1,000,000 key ใช้ 259 ms ซึ่งทุก client อื่น รวมถึง request ของเว็บไซต์เอง ติดอยู่ (PING รอ 236 ms) ที่ขนาด production นั่นคือ outage หลายวินาทีต่อการคลิกหนึ่งครั้ง BGSAVE fork และตอบใน 7 ms จากนั้นตรวจผลลัพธ์แทนการสมมติ:
await redis.bgsave(); // "Background saving started"// poll INFO persistence: rdb_bgsave_in_progress === '0' and rdb_last_bgsave_status === 'ok'loop poll ที่สมบูรณ์คือ persistence-expiration-rdb-aof-bgsave.ts ที่รันได้ข้างบน
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”Persistence คือ คำสัญญาสองแบบที่ต่างกัน RDB สัญญาภาพที่สอดคล้องกัน ณ ช่วงเวลาในอดีต เก็บถูกและโหลดเร็ว AOF สัญญาบันทึกความเปลี่ยนแปลงที่ต่อเนื่อง และส่วนของบันทึกที่รอดจาก crash ขึ้นกับ appendfsync Redis สมัยใหม่เก็บ AOF เป็นภาพบวกบันทึกสั้นๆ ต่อท้าย จึงได้ทั้ง restart เร็วและช่วงที่เสียเล็กพร้อมกัน
Trade-offs
หัวข้อที่มีชื่อว่า “Trade-offs”| ตัวเลือก | ข้อดี | ข้อเสีย |
|---|---|---|
| RDB อย่างเดียว | ไฟล์เล็ก restart เร็วที่สุด เหมาะกับ backup | เสียทุกอย่างตั้งแต่ snapshot ล่าสุด; การ fork ของ BGSAVE ทำ latency spike บน dataset ใหญ่มากได้ |
AOF (everysec) | เสี่ยงราวหนึ่งวินาที | ไฟล์ใหญ่ขึ้น เวลา replay งาน fsync |
AOF (always) | ช่วงที่เสียเล็กที่สุด | เขียนช้าที่สุด |
| base แบบ hybrid (ค่าเริ่มต้น) | โหลดเร็วแบบ RDB พร้อม tail เป็น command log | มีสองรูปแบบไฟล์ในไดเรกทอรีเดียวที่ต้องเข้าใจ |
SAVE | เรียบง่าย synchronous | บล็อกทุก client (259 ms ที่ 1M key) |
BACKUP (8.10+) | backup ออนไลน์ที่สอดคล้องกันพร้อม manifest | ต้อง 8.10; ยังต้องคัดลอกไฟล์ออกจากโฮสต์เอง |
Common Misconceptions
หัวข้อที่มีชื่อว่า “Common Misconceptions”- “AOF เป็นไฟล์เดียว” ตั้งแต่ 7.0 มันเป็นไดเรกทอรี: base + incr + manifest
- “Hybrid มาใน 7.0”
aof-use-rdb-preamble yesเป็นค่าเริ่มต้นตั้งแต่ 5.0 - “
BGSAVEตอบกลับแปลว่าข้อมูลปลอดภัย” แปลว่าการบันทึก เริ่มแล้ว; ตรวจrdb_last_bgsave_status - “Hybrid ต้องเปิดทั้ง RDB และ AOF” preamble ใช้ทุกครั้งที่
appendonly yes - “AOF กับ
everysecไม่เสียข้อมูลเลย” อาจเสียราวหนึ่งวินาที
Common mistakes
หัวข้อที่มีชื่อว่า “Common mistakes”SAVEบนเซิร์ฟเวอร์ที่ทำงานอยู่ — ใช้BGSAVE- RDB อย่างเดียวแต่คาดหวังความคงทนสูง — ช่วงที่เสียคือช่วง
save - ไม่เคยทดสอบการ restore — รัน
redis-check-aof/redis-check-rdbและสตาร์ตเซิร์ฟเวอร์จากสำเนาจริง - คัดลอกไฟล์ด้วยมือขณะ rewrite ทำงาน — ใช้กลุ่ม
BACKUP(8.10+) หรือขั้นตอนที่เอกสารระบุ
Real-world example
เอกสารของ Redis เอง เปรียบเทียบ RDB กับ AOF อธิบาย multi-part AOF และ RDB preamble และระบุคำสั่ง
BACKUPที่เพิ่มใน 8.10 (Redis persistence)