Atomic Commits Versus Atomic Deploy (Atomic Commit เทียบกับ Atomic Deploy)

การ ที่สามารถทำ atomic commit ข้ามหลาย service ได้ ไม่ได้แปลว่าคุณจะได้ atomic rollout ไปด้วย ถ้าคุณพบว่าตัวเองต้องการเปลี่ยนโค้ดข้ามหลาย service พร้อมกัน และ roll out เข้า production พร้อมกันทั้งหมดในเวลาเดียวกัน นี่เป็นการละเมิดหลักการหลักของ independent deployability สำหรับข้อมูลเพิ่มเติมเรื่องนี้ ดูได้ที่ "DRY and the Perils of Code Reuse in a Microservice World"

Mapping to build (การจับคู่ไปยัง Build)

เมื่อมี source code repository เดียวต่อหนึ่ง microservice การจับคู่จาก source code ไปยัง build process นั้นตรงไปตรงมา การเปลี่ยนแปลงใดๆ ใน source code repository นั้นสามารถ trigger CI build ที่ตรงกันได้ แต่กับ monorepo มันจะซับซ้อนขึ้นอีกหน่อย

จุดเริ่มต้นง่ายๆ คือการจับคู่ folder ภายใน monorepo เข้ากับ build ดังที่แสดงใน Figure 7-10 ตัวอย่างเช่น การเปลี่ยนแปลงใน folder user-service จะ trigger การ build ของ User service ถ้าคุณ check in โค้ดที่เปลี่ยนแปลงไฟล์ทั้งใน folder user-service และ folder catalog-service ทั้ง build ของ User และ build ของ Catalog ก็จะถูก trigger ทั้งคู่

A single source repo with subdirectories mapped to independent builds

Figure 7-10. source repo เดียวที่มี subdirectory ถูกจับคู่ไปยัง build ที่เป็นอิสระต่อกัน

เรื่องนี้จะซับซ้อนขึ้นเมื่อคุณมีโครงสร้าง folder ที่เกี่ยวข้องมากขึ้น ในโปรเจกต์ขนาดใหญ่ คุณอาจจบลงด้วยการมี folder ที่แตกต่างกันหลายตัวที่ต้องการ trigger build เดียวกัน และบาง folder ก็ trigger มากกว่าหนึ่ง build ในกรณีง่ายที่สุด คุณอาจมี folder "common" ที่ใช้ร่วมกันโดย microservices ทั้งหมด ซึ่งการเปลี่ยนแปลงในนั้นจะทำให้ microservices ทั้งหมดถูก rebuild ในกรณีที่ซับซ้อนกว่านั้น ทีมต่างๆ ต้องหันมาใช้ graph-based build tool อย่างเครื่องมือ open source Bazel เพื่อจัดการ dependency เหล่านี้อย่างมีประสิทธิภาพมากขึ้น (Bazel เป็น open source เวอร์ชันหนึ่งของ build tool ภายในของ Google เอง) การนำ build system ใหม่มาใช้อาจเป็นงานใหญ่ที่ต้องใช้ความพยายามมาก ดังนั้นจึงไม่ใช่สิ่งที่ควรทำอย่างไม่ใส่ใจ แต่ monorepo ของ Google เองก็คงเป็นไปไม่ได้เลยถ้าไม่มีเครื่องมือแบบนี้

หนึ่งในประโยชน์ของแนวทาง monorepo คือเราสามารถใช้โค้ดซ้ำข้ามโปรเจกต์ได้อย่างละเอียดมากขึ้น ในโมเดลแบบ multirepo ถ้าผมต้องการใช้โค้ดของคนอื่นซ้ำ มันมักจะต้องถูกแพ็คเป็น versioned artifact ที่ผมจะรวมเข้าเป็นส่วนหนึ่งของ build ได้ (เช่น Nuget package, JAR file หรือ NPM) เมื่อหน่วยของการใช้ซ้ำของเราคือ library เราก็อาจดึงโค้ดเข้ามามากกว่าที่เราต้องการจริงๆ ในทางทฤษฎี ด้วย monorepo ผมสามารถ depend แค่ไฟล์ source เดียวจากอีกโปรเจกต์หนึ่งได้เลย แม้ว่าแน่นอนว่ามันจะทำให้ผมมี build mapping ที่ซับซ้อนขึ้นก็ตาม

Defining ownership (การกำหนด Ownership)

