การจัดการการพึ่งพา

รายงานปัญหา ดูแหล่งที่มา

เมื่อดูหน้าก่อนหน้า ธีมหนึ่งจะต้องทำซ้ำๆ ไปเรื่อยๆ นั่นคือการจัดการโค้ดของคุณเองค่อนข้างตรงไปตรงมา แต่การจัดการทรัพยากร Dependency นั้นยากกว่ามาก ทรัพยากร Dependency นั้นมีหลายประเภท กล่าวคือ บางครั้งมีความจำเป็นในงาน (เช่น "พุชเอกสารก่อนที่เราจะทำเครื่องหมายรุ่นว่าเสร็จสิ้น") และบางครั้งอาจต้องอาศัยอาร์ติแฟกต์ (เช่น "ฉันต้องการไลบรารีคอมพิวเตอร์วิทัศน์เวอร์ชันล่าสุดเพื่อสร้างโค้ด") บางครั้งมีทรัพยากร Dependency ภายในของโค้ดอีกส่วนหนึ่งที่คุณเป็นเจ้าของ (หรือในฐานข้อมูลอื่นของทีมอื่น) แต่ไม่ว่ากรณีใดก็ตาม แนวคิดที่ว่า "ฉันต้องการสิ่งนั้นก่อนที่จะมี" คือสิ่งที่เกิดซ้ำซ้ำๆ ในการออกแบบระบบบิลด์ และการจัดการทรัพยากร Dependency อาจเป็นงานพื้นฐานที่สุดของระบบบิลด์

การจัดการกับโมดูลและการขึ้นต่อกัน

โปรเจ็กต์ที่ใช้ระบบบิลด์ที่อิงตามอาร์ติแฟกต์ เช่น Bazel จะแยกออกเป็นชุดโมดูลที่มีโมดูลแสดงการพึ่งพากันและกันผ่านไฟล์ BUILD การจัดระเบียบที่เหมาะสมของโมดูลและทรัพยากร Dependency อาจส่งผลกระทบอย่างมากต่อทั้งประสิทธิภาพของระบบบิลด์และภาระงานที่ต้องดูแลรักษา

การใช้โมดูลแบบละเอียดและกฎ 1:1:1

คำถามแรกที่ปรากฏขึ้นเมื่อจัดโครงสร้างบิลด์ที่อิงตามอาร์ติแฟกต์คือการตัดสินใจว่าควรรวมฟังก์ชันการทำงานที่แต่ละโมดูลควรมีมากน้อยเพียงใด ใน Bazel โมดูลจะแสดงโดยเป้าหมายที่ระบุหน่วยที่บิลด์ได้ เช่น java_library หรือ go_binary หากเป็นไปได้ ทั้งโปรเจ็กต์อาจอยู่ในโมดูลเดียวได้โดยการวางไฟล์ BUILD 1 ไฟล์ไว้ที่รูท แล้วรวบรวมข้อมูลไฟล์ต้นทางทั้งหมดของโปรเจ็กต์ดังกล่าวซ้ำๆ แต่ในทางกลับกัน คุณอาจสร้างไฟล์ต้นฉบับแทบทุกไฟล์เป็นโมดูลของตัวเอง โดยกำหนดให้แต่ละไฟล์ต้องแสดงอยู่ในไฟล์ BUILD ทุกๆ ไฟล์ที่ต้องพึ่งพา

โปรเจ็กต์ส่วนใหญ่จะอยู่ระหว่างช่วงปลายสุดเหล่านี้และการเลือกนี้จะต้องมีการแลกเปลี่ยนระหว่างประสิทธิภาพกับความสามารถในการบำรุงรักษา การใช้โมดูลเดียวสำหรับทั้งโปรเจ็กต์อาจหมายความว่าคุณไม่จำเป็นต้องแตะไฟล์ BUILD ยกเว้นเมื่อเพิ่มทรัพยากร Dependency ภายนอก แต่หมายความว่าระบบของบิลด์ต้องสร้างโปรเจ็กต์ทั้งหมดพร้อมกันทุกครั้ง ซึ่งหมายความว่าโมเดลจะไม่สามารถโหลดพร้อมกันหรือกระจายส่วนต่างๆ ของบิลด์ และไม่สามารถแคชส่วนต่างๆ ที่สร้างไว้แล้วได้ 1 โมดูลต่อไฟล์ในทางตรงกันข้าม ระบบบิลด์มีความยืดหยุ่นสูงสุดในการแคชและกำหนดเวลาของขั้นตอนการสร้าง แต่วิศวกรจำเป็นต้องใช้ความพยายามมากขึ้นในการรักษารายการทรัพยากร Dependency เมื่อใดก็ตามที่มีการเปลี่ยนแปลงไฟล์ที่อ้างอิง

