Μεταφορά του backend της PHP στο δίαυλο ροών Redis και επιλογή μιας βιβλιοθήκης ανεξάρτητης από το πλαίσιο

Μεταφορά του backend της PHP στο δίαυλο ροών Redis και επιλογή μιας βιβλιοθήκης ανεξάρτητης από το πλαίσιο

πρόλογος

Ο ιστότοπός μου, τον οποίο διαχειρίζομαι ως χόμπι, έχει σχεδιαστεί για να φιλοξενεί ενδιαφέρουσες αρχικές σελίδες και προσωπικές τοποθεσίες. Αυτό το θέμα άρχισε να με ενδιαφέρει στην αρχή του ταξιδιού μου στον προγραμματισμό· εκείνη τη στιγμή με γοήτευσε να βρω σπουδαίους επαγγελματίες που γράφουν για τον εαυτό τους, τα χόμπι και τα έργα τους. Η συνήθεια να τα ανακαλύπτω για τον εαυτό μου παραμένει μέχρι σήμερα: σχεδόν σε κάθε εμπορικό και όχι πολύ εμπορικό ιστότοπο, συνεχίζω να ψάχνω στο υποσέλιδο αναζητώντας συνδέσμους προς τους συγγραφείς.

Υλοποίηση της ιδέας

Η πρώτη έκδοση ήταν απλώς μια σελίδα html στον προσωπικό μου ιστότοπο, όπου έβαλα συνδέσμους με υπογραφές σε μια λίστα ul. Έχοντας πληκτρολογήσει 20 σελίδες σε μια χρονική περίοδο, άρχισα να πιστεύω ότι αυτό δεν ήταν πολύ αποτελεσματικό και αποφάσισα να προσπαθήσω να αυτοματοποιήσω τη διαδικασία. Στο stackoverflow, παρατήρησα ότι πολλοί άνθρωποι υποδεικνύουν ιστότοπους στα προφίλ τους, έτσι έγραψα έναν αναλυτή σε php, ο οποίος απλώς πέρασε από τα προφίλ, ξεκινώντας από το πρώτο (οι διευθύνσεις στο SO μέχρι σήμερα είναι ως εξής: `/users/1` ), εξήγαγε συνδέσμους από την επιθυμητή ετικέτα και την πρόσθεσε στο SQLite.

Αυτό μπορεί να ονομαστεί η δεύτερη έκδοση: μια συλλογή από δεκάδες χιλιάδες URL σε έναν πίνακα SQLite, που αντικατέστησε τη στατική λίστα σε HTML. Έκανα μια απλή αναζήτηση σε αυτή τη λίστα. Επειδή υπήρχαν μόνο διευθύνσεις URL, τότε η αναζήτηση βασίστηκε απλώς σε αυτές.

Σε αυτό το στάδιο εγκατέλειψα το έργο και επέστρεψα σε αυτό μετά από πολύ καιρό. Σε αυτό το στάδιο, η εργασιακή μου εμπειρία ήταν ήδη περισσότερα από τρία χρόνια και ένιωθα ότι μπορούσα να κάνω κάτι πιο σοβαρό. Επιπλέον, υπήρχε μεγάλη επιθυμία να κυριαρχήσουν σχετικά νέες τεχνολογίες.

Σύγχρονη έκδοση

Σχέδιο που αναπτύχθηκε στο Docker, η βάση δεδομένων μεταφέρθηκε στο mongoDb και πιο πρόσφατα προστέθηκε το radish, το οποίο αρχικά ήταν μόνο για προσωρινή αποθήκευση. Ένα από τα μικροπλαίσια της PHP χρησιμοποιείται ως βάση.

πρόβλημα

Νέοι ιστότοποι προστίθενται με μια εντολή κονσόλας που κάνει συγχρονισμένα τα εξής:

  • Λήψη περιεχομένου κατά διεύθυνση URL
  • Ορίζει μια σημαία που υποδεικνύει εάν το HTTPS ήταν διαθέσιμο
  • Διατηρεί την ουσία της ιστοσελίδας
  • Η πηγή HTML και οι κεφαλίδες αποθηκεύονται στο ιστορικό "ευρετηρίασης".
  • Αναλύει το περιεχόμενο, αποσπά τον τίτλο και την περιγραφή
  • Αποθηκεύει δεδομένα σε ξεχωριστή συλλογή

Αυτό ήταν αρκετό για να αποθηκεύσετε απλώς ιστότοπους και να τους εμφανίσετε σε μια λίστα:

Μεταφορά του backend της PHP στο δίαυλο ροών Redis και επιλογή μιας βιβλιοθήκης ανεξάρτητης από το πλαίσιο

Αλλά η ιδέα της αυτόματης ευρετηρίασης, κατηγοριοποίησης και κατάταξης των πάντων, διατηρώντας τα πάντα ενημερωμένα, δεν ταίριαζε καλά σε αυτό το παράδειγμα. Ακόμη και η απλή προσθήκη μιας μεθόδου ιστού για την προσθήκη σελίδων απαιτούσε αντιγραφή και αποκλεισμό κώδικα για την αποφυγή πιθανών DDoS.

