ความท้าทายของกฎการเขียน

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

หน้านี้จะให้ภาพรวมระดับสูงของปัญหาและความท้าทาย ในการเขียนกฎ Bazel ที่มีประสิทธิภาพ

ข้อกำหนดในการสรุป

  • สมมติฐาน: มุ่งเน้นความถูกต้อง อัตราการส่งข้อมูล การใช้งานง่าย และเวลาในการตอบสนอง
  • สมมติฐาน: ที่เก็บขนาดใหญ่
  • สมมติฐาน: ภาษาคำอธิบายเหมือนกับ BUILD
  • ประวัติศาสตร์: การแยกอย่างชัดเจนระหว่างการโหลด การวิเคราะห์ และการดำเนินการล้าสมัยแล้ว แต่ยังคงส่งผลต่อ API
  • Intrinsic: การดำเนินการจากระยะไกลและการแคชเป็นเรื่องยาก
  • Intrinsic: การใช้ข้อมูลการเปลี่ยนแปลงเพื่อสร้างส่วนเพิ่มที่ถูกต้องและเร็ว ต้องใช้รูปแบบการเขียนโค้ดที่ผิดปกติ
  • ภายใน: การหลีกเลี่ยงเวลากำลังสองและการใช้หน่วยความจำนั้นเป็นเรื่องยาก

สมมติฐาน

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

มุ่งเน้นความถูกต้อง อัตราการส่งข้อมูล การใช้งานง่าย และเวลาในการตอบสนอง

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

เป้าหมายข้อที่สองของระบบบิลด์คือการมีอัตราการส่งข้อมูลสูง เราจะขยายขอบเขตของสิ่งที่ทำได้ภายในการจัดสรรเครื่องปัจจุบันสำหรับบริการดำเนินการระยะไกลอย่างถาวร หากบริการดำเนินการจากระยะไกลทำงานหนักเกินไป ก็จะไม่มีใครทำงานให้เสร็จได้

มาพร้อมกับความง่ายในการใช้งาน จากวิธีการที่ถูกต้องหลายแนวทางซึ่งมีพื้นที่เหมือนกัน (หรือคล้ายกัน) ของบริการการดำเนินการระยะไกล เราจะเลือกวิธีที่ใช้งานง่ายกว่า

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

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

ที่เก็บขนาดใหญ่

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

ภาษาคำอธิบายที่คล้ายกับ BUILD

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

ประวัติ

เวอร์ชัน Bazel ที่ก่อให้เกิดความท้าทายนั้นมีความแตกต่างกัน และบางเวอร์ชันจะอยู่ในส่วนต่อไปนี้

การแยกอย่างชัดเจนระหว่างการโหลด การวิเคราะห์ และการดำเนินการล้าสมัยแล้วแต่ยังคงส่งผลต่อ API

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

ซึ่งหมายความว่า API ของกฎต้องมีคำอธิบายที่ชัดเจนของอินเทอร์เฟซกฎ (แอตทริบิวต์ที่มี ประเภทของแอตทริบิวต์) มีข้อยกเว้นบางประการที่ API อนุญาตให้เรียกใช้โค้ดที่กำหนดเองระหว่างการโหลดเพื่อคำนวณชื่อโดยนัยของไฟล์เอาต์พุตและค่าโดยนัยของแอตทริบิวต์ ตัวอย่างเช่น กฎ java_library ชื่อ "foo" จะสร้างเอาต์พุตที่ชื่อ "libfoo.jar" โดยปริยาย ซึ่งสามารถอ้างอิงจากกฎอื่นๆ ในกราฟบิลด์

นอกจากนี้ การวิเคราะห์กฎไม่สามารถอ่านไฟล์ต้นฉบับหรือตรวจสอบเอาต์พุตของการดำเนินการได้ แต่จะต้องสร้างกราฟแบ่งสองส่วนที่มีทิศทางเพียงบางส่วนของขั้นตอนบิลด์และชื่อไฟล์เอาต์พุตที่ระบุจากตัวกฎเองและการอ้างอิงของกฎเท่านั้น

ภายใน

คุณสมบัติเฉพาะที่ทำให้การเขียนกฎเป็นเรื่องท้าทายและบางส่วนซึ่งพบได้บ่อยที่สุดจะอธิบายในส่วนต่อไปนี้

การดำเนินการและการแคชระยะไกลนั้นทำได้ยาก

การดำเนินการและการแคชระยะไกลช่วยเพิ่มเวลาบิลด์ในที่เก็บขนาดใหญ่ได้ประมาณ 2 ลำดับเมื่อเปรียบเทียบกับการเรียกใช้บิลด์บนเครื่องเดียว อย่างไรก็ตาม ปริมาณงานที่จำเป็นต้องดำเนินการนั้นลดหลั่นกันไป กล่าวคือ บริการการดำเนินการระยะไกลของ Google ได้รับการออกแบบมาให้รองรับคำขอจำนวนมหาศาลต่อวินาที และโปรโตคอลอย่างระมัดระวังเพื่อหลีกเลี่ยงการรับส่งข้อมูลไปกลับที่ไม่จำเป็น รวมถึงการทำงานที่ไม่จำเป็นในส่วนบริการ

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

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

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

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

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