แม้ว่ารายละเอียดที่แน่นอนจะแตกต่างกันไปในแต่ละภาษา (และมักแม้กระทั่งภายในภาษา) Google มักจะสนับสนุนโมดูลที่มีขนาดเล็กกว่ามากเมื่อเทียบกับโมดูลที่มักเขียนในระบบบิลด์ตามงาน ไบนารีเวอร์ชันที่ใช้งานจริงทั่วไปที่ Google มักอาศัยเป้าหมายหลายหมื่นเป้าหมาย และแม้แต่ทีมที่มีขนาดปานกลางอาจมีเป้าหมายหลายร้อยรายการภายในฐานของโค้ด สำหรับภาษาอย่าง Java ซึ่งมีแนวคิดที่ชัดเจนเกี่ยวกับการสร้างแพ็กเกจ ไดเรกทอรีแต่ละรายการมักจะมีแพ็กเกจเดียว เป้าหมาย และไฟล์ BUILD (Pants ซึ่งเป็นระบบบิลด์อื่นที่ใช้ Bazel เรียกกฎนี้แบบ 1:1:1) ภาษาที่มีรูปแบบการสร้างแพ็กเกจที่ไม่รัดกุมมักกำหนดเป้าหมายหลายรายการต่อไฟล์ BUILD

ประโยชน์ของเป้าหมายบิลด์ขนาดเล็กเริ่มแสดงในวงกว้างจริงๆ เนื่องจากจะทำให้มีการสร้างบิลด์ที่กระจายเร็วขึ้นและไม่จำเป็นต้องสร้างเป้าหมายใหม่น้อยลง ข้อได้เปรียบจะน่าสนใจยิ่งขึ้นหลังจากการทดสอบเข้าสู่ภาพ เนื่องจากเป้าหมายแบบละเอียดหมายความว่าระบบบิลด์จะชาญฉลาดยิ่งขึ้นในการทำการทดสอบเพียงบางส่วนซึ่งอาจได้รับผลจากการเปลี่ยนแปลงใดๆ ที่ระบุ เนื่องจาก Google เชื่อมั่นในประโยชน์เชิงระบบของการใช้เป้าหมายขนาดเล็ก เราจึงได้เดินหน้าลดข้อเสียโดยการลงทุนกับเครื่องมือในการจัดการไฟล์ BUILD โดยอัตโนมัติเพื่อไม่ให้นักพัฒนาซอฟต์แวร์เป็นภาระ

เครื่องมือเหล่านี้ เช่น buildifier และ buildozer พร้อมให้ใช้งานกับ Bazel ในไดเรกทอรี buildtools

การลดการเปิดเผยโมดูล

Bazel และระบบบิลด์อื่นๆ ช่วยให้แต่ละเป้าหมายระบุระดับการเข้าถึง ซึ่งเป็นพร็อพเพอร์ตี้ที่กำหนดเป้าหมายอื่นๆ ที่อาจอิงตามพร็อพเพอร์ตี้นั้นได้ อ้างอิงเป้าหมายส่วนตัวได้ภายในไฟล์ BUILD ของตัวเองเท่านั้น เป้าหมายอาจให้ระดับการเข้าถึงที่กว้างขึ้นแก่เป้าหมายของรายการไฟล์ BUILD ที่กำหนดไว้อย่างชัดเจน หรือกำหนดเป้าหมายทุกรายการในพื้นที่ทำงานในกรณีของการเปิดเผยแบบสาธารณะ

โดยทั่วไปแล้ว วิธีที่ดีที่สุดคือลดระดับการเข้าถึงให้น้อยที่สุดเช่นเดียวกับภาษาโปรแกรมส่วนใหญ่ โดยทั่วไป ทีมใน Google จะทําให้เป้าหมายเป็นแบบสาธารณะก็ต่อเมื่อเป้าหมายเหล่านั้นแสดงถึงไลบรารีที่ใช้กันอย่างแพร่หลายซึ่งพร้อมให้บริการแก่ทุกทีมใน Google ทีมที่ต้องให้ผู้อื่นประสานงานก่อนใช้โค้ดจะเก็บข้อมูลเป้าหมายลูกค้าที่อนุญาตไว้เป็นระดับการเข้าถึงของเป้าหมาย เป้าหมายการใช้งานภายในของแต่ละทีมจะจำกัดไว้เฉพาะไดเรกทอรีของทีมเท่านั้น และไฟล์ BUILD ส่วนใหญ่จะมีเพียงเป้าหมายเดียวที่ไม่เป็นส่วนตัว

