{"id":56186,"date":"2020-02-06T00:00:00","date_gmt":"2020-02-05T21:00:00","guid":{"rendered":"https:\/\/prohoster.info\/blog\/blog_prohoster\/povtornaya-obrabotka-sobytij-poluchennyh-iz-kafka"},"modified":"2020-02-18T14:04:24","modified_gmt":"2020-02-18T11:04:24","slug":"povtornaya-obrabotka-sobytij-poluchennyh-iz-kafka","status":"publish","type":"post","link":"https:\/\/prohoster.info\/it\/blog\/administrirovanie\/povtornaya-obrabotka-sobytij-poluchennyh-iz-kafka","title":{"rendered":"Rielaborazione degli eventi ricevuti da Kafka","gt_translate_keys":[{"key":"rendered","format":"text"}]},"content":{"rendered":"<p><img decoding=\"async\" alt=\"Rielaborazione degli eventi ricevuti da Kafka\" src=\"\/wp-content\/uploads\/2020\/02\/d04319691af5c5de40ccfe4a2268558c.jpg\" style=\"display:block;margin: 0 auto;\" \/><\/p>\n<p><\/p>\n<p>Ciao, Habr.<\/p>\n<p><\/p>\n<p>Recentemente ho <noindex><a rel=\"nofollow\" href=\"https:\/\/habr.com\/ru\/company\/tinkoff\/blog\/481784\/\">condiviso un'esperienza<\/a><\/noindex> su quali parametri utilizziamo pi\u00f9 frequentemente all'interno del nostro team per Kafka Producer e Consumer, al fine di avvicinarci alla consegna garantita. In questo articolo voglio raccontare come abbiamo organizzato la re-elaborazione di un evento ricevuto da Kafka, a seguito dell'inaccessibilit\u00e0 temporanea di un sistema esterno.<\/p>\n<p><\/p>\n<p>Le applicazioni moderne operano in un ambiente molto complesso. La logica aziendale, incapsulata in un moderno stack tecnologico, funziona in un'immagine Docker gestita da un orchestratore come Kubernetes o OpenShift e comunica con altre applicazioni o soluzioni aziendali attraverso una rete di router fisici e virtuali. In un tale contesto, qualcosa pu\u00f2 sempre andare storto, quindi la re-elaborazione degli eventi in caso di inaccessibilit\u00e0 di uno dei sistemi esterni \u00e8 una parte importante dei nostri processi aziendali.<\/p>\n<p><noindex><a rel=\"nofollow\" name=\"habracut\"><\/a><\/noindex><\/p>\n<h2 id=\"kak-bylo-do-kafka\">Come era prima di Kafka<\/h2>\n<p><\/p>\n<p>In precedenza, nel progetto utilizzavano IBM MQ per la consegna asincrona dei messaggi. Quando si verificava un errore nel processo di funzionamento del servizio, il messaggio ricevuto poteva essere collocato in una coda di messaggi non elaborabili (DLQ) per un successivo esame manuale. La DLQ veniva creata accanto alla coda in ingresso e il messaggio veniva spostato all'interno di IBM MQ. <\/p>\n<p><\/p>\n<p>Se l'errore era di natura temporanea e riuscivamo a determinarlo (ad esempio, ResourceAccessException durante una chiamata HTTP o MongoTimeoutException durante una richiesta a MongoDb), entrava in gioco la strategia dei ripetuti tentativi. Indipendentemente dallo branching della logica dell'applicazione, il messaggio originale veniva spostato o in una coda di sistema per l'invio posticipato, oppure in un'applicazione separata creata tempo fa per il reinvio dei messaggi. In questo caso, nell'intestazione del messaggio veniva registrato il numero del tentativo di reinvio, legato a un intervallo di ritardo o alla fine della strategia a livello applicativo. Se raggiungevamo la fine della strategia ma il sistema esterno era ancora inaccessibile, il messaggio veniva collocato nella DLQ per un esame manuale.<\/p>\n<p><\/p>\n<h2 id=\"poisk-resheniya\">Ricerca della soluzione<\/h2>\n<p><\/p>\n<p><noindex><a rel=\"nofollow\" href=\"https:\/\/www.google.com\/search?q=kafka+retry+message\">Cercando su internet<\/a><\/noindex>, si pu\u00f2 trovare quanto segue <noindex><a rel=\"nofollow\" href=\"https:\/\/blog.pragmatists.com\/retrying-consumer-architecture-in-the-apache-kafka-939ac4cb851a\">risultato<\/a><\/noindex>. In breve, viene proposta la creazione di un argomento per ogni intervallo di ritardo e l'implementazione da parte dell'applicazione di Consumer che leggeranno i messaggi con il ritardo necessario. <\/p>\n<p><\/p>\n<p><img decoding=\"async\" alt=\"Rielaborazione degli eventi ricevuti da Kafka\" src=\"\/wp-content\/uploads\/2020\/02\/718c4c3f71ccf95b5a6f60e1b2c685d2.jpg\" style=\"display:block;margin: 0 auto;\" \/><\/p>\n<p><\/p>\n<p>Nonostante il grande numero di recensioni positive, mi sembra non del tutto riuscito. Prima di tutto perch\u00e9, oltre a soddisfare i requisiti di business, lo sviluppatore dovr\u00e0 spendere molto tempo per implementare il meccanismo descritto.<\/p>\n<p><\/p>\n<p>Inoltre, se il cluster Kafka ha attivata la gestione degli accessi, ci vorr\u00e0 del tempo per creare i topic e garantire i necessari diritti di accesso. In aggiunta a questo, sar\u00e0 necessario trovare il giusto parametro retention.ms per ciascuno dei topic di retry, affinch\u00e9 i messaggi possano essere reinviati e non vengano persi. L'implementazione e la richiesta di accessi dovranno essere ripetute per ciascun servizio esistente o nuovo.<\/p>\n<p><\/p>\n<p>Vediamo ora quali meccanismi per la rielaborazione dei messaggi ci offre Spring in generale e Spring-Kafka in particolare. Spring-Kafka ha una dipendenza transitiva su Spring-Retry, che fornisce astrazioni per gestire diverse BackOffPolicy. Questo \u00e8 uno strumento piuttosto flessibile, ma un suo notevole svantaggio \u00e8 l'archiviazione dei messaggi per il reinvio nella memoria dell'applicazione. Questo significa che un riavvio dell'applicazione a causa di un aggiornamento o di un errore durante l'utilizzo porter\u00e0 alla perdita di tutti i messaggi in attesa di rielaborazione. Poich\u00e9 questo punto \u00e8 critico per il nostro sistema, non l'abbiamo considerato ulteriormente.<\/p>\n<p><\/p>\n<p>Spring-Kafka stesso fornisce diverse implementazioni di ContainerAwareErrorHandler, ad esempio <noindex><a rel=\"nofollow\" href=\"https:\/\/github.com\/spring-projects\/spring-kafka\/blob\/master\/spring-kafka\/src\/main\/java\/org\/springframework\/kafka\/listener\/SeekToCurrentErrorHandler.java\">SeekToCurrentErrorHandler<\/a><\/noindex>, che permette di elaborare successivamente un messaggio senza spostare l'offset in caso di errore. A partire dalla versione 2.3 di Spring-Kafka \u00e8 stata introdotta la possibilit\u00e0 di definire una BackOffPolicy.<\/p>\n<p><\/p>\n<p>Questo approccio consente ai messaggi rielaborati di sopravvivere al riavvio dell'applicazione, ma il meccanismo DLQ \u00e8 ancora assente. \u00c8 proprio questa opzione che abbiamo scelto all'inizio del 2019, ottimisticamente ritenendo che il DLQ non sarebbe stato necessario (siamo stati fortunati e in effetti non \u00e8 stato necessario per diversi mesi di utilizzo dell'applicazione con questo sistema di rielaborazione). Gli errori temporanei portavano all'attivazione di SeekToCurrentErrorHandler. Altri errori venivano registrati nei log, portavano a uno spostamento dell'offset e l'elaborazione continuava con il messaggio successivo.<\/p>\n<p><\/p>\n<h2 id=\"itogovoe-reshenie\">La soluzione finale<\/h2>\n<p><\/p>\n<p>L'implementazione basata su SeekToCurrentErrorHandler ci ha spinto a sviluppare il nostro meccanismo per la reinvio dei messaggi.<\/p>\n<p><\/p>\n<p>In primo luogo, volevamo utilizzare l'esperienza gi\u00e0 esistente e ampliarla in base alla logica dell'applicazione. Per un'applicazione con logica lineare, sarebbe stato ottimale interrompere la lettura di nuovi messaggi per un breve intervallo di tempo, stabilito all'interno della strategia di retry. Per le altre applicazioni, volevamo avere un unico punto che garantisse l'esecuzione della strategia di retry. Inoltre, questo unico punto dovrebbe possedere funzionalit\u00e0 DLQ per entrambi gli approcci.<\/p>\n<p><\/p>\n<p>La strategia di retry stessa dovrebbe essere conservata nell'applicazione che si occupa di ricevere il successivo intervallo in caso di errore temporaneo.<\/p>\n<p><\/p>\n<h3 id=\"ostanovka-consumera-dlya-prilozheniya-s-lineynoy-logikoy\">Arresto del Consumer per un'applicazione con logica lineare<\/h3>\n<p><\/p>\n<p>Durante l'uso di spring-kafka, il codice per arrestare il Consumer pu\u00f2 apparire pi\u00f9 o meno cos\u00ec:<\/p>\n<p><\/p>\n<pre><code class=\"java\">public void pauseListenerContainer(MessageListenerContainer listenerContainer, \n                                   Instant retryAt) {\n        if (nonNull(retryAt) &amp;&amp; listenerContainer.isRunning()) {\n            listenerContainer.stop();\n            taskScheduler.schedule(() -&gt; listenerContainer.start(), retryAt);\n            return;\n        }\n        \/\/ a DLQ\n    }<\/code><\/pre>\n<p><\/p>\n<p>Nell'esempio, retryAt \u00e8 il momento in cui riavviare il MessageListenerContainer, se \u00e8 ancora in esecuzione. Il riavvio avverr\u00e0 in un thread separato avviato nel TaskScheduler, la cui implementazione \u00e8 fornita anche da spring. <\/p>\n<p><\/p>\n<p>Troviamo il valore di retryAt nel modo seguente:<\/p>\n<p><\/p>\n<ol>\n<li>Si cerca il valore del contatore dei retry.<\/li>\n<li>In base al valore del contatore, si cerca l'intervallo di attesa attuale nella strategia di retry. La strategia \u00e8 dichiarata nell'applicazione stessa e per la sua memorizzazione abbiamo scelto il formato JSON.<\/li>\n<li>L'intervallo trovato nell'array JSON contiene il numero di secondi dopo i quali sar\u00e0 necessario ripetere l'elaborazione. Questo numero di secondi viene aggiunto al tempo attuale, creando il valore per retryAt.<\/li>\n<li>Se l'intervallo non viene trovato, il valore di retryAt \u00e8 null e il messaggio sar\u00e0 inviato a DLQ per un'analisi manuale.<\/li>\n<\/ol>\n<p><\/p>\n<p>Con questo approccio, resta solo da conservare il numero di tentativi per ogni messaggio che \u00e8 attualmente in trattamento, ad esempio nella memoria dell'applicazione. La conservazione del contatore dei tentativi in memoria non \u00e8 critica per questo approccio, poich\u00e9 un'applicazione con logica lineare non pu\u00f2 eseguire la lavorazione nel suo complesso. A differenza di spring-retry, il riavvio dell'applicazione non comporter\u00e0 la perdita di tutti i messaggi da rielaborare, ma semplicemente un riavvio della strategia. <\/p>\n<p><\/p>\n<p>Questo approccio aiuta a ridurre il carico su un sistema esterno, che potrebbe non essere disponibile a causa di un carico molto alto. In altre parole, oltre alla rielaborazione, abbiamo realizzato l'implementazione del pattern <noindex><a rel=\"nofollow\" href=\"https:\/\/microservices.io\/patterns\/reliability\/circuit-breaker.html\">circuit breaker<\/a><\/noindex>.<\/p>\n<p><\/p>\n<p>Nel nostro caso, la soglia di errore \u00e8 di solo 1, e per minimizzare il downtime del sistema a causa di un temporaneo guasto di rete, utilizziamo una strategia di tentativi molto granulare con brevi intervalli di attesa. Potrebbe non essere adatta a tutte le applicazioni del gruppo, quindi la relazione tra la soglia di errore e la dimensione dell'intervallo deve essere regolata in base alle caratteristiche del sistema.<\/p>\n<p><\/p>\n<h3 id=\"otdelnoe-prilozhenie-dlya-obrabotki-soobscheniy-ot-prilozheniy-s-nedeterminirovannoy-logikoy\">Un'applicazione separata per l'elaborazione di messaggi provenienti da applicazioni con logica non deterministica<\/h3>\n<p><\/p>\n<p>Ecco un esempio di codice che invia un messaggio a tale applicazione (Retryer), che eseguir\u00e0 un nuovo invio al topic DESTINATION quando raggiunge il tempo RETRY_AT:<\/p>\n<p><\/p>\n<pre><code class=\"java\">\npublic  void retry(ConsumerRecord record, String retryToTopic, \n                         Instant retryAt, String counter, String groupId, Exception e) {\n        Headers headers = ofNullable(record.headers()).orElse(new RecordHeaders());\n        List<Header> arrayOfHeaders = \n            new ArrayList(Arrays.asList(headers.toArray()));\n        updateHeader(arrayOfHeaders, GROUP_ID, groupId::getBytes);\n        updateHeader(arrayOfHeaders, DESTINATION, retryToTopic::getBytes);\n        updateHeader(arrayOfHeaders, ORIGINAL_PARTITION, \n                     () -&gt; Integer.toString(record.partition()).getBytes());\n        if (nonNull(retryAt)) {\n            updateHeader(arrayOfHeaders, COUNTER, counter::getBytes);\n            updateHeader(arrayOfHeaders, SEND_TO, \"retry\"::getBytes);\n            updateHeader(arrayOfHeaders, RETRY_AT, retryAt.toString()::getBytes);\n        } else {\n            updateHeader(arrayOfHeaders, REASON, \n                         ExceptionUtils.getStackTrace(e)::getBytes);\n            updateHeader(arrayOfHeaders, SEND_TO, \"backout\"::getBytes);\n        }\n        ProducerRecord messageToSend =\n            new ProducerRecord(retryTopic, null, null, record.key(), record.value(), arrayOfHeaders);\n        kafkaTemplate.send(messageToSend);\n    }<\/code><\/pre>\n<p><\/p>\n<p>Dall'esempio si pu\u00f2 notare che molte informazioni vengono trasmesse negli header. Il valore di RETRY_AT si trova allo stesso modo di quanto avviene per il meccanismo di ripetizione tramite la fermata del Consumer. Oltre a DESTINATION e RETRY_AT, trasmettiamo:<\/p>\n<p><\/p>\n<ul>\n<li>GROUP_ID, utilizzato per raggruppare i messaggi per analisi manuale e semplificazione della ricerca.<\/li>\n<li>ORIGINAL_PARTITION, per cercare di mantenere lo stesso Consumer per un rielaborazione. Questo parametro pu\u00f2 essere nullo, in tal caso una nuova partition sar\u00e0 ottenuta tramite la chiave record.key() del messaggio originale.<\/li>\n<li>Valore aggiornato del COUNTER, per seguire la strategia delle ripetizioni.<\/li>\n<li>SEND_TO \u2014 costante che indica se il messaggio debba essere reinviato per la rielaborazione al raggiungimento di RETRY_AT o inserito in DLQ.<\/li>\n<li>REASON \u2014 motivo per cui l'elaborazione del messaggio \u00e8 stata interrotta.<\/li>\n<\/ul>\n<p><\/p>\n<p>Il Retryer conserva i messaggi per il reinvio e l'analisi manuale in PostgreSQL. In base a un timer, viene avviato un compito che trova i messaggi con RETRY_AT raggiunto e li reinvia nella partition ORIGINAL_PARTITION del topic DESTINATION con la chiave record.key().<\/p>\n<p><\/p>\n<p>Dopo l'invio, i messaggi vengono rimossi da PostgreSQL. L'analisi manuale dei messaggi avviene tramite un'interfaccia utente semplice, che interagisce con il Retryer tramite REST API. Le sue principali caratteristiche includono il reinvio o la rimozione dei messaggi dalla DLQ, la visualizzazione delle informazioni sugli errori e la ricerca di messaggi, ad esempio per nome dell'errore. <\/p>\n<p><\/p>\n<p>Poich\u00e9 nei nostri cluster \u00e8 abilitata la gestione degli accessi, \u00e8 necessario richiedere accessi aggiuntivi al topic a cui ascolta il Retryer e consentire a quest'ultimo di scrivere nel topic DESTINATION. \u00c8 scomodo, ma a differenza dell'approccio con il topic a intervallo, abbiamo a disposizione una DLQ completa e un'interfaccia per la sua gestione.<\/p>\n<p><\/p>\n<p>Ci sono casi in cui il topic in ingresso viene letto da pi\u00f9 gruppi di consumer diversi, le cui applicazioni implementano logiche diverse. La rielaborazione di un messaggio tramite il Retryer per una di queste applicazioni dar\u00e0 luogo a un duplicato nell'altra. Per proteggersi da questo, creiamo un topic separato per la rielaborazione. Il topic in ingresso e il topic di retry possono essere letti dallo stesso Consumer senza alcuna restrizione. <\/p>\n<p><\/p>\n<p><img decoding=\"async\" alt=\"Rielaborazione degli eventi ricevuti da Kafka\" src=\"\/wp-content\/uploads\/2020\/02\/06f2355839c6dd812ee3304bf2c72226.jpg\" style=\"display:block;margin: 0 auto;\" \/><\/p>\n<p><\/p>\n<p>Di default, questo approccio non fornisce la possibilit\u00e0 di un circuito di interruzione, tuttavia pu\u00f2 essere aggiunto all'applicazione tramite <noindex><a rel=\"nofollow\" href=\"https:\/\/spring.io\/projects\/spring-cloud-netflix\">spring-cloud-netflix<\/a><\/noindex> o il nuovo <noindex><a rel=\"nofollow\" href=\"https:\/\/spring.io\/projects\/spring-cloud-circuitbreaker\">spring cloud circuit breaker<\/a><\/noindex>, avvolgendo i punti di chiamata ai servizi esterni nelle relative astrazioni. Inoltre, si offre la possibilit\u00e0 di scegliere una strategia per <noindex><a rel=\"nofollow\" href=\"https:\/\/docs.microsoft.com\/en-us\/azure\/architecture\/patterns\/bulkhead\">bulkhead<\/a><\/noindex> un modello, che pu\u00f2 essere utile. Ad esempio, in spring-cloud-netflix pu\u00f2 trattarsi di un thread pool o di un semaforo.<\/p>\n<p><\/p>\n<h2 id=\"vyvod\">Conclusione<\/h2>\n<p><\/p>\n<p>Di conseguenza, abbiamo creato un'applicazione separata che consente di ripetere l'elaborazione di un messaggio in caso di temporanea indisponibilit\u00e0 di un sistema esterno.<\/p>\n<p><\/p>\n<p>Uno dei principali vantaggi dell'applicazione \u00e8 che possono utilizzarla sistemi esterni che operano nello stesso cluster Kafka, senza modifiche significative da parte loro! Questa applicazione avr\u00e0 solo bisogno di accedere al topic di retry, riempiere alcuni header Kafka e inviare il messaggio al Retryer. Non \u00e8 necessario creare infrastrutture aggiuntive. Inoltre, per ridurre il numero di messaggi trasferiti dall'applicazione al Retryer e viceversa, abbiamo isolato le applicazioni con logica lineare e implementato la ripetizione attraverso l'interruzione del Consumer.<\/p>\n<p>Fonte: <a content=\"nofollow\" rel=\"nofollow\" href=\"https:\/\/habr.com\/ru\/company\/tinkoff\/blog\/487094\/\">habr.com<\/a><\/p>","protected":false,"gt_translate_keys":[{"key":"rendered","format":"html"}]},"excerpt":{"rendered":"<p>\u041f\u0440\u0438\u0432\u0435\u0442, \u0425\u0430\u0431\u0440. \u041d\u0435\u0434\u0430\u0432\u043d\u043e \u044f \u043f\u043e\u0434\u0435\u043b\u0438\u043b\u0441\u044f \u043e\u043f\u044b\u0442\u043e\u043c \u043e \u0442\u043e\u043c, \u043a\u0430\u043a\u0438\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u043c\u044b \u0432 \u043a\u043e\u043c\u0430\u043d\u0434\u0435 \u0447\u0430\u0449\u0435 \u0432\u0441\u0435\u0433\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c \u0434\u043b\u044f Kafka Producer \u0438 Consumer, \u0447\u0442\u043e\u0431\u044b \u043f\u0440\u0438\u0431\u043b\u0438\u0437\u0438\u0442\u044c\u0441\u044f \u043a \u0433\u0430\u0440\u0430\u043d\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u0439 \u0434\u043e\u0441\u0442\u0430\u0432\u043a\u0435. \u0412 \u044d\u0442\u043e\u0439 \u0441\u0442\u0430\u0442\u044c\u0435 \u0445\u043e\u0447\u0443 \u0440\u0430\u0441\u0441\u043a\u0430\u0437\u0430\u0442\u044c, \u043a\u0430\u043a \u043c\u044b \u043e\u0440\u0433\u0430\u043d\u0438\u0437\u043e\u0432\u0430\u043b\u0438 \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u0443\u044e \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0443 \u0441\u043e\u0431\u044b\u0442\u0438\u044f, \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u043d\u043e\u0433\u043e \u0438\u0437 Kafka, \u0432 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u0435 \u0432\u0440\u0435\u043c\u0435\u043d\u043d\u043e\u0439 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u043e\u0441\u0442\u0438 \u0432\u043d\u0435\u0448\u043d\u0435\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u044b. \u0421\u043e\u0432\u0440\u0435\u043c\u0435\u043d\u043d\u044b\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0440\u0430\u0431\u043e\u0442\u0430\u044e\u0442 \u0432 \u043e\u0447\u0435\u043d\u044c \u0441\u043b\u043e\u0436\u043d\u043e\u0439 \u0441\u0440\u0435\u0434\u0435. \u0411\u0438\u0437\u043d\u0435\u0441-\u043b\u043e\u0433\u0438\u043a\u0430, \u043e\u0431\u0435\u0440\u043d\u0443\u0442\u0430\u044f [&hellip;]<\/p>\n","protected":false,"gt_translate_keys":[{"key":"rendered","format":"html"}]},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[688],"tags":[],"class_list":["post-56186","post","type-post","status-publish","format-standard","hentry","category-administrirovanie"],"aioseo_notices":[],"aioseo_head":"\n\t\t<!-- All in One SEO 5.0.1.1 - aioseo.com -->\n\t<meta name=\"description\" content=\"\u041f\u0440\u0438\u0432\u0435\u0442, \u0425\u0430\u0431\u0440. \u041d\u0435\u0434\u0430\u0432\u043d\u043e.\" \/>\n\t<meta name=\"robots\" content=\"max-image-preview:large\" \/>\n\t<meta name=\"author\" content=\"Yuri Gagarin\"\/>\n\t<link rel=\"canonical\" href=\"https:\/\/prohoster.info\/it\/blog\/administrirovanie\/povtornaya-obrabotka-sobytij-poluchennyh-iz-kafka\" \/>\n\t<meta name=\"generator\" content=\"All in One SEO (AIOSEO) 5.0.1.1\" \/>\n\t\t<meta property=\"og:locale\" content=\"it_IT\" \/>\n\t\t<meta property=\"og:site_name\" content=\"ProHoster | \u041a\u0443\u043f\u0438\u0442\u044c \u043d\u0430\u0434\u0435\u0436\u043d\u044b\u0439 \u0445\u043e\u0441\u0442\u0438\u043d\u0433 \u0434\u043b\u044f \u0441\u0430\u0439\u0442\u043e\u0432 \u0441 \u0437\u0430\u0449\u0438\u0442\u043e\u0439 \u043e\u0442 DDoS, VPS VDS \u0441\u0435\u0440\u0432\u0435\u0440\u044b\" \/>\n\t\t<meta property=\"og:type\" content=\"article\" \/>\n\t\t<meta property=\"og:title\" content=\"\ud83e\udd47\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430 \u0441\u043e\u0431\u044b\u0442\u0438\u0439, \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u043d\u044b\u0445 \u0438\u0437 Kafka | ProHoster\" \/>\n\t\t<meta property=\"og:description\" content=\"\u041f\u0440\u0438\u0432\u0435\u0442, \u0425\u0430\u0431\u0440. \u041d\u0435\u0434\u0430\u0432\u043d\u043e.\" \/>\n\t\t<meta property=\"og:url\" content=\"https:\/\/prohoster.info\/it\/blog\/administrirovanie\/povtornaya-obrabotka-sobytij-poluchennyh-iz-kafka\" \/>\n\t\t<meta property=\"og:image\" content=\"https:\/\/prohoster.info\/wp-content\/uploads\/2021\/11\/logo-350.jpg\" \/>\n\t\t<meta property=\"og:image:secure_url\" content=\"https:\/\/prohoster.info\/wp-content\/uploads\/2021\/11\/logo-350.jpg\" \/>\n\t\t<meta property=\"og:image:width\" content=\"350\" \/>\n\t\t<meta property=\"og:image:height\" content=\"350\" \/>\n\t\t<meta property=\"article:published_time\" content=\"2020-02-05T21:00:00+00:00\" \/>\n\t\t<meta property=\"article:modified_time\" content=\"2020-02-18T11:04:24+00:00\" \/>\n\t\t<meta property=\"article:publisher\" content=\"https:\/\/www.facebook.com\/prohoster\" \/>\n\t\t<meta property=\"article:author\" content=\"https:\/\/www.facebook.com\/prohoster\" \/>\n\t\t<!-- All in One SEO -->\n\n","aioseo_head_json":{"title":"\ud83e\udd47Ripetizione dell'elaborazione degli eventi ricevuti da Kafka | ProHoster","description":"Ciao, Habr. Recentemente.","canonical_url":"https:\/\/prohoster.info\/it\/blog\/administrirovanie\/povtornaya-obrabotka-sobytij-poluchennyh-iz-kafka","robots":"max-image-preview:large","keywords":"","webmasterTools":{"miscellaneous":""},"schema":null,"og:locale":"it_IT","og:site_name":"ProHoster | \u041a\u0443\u043f\u0438\u0442\u044c \u043d\u0430\u0434\u0435\u0436\u043d\u044b\u0439 \u0445\u043e\u0441\u0442\u0438\u043d\u0433 \u0434\u043b\u044f \u0441\u0430\u0439\u0442\u043e\u0432 \u0441 \u0437\u0430\u0449\u0438\u0442\u043e\u0439 \u043e\u0442 DDoS, VPS VDS \u0441\u0435\u0440\u0432\u0435\u0440\u044b","og:type":"article","og:title":"\ud83e\udd47\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430 \u0441\u043e\u0431\u044b\u0442\u0438\u0439, \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u043d\u044b\u0445 \u0438\u0437 Kafka | ProHoster","og:description":"\u041f\u0440\u0438\u0432\u0435\u0442, \u0425\u0430\u0431\u0440. \u041d\u0435\u0434\u0430\u0432\u043d\u043e.","og:url":"https:\/\/prohoster.info\/it\/blog\/administrirovanie\/povtornaya-obrabotka-sobytij-poluchennyh-iz-kafka","og:image":"https:\/\/prohoster.info\/wp-content\/uploads\/2021\/11\/logo-350.jpg","og:image:secure_url":"https:\/\/prohoster.info\/wp-content\/uploads\/2021\/11\/logo-350.jpg","og:image:width":350,"og:image:height":350,"article:published_time":"2020-02-05T21:00:00+00:00","article:modified_time":"2020-02-18T11:04:24+00:00","article:publisher":"https:\/\/www.facebook.com\/prohoster","article:author":"https:\/\/www.facebook.com\/prohoster"},"aioseo_meta_data":{"post_id":"56186","title":null,"description":null,"keywords":null,"keyphrases":null,"primary_term":null,"canonical_url":null,"og_title":null,"og_description":null,"og_object_type":"default","og_image_type":"default","og_image_url":null,"og_image_width":null,"og_image_height":null,"og_image_custom_url":null,"og_image_custom_fields":null,"og_video":null,"og_custom_url":null,"og_article_section":null,"og_article_tags":null,"twitter_use_og":false,"twitter_card":"default","twitter_image_type":"default","twitter_image_url":null,"twitter_image_custom_url":null,"twitter_image_custom_fields":null,"twitter_title":null,"twitter_description":null,"schema":{"blockGraphs":[],"customGraphs":[],"default":{"data":{"Article":[],"Course":[],"Dataset":[],"FAQPage":[],"Movie":[],"Person":[],"Product":[],"ProductReview":[],"Car":[],"Recipe":[],"Service":[],"SoftwareApplication":[],"WebPage":[]},"graphName":"","isEnabled":true},"graphs":[]},"schema_type":null,"schema_type_options":null,"pillar_content":false,"robots_default":true,"robots_noindex":false,"robots_noarchive":false,"robots_nosnippet":false,"robots_nofollow":false,"robots_noimageindex":false,"robots_noodp":false,"robots_notranslate":false,"robots_max_snippet":null,"robots_max_videopreview":null,"robots_max_imagepreview":"large","priority":null,"frequency":null,"local_seo":null,"seo_analyzer_scan_date":null,"breadcrumb_settings":null,"limit_modified_date":false,"reviewed_by":null,"ai":null,"created":"2021-02-28 19:29:19","updated":"2022-10-02 15:56:10","focus_keyword":null,"additional_keywords":null,"truseo_locale":null},"gt_translate_keys":[{"key":"link","format":"url"}],"_links":{"self":[{"href":"https:\/\/prohoster.info\/it\/wp-json\/wp\/v2\/posts\/56186","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/prohoster.info\/it\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/prohoster.info\/it\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/prohoster.info\/it\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/prohoster.info\/it\/wp-json\/wp\/v2\/comments?post=56186"}],"version-history":[{"count":0,"href":"https:\/\/prohoster.info\/it\/wp-json\/wp\/v2\/posts\/56186\/revisions"}],"wp:attachment":[{"href":"https:\/\/prohoster.info\/it\/wp-json\/wp\/v2\/media?parent=56186"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/prohoster.info\/it\/wp-json\/wp\/v2\/categories?post=56186"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/prohoster.info\/it\/wp-json\/wp\/v2\/tags?post=56186"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}