Γενικά, φυσικά, όλα μπορούν να γίνουν συγχρονισμένα και στη μέθοδο web μπορείτε απλά να αποθηκεύσετε τη διεύθυνση URL έτσι ώστε ο τερατώδες δαίμονας να εκτελεί όλες τις εργασίες για τις διευθύνσεις URL από τη λίστα. Ωστόσο, ακόμα και εδώ η λέξη «ουρά» υποδηλώνεται. Και αν υλοποιηθεί μια ουρά, τότε όλες οι εργασίες μπορούν να χωριστούν και να εκτελεστούν τουλάχιστον ασύγχρονα.

Λύση

Υλοποιήστε ουρές και δημιουργήστε ένα σύστημα που βασίζεται σε συμβάντα για την επεξεργασία όλων των εργασιών. Και ήθελα να δοκιμάσω το Redis Streams εδώ και πολύ καιρό.

Χρήση ροών Redis στην PHP

Επειδή Επειδή το πλαίσιο μου δεν είναι ένας από τους τρεις γίγαντες Symfony, Laravel, Yii, θα ήθελα να βρω μια ανεξάρτητη βιβλιοθήκη. Αλλά, όπως αποδείχθηκε (με την πρώτη εξέταση), είναι αδύνατο να βρεθούν μεμονωμένες σοβαρές βιβλιοθήκες. Ό,τι σχετίζεται με ουρές είναι είτε έργο από 3 commit πριν από πέντε χρόνια, είτε συνδέεται με το πλαίσιο.

Έχω ακούσει πολλά για τη Symfony ως προμηθευτή μεμονωμένων χρήσιμων εξαρτημάτων και ήδη χρησιμοποιώ μερικά από αυτά. Και επίσης ορισμένα πράγματα από τη Laravel μπορούν επίσης να χρησιμοποιηθούν, για παράδειγμα το ORM τους, χωρίς την παρουσία του ίδιου του πλαισίου.

σύμφωνος/αγγελιοφόρος

Ο πρώτος υποψήφιος μου φάνηκε αμέσως ιδανικός και χωρίς καμία αμφιβολία το εγκατέστησα. Αλλά αποδείχθηκε ότι ήταν πιο δύσκολο να αναζητήσετε στο google παραδείγματα χρήσης εκτός Symfony. Πώς να συναρμολογήσετε από ένα σωρό τάξεις με καθολικά, χωρίς νόημα ονόματα, ένα λεωφορείο για τη μετάδοση μηνυμάτων, ακόμα και στο Redis;

Μεταφορά του backend της PHP στο δίαυλο ροών Redis και επιλογή μιας βιβλιοθήκης ανεξάρτητης από το πλαίσιο

Η τεκμηρίωση στον επίσημο ιστότοπο ήταν αρκετά λεπτομερής, αλλά η αρχικοποίηση περιγράφηκε μόνο για τη Symfony χρησιμοποιώντας το αγαπημένο τους YML και άλλες μαγικές μεθόδους για τους μη-συμφωνιστές. Δεν με ενδιέφερε η ίδια η διαδικασία εγκατάστασης, ειδικά κατά τις διακοπές της Πρωτοχρονιάς. Αλλά έπρεπε να το κάνω αυτό για απροσδόκητα μεγάλο χρονικό διάστημα.

Η προσπάθεια να καταλάβετε πώς να δημιουργήσετε ένα σύστημα χρησιμοποιώντας πηγές Symfony δεν είναι επίσης η πιο ασήμαντη εργασία για μια στενή προθεσμία:

Μεταφορά του backend της PHP στο δίαυλο ροών Redis και επιλογή μιας βιβλιοθήκης ανεξάρτητης από το πλαίσιο

Αφού έψαξα σε όλα αυτά και προσπάθησα να κάνω κάτι με τα χέρια μου, κατέληξα στο συμπέρασμα ότι έκανα κάποιου είδους πατερίτσες και αποφάσισα να δοκιμάσω κάτι άλλο.

φωτισμένο/ουρά

Αποδείχθηκε ότι αυτή η βιβλιοθήκη ήταν στενά συνδεδεμένη με την υποδομή Laravel και μια δέσμη άλλων εξαρτήσεων, επομένως δεν ξόδεψα πολύ χρόνο σε αυτήν: την εγκατέστησα, την κοίταξα, είδα τις εξαρτήσεις και τη διέγραψα.

yiisoft/yii2-ουρά

Λοιπόν, εδώ υποτέθηκε αμέσως από το όνομα, πάλι, μια αυστηρή σύνδεση με το Yii2. Έπρεπε να χρησιμοποιήσω αυτήν τη βιβλιοθήκη και δεν ήταν κακό, αλλά δεν σκέφτηκα το γεγονός ότι εξαρτάται πλήρως από το Yii2.

Τα υπόλοιπα