การจัดการการขึ้นต่อกัน

โมดูลต้องสามารถอ้างอิงกันและกันได้ ข้อเสียของการแบ่งฐานของโค้ดเป็นโมดูลแบบละเอียดก็คือคุณต้องจัดการทรัพยากร Dependency ในโมดูลเหล่านั้น (แต่เครื่องมือจะช่วยให้การดำเนินการนี้เป็นไปโดยอัตโนมัติได้) การแสดงการขึ้นต่อกันมักจะจบลงด้วยการรวมเนื้อหาจำนวนมากในไฟล์ BUILD

ทรัพยากร Dependency ภายใน

ในโปรเจ็กต์ขนาดใหญ่ที่แบ่งออกเป็นโมดูลย่อยๆ ทรัพยากร Dependency ส่วนใหญ่มักเป็นทรัพยากรภายใน กล่าวคืออยู่ในเป้าหมายอื่นที่กำหนดไว้และสร้างขึ้นในที่เก็บต้นทางเดียวกัน ทรัพยากร Dependency ภายในจะแตกต่างจากทรัพยากร Dependency ภายนอก เนื่องจากทรัพยากร Dependency สร้างขึ้นจากต้นทาง แทนที่จะดาวน์โหลดเป็นอาร์ติแฟกต์ที่สร้างไว้ล่วงหน้าขณะเรียกใช้บิลด์ นอกจากนี้ยังหมายความว่าไม่มี "เวอร์ชัน" สำหรับทรัพยากร Dependency ภายใน เนื่องจากเป้าหมายและการอ้างอิงภายในทั้งหมดจะสร้างขึ้นที่คอมมิต/การแก้ไขเดียวกันในที่เก็บเสมอ ปัญหาหนึ่งที่ควรจัดการอย่างรอบคอบเกี่ยวกับทรัพยากร Dependency ภายในคือวิธีจัดการทรัพยากร Dependency แบบสับเปลี่ยน (รูปที่ 1) สมมติว่าเป้าหมาย A ขึ้นอยู่กับเป้าหมาย B ซึ่งขึ้นอยู่กับเป้าหมายไลบรารีทั่วไป C เป้าหมาย A ใช้คลาสที่ระบุในเป้าหมาย C ได้ใช่ไหม

ทรัพยากร Dependency แบบทรานซิทีฟ

รูปที่ 1 ทรัพยากร Dependency แบบทรานซิทีฟ

ทั้งนี้ในด้านของเครื่องมือพื้นฐาน ก็ไม่เป็นปัญหาใด เพราะทั้ง B และ C จะลิงก์ไปที่เป้าหมาย A เมื่อสร้าง ดังนั้นสัญลักษณ์ใดก็ตามที่กำหนดไว้ใน C นั้น A จะทราบดี Bazel ทำสิ่งนี้มานานหลายปี แต่เมื่อ Google เติบโตขึ้น เราก็เริ่มมองเห็นปัญหา สมมติว่า B ถูกเปลี่ยนโครงสร้างภายในโค้ดจนทำให้ไม่จำเป็นต้องใช้ C อีกต่อไป หาก Dependency ของ B ถูกนำออกไปแล้ว A และเป้าหมายอื่นๆ ที่ใช้ C ผ่านการอ้างอิง B จะใช้งานไม่ได้ ทรัพยากร Dependency ของเป้าหมายได้กลายเป็นส่วนหนึ่งของสัญญาสาธารณะอย่างมีประสิทธิภาพและจะไม่เปลี่ยนแปลงได้อย่างปลอดภัย ซึ่งหมายความว่าทรัพยากร Dependency ที่สะสมขึ้นเมื่อเวลาผ่านไปและบิลด์ที่ Google เริ่มช้าลง

