วิธีที่ GitLab ลดเวลาแบ็กอัปรีโพจาก 48 ชั่วโมงเหลือ 41 นาที
(about.gitlab.com)- GitLab พบปัญหาว่าการแบ็กอัปรีโพขนาดใหญ่ใช้เวลานานมาก และได้ดำเนินการปรับปรุง
- สาเหตุหลักคือฟังก์ชัน Git ที่มีอายุ 15 ปีซึ่งมี ความซับซ้อนแบบ O(N²) และได้เพิ่มประสิทธิภาพอย่างมากผ่านการปรับอัลกอริทึม
- หลังการปรับปรุง เวลาแบ็กอัปของรีโพที่ใหญ่ที่สุดลดลงจาก 48 ชั่วโมงเหลือ 41 นาที
- วิธีที่ปรับปรุงแล้วให้ทั้งประสิทธิภาพการใช้ทรัพยากรและความน่าเชื่อถือ พร้อมส่งผลดีต่อ เครื่องมือที่ใช้ Git และคอมมูนิตี้ อื่น ๆ ด้วย
- ตั้งแต่ GitLab เวอร์ชัน 18.0 เป็นต้นไป ผู้ใช้ทุกคนจะได้รับประโยชน์เหล่านี้โดยไม่ต้องตั้งค่าเพิ่มเติม
ความสำคัญและความท้าทายของการแบ็กอัปรีโพ
- การแบ็กอัปรีโพเป็นองค์ประกอบหลักของ กลยุทธ์การกู้คืนยามฉุกเฉิน
- ยิ่งรีโพมีขนาดใหญ่ขึ้น ความซับซ้อนของ กระบวนการแบ็กอัปที่เชื่อถือได้ ก็ยิ่งเพิ่มขึ้น
- รีโพ Rails ภายในของ GitLab ใช้เวลาแบ็กอัปนานถึง 48 ชั่วโมง ทำให้ต้องเลือกระหว่าง ความถี่ในการแบ็กอัปกับประสิทธิภาพของระบบ
- มีปัญหาหลากหลายในรีโพขนาดใหญ่ เช่น เวลา ทรัพยากร ความเสี่ยงต่อความล้มเหลว และ race condition
- ส่งผลให้แต่ละองค์กรต้องใช้กลยุทธ์ที่ไม่สม่ำเสมอ เช่น ตั้งเวลาแบ็กอัปลำบาก พึ่งพาเครื่องมือภายนอก หรือทำแบ็กอัปน้อยลง
การวิเคราะห์คอขวดด้านประสิทธิภาพและการหาสาเหตุ
- ฟีเจอร์แบ็กอัปของ GitLab ใช้คำสั่ง
git bundle createเพื่อสร้างสแนปชอตของรีโพ - ในกระบวนการทำงานของคำสั่งนี้เกิดคอขวดด้านประสิทธิภาพเมื่อจำนวน reference เพิ่มขึ้น
- มีกรณีที่การแบ็กอัปรีโพขนาดใหญ่ซึ่งมี reference หลายล้านรายการใช้เวลามากกว่า 48 ชั่วโมง
การวิเคราะห์สาเหตุ
- ใช้การวิเคราะห์ Flame Graph ระหว่างรันคำสั่งเพื่อตรวจสอบจุดคอขวด
- เมื่อมี reference 10,000 รายการ เวลาทำงานประมาณ 80% ถูกใช้ไปกับฟังก์ชัน
object_array_remove_duplicates() - ฟังก์ชันนี้ถูกเพิ่มเข้ามาในปี 2009 ผ่านคอมมิตนี้ เพื่อจัดการ reference ที่ซ้ำกัน
- ป้องกันปัญหาที่อาจเกิดขึ้นเมื่อใช้
git bundle createแล้วมี reference ซ้ำอยู่ - แต่การตรวจหาค่าซ้ำถูกเขียนเป็น nested for loop จึงมีความซับซ้อนแบบ O(N²)
- ยิ่งจำนวน reference มาก คอขวด ก็ยิ่งรุนแรงขึ้น
- ป้องกันปัญหาที่อาจเกิดขึ้นเมื่อใช้
เปลี่ยนจาก O(N²) ไปสู่การแมปที่มีประสิทธิภาพ
- GitLab ได้ส่งแพตช์ให้ Git โดยเปลี่ยนจาก nested loop มาใช้ โครงสร้างข้อมูลแบบ map
- เพิ่มแต่ละ reference ลงใน map เพื่อกำจัดค่าซ้ำโดยอัตโนมัติและประมวลผลได้อย่างมีประสิทธิภาพ
- การเปลี่ยนแปลงนี้ทำให้ประสิทธิภาพของ
git bundle createดีขึ้นอย่างมาก และขยายขีดความสามารถได้แม้ในสภาพแวดล้อมที่มี reference จำนวนมาก - ผลเบนช์มาร์กยืนยันว่า ที่ระดับ reference 100,000 รายการ ประสิทธิภาพดีขึ้น มากกว่า 6 เท่า
เวลาแบ็กอัปที่ลดลงอย่างมากและผลลัพธ์ที่ได้
- เวลาแบ็กอัปสูงสุดของรีโพลดลงจาก 48 ชั่วโมงเหลือ 41 นาที (เหลือเพียง 1.4%)
- สามารถให้ ประสิทธิภาพที่สม่ำเสมอ ได้โดยไม่ขึ้นกับขนาดของรีโพ
- ได้ประโยชน์เพิ่มเติม เช่น ภาระบนเซิร์ฟเวอร์ลดลง และ ประสิทธิภาพที่ดีขึ้น ของคำสั่ง Git อื่น ๆ ที่อาศัยการแบ็กอัป
- แพตช์นี้ถูกรวมเข้า upstream Git แล้ว และ GitLab ก็ใช้งานทันทีเพื่อให้ลูกค้าได้รับประสบการณ์ที่เร็วขึ้น
การเปลี่ยนแปลงที่เกิดขึ้นจริงสำหรับลูกค้า GitLab
- ลูกค้าองค์กรขนาดใหญ่สามารถรัน การแบ็กอัปข้ามคืนต่อเนื่อง ได้โดยไม่ชนกับเวิร์กโฟลว์การพัฒนา
- ด้วย Recovery Point Objective (RPO) ที่สั้นลง ความเสี่ยงของการสูญเสียข้อมูลในกรณีเกิดภัยพิบัติจึงลดลงอย่างมาก
- โอเวอร์เฮดด้านปฏิบัติการ เช่น การใช้ทรัพยากร เวลาบำรุงรักษา และ ค่าใช้จ่ายบนคลาวด์ ก็ลดลงด้วย
- แม้ขนาดรีโพจะเพิ่มขึ้น ก็ไม่ต้องกังวลอีกต่อไประหว่าง ความถี่ในการแบ็กอัปกับประสิทธิภาพของระบบ
- ตอนนี้ผู้ใช้ GitLab ทุกคนสามารถรับประโยชน์เหล่านี้ได้ โดยไม่ต้องเปลี่ยนคอนฟิก
ขั้นตอนถัดไปและความหมายของเรื่องนี้
- การปรับปรุงครั้งนี้เป็นส่วนหนึ่งของความพยายามต่อเนื่องของ GitLab ในการสร้าง โครงสร้างพื้นฐาน Git ระดับองค์กรที่ขยายขนาดได้ดี
- การเปลี่ยนแปลงที่ GitLab มีส่วนร่วมถูกนำกลับไปยังโปรเจกต์ upstream Git ด้วย ทำให้เกิดผลกระทบเชิงบวกต่อ ทั้งอุตสาหกรรมและคอมมูนิตี้โอเพนซอร์ส
- ความพยายามในการปรับปรุงโครงสร้างพื้นฐานลักษณะนี้กำลังกลายเป็นต้นแบบให้กับงาน ปรับปรุงประสิทธิภาพหลัก อื่น ๆ
เรื่องราวเชิงลึกเพิ่มเติมเกี่ยวกับแนวทางด้านประสิทธิภาพของ GitLab สามารถดูได้ในงาน GitLab 18 virtual launch event
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
มีการแชร์ข้อมูลว่าโค้ดปรับปรุงประสิทธิภาพที่ GitLab มีส่วนร่วมกับ Git มีกำหนดปล่อยใน v2.50.0 ลิงก์คอมมิตที่เกี่ยวข้อง
จากประสบการณ์ตรง ย้ำว่าการตัดการคำนวณแบบ n^2 ออกจากโค้ดที่เขียนเองเป็นทางเลือกที่ถูกต้องเสมอ และรู้สึกแปลกใจที่แม้ไม่ต้องเขียนอัลกอริทึมพิเศษ ปัญหาก็มักเผยให้เห็นได้ง่ายแม้ที่ค่า n เล็กมาก
อ้างถึงกฎข้อแรกของการคอมพิวต์ของ Bruce Dawson: O(n^2) คือจุดที่ก่อปัญหาด้านการขยายระบบที่เลวร้ายที่สุด มันเร็วพอในโปรดักชัน แต่พอปล่อยใช้งานจริงกลับเกิดการเสื่อมประสิทธิภาพอย่างร้ายแรง ลิงก์บทความที่เกี่ยวข้อง
แชร์ประสบการณ์ส่วนตัวว่าเคยเห็นหลายครั้งที่ time complexity แบบ O(N^2) ดูเร็วในช่วงทดสอบ แต่พอขึ้นโปรดักชันแล้วเกิดปัญหารุนแรง
จากประสบการณ์ของตน ปัญหาส่วนใหญ่ (80~90%) ถ้าต้องใช้อัลกอริทึมซับซ้อน มักเป็นสัญญาณว่า data model เองไม่ถูกต้อง คิดว่ามีเพียงบางกรณีพิเศษ เช่น คอมไพเลอร์ ภายใน DB หรือการวางแผนเส้นทาง ที่จำเป็นต้องใช้อัลกอริทึมซับซ้อนโดยเนื้อแท้
กล่าวถึงข้อยกเว้นเฉพาะกรณีฮาร์ดแวร์ขนาดเล็กที่ n น้อยกว่า 10 เท่านั้น (เช่น CAN interface) แต่ถ้า n มีโอกาสเพิ่มขึ้นได้โดยไม่เปลี่ยนฮาร์ดแวร์ ก็ต้องหลีกเลี่ยงการคำนวณแบบ n^2 อย่างเด็ดขาด และควรจำกัดไว้ที่ระดับการออกแบบหรือมีการตรวจจับล่วงหน้าเพื่อรับรู้ว่าจำเป็นต้องออกแบบใหม่
เจ้าตัวระบายความอัดอั้นว่ากำลังเจอคอขวดจากการคำนวณแบบ n^3 ที่ทำให้ติดขัดแม้มีเพียง 10,000 องค์ประกอบ และยังหาวิธีแก้ไม่ได้
มองว่าเป็นการค้นพบที่น่าสนใจ พร้อมแชร์ความเสียดายว่าต่อให้เนื้อหาสั้นลงเหลือเพียง 1/10 ก็น่าจะสื่อสารได้มีประสิทธิภาพพออยู่ดี อย่างไรก็ดี ก็ยังมีข้อดีตรงที่ไม่ใช่คอนเทนต์วิดีโอจึงกวาดอ่านได้ง่าย
แนะนำสำหรับคนที่ยังไม่ได้อ่านบทความว่า ถ้าอยากจับประเด็นสำคัญให้คุ้มที่สุด ให้อ่านถึงช่วงหลัง flame graph แล้วหยุดก่อนถึงตอนพูดเรื่อง backport
รู้สึกว่าสไตล์ของบทความทั้งชิ้นยาวมากจนเหมือนข้อความที่ LLM (โมเดลภาษาขนาดใหญ่) สร้างขึ้น และนั่นเป็นสิ่งที่ติดอยู่ในความทรงจำ
คิดว่าต่อให้บทความยาวกว่านี้ก็ยังไม่เป็นไร และยังสงสัยอยู่ดีว่าทำไมถึงสร้าง backup bundle สองชุดจาก ref
รู้สึกว่าการใช้เวลาถึง 48 ชั่วโมงในการบีบอัดโฟลเดอร์ git ขนาดหลาย GB นั้นนานมาก และแม้แต่ 41 นาทีก็ยังถือว่านานอยู่ สงสัยว่าทำไมไม่ snapshot/archive ทั้ง repo ไปเลย แต่ต้องใช้ git bundle โดยเฉพาะ และอยากรู้ว่า
git bundleมีข้อดีอะไรเหนือกว่าแบ็กอัป ZFS ที่ทำเป็นประจำใน FAQ ทางการของ Git ระบุว่าวิธีแบบนี้มีความเสี่ยง เพราะเป็นการข้ามขั้นตอนตรวจสอบความถูกต้องตามปกติของ Git จึงแนะนำให้ใช้ git fsck เพื่อตรวจสอบความสมบูรณ์ของคอลเลกชัน ในระดับบุคคล แค่ Syncthing กับ Btrfs snapshot ก็เร็วและเสถียรพอแล้ว เอกสารที่เกี่ยวข้อง
มีการพูดถึงว่าการนำ ZFS snapshot ไปทำ offsite backup บนระบบที่ไม่ใช่ฐาน ZFS อย่าง S3 มีข้อจำกัด และหนึ่งในความสามารถที่ไม่ค่อยมีคนรู้ของ git bundle คือสามารถระบุตำแหน่งให้
git clone --bundle-uriได้ หากเซิร์ฟเวอร์บอกตำแหน่ง bundle ให้ไคลเอนต์ ไคลเอนต์ก็จะดึง bundle มาแตกอย่างรวดเร็ว แล้วค่อยรับ delta update จากเซิร์ฟเวอร์เท่านั้น ทำให้ภาระในการโคลน repo ขนาดใหญ่ลดลงมากมองว่าสุดท้ายแล้วก็คือการเพิ่มแคชในจุดที่ต้องมีแคช โดยปกติ repo ของ git มีลักษณะเป็นระบบกระจายอยู่แล้ว จึงสงสัยว่าแค่ mirror ไปยัง repository อื่นและจัดการด้วยเครื่องมือ snapshot/backup ของ filesystem ก็น่าจะพอไม่ใช่หรือ ประเด็นสำคัญคือข้อมูล version control ที่สำคัญควรถูกกระจายอยู่แล้ว
แม้จะไม่มีประสบการณ์กับการแบ็กอัป git มากนัก แต่ก็สงสัยว่าทำไมการสร้างแบ็กอัปจาก local repo โดยตรงถึงทำให้เกิด race condition ได้
ถ้าปัญหายุ่งยากถึงระดับนี้เพราะ data protocol ชั้นบน ก็สงสัยว่าทำไมไม่ใช้ block-level snapshot ไปเลย แม้ Git จะไม่มี WAL (Write Ahead Logging) เหมือนเป็นอุปสรรค แต่กับ SQLite แค่เพิ่ม WAL mode ก็สามารถใช้กลยุทธ์ block snapshot ได้อย่างสะดวกในสภาพแวดล้อมจริง และคิดว่าถ้า Git มีสถาปัตยกรรมแบบนี้ก็น่าจะวางกลยุทธ์แบ็กอัปที่เสถียรกว่านี้ได้มาก
เห็นด้วยว่าปัญหาส่วนหนึ่งมาจาก Git ไม่มีเมกานิซึมคล้าย WAL และแชร์ประสบการณ์ว่าเคยเชื่อ snapshot อย่างเดียวจนตอนกู้คืนพบว่า repository เสียหาย ซึ่งเป็นปัญหาร้ายแรง แม้จะกู้ได้แต่ยุ่งยากมาก
แชร์ทิปว่าใน SQLite ช่วงหลังมีทางเลือกที่ดีกว่าแล้วคือโซลูชัน sqlite3_rsync
อธิบายมุมของ GitLab ว่าไม่ใช่แค่บริการ managed service อย่างเดียว แต่เป็นผลิตภัณฑ์ที่ผู้ใช้สามารถติดตั้งเองและนำไปใช้ในสภาพแวดล้อมได้หลากหลาย ดังนั้น filesystem, การรองรับ snapshot และเงื่อนไขของระบบปฏิบัติการของผู้ใช้แต่ละรายจึงต่างกัน กล่าวคือ GitLab ต้องการระบบแบ็กอัปแบบอิสระที่ทำงานได้ทั่วไปในทุกสภาพแวดล้อม
เห็นข้อความว่า "ลดเวลาแบ็กอัปแบบ exponential ด้วยการเปลี่ยนอัลกอริทึม" แล้วก็สงสัยว่าหมายถึงลดจาก O(n^2) ไปเป็น O(n^2/2^n) หรือไม่ ซึ่งเดาว่าคงไม่ใช่แบบนั้นจริง
อธิบายว่าความซับซ้อนของอัลกอริทึมในฟังก์ชันที่แก้จริง ๆ เร็วขึ้น 6 เท่า และในบริบทการใช้งานอื่น ๆ ทำให้เวลาทำงานรวมลดเหลือ 1% ซึ่งในกรณีนี้การเรียกว่า "พัฒนาขึ้นแบบ exponential" ในเชิงการตลาดก็พอเข้าใจได้ ส่วนการนิยามความซับซ้อนจริง ๆ อาจไม่ได้มีความหมายมากนัก
ชี้ชัดว่าในการสนทนาทั่วไป คำว่า "exponential" ไม่ได้ถูกใช้ในความหมายทางคณิตศาสตร์แบบเป๊ะ ๆ แต่เป็นการเปรียบเปรยว่า "ดีขึ้นมาก"
ตามความเข้าใจของตน คิดว่าอาจหมายถึงลดจาก n ลงไปถึง log(n) คล้ายกับที่มักอธิบายว่า quantum Fourier transform เร็วกว่า DFT แบบเดิมอย่าง exponential โดยยกสถานการณ์ที่ความซับซ้อนเปลี่ยนจาก n^2 เป็น nlogn
ถ้าแทนอัลกอริทึม n^2 ด้วยการ lookup แบบ log(n) ก็ถือว่าเร็วขึ้นแบบ exponential จริง แต่ในทางปฏิบัติมักไปได้ถึง O(1) อย่างการ lookup ใน hash map จึงเร็วกว่าเสียอีก
คิดว่าการถกเถียงเรื่องนี้เองเป็นการจับผิดที่ไม่ก่อประโยชน์
มีความเห็นว่านี่เป็นตัวอย่างที่ดีว่าการเขียนด้วย C ไม่ได้แปลว่าจะได้ประสิทธิภาพสูงเสมอไป และโครงสร้างข้อมูลกับอัลกอริทึมสำคัญกว่า
กล่าวถึงบทเรียนว่าต้องหาสมดุลระหว่างการ optimize ก่อนเวลาอันควรกับการ optimize แบบคาดการณ์ล่วงหน้า โดยทั่วไปมักเตือนให้ระวังการ optimize เร็วเกินไป แต่ก็เสนอเป็นกฎว่า ถ้าเป็นฟังก์ชันที่ถูกเรียกบ่อยมากและมีการปรับปรุงที่ชัดเจน ทำได้ง่าย ก็ควรใส่ไว้ล่วงหน้า
ส่งต่อลิงก์คอมมิตที่เกี่ยวข้องโดยตรง (รายละเอียดการปรับปรุงอัลกอริทึม) ลิงก์คอมมิตที่เกี่ยวข้อง