วิธีคัดลอกฐานข้อมูล SQLite ระหว่างคอมพิวเตอร์ให้เร็วขึ้น
(alexwlchan.net)- เมื่อต้องย้าย ฐานข้อมูล SQLite ขนาดใหญ่จากเซิร์ฟเวอร์ระยะไกลมายังเครื่อง local การคัดลอก SQL dump ที่บีบอัดด้วย gzip แทนไฟล์
.dbต้นฉบับจะช่วยลดปริมาณการส่งข้อมูลได้อย่างมาก - สาเหตุหลักที่ทำให้ไฟล์มีขนาดใหญ่ขึ้นคือ index ซึ่งเก็บข้อมูลตารางซ้ำเพื่อให้ query เร็วขึ้น และทำให้ใช้พื้นที่ดิสก์มากขึ้น
.dumpของ SQLite จะแปลงฐานข้อมูลเป็นข้อความคำสั่ง SQL และแทน index ด้วยบรรทัดCREATE INDEXเพียงบรรทัดเดียว ไม่ใช่ข้อมูลจริง จึงหลีกเลี่ยงการส่งข้อมูลซ้ำ- ฐานข้อมูลตัวอย่างมีไฟล์ต้นฉบับ 3.4GB, ข้อความ dump 1.3GB, และไฟล์บีบอัด gzip 240MB โดย dump ที่บีบอัดแล้วมีขนาดเล็กกว่าต้นฉบับ 14 เท่า
- หากทำไฟล์ dump ให้คงที่ไว้ก่อนคัดลอก จะช่วยลดกรณีที่ต้นฉบับเปลี่ยนระหว่างการส่งจนเกิดข้อผิดพลาด
database disk image is malformed
คัดลอก SQLite dump หลังบีบอัด
- สำหรับฐานข้อมูล SQLite ขนาดเล็ก การคัดลอกโดยตรงด้วย
rsyncก็เพียงพอ- ตัวอย่าง:
rsync --progress username@server:my_remote_database.db my_local_database.db
- ตัวอย่าง:
- เมื่อฐานข้อมูลมีขนาดใหญ่ขึ้น การคัดลอกโดยตรงอาจช้าและไม่เสถียร
- ฐานข้อมูลขนาด 250MB ใช้เวลาประมาณ 1 นาทีในการดาวน์โหลดจากเว็บเซิร์ฟเวอร์ผ่านอินเทอร์เน็ตบ้าน
- สำหรับฐานข้อมูลขนาดหลาย GB เวลาในการรอจะเพิ่มขึ้นอีก
- คอขวดของการเพิ่มขนาดอยู่ที่ index
- index ช่วยเพิ่มความเร็ว query ได้มาก แต่ก็ทำให้ไฟล์ฐานข้อมูลใหญ่ขึ้นด้วย
- ในฐานข้อมูลหนึ่ง index บางตัวกินพื้นที่ดิสก์ไปครึ่งหนึ่ง
- index ไม่ได้เก็บข้อมูลที่ไม่ซ้ำเป็นหลัก แต่ทำซ้ำข้อมูลจากตารางอื่นเพื่อให้ค้นหาได้เร็วขึ้น
- SQLite สามารถ dump ฐานข้อมูลทั้งหมดเป็นไฟล์ข้อความ ได้
- คำสั่ง:
sqlite3 my_database.db .dump > my_database.db.txt - ไฟล์ผลลัพธ์ประกอบด้วยคำสั่ง SQL เช่น
CREATE TABLE,INSERT INTO,CREATE INDEX - index ถูกบันทึกเป็นคำสั่งสร้าง เช่น
CREATE INDEX [idx_photo_locations] ON [photos] ([longitude], [latitude]);ไม่ใช่ข้อมูล index จริง
- คำสั่ง:
- ไฟล์ dump สามารถกู้คืนกลับเป็นฐานข้อมูล SQLite ได้
- คำสั่ง:
cat my_database.db.txt | sqlite3 my_reconstructed_database.db
- คำสั่ง:
- SQL dump มีข้อมูลซ้ำจำนวนมาก จึงเหมาะกับ การบีบอัดด้วย gzip อย่างมีประสิทธิภาพ
- ตัวอย่าง:
sqlite3 explorer.db .dump | gzip -c > explorer.db.txt.gz - เปรียบเทียบขนาด:
- ฐานข้อมูล SQLite ต้นฉบับ: 3.4GB
- dump แบบข้อความ: 1.3GB
- ข้อความที่บีบอัดด้วย gzip: 240MB
- ไฟล์ gzip มีขนาดเล็กกว่าฐานข้อมูล SQLite ต้นฉบับ 14 เท่า
- ตัวอย่าง:
ขั้นตอนจริงและผลด้านความเสถียร
- วิธีใหม่คือสร้าง dump ที่บีบอัดบนเซิร์ฟเวอร์ระยะไกล ดาวน์โหลดเฉพาะไฟล์นั้น แล้วค่อยประกอบฐานข้อมูลใหม่บนเครื่อง local
- ขั้นตอนการคัดลอกมีดังนี้
- สร้างไฟล์ข้อความที่บีบอัดด้วย gzip บนเซิร์ฟเวอร์:
ssh username@server "sqlite3 my_remote_database.db .dump | gzip -c > my_remote_database.db.txt.gz" - คัดลอกมายังเครื่อง local:
rsync --progress username@server:my_remote_database.db.txt.gz my_local_database.db.txt.gz - ลบไฟล์บีบอัดชั่วคราวบนเซิร์ฟเวอร์:
ssh username@server "rm my_remote_database.db.txt.gz" - แตกไฟล์บนเครื่อง local:
gunzip my_local_database.db.txt.gz - ประกอบฐานข้อมูล local ใหม่:
cat my_local_database.db.txt | sqlite3 my_local_database.db - ลบไฟล์ข้อความบนเครื่อง local:
rm my_local_database.db.txt
- สร้างไฟล์ข้อความที่บีบอัดด้วย gzip บนเซิร์ฟเวอร์:
- dump ที่บีบอัดทำหน้าที่เป็น ต้นทางการคัดลอกที่สอดคล้องกัน
- หากต้นฉบับถูกอัปเดตระหว่างการคัดลอกฐานข้อมูลขนาดใหญ่ที่ใช้เวลานาน ไฟล์ผลลัพธ์อาจมีส่วนต้นเป็นสถานะก่อนอัปเดต และส่วนท้ายเป็นสถานะหลังอัปเดต
- ไฟล์ลักษณะนี้อาจเกิดข้อผิดพลาด
database disk image is malformedเมื่อเปิดบนเครื่อง local
- เมื่อสร้าง text dump ก่อนคัดลอก ไฟล์ที่
rsyncส่งจะไม่เปลี่ยนกลางทาง- จึงมีโอกาสสูงขึ้นที่จะคัดลอกไฟล์ข้อความที่สมบูรณ์และสอดคล้องกัน
- หากต้องย้ายฐานข้อมูล SQLite ขนาดใหญ่บ่อย ๆ วิธีนี้ช่วยลดเวลาดาวน์โหลดและเพิ่มความน่าเชื่อถือในการคัดลอกได้
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
ถ้าคัดลอกไฟล์ฐานข้อมูลที่กำลังทำงานอยู่และกำลังรับการอัปเดตไปตรง ๆ ก็ย่อมมีโอกาส เสียหาย ได้อยู่แล้ว
ถ้าต้องการทำสำเนาฐานข้อมูล SQLite อย่างปลอดภัย มี Litestream: https://github.com/benbjohnson/litestream
มีบริการที่ชอบใช้อยู่ แต่ SFTP server ไหนก็ใช้ได้
[1] https://github.com/benbjohnson/litestream/issues/140
[2] https://www.rsync.net/resources/notes/2021-q3-rsync.net_technotes.html
แน่นอนว่าถ้ามีหลายไฟล์ ก็ต้องรวมไว้เป็น subvolume เพื่อให้ทั้งหมดถูกคัดลอกภายใน transaction เดียวกัน และแค่ใช้
cp --reflink=alwaysอย่างเดียวไม่พอการหยุดโปรเซสด้วย
SIGSTOPก็อาจให้ผลคล้ายกันได้ แต่ไม่อยากพึ่งวิธีนั้น.backupที่มากับ SQLite เองก็เป็นเครื่องมือทางการที่ตั้งใจให้สร้างเวอร์ชัน snapshot ของฐานข้อมูลที่กำลังรันอยู่เพื่อให้คัดลอกได้ถ้าจำไม่ผิด เขาเขียนไกด์ไว้ครบถ้วนมาก แต่โอกาสที่ผมจะหาเจอแทบเป็น 0 ถ้ามีใครรู้ reference นี้ ช่วยโพสต์ลิงก์ให้หน่อยก็ดี
บอกว่าเป็นการคัดลอกฐานข้อมูลระหว่างคอมพิวเตอร์ แต่รู้สึกเหมือนวาดแค่วงกลมแล้วละส่วนที่เหลือของนกฮูกไว้เลย
อย่างที่คนอื่นบอก incremental
rsyncน่าจะเร็วกว่ามาก แต่สิ่งที่น่าหงุดหงิดกว่าคือ ส่วนที่บอกว่าการส่ง SQL statement เร็วกว่าการส่งฐานข้อมูล กลับลืมไปสนิทว่า ต้อง execute SQL statement เหล่านั้นด้วย จากนั้นยังต้องรัน/optimize/และ/vacuum/อีกตอนนี้มีสถานการณ์ที่ต้อง “rebuild แบบ incremental” ฐานข้อมูลจากไฟล์ CSV อยู่ ซึ่งในกรณีของผม การสร้างใหม่ตั้งแต่ต้นเหมาะสมกว่า ถึงจะ optimize ไปมากแล้ว แต่แค่ batch insert ลงฐานข้อมูลใน memory เปล่า ๆ แล้วสร้าง index ก็ใช้เวลา 30 นาที
เป็นบทความที่สรุปวิธี insert ให้เร็วใน SQLite3 ได้ดีมาก
เขาบอกว่าดาวน์โหลดฐานข้อมูล 250MB ใช้เวลา 1 นาที แต่ผมมีไฟเบอร์ 1Gbps เลยดึงฐานข้อมูลทดสอบ SQLite ขนาด 2GB จากเซิร์ฟเวอร์บริษัทมาได้ใน 15 วินาที
ต้องใช้เทคนิคอย่าง Bloom filter กับ LRU cache แต่ตอนนี้สร้างฐานข้อมูล 6GB ได้ในราว 20 นาที
SQLite มีเครื่องมือทางการสำหรับงานนี้: https://www.sqlite.org/rsync.html
ทำงานใน ระดับ page
“โปรโตคอลนี้ให้ replica ส่ง hash แบบ cryptographic ของแต่ละ page ไปยังฝั่งต้นทาง แล้วต้นทางจะส่งเนื้อหาทั้งหมดของ page ที่ hash ไม่ตรงกันกลับมา”
ถ้าจะใช้เครื่องมือนี้ ก็มีแต่ต้องคอมไพล์เอง
ยูทิลิตี
sqlite_rsyncที่เพิ่ง release ล่าสุด ใช้เวอร์ชันของ อัลกอริทึม rsync ที่ optimize ให้เข้ากับโครงสร้างภายในของฐานข้อมูล SQLite โดยเปรียบเทียบ data page ภายในอย่างมีประสิทธิภาพ แล้ว sync เฉพาะ page ที่เปลี่ยนไปหรือยังไม่มีเทคนิคในบทความก็ดี แต่ตอนนี้ใช้ยูทิลิตีที่มีมาให้จะง่ายกว่า :)
เขียนรายละเอียดหลักการทำงานไว้ในบล็อกแล้ว: https://nochlin.com/blog/how-the-new-sqlite3_rsync-utility-works
sqlite3_rsyncถูกใส่ไว้ใน แพลตฟอร์ม rsync.net แล้วssh user@rsync.net sqlite3_rsync … blah blah …เพิ่งเพิ่มเข้าไปเมื่อสัปดาห์ที่แล้ว จึงยังไม่ได้ deploy ไปครบทุก region แต่ผู้ใช้ช่วงแรกทั้งหมดบอกว่าทำงานได้ตรงตามที่คาด
sqlite_rsyncใช้ได้เฉพาะใน WAL mode เท่านั้น ข้อจำกัดเพิ่มเติมของ WAL mode คือไฟล์ฐานข้อมูลต้องอยู่บนดิสก์ localส่วนใหญ่ก็คงเป็นแบบนั้นอยู่แล้ว แต่ถ้าทำไม่ได้ ก็ใช้ยูทิลิตีนี้ไม่ได้
น่าแปลกที่ไม่ได้ลองใช้การบีบอัดระหว่างถ่ายโอนที่
rsyncมีให้-z, --compressจะบีบอัดข้อมูลไฟล์ระหว่างถ่ายโอน และ--compress-level=NUMจะระบุระดับการบีบอัดอย่างชัดเจนบางทีการ gzip ก่อนแล้วค่อยส่งอาจเร็วกว่า แต่ข้อดีคือสามารถปรับปรุงการถ่ายโอนได้ด้วยแฟล็กเดียว
sqlite3_rsync(https://sqlite.org/rsync.html) ร่วมกับ-zน่าจะดีกว่าก็เหมือน rsync ที่เข้าใจ transaction ของ SQLite และ WAL แถมมีการบีบอัดระหว่างถ่ายโอนด้วย
เวลาทำงานแบบนี้จะสตรีม dump เข้า gzip โดยตรง ปกติยังหาวิธีสตรีมตรงไปยังปลายทางได้โดยไม่ต้องมีไฟล์กลางด้วย
นอกจากนี้ยังทำให้ปลายทางเก็บไว้ในสถานะที่ถูกบีบอัดแล้ว โดยเฉพาะถ้าเป้าหมายคือการสำรองข้อมูล ไม่ใช่การจำลองแบบสไตล์คนงบน้อย
rsyncกับไฟล์ที่บีบอัดแล้ว ก็เท่ากับอ้อมข้ามหัวใจสำคัญของการใช้rsyncอย่าง การถ่ายโอนส่วนต่างตาม rolling checksum ไปทั้งหมด--remove-source-filesได้ จะได้ไม่ต้อง SSH เข้าไปอีกครั้งเพื่อrmการเก็บเป็นไฟล์ข้อความไม่มีประสิทธิภาพ ผมเก็บฐานข้อมูล SQLite ด้วย
VACUUM INTOsqlite3 -readonly /path/db.sqlite "VACUUM INTO '/path/backup.sqlite';"ตาม https://sqlite.org/lang_vacuum.html คำสั่ง
VACUUMที่มี clauseINTOเป็นทางเลือกแทน backup API สำหรับสร้างสำเนาสำรองของฐานข้อมูลที่กำลังทำงานอยู่ ข้อดีคือฐานข้อมูลสำรองที่ได้มีขนาดเล็กที่สุด จึงอาจลด I/O ของระบบไฟล์ได้VACUUM INTOยังคงเก็บดัชนีไว้ไฟล์ข้อความอาจไม่มีประสิทธิภาพถ้าใช้ตามนั้น แต่บีบอัดได้ดีแม้ด้วยเครื่องมือพื้นฐานอย่าง gzip ส่วนรูปแบบไบนารีของ SQLite จะบีบอัดได้ดีพอ ๆ กันไหมยังไม่แน่ใจ แต่ก็เป็นไปได้
ใน DuckDB สามารถทำสิ่งเดียวกันแต่ส่งออกเป็น Parquet ได้ ซึ่งทำให้ขนาดข้อมูลเล็กกว่าคำสั่ง SQL แบบข้อความเป็นระดับหลักเดียว ส่งก็เร็วกว่า โหลดเข้าก็เร็วกว่า
https://duckdb.org/docs/stable/sql/statements/export.html
duckdb -c "attach 'sqlite-database.db' as db; copy db.table_name to 'table_name.parquet' (format parquet, compression zstd)"ในฐานข้อมูลทดสอบของผม ขนาดเล็กกว่าคำสั่ง SQL แบบข้อความที่ gzip แล้วประมาณ 20%
SQLite มี session extension ที่ติดตามการเปลี่ยนแปลงของชุดตาราง และสร้าง changeset/patchset ที่นำไปใช้กับฐานข้อมูล SQLite เวอร์ชันก่อนหน้าได้
https://www.sqlite.org/sessionintro.html
ในโปรเจกต์หนึ่งของผม ผมทำ session แบบคนงบน้อยเอง โดยบันทึกคำสั่ง SQL และพารามิเตอร์ทั้งหมดลงฐานข้อมูลแยก จากนั้นซิงก์และ replay มัน วิธีนี้ทำงานได้ดีพอสำหรับฐานข้อมูลราว 30GB ที่เปลี่ยนแค่ประมาณ 0.1% ต่อวัน
ถ้าซิงก์จากเวอร์ชันเก่าไปเวอร์ชันใหม่เป็นประจำ สามารถปรับให้เหมาะขึ้นได้ด้วย ตัวเลือก
--rsyncableของ gzipอัตราการบีบอัดจะลดลงประมาณ 1% แต่ความต่างจากเวอร์ชันหนึ่งไปยังเวอร์ชันถัดไปจะถูกจำกัดอยู่เฉพาะบริเวณ ไม่ลามต่อเนื่องไปทั่วเอาต์พุตที่บีบอัดทั้งหมด
อีกวิธีคือไม่ต้องบีบอัดเอาต์พุต dump แล้วให้
rsyncคำนวณส่วนต่างระหว่าง dump แบบไม่บีบอัดตัวเก่ากับตัวปัจจุบัน จากนั้นค่อยบีบอัดเฉพาะชุดการเปลี่ยนแปลงที่จะส่งผ่านเครือข่าย กล่าวคือใช้rsync -zน่าจะทำเป็น pipeline เดียวได้
ssh username@server "sqlite3 my_remote_database.db .dump | gzip -c" | gunzip -c | sqlite3 my_local_database.db-oCompression=onหรือ-Cในคำสั่ง SSH แล้วgzip/gunzipก็อาจซ้ำซ้อนpvใส่เข้าไปด้วย