ในที่สุด Google ก็ได้แก้ปัญหานี้ด้วยการเปิดตัว "โหมดการขึ้นต่อกันแบบทรานซิทีฟแบบเข้มงวด" ใน Bazel ในโหมดนี้ Bazel จะตรวจจับว่าเป้าหมายพยายามอ้างอิงสัญลักษณ์โดยไม่ขึ้นอยู่กับสัญลักษณ์โดยตรงหรือไม่ และหากทำแล้ว จะล้มเหลวโดยมีข้อผิดพลาดและคำสั่ง Shell ที่สามารถใช้แทรกการขึ้นต่อกันโดยอัตโนมัติ การเปิดตัวการเปลี่ยนแปลงนี้ทั่วทั้งฐานของโค้ดทั้งหมดของ Google และเปลี่ยนโครงสร้างภายในโค้ดเป้าหมายทั้งหมดหลายล้านรายการเพื่อให้แสดงการขึ้นต่อกันอย่างชัดเจนเป็นงานที่ต้องทำมาหลายปี แต่ก็คุ้มค่ามาก ตอนนี้บิลด์ของเราทำงานเร็วขึ้นมากเนื่องจากเป้าหมายมีทรัพยากร Dependency ที่ไม่จำเป็นน้อยลง และวิศวกรก็มีอำนาจในการนำทรัพยากร Dependency ที่ไม่ต้องการออกโดยไม่ต้องกังวลเกี่ยวกับการสูญเสียเป้าหมายที่ขึ้นอยู่กับทรัพยากรเหล่านั้น

และเช่นเคย การบังคับใช้ทรัพยากร Dependency แบบทรานซิชันที่เข้มงวด แต่ก็ต้องแลกกับการสร้างไฟล์ ทำให้การสร้างไฟล์มีรายละเอียดมากขึ้น เนื่องจากตอนนี้ไลบรารีที่ใช้บ่อยต้องแสดงอย่างชัดเจนในหลายตำแหน่ง แทนที่จะดึงเข้ามาโดยบังเอิญ และวิศวกรจำเป็นต้องทุ่มเทมากขึ้นในการเพิ่มทรัพยากร Dependency ให้กับไฟล์ BUILD จากนั้นเราได้พัฒนาเครื่องมือที่ลดการทำงานหนักนี้ด้วยการตรวจหาทรัพยากร Dependency ที่ขาดหายไปโดยอัตโนมัติและเพิ่มไปยังไฟล์ BUILD โดยนักพัฒนาซอฟต์แวร์ไม่ต้องดำเนินการใดๆ แต่ถึงจะไม่มีเครื่องมือดังกล่าว แต่เราก็พบข้อเสียที่คุ้มค่าเมื่อปรับขนาดฐานของโค้ด กล่าวคือ การเพิ่มทรัพยากร Dependency ไปยังไฟล์ BUILD อย่างชัดแจ้งเป็นค่าใช้จ่ายแบบครั้งเดียว แต่การจัดการกับทรัพยากร Dependency แบบโดยนัยอาจทำให้เกิดปัญหาต่อเนื่องตราบใดที่มีเป้าหมายของบิลด์อยู่ Bazel บังคับใช้ทรัพยากร Dependency แบบทรานซิทีฟแบบเข้มงวดในโค้ด Java โดยค่าเริ่มต้น

ทรัพยากร Dependency ภายนอก

หากทรัพยากร Dependency ไม่ใช่ทรัพยากรภายใน ก็ต้องเป็นทรัพยากรภายนอก ทรัพยากร Dependency ภายนอกคืออาร์ติแฟกต์ที่สร้างขึ้นและจัดเก็บไว้นอกระบบบิลด์ ระบบจะนำเข้าการขึ้นต่อกันโดยตรงจากที่เก็บอาร์ติแฟกต์ (โดยทั่วไปจะเข้าถึงผ่านอินเทอร์เน็ต) และใช้งานตามที่เป็นอยู่แทนที่จะสร้างจากต้นทาง ความแตกต่างที่สำคัญที่สุดอย่างหนึ่งระหว่างทรัพยากร Dependency ภายนอกและภายในคือทรัพยากร Dependency ภายนอกนั้นมีเวอร์ชัน และเวอร์ชันเหล่านั้นมีอยู่อย่างอิสระจากซอร์สโค้ดของโปรเจ็กต์

การจัดการทรัพยากร Dependency อัตโนมัติเทียบกับการจัดการทรัพยากรด้วยตนเอง