เมื่อขนาดทีมและขนาด codebase เล็ก monorepo มักจะทำงานได้ดีกับ build tool และ source code management tool แบบดั้งเดิมที่คุณคุ้นเคย แต่เมื่อ monorepo ของคุณใหญ่ขึ้น คุณจะต้องเริ่มมองหาเครื่องมือประเภทอื่นๆ เราจะสำรวจโมเดล ownership อย่างละเอียดมากขึ้นใน Chapter 15 แต่ในระหว่างนี้ก็คุ้มค่าที่จะสำรวจกันสั้นๆ ว่าเรื่องนี้ส่งผลอย่างไรเมื่อเราคิดถึง source control

Martin Fowler เคย เขียนไว้ก่อนหน้านี้ เกี่ยวกับโมเดล ownership ที่แตกต่างกัน โดยอธิบายสเกลของ ownership ตั้งแต่ strong ownership ผ่าน weak ownership ไปจนถึง collective ownership นับตั้งแต่ที่ Martin บัญญัติคำเหล่านี้ขึ้นมา แนวปฏิบัติในการพัฒนาก็เปลี่ยนไป ดังนั้นบางทีก็คุ้มค่าที่จะทบทวนและนิยามคำเหล่านี้ใหม่

ใน strong ownership โค้ดบางส่วนเป็นของกลุ่มคนกลุ่มหนึ่งโดยเฉพาะ ถ้ามีคนนอกกลุ่มนั้นต้องการเปลี่ยนแปลง พวกเขาต้องขอให้เจ้าของทำการเปลี่ยนแปลงให้ ส่วน weak ownership ยังคงมีแนวคิดของเจ้าของที่ถูกกำหนดไว้ แต่คนนอกกลุ่ม ownership ก็สามารถทำการเปลี่ยนแปลงได้ แม้ว่าการเปลี่ยนแปลงเหล่านั้นจะต้องถูก review และยอมรับโดยใครสักคนในกลุ่ม ownership ก่อน ซึ่งครอบคลุมถึงการส่ง pull request ไปให้ทีม ownership หลัก review ก่อนที่ pull request จะถูก merge ส่วน collective ownership นักพัฒนาคนไหนก็สามารถเปลี่ยนโค้ดส่วนไหนก็ได้

เมื่อมีนักพัฒนาจำนวนน้อย (20 คนหรือน้อยกว่า เป็นแนวทางทั่วไป) คุณสามารถปฏิบัติ collective ownership ได้ โดยที่นักพัฒนาคนไหนก็เปลี่ยน microservice อื่นๆ ได้ แต่เมื่อคุณมีคนมากขึ้น คุณก็มีแนวโน้มที่จะอยากย้ายไปใช้โมเดล strong หรือ weak ownership เพื่อสร้างขอบเขตความรับผิดชอบที่ชัดเจนขึ้น เรื่องนี้อาจสร้างความท้าทายให้กับทีมที่ใช้ monorepo ถ้า source control tool ของพวกเขาไม่รองรับการควบคุม ownership แบบละเอียด

source code tool บางตัวให้คุณระบุ ownership ของ directory เฉพาะเจาะจง หรือแม้แต่ filepath เฉพาะภายใน repository เดียวได้ ในตอนแรก Google implement ระบบนี้บน Perforce สำหรับ monorepo ของตัวเอง ก่อนที่จะพัฒนา source control system ของตัวเองขึ้นมา และนี่ก็เป็น สิ่งที่ GitHub รองรับ มาตั้งแต่ปี 2016 ด้วย บน GitHub คุณจะสร้างไฟล์ CODEOWNERS ซึ่งช่วยให้คุณจับคู่เจ้าของกับ directory หรือ filepath ได้ คุณสามารถดูตัวอย่างบางส่วนได้ใน Example 7-1 ซึ่งดึงมาจากเอกสารของ GitHub เอง ที่แสดงให้เห็นถึงความยืดหยุ่นที่ระบบเหล่านี้สามารถนำมาให้ได้

Example 7-1. ตัวอย่างวิธีการระบุ ownership ใน directory เฉพาะภายในไฟล์ CODEOWNERS ของ GitHub (ตัวอย่างการระบุ Ownership ใน Directory เฉพาะภายในไฟล์ CODEOWNERS ของ GitHub)
# In this example, @doctocat owns any files in the build/logs
# directory at the root of the repository and any of its
# subdirectories.
/build/logs/ @doctocat

# In this example, @octocat owns any file in an apps directory
# anywhere in your repository.
apps/ @octocat

# In this example, @doctocat owns any file in the `/docs`
# directory in the root of your repository.
/docs/ @doctocat

