{"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\/da\/blog\/administrirovanie\/povtornaya-obrabotka-sobytij-poluchennyh-iz-kafka","title":{"rendered":"Genoptagelse af begivenheder modtaget fra Kafka","gt_translate_keys":[{"key":"rendered","format":"text"}]},"content":{"rendered":"<p><img decoding=\"async\" alt=\"Genoptagelse af begivenheder modtaget fra Kafka\" src=\"\/wp-content\/uploads\/2020\/02\/d04319691af5c5de40ccfe4a2268558c.jpg\" style=\"display:block;margin: 0 auto;\" \/><\/p>\n<p><\/p>\n<p>Hej, Habr.<\/p>\n<p><\/p>\n<p>For nylig delte jeg <noindex><a rel=\"nofollow\" href=\"https:\/\/habr.com\/ru\/company\/tinkoff\/blog\/481784\/\">min erfaring<\/a><\/noindex> om hvilke parametre vi oftest bruger i teamet til Kafka Producer og Consumer for at n\u00e6rme os garanteret levering. I denne artikel vil jeg fort\u00e6lle, hvordan vi organiserede genoptagelsen af en begivenhed modtaget fra Kafka som f\u00f8lge af midlertidig utilg\u00e6ngelighed af et eksternt system.<\/p>\n<p><\/p>\n<p>Moderne applikationer fungerer i et meget komplekst milj\u00f8. Forretningslogik pakket ind i en moderne teknologistak, der k\u00f8rer i en Docker-image, som styres af en orkestrator som Kubernetes eller OpenShift, og kommunikerer med andre applikationer eller enterprise-l\u00f8sninger gennem en k\u00e6de af fysiske og virtuelle routere. I et s\u00e5dant milj\u00f8 kan der altid opst\u00e5 problemer, derfor er genoptagelse af begivenheder i tilf\u00e6lde af utilg\u00e6ngelighed af et eksternt system en vigtig del af vores forretningsprocesser.<\/p>\n<p><noindex><a rel=\"nofollow\" name=\"habracut\"><\/a><\/noindex><\/p>\n<h2 id=\"kak-bylo-do-kafka\">Hvordan det var f\u00f8r Kafka<\/h2>\n<p><\/p>\n<p>Tidligere i projektet brugte vi IBM MQ til asynkron levering af beskeder. Ved enhver fejl i serviceprocessen kunne den modtagne besked blive placeret i en dead-letter-queue (DLQ) for manuel behandling. DLQ blev oprettet ved siden af den indkommende k\u00f8, og flytningen af beskeden skete inden for IBM MQ. <\/p>\n<p><\/p>\n<p>Hvis fejlen var midlertidig, og vi kunne identificere det (f.eks. ResourceAccessException ved HTTP-kald eller MongoTimeoutException ved foresp\u00f8rgsel til MongoDb), tr\u00e5dte strategien for genkaldelser i kraft. Uanset hvordan applikationens logik grenes, blev den oprindelige besked flyttet til enten en systemk\u00f8 for forsinket levering eller til en separat applikation, der engang blev lavet til genlevering af beskeder. Heri skrives nummeret p\u00e5 genleveringen i beskedens header, hvilket er knyttet til forsinkelsesintervallet eller slutningen af strategien p\u00e5 applikationsniveau. Hvis vi n\u00e5r slutningen af strategien, men det eksterne system stadig er utilg\u00e6ngeligt, placeres beskeden i DLQ til manuel behandling.<\/p>\n<p><\/p>\n<h2 id=\"poisk-resheniya\">S\u00f8gning efter l\u00f8sning<\/h2>\n<p><\/p>\n<p><noindex><a rel=\"nofollow\" href=\"https:\/\/www.google.com\/search?q=kafka+retry+message\">Ved at s\u00f8ge p\u00e5 internettet<\/a><\/noindex>, kan man finde f\u00f8lgende <noindex><a rel=\"nofollow\" href=\"https:\/\/blog.pragmatists.com\/retrying-consumer-architecture-in-the-apache-kafka-939ac4cb851a\">l\u00f8sning<\/a><\/noindex>. Hvis man skal opsummere, foresl\u00e5s det at oprette et emne for hver forsinkelse og implementere Consumers p\u00e5 applikationssiden, som vil l\u00e6se beskeder med den n\u00f8dvendige forsinkelse. <\/p>\n<p><\/p>\n<p><img decoding=\"async\" alt=\"Genoptagelse af begivenheder modtaget fra Kafka\" src=\"\/wp-content\/uploads\/2020\/02\/718c4c3f71ccf95b5a6f60e1b2c685d2.jpg\" style=\"display:block;margin: 0 auto;\" \/><\/p>\n<p><\/p>\n<p>P\u00e5 trods af det store antal positive anmeldelser virker det ikke helt vellykket for mig. F\u00f8rst og fremmest fordi udvikleren, udover at implementere forretningskravene, vil skulle bruge meget tid p\u00e5 at implementere den beskrevne mekanisme.<\/p>\n<p><\/p>\n<p>Derudover, hvis adgangskontrol er aktiveret p\u00e5 Kafka-klyngen, vil der g\u00e5 noget tid med at oprette emner og sikre de n\u00f8dvendige adgange til dem. Udover dette skal den rette parameter retention.ms v\u00e6lges for hver af retry-emnerne, s\u00e5 beskederne n\u00e5r at blive sendt igen og ikke g\u00e5r tabt. Implementeringen og anmodningen om adgange skal gentages for hver eksisterende eller ny service.<\/p>\n<p><\/p>\n<p>Lad os nu se p\u00e5, hvilke mekanismer der tilbydes af spring til genbehandling af beskeder generelt og af spring-kafka specifikt. Spring-kafka har en transitive afh\u00e6ngighed til spring-retry, som tilbyder abstraktioner til at styre forskellige BackOffPolicy. Dette er et ret fleksibelt v\u00e6rkt\u00f8j, men dets betydelige ulempe er, at beskeder til gensendelse opbevares i applikationens hukommelse. Det betyder, at genstart af applikationen p\u00e5 grund af opdateringer eller fejl under drift vil f\u00f8re til tab af alle beskeder, der venter p\u00e5 genbehandling. Da dette punkt er kritisk for vores system, besluttede vi ikke at overveje det yderligere.<\/p>\n<p><\/p>\n<p>Selve spring-kafka tilbyder flere implementeringer af ContainerAwareErrorHandler, for eksempel <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>, som g\u00f8r det muligt at behandle en besked senere uden at flytte offset i tilf\u00e6lde af en fejl. Fra version 2.3 af spring-kafka er det blevet muligt at angive BackOffPolicy.<\/p>\n<p><\/p>\n<p>Denne tilgang lader genbehandlede beskeder overleve genstart af applikationen, men mekanismen DLQ mangler stadig. Det var netop denne l\u00f8sning, vi valgte i starten af 2019, optimistisk i troen p\u00e5, at DLQ ikke ville v\u00e6re n\u00f8dvendigt (vi var heldige, og det blev faktisk ikke n\u00f8dvendigt i flere m\u00e5neder med drift af applikationen med dette system til genbehandling). Midlertidige fejl f\u00f8rte til aktivering af SeekToCurrentErrorHandler. Andre fejl blev udskrevet i loggen, f\u00f8rte til flytning af offset, og behandlingen fortsatte med den n\u00e6ste besked.<\/p>\n<p><\/p>\n<h2 id=\"itogovoe-reshenie\">Den endelige l\u00f8sning<\/h2>\n<p><\/p>\n<p>Implementeringen baseret p\u00e5 SeekToCurrentErrorHandler har tilskyndet os til at udvikle vores egen mekanisme til genudsendelse af meddelelser.<\/p>\n<p><\/p>\n<p>F\u00f8rst og fremmest \u00f8nskede vi at udnytte eksisterende erfaring og udvide den afh\u00e6ngigt af applikationens logik. For en applikation med line\u00e6r logik ville det v\u00e6re optimalt at stoppe med at l\u00e6se nye meddelelser i en kort periode, fastsat i genudsendelsesstrategien. For de andre applikationer \u00f8nskede vi at have et enkelt punkt, der ville sikre udf\u00f8relsen af genudsendelsesstrategien. Derudover skulle dette enhedspunkt have funktionalitet til DLQ for begge tilgange.<\/p>\n<p><\/p>\n<p>Selve genudsendelsesstrategien skal v\u00e6re gemt i applikationen, der er ansvarlig for at modtage det n\u00e6ste interval ved opst\u00e5en af en midlertidig fejl.<\/p>\n<p><\/p>\n<h3 id=\"ostanovka-consumera-dlya-prilozheniya-s-lineynoy-logikoy\">Stop af Consumer'en for applikationen med line\u00e6r logik<\/h3>\n<p><\/p>\n<p>N\u00e5r man arbejder med spring-kafka, kan koden til at stoppe Consumer'en se omtrent s\u00e5dan ud:<\/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        \/\/ til DLQ\n    }<\/code><\/pre>\n<p><\/p>\n<p>I eksemplet er retryAt det tidspunkt, hvor MessageListenerContainer skal genstartes, hvis det stadig er i gang. Genstarten vil finde sted i en separat tr\u00e5d, der er startet i TaskScheduler, hvis implementering ogs\u00e5 leveres af spring. <\/p>\n<p><\/p>\n<p>V\u00e6rdien af retryAt finder vi p\u00e5 f\u00f8lgende m\u00e5de:<\/p>\n<p><\/p>\n<ol>\n<li>V\u00e6rdien af genudsendelsest\u00e6lleren s\u00f8ges.<\/li>\n<li>I overensstemmelse med v\u00e6rdien af t\u00e6lleren findes det aktuelle forsinkelsesinterval i genudsendelsesstrategien. Strategien erkl\u00e6res i selve applikationen; til opbevaring har vi valgt JSON-formatet.<\/li>\n<li>Det fundne interval i JSON-arrayet indeholder antallet af sekunder, hvorefter behandlingen skal gentages. Dette antal sekunder l\u00e6gges til den aktuelle tid, hvilket danner v\u00e6rdien for retryAt.<\/li>\n<li>Hvis intervallet ikke findes, er v\u00e6rdien af retryAt null, og meddelelsen vil blive sendt til DLQ til manuel gennemgang.<\/li>\n<\/ol>\n<p><\/p>\n<p>Med denne tilgang er det kun muligt at gemme antallet af genkald for hver besked, der i \u00f8jeblikket er under behandling, for eksempel i applikationens hukommelse. At gemme antallet af fors\u00f8g i hukommelsen er ikke kritisk for denne tilgang, da en applikation med line\u00e6r logik ikke kan udf\u00f8re behandlingen som helhed. I mods\u00e6tning til spring-retry vil en genstart af applikationen ikke f\u00f8re til tab af alle beskeder til genbehandling, men blot genstarte strategien. <\/p>\n<p><\/p>\n<p>Denne tilgang hj\u00e6lper med at reducere belastningen p\u00e5 det eksterne system, som muligvis ikke er tilg\u00e6ngeligt p\u00e5 grund af en meget h\u00f8j belastning. Med andre ord har vi udover genbehandling opn\u00e5et implementeringen af et m\u00f8nster. <noindex><a rel=\"nofollow\" href=\"https:\/\/microservices.io\/patterns\/reliability\/circuit-breaker.html\">circuit breaker<\/a><\/noindex>.<\/p>\n<p><\/p>\n<p>I vores tilf\u00e6lde er fejlt\u00e6rsklen kun 1, og for at minimere systemets nedetid p\u00e5 grund af midlertidige netv\u00e6rksproblemer anvender vi en meget granular strategi for genkald med sm\u00e5 ventetider. Dette er m\u00e5ske ikke egnet til alle applikationer i koncernen, s\u00e5 forholdet mellem fejlt\u00e6rsklen og st\u00f8rrelsen p\u00e5 intervallet skal tilpasses baseret p\u00e5 systemets egenskaber.<\/p>\n<p><\/p>\n<h3 id=\"otdelnoe-prilozhenie-dlya-obrabotki-soobscheniy-ot-prilozheniy-s-nedeterminirovannoy-logikoy\">En s\u00e6rskilt applikation til behandling af beskeder fra applikationer med nondeterministisk logik<\/h3>\n<p><\/p>\n<p>Her er et kodeeksempel, der sender en besked til en s\u00e5dan applikation (Retryer), som vil fors\u00f8ge at sende igen til emnet DESTINATION, n\u00e5r tiden RETRY_AT er n\u00e5et:<\/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>Eksemplet viser, at der overf\u00f8res meget information i headerne. V\u00e6rdien RETRY_AT findes ligeledes som for mekanismen til gentagelse via stoppet af Consumer\u2019en. Udover DESTINATION og RETRY_AT overf\u00f8rer vi:<\/p>\n<p><\/p>\n<ul>\n<li>GROUP_ID, som vi bruger til at gruppere meddelelser til manuel analyse og forenkling af s\u00f8gningen.<\/li>\n<li>ORIGINAL_PARTITION, for at fors\u00f8ge at bevare den samme Consumer til genbehandling. Denne parameter kan v\u00e6re lig med null, i s\u00e5 fald vil en ny partition blive opn\u00e5et baseret p\u00e5 n\u00f8gle record.key() fra den originale meddelelse.<\/li>\n<li>Det opdaterede v\u00e6rdig COUNTER, s\u00e5 vi kan f\u00f8lge strategien for gentagelser.<\/li>\n<li>SEND_TO \u2014 en konstant, der angiver, om meddelelsen skal sendes til gentagelse, n\u00e5r RETRY_AT n\u00e5s, eller om den skal placeres i DLQ.<\/li>\n<li>REASON \u2014 \u00e5rsagen til, at behandlingen af meddelelsen blev afbrudt.<\/li>\n<\/ul>\n<p><\/p>\n<p>Retryer gemmer meddelelser til genudsendelse og manuel gennemgang i PostgreSQL. En opgave starter med et tidsur, der finder meddelelser med n\u00e5et RETRY_AT og sender dem tilbage til partitionen ORIGINAL_PARTITION i DESTINATION-topikken med n\u00f8glen record.key().<\/p>\n<p><\/p>\n<p>Efter at meddelelsen er sendt, fjernes de fra PostgreSQL. Den manuelle gennemgang af meddelelser sker i en simpel UI, der interagerer med Retryer via REST API. Dens vigtigste funktioner inkluderer genudsendelse eller sletning af meddelelser fra DLQ, visning af fejloplysninger og s\u00f8gning af meddelelser, eksempelvis efter fejlens navn. <\/p>\n<p><\/p>\n<p>Da adgangsstyring er aktiveret p\u00e5 vores klynger, er det n\u00f8dvendigt at anmode om adgang til den topik, som Retryer lytter til, og give Retryer mulighed for at skrive til DESTINATION-topikken. Dette er besv\u00e6rligt, men i mods\u00e6tning til tilgangen med en interval-topik f\u00e5r vi en fuldgyldig DLQ og UI til at administrere den.<\/p>\n<p><\/p>\n<p>Der er tilf\u00e6lde, hvor den indg\u00e5ende topik l\u00e6ses af flere forskellige consumer-grupper, hvis applikationer implementerer forskellig logik. Genbehandling af meddelelsen gennem Retryer for en af disse applikationer vil f\u00f8re til en duplikat i en anden. For at beskytte os mod dette opretter vi en separat topik til genbehandling. Den indg\u00e5ende og retry-topik kan l\u00e6ses af den samme Consumer uden begr\u00e6nsninger. <\/p>\n<p><\/p>\n<p><img decoding=\"async\" alt=\"Genoptagelse af begivenheder modtaget fra Kafka\" src=\"\/wp-content\/uploads\/2020\/02\/06f2355839c6dd812ee3304bf2c72226.jpg\" style=\"display:block;margin: 0 auto;\" \/><\/p>\n<p><\/p>\n<p>Som standard giver denne tilgang ikke mulighed for circuit breaker, men det kan tilf\u00f8jes i applikationen ved hj\u00e6lp af <noindex><a rel=\"nofollow\" href=\"https:\/\/spring.io\/projects\/spring-cloud-netflix\">spring-cloud-netflix<\/a><\/noindex> eller den nye <noindex><a rel=\"nofollow\" href=\"https:\/\/spring.io\/projects\/spring-cloud-circuitbreaker\">spring cloud circuit breaker<\/a><\/noindex>, ved at indkapsle opkald til eksterne tjenester i de relevante abstraktioner. Derudover \u00e5bnes der mulighed for at v\u00e6lge strategi for <noindex><a rel=\"nofollow\" href=\"https:\/\/docs.microsoft.com\/en-us\/azure\/architecture\/patterns\/bulkhead\">bulkhead<\/a><\/noindex> m\u00f8nstre, hvilket ogs\u00e5 kan v\u00e6re nyttigt. For eksempel kan det i spring-cloud-netflix v\u00e6re en tr\u00e5dgruppe eller et semaphore.<\/p>\n<p><\/p>\n<h2 id=\"vyvod\">Output<\/h2>\n<p><\/p>\n<p>Som resultat har vi f\u00e5et en separat applikation, der g\u00f8r det muligt at gentage behandlingen af en besked, n\u00e5r en ekstern system midlertidigt ikke er tilg\u00e6ngelig.<\/p>\n<p><\/p>\n<p>En af de vigtigste fordele ved applikationen er, at eksterne systemer, der k\u00f8rer p\u00e5 den samme Kafka-klynge, kan bruge den uden betydelige \u00e6ndringer p\u00e5 deres side! S\u00e5dan en applikation har kun brug for at f\u00e5 adgang til retry-topikken, udfylde nogle Kafka-headere og sende beskeden til Retryer. Der er ikke behov for at oprette yderligere infrastruktur. For at reducere antallet af beskeder, der overf\u00f8res fra applikationen til Retryer og tilbage, har vi adskilt applikationer med line\u00e6r logik og gjort dem i stand til at gentage behandling gennem stop af Consumer.<\/p>\n<p>Kilde: <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.3 - 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\/da\/blog\/administrirovanie\/povtornaya-obrabotka-sobytij-poluchennyh-iz-kafka\" \/>\n\t\t<meta name=\"generator\" content=\"All in One SEO (AIOSEO) 5.0.3\" \/>\n\t\t<meta property=\"og:locale\" content=\"da_DK\" \/>\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\/da\/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\udd47Gentagelse af h\u00e6ndelser modtaget fra Kafka | ProHoster","description":"Hej, Habr. For nylig.","canonical_url":"https:\/\/prohoster.info\/da\/blog\/administrirovanie\/povtornaya-obrabotka-sobytij-poluchennyh-iz-kafka","robots":"max-image-preview:large","keywords":"","webmasterTools":{"miscellaneous":""},"schema":null,"og:locale":"da_DK","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\/da\/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\/da\/wp-json\/wp\/v2\/posts\/56186","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/prohoster.info\/da\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/prohoster.info\/da\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/prohoster.info\/da\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/prohoster.info\/da\/wp-json\/wp\/v2\/comments?post=56186"}],"version-history":[{"count":0,"href":"https:\/\/prohoster.info\/da\/wp-json\/wp\/v2\/posts\/56186\/revisions"}],"wp:attachment":[{"href":"https:\/\/prohoster.info\/da\/wp-json\/wp\/v2\/media?parent=56186"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/prohoster.info\/da\/wp-json\/wp\/v2\/categories?post=56186"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/prohoster.info\/da\/wp-json\/wp\/v2\/tags?post=56186"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}