หน้านี้จะอธิบายวิธีใช้ Worker แบบถาวร ประโยชน์ ข้อกำหนด และ วิธีที่ Worker ส่งผลต่อการแซนด์บ็อกซ์
Worker แบบถาวรคือกระบวนการที่ทำงานเป็นเวลานานซึ่งเซิร์ฟเวอร์ Bazel เริ่มต้น โดยจะทำหน้าที่เป็นWrapper รอบเครื่องมือจริง (โดยปกติคือคอมไพเลอร์) หรือเป็นเครื่องมือเอง หากต้องการใช้ประโยชน์จาก Worker แบบถาวร เครื่องมือต้อง
รองรับการคอมไพล์ตามลำดับ และ Wrapper ต้องแปล
ระหว่าง API ของเครื่องมือกับรูปแบบคำขอ/คำตอบที่อธิบายไว้ด้านล่าง Worker เดียวกันอาจถูกเรียกใช้โดยมีและไม่มีแฟล็ก --persistent_worker
ในบิลด์เดียวกัน และมีหน้าที่รับผิดชอบในการเริ่มต้นและสื่อสารกับเครื่องมืออย่างเหมาะสม รวมถึงปิด Worker เมื่อออกจากระบบ ระบบจะกำหนดอินสแตนซ์ของ Worker แต่ละรายการ
(แต่ไม่ได้ chroot ไปยัง) ไดเรกทอรีการทำงานแยกต่างหากภายใต้
<outputBase>/bazel-workers
การใช้ Worker แบบถาวรเป็นกลยุทธ์การดำเนินการที่ช่วยลดค่าใช้จ่ายในการเริ่มต้น ช่วยให้คอมไพล์ JIT ได้มากขึ้น และเปิดใช้การแคช เช่น Abstract Syntax Tree ในการดำเนินการ กลยุทธ์นี้ ปรับปรุงประสิทธิภาพได้ด้วยการส่งคำขอหลายรายการไปยังกระบวนการที่ทำงานเป็นเวลานาน
มีการใช้งาน Worker แบบถาวรสำหรับหลายภาษา ซึ่งรวมถึง Java, Scala, Kotlin และอื่นๆ
โปรแกรมที่ใช้รันไทม์ NodeJS สามารถใช้ไลบรารีตัวช่วย @bazel/worker เพื่อ ใช้โปรโตคอล Worker ได้
การใช้ Worker แบบถาวร
Bazel 0.27 ขึ้นไป
จะใช้ Worker แบบถาวรโดยค่าเริ่มต้นเมื่อดำเนินการบิลด์ แม้ว่าการดำเนินการจากระยะไกล
จะมีความสำคัญเหนือกว่าก็ตาม สำหรับดำเนินการที่ไม่รองรับ Worker แบบถาวร
Bazel จะกลับไปเริ่มต้นอินสแตนซ์เครื่องมือสำหรับการดำเนินการแต่ละรายการ คุณสามารถ
ตั้งค่าบิลด์ให้ใช้ Worker แบบถาวรได้อย่างชัดเจนโดยการตั้งค่าworker
กลยุทธ์สำหรับ
ตัวช่วยจำเครื่องมือที่เกี่ยวข้อง แนวทางปฏิบัติแนะนำคือตัวอย่างนี้รวมถึงการระบุ local
เป็นกลยุทธ์worker
สำรอง
bazel build //my:target --strategy=Javac=worker,local
การใช้กลยุทธ์ Worker แทนกลยุทธ์ในเครื่องจะช่วยเพิ่มความเร็วในการคอมไพล์ได้อย่างมาก ทั้งนี้ขึ้นอยู่กับการติดตั้งใช้งาน สำหรับ Java การบิลด์จะเร็วขึ้น 2-4 เท่า และบางครั้งอาจเร็วกว่านี้สำหรับการคอมไพล์แบบเพิ่ม การคอมไพล์ Bazel จะเร็วขึ้นประมาณ 2.5 เท่าเมื่อใช้ Worker ดูรายละเอียดเพิ่มเติมได้ที่ส่วน "การเลือกจำนวน Worker"
หากคุณมีสภาพแวดล้อมการสร้างจากระยะไกลที่ตรงกับสภาพแวดล้อมการสร้างในเครื่อง คุณสามารถใช้กลยุทธ์แบบไดนามิก
เวอร์ชันทดลอง
ซึ่งจะแข่งกันระหว่างการดำเนินการจากระยะไกลและการดำเนินการของ Worker หากต้องการเปิดใช้กลยุทธ์แบบไดนามิก
ให้ส่ง Flag --experimental_spawn_scheduler
กลยุทธ์นี้จะเปิดใช้ Worker โดยอัตโนมัติ คุณจึงไม่จำเป็นต้องระบุกลยุทธ์ worker
แต่ยังคงใช้ local
หรือ sandboxed
เป็น
การสำรองได้
การเลือกจำนวนผู้ปฏิบัติงาน
จำนวนอินสแตนซ์ของ Worker ต่อ Mnemonic จะเป็น 4 โดยค่าเริ่มต้น แต่สามารถปรับได้ด้วยแฟล็ก worker_max_instances
การใช้ CPU ที่มีอยู่ให้เกิดประโยชน์สูงสุดและการคอมไพล์ JIT และการเข้าถึงแคชที่คุณได้รับนั้นมีข้อแลกเปลี่ยนกัน เมื่อมี Worker มากขึ้น เป้าหมายจำนวนมากขึ้นจะจ่ายค่าใช้จ่ายเริ่มต้นของการเรียกใช้โค้ดที่ไม่ได้ JIT และการเข้าถึงแคชเย็น
หากมีเป้าหมายจำนวนเล็กน้อยที่ต้องสร้าง ผู้ปฏิบัติงานคนเดียวอาจให้
การแลกเปลี่ยนที่ดีที่สุดระหว่างความเร็วในการคอมไพล์และการใช้ทรัพยากร (เช่น
ดูปัญหา #8586
แฟล็ก worker_max_instances
จะตั้งค่าจำนวนอินสแตนซ์ของ Worker สูงสุดต่อ
นิโมนิกและชุดแฟล็ก (ดูด้านล่าง) ดังนั้นในระบบแบบผสม คุณอาจใช้
หน่วยความจำค่อนข้างมากหากใช้ค่าเริ่มต้น สำหรับการสร้างที่เพิ่มขึ้น ประโยชน์ของอินสแตนซ์ Worker หลายรายการจะยิ่งน้อยลง
กราฟนี้แสดงเวลาในการคอมไพล์ตั้งแต่ต้นสำหรับ Bazel (เป้าหมาย
//src:bazel
) ในเวิร์กสเตชัน Linux ที่มี Intel Xeon 3.5 GHz แบบไฮเปอร์เธรด 6 คอร์
พร้อม RAM 64 GB สำหรับการกำหนดค่าผู้ปฏิบัติงานแต่ละรายการ ระบบจะเรียกใช้การสร้างที่สะอาด 5 ครั้งและใช้ค่าเฉลี่ยของการสร้าง 4 ครั้งล่าสุด
รูปที่ 1 กราฟการปรับปรุงประสิทธิภาพของการสร้างที่สะอาด
สำหรับการกำหนดค่านี้ ผู้ปฏิบัติงาน 2 คนจะให้การคอมไพล์ที่เร็วที่สุด แม้ว่าจะมีการปรับปรุงเพียง 14% เมื่อเทียบกับผู้ปฏิบัติงาน 1 คนก็ตาม Worker 1 รายการเป็นตัวเลือกที่ดีหากคุณต้องการ ใช้หน่วยความจำน้อยลง
โดยปกติแล้วการคอมไพล์แบบเพิ่มจะให้ประโยชน์มากยิ่งขึ้น การบิลด์ใหม่ทั้งหมดนั้น ค่อนข้างเกิดขึ้นได้ยาก แต่การเปลี่ยนไฟล์เดียวระหว่างการคอมไพล์นั้นเป็นเรื่องปกติ โดยเฉพาะอย่างยิ่งในการพัฒนาที่ขับเคลื่อนด้วยการทดสอบ ตัวอย่างข้างต้นยังมีการดำเนินการที่ไม่ใช่ Java packaging ซึ่งอาจบดบังเวลาในการคอมไพล์ที่เพิ่มขึ้น
การคอมไพล์แหล่งที่มาของ Java อีกครั้งเท่านั้น
(//src/main/java/com/google/devtools/build/lib/bazel:BazelServer_deploy.jar
)
หลังจากเปลี่ยนค่าคงที่สตริงภายในใน
AbstractContainerizingSandboxedSpawn.java
จะช่วยเพิ่มความเร็วได้ 3 เท่า (ค่าเฉลี่ยของการสร้างแบบเพิ่มทีละรายการ 20 รายการโดยทิ้งการสร้างแบบวอร์มอัพ 1 รายการ)
รูปที่ 2 กราฟการปรับปรุงประสิทธิภาพของการสร้างแบบเพิ่ม
การเร่งความเร็วจะขึ้นอยู่กับการเปลี่ยนแปลงที่ดำเนินการ การเพิ่มความเร็วขึ้น 6 เท่า วัดได้ในสถานการณ์ข้างต้นเมื่อมีการเปลี่ยนแปลงค่าคงที่ที่ใช้กันทั่วไป
การแก้ไขผู้ปฏิบัติงานแบบถาวร
คุณสามารถส่งแฟล็ก
--worker_extra_flag
เพื่อระบุแฟล็กเริ่มต้นไปยัง Worker โดยใช้คีย์เป็นมνηนิค เช่น การส่ง --worker_extra_flag=javac=--debug
จะเปิดการแก้ไขข้อบกพร่องสำหรับ Javac เท่านั้น
ตั้งค่าแฟล็กคนงานได้เพียง 1 รายการต่อการใช้แฟล็กนี้ และใช้ได้กับเฉพาะหนึ่งนิโมนิกเท่านั้น
ระบบไม่ได้สร้าง Worker แยกกันสำหรับแต่ละ Mnemonic เท่านั้น แต่ยังสร้างสำหรับ
รูปแบบต่างๆ ใน Flag การเริ่มต้นด้วย ระบบจะรวมชุดค่าผสมของ Mnemonic และแฟล็กการเริ่มต้น
เข้าด้วยกันเป็น WorkerKey
และสำหรับ WorkerKey
แต่ละรายการ ระบบจะสร้าง Worker ได้สูงสุด worker_max_instances
ราย ดูส่วนถัดไปเกี่ยวกับวิธีที่
การกําหนดค่าการกระทําระบุแฟล็กการตั้งค่าได้ด้วย
คุณสามารถใช้แฟล็ก
--high_priority_workers
เพื่อระบุตัวช่วยจำที่ควรเรียกใช้แทนตัวช่วยจำที่มีลำดับความสำคัญปกติ ซึ่งจะช่วยจัดลำดับความสำคัญของการดำเนินการที่อยู่ในเส้นทางวิกฤตเสมอ หากมีผู้ปฏิบัติงานที่มีลำดับความสำคัญสูง 2 คนขึ้นไปที่กำลังดำเนินการตามคำขอ ระบบจะป้องกันไม่ให้ผู้ปฏิบัติงานคนอื่นๆ ทำงาน คุณใช้เครื่องหมายนี้ได้หลายครั้ง
การส่งแฟล็ก
--worker_sandboxing
จะทำให้คำขอของ Worker แต่ละรายการใช้ไดเรกทอรีแซนด์บ็อกซ์แยกต่างหากสำหรับอินพุตทั้งหมด
การตั้งค่าแซนด์บ็อกซ์ต้องใช้เวลาเพิ่มขึ้นเล็กน้อย
โดยเฉพาะใน macOS แต่จะช่วยให้มั่นใจได้ถึงความถูกต้องมากขึ้น
โดย
--worker_quit_after_build
แฟล็กมีประโยชน์หลักๆ ในการแก้ไขข้อบกพร่องและการสร้างโปรไฟล์ โดยแฟล็กนี้จะบังคับให้ Worker ทั้งหมด
หยุดทำงานเมื่อบิลด์เสร็จสมบูรณ์ นอกจากนี้ คุณยังส่ง
--worker_verbose
ไปยัง
เพื่อรับเอาต์พุตเพิ่มเติมเกี่ยวกับสิ่งที่ Worker กำลังทำได้ด้วย โดยแฟล็กนี้จะแสดงในฟิลด์
verbosity
ใน WorkRequest
ซึ่งจะช่วยให้การใช้งาน Worker มีรายละเอียดมากขึ้นด้วย
ผู้ปฏิบัติงานจะจัดเก็บบันทึกไว้ในไดเรกทอรี <outputBase>/bazel-workers
เช่น
/tmp/_bazel_larsrc/191013354bebe14fdddae77f2679c3ef/bazel-workers/worker-1-Javac.log
ชื่อไฟล์มีรหัสผู้ปฏิบัติงานและ Mnemonic เนื่องจากมี WorkerKey
ได้มากกว่า 1 รายการต่อมnemonic คุณจึงอาจเห็นไฟล์บันทึกมากกว่า worker_max_instances
รายการสำหรับ mnemonic ที่กำหนด
สําหรับการสร้าง Android โปรดดูรายละเอียดที่หน้าประสิทธิภาพการสร้าง Android
การใช้งานผู้ปฏิบัติงานแบบถาวร
ดูข้อมูลเพิ่มเติมเกี่ยวกับวิธีสร้าง 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" }
)
เมื่อกำหนดค่าเช่นนี้ การใช้การดำเนินการนี้ครั้งแรกจะเริ่มต้นด้วยการเรียกใช้บรรทัดคำสั่ง /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 ไปยัง Bazel ใน stdout จากนั้น Bazel จะ
แยกวิเคราะห์การตอบกลับนี้และแปลงเป็น WorkResponse
proto ด้วยตนเอง หากต้องการสื่อสารกับ Worker ที่เชื่อมโยงโดยใช้ Protobuf ที่เข้ารหัสแบบไบนารีแทน JSON ให้ตั้งค่า requires-worker-protocol
เป็น proto
ดังนี้
execution_requirements = {
"supports-workers" : "1" ,
"requires-worker-protocol" : "proto"
}
หากคุณไม่ระบุ requires-worker-protocol
ในข้อกำหนดการดำเนินการ
Bazel จะตั้งค่าเริ่มต้นให้การสื่อสารของ Worker ใช้ Protobuf
Bazel จะสร้าง WorkerKey
จาก Mnemonic และ Flag ที่แชร์ ดังนั้นหากการกำหนดค่านี้อนุญาตให้เปลี่ยนพารามิเตอร์ max_mem
ระบบจะสร้าง Worker แยกต่างหากสำหรับแต่ละค่าที่ใช้ ซึ่งอาจทำให้ใช้หน่วยความจำมากเกินไปหากใช้ตัวแปรมากเกินไป
ปัจจุบันผู้ปฏิบัติงานแต่ละคนจะประมวลผลคำขอได้ครั้งละ 1 รายการเท่านั้น ฟีเจอร์Multiplex Worker เวอร์ชันทดลองช่วยให้ใช้หลายเธรดได้ หากเครื่องมือพื้นฐานเป็นแบบมัลติเธรดและตั้งค่า Wrapper ให้เข้าใจเรื่องนี้
ในที่เก็บ GitHub นี้ คุณจะเห็น Wrapper ของ Worker ตัวอย่างที่เขียนด้วย Java และ Python หากคุณทำงานใน JavaScript หรือ TypeScript แพ็กเกจ@bazel/worker และตัวอย่าง Worker ของ Node.js อาจมีประโยชน์
Worker ส่งผลต่อแซนด์บ็อกซ์อย่างไร
การใช้กลยุทธ์ worker
โดยค่าเริ่มต้นจะไม่เรียกใช้การดำเนินการในแซนด์บ็อกซ์ ซึ่งคล้ายกับกลยุทธ์ local
คุณตั้งค่า
--worker_sandboxing
เพื่อเรียกใช้ Worker ทั้งหมดภายในแซนด์บ็อกซ์ได้ เพื่อให้มั่นใจว่าการ
เรียกใช้เครื่องมือแต่ละครั้งจะเห็นเฉพาะไฟล์อินพุตที่ควรมี เครื่องมือ
อาจยังคงรั่วไหลข้อมูลระหว่างคำขอภายใน เช่น ผ่านแคช
การใช้dynamic
กลยุทธ์
กำหนดให้ต้องมีการแซนด์บ็อกซ์พนักงาน
หากต้องการอนุญาตให้ใช้แคชคอมไพเลอร์กับ Worker อย่างถูกต้อง ระบบจะส่งข้อมูลสรุปพร้อมกับไฟล์อินพุตแต่ละไฟล์ ดังนั้นคอมไพเลอร์หรือ Wrapper จึงตรวจสอบได้ว่าอินพุตยังคงถูกต้องหรือไม่โดยไม่ต้องอ่านไฟล์
แม้จะใช้ข้อมูลสรุปของอินพุตเพื่อป้องกันการแคชที่ไม่ต้องการ แต่ Sandbox Worker ก็มีการแซนด์บ็อกซ์ที่เข้มงวดน้อยกว่าแซนด์บ็อกซ์แบบเพียว เนื่องจากเครื่องมืออาจเก็บสถานะภายในอื่นๆ ที่ได้รับผลกระทบจากคำขอก่อนหน้า
Multiplex Worker จะอยู่ในแซนด์บ็อกซ์ได้ก็ต่อเมื่อการใช้งาน Worker รองรับ
และต้องเปิดใช้แซนด์บ็อกซ์นี้แยกต่างหากด้วย
แฟล็ก --experimental_worker_multiplex_sandboxing
ดูรายละเอียดเพิ่มเติมได้ใน
เอกสารการออกแบบ)
อ่านเพิ่มเติม
ดูข้อมูลเพิ่มเติมเกี่ยวกับพนักงานที่ทำงานอย่างต่อเนื่องได้ที่