Όλα τα άλλα που βρήκα στο GitHub ήταν αναξιόπιστα, ξεπερασμένα και εγκαταλελειμμένα έργα χωρίς αστέρια, πιρούνια και μεγάλο αριθμό δεσμεύσεων.

Επιστροφή στο symfony/messenger, τεχνικές λεπτομέρειες

Έπρεπε να καταλάβω αυτή τη βιβλιοθήκη και, αφού πέρασα λίγο περισσότερο χρόνο, τα κατάφερα. Αποδείχθηκε ότι όλα ήταν αρκετά συνοπτικά και απλά. Για να δημιουργήσω το λεωφορείο, έφτιαξα ένα μικρό εργοστάσιο, γιατί... Υποτίθεται ότι είχα πολλά ελαστικά και με διαφορετικούς χειριστές.

Μεταφορά του backend της PHP στο δίαυλο ροών Redis και επιλογή μιας βιβλιοθήκης ανεξάρτητης από το πλαίσιο

Μόνο μερικά βήματα:

  • Δημιουργούμε προγράμματα χειρισμού μηνυμάτων που θα πρέπει να είναι απλά καλέσιμα
  • Τα τυλίγουμε σε HandlerDescriptor (τάξη από τη βιβλιοθήκη)
  • Αναδιπλώνουμε αυτούς τους "Περιγραφείς" σε μια παρουσία HandlersLocator
  • Προσθήκη HandlersLocator στο στιγμιότυπο MessageBus
  • Περνάμε ένα σύνολο «SenderInterface» στο SendersLocator, στην περίπτωσή μου περιπτώσεις κλάσεων «RedisTransport», οι οποίες έχουν ρυθμιστεί με προφανή τρόπο
  • Προσθήκη του SenderSLocator στο στιγμιότυπο MessageBus

Το MessageBus έχει μια μέθοδο `->dispatch()` που αναζητά τους κατάλληλους χειριστές στο HandlersLocator και τους μεταβιβάζει το μήνυμα, χρησιμοποιώντας το αντίστοιχο `SenderInterface` για αποστολή μέσω του διαύλου (ροές Redis).

Στη διαμόρφωση του κοντέινερ (σε αυτήν την περίπτωση 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 έχουμε αντιστοιχίσει διαφορετικές «μεταφορές» για δύο διαφορετικά μηνύματα, καθένα από τα οποία έχει τη δική του σύνδεση με τις αντίστοιχες ροές.

Έκανα ένα ξεχωριστό έργο επίδειξης που επιδεικνύει μια εφαρμογή τριών δαιμόνων που επικοινωνούν μεταξύ τους χρησιμοποιώντας τον ακόλουθο δίαυλο: https://github.com/backend-university/products/tree/master/products/02-redis-streams-bus.

Αλλά θα σας δείξω πώς μπορεί να δομηθεί ένας καταναλωτής:

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();

Χρήση αυτής της υποδομής σε μια εφαρμογή

Έχοντας εφαρμόσει το bus στο backend μου, ξεχώρισα μεμονωμένα στάδια από την παλιά σύγχρονη εντολή και έφτιαξα ξεχωριστούς χειριστές, ο καθένας από τους οποίους κάνει το δικό του.

Η διοχέτευση για την προσθήκη ενός νέου ιστότοπου στη βάση δεδομένων έμοιαζε ως εξής:

Μεταφορά του backend της PHP στο δίαυλο ροών Redis και επιλογή μιας βιβλιοθήκης ανεξάρτητης από το πλαίσιο

Και αμέσως μετά, έγινε πολύ πιο εύκολο για μένα να προσθέσω νέες λειτουργίες, για παράδειγμα, εξαγωγή και ανάλυση Rss. Επειδή Αυτή η διαδικασία απαιτεί επίσης το αρχικό περιεχόμενο και, στη συνέχεια, ο χειριστής εξαγωγής συνδέσμων RSS, όπως το WebsiteIndexHistoryPersistor, εγγράφεται στο μήνυμα «Content/HtmlContent», το επεξεργάζεται και περνά το επιθυμητό μήνυμα κατά μήκος της διοχέτευσής του περαιτέρω.

Μεταφορά του backend της PHP στο δίαυλο ροών Redis και επιλογή μιας βιβλιοθήκης ανεξάρτητης από το πλαίσιο

Στο τέλος, καταλήξαμε με αρκετούς δαίμονες, καθένας από τους οποίους διατηρεί συνδέσεις μόνο με τους απαραίτητους πόρους. Για παράδειγμα ένας δαίμονας crawlers περιέχει όλους τους χειριστές που απαιτούν πρόσβαση στο Διαδίκτυο για περιεχόμενο και τον δαίμονα επιμένω διατηρεί μια σύνδεση με τη βάση δεδομένων.

Τώρα, αντί να επιλέγονται από τη βάση δεδομένων, τα απαιτούμενα αναγνωριστικά μετά την εισαγωγή από τον persister απλώς μεταδίδονται μέσω του διαύλου σε όλους τους ενδιαφερόμενους χειριστές.

Πηγή: www.habr.com

Προσθέστε ένα σχόλιο