ระบบบิลด์สามารถอนุญาตให้จัดการเวอร์ชันทรัพยากร Dependency ภายนอกได้ทั้งแบบด้วยตัวเองหรือโดยอัตโนมัติ เมื่อจัดการด้วยตนเองแล้ว ไฟล์บิลด์จะแสดงเวอร์ชันที่ต้องการดาวน์โหลดจากที่เก็บอาร์ติแฟกต์อย่างชัดแจ้ง ซึ่งมักจะใช้สตริงเวอร์ชันเชิงความหมาย เช่น 1.1.4 เมื่อมีการจัดการโดยอัตโนมัติ ไฟล์แหล่งที่มาจะระบุช่วงของเวอร์ชันที่ยอมรับได้ และระบบของบิลด์จะดาวน์โหลดเวอร์ชันล่าสุดเสมอ ตัวอย่างเช่น Gradle อนุญาตให้ประกาศเวอร์ชันทรัพยากร Dependency เป็น "1.+" เพื่อระบุว่าทรัพยากร Dependency ในเวอร์ชันย่อยหรือเวอร์ชันแพตช์นั้นยอมรับได้ตราบใดที่เวอร์ชันหลักเป็น 1

ทรัพยากร Dependency ที่มีการจัดการโดยอัตโนมัติเป็นวิธีที่ช่วยอำนวยความสะดวกสำหรับโปรเจ็กต์ขนาดเล็ก แต่ทรัพยากรประเภทนี้มักจะเป็นสูตรลับในการทำให้เกิดภัยพิบัติในโปรเจ็กต์ขนาดเล็กหรือวิศวกรมากกว่า 1 คน ปัญหาของทรัพยากร Dependency ที่จัดการโดยอัตโนมัติคือคุณจะควบคุมเวลาที่อัปเดตเวอร์ชันไม่ได้ ไม่มีอะไรรับประกันได้ว่าบุคคลภายนอกจะไม่ทำการอัปเดตที่ส่งผลกับส่วนอื่นในระบบ (แม้ว่าจะอ้างว่าใช้การกำหนดเวอร์ชันทางอรรถศาสตร์ก็ตาม) ดังนั้นการสร้างเวอร์ชันที่ใช้ไปได้ในวันหนึ่งก็อาจเสียหายในวันถัดไป และไม่สามารถตรวจหาสิ่งที่เปลี่ยนแปลงหรือย้อนกลับสู่สถานะการทำงานได้ แม้ว่าบิลด์จะไม่เสียหาย แต่ก็อาจมีลักษณะการทำงานที่เป็นส่วนเล็กๆ หรือการเปลี่ยนแปลงด้านประสิทธิภาพที่ไม่สามารถหาเจอได้

ในทางตรงกันข้าม เนื่องจากทรัพยากร Dependency ที่จัดการด้วยตนเองจำเป็นต้องมีการเปลี่ยนแปลงในการควบคุมแหล่งที่มา ทำให้คุณค้นพบและย้อนกลับได้อย่างง่ายดาย รวมถึงคุณอาจลองใช้ที่เก็บเวอร์ชันเก่าเพื่อสร้างด้วยทรัพยากร Dependency เก่าได้ Bazel กำหนดให้ต้องระบุเวอร์ชันของทรัพยากร Dependency ทั้งหมดด้วยตนเอง ไม่ว่าจะเป็นระดับปานกลาง ค่าใช้จ่ายที่คุ้มค่าในการจัดการเวอร์ชันด้วยตนเองก็คุ้มค่ากับความเสถียรที่มี

กฎหนึ่งเวอร์ชัน

ไลบรารีเวอร์ชันต่างๆ มักจะแสดงด้วยอาร์ติแฟกต์ที่ต่างกัน ดังนั้นตามทฤษฎีแล้ว จึงไม่มีเหตุผลว่าเวอร์ชันต่างๆ ของการอ้างอิงภายนอกที่เหมือนกันจะประกาศทั้ง 2 เวอร์ชันในระบบบิลด์โดยใช้ชื่อต่างกันไม่ได้ วิธีนี้จะช่วยให้เป้าหมายแต่ละรายการเลือกเวอร์ชันทรัพยากร Dependency ที่ต้องการใช้ได้ วิธีนี้ทำให้เกิดปัญหามากมายในทางปฏิบัติ Google จึงบังคับใช้กฎเวอร์ชันเดียวที่เข้มงวดสำหรับทรัพยากร Dependency ของบุคคลที่สามทั้งหมดในโค้ดเบสของเรา