แนวคิดเรื่อง code ownership ของ GitHub เองช่วยให้มั่นใจว่าเจ้าของโค้ดของไฟล์ source จะถูกขอให้ review ทุกครั้งที่มีการเปิด pull request สำหรับไฟล์ที่เกี่ยวข้อง เรื่องนี้อาจเป็นปัญหากับ pull request ขนาดใหญ่ เพราะคุณอาจจบลงด้วยการต้องขอ sign-off จาก reviewer หลายคน แต่ก็มีเหตุผลดีๆ มากมายที่จะมุ่งไปที่ pull request ขนาดเล็กกว่าอยู่ดี ไม่ว่าในกรณีใด

Tooling (เครื่องมือ)

monorepo ของ Google เองมีขนาดมหึมา และต้องใช้ความพยายามด้าน engineering อย่างมากเพื่อให้มันทำงานได้ในระดับสเกลนั้น ลองนึกถึงสิ่งต่างๆ เช่น graph-based build system ที่ผ่านการพัฒนามาหลายรุ่น distributed object linker ที่ช่วยเร่งความเร็ว build, plug-in สำหรับ IDE และ text editor ที่สามารถตรวจสอบไฟล์ dependency แบบไดนามิกได้ นี่เป็นงานจำนวนมหาศาล เมื่อ Google เติบโตขึ้น มันก็ยิ่งเจอข้อจำกัดในการใช้ Perforce มากขึ้นเรื่อยๆ จนต้องสร้าง source control tool ของตัวเองที่เรียกว่า Piper ขึ้นมา ตอนที่ผมทำงานในส่วนนี้ของ Google ช่วงปี 2007-2008 มีคนมากกว่าร้อยคนที่ดูแล developer tool ต่างๆ โดยความพยายามส่วนใหญ่ทุ่มเทให้กับการจัดการผลกระทบของแนวทาง monorepo แน่นอนว่านี่เป็นสิ่งที่คุณสามารถให้เหตุผลสนับสนุนได้ ถ้าคุณมีวิศวกรนับหมื่นคน

สำหรับภาพรวมที่ละเอียดขึ้นเกี่ยวกับเหตุผลเบื้องหลังการใช้ monorepo ของ Google ผมขอแนะนำบทความ "Why Google Stores Billions of Lines of Code in a Single Repository" โดย Rachel Potvin และ Josh Levenberg 5 อันที่จริงผมขอแนะนำว่ามันเป็นสิ่งที่ต้องอ่านสำหรับใครก็ตามที่คิดว่า "เราควรใช้ monorepo เพราะ Google ใช้!" องค์กรของคุณคงไม่ใช่ Google และคงไม่มีปัญหา ข้อจำกัด หรือทรัพยากรแบบที่ Google มี พูดอีกอย่างก็คือ ไม่ว่า monorepo ที่คุณจะจบลงด้วยจะเป็นแบบไหน มันคงไม่ใช่ของ Google อยู่ดี

Microsoft ก็เจอปัญหาคล้ายๆ กันเรื่องสเกล บริษัทนำ Git มาใช้ช่วยจัดการ source code repository หลักสำหรับ Windows working directory เต็มรูปแบบของ codebase นี้มีขนาดประมาณ 270 GB ของไฟล์ source 6 การดาวน์โหลดทั้งหมดนั้นจะใช้เวลานานมาก และก็ไม่จำเป็นด้วย เพราะนักพัฒนาสุดท้ายแล้วก็จะทำงานอยู่แค่ส่วนเล็กๆ ส่วนหนึ่งของระบบทั้งหมดเท่านั้น ดังนั้น Microsoft จึงต้องสร้าง virtual file system เฉพาะทางขึ้นมา คือ VFS for Git (เดิมรู้จักกันในชื่อ GVFS) ซึ่งช่วยให้มั่นใจว่ามีเพียงไฟล์ source ที่นักพัฒนาต้องการเท่านั้นที่จะถูกดาวน์โหลดจริงๆ

VFS for Git เป็นความสำเร็จที่น่าประทับใจ เช่นเดียวกับ toolchain ของ Google เอง แม้ว่าการให้เหตุผลสนับสนุนการลงทุนแบบนี้ในเทคโนโลยีประเภทนี้จะง่ายกว่ามากสำหรับบริษัทแบบนี้ ก็ควรชี้ให้เห็นด้วยว่าแม้ VFS for Git จะเป็น open source แต่ผมก็ยังไม่เคยเจอทีมนอก Microsoft ที่ใช้มันเลย และ toolchain ส่วนใหญ่ของ Google เองที่รองรับ monorepo ก็เป็น closed source (Bazel เป็นข้อยกเว้นที่โดดเด่น แต่ก็ยังไม่ชัดเจนว่า Bazel เวอร์ชัน open source สะท้อนสิ่งที่ใช้ภายใน Google เองมากน้อยแค่ไหน)

