สวัสดีเพื่อนรัก! การรีสตาร์ท daemon ใน PHP โดยไม่สูญเสียการเชื่อมต่อกับมัน การสร้างสคริปต์ js

- คลาวด์ได้รับการออกแบบให้รันสคริปต์ PHP ต่างๆ ตามกำหนดเวลาหรือผ่าน API ตามกฎแล้ว สคริปต์เหล่านี้จะประมวลผลคิว และโหลดจะ "กระจาย" ไปยังเซิร์ฟเวอร์ประมาณ 100 เครื่อง ก่อนหน้านี้ เรามุ่งเน้นไปที่วิธีการใช้ตรรกะการควบคุม ซึ่งมีหน้าที่ในการกระจายโหลดอย่างสม่ำเสมอไปยังเซิร์ฟเวอร์จำนวนดังกล่าว และสร้างงานตามกำหนดเวลา แต่นอกเหนือจากนี้ เรายังจำเป็นต้องเขียน daemon ที่สามารถรันสคริปต์ PHP ของเราใน CLI และติดตามสถานะการดำเนินการได้

เดิมทีเขาเขียนด้วยภาษาซี เช่นเดียวกับปีศาจอื่นๆ ในบริษัทของเรา อย่างไรก็ตาม เราต้องเผชิญกับความจริงที่ว่าส่วนสำคัญของเวลาประมวลผล (ประมาณ 10%) สูญเปล่าไปโดยเปล่าประโยชน์: การเปิดตัวล่ามและการโหลด "แกนกลาง" ของเฟรมเวิร์กของเรา ดังนั้น เพื่อให้สามารถเริ่มต้นล่ามและเฟรมเวิร์กของเราได้เพียงครั้งเดียว จึงตัดสินใจเขียน daemon ใหม่ใน PHP เราเรียกมันว่า ป หิน syd (คล้ายกับ Phproxyd - PHP Proxy Daemon ซึ่งเป็น C daemon ที่เรามีมาก่อน) โดยยอมรับคำขอเพื่อรันแต่ละคลาสและสร้าง fork() ในแต่ละคำขอ และยังสามารถรายงานสถานะการดำเนินการของการรันแต่ละครั้งได้อีกด้วย สถาปัตยกรรมนี้มีลักษณะคล้ายกับโมเดลเว็บเซิร์ฟเวอร์ Apache หลายประการ เมื่อการกำหนดค่าเริ่มต้นทั้งหมดเสร็จสิ้นเพียงครั้งเดียวใน "ต้นแบบ" และ "ลูก" มีส่วนร่วมในการประมวลผลคำขอ เพื่อเป็นโบนัสเพิ่มเติม เราได้รับความสามารถในการเปิดใช้งานแคช opcode ใน CLI ซึ่งจะทำงานได้อย่างถูกต้องเนื่องจากลูก ๆ ทุกคนสืบทอดพื้นที่หน่วยความจำที่ใช้ร่วมกันเดียวกันกับกระบวนการหลัก เพื่อลดความล่าช้าในการประมวลผลคำขอการเปิดตัว คุณสามารถดำเนินการ fork() ล่วงหน้าได้ (รุ่น prefork) แต่ในกรณีของเรา ความล่าช้าสำหรับ fork() อยู่ที่ประมาณ 1 ms ซึ่งเหมาะกับเราค่อนข้างดี

อย่างไรก็ตาม เนื่องจากเราอัปเดตโค้ดค่อนข้างบ่อย ดีมอนนี้จึงต้องรีสตาร์ทบ่อยครั้ง ไม่เช่นนั้นโค้ดที่โหลดเข้าไปอาจล้าสมัย เนื่องจากการรีสตาร์ทแต่ละครั้งจะมาพร้อมกับข้อผิดพลาดมากมายเช่น การเชื่อมต่อรีเซ็ตโดยเพียร์รวมถึงการปฏิเสธการให้บริการแก่ผู้ใช้ปลายทาง (daemon มีประโยชน์ไม่เพียงแต่สำหรับคลาวด์เท่านั้น แต่ยังสำหรับส่วนหนึ่งของไซต์ของเราด้วย) เราตัดสินใจค้นหาวิธีรีสตาร์ท daemon โดยไม่สูญเสียการเชื่อมต่อที่สร้างไว้แล้ว มีเทคนิคหนึ่งที่นิยมใช้กันคือ โหลดซ้ำอย่างสง่างามสำหรับ daemons: fork-exec เสร็จสิ้นแล้ว และ descriptor จากซ็อกเก็ต Listen ถูกส่งไปยังชายด์ ดังนั้นการเชื่อมต่อใหม่จึงได้รับการยอมรับโดย daemon เวอร์ชันใหม่และการเชื่อมต่อเก่าจะถูก "แก้ไข" โดยใช้เวอร์ชันเก่า

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

ทฤษฎี ลองคิดดูก่อน: สิ่งที่เราต้องการทำให้เป็นไปได้คืออะไร? และถ้าเป็นเช่นนั้นจะบรรลุเป้าหมายนี้ได้อย่างไร?