ปัญหาที่ใหญ่ที่สุดในการอนุญาตให้มีหลายเวอร์ชันคือปัญหาการขึ้นต่อกันของเพชร สมมติว่าเป้าหมาย A ขึ้นอยู่กับเป้าหมาย B และ v1 ของไลบรารีภายนอก หากระบบเปลี่ยนโครงสร้างภายในโค้ดเป้าหมาย B ในภายหลังเพื่อเพิ่มทรัพยากร Dependency ในไลบรารีภายนอก v2 เดียวกัน เป้าหมาย A จะใช้งานไม่ได้เนื่องจากตอนนี้ขึ้นอยู่กับไลบรารีเดียวกัน 2 เวอร์ชันโดยปริยาย แน่นอนว่าการเพิ่มทรัพยากร Dependency ใหม่จากเป้าหมายไปยังไลบรารีของบุคคลที่สามที่มีหลายเวอร์ชันนั้นไม่มีความปลอดภัย เพราะผู้ใช้เป้าหมายอาจเปลี่ยนไปใช้เวอร์ชันอื่นอยู่แล้ว การปฏิบัติตามกฎเวอร์ชันเดียวทำให้ความขัดแย้งนี้เป็นไปไม่ได้ หากเป้าหมายเพิ่มทรัพยากร Dependency ในไลบรารีของบุคคลที่สาม ทรัพยากร Dependency ที่มีอยู่ก็จะอยู่ในเวอร์ชันเดียวกันนั้นอยู่แล้ว เพื่อให้อยู่ร่วมกันได้อย่างมีความสุข

ทรัพยากร Dependency ภายนอกแบบทรานซิทีฟ

การจัดการกับทรัพยากร Dependency แบบทรานซิทีฟของทรัพยากร Dependency ภายนอกอาจเป็นเรื่องยากเป็นพิเศษ ที่เก็บอาร์ติแฟกต์จำนวนมาก เช่น Maven Central ช่วยให้อาร์ติแฟกต์ระบุทรัพยากร Dependency ของอาร์ติแฟกต์อื่นๆ บางเวอร์ชันในที่เก็บได้ สร้างเครื่องมืออย่าง Maven หรือ Gradle มักดาวน์โหลดทรัพยากร Dependency แบบสับเปลี่ยนแต่ละรายการซ้ำๆ โดยค่าเริ่มต้น ซึ่งหมายความว่าการเพิ่มทรัพยากร Dependency เพียง 1 รายการในโปรเจ็กต์อาจทำให้มีการดาวน์โหลดอาร์ติแฟกต์ทั้งหมดหลายสิบรายการ

วิธีนี้สะดวกมาก: เมื่อเพิ่มทรัพยากร Dependency ในไลบรารีใหม่ การต้องติดตามทรัพยากร Dependency แบบทรานซิทีฟของไลบรารีแต่ละรายการและเพิ่มทรัพยากรทั้งหมดด้วยตนเองเป็นเรื่องยุ่งยาก แต่ก็มีข้อเสียสำคัญเช่นกัน เนื่องจากไลบรารีที่แตกต่างกันอาจขึ้นอยู่กับเวอร์ชันต่างๆ ของไลบรารีของบุคคลที่สามเดียวกัน กลยุทธ์นี้จำเป็นต้องละเมิดกฎเวอร์ชันเดียว และนำไปสู่ปัญหาการขึ้นต่อกันระดับเพชร หากเป้าหมายของคุณขึ้นอยู่กับไลบรารีภายนอก 2 รายการที่ใช้ทรัพยากร Dependency เดียวกันแต่เวอร์ชันต่างกัน จะไม่มีการบอกว่าคุณจะได้ไฟล์ใด นอกจากนี้ยังหมายความว่าการอัปเดตทรัพยากร Dependency ภายนอกอาจทำให้เกิดความล้มเหลวที่ดูเหมือนจะไม่เกี่ยวข้องกันทั่วทั้งฐานของโค้ด หากเวอร์ชันใหม่เริ่มดึงทรัพยากร Dependency บางรายการในเวอร์ชันที่ขัดแย้งกัน