บทความ "Monorepos in the Wild" ของ Markus Oberlehner แนะนำให้ผมรู้จักกับ Lerna เครื่องมือที่สร้างโดยทีมเบื้องหลัง Babel JavaScript compiler Lerna ถูกออกแบบมาให้ผลิต versioned artifact หลายตัวจาก source code repository เดียวกันได้ง่ายขึ้น ผมพูดไม่ได้เต็มปากว่า Lerna มีประสิทธิภาพแค่ไหนในงานนี้ (นอกเหนือจากข้อบกพร่องที่น่าสังเกตอื่นๆ อีกหลายอย่าง ผมเองก็ไม่ใช่นักพัฒนา JavaScript ที่มีประสบการณ์มากนัก) แต่จากการพิจารณาผิวเผิน ดูเหมือนมันจะช่วยลดความซับซ้อนของแนวทางนี้ได้บ้าง

How "mono" is mono? ("mono" นั้น mono แค่ไหน)

Google ไม่ได้เก็บโค้ด ทั้งหมด ไว้ใน monorepo มีบางโปรเจกต์ โดยเฉพาะโปรเจกต์ที่พัฒนาแบบเปิด ที่ถูกเก็บไว้ที่อื่น กระนั้นก็ตาม อย่างน้อยตามบทความ ACM ที่กล่าวถึงก่อนหน้านี้ 95% ของโค้ดของ Google ถูกเก็บไว้ใน monorepo ณ ปี 2016 ในองค์กรอื่นๆ monorepo อาจถูกจำกัดขอบเขตไว้แค่ระบบเดียว หรือระบบจำนวนน้อย ซึ่งหมายความว่าบริษัทหนึ่งอาจมี monorepo จำนวนน้อยสำหรับส่วนต่างๆ ขององค์กร

ผมยังเคยคุยกับทีมที่ปฏิบัติ monorepo แบบต่อทีมด้วย แม้ว่าในทางเทคนิคแล้วมันอาจไม่ตรงกับนิยามดั้งเดิมของ pattern นี้เสียทีเดียว (ซึ่งโดยทั่วไปพูดถึงในแง่ของหลายทีมที่แชร์ repository เดียวกัน) ผมก็ยังคิดว่ามันเป็น "monorepo" มากกว่าอย่างอื่น ในสถานการณ์นี้ แต่ละทีมมี monorepo ของตัวเองที่อยู่ภายใต้การควบคุมของตัวเองอย่างเต็มที่ microservices ทั้งหมดที่เป็นของทีมนั้นจะมีโค้ดถูกเก็บไว้ใน monorepo ของทีมนั้น ดังที่แสดงใน Figure 7-11

A pattern variation where each team has its own monorepo

Figure 7-11. รูปแบบหนึ่งของ pattern นี้ที่แต่ละทีมมี monorepo ของตัวเอง

สำหรับทีมที่ปฏิบัติ collective ownership โมเดลนี้มีประโยชน์มากมาย อาจกล่าวได้ว่าให้ข้อดีส่วนใหญ่ของแนวทาง monorepo ในขณะที่หลีกเลี่ยงความท้าทายบางอย่างที่เกิดขึ้นในสเกลที่ใหญ่กว่า ทางเลือกกึ่งกลางแบบนี้สมเหตุสมผลมากในแง่ของการทำงานภายในขอบเขต ownership ขององค์กรที่มีอยู่แล้ว และมันสามารถบรรเทาข้อกังวลเกี่ยวกับการใช้ pattern นี้ในสเกลที่ใหญ่กว่าได้บ้าง

Where to use this pattern (ควรใช้ Pattern นี้เมื่อไร)

