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

หน้านี้จะอธิบายวิธีใช้ 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 ที่เชื่อมโยงโดยใช้ protobuf ที่เข้ารหัสแบบไบนารีแทน JSON, requires-worker-protocol จะถูกตั้งค่าเป็น proto ดังนี้

  execution_requirements = {
    "supports-workers" : "1" ,
    "requires-worker-protocol" : "proto"
  }

หากคุณไม่ใส่ requires-worker-protocol ในข้อกำหนดการดำเนินการ Bazel จะตั้งค่าเริ่มต้นการสื่อสาร Worker ให้ใช้ protobuf

Bazel จะได้ WorkerKey จากตัวย่อและแฟล็กที่แชร์ ดังนั้นหากการกำหนดค่านี้อนุญาตให้เปลี่ยนพารามิเตอร์ max_mem ระบบจะสร้าง Worker แยกกันสำหรับแต่ละค่าที่ใช้ ซึ่งอาจทำให้ใช้หน่วยความจำมากเกินไปหากมีการใช้ตัวแปรมากเกินไป

ปัจจุบัน Worker แต่ละรายการประมวลผลคำขอได้เพียงครั้งละ 1 รายการเท่านั้น ฟีเจอร์ Worker แบบมัลติเพล็กซ์แบบทดลอง อนุญาตให้ใช้หลาย เธรด หากเครื่องมือพื้นฐานเป็นแบบมัลติเธรดและตั้งค่า Wrapper ให้ เข้าใจฟีเจอร์นี้

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

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

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

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

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

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

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

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