ด้วยเหตุนี้ Bazel จึงไม่ดาวน์โหลดทรัพยากร Dependency แบบทรานซิทีฟโดยอัตโนมัติ และน่าเสียดายที่ไม่มีหัวข้อเงิน ทางเลือกของ Bazel คือต้องการไฟล์ส่วนกลางที่แสดงทรัพยากร Dependency ภายนอกของที่เก็บทุกรายการและเวอร์ชันที่ชัดเจนซึ่งใช้สำหรับทรัพยากร Dependency ดังกล่าวทั่วทั้งที่เก็บ โชคดีที่ Bazel มีเครื่องมือที่สามารถสร้างไฟล์ดังกล่าวโดยอัตโนมัติ ซึ่งจะมีทรัพยากร Dependency แบบทรานซิทีฟของชุดอาร์ติแฟกต์ Maven เครื่องมือนี้จะทำงานได้ 1 ครั้งเพื่อสร้างไฟล์ WORKSPACE เริ่มต้นสำหรับโปรเจ็กต์ แล้วอัปเดตไฟล์ดังกล่าวด้วยตนเองเพื่อปรับเวอร์ชันของทรัพยากร Dependency แต่ละรายการได้

ขอย้ำอีกครั้งว่าทางเลือกนี้อยู่ระหว่างความสะดวกสบายและความสามารถในการปรับขนาด โปรเจ็กต์ขนาดเล็กอาจไม่ต้องการกังวลเรื่องการจัดการทรัพยากร Dependency แบบทรานซิทีฟด้วยตนเอง และอาจใช้การขึ้นต่อกันแบบทรานซิทีฟแบบอัตโนมัติได้ กลยุทธ์นี้จะน่าสนใจน้อยลงเมื่อองค์กรและฐานของโค้ดเติบโตขึ้น เกิดความขัดแย้งและผลลัพธ์ที่ไม่คาดคิดมากขึ้นเรื่อยๆ สำหรับขนาดใหญ่ การจัดการทรัพยากร Dependency ด้วยตนเองนั้นต่ำกว่าค่าใช้จ่ายในการจัดการปัญหาที่เกิดจากการจัดการทรัพยากร Dependency โดยอัตโนมัติเป็นอย่างมาก

การแคชผลลัพธ์ของบิลด์โดยใช้ทรัพยากร Dependency ภายนอก

ทรัพยากร Dependency ภายนอกมักจะให้บริการโดยบุคคลที่สามที่เผยแพร่ไลบรารีเวอร์ชันเสถียร โดยอาจไม่มีการให้ซอร์สโค้ด นอกจากนี้ องค์กรบางแห่งอาจเลือกให้โค้ดบางส่วนของตนพร้อมใช้งานเป็นอาร์ติแฟกต์ ซึ่งทำให้โค้ดอื่นๆ อิงตามบุคคลที่สาม ไม่ใช่ทรัพยากร Dependency ภายใน ในทางทฤษฎี เราสามารถเร่งให้บิลด์ได้เร็วขึ้นหากอาร์ติแฟกต์สร้างได้ช้าแต่ดาวน์โหลดได้เร็ว

อย่างไรก็ตาม การดำเนินการนี้ยังเพิ่มค่าใช้จ่ายและความซับซ้อนอย่างมากด้วย เนื่องจากต้องมีคนรับผิดชอบการสร้างอาร์ติแฟกต์เหล่านั้นแต่ละรายการและอัปโหลดไปยังที่เก็บอาร์ติแฟกต์ และลูกค้าก็ต้องคอยอัปเดตให้เป็นปัจจุบันอยู่เสมอด้วยเวอร์ชันล่าสุด นอกจากนี้ การแก้ไขข้อบกพร่องยังทำได้ยากขึ้นมากเนื่องจากส่วนต่างๆ ของระบบสร้างขึ้นจากจุดต่างๆ ในที่เก็บ และไม่มีมุมมองที่สอดคล้องกันของโครงสร้างซอร์สอีกต่อไป

วิธีที่ดีกว่าในการแก้ปัญหาอาร์ติแฟกต์ที่ใช้เวลานานในการสร้างคือการใช้ระบบบิลด์ที่รองรับการแคชจากระยะไกล ตามที่อธิบายไว้ก่อนหน้านี้ ระบบบิลด์ดังกล่าวจะบันทึกอาร์ติแฟกต์ที่ได้จากทุกบิลด์ไปยังตำแหน่งที่แชร์กันระหว่างวิศวกร ดังนั้นหากนักพัฒนาซอฟต์แวร์อาศัยอาร์ติแฟกต์ที่ผู้อื่นสร้างขึ้นเมื่อเร็วๆ นี้ ระบบของบิลด์จะดาวน์โหลดอาร์ติแฟกต์ดังกล่าวโดยอัตโนมัติแทนที่จะสร้างขึ้นมา การทำเช่นนี้มีประโยชน์ด้านประสิทธิภาพทั้งหมด ซึ่งขึ้นอยู่กับอาร์ติแฟกต์โดยตรง ในขณะที่ยังคงดูแลให้บิลด์มีความสอดคล้องกันราวกับว่าสร้างขึ้นจากแหล่งที่มาเดียวกันเสมอ นี่เป็นกลยุทธ์ที่ Google ใช้ภายใน และกำหนดค่า Bazel ให้ใช้แคชระยะไกลได้

ความปลอดภัยและความน่าเชื่อถือของทรัพยากร Dependency ภายนอก

ทั้งนี้ขึ้นอยู่กับอาร์ติแฟกต์จากแหล่งที่มาของบุคคลที่สามซึ่งมีความเสี่ยงอยู่แล้วโดยธรรมชาติ มีความเสี่ยงด้านความพร้อมใช้งานหากแหล่งที่มาของบุคคลที่สาม (เช่น ที่เก็บอาร์ติแฟกต์) ใช้งานไม่ได้ เนื่องจากบิลด์ทั้งหมดอาจหยุดทำงานหากดาวน์โหลดทรัพยากร Dependency ภายนอกไม่ได้ นอกจากนี้ ยังมีความเสี่ยงด้านความปลอดภัยด้วย เช่น หากระบบของบุคคลที่สามถูกบุกรุกโดยผู้โจมตี ผู้โจมตีอาจแทนที่อาร์ติแฟกต์ที่อ้างอิงด้วยการออกแบบของตนเองได้ ซึ่งจะทำให้ผู้โจมตีแทรกโค้ดที่กำหนดเองลงในบิลด์ได้ ปัญหาทั้ง 2 อย่างสามารถบรรเทาลงได้ด้วยการมิเรอร์อาร์ติแฟกต์ที่คุณ ต้องใช้ในเซิร์ฟเวอร์ที่คุณควบคุม และบล็อกระบบบิลด์ไม่ให้เข้าถึงที่เก็บอาร์ติแฟกต์ของบุคคลที่สาม เช่น Maven Central ข้อดีก็คือกระจกเหล่านี้ต้องใช้ความพยายามและทรัพยากรในการรักษา ดังนั้นการเลือกว่าจะใช้กระจกเหล่านี้หรือไม่จึงมักขึ้นอยู่กับขนาดของโปรเจ็กต์ นอกจากนี้ ปัญหาด้านความปลอดภัยยังสามารถป้องกันได้อย่างสมบูรณ์โดยมีค่าใช้จ่ายเล็กน้อยโดยการกำหนดให้ต้องระบุแฮชของอาร์ติแฟกต์ของบุคคลที่สามแต่ละรายการในที่เก็บต้นทาง ซึ่งจะทำให้บิลด์ล้มเหลวหากมีการเปลี่ยนแปลงอาร์ติแฟกต์ อีกทางเลือกหนึ่งที่ทำให้เกิดปัญหาอย่างสมบูรณ์คือผู้ให้บริการทรัพยากร Dependency ของโปรเจ็กต์ เมื่อผู้ให้บริการมีทรัพยากร Dependency ของโปรเจ็กต์แล้ว ผู้ให้บริการจะตรวจสอบการควบคุมแหล่งที่มาควบคู่ไปกับซอร์สโค้ดของโปรเจ็กต์ ไม่ว่าจะเป็นต้นทางหรือไบนารี ซึ่งหมายความว่าทรัพยากร Dependency ภายนอกทั้งหมดของโปรเจ็กต์จะเปลี่ยนเป็นทรัพยากร Dependency ภายในได้อย่างมีประสิทธิภาพ Google ใช้วิธีการนี้เป็นการภายในโดยตรวจสอบไลบรารีของบุคคลที่สามทุกรายการที่อ้างอิงทั่วทั้ง Google ลงในไดเรกทอรี third_party ที่รูทของโครงสร้างซอร์สของ Google อย่างไรก็ตาม วิธีนี้ได้ผลกับ Google เท่านั้นเนื่องจากระบบควบคุมแหล่งที่มาของ Google สร้างขึ้นมาโดยเฉพาะเพื่อรองรับการผูกขาดขนาดใหญ่มาก ดังนั้นผู้ให้บริการจึงอาจไม่ใช่ตัวเลือกสำหรับองค์กรทั้งหมด