องค์กรบางแห่งที่ทำงานในสเกลใหญ่มากพบว่าแนวทาง monorepo ทำงานได้ดีมากสำหรับพวกเขา เราได้พูดถึง Google และ Microsoft ไปแล้ว และเราสามารถเพิ่ม Facebook, Twitter และ Uber เข้าไปในรายการได้ องค์กรเหล่านี้มีสิ่งหนึ่งที่เหมือนกันคือ พวกเขาเป็นบริษัทใหญ่ที่มุ่งเน้นเทคโนโลยี ที่สามารถทุ่มทรัพยากรจำนวนมากเพื่อดึงประโยชน์สูงสุดจาก pattern นี้ได้ อีกจุดหนึ่งที่ผมเห็นว่า monorepo ทำงานได้ดีคือปลายอีกด้านของสเปกตรัม กับนักพัฒนาและทีมจำนวนน้อยกว่า เมื่อมีนักพัฒนา 10 ถึง 20 คน มันง่ายกว่าที่จะจัดการขอบเขต ownership และทำให้ build process เรียบง่ายด้วยแนวทาง monorepo จุดที่เจ็บปวดดูเหมือนจะเกิดขึ้นกับองค์กรตรงกลาง นั่นคือองค์กรที่มีสเกลใหญ่พอจะเริ่มเจอปัญหาที่ต้องการ tooling หรือวิธีการทำงานใหม่ๆ แต่ไม่มีทรัพยากรเหลือพอที่จะลงทุนกับแนวคิดเหล่านี้

Which Approach Would I Use? (ผมจะเลือกใช้แนวทางไหน)

จากประสบการณ์ของผม ข้อดีหลักของแนวทาง monorepo ทั้งการใช้โค้ดซ้ำแบบละเอียดและ atomic commit ดูเหมือนจะไม่คุ้มค่ากับความท้าทายที่เกิดขึ้นเมื่อขยายสเกล สำหรับทีมเล็กๆ ทั้งสองแนวทางก็ใช้ได้ดี แต่เมื่อคุณขยายสเกล ผมรู้สึกว่าแนวทางหนึ่ง repository ต่อหนึ่ง microservice (multirepos) นั้นตรงไปตรงมากว่า โดยพื้นฐานแล้ว ผมกังวลเรื่องการส่งเสริมให้เกิดการเปลี่ยนแปลงข้าม service เส้นแบ่ง ownership ที่สับสนมากขึ้น และความจำเป็นในการใช้ tooling ใหม่ๆ ที่ monorepo อาจนำมาด้วย

ปัญหาหนึ่งที่ผมเห็นซ้ำแล้วซ้ำเล่าคือ องค์กรที่เริ่มต้นจากขนาดเล็ก ซึ่ง collective ownership (และดังนั้น monorepo) ทำงานได้ดีในตอนแรก กลับดิ้นรนที่จะย้ายไปใช้โมเดลอื่นในภายหลัง เพราะแนวคิดของ monorepo ฝังรากลึกมาก เมื่อองค์กรด้าน delivery เติบโตขึ้น ความเจ็บปวดของ monorepo เพิ่มขึ้น แต่ต้นทุนในการย้ายไปใช้แนวทางอื่นก็เพิ่มขึ้นเช่นกัน เรื่องนี้ยิ่งท้าทายมากขึ้นสำหรับองค์กรที่เติบโตอย่างรวดเร็ว เพราะมักจะเป็นหลังจากที่การเติบโตอย่างรวดเร็วนั้นเกิดขึ้นแล้วเท่านั้นที่ปัญหาจะปรากฏชัดเจน ซึ่ง ณ จุดนั้นต้นทุนของการย้ายไปใช้แนวทาง multirepo ดูสูงเกินไป สิ่งนี้อาจนำไปสู่ sunk cost fallacy: คุณลงทุนไปมากมายเพื่อทำให้ monorepo ทำงานได้จนถึงจุดนี้ แค่ลงทุนอีกนิดหน่อยก็จะทำให้มันทำงานได้ดีเหมือนเดิมใช่ไหม บางทีอาจไม่ใช่ แต่ต้องเป็นคนที่กล้าหาญมากถึงจะยอมรับได้ว่าตัวเองกำลังทุ่มเงินดีๆ ตามเงินเสียไปเรื่อยๆ และตัดสินใจเปลี่ยนทิศทาง

ข้อกังวลเกี่ยวกับ ownership และ monorepo สามารถบรรเทาได้ด้วยการใช้การควบคุม ownership แบบละเอียด แต่นั่นมักต้องการ tooling และ/หรือความเอาใจใส่ที่เพิ่มขึ้น ความเห็นของผมในเรื่องนี้อาจเปลี่ยนไปเมื่อความสมบูรณ์ของ tooling รอบๆ monorepo ดีขึ้น แต่ถึงแม้จะมีการทำงานมากมายเกี่ยวกับการพัฒนา graph-based build tool แบบ open source ผมก็ยังเห็นการนำไปใช้ toolchain เหล่านี้ในระดับต่ำมาก ดังนั้นสำหรับผมแล้ว multirepos คือคำตอบ