เนื่องจาก daemon ทำงานบน Linux ซึ่งเป็นไปตาม POSIX เราจึงมีตัวเลือกต่อไปนี้:

  • ไฟล์และซ็อกเก็ตที่เปิดอยู่ทั้งหมดเป็นตัวเลขที่สอดคล้องกับหมายเลขตัวอธิบายที่เปิดอยู่ อินพุต เอาต์พุต และสตรีมข้อผิดพลาดมาตรฐานมีตัวอธิบาย 0, 1 และ 2 ตามลำดับ
  • ไม่มีความแตกต่างที่มีนัยสำคัญระหว่างไฟล์ที่เปิด ซ็อกเก็ต และไปป์ (ตัวอย่างเช่น คุณสามารถทำงานกับซ็อกเก็ตโดยใช้ทั้งการเรียกระบบแบบอ่าน/เขียนและ sendto/recvfrom)
  • เมื่อเรียกใช้ระบบ fork() แฮนเดิลที่เปิดอยู่ทั้งหมดจะได้รับการสืบทอด โดยคงหมายเลขและตำแหน่งการอ่าน/เขียนไว้ (ในไฟล์)
  • เมื่อดำเนินการเรียกของระบบ execve() หมายเลขอ้างอิงที่เปิดอยู่ทั้งหมดก็จะได้รับสืบทอดเช่นกัน และนอกจากนี้ PID ของกระบวนการจะถูกรักษาไว้และเชื่อมโยงกับรายการลูกของมันด้วย
  • รายการตัวอธิบายกระบวนการเปิดมีอยู่ในไดเร็กทอรี /dev/fd ซึ่งบน Linux เป็นลิงก์สัญลักษณ์ไปยัง /proc/self/fd
  • ดังนั้นเราจึงมีเหตุผลทุกประการที่จะเชื่อว่างานของเราสามารถทำได้สำเร็จโดยไม่ต้องใช้ความพยายามมากนัก มาเริ่มกันเลย แพตช์สำหรับ PHP น่าเสียดายที่มีรายละเอียดเล็ก ๆ อย่างหนึ่งที่ทำให้งานของเราซับซ้อน: ใน PHP ไม่มีวิธีรับหมายเลขตัวอธิบายไฟล์สำหรับสตรีมและเปิดตัวอธิบายไฟล์ตามหมายเลข (แทนที่จะเป็นสำเนาของไฟล์) มีการเปิด descriptor ซึ่งไม่เหมาะสมสำหรับ daemon ของเรา เนื่องจากเราตรวจสอบจุดจับที่เปิดอยู่อย่างระมัดระวัง เพื่อไม่ให้เกิดการรั่วไหลระหว่างการรีสตาร์ทและเมื่อเริ่มกระบวนการลูก)

    ขั้นแรก เราจะสร้างแพตช์เล็กๆ สองสามรายการในโค้ด PHP เพื่อเพิ่มความสามารถในการรับ fd จากสตรีม และตรวจสอบให้แน่ใจว่า fopen(php://fd/) จะไม่เปิดสำเนาของที่จับ (การเปลี่ยนแปลงครั้งที่สอง เข้ากันไม่ได้กับพฤติกรรม PHP ในปัจจุบัน ดังนั้นคุณสามารถเพิ่ม “ที่อยู่” ใหม่แทนได้ เช่น php://fdraw/):

    รหัสแพทช์

    diff --git a/ext/standard/php_fopen_wrapper.c b/ext/standard/php_fopen_wrapper.c ดัชนี f8d7bda..fee964c 100644 --- a/ext/standard/php_fopen_wrapper.c +++ b/ext/standard/php_fopen_wrapper. c @@ -24.6 +24.7 @@ #if HAVE_UNISTD_H #include #endif +#include #include "php.h" #include "php_globals.h" @@ -296.11 +297.11 @@ php_stream * php_stream_url_wrap_php(php_stream_wrapper *wrapper, char *path, ch "ตัวอธิบายไฟล์ต้องเป็นตัวเลขที่ไม่ใช่ค่าลบซึ่งน้อยกว่า %d", dtablesize);


    เราได้เพิ่มฟิลด์ fd ให้กับผลลัพธ์ที่ส่งกลับโดย stream_get_meta_data() หากเหมาะสม (เช่น สำหรับสตรีม zlib ฟิลด์ fd จะไม่ปรากฏ) นอกจากนี้เรายังแทนที่การเรียก dup() จาก file descriptor ที่ส่งผ่านด้วยการตรวจสอบง่ายๆ น่าเสียดายที่โค้ดนี้จะไม่ทำงานหากไม่มีการแก้ไขบน Windows เนื่องจากการเรียก fcntl() เป็นแบบเฉพาะของ POSIX ดังนั้นแพตช์เต็มจะต้องมีสาขาโค้ดเพิ่มเติมสำหรับระบบปฏิบัติการอื่น ๆ อันดับแรก เรามาเขียนเซิร์ฟเวอร์ขนาดเล็กกัน ที่จะสามารถรับคำขอในรูปแบบ JSON และตอบกลับได้บ้าง ตัวอย่างเช่น มันจะส่งคืนจำนวนองค์ประกอบในอาร์เรย์ที่มาในคำขอ

    daemon ฟังบนพอร์ต 31337 ผลลัพธ์ควรเป็นดังนี้:

    $ telnet localhost 31337 กำลังพยายาม 127.0.0.1... เชื่อมต่อกับ localhost แล้ว อักขระหลีกคือ "^]" ("hash":1) # ผู้ใช้ป้อนข้อมูล "คำขอมี 1 คีย์" ("hash":1,"cnt":2) # ผู้ใช้ป้อนข้อมูล "คำขอมี 2 คีย์"

    เราจะใช้ stream_socket_server() เพื่อเริ่มฟังพอร์ต และ stream_select() เพื่อกำหนดว่าแฮนเดิลใดพร้อมที่จะอ่าน/เขียน

    รหัสการใช้งานอย่างง่าย (Simple.php)