ผู้ปฏิบัติงานถาวร

หน้านี้จะอธิบายวิธีใช้ Worker แบบถาวร รวมถึงประโยชน์ ข้อกำหนด และวิธีที่ Worker ส่งผลต่อการแซนด์บ็อกซ์

Worker แบบถาวรคือกระบวนการที่ทำงานเป็นเวลานานซึ่งเซิร์ฟเวอร์ Bazel เริ่มต้นขึ้น โดยทำหน้าที่เป็น Wrapper รอบ เครื่องมือจริง (โดยปกติจะเป็นคอมไพเลอร์) หรือเป็น เครื่องมือเอง เครื่องมือต้องรองรับการคอมไพล์ตามลำดับ และ Wrapper ต้องแปลระหว่าง API ของเครื่องมือกับรูปแบบคำขอ/การตอบสนองที่อธิบายไว้ด้านล่างเพื่อให้ได้รับประโยชน์จาก Worker แบบถาวร ระบบอาจเรียก Worker รายเดียวกันโดยมีและไม่มีแฟล็ก --persistent_worker ในบิวด์เดียวกัน และ Worker มีหน้าที่เริ่มต้นและสื่อสารกับเครื่องมืออย่างเหมาะสม รวมถึงปิด Worker เมื่อออกจากระบบ ระบบจะกำหนดไดเรกทอรีการทำงานแยกต่างหากให้กับอินสแตนซ์ Worker แต่ละรายการภายใต้ <outputBase>/bazel-workers (แต่ไม่ได้ chroot)

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

เราได้ใช้ Worker แบบถาวรกับหลายภาษา รวมถึง Java, Scala, Kotlin และอื่นๆ

โปรแกรมที่ใช้รันไทม์ NodeJS สามารถใช้ไลบรารีตัวช่วย @bazel/worker เพื่อ ใช้โปรโตคอล Worker ได้

การใช้ Worker แบบถาวร

Bazel 0.27 ขึ้นไป จะใช้ Worker แบบถาวรโดยค่าเริ่มต้นเมื่อดำเนินการบิวด์ แม้ว่าการดำเนินการระยะไกล จะมีความสำคัญมากกว่า สำหรับ Actions ที่ไม่รองรับ Worker แบบถาวร Bazel จะกลับไปเริ่มต้นอินสแตนซ์เครื่องมือสำหรับการดำเนินการแต่ละรายการ คุณสามารถตั้งค่าบิวด์ให้ใช้ 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 แต่ละรายการ ดูวิธีที่การกำหนดค่าการดำเนินการระบุแฟล็กการตั้งค่าได้ด้วยในส่วนถัดไป

คุณสามารถใช้ --high_priority_workers แฟล็กเพื่อระบุตัวย่อที่ควรเรียกใช้ก่อนตัวย่อที่มีลำดับความสำคัญปกติ ซึ่งจะช่วยจัดลำดับความสำคัญของการดำเนินการที่อยู่ในเส้นทางวิกฤตเสมอ หากมี Worker ที่มีลำดับความสำคัญสูง 2 รายการขึ้นไปกำลังดำเนินการคำขออยู่ ระบบจะป้องกันไม่ให้ Worker อื่นๆ ทำงาน คุณสามารถใช้แฟล็กนี้ได้หลายครั้ง

การส่งแฟล็ก --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" }
)

เมื่อใช้คำจำกัดความนี้ การใช้การดำเนินการนี้ครั้งแรกจะเริ่มต้นด้วยการเรียกใช้บรรทัดคำสั่ง /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 ด้วยตนเอง หากต้องการสื่อสารกับ 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 ให้ เข้าใจฟีเจอร์นี้

ใน ที่เก็บ GitHub นี้, คุณจะเห็น Wrapper Worker ตัวอย่างที่เขียนด้วย Java และ Python หากคุณ ทำงานใน JavaScript หรือ TypeScript แพ็กเกจ @bazel/worker และ ตัวอย่าง Worker ของ Node.js อาจเป็นประโยชน์

Worker ส่งผลต่อการแซนด์บ็อกซ์อย่างไร

การใช้กลยุทธ์ worker โดยค่าเริ่มต้นจะไม่เรียกใช้การดำเนินการใน แซนด์บ็อกซ์ ซึ่งคล้ายกับกลยุทs local คุณสามารถตั้งค่าแฟล็ก --worker_sandboxing เพื่อเรียกใช้ Worker ทั้งหมดภายในแซนด์บ็อกซ์ เพื่อให้แน่ใจว่าการดำเนินการเครื่องมือแต่ละครั้งจะเห็นเฉพาะไฟล์อินพุตที่ควรมี เครื่องมืออาจยังคงรั่วไหลข้อมูลระหว่างคำขอภายใน เช่น ผ่านแคช การใช้กลยุทธ์ dynamic กำหนดให้ Worker ต้องอยู่ในแซนด์บ็อกซ์

ระบบจะส่งไดเจสต์ไปพร้อมกับไฟล์อินพุตแต่ละไฟล์เพื่อให้ใช้แคชคอมไพเลอร์กับ Worker ได้อย่างถูกต้อง คอมไพเลอร์หรือ Wrapper จึงตรวจสอบได้ว่าอินพุตยังคงถูกต้องหรือไม่โดยไม่ต้องอ่านไฟล์

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

Worker แบบมัลติเพล็กซ์จะอยู่ในแซนด์บ็อกซ์ได้ก็ต่อเมื่อการใช้งาน Worker รองรับ และต้องเปิดใช้การแซนด์บ็อกซ์นี้แยกกันด้วยแฟล็ก --experimental_worker_multiplex_sandboxing ดูรายละเอียดเพิ่มเติมได้ใน เอกสารการออกแบบ)

อ่านเพิ่มเติม

ดูข้อมูลเพิ่มเติมเกี่ยวกับ Worker แบบถาวรได้ที่