หน้านี้จะอธิบายวิธีใช้ Worker แบบถาวร ประโยชน์ ข้อกำหนด และวิธีที่ Worker ส่งผลต่อการแซนด์บ็อกซ์
Worker แบบถาวรคือกระบวนการที่ทำงานเป็นเวลานานซึ่งเซิร์ฟเวอร์ Bazel เริ่มต้นขึ้น โดยทำหน้าที่เป็น Wrapper รอบๆ เครื่องมือจริง (โดยปกติจะเป็นคอมไพเลอร์) หรือเป็น เครื่องมือเอง เครื่องมือต้องรองรับการคอมไพล์ตามลำดับ และ Wrapper ต้องแปลระหว่าง API ของเครื่องมือกับรูปแบบคำขอ/การตอบสนองที่อธิบายไว้ด้านล่างเพื่อให้ได้รับประโยชน์จาก Worker แบบถาวร ระบบอาจเรียก Worker รายเดียวกันโดยมีและไม่มีแฟล็ก --persistent_worker ในบิลด์เดียวกัน และ Worker มีหน้าที่เริ่มต้นและสื่อสารกับเครื่องมืออย่างเหมาะสม รวมถึงปิด Worker เมื่อออกจากระบบ ระบบจะกำหนดไดเรกทอรีการทำงานแยกต่างหากภายใต้
<outputBase>/bazel-workers ให้กับอินสแตนซ์ Worker แต่ละรายการ
(แต่ไม่ได้ chroot)
การใช้ Worker แบบถาวรเป็น กลยุทธ์การดำเนินการที่ช่วยลด ค่าใช้จ่ายเริ่มต้น อนุญาตให้คอมไพล์ JIT มากขึ้น และเปิดใช้การแคช เช่น ต้นไม้ไวยากรณ์นามธรรมในการดำเนินการ กลยุทธ์นี้ช่วยปรับปรุงประสิทธิภาพโดยการส่งคำขอหลายรายการไปยังกระบวนการที่ทำงานเป็นเวลานาน
เราได้ใช้ Worker แบบถาวรกับหลายภาษา รวมถึง Java, Scala, Kotlin และอื่นๆ
โปรแกรมที่ใช้รันไทม์ NodeJS สามารถใช้ไลบรารีตัวช่วย @bazel/worker เพื่อ ใช้โปรโตคอล Worker ได้
การใช้ Worker แบบถาวร
Bazel 0.27 ขึ้นไป
ใช้ Worker แบบถาวรโดยค่าเริ่มต้นเมื่อดำเนินการบิลด์ แม้ว่าการดำเนินการระยะไกล
จะมีลำดับความสำคัญสูงกว่า สำหรับ Actions ที่ไม่รองรับ Worker แบบถาวร Bazel จะกลับไปเริ่มต้นอินสแตนซ์เครื่องมือสำหรับแต่ละ Action คุณสามารถตั้งค่าบิลด์ให้ใช้ Worker แบบถาวรอย่างชัดแจ้งได้โดยตั้งค่า
worker
กลยุทธ์สำหรับตัวย่อของเครื่องมือที่เกี่ยวข้อง
ตัวอย่างนี้รวมถึงการระบุ local เป็นกลยุทธ์สำรองสำหรับกลยุทธ์ worker ซึ่งเป็นแนวทางปฏิบัติแนะนำ
bazel build //my:target --strategy=Javac=worker,localการใช้กลยุทธ์ Worker แทนกลยุทธ์ Local สามารถเพิ่มความเร็วในการคอมไพล์ได้อย่างมาก ทั้งนี้ขึ้นอยู่กับการใช้งาน สำหรับ Java บิลด์อาจเร็วขึ้น 2-4 เท่า และบางครั้งอาจเร็วกว่านี้สำหรับการคอมไพล์แบบเพิ่ม การคอมไพล์ Bazel เร็วขึ้นประมาณ 2.5 เท่าเมื่อใช้ Worker ดูรายละเอียดเพิ่มเติมได้ที่ส่วน "การเลือกจำนวน Worker"
หากคุณมีสภาพแวดล้อมบิลด์ระยะไกลที่ตรงกับสภาพแวดล้อมบิลด์ในเครื่อง
คุณสามารถใช้กลยุทธ์
ไดนามิกแบบทดลอง,
ซึ่งจะแข่งกันระหว่างการดำเนินการระยะไกลกับการดำเนินการของ Worker หากต้องการเปิดใช้กลยุทธ์ไดนามิก
ให้ส่งแฟล็ก
--experimental_spawn_scheduler
กลยุทธ์นี้จะเปิดใช้ Worker โดยอัตโนมัติ คุณจึงไม่จำเป็นต้องระบุกลยุทธ์ worker แต่ยังคงใช้ local หรือ sandboxed เป็นกลยุทธ์สำรองได้
การเลือกจำนวน Worker
จำนวนอินสแตนซ์ Worker เริ่มต้นต่อตัวย่อคือ 4 แต่คุณสามารถปรับได้
ด้วย
worker_max_instances
แฟล็ก คุณต้องเลือกระหว่างการใช้ CPU ที่มีอยู่ให้เกิดประโยชน์สูงสุดกับจำนวนการคอมไพล์ JIT และการเข้าถึงแคชที่คุณได้รับ หากมี Worker มากขึ้น เป้าหมายจำนวนมากขึ้นจะต้องเสียค่าใช้จ่ายเริ่มต้นในการเรียกใช้โค้ดที่ไม่ใช่ JIT และเข้าถึงแคชที่ยังไม่ได้ใช้งาน หากมีเป้าหมายจำนวนน้อยที่จะสร้าง Worker รายเดียวอาจเป็นตัวเลือกที่ดีที่สุดในการเลือกระหว่างความเร็วในการคอมไพล์กับการใช้ทรัพยากร (เช่น ดู
ปัญหา #8586)
แฟล็ก worker_max_instances จะกำหนดจำนวนอินสแตนซ์ Worker สูงสุดต่อตัวย่อและชุดแฟล็ก (ดูด้านล่าง) ดังนั้นในระบบแบบผสม คุณอาจใช้หน่วยความจำจำนวนมากหากเก็บค่าเริ่มต้นไว้ สำหรับบิลด์แบบเพิ่ม ประโยชน์ของอินสแตนซ์ Worker หลายรายการจะน้อยลง
กราฟนี้แสดงเวลาในการคอมไพล์ตั้งแต่ต้นสำหรับ Bazel (เป้าหมาย //src:bazel) ในเวิร์กสเตชัน Linux Intel Xeon 3.5 GHz แบบไฮเปอร์เธรด 6 คอร์ที่มี RAM 64 GB เราจะเรียกใช้บิลด์เปล่า 5 รายการสำหรับการกำหนดค่า Worker แต่ละรายการ และใช้ค่าเฉลี่ยของ 4 รายการสุดท้าย

รูปที่ 1 กราฟแสดงการปรับปรุงประสิทธิภาพของบิลด์เปล่า
สำหรับการกำหนดค่านี้ Worker 2 รายจะคอมไพล์ได้เร็วที่สุด แม้ว่าจะมีการปรับปรุงเพียง 14% เมื่อเทียบกับ Worker 1 ราย Worker 1 รายเป็นตัวเลือกที่ดีหากคุณต้องการใช้หน่วยความจำน้อยลง
โดยปกติการคอมไพล์แบบเพิ่มจะได้รับประโยชน์มากยิ่งขึ้น บิลด์เปล่าค่อนข้างเกิดขึ้นไม่บ่อยนัก แต่การเปลี่ยนไฟล์เดียวระหว่างการคอมไพล์เป็นเรื่องปกติ โดยเฉพาะอย่างยิ่งในการพัฒนาแบบทดสอบเป็นตัวขับเคลื่อน ตัวอย่างข้างต้นยังมีการดำเนินการแพ็กเกจที่ไม่ใช่ Java ซึ่งอาจบดบังเวลาในการคอมไพล์แบบเพิ่ม
การคอมไพล์ซอร์สโค้ด Java เท่านั้น
(//src/main/java/com/google/devtools/build/lib/bazel:BazelServer_deploy.jar)
หลังจากเปลี่ยนค่าคงที่สตริงภายในใน
AbstractContainerizingSandboxedSpawn.java
จะทำให้เร็วขึ้น 3 เท่า (ค่าเฉลี่ยของบิลด์แบบเพิ่ม 20 รายการโดยทิ้งบิลด์วอร์มอัป 1 รายการ

รูปที่ 2 กราฟแสดงการปรับปรุงประสิทธิภาพของบิลด์แบบเพิ่ม
ความเร็วที่เพิ่มขึ้นจะขึ้นอยู่กับการเปลี่ยนแปลงที่เกิดขึ้น ในสถานการณ์ข้างต้น เราวัดความเร็วที่เพิ่มขึ้นได้ 6 เท่าเมื่อมีการเปลี่ยนแปลงค่าคงที่ที่ใช้กันโดยทั่วไป
การแก้ไข Worker แบบถาวร
คุณสามารถส่งแฟล็ก
--worker_extra_flag
เพื่อระบุแฟล็กเริ่มต้นให้กับ Worker โดยใช้ตัวย่อเป็นคีย์ เช่น การส่ง --worker_extra_flag=javac=--debug จะเปิดใช้การแก้ไขข้อบกพร่องสำหรับ Javac เท่านั้น
คุณตั้งค่าแฟล็ก Worker ได้เพียง 1 รายการต่อการใช้แฟล็กนี้ และสำหรับตัวย่อเพียง 1 รายการ
ระบบไม่ได้สร้าง Worker แยกกันสำหรับตัวย่อแต่ละรายการเท่านั้น แต่ยังสร้างสำหรับแฟล็กเริ่มต้นที่แตกต่างกันด้วย ระบบจะรวมตัวย่อและแฟล็กเริ่มต้นแต่ละรายการเข้าด้วยกันเป็น WorkerKey และสร้าง Worker ได้สูงสุด worker_max_instances รายการสำหรับ WorkerKey แต่ละรายการ ดูวิธีที่การกำหนดค่า Action สามารถระบุแฟล็กการตั้งค่าได้ในส่วนถัดไป
การส่งแฟล็ก
--worker_sandboxing
จะทำให้คำขอ Worker แต่ละรายการใช้ไดเรกทอรีแซนด์บ็อกซ์แยกกันสำหรับ
อินพุตทั้งหมด การตั้งค่าแซนด์บ็อกซ์ใช้เวลาเพิ่มขึ้นเล็กน้อย
โดยเฉพาะใน macOS แต่รับประกันความถูกต้องได้ดีขึ้น
แฟล็ก
--worker_quit_after_build
มีประโยชน์หลักๆ สำหรับการแก้ไขข้อบกพร่องและการสร้างโปรไฟล์ แฟล็กนี้จะบังคับให้ Worker ทั้งหมดออกจากระบบเมื่อบิลด์เสร็จสมบูรณ์ นอกจากนี้ คุณยังส่ง
--worker_verbose เพื่อ
รับเอาต์พุตเพิ่มเติมเกี่ยวกับสิ่งที่ Worker กำลังทำอยู่ได้ด้วย แฟล็กนี้จะแสดงในช่อง verbosity ใน WorkRequest ซึ่งช่วยให้การใช้งาน Worker มีรายละเอียดมากขึ้นด้วย
Worker จะจัดเก็บบันทึกในไดเรกทอรี <outputBase>/bazel-workers เช่น
ตัวอย่าง
/tmp/_bazel_larsrc/191013354bebe14fdddae77f2679c3ef/bazel-workers/worker-1-Javac.log
ชื่อไฟล์จะมีรหัส Worker และตัวย่อ เนื่องจากตัวย่อ 1 รายการอาจมี WorkerKey มากกว่า 1 รายการ คุณจึงอาจเห็นไฟล์บันทึกมากกว่า worker_max_instances รายการสำหรับตัวย่อที่กำหนด
สำหรับบิลด์ Android โปรดดูรายละเอียดที่ หน้าประสิทธิภาพของบิลด์ Android
การใช้ Worker แบบถาวร
ดูข้อมูลเพิ่มเติมเกี่ยวกับวิธีสร้าง Worker ได้ที่หน้า การสร้าง Worker แบบถาวร
ตัวอย่างนี้แสดงการกำหนดค่า Starlark สำหรับ Worker ที่ใช้ JSON
args_file = ctx.actions.declare_file(ctx.label.name + "_args_file")
ctx.actions.write(
output = args_file,
content = "\n".join(["-g", "-source", "1.5"] + ctx.files.srcs),
)
ctx.actions.run(
mnemonic = "SomeCompiler",
executable = "bin/some_compiler_wrapper",
inputs = inputs,
outputs = outputs,
arguments = [ "-max_mem=4G", "@%s" % args_file.path],
execution_requirements = {
"supports-workers" : "1", "requires-worker-protocol" : "json" }
)
เมื่อใช้คำจำกัดความนี้ การใช้ Action นี้ครั้งแรกจะเริ่มต้นด้วยการเรียกใช้บรรทัดคำสั่ง /bin/some_compiler -max_mem=4G --persistent_worker จากนั้นคำขอให้คอมไพล์ Foo.java จะมีลักษณะดังนี้
หมายเหตุ: แม้ว่าข้อกำหนดโปรโตคอลบัฟเฟอร์จะใช้ "snake case" (request_id) แต่โปรโตคอล JSON จะใช้ "camel case" (requestId) ในเอกสารนี้ เราจะใช้ camel case ในตัวอย่าง JSON แต่ใช้ snake case เมื่อพูดถึงช่องโดยไม่คำนึงถึงโปรโตคอล
{
"arguments": [ "-g", "-source", "1.5", "Foo.java" ]
"inputs": [
{ "path": "symlinkfarm/input1", "digest": "d49a..." },
{ "path": "symlinkfarm/input2", "digest": "093d..." },
],
}
Worker จะได้รับข้อมูลนี้ใน stdin ในรูปแบบ JSON ที่คั่นด้วยการขึ้นบรรทัดใหม่ (เนื่องจากตั้งค่า requires-worker-protocol เป็น JSON) จากนั้น Worker จะดำเนินการและส่ง WorkResponse ที่จัดรูปแบบ JSON ไปยัง stdout ของ Bazel จากนั้น Bazel จะแยกวิเคราะห์การตอบสนองนี้และแปลงเป็นการตอบสนอง WorkResponse ด้วยตนเอง หากต้องการสื่อสารกับ Worker ที่เชื่อมโยงโดยใช้โปรโตคอลบัฟเฟอร์ที่เข้ารหัสแบบไบนารีแทน
JSON, requires-worker-protocol จะถูกตั้งค่าเป็น proto ดังนี้
execution_requirements = {
"supports-workers" : "1" ,
"requires-worker-protocol" : "proto"
}
หากคุณไม่ใส่ requires-worker-protocol ในข้อกำหนดการดำเนินการ Bazel จะตั้งค่าเริ่มต้นการสื่อสารของ Worker ให้ใช้โปรโตคอลบัฟเฟอร์
Bazel จะได้ WorkerKey จากตัวย่อและแฟล็กที่แชร์ ดังนั้นหากการกำหนดค่านี้อนุญาตให้เปลี่ยนพารามิเตอร์ max_mem ระบบจะสร้าง Worker แยกต่างหากสำหรับแต่ละค่าที่ใช้ ซึ่งอาจทำให้ใช้หน่วยความจำมากเกินไปหากใช้ค่าที่แตกต่างกันมากเกินไป
ปัจจุบัน Worker แต่ละรายการสามารถประมวลผลคำขอได้ครั้งละ 1 รายการเท่านั้น ฟีเจอร์ Worker แบบมัลติเพล็กซ์แบบทดลอง อนุญาตให้ใช้หลาย เธรด หากเครื่องมือพื้นฐานเป็นแบบมัลติเธรดและตั้งค่า Wrapper ให้ เข้าใจฟีเจอร์นี้
คุณดู Wrapper ของ Worker ตัวอย่างที่เขียนด้วย Java และ Python ได้ในที่เก็บ GitHub นี้ หากคุณ ทำงานใน JavaScript หรือ TypeScript แพ็กเกจ @bazel/worker และ ตัวอย่าง Worker ของ Node.js อาจเป็นประโยชน์
Worker ส่งผลต่อการแซนด์บ็อกซ์อย่างไร
การใช้กลยุทธ์ worker โดยค่าเริ่มต้นจะไม่เรียกใช้ Action ใน
แซนด์บ็อกซ์ ซึ่งคล้ายกับกลยุทธ์ local คุณสามารถตั้งค่าแฟล็ก --worker_sandboxing เพื่อเรียกใช้ Worker ทั้งหมดภายในแซนด์บ็อกซ์ เพื่อให้แน่ใจว่าการดำเนินการเครื่องมือแต่ละครั้งจะเห็นเฉพาะไฟล์อินพุตที่ควรมี เครื่องมืออาจยังคงรั่วไหลข้อมูลระหว่างคำขอภายใน เช่น ผ่านแคช การใช้กลยุทธ์ dynamic
กำหนดให้ Worker ต้องอยู่ในแซนด์บ็อกซ์
ระบบจะส่งไดเจสต์ไปพร้อมกับไฟล์อินพุตแต่ละไฟล์เพื่อให้ใช้แคชของคอมไพเลอร์กับ Worker ได้อย่างถูกต้อง คอมไพเลอร์หรือ Wrapper จึงตรวจสอบได้ว่าอินพุตยังคงถูกต้องหรือไม่โดยไม่ต้องอ่านไฟล์
แม้จะใช้ไดเจสต์อินพุตเพื่อป้องกันการแคชที่ไม่ต้องการ แต่ Worker ที่อยู่ในแซนด์บ็อกซ์จะมีการแซนด์บ็อกซ์ที่เข้มงวดน้อยกว่าแซนด์บ็อกซ์แบบเพียว เนื่องจากเครื่องมืออาจเก็บสถานะภายในอื่นๆ ที่ได้รับผลกระทบจากคำขอก่อนหน้า
Worker แบบมัลติเพล็กซ์จะอยู่ในแซนด์บ็อกซ์ได้ก็ต่อเมื่อการใช้งาน Worker รองรับ และคุณต้องเปิดใช้การแซนด์บ็อกซ์นี้แยกกันด้วยแฟล็ก --experimental_worker_multiplex_sandboxing ดูรายละเอียดเพิ่มเติมได้ใน
เอกสารการออกแบบ)
อ่านเพิ่มเติม
ดูข้อมูลเพิ่มเติมเกี่ยวกับ Worker แบบถาวรได้ที่