ข้ามไปยังเนื้อหา

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 คือ session docker exec จริงกับ container ชั่วคราวของผมที่มี volume บน /data (มันเปลี่ยนไฟล์ จึงไม่ถูกรันซ้ำ) เอกสารอ้างอิง persistence: Redis persistence docs

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 ทางการจากเซิร์ฟเวอร์โดยตรง พร้อมคำตอบที่ตัดสินหัวข้อถัดไป:

cli/persistence-expiration-rdb-aof-1.redis
127.0.0.1:6379> CONFIG GET save
1) "save"
2) "3600 1 300 100 60 10000"
127.0.0.1:6379> CONFIG GET appendonly
1) "appendonly"
2) "no"
127.0.0.1:6379> CONFIG GET aof-use-rdb-preamble
1) "aof-use-rdb-preamble"
2) "yes"
127.0.0.1:6379> CONFIG GET dbfilename
1) "dbfilename"
2) "dump.rdb"
127.0.0.1:6379> SET greeting hello
OK
127.0.0.1:6379> BGSAVE
Background saving started
127.0.0.1:6379> LASTSAVE
(integer) 1790750454

image ใหม่จึง snapshot ตามกฎ 3600 1 300 100 60 10000 ไม่มี AOF (appendonly no) และจะใช้ RDB preamble ถ้าคุณเปิด AOF BGSAVE ตอบทันทีด้วย Background saving started; การบันทึกเสร็จทีหลัง ดังนั้นให้ตรวจว่าเสร็จแล้วแทนที่จะสมมติ:

src/persistence-expiration-rdb-aof-bgsave.ts
// @run
import { Redis } from 'ioredis';
const redis = new Redis(process.env.REDIS_URL!);
// INFO returns "field:value" lines; turn a section into an object
async 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 status
let 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); // ok
console.log('changes since last save:', p.rdb_changes_since_last_save); // 0
console.log('LASTSAVE matches INFO:', (await redis.lastsave()) === Number(p.rdb_last_save_time)); // true
await redis.quit();

Output จริง (Mac เครื่องนี้):

Background saving started
rdb_last_bgsave_status: ok
changes since last save: 0
LASTSAVE matches INFO: true

LASTSAVE เป็น Unix timestamp หน่วย วินาที ดังนั้นสอง save ในวินาทีเดียวกันแยกไม่ออก loop ข้างบนจึงดู rdb_saves และ rdb_bgsave_in_progress จาก INFO persistence แทน

config/persistence-expiration-rdb-aof-save.conf
save 3600 1
save 300 100
save 60 10000
dbfilename dump.rdb
dir /data

สามกฎ save นั้นหมายถึง: ทุกชั่วโมงถ้ามี key เปลี่ยนอย่างน้อย 1, ทุก 5 นาทีถ้าเปลี่ยน 100, ทุกนาทีถ้าเปลี่ยน 10,000 save "" ปิด snapshot อัตโนมัติ ต้นทุนของ RDB คือช่วงเวลา: อะไรที่เขียนหลัง snapshot ล่าสุดจะหายเมื่อ crash

ผมโหลด key เล็กๆ 1,000,000 ตัว รัน SAVE จาก client หนึ่งและส่ง PING จากอีก client 20 ms ต่อมา:

src/persistence-expiration-rdb-aof-save-blocks.ts
// @run
import { 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 keys
await b.ping();
// SAVE runs on the command thread: a second client's PING waits for it
const 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 served
await 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 ms
Background saving started in 7.3 ms
PING 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

เมื่อ appendonly yes Redis ต่อท้ายทุกคำสั่งเขียนในรูปแบบ wire ลง log และเล่นซ้ำตอน restart appendfsync ควบคุมว่าบอกให้ OS flush ลงดิสก์บ่อยแค่ไหน: always (หลังทุกคำสั่ง; ปลอดภัยสุด ช้าสุด), everysec (วินาทีละครั้ง; อาจเสียราวหนึ่งวินาที; ค่าเริ่มต้น) และ no (OS ตัดสินใจ)

config/persistence-expiration-rdb-aof-aof.conf
appendonly yes
appendfilename "appendonly.aof"
appendfsync everysec
aof-use-rdb-preamble yes

ตั้งแต่ Redis 7.0 AOF คือ ไดเรกทอรีของหลายไฟล์ (multi-part AOF) ไม่ใช่ไฟล์เดียว ผมเปิด AOF บน container ที่รันอยู่ เขียนหนึ่ง key และรัน rewrite:

Terminal window
$ docker exec redis-b2-persist ls -A /data
(nothing yet: no snapshot, no AOF)
$ docker exec redis-b2-persist redis-cli SET greeting hello
OK
$ docker exec redis-b2-persist redis-cli CONFIG SET appendonly yes
OK
$ 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.manifest
file appendonly.aof.1.base.rdb seq 1 type b
file 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.rdb
REDIS0015
$ docker exec redis-b2-persist redis-cli SET more 1
OK
$ docker exec redis-b2-persist cat /data/appendonlydir/appendonly.aof.1.incr.aof
*2
$6
SELECT
$1
0
*3
$3
SET
$4
more
$1
1
$ docker exec redis-b2-persist redis-cli BGREWRITEAOF
Background append only file rewriting started
$ docker exec redis-b2-persist ls /data/appendonlydir
appendonly.aof.2.base.rdb
appendonly.aof.2.incr.aof
appendonly.aof.manifest
$ docker exec redis-b2-persist cat /data/appendonlydir/appendonly.aof.manifest
file appendonly.aof.2.base.rdb seq 2 type b
file appendonly.aof.2.incr.aof seq 2 type i startoffset 1

อ่านรายการแบบนี้:

  • appendonly.aof.1.base.rdb คือ base: snapshot ของ dataset ณ ตอนที่เปิด AOF ขึ้นต้นด้วย byte REDIS0015 มันจึงเป็นไฟล์ RDB
  • appendonly.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:

Terminal window
$ docker exec redis-b2-persist redis-check-aof /data/appendonlydir/appendonly.aof.manifest (trimmed)
Start checking Multi Part AOF
Start to check BASE AOF (RDB format).
[offset 0] Checking RDB file /data/appendonlydir/appendonly.aof.4.base.rdb
[offset 143] Checksum OK
RDB preamble is OK, proceeding with AOF tail...
BASE AOF appendonly.aof.4.base.rdb is valid
Start to check INCR files.
INCR AOF appendonly.aof.5.incr.aof is valid
All AOF files and manifest are valid

(เลขไฟล์สูงขึ้นที่นี่เพราะตอนนั้น container รัน rewrite และ backup มากขึ้นแล้ว)

“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)

การคัดลอก 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 ตรวจและยกเลิก ทำงานได้ขณะที่การเขียนดำเนินต่อ:

Terminal window
127.0.0.1:6379> BACKUP STATUS
1) "state"
2) "idle"
3) "error"
4) ""
5) "start_time"
6) (integer) 0
7) "end_time"
8) (integer) 0
127.0.0.1:6379> BACKUP START
OK
127.0.0.1:6379> BACKUP LIST
1) "/data/backupdir/appendonly.aof.4.base.rdb"
127.0.0.1:6379> SET written-during-backup 1
OK
127.0.0.1:6379> BACKUP SEAL
OK
127.0.0.1:6379> BACKUP LIST
1) "/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 CLEANUP
OK
127.0.0.1:6379> BACKUP STATUS
1) "state"
2) "idle"
3) "error"
4) ""
5) "start_time"
6) (integer) 0
7) "end_time"
8) (integer) 0

SET ที่เขียนหลัง 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

AI เพิ่ม endpoint สำหรับแอดมิน “เพื่อ persist Redis เดี๋ยวนี้”:

🐛 หา bug ในโค้ดนี้

app.post('/admin/persist', async (_req, res) => {
await redis.save(); // write dump.rdb now
res.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 ที่รันได้ข้างบน

Persistence คือ คำสัญญาสองแบบที่ต่างกัน RDB สัญญาภาพที่สอดคล้องกัน ณ ช่วงเวลาในอดีต เก็บถูกและโหลดเร็ว AOF สัญญาบันทึกความเปลี่ยนแปลงที่ต่อเนื่อง และส่วนของบันทึกที่รอดจาก crash ขึ้นกับ appendfsync Redis สมัยใหม่เก็บ AOF เป็นภาพบวกบันทึกสั้นๆ ต่อท้าย จึงได้ทั้ง restart เร็วและช่วงที่เสียเล็กพร้อมกัน

ตัวเลือกข้อดีข้อเสีย
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; ยังต้องคัดลอกไฟล์ออกจากโฮสต์เอง
  • “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 ไม่เสียข้อมูลเลย” อาจเสียราวหนึ่งวินาที
  • 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)

คำสั่งไหนสั่ง snapshot RDB เบื้องหลังโดยไม่บล็อกเซิร์ฟเวอร์?
การตั้ง appendonly yes เปิดใช้อะไร?
Redis ใช้ชื่อไฟล์ใดเป็นค่าเริ่มต้นสำหรับ snapshot RDB?
ค่า appendfsync ไหน flush buffer ของ AOF ลงดิสก์วินาทีละครั้ง?
คุณเปิด AOF ด้วยค่าเริ่มต้นบน Redis 8.10.2 /data/appendonlydir มีอะไร?
aof-use-rdb-preamble yes เป็นค่าเริ่มต้นตั้งแต่ Redis เวอร์ชันไหน?
ลำดับที่ถูกสำหรับ backup ออนไลน์ด้วยกลุ่ม BACKUP ของ 8.10 คือ?