คำปรารภ
เว็บไซต์ของฉันซึ่งฉันทำเป็นงานอดิเรก ได้รับการออกแบบมาเพื่อโฮสต์โฮมเพจและไซต์ส่วนตัวที่น่าสนใจ หัวข้อนี้เริ่มทำให้ฉันสนใจตั้งแต่เริ่มต้นการเดินทางด้านการเขียนโปรแกรม ในขณะนั้น ฉันรู้สึกทึ่งที่ได้เจอผู้เชี่ยวชาญที่เก่งๆ ที่เขียนเกี่ยวกับตัวเอง งานอดิเรก และโปรเจ็กต์ของพวกเขา นิสัยในการค้นพบสิ่งเหล่านี้ด้วยตนเองยังคงอยู่มาจนถึงทุกวันนี้: ในเกือบทุกไซต์เชิงพาณิชย์และไม่ใช่เชิงพาณิชย์ ฉันยังคงดูในส่วนท้ายเพื่อค้นหาลิงก์ไปยังผู้เขียน
การนำแนวคิดไปใช้
เวอร์ชันแรกเป็นเพียงหน้า html บนเว็บไซต์ส่วนตัวของฉัน ซึ่งฉันใส่ลิงก์พร้อมลายเซ็นลงในรายการ ul หลังจากพิมพ์ไป 20 หน้าในช่วงระยะเวลาหนึ่ง ฉันเริ่มคิดว่าวิธีนี้ไม่ได้ผลมากนัก และตัดสินใจที่จะพยายามทำให้กระบวนการเป็นแบบอัตโนมัติ ใน stackoverflow ฉันสังเกตเห็นว่ามีคนจำนวนมากระบุไซต์ในโปรไฟล์ของพวกเขา ดังนั้นฉันจึงเขียน parser ใน php ซึ่งเพียงแค่ดูโปรไฟล์ต่างๆ โดยเริ่มจากโปรไฟล์แรก (ที่อยู่ใน SO จนถึงทุกวันนี้จะเป็นดังนี้: `/users/1` ) แยกลิงก์จากแท็กที่ต้องการและเพิ่มลงใน SQLite
สิ่งนี้สามารถเรียกได้ว่าเป็นเวอร์ชันที่สอง: คอลเลกชันของ URL นับหมื่นในตาราง SQLite ซึ่งแทนที่รายการคงที่ใน HTML ฉันค้นหาอย่างง่าย ๆ ในรายการนี้ เพราะ มีเพียง URL เท่านั้น จากนั้นการค้นหาก็ขึ้นอยู่กับสิ่งเหล่านั้น
ในขั้นตอนนี้ฉันละทิ้งโครงการและกลับมาทำใหม่อีกครั้งหลังจากผ่านไปนาน ในขั้นตอนนี้ประสบการณ์การทำงานของฉันมากกว่าสามปีแล้วและฉันรู้สึกว่าฉันสามารถทำอะไรที่จริงจังกว่านี้ได้ นอกจากนี้ยังมีความปรารถนาอย่างยิ่งที่จะเชี่ยวชาญเทคโนโลยีที่ค่อนข้างใหม่
รุ่นทันสมัย
ปัญหา
ไซต์ใหม่จะถูกเพิ่มโดยคำสั่งคอนโซลที่ทำสิ่งต่อไปนี้พร้อมกัน:
- ดาวน์โหลดเนื้อหาตาม URL
- ตั้งค่าสถานะที่ระบุว่า HTTPS พร้อมใช้งานหรือไม่
- รักษาสาระสำคัญของเว็บไซต์
- HTML ต้นฉบับและส่วนหัวจะถูกบันทึกไว้ในประวัติ "การจัดทำดัชนี"
- แยกวิเคราะห์เนื้อหา แยกชื่อและคำอธิบาย
- บันทึกข้อมูลไปยังคอลเลกชันแยกต่างหาก
เพียงพอที่จะจัดเก็บไซต์และแสดงในรายการ:
แต่แนวคิดในการจัดทำดัชนี จัดหมวดหมู่ และจัดอันดับทุกอย่างโดยอัตโนมัติ ทำให้ทุกอย่างทันสมัยอยู่เสมอ ไม่สอดคล้องกับกระบวนทัศน์นี้ แม้แต่เพียงแค่เพิ่มวิธีการทางเว็บเพื่อเพิ่มหน้าก็จำเป็นต้องมีการทำซ้ำโค้ดและการบล็อกเพื่อหลีกเลี่ยง DDoS ที่อาจเกิดขึ้น
โดยทั่วไปแล้ว ทุกอย่างสามารถทำได้พร้อมกัน และในวิธีการทางเว็บ คุณสามารถบันทึก URL เพื่อให้ daemon ตัวมหึมาทำงานทั้งหมดสำหรับ URL จากรายการได้ แต่ถึงกระนั้น แม้แต่ที่นี่ คำว่า "คิว" ก็บ่งบอกถึงตัวมันเอง และหากมีการนำคิวไปใช้ งานทั้งหมดก็สามารถแบ่งและดำเนินการได้แบบอะซิงโครนัสเป็นอย่างน้อย
การตัดสิน
ใช้งานคิวและสร้างระบบที่ขับเคลื่อนด้วยเหตุการณ์เพื่อประมวลผลงานทั้งหมด และฉันอยากลองใช้ Redis Streams มาเป็นเวลานานแล้ว
การใช้สตรีม Redis ใน PHP
เพราะ เนื่องจากเฟรมเวิร์กของฉันไม่ใช่หนึ่งในสามยักษ์ใหญ่ Symfony, Laravel, Yii ฉันจึงต้องการค้นหาไลบรารีอิสระ แต่เมื่อปรากฏออกมา (ในการตรวจสอบครั้งแรก) มันเป็นไปไม่ได้เลยที่จะหาห้องสมุดที่จริงจังเป็นรายบุคคล ทุกสิ่งที่เกี่ยวข้องกับคิวอาจเป็นโปรเจ็กต์จาก 3 คอมมิตเมื่อห้าปีที่แล้วหรือเชื่อมโยงกับเฟรมเวิร์ก
ฉันได้ยินมามากมายเกี่ยวกับ Symfony ในฐานะซัพพลายเออร์ส่วนประกอบที่มีประโยชน์ส่วนบุคคล และฉันก็ใช้บางส่วนไปแล้ว และบางสิ่งจาก Laravel ก็สามารถใช้ได้เช่นกัน เช่น ORM โดยไม่ต้องมีเฟรมเวิร์กเอง
ซิมโฟนี่/เมสเซนเจอร์
ผู้สมัครคนแรกดูเหมือนจะเหมาะสมในทันที และฉันก็ติดตั้งมันอย่างไม่ต้องสงสัย แต่กลับกลายเป็นว่ายากกว่าสำหรับตัวอย่างการใช้งาน Google นอก Symfony จะรวบรวมคลาสที่มีชื่อสากลและไร้ความหมายจากคลาสจำนวนมาก บัสสำหรับส่งข้อความ และแม้แต่บน Redis ได้อย่างไร
เอกสารในเว็บไซต์อย่างเป็นทางการมีรายละเอียดค่อนข้างมาก แต่การเริ่มต้นนั้นอธิบายไว้สำหรับ Symfony เท่านั้นโดยใช้ YML ที่พวกเขาชื่นชอบและวิธีการมหัศจรรย์อื่น ๆ สำหรับผู้ที่ไม่ใช่ซิมโฟนี ฉันไม่สนใจกระบวนการติดตั้งเลย โดยเฉพาะในช่วงวันหยุดปีใหม่ แต่ฉันต้องทำสิ่งนี้เป็นเวลานานโดยไม่คาดคิด
การพยายามหาวิธีสร้างอินสแตนซ์ของระบบโดยใช้แหล่งที่มาของ Symfony ก็ไม่ใช่งานที่ไม่สำคัญที่สุดสำหรับกำหนดเวลาที่จำกัด:
หลังจากเจาะลึกทั้งหมดนี้และพยายามทำอะไรบางอย่างด้วยมือของฉัน ฉันก็สรุปได้ว่าฉันกำลังทำไม้ค้ำอยู่และตัดสินใจลองทำอย่างอื่น
สว่าง/เข้าคิว
ปรากฎว่าไลบรารีนี้เชื่อมโยงอย่างแน่นหนากับโครงสร้างพื้นฐาน Laravel และการขึ้นต่อกันอื่น ๆ มากมาย ดังนั้นฉันจึงไม่ได้ใช้เวลามากนักกับมัน: ฉันติดตั้งแล้ว ดูมัน เห็นการขึ้นต่อกัน และลบมันออก
yiisoft/yii2-คิว
ที่นี่มันถูกสันนิษฐานจากชื่อทันทีอีกครั้งซึ่งมีความเชื่อมโยงอย่างเข้มงวดกับ Yii2 ฉันต้องใช้ไลบรารีนี้และมันก็ไม่ได้แย่ แต่ฉันไม่ได้คิดถึงความจริงที่ว่ามันขึ้นอยู่กับ Yii2 โดยสิ้นเชิง
ที่เหลือ
ทุกสิ่งทุกอย่างที่ฉันพบใน GitHub เป็นโปรเจ็กต์ที่ไม่น่าเชื่อถือ ล้าสมัย และละทิ้งไปโดยไม่มีดาว ส้อม และคอมมิตจำนวนมาก
กลับไปที่รายละเอียดด้านเทคนิคของ Symfony/Messenger
ฉันต้องหาห้องสมุดนี้ให้เจอ และหลังจากใช้เวลาสักพักฉันก็ทำสำเร็จ ปรากฎว่าทุกอย่างค่อนข้างกระชับและเรียบง่าย เพื่อยกตัวอย่างรถบัส ฉันสร้างโรงงานขนาดเล็กขึ้นมา เพราะ... ฉันควรจะมียางหลายเส้นและมีแฮนด์ที่แตกต่างกัน
เพียงไม่กี่ขั้นตอน:
- เราสร้างตัวจัดการข้อความที่ควรเรียกง่ายๆ
- เราล้อมพวกมันไว้ใน HandlerDescriptor (คลาสจากไลบรารี)
- เรารวม "Descriptors" เหล่านี้ไว้ในอินสแตนซ์ HandlersLocator
- การเพิ่ม HandlersLocator ไปยังอินสแตนซ์ MessageBus
- เราส่งชุด `SenderInterface` ไปยัง SendersLocator ในกรณีของฉันคือคลาส `RedisTransport` ซึ่งได้รับการกำหนดค่าในลักษณะที่ชัดเจน
- การเพิ่ม SendersLocator ไปยังอินสแตนซ์ MessageBus
MessageBus имеет метод `->dispatch()`, который ищет соответствующие обработчики в HandlersLocator и передает сообщение им, пользуясь соответствующими `SenderInterface` для отправки через шину (Redis streams).
ในการกำหนดค่าคอนเทนเนอร์ (ในกรณีนี้คือ php-di) บันเดิลทั้งหมดนี้สามารถกำหนดค่าได้ดังนี้:
CONTAINER_REDIS_TRANSPORT_SECRET => function (ContainerInterface $c) {
return new RedisTransport(
$c->get(CONTAINER_REDIS_STREAM_CONNECTION_SECRET),
$c->get(CONTAINER_SERIALIZER))
;
},
CONTAINER_REDIS_TRANSPORT_LOG => function (ContainerInterface $c) {
return new RedisTransport(
$c->get(CONTAINER_REDIS_STREAM_CONNECTION_LOG),
$c->get(CONTAINER_SERIALIZER))
;
},
CONTAINER_REDIS_STREAM_RECEIVER_SECRET => function (ContainerInterface $c) {
return new RedisReceiver(
$c->get(CONTAINER_REDIS_STREAM_CONNECTION_SECRET),
$c->get(CONTAINER_SERIALIZER)
);
},
CONTAINER_REDIS_STREAM_RECEIVER_LOG => function (ContainerInterface $c) {
return new RedisReceiver(
$c->get(CONTAINER_REDIS_STREAM_CONNECTION_LOG),
$c->get(CONTAINER_SERIALIZER)
);
},
CONTAINER_REDIS_STREAM_BUS => function (ContainerInterface $c) {
$sendersLocator = new SendersLocator([
AppMessagesSecretJsonMessages::class => [CONTAINER_REDIS_TRANSPORT_SECRET],
AppMessagesDaemonLogMessage::class => [CONTAINER_REDIS_TRANSPORT_LOG],
], $c);
$middleware[] = new SendMessageMiddleware($sendersLocator);
return new MessageBus($middleware);
},
CONTAINER_REDIS_STREAM_CONNECTION_SECRET => function (ContainerInterface $c) {
$host = 'bu-02-redis';
$port = 6379;
$dsn = "redis://$host:$port";
$options = [
'stream' => 'secret',
'group' => 'default',
'consumer' => 'default',
];
return Connection::fromDsn($dsn, $options);
},
CONTAINER_REDIS_STREAM_CONNECTION_LOG => function (ContainerInterface $c) {
$host = 'bu-02-redis';
$port = 6379;
$dsn = "redis://$host:$port";
$options = [
'stream' => 'log',
'group' => 'default',
'consumer' => 'default',
];
return Connection::fromDsn($dsn, $options);
},
ที่นี่คุณจะเห็นว่าใน SendersLocator เราได้กำหนด "การขนส่ง" ที่แตกต่างกันสำหรับข้อความสองข้อความที่แตกต่างกัน ซึ่งแต่ละข้อความมีการเชื่อมต่อกับสตรีมที่สอดคล้องกัน
ฉันสร้างโปรเจ็กต์สาธิตแยกต่างหากเพื่อสาธิตแอปพลิเคชันของ daemons สามตัวที่สื่อสารกันโดยใช้บัสต่อไปนี้:
แต่ฉันจะแสดงให้คุณเห็นว่าผู้บริโภคสามารถมีโครงสร้างได้อย่างไร:
use AppMessagesDaemonLogMessage;
use SymfonyComponentMessengerHandlerHandlerDescriptor;
use SymfonyComponentMessengerHandlerHandlersLocator;
use SymfonyComponentMessengerMessageBus;
use SymfonyComponentMessengerMiddlewareHandleMessageMiddleware;
use SymfonyComponentMessengerMiddlewareSendMessageMiddleware;
use SymfonyComponentMessengerTransportSenderSendersLocator;
require_once __DIR__ . '/../vendor/autoload.php';
/** @var PsrContainerContainerInterface $container */
$container = require_once('config/container.php');
$handlers = [
DaemonLogMessage::class => [
new HandlerDescriptor(
function (DaemonLogMessage $m) {
error_log('DaemonLogHandler: message handled: / ' . $m->getMessage());
},
['from_transport' => CONTAINER_REDIS_TRANSPORT_LOG]
)
],
];
$middleware = [];
$middleware[] = new HandleMessageMiddleware(new HandlersLocator($handlers));
$sendersLocator = new SendersLocator(['*' => [CONTAINER_REDIS_TRANSPORT_LOG]], $container);
$middleware[] = new SendMessageMiddleware($sendersLocator);
$bus = new MessageBus($middleware);
$receivers = [
CONTAINER_REDIS_TRANSPORT_LOG => $container->get(CONTAINER_REDIS_STREAM_RECEIVER_LOG),
];
$w = new SymfonyComponentMessengerWorker($receivers, $bus, $container->get(CONTAINER_EVENT_DISPATCHER));
$w->run();
การใช้โครงสร้างพื้นฐานนี้ในแอปพลิเคชัน
หลังจากติดตั้งบัสในแบ็กเอนด์แล้ว ฉันได้แยกแต่ละขั้นตอนออกจากคำสั่งซิงโครนัสแบบเก่า และสร้างตัวจัดการแยกกัน ซึ่งแต่ละคนทำหน้าที่ของตัวเอง
ไปป์ไลน์สำหรับการเพิ่มไซต์ใหม่ลงในฐานข้อมูลมีลักษณะดังนี้:
และหลังจากนั้นทันที มันกลายเป็นเรื่องง่ายสำหรับฉันที่จะเพิ่มฟังก์ชันการทำงานใหม่ เช่น การแยกและแยกวิเคราะห์ Rss เพราะ กระบวนการนี้ยังต้องใช้เนื้อหาต้นฉบับ จากนั้นตัวจัดการตัวแยกลิงก์ RSS เช่น WebsiteIndexHistoryPersistor จะสมัครรับข้อความ "เนื้อหา/HtmlContent" จากนั้นจะประมวลผลและส่งข้อความที่ต้องการไปตามขั้นตอนต่อไป
ในท้ายที่สุด เราก็ได้ daemons หลายตัว ซึ่งแต่ละ daemons จะรักษาการเชื่อมต่อกับทรัพยากรที่จำเป็นเท่านั้น ยกตัวอย่างปีศาจ รวบรวมข้อมูล ประกอบด้วยตัวจัดการทั้งหมดที่จำเป็นต้องเชื่อมต่ออินเทอร์เน็ตเพื่อดูเนื้อหา และ daemon ยังคงมีอยู่ เชื่อมต่อกับฐานข้อมูล
ตอนนี้ แทนที่จะเลือกจากฐานข้อมูล รหัสที่ต้องการหลังจากการแทรกโดยผู้คงอยู่จะถูกส่งผ่านบัสไปยังตัวจัดการที่สนใจทั้งหมด
ที่มา: will.com