
Letzte Woche haben
wir über unser kleines Demo-Projekt Shrimp gesprochen , das deutlich zeigt, wie Sie die C ++ - Bibliotheken
RESTinio und
SObjectizer unter mehr oder weniger ähnlichen Bedingungen verwenden können. Shrimp ist eine kleine C ++ 17-Anwendung, die über RESTinio HTTP-Anforderungen für die Bildskalierung akzeptiert und diese Anforderungen im Multithread-Modus über SObjectizer und ImageMagick ++ bereitstellt.
Das Projekt hat sich für uns als mehr als nützlich erwiesen. Das Sparschwein von Wishlist zur Erweiterung der Funktionalität von RESTinio und SObjectizer wurde erheblich aufgefüllt. Etwas, das sogar in einer sehr
aktuellen Version von RESTinio-0.4.7 enthalten ist . Deshalb haben wir uns entschlossen, nicht auf die allererste und trivialste Version von Shrimp einzugehen, sondern ein oder zwei weitere Iterationen um dieses Projekt herum durchzuführen. Wenn sich jemand dafür interessiert, was und wie wir in dieser Zeit gemacht haben, sind Sie unter cat willkommen.
Als Spoiler: Es geht darum, wie wir die parallele Verarbeitung identischer Anforderungen beseitigt haben , wie wir die Protokollierung zu Shrimp mithilfe der hervorragenden spdlog-Bibliothek hinzugefügt und einen Befehl zum Zurücksetzen des Caches transformierter Bilder erstellt haben.
v0.3: Steuerung der parallelen Verarbeitung identischer Anforderungen
Die allererste Version von Shrimp, die in einem früheren Artikel beschrieben wurde, enthielt eine ernsthafte Vereinfachung: Es gab keine Kontrolle darüber, ob dieselbe Anfrage derzeit verarbeitet wird oder nicht.
Stellen Sie sich vor, Shrimp erhält zum ersten Mal eine Anfrage mit dem Formular "/demo.jpg?op=resize&max=1024". Es gibt noch kein solches Bild im transformierten Bildcache, daher wird die Anforderung verarbeitet. Die Verarbeitung kann beträchtliche Zeit in Anspruch nehmen, beispielsweise einige hundert Millisekunden.
Die Anforderungsverarbeitung ist noch nicht abgeschlossen, und Shrimp erhält erneut dieselbe Anforderung "/demo.jpg?op=resize&max=1024", jedoch von einem anderen Client. Es gibt noch kein Transformationsergebnis im Cache, daher wird diese Anforderung ebenfalls verarbeitet.
Weder die erste noch die zweite Anfrage wurden bereits abgeschlossen, und Shrimp kann erneut dieselbe Anfrage "/demo.jpg?op=resize&max=1024" erhalten. Und diese Anfrage wird auch bearbeitet. Es stellt sich heraus, dass dasselbe Bild mehrmals parallel auf dieselbe Größe skaliert wird.
Das ist nicht gut Deshalb haben wir uns bei Shrimp als erstes entschieden, einen so ernsten Pfosten loszuwerden. Wir haben dies aufgrund von zwei kniffligen Containern im transform_manager-Agenten getan. Der erste Container ist eine Warteschlange zum Warten auf kostenlose Transformatoranforderungen. Dies ist ein Container mit dem Namen m_pending_requests. Der zweite Container speichert Anforderungen, die bereits verarbeitet wurden (d. H. Diesen Anforderungen wurden bestimmte Transformatoren zugewiesen). Dies ist ein Container mit dem Namen m_inprogress_requests.
Wenn transform_manager die nächste Anforderung empfängt, prüft es, ob das fertige Bild im Cache der transformierten Bilder vorhanden ist. Wenn kein konvertiertes Bild vorhanden ist, werden die Container m_inprogress_requests und m_pending_requests überprüft. Und wenn in keinem dieser Container eine Anforderung mit solchen Parametern vorhanden ist, wird nur dann versucht, die Anforderung in die Warteschlange m_pending_requests zu stellen. Es sieht
so aus :
void a_transform_manager_t::handle_not_transformed_image( transform::resize_request_key_t request_key, sobj_shptr_t<resize_request_t> cmd ) { const auto store_to = [&](auto & queue) { queue.insert( std::move(request_key), std::move(cmd) ); }; if( m_inprogress_requests.has_key( request_key ) ) {
Es wurde oben gesagt, dass m_inprogress_requests und m_pending_requests knifflige Container sind. Aber was ist der Trick?
Der Trick besteht darin, dass diese Container die Eigenschaften sowohl einer regulären FIFO-Warteschlange (in der die chronologische Reihenfolge des Hinzufügens von Elementen beibehalten wird) als auch einer Multimap kombinieren, d. H. Ein assoziativer Container, in dem mehrere Werte einem einzelnen Schlüssel zugeordnet werden können.
Das Beibehalten der chronologischen Reihenfolge ist wichtig, da die ältesten Elemente in m_pending_requests regelmäßig überprüft und aus m_pending_requests die Anforderungen entfernt werden müssen, für die das maximale Zeitlimit überschritten wird. Ein effektiver Zugriff auf die Elemente per Schlüssel ist erforderlich, um sowohl das Vorhandensein identischer Anforderungen in den Warteschlangen zu überprüfen als auch um sicherzustellen, dass alle doppelten Anforderungen gleichzeitig aus der Warteschlange entfernt werden können.
Bei Shrimp sind wir zu diesem Zweck mit
unserem kleinen Container gefahren. Wenn Boost in Shrimp verwendet würde, könnte Boost.MultiIndex verwendet werden. Und wahrscheinlich muss im Laufe der Zeit eine effektive Suche in m_pending_requests nach anderen Kriterien organisiert werden, dann muss Boost.MultiIndex in Shrimp aktiviert werden.
v0.4: Protokollierung mit spdlog
Wir haben versucht, die erste Version von Shrimp so einfach und kompakt wie möglich zu gestalten. Aus diesem Grund haben wir in der ersten Version von Shrimp keine Protokollierung verwendet. Im Allgemeinen.
Dies ermöglichte es einerseits, den Code der ersten Version kurz zu halten, der nur die notwendige Shrimp-Geschäftslogik enthielt. Andererseits macht es das Fehlen von Protokollierung schwierig, Shrimp und seinen Betrieb zu entwickeln. Sobald wir es in die Hände bekommen haben, haben wir sofort eine exzellente moderne C ++ - Bibliothek für die Protokollierung -
spdlog - in Shrimp
gezogen . Das Atmen wurde sofort einfacher, obwohl der Code einiger Methoden an Volumen zunahm.
Der obige Code der handle_not_transformed_image () -Methode mit Protokollierung sieht beispielsweise ungefähr
so aus :
void a_transform_manager_t::handle_not_transformed_image( transform::resize_request_key_t request_key, sobj_shptr_t<resize_request_t> cmd ) { const auto store_to = [&](auto & queue) { queue.insert( std::move(request_key), std::move(cmd) ); }; if( m_inprogress_requests.has_key( request_key ) ) {
SPDlog-Logger konfigurieren
Die Anmeldung bei Shrimp erfolgt auf der Konsole (d. H. Im Standardausgabestream). Im Prinzip könnte man einen sehr einfachen Weg gehen und in Shrimp die einzige Instanz des SPD-Loggers erstellen. Das heißt, Man könnte
stdout_color_mt (oder
stdout_logger_mt )
aufrufen und diesen Logger dann an alle Entitäten in Shrimp übergeben. Aber wir sind etwas komplizierter gegangen: Wir haben das sogenannte manuell erstellt sink (d. h. der Kanal, in dem spdlog die generierten Nachrichten ausgibt), und für die Shrimp-Entitäten haben sie separate Logger erstellt, die an diese Senke angehängt sind.
Die Konfiguration von Loggern in spdlog hat einen subtilen Punkt: Standardmäßig ignoriert der Logger Nachrichten mit Schweregraden für Trace und Debug. Sie erweisen sich nämlich als am nützlichsten beim Debuggen. Daher aktivieren wir in make_logger standardmäßig die Protokollierung für alle Ebenen, einschließlich Trace / Debug.
Aufgrund der Tatsache, dass jede Entität in Shrimp einen eigenen Logger mit einem eigenen Namen hat, können wir im Protokoll sehen, wer was tut:

SObjectizer mit spdlog verfolgen
Protokollierungszeiten, die als Teil der Hauptgeschäftslogik einer SObjectizer-Anwendung ausgeführt werden, reichen nicht aus, um die Anwendung zu debuggen. Es ist nicht klar, warum eine Aktion in einem Agenten initiiert wird, aber nicht tatsächlich in einem anderen Agenten ausgeführt wird. In diesem Fall hilft der in SObjectizer integrierte msg_tracing-Mechanismus sehr (worüber wir
in einem separaten Artikel gesprochen haben ). Unter den Standardimplementierungen von msg_tracing für SObjectizer gibt es jedoch keine, die spdlog verwendet. Wir werden diese Implementierung für Shrimp selbst durchführen:
class spdlog_sobj_tracer_t : public so_5::msg_tracing::tracer_t { std::shared_ptr<spdlog::logger> m_logger; public: spdlog_sobj_tracer_t( std::shared_ptr<spdlog::logger> logger ) : m_logger{ std::move(logger) } {} virtual void trace( const std::string & what ) noexcept override { m_logger->trace( what ); } [[nodiscard]] static so_5::msg_tracing::tracer_unique_ptr_t make( spdlog::sink_ptr sink ) { return std::make_unique<spdlog_sobj_tracer_t>( make_logger( "sobjectizer", std::move(sink) ) ); } };
Hier sehen wir die Implementierung der speziellen SObjectizer-Schnittstelle tracer_t, in der die virtuelle trace () -Methode die Hauptsache ist. Er ist es, der die Verfolgung der Interna von SObjectizer mittels spdlog durchführt.
Als nächstes wird diese Implementierung beim Starten des SObjectizer als Tracer installiert:
so_5::wrapped_env_t sobj{ [&]( so_5::environment_t & env ) {...}, [&]( so_5::environment_params_t & params ) { if( sobj_tracing_t::on == sobj_tracing ) params.message_delivery_tracer( spdlog_sobj_tracer_t::make( logger_sink ) ); } };
RESTinio-Trace durch spdlog
Zusätzlich zum Nachverfolgen, was im SObjectizer passiert, kann es manchmal sehr nützlich sein, das zu verfolgen, was im RESTinio passiert. In der aktualisierten Version von Shrimp wird auch eine solche Ablaufverfolgung hinzugefügt.
Diese Ablaufverfolgung wird durch die Definition einer speziellen Klasse implementiert, die die Protokollierung in RESTinio durchführen kann:
class http_server_logger_t { public: http_server_logger_t( std::shared_ptr<spdlog::logger> logger ) : m_logger{ std::move( logger ) } {} template< typename Builder > void trace( Builder && msg_builder ) { log_if_enabled( spdlog::level::trace, std::forward<Builder>(msg_builder) ); } template< typename Builder > void info( Builder && msg_builder ) { log_if_enabled( spdlog::level::info, std::forward<Builder>(msg_builder) ); } template< typename Builder > void warn( Builder && msg_builder ) { log_if_enabled( spdlog::level::warn, std::forward<Builder>(msg_builder) ); } template< typename Builder > void error( Builder && msg_builder ) { log_if_enabled( spdlog::level::err, std::forward<Builder>(msg_builder) ); } private: template< typename Builder > void log_if_enabled( spdlog::level::level_enum lv, Builder && msg_builder ) { if( m_logger->should_log(lv) ) { m_logger->log( lv, msg_builder() ); } } std::shared_ptr<spdlog::logger> m_logger; };
Diese Klasse wird von nichts geerbt, da der Protokollierungsmechanismus in RESTinio auf einer allgemeinen Programmierung und nicht auf dem traditionellen objektorientierten Ansatz basiert. Auf diese Weise können Sie den Overhead in Fällen, in denen keine Protokollierung erforderlich ist, vollständig
beseitigen (wir haben dieses Thema ausführlicher behandelt, als wir
über die Verwendung von Vorlagen in RESTinio gesprochen haben ).
Als nächstes müssen wir angeben, dass der HTTP-Server die oben gezeigte Klasse http_server_logger_t als Logger verwendet. Dazu werden die Eigenschaften des HTTP-Servers geklärt:
struct http_server_traits_t : public restinio::default_traits_t { using logger_t = http_server_logger_t; using request_handler_t = http_req_router_t; };
Nun, dann bleibt nichts mehr zu tun - erstellen Sie eine bestimmte Instanz des SPD-Loggers und senden Sie diesen Logger an den erstellten HTTP-Server:
auto restinio_logger = make_logger( "restinio", logger_sink, restinio_tracing_t::off == restinio_tracing ? spdlog::level::off : log_level ); restinio::run( asio_io_ctx, shrimp::make_http_server_settings( thread_count.m_io_threads, params, std::move(restinio_logger), manager_mbox_promise.get_future().get() ) );
v0.5: erzwungenes Zurücksetzen des transformierten Bildcaches
Beim Debuggen von Shrimp wurde eine kleine Sache entdeckt, die etwas ärgerlich war: Um den Inhalt des transformierten Bildcaches zu leeren, musste der gesamte Shrimp neu gestartet werden. Es scheint eine Kleinigkeit, aber unangenehm.
Wenn es unangenehm ist, sollten Sie dieses Manko beseitigen. Zum Glück ist das überhaupt nicht schwierig.
Zuerst definieren wir eine andere URL in Shrimp, an die Sie HTTP DELETE-Anforderungen senden können: "/ cache". Dementsprechend hängen wir unseren Handler an diese URL:
std::unique_ptr< http_req_router_t > make_router( const app_params_t & params, so_5::mbox_t req_handler_mbox ) { auto router = std::make_unique< http_req_router_t >(); add_transform_op_handler( params, *router, req_handler_mbox ); add_delete_cache_handler( *router, req_handler_mbox ); return router; }
Dabei sieht die Funktion add_delete_cache_handler () folgendermaßen aus:
void add_delete_cache_handler( http_req_router_t & router, so_5::mbox_t req_handler_mbox ) { router.http_delete( "/cache", [req_handler_mbox]( auto req, auto ) { const auto qp = restinio::parse_query( req->header().query() ); auto token = qp.get_param( "token"sv ); if( !token ) { return do_403_response( req, "No token provided\r\n" ); }
Ein bisschen ausführlich, aber nichts kompliziertes. Die Abfragezeichenfolge der Abfrage muss einen Token-Parameter haben. Dieser Parameter muss eine Zeichenfolge mit einem speziellen Wert für das Verwaltungstoken enthalten. Sie können den Cache nur zurücksetzen, wenn der Token-Wert aus dem Token-Parameter mit dem Wert übereinstimmt, der beim Start von Shrimp festgelegt wurde. Wenn kein Token-Parameter vorhanden ist, wird die Verarbeitungsanforderung nicht akzeptiert. Wenn ein Token vorhanden ist, wird dem transform_manager-Agenten, dem der Cache gehört, eine spezielle Befehlsnachricht gesendet, durch deren Ausführung der transform_manager-Agent selbst auf die HTTP-Anforderung antwortet.
Zweitens implementieren wir den neuen Nachrichtenhandler delete_cache_request_t im Agenten transform_manager_t:
void a_transform_manager_t::on_delete_cache_request( mutable_mhood_t<delete_cache_request_t> cmd ) { m_logger->warn( "delete cache request received; " "connection_id={}, token={}", cmd->m_http_req->connection_id(), cmd->m_token ); const auto delay_response = [&]( std::string response_text ) { so_5::send_delayed< so_5::mutable_msg<negative_delete_cache_response_t> >( *this, std::chrono::seconds{7}, std::move(cmd->m_http_req), std::move(response_text) ); }; if( const char * env_token = std::getenv( "SHRIMP_ADMIN_TOKEN" );
Hier gibt es zwei Punkte, die geklärt werden sollten.
Der erste Punkt bei der Implementierung von on_delete_cache_request () ist die Überprüfung des Token-Werts selbst. Das administrative Token wird über die Umgebungsvariable SHRIMP_ADMIN_TOKEN festgelegt. Wenn diese Variable festgelegt ist und ihr Wert mit dem Wert aus dem Token-Parameter der HTTP-DELETE-Anforderung übereinstimmt, wird der Cache geleert und sofort eine positive Antwort auf die Anforderung generiert.
Und der zweite Punkt bei der Implementierung von on_delete_cache_request () ist die erzwungene Verzögerung einer negativen Antwort auf HTTP DELETE. Wenn der falsche Wert des administrativen Tokens eingetreten ist, sollten Sie die Antwort auf HTTP DELETE verzögern, damit Sie den Wert des Tokens nicht mit brutaler Gewalt auswählen können. Aber wie macht man diese Verzögerung? Der Aufruf von std :: thread :: sleep_for () ist schließlich keine Option.
Hier kommen die ausstehenden Nachrichten von SObjectizer zur Rettung. Anstatt sofort eine negative Antwort in on_delete_cache_request () zu generieren, sendet sich der transform_manager-Agent einfach eine ausstehende negative_delete_cache_response_t-Nachricht. Der SObjectizer-Timer zählt die eingestellte Zeit und übermittelt diese Nachricht an den Agenten, nachdem die angegebene Verzögerung verstrichen ist. Und jetzt können Sie im Handler negative_delete_cache_response_t bereits sofort eine Antwort auf die HTTP-DELETE-Anforderung generieren:
void a_transform_manager_t::on_negative_delete_cache_response( mutable_mhood_t<negative_delete_cache_response_t> cmd ) { m_logger->debug( "send negative response to delete cache request; " "connection_id={}", cmd->m_http_req->connection_id() ); do_403_response( std::move(cmd->m_http_req), std::move(cmd->m_response_text) ); }
Das heißt, Es stellt sich das folgende Szenario heraus:
- Der HTTP-Server empfängt eine HTTP-DELETE-Anforderung und konvertiert diese Anforderung in eine delete_cache_request_t-Nachricht an den transform_manager-Agenten.
- Der transform_manager-Agent empfängt die Nachricht delete_cache_request_t und generiert entweder sofort eine positive Antwort auf die Anforderung oder sendet sich selbst eine ausstehende negative_delete_cache_response_t-Nachricht.
- transform_manager empfängt eine negative_delete_cache_response_t-Nachricht und generiert sofort eine negative Antwort auf die entsprechende HTTP-DELETE-Anforderung.
Ende des zweiten Teils
Am Ende des zweiten Teils ist es ganz natürlich, die Frage zu stellen: "Was kommt als nächstes?"
Außerdem wird es wahrscheinlich eine weitere Iteration und ein weiteres Update unseres Demo-Projekts geben. Ich möchte so etwas wie ein Bild von einem Format in ein anderes konvertieren. Angenommen, auf dem Server befindet sich das Image in JPG und wird nach der Umwandlung in WebP an den Client gesendet.
Es wäre auch interessant, eine separate "Seite" mit der Anzeige der aktuellen Statistiken über die Arbeit von Shrimp beizufügen. Zuallererst ist es nur neugierig. Grundsätzlich kann eine solche Seite aber auch an die Bedürfnisse der Überwachung der Lebensfähigkeit von Shrimp angepasst werden.
Wenn jemand anderes Vorschläge dazu hat, was ich in Shrimp oder in Artikeln rund um Shrimp sehen möchte, freuen wir uns über konstruktive Gedanken.
Unabhängig davon möchte ich einen Aspekt bei der Implementierung von Shrimp erwähnen, der uns etwas überrascht hat. Dies ist eine aktive Verwendung von veränderlichen Nachrichten bei der Kommunikation untereinander und mit dem HTTP-Server. In unserer Praxis geschieht normalerweise das Gegenteil: Häufiger werden Daten über Immunnachrichten ausgetauscht. Nicht so hier. Dies deutet darauf hin, dass wir zu gegebener Zeit wissentlich auf die Wünsche der Benutzer gehört und SObjectizer veränderbare Nachrichten hinzugefügt haben. Wenn Sie also etwas in RESTinio oder SObjectizer sehen möchten, können Sie Ihre Ideen gerne teilen. Wir werden sicher auf die Guten hören.
Abschließend möchte ich mich bei allen bedanken, die sich die Zeit genommen und über die erste Version von Shrimp gesprochen haben, sowohl hier auf Habré als auch über andere Ressourcen. Vielen Dank!
Fortsetzung folgt ...