- การนำ Monorepo มาใช้มีข้อดี เช่น ความสอดคล้องภายในองค์กร การแชร์โค้ด และการเสริมความแข็งแกร่งให้สภาพแวดล้อมเครื่องมือที่ใช้ร่วมกัน แต่หากทำตามกรณีศึกษาของบริษัทบิ๊กเทคแบบตรงตัว ก็จะต้องเผชิญกับปัญหาและความท้าทายใหม่
- เพื่อให้ Monorepo ประสบความสำเร็จ ต้องยึดหลักให้ทุกงานสำคัญเป็น O(change) ไม่ใช่ O(repo) และต้องมีเครื่องมือกับกลยุทธ์ที่สอดคล้องกันในแต่ละขั้นของ build, test และ CI/CD
- Source control ควรเริ่มจาก Git และเมื่อขนาดใหญ่ขึ้นค่อยพิจารณาการขยายแบบค่อยเป็นค่อยไป เช่น sparse checkout หรือ virtual filesystem
- Build system ควรรักษาให้เป็นภาษาเดียวเท่าที่เป็นไปได้ ใช้ build tool มาตรฐานของแต่ละภาษาให้นานที่สุด และค่อยเปลี่ยนไปใช้ Bazel/Buck2 แบบค่อยเป็นค่อยไปเมื่อจำเป็นจริง ๆ
- Test·CI/CD ต้องตรวจจับขอบเขตผลกระทบของการเปลี่ยนแปลงให้เร็ว เพื่อ build, test และ deploy เฉพาะส่วนที่เกี่ยวข้อง และใน Monorepo ขนาดใหญ่ก็จำเป็นต้องมีกลยุทธ์ด้านความน่าเชื่อถือ เช่น การ retry อัตโนมัติและการแยก flaky test
บทนำ: จุดเริ่มต้นของการเดินทางสู่ Monorepo
- หากคุณเป็นวิศวกรในทีม Developer Productivity ทีมใหม่ ความกังวลเรื่องการเตรียมตัวและความพยายามที่ต้องใช้หลังตัดสินใจนำ Monorepo มาใช้ย่อมเพิ่มขึ้น
- แนวปฏิบัติที่ดีของบริษัทใหญ่ เช่น Google, Meta, Uber อาจดูน่าประทับใจ แต่ในความเป็นจริง การได้ผลลัพธ์ในระดับเดียวกับพวกเขานั้นเป็นไปไม่ได้
- แต่ละองค์กรควรตัดสินใจนำ Monorepo มาใช้ตามเหตุผลและความจำเป็นของตัวเอง และในกระบวนการนั้นสามารถมุ่งหวังประโยชน์ด้าน ความสอดคล้อง (consistency), การบูรณาการระดับองค์กร และ เครื่องมือที่ใช้ร่วมกัน
ทำให้ความจำเป็นของ Monorepo ชัดเจน
- กรณีศึกษาของบริษัทใหญ่ เป็นเพียงภาพของสิ่งที่ไปถึงในท้ายที่สุด จึงไม่เหมาะจะใช้เป็นหลักอ้างอิงสำหรับช่วงเริ่มต้น
- ในทางปฏิบัติจะเกิดปัญหาใหม่ขึ้นจริง และเป็นปัญหาคนละแบบกับที่พบในระบบจัดการหลาย repo เดิม
- เป้าหมายของการนำ Monorepo มาใช้คือการรักษาความสอดคล้อง การรวมเครื่องมือให้ครอบคลุมทั้งองค์กร และการบังคับใช้มาตรฐานกับ convention ทางวิศวกรรม
- แต่ละทีมต้องกำหนดเป้าหมายให้ชัดเจนตามวัฒนธรรมและทิศทางของตนเอง จึงจะได้ผลลัพธ์ที่มีประสิทธิภาพ
กฎทอง: หลักการของ O(change)
- เครื่องมือที่เกี่ยวข้องกับ repository ทั้งหมดควรมีความซับซ้อนแบบ O(change) ไม่ใช่ O(repo) เพื่อให้ทำงานได้รวดเร็ว
- ยิ่ง Monorepo ขนาดใหญ่ขึ้น ความไม่มีประสิทธิภาพของเครื่องมือเดิมยิ่งเด่นชัด ดังนั้นการออกแบบเชิงโครงสร้างเพื่อเอาชนะปัญหาด้านประสิทธิภาพจึงเป็นสิ่งจำเป็น
- นวัตกรรมที่ถูกกล่าวถึงในบล็อกเทคของบริษัทใหญ่ ส่วนมากก็เน้นไปที่การแก้ความไม่มีประสิทธิภาพที่เกิดจาก O(repo)
Source control
- องค์กรซอฟต์แวร์ส่วนใหญ่ใช้ Git เป็นพื้นฐาน แต่ Git มีข้อจำกัดด้านประสิทธิภาพเมื่อขยายไปสู่สภาพแวดล้อม Monorepo แบบรวมศูนย์ในระดับใหญ่
- ในความเป็นจริง องค์กรส่วนใหญ่สามารถใช้ git+GitHub ต่อไปได้อีกนานพอสมควร
- เมื่อองค์กรเติบโตเร็วขึ้น ก็จะเริ่มต้องมีโครงสร้างอย่าง sparse checkout (clone เฉพาะบางส่วน) และ virtual filesystem (ดาวน์โหลดไฟล์จากเซิร์ฟเวอร์แบบไดนามิกเมื่อจำเป็น)
- บริษัทใหญ่จึง fork Git หรือพัฒนาระบบแยกขึ้นมาให้เหมาะกับเรื่องนี้ (Microsoft: Git fork ภายใน, Meta: Mercurial fork, Google: Piper)
- Jujutsu และ source control ยุคถัดไปอื่น ๆ ก็เป็นตัวเลือกที่น่าพิจารณา
- เมื่อขนาดยังเล็กก็ยังใช้ Git ได้อย่างไม่มีปัญหา แต่ระหว่างการเติบโตควรคำนึงถึงกลยุทธ์การขยายระบบไว้ล่วงหน้า
- ยังมีปัญหาในโลกจริงอีกอย่างคือ หากมีโค้ดที่สร้างจาก IDL (Interface Definition Language) อยู่ในซอร์สโค้ด ขนาดของ repository อาจเพิ่มขึ้นแบบทวีคูณ
Build system
- Bazel, Buck2 เป็นต้น คือตัวอย่าง build tool สำหรับ Monorepo ที่รองรับหลายภาษาและ build graph ที่ซับซ้อน
- แม้จะทรงพลัง แต่ก็มาพร้อมความซับซ้อนและภาระในการดูแลระบบสูง
- หากรักษา build ให้เป็นภาษาเดียวได้ ชีวิตจะง่ายขึ้นมาก และ build system ของแต่ละภาษาเอง (เช่น Maven, Gradle, Cargo, Go) ก็มีความสามารถในการขยายสูงเช่นกัน
- บทบาทหลักของ build system คือ “build target ที่ระบุอย่างมีประสิทธิภาพ (สร้าง artifact อย่างมีประสิทธิภาพ)” และ “คำนวณ target ที่ได้รับผลกระทบจากไฟล์ที่เปลี่ยนแปลงได้อย่างรวดเร็ว”
- เพื่อสิ่งนี้จึงต้องมีแนวคิดของ target determinator (เครื่องมือระบุ target) และใน ecosystem ของ Rust, Go, Bazel เป็นต้น ก็มีคำตอบหลากหลายอยู่แล้ว
- Remote execution และ caching จะจำเป็นจริง ๆ ก็ต่อเมื่อขนาดใหญ่ระดับมหาศาลเท่านั้น ส่วนในบริษัททั่วไป target determination ใช้งานได้จริงและเป็นประโยชน์มากกว่า
Test
- การรัน test ทั้งหมดทุกครั้งนั้นไม่มีประสิทธิภาพ จึงจำเป็นต้องมีระบบที่ ทดสอบเฉพาะขอบเขตที่ได้รับผลกระทบจากการเปลี่ยนแปลง
- Flaky test อาจกลายเป็นปัญหาที่ร้ายแรงยิ่งขึ้นในระบบทดสอบขนาดใหญ่
- ระบบทดสอบต้องรองรับการ retry อัตโนมัติ การตัดสินขอบเขตผลกระทบของ test แบบอัตโนมัติ และการแยก flaky test
- บางภาษา (เช่น Rust กับ nextest, Java กับ JUnit) มีฟีเจอร์ขั้นสูงเหล่านี้มาให้เป็นพื้นฐานหรือผ่านส่วนขยาย
- ระบบ test ของ Monorepo จะมีประสิทธิภาพก็ต่อเมื่อผสานเข้ากับ build system อย่างใกล้ชิด
การรวมต่อเนื่อง (CI)
- ระบบ CI ต้องทำ build artifact และ การตรวจสอบความถูกต้อง ที่จำเป็นโดยอัตโนมัติตามการเปลี่ยนแปลง
- ประสิทธิภาพและความมีประสิทธิผลของ target determinator เป็นองค์ประกอบหลักของ pipeline CI
- CI ยุคใหม่ใช้กลยุทธ์หลากหลาย เช่น “Merge Queue” เพื่อหาสมดุลระหว่างการรักษาคุณภาพโค้ดกับการเพิ่มความเร็วในการ merge
- เช่น จะรันการตรวจสอบทั้งหมดในทุก commit/PR หรือเลือกเฉพาะบางส่วน หรือจัดการหลาย PR เป็น batch
- ต้องกำหนดและออกแบบ trade-off ระหว่าง Throughput (ปริมาณงาน), Correctness (ความถูกต้อง), Tail latency (เวลารอสูงสุด) ให้เหมาะกับองค์กรของตนเอง
- การจัดการการ merge และการเพิ่มประสิทธิภาพ CI ใน Monorepo ขนาดใหญ่ยังคงเป็นโจทย์ท้าทายที่ไม่มีคำตอบสมบูรณ์
- Rust (bors), Chromium, Uber ต่างก็เลือกกลยุทธ์ด้าน merge/validation ที่แตกต่างกัน
การส่งมอบต่อเนื่อง (CD)
- ภาพฝัน ที่ว่าการเปลี่ยนแปลงทั้งหมดใน Monorepo จะ deploy ได้แบบอะตอมมิกนั้นไม่สอดคล้องกับความจริง
- แม้จะสามารถเปลี่ยน interface, implementation และ client ของหลายบริการพร้อมกันได้ใน PR เดียว แต่การ deploy จริงท้ายที่สุดก็เกิดขึ้นแบบอะซิงโครนัส จึงอาจเกิดปัญหาในจังหวะ deploy ได้
- การเปลี่ยนแปลงที่ทำให้ contract ระหว่างบริการเสียหายอาจก่อให้เกิดเหตุขัดข้องรุนแรงระหว่างการ deploy
- กลยุทธ์ CD สำหรับ Monorepo ที่มีประสิทธิภาพจึงต้องมีรอบการทำงานของระบบ deploy การตรวจสอบ service contract และความสามารถในการตรวจจับและตอบสนองอย่างรวดเร็วเมื่อเกิดปัญหา
บทสรุป
- Monorepo เป็นเครื่องมือทรงพลังสำหรับเสริมความสอดคล้องระดับองค์กรและวัฒนธรรมทางวิศวกรรม แต่ต้องอาศัยการลงทุนด้านวิศวกรรมและ tooling อย่างต่อเนื่อง
- หัวใจสำคัญคือการสร้างระบบอัตโนมัติ เครื่องมือ และวัฒนธรรมในแต่ละขั้นให้สอดคล้องกับหลัก O(change)
- เมื่อองค์กรเติบโต เครื่องมือก็ต้องพัฒนาต่อเนื่องเช่นกัน และการดูแลอย่างเป็นระบบที่สะท้อนเป้าหมายกับวัฒนธรรมขององค์กรเป็นสิ่งสำคัญ
- หากมีความตั้งใจจริง ความทุ่มเท และการลงทุนอย่างต่อเนื่องมากพอ Monorepo ก็จะสร้างคุณค่าได้คุ้มกับที่ลงแรงไป
4 ความคิดเห็น
เป็นบทความที่ให้สาระและเป็นประโยชน์จริง ๆ ไม่ใช่แค่มีเครื่องมือทรงพลังเท่านั้น แต่ยังต้องเตรียมใจที่จะสร้างเครื่องมือที่จำเป็นขึ้นมาเองเมื่อจำเป็นด้วย ดังนั้นถ้าทำให้มันเดินหน้าได้ดี ก็จะได้รับประโยชน์มากมายเช่นกัน
สมัยเรียนปริญญาโท เคยมีครั้งหนึ่งที่อาจารย์ที่ปรึกษาไปทานข้าวกับวิศวกรที่มาจาก Google แล้วเหมือนได้ยินเรื่อง monorepo กลับมา เลยเสนอว่าเราควรเริ่มจัดการทุกอย่างด้วย monorepo ต่อจากนี้ แต่ตอนนั้นผมต้องพยายามห้ามกันอยู่พักใหญ่... แม้ monorepo จะมีข้อดีหลายอย่าง แต่ด้วยลักษณะงานของห้องแล็บเรา ที่ต้องแชร์ผลงานให้คนนอกอยู่บ่อย ๆ ถ้าจัดการผลงานทั้งหมดด้วย monorepo ก็น่าจะลำบากมากเป็นพิเศษในจุดนี้ ถ้าเป็น multirepo ก็แค่ปรับขอบเขตการเปิดเผยของแต่ละผลงานแยกกันได้ง่าย ๆ ครับ
กรณีที่ต้องทนทุกข์กับการทำโมโนรีโป ส่วนใหญ่ดูเหมือนจะเป็นเพราะแตกโปรเจ็กต์ย่อยไว้ละเอียดเกินไปตั้งแต่แรก เดิมทีโปรเจ็กต์ที่ควรมีแค่หนึ่งหรือสองโปรเจ็กต์กลับถูกแยกออกเป็นราวสิบกว่าโปรเจ็กต์ แล้วพอจะรวมมาจัดการด้วยโมโนรีโป ก็ต้องใช้ทั้งเครื่องมือจัดการโมโนรีโปและทำให้ความซับซ้อนเพิ่มขึ้นไปอีก ทางที่ดีกว่าคือรวมตัวโปรเจ็กต์ให้เหลือแค่หนึ่งหรือสองโปรเจ็กต์ไปเลย และถึงจะมีมากกว่าสองโปรเจ็กต์ ก็ควรคิดแบบง่าย ๆ ว่าไม่ต้องใช้เครื่องมือจัดการแยกต่างหาก แค่แบ่งเป็นไดเรกทอรีแล้วเก็บไว้ในรีโพเดียว ก็จะจัดการได้สบายใจกว่าเยอะ
ความคิดเห็นบน Hacker News
มีการขอให้แชร์ประสบการณ์ว่าเธรดนี้ทำให้นึกถึงเรื่อง complexity merchants ในอดีต ผู้แสดงความเห็นบอกว่าไม่เห็นด้วยเลยกับแนวคิดที่ว่าการย้ายไปใช้ monorepo ต้องแลกด้วยการเสียสละทางเทคนิค หากเข้าใจพลังของระบบไฟล์แบบลำดับชั้นก็จะเห็นคุณค่าของ monorepo ได้ชัดเจน การทำ CI/CD ด้วย monorepo เดียวก็ชัดเจนกว่าการมีคอนฟิกกระจัดกระจายหลายแห่งมาก แก่นสำคัญของ monorepo คือทั้งองค์กรสามารถทำ atomic commit ได้ และเมื่อประสานงานนักพัฒนาจำนวนมาก ประโยชน์ของมันก็ท่วมท้น แค่ rebase ครั้งเดียวและประชุมใหญ่ครั้งเดียวก็พอ แม้สมาชิกทีมจะไม่ชอบกันและไม่ค่อยร่วมมือกัน ในแง่การจัดการแล้ว monorepo ก็ยังเป็นเครื่องมือด้าน HR ที่ทรงพลัง
นักพัฒนายุคนี้มีแนวโน้มจะแยกทุกอย่างมากเกินไป ใช้ microservices, repository เล็ก ๆ จำนวนมาก และพยายามหลีกเลี่ยง monolith แบบสุดโต่ง สิ่งนี้ทำให้ความซับซ้อนเพิ่มขึ้น และเปลี่ยนปัญหาเชิงโครงสร้างองค์กรให้กลายเป็นปัญหาเทคนิคในอนาคต อีกทั้งยังไม่ค่อยตระหนักถึงความพึ่งพากันภายในระบบซอฟต์แวร์ด้วย ผู้แสดงความเห็นบอกว่าไม่น่าเชื่อเลยว่าเคยเสียเวลากับการอัปเดตไฟล์ schema ของ Protocol Buffers มากแค่ไหนในที่ทำงานเก่า โชคดีที่บริษัทปัจจุบันไม่เป็นแบบนั้น
การติดตาม commit ข้ามหลายโปรเจ็กต์เป็นเรื่องที่มีก็ดี แต่ในทางปฏิบัติไม่ได้ต่างมากนักในแง่การติดตาม dependency หรือการ trigger downstream test ระบบอัตโนมัติสำหรับ multi-repo ก็ทำสิ่งเหล่านี้ได้เช่นกัน monorepo ช่วยได้ แต่ไม่ใช่คำตอบสมบูรณ์แบบและมีต้นทุนสูง การ deploy หรือ build ก็ไม่ได้เป็น atomic จริง ๆ เมื่อ monorepo โตมากขึ้นก็ต้องเลิกพึ่ง git และใช้เครื่องมือใหม่ ซึ่งเป็นงานใหญ่มาก เรื่องนี้ไม่ใช่สิ่งที่คนไม่มีประสบการณ์จะพูดกันได้ง่าย ๆ
ข้อดีของ monorepo มีอยู่ชัดเจน แต่ต้นทุนการดูแลแพงกว่า polyrepo ไม่ใช่ว่า monorepo จะดีที่สุดเสมอในทุกสถานการณ์ ดูรายละเอียดเพิ่มเติมได้ในบทความนี้ ความคุ้มค่าขึ้นอยู่กับบริบท
ในการออกแบบสภาพแวดล้อมการเขียนโปรแกรม มีกฎเชิงประสบการณ์ที่มีประโยชน์ว่า ยิ่งให้พลังกับทีมมาก ปัญหาก็มากขึ้น ในเชิงเทคนิค atomic commit ไม่ได้เป็นพลังที่มากกว่า กลับเป็นพลังที่น้อยกว่า แต่เพราะมันทำให้สามารถทำงานกับ interface ที่ออกแบบไม่ดีได้ จึงกลายเป็นพลังที่ก่อปัญหาแทน
มีความเห็นว่าความเชื่อที่ว่าเมื่อเปลี่ยนไปใช้ monorepo แล้วการเปลี่ยนแปลงจะ atomic มากขึ้นนั้นเป็นกับดัก [อ้างอิงต้นฉบับ: ภาพลวงตาใหญ่ที่สุดของ monorepo คือการที่ดูเหมือนว่าจะทำ atomic commit ได้ทั้ง codebase แต่ในความเป็นจริงมี deployment artifact หลายชนิด แม้จะเปลี่ยน service และ client พร้อมกัน การ deploy ก็ยังเกิดขึ้นแบบ asynchronous อยู่ดี ในระบบหลาย repo คุณต้องทำงานผ่านหลาย PR จึงมีการรับรู้ความเสี่ยงอยู่แล้ว ส่วน CI ใน monorepo มักทำหน้าที่ตรวจสอบ service contract (CI job) และเมื่อจำเป็นก็ต้องระบุเหตุผลของการเปลี่ยนแปลงไว้]
Monorepo ของบริษัทเทคขนาดใหญ่มีอยู่สองประเภท ประเภทแรกคือ monorepo เดียวระดับทั้งองค์กรแบบ "THE" monorepo ตามที่บทความพูดถึง ซึ่งต้องใช้ VCS/CI แบบ custom และมีวิศวกร 200 คนคอยซัพพอร์ต Google, Meta และ Uber ใช้แนวทางนี้ ความเจ็บปวดกว่าจะไปถึงจุดนั้นหนักเกินจินตนาการ และโดยทั่วไปจะค่อย ๆ ขยายมาจาก monorepo ที่เล็กกว่าในระดับทีม แต่ละสแตก/ภาษา/ทีมก็จัดการกันเองด้วยเครื่องมืออย่าง Bazel, Turborepo หรือ Poetry แล้วเมื่อเวลาผ่านไปจึงค่อยรวมเป็น monorepo ที่ใหญ่ขึ้น อย่างไรก็ตาม ไม่ว่าจะเป็นแบบไหนก็ต้องใช้เงินและเวลาของทั้งฝั่งนักพัฒนาและธุรกิจเป็นหลักล้าน และท้ายที่สุดก็ต้องอาศัยการสนับสนุนจากนักพัฒนาที่ผ่านกระบวนการนี้มา
เมื่อเคยทำงานในบริษัทที่มี monorepo ขนาดใหญ่ ก็พบว่าชอบ monorepo มากกว่ามาก monorepo เดียวช่วยให้มองเห็นทั้ง service graph, โครงสร้างการเรียกใช้โค้ด และภาพรวมทั้งหมดได้อย่างโปร่งใส ส่วน polyrepo ทำให้ความรู้กระจัดกระจายอยู่ตามทีม รับช่วงโค้ดใหม่ก็ยาก และการทำความเข้าใจ code archive ก็เหมือนหลงเข้าเขาวงกต polyrepo ให้ความรู้สึกเหมือนข้อความ Discord/Slack เก่า ๆ ที่ถูกลืมเลือน หาก monorepo มีต้นทุนสูง polyrepo ก็มีต้นทุนเช่นกัน เพียงแต่เป็นคนละรูปแบบ monorepo เปรียบเหมือนสัตว์กินพืชขนาดมหึมาบนทวีป ส่วน polyrepo คือสิ่งมีชีวิตหลากชนิดที่จมหายอยู่ในความมืด
บริษัทปัจจุบันมี backend แยกเป็นราว 11 git repo และการทำฟีเจอร์หนึ่งอย่างต้องใช้ merge request ถึง 4–5 อัน จึงยุ่งยากมาก กำลังพิจารณาใช้ monorepo เพื่อรวบหลายโปรเจ็กต์เข้าด้วยกัน แต่ถ้ารวม repo ไม่ได้ ก็สงสัยว่ามีทางเลือกอื่นแทน monorepo หรือไม่
ยังไม่มีระบบ orchestration สำหรับ monorepo ที่ทั้งง่ายและทรงพลังโดยไม่ขึ้นกับภาษา Bazel ซับซ้อนและเรียนรู้ยาก แต่ช่วงหลังเอกสารดีขึ้นมาก ยังมีตัวเลือกอย่าง Buck, NX, Pants เป็นต้น แต่แต่ละตัวก็มีจุดเด่นเฉพาะ และโดยเฉพาะฝั่งเว็บยังรองรับจำกัด CI ส่วนใหญ่ก็ยังซัพพอร์ตเครื่องมือเหล่านี้ได้ไม่ดี ทำให้การตั้งค่าค่อนข้างยุ่งยาก อนึ่ง Rush ของ Microsoft มอบประสบการณ์ที่ดีที่สุด โดยเฉพาะกับ monorepo สาย frontend/NodeJS แนะนำ Rush เว็บไซต์ทางการของ Rush
มีการพูดถึงความจริงที่ว่า monorepo ส่วนใหญ่ไม่ได้โตไปถึงระดับบริษัทอย่าง Google, Uber, Meta จำนวน service ก็แตกต่างกันไปในแต่ละบริษัท และแม้จะมีสัก 100 ตัว ก็ยังไม่มีปัญหาเรื่องสเกลของ VCS และ LSP tags ก็ยังรันบนแล็ปท็อปได้สบาย แม้จะสั่งรันทุก test ใน CI แบบไม่คัดกรองก็ยังพอไหว สรุปคือ ไม่ใช่ทุกบริษัทที่ต้องการสเกลระดับ Google
บริษัทปัจจุบันกำลังสร้าง monorepo แยกตาม language stack ซึ่งเป็นทางสายกลางที่ใช้ได้ดีทีเดียว
ประเด็นที่ไม่ค่อยถูกพูดถึงในข้อถกเถียง monorepo vs multi-repo คือการเกิด "กฎของคอนเวย์แบบย้อนกลับ" กล่าวคือ โครงสร้าง repo ส่งอิทธิพลต่อโครงสร้างองค์กรและวิธีแก้ปัญหา monorepo ทำให้ทีม infrastructure กลางต้องทำงานแบบฮีโร่มากขึ้น และเมื่อแตะพื้นที่ส่วนกลางก็มีโอกาสพังได้หลายจุด ทำให้แม้แต่การพัฒนาฟีเจอร์เดียวก็ยากขึ้น ส่วน multi-repo ต้องใช้หลาย PR, การประสานงานระหว่างทีม และการเมืองภายใน แต่ก็เปิดโอกาสให้นักพัฒนาหลากหลายคนกระจายบทบาทกันจัดการได้
แม้ใน monorepo หากเป็นการเปลี่ยนแปลงที่เชื่อมโยงลึกกับส่วนกลาง ก็ยังสามารถทยอยทำเป็นหลายขั้นได้อยู่ดี ระหว่างทางก็ยังต้องจัดการหลาย PR, การประสานงาน และประเด็นทางการเมือง แต่ข้อดีคือ monorepo ทำให้เห็นสถานะ rollout ได้ชัดเจนกว่า
ใน polyrepo การเปลี่ยนแปลงในส่วนกลางมักไม่ถูกสะท้อนไปยัง downstream repo ทำให้แต่ละ repo ตรึงอยู่กับเวอร์ชันต่างกัน และมีกรณีที่ไม่ได้อัปเดตกันเป็นปี ๆ จนสร้างปัญหา ซึ่งเกิดบ่อยกว่ามาก
มีคำถามว่าสมมติฐานที่ว่าองค์กรเลือกทิศทางผ่านโครงสร้าง repo ก่อน แล้วค่อยให้การเลือกเทคโนโลยีตามมานั้นถูกต้องหรือไม่ ในความเป็นจริง สิ่งที่มาก่อนมักเป็นปรัชญาองค์กรที่ลึกกว่านั้น เช่น จะเน้นการแตกย่อยหรือการแบ่งปัน มากกว่ารายละเอียดของโครงสร้าง repo โดยตรง แม้ภายหลังจะเปลี่ยนทิศทาง วิธีจัดการโค้ดก็ยังปรับแก้ได้ ต่อให้เป็น multi-repo วิศวกรก็อาจเข้าถึงโค้ดแทบทั้งหมดได้ และ monorepo เองก็อาจกำหนด isolation ที่เข้มงวด รวมถึงมีกฎ CI หรือกฎการ deploy แยกต่างหากได้เช่นกัน
ใน monorepo การเปลี่ยนข้ามโปรเจ็กต์ทำได้ง่าย จึงมีหลายกรณีที่ใน polyrepo มันยุ่งยากเกินไปจนคนไม่คิดจะลองทำตั้งแต่แรก
จากประสบการณ์ในบริษัทเทคขนาดใหญ่ การดูแล build system ต้องมีทีมรับผิดชอบโดยเฉพาะ monorepo ขนาดใหญ่ทำงานบน virtual file system ที่ดาวน์โหลด source file เมื่อจำเป็น อีกประเด็นที่บทความไม่ได้พูดถึงคือ แทบทุกการพัฒนาจะเกิดบน dev server ที่รันในดาต้าเซ็นเตอร์ ใช้สภาพแวดล้อมระดับ 50–100 คอร์ หรือใช้ container แบบ on-demand ที่อัปเดตเป็น commit ล่าสุดอยู่เสมอ IDE จะเชื่อมรวมกับ dev server และมีการเตรียมพร้อม/ตั้งค่าอัตโนมัติสำหรับแต่ละภาษาและแต่ละ service ผ่าน chef/ansible การพัฒนา monorepo ขนาดใหญ่โดยตรงบนแล็ปท็อปเกิดขึ้นน้อยมาก ยกเว้นบางกรณีอย่าง mobile หรือแอป Mac
น่าจะเคยอยู่ทีม build เดียวกัน ไม่ว่าสภาพแวดล้อมการพัฒนา monorepo จะเป็น local หรือ remote สิ่งที่สำคัญกว่าคือ reproducibility หากเป็น remote dev server ที่ทำ image ไว้ได้ก็จะง่ายขึ้นและเชื่อถือได้มากขึ้น
มีประสบการณ์ใช้สภาพแวดล้อมพัฒนาในดาต้าเซ็นเตอร์แม้กับทีมขนาดเล็ก เมื่อดูจากราคาฮาร์ดแวร์และความหนาแน่นในทุกวันนี้ การตั้ง rack ของตัวเองเพื่อรันเครื่องมือ on-demand สำหรับ dev/staging/test กลับสมเหตุสมผลกว่ามาก เมื่อทุกคนได้ใช้สภาพแวดล้อมพัฒนาที่คล้าย production ร่วมกัน มุมมองต่อแนวทาง monorepo ก็เปลี่ยนไปมาก อย่างไรก็ตาม บริษัทขนาดเล็กถึงกลางมักไม่มีงบให้ลงทุนกับ build system และก็ไม่ได้เจอปัญหาระดับ build system ขนาดใหญ่แบบนั้นด้วยอยู่แล้ว (ขนาดทีมอย่างน้อย 10–20 คน และแม้ผลิตภัณฑ์จะซับซ้อนมาก การดูแลอาจเป็นแค่งาน part-time ก็ยังได้)
เรื่องเล่าจากทีมเล็กที่ Molnett (serverless cloud) ซึ่งใช้ monorepo บน Bazel แล้วได้ประสิทธิภาพสูงมาก ทีมมีคนทำงานเต็มเวลาเพียง 1.5 คน ใช้ Tilt+Bazel+Kind เพื่อรันทั้งแพลตฟอร์มรวมถึง Kubernetes operator บนแล็ปท็อป รองรับทั้ง Mac/Linux ตรวจสอบได้แม้กระทั่ง OS ที่ใช้ Bottlerocket และ Firecracker ในเครื่อง local เอง มี tool layer ที่ทำให้นักพัฒนาทุกคนใช้ go/kubectl เวอร์ชันเดียวกันโดยไม่ต้องติดตั้งในเครื่อง ต้องทุ่มแรงกับการดูแลอยู่บ้าง แต่ทำได้เพราะมีอดีตสมาชิก Google SRE อยู่ด้วย และจากนี้ก็อยากทำงานแนวนี้ต่อไปเท่านั้น (ภาษาหลักคือ Golang, Bash, Rust)
ถ้าเป็นทีมเล็กแค่ 1.5 คน single repo ก็เป็นเรื่องธรรมดาอยู่แล้ว ประสบการณ์กับ Bazel นั้นแย่มาก แต่ก็อาจคุ้มค่าสำหรับโปรเจ็กต์ขนาดใหญ่กว่า สำหรับทีมเล็กกว่า 2 คน แค่ Kind+Tilt ก็น่าจะพอ ส่วน tool layer นั้น Go ก็ช่วยได้ระดับหนึ่งอยู่แล้วผ่าน go.mod และ kubectl ก็ทำแนวเดียวกันได้ ต้องคิดถึงระดับเงินเดือนของอดีต Googler ด้วย หวังว่าต้นทุนการดูแล Bazel จะยังคุ้มค่าต่อไป
บริษัทของเรา deploy ผ่านบริการที่อิง systemd และ ansible playbooks และใช้ tmuxinator เพื่อสตาร์ต backend/DB/search engine/frontend ทั้งหมดในโหมด dev ผ่านเทอร์มินัลแบบอัตโนมัติ เพียงรันคำสั่ง
tmuxinatorที่ root ครั้งเดียวก็ได้ dev environment ครบทั้งชุด monorepo เดียวสะดวกกว่าสมัยก่อนอย่างท่วมท้นสถานการณ์คล้ายกัน และขอแชร์ว่าการนำ Bazel มาใช้ให้ผลลัพธ์สูงมาก tool layer ช่วยรักษาสภาพแวดล้อมการพัฒนาให้สม่ำเสมอได้ดี ตอนนี้ยังต้องใช้
bazel runโดยตรงอยู่ และอยากรู้ว่ามีวิธีทำ automation ที่ดีกว่านี้หรือไม่ จึงขอให้อธิบายวิธีการทำงานสำหรับทีม 2 คน การใช้รูปแบบ microservices/K8s เองก็เป็น overengineering อยู่แล้ว ที่ขนาดคนเท่านี้ ไม่ว่าจะใช้วิธีไหนก็แทบไม่มีปัญหา สมัยก่อนจะใช้ Dropbox/SVN/MS VCS หรืออะไรก็พอทำงานได้ทั้งหมด (แม้จะมีจุดไม่สะดวกอยู่บ้าง) และไม่ได้เป็นปัญหาอะไรจริงจัง ในขนาดทีมแบบนี้ ทุกคนยังเห็นภาพทั้งกระบวนการได้ในหัว ประสบการณ์นี้ชี้ว่าความสำเร็จไม่ได้ขึ้นกับเครื่องมือหรือโครงสร้างพื้นฐานที่ซับซ้อน
มีฟรีแลนซ์คนหนึ่งแชร์ว่าตลอด 4 ปีที่ผ่านมา ต้องตั้งค่า monorepo มาแล้วถึง 3 ครั้งในหลายบริษัท โดยจำกัดอยู่แค่ฝั่ง frontend และใช้เฉพาะ ecosystem ของ JavaScript/TypeScript จึงยังพอจัดการได้ monorepo ที่ดีจริง ๆ จะทำงานภายในเหมือน polyrepo กล่าวคือ แต่ละโปรเจ็กต์พัฒนา/deploy/host แยกกันได้อย่างอิสระ แต่ก็อยู่ร่วมกันใน codebase เดียว แชร์องค์ประกอบร่วมอย่าง UI ได้อย่างอิสระ และรักษา look and feel ให้สม่ำเสมอ มีการแนะนำเอกสารอ้างอิงเป็นคู่มือเชิงปฏิบัติ
สุดท้ายแล้ว ทุกอย่างขึ้นอยู่กับบริบท บริษัทของเราดูแล git repo แยกกันราว 40 กว่า repo แต่ละอันมี CI ของตัวเอง ทำ build/test/package แล้วสุดท้ายจึงรวมเป็นภาพไฟล์ระบบเดียวเพื่อทำ integration test ส่วนประกอบต่าง ๆ สื่อสารกันผ่านข้อความ Flatbuffers และ flatbuffers เองก็จัดการเป็น submodule การจัดการ downstream dependency ทำได้ยากอยู่บ้าง แต่ก็ได้ความยืดหยุ่นบางส่วนผ่าน progressive enhancement ในกรณีแบบนี้จะเรียกว่า multi-repo หรือ monorepo ที่มี submodule จำนวนมากก็ยังตอบยาก และยังไม่แน่ใจว่าถ้าเปลี่ยนเป็น monorepo แล้วจะได้ประโยชน์หรือไม่ ท้ายที่สุดก็คือเรื่องของ trade-off และการเลือกว่าจะยอมรับความไม่สะดวกแบบไหน
ผู้เขียนบล็อกเกี่ยวกับเครื่องมือ monorepoแชร์ประสบการณ์ว่า ผู้คนมักเน้นแต่ข้อดีของ monorepo แต่ความซับซ้อนของการดูแล monorepo ให้สำเร็จในโลกจริงนั้น ส่วนใหญ่ถูกแบกรับอยู่เบื้องหลังโดยทีม devops/devtools เพราะฉะนั้นควรตัดสินใจนำมาใช้อย่างระมัดระวัง แต่ถ้าสร้างได้ดี มันก็ให้คุณค่าได้มากพอ
ประสบการณ์กับ monorepo ที่ดูแลอย่างดีนั้นยอดเยี่ยมมากจนไม่อยากกลับไปใช้ workflow แบบอื่นอีกเลย แต่การเริ่มแบบไม่พร้อมในสไตล์ "เราก็ทำ monorepo กันเถอะ" นั้นเหมือนฝันร้าย หากมีใครแพ็กเกจสภาพแวดล้อมและเครื่องมือสำหรับ monorepo แบบพร้อมใช้มาขาย ก็น่าจะเป็นโอกาสทางธุรกิจขนาดใหญ่
มีประสบการณ์ว่าในองค์กรขนาดใหญ่ monorepo อาจจำกัด dependency ระหว่างทีมอย่างรุนแรงจนกลับทำให้การนำโค้ดมาใช้ซ้ำลดลงได้ หากทีมไลบรารีจะเปลี่ยนอะไร ก็ต้องให้ผู้ใช้ปลายทางทั้งหมดอัปเดตตาม แต่เพราะมีทีมที่ใช้งานในรูปแบบไม่คาดคิดอยู่เสมอ การแก้ไขจึงซับซ้อนพันกันตาม Hyrum's Law สุดท้ายองค์กรใหญ่ก็มักลงเอยด้วยการ copy-paste ภายใน, การ fork, การควบคุมสิทธิ์เข้าถึงที่เข้มงวด และการอนุมัติการเปลี่ยนแปลงที่ล่าช้า
เมื่อต้องสร้างไลบรารีเพื่อใช้แบบทั่วไป ควรออกแบบ API อย่างระมัดระวัง หากเป็นไปได้อย่าเปลี่ยน API และถ้าจำเป็นต้องเปลี่ยน ก็ควรวางแผนการเปลี่ยนแปลงขนาดใหญ่ให้ชัดเจน หรือแทนที่ด้วยฟังก์ชันใหม่พร้อมประกาศ deprecated เวอร์ชันเก่า สำหรับโค้ดขนาดเล็ก การ copy-paste ก็ไม่ได้แย่เสมอไป
ถึงอย่างนั้น ข้อดีของ monorepo ก็คือสามารถค้นหาทุกจุดที่ใช้งานได้ง่าย และหากจำเป็นก็แก้ไขแบบ atomic ได้
ซอฟต์แวร์ทุกชนิดต้องคำนึงถึง dependency และ monorepo กลับเพิ่มอำนาจให้ทั้งฝั่งไลบรารีและฝั่งผู้ใช้ในการเปลี่ยนแปลงซึ่งกันและกัน
ใน monorepo การแก้ให้เหมาะกับบริบทของตัวเองทำได้ง่ายกว่า จึงมีโอกาสที่โค้ดจะถูกนำกลับมาใช้ซ้ำมากกว่า polyrepo