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

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

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

ข้อกำหนดโดยสรุป

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

สมมติฐาน

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

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

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

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

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

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

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

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

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

ภาษาของคำอธิบายที่คล้ายกับบิลด์

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

ประวัติ

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

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

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

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

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

ภายใน

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

การดำเนินการจากระยะไกลและการแคชเป็นเรื่องยาก

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

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

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

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

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

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

แต่ Bazel จะใช้พูลชุดข้อความที่มีขนาดคงที่แทน อย่างไรก็ตาม ซึ่งหมายความว่าหากโหนดประกาศทรัพยากร Dependency ที่ยังไม่พร้อมใช้งาน เราอาจต้องล้มเลิกการประเมินดังกล่าวและรีสตาร์ท (อาจอยู่ในเทรดอื่น) เมื่อทรัพยากร Dependency พร้อมใช้งาน ด้วยเหตุนี้ โหนดจึงไม่ควรทำสิ่งนี้มากเกินไป โหนดที่ประกาศทรัพยากร Dependency N ตามลำดับอาจรีสตาร์ทได้ 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 รายการในคลาสพาธ, 2, 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 ในช่วงหลายปีที่ผ่านมาคือการเปลี่ยนการใช้ดีพเซ็ตแทนการใช้สิ่งที่เคยใช้ก่อนหน้านี้

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