แต่ Bazel จะใช้ Thread Pool ที่มีขนาดคงที่แทน อย่างไรก็ตาม ซึ่งหมายความว่าหากโหนดประกาศทรัพยากร Dependency ที่ยังไม่พร้อมใช้งาน เราอาจต้องล้มเลิกการประเมินนั้นและเริ่มการประเมินใหม่ (อาจดำเนินการในเทรดอื่น) เมื่อทรัพยากร Dependency พร้อมใช้งาน ในทางกลับกัน ซึ่งหมายความว่าโหนดไม่ควรทำงานนี้มากเกินไป โหนดที่มีการประกาศทรัพยากร Dependency แบบต่อเนื่องอาจเริ่มต้นใหม่ได้ N ครั้ง ซึ่งทำให้เสียเวลา O(N^2) เราตั้งใจที่จะประกาศการขึ้นต่อกันเป็นกลุ่มล่วงหน้าแทน ซึ่งบางครั้งก็ต้องมีการจัดระเบียบโค้ดใหม่ หรือแม้แต่การแยกโหนดออกเป็นหลายโหนดเพื่อจำกัดจำนวนการรีสตาร์ท

โปรดทราบว่าเทคโนโลยีนี้ยังไม่พร้อมใช้งานในกฎ API ในขณะนี้ แต่ API ของกฎยังคงใช้แนวคิดเดิมเกี่ยวกับระยะการโหลด การวิเคราะห์ และการดำเนินการ อย่างไรก็ตาม ข้อจำกัดพื้นฐานคือการเข้าถึงโหนดอื่นๆ ทั้งหมดจะต้องผ่านเฟรมเวิร์กเพื่อให้ติดตามทรัพยากร Dependency ที่เกี่ยวข้องได้ ผู้เขียนกฎต้องไม่ใช้ไลบรารีมาตรฐานหรือรูปแบบที่ข้าม Skyframe ไม่ว่าจะใช้ภาษาใดหรือเขียนกฎเป็นภาษาใด (ไม่จำเป็นต้องเหมือนกัน) สำหรับ Java ข้อมูลดังกล่าวหมายถึงการหลีกเลี่ยง java.io.File รวมถึงการสะท้อนทุกรูปแบบ และไลบรารีที่ทำเช่นนั้น ไลบรารีที่รองรับการแทรกทรัพยากร Dependency ของอินเทอร์เฟซระดับต่ำเหล่านี้ยังคงต้องตั้งค่าอย่างถูกต้องสำหรับ Skyframe

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

การหลีกเลี่ยงเวลากำลังสองและการใช้หน่วยความจำเป็นเรื่องยาก

นอกจากข้อกำหนดที่ Skyframe กำหนดแล้ว ข้อจำกัดในอดีตของการใช้ Java และความล้าสมัยของกฎ API แล้ว การนำเวลารูปสองหรือการใช้หน่วยความจำมาโดยไม่ตั้งใจเป็นปัญหาพื้นฐานในระบบบิลด์ที่อิงตามกฎไลบรารีและกฎไบนารี มีรูปแบบที่พบได้ทั่วไป 2 รูปแบบซึ่งทำให้เกิดการใช้หน่วยความจำกำลังสอง (จึงกินเวลากำลังสอง)

  1. กลุ่มของกฎห้องสมุด - ลองพิจารณากรณีของเชนกฎไลบรารี A ขึ้นอยู่กับข้อ B ขึ้นอยู่กับ C เป็นต้น จากนั้น เราต้องการคำนวณพร็อพเพอร์ตี้บางอย่างสำหรับการปิดแบบทรานซิทีฟของกฎเหล่านี้ เช่น คลาสพาธรันไทม์ของ Java หรือคำสั่ง C++ Linker สำหรับไลบรารีแต่ละรายการ เราอาจใช้รายการมาตรฐาน แต่ก็มีการใช้หน่วยความจำกำลังสองอยู่แล้ว ไลบรารีแรกมี 1 รายการบน classpath, 2 รายการ, 3 รายการ และอื่นๆ รวม 1+2+3+...+N = O(N^2)

  2. กฎไบนารีที่ขึ้นอยู่กับกฎไลบรารีเดียวกัน - ลองพิจารณากรณีที่ชุดไบนารีที่อิงตามกฎไลบรารีเดียวกัน เช่น ในกรณีที่คุณมีกฎทดสอบจำนวนหนึ่งที่ทดสอบโค้ดไลบรารีเดียวกัน สมมติว่าจากกฎ N กฎมีกฎครึ่งหนึ่งเป็นกฎไบนารี และกฎอีกครึ่งหนึ่งของไลบรารี ตอนนี้ให้พิจารณาว่าไบนารีแต่ละรายการจะทำสำเนาพร็อพเพอร์ตี้บางรายการที่คํานวณผ่านการปิดแบบทรานซิทีฟของกฎไลบรารี เช่น คลาสพาธรันไทม์ของ Java หรือบรรทัดคำสั่ง C++ Linker เช่น อาจขยายการแสดงสตริงบรรทัดคำสั่งของการดำเนินการลิงก์ C++ สำเนา N/2 ขององค์ประกอบ N/2 คือหน่วยความจำ O(N^2)

คลาสคอลเล็กชันที่กำหนดเองเพื่อหลีกเลี่ยงความซับซ้อนของกำลังสอง

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

น่าเสียดายที่การใช้งาน Depset ไม่ได้แก้ปัญหาทั้งหมดโดยอัตโนมัติ โดยเฉพาะแค่เพียงทำซ้ำทีละค่าในแต่ละกฎก็จะนำเวลากำลังสองกลับมาใช้งานอีกครั้ง นอกจากนี้ NestedSets ภายในยังมีวิธีการที่เป็นตัวช่วยบางอย่างเพื่ออำนวยความสะดวกในการทำงานร่วมกันกับคลาสคอลเล็กชันปกติ แต่การส่ง NestedSet ไปยังหนึ่งในวิธีการเหล่านี้โดยไม่ได้ตั้งใจทำให้เกิดการคัดลอกลักษณะการทำงานและกลับมาใช้หน่วยความจำกำลังสองอีกครั้ง