{"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\/fr\/blog\/administrirovanie\/povtornaya-obrabotka-sobytij-poluchennyh-iz-kafka","title":{"rendered":"Reprocessing des \u00e9v\u00e9nements re\u00e7us de Kafka","gt_translate_keys":[{"key":"rendered","format":"text"}]},"content":{"rendered":"<p><img decoding=\"async\" alt=\"Reprocessing des \u00e9v\u00e9nements re\u00e7us de Kafka\" src=\"\/wp-content\/uploads\/2020\/02\/d04319691af5c5de40ccfe4a2268558c.jpg\" style=\"display:block;margin: 0 auto;\" \/><\/p>\n<p><\/p>\n<p>Bonjour, Habr.<\/p>\n<p><\/p>\n<p>R\u00e9cemment, j'ai <noindex><a rel=\"nofollow\" href=\"https:\/\/habr.com\/ru\/company\/tinkoff\/blog\/481784\/\">partag\u00e9 mon exp\u00e9rience<\/a><\/noindex> sur les param\u00e8tres que nous utilisons le plus souvent dans notre \u00e9quipe pour Kafka Producer et Consumer, afin de nous rapprocher d'une livraison garantie. Dans cet article, je souhaite expliquer comment nous avons organis\u00e9 le traitement des \u00e9v\u00e9nements re\u00e7us de Kafka en raison de l'indisponibilit\u00e9 temporaire d'un syst\u00e8me externe.<\/p>\n<p><\/p>\n<p>Les applications modernes fonctionnent dans un environnement tr\u00e8s complexe. La logique m\u00e9tier, envelopp\u00e9e dans une pile technologique moderne, fonctionne dans une image Docker g\u00e9r\u00e9e par un orchestrateur tel que Kubernetes ou OpenShift, et communique avec d'autres applications ou des solutions d'entreprise via une cha\u00eene de routeurs physiques et virtuels. Dans un tel environnement, quelque chose peut toujours casser, c'est pourquoi le traitement des \u00e9v\u00e9nements en cas d'indisponibilit\u00e9 de l'un des syst\u00e8mes externes est une partie importante de nos processus m\u00e9tier.<\/p>\n<p><noindex><a rel=\"nofollow\" name=\"habracut\"><\/a><\/noindex><\/p>\n<h2 id=\"kak-bylo-do-kafka\">Comment c'\u00e9tait avant Kafka<\/h2>\n<p><\/p>\n<p>Auparavant, dans le projet, nous utilisions IBM MQ pour la livraison asynchrone des messages. En cas d'erreur survenant dans le processus de travail du service, le message re\u00e7u pouvait \u00eatre plac\u00e9 dans une dead-letter-queue (DLQ) pour un examen manuel ult\u00e9rieur. La DLQ \u00e9tait cr\u00e9\u00e9e \u00e0 c\u00f4t\u00e9 de la file d'attente entrante, le transfert du message se faisait au sein d'IBM MQ. <\/p>\n<p><\/p>\n<p>Si l'erreur \u00e9tait temporaire et que nous pouvions le d\u00e9terminer (par exemple, ResourceAccessException lors d'un appel HTTP ou MongoTimeoutException lors d'une requ\u00eate \u00e0 MongoDb), alors la strat\u00e9gie des appels r\u00e9p\u00e9t\u00e9s entrait en vigueur. Peu importe la branche de la logique de l'application, le message d'origine \u00e9tait transf\u00e9r\u00e9 soit dans une file d'attente syst\u00e8me pour un envoi diff\u00e9r\u00e9, soit dans une application distincte qui avait \u00e9t\u00e9 cr\u00e9\u00e9e il y a longtemps pour renvoyer des messages. Dans ce cas, le num\u00e9ro de r\u00e9exp\u00e9dition, li\u00e9 \u00e0 l'intervalle de retard ou \u00e0 la fin de la strat\u00e9gie au niveau de l'application, \u00e9tait enregistr\u00e9 dans l'en-t\u00eate du message. Si nous atteignons la fin de la strat\u00e9gie mais que le syst\u00e8me externe reste indisponible, alors le message sera plac\u00e9 dans une DLQ pour un examen manuel.<\/p>\n<p><\/p>\n<h2 id=\"poisk-resheniya\">Recherche de solution<\/h2>\n<p><\/p>\n<p><noindex><a rel=\"nofollow\" href=\"https:\/\/www.google.com\/search?q=kafka+retry+message\">En cherchant sur Internet<\/a><\/noindex>, on peut trouver ce qui suit <noindex><a rel=\"nofollow\" href=\"https:\/\/blog.pragmatists.com\/retrying-consumer-architecture-in-the-apache-kafka-939ac4cb851a\">la solution<\/a><\/noindex>. En bref, il est propos\u00e9 de cr\u00e9er un topic par intervalle de retard et de mettre en \u0153uvre des Consumers du c\u00f4t\u00e9 de l'application, qui liront les messages avec le retard n\u00e9cessaire. <\/p>\n<p><\/p>\n<p><img decoding=\"async\" alt=\"Reprocessing des \u00e9v\u00e9nements re\u00e7us de Kafka\" src=\"\/wp-content\/uploads\/2020\/02\/718c4c3f71ccf95b5a6f60e1b2c685d2.jpg\" style=\"display:block;margin: 0 auto;\" \/><\/p>\n<p><\/p>\n<p>Bien qu'il y ait de nombreux avis positifs, il me semble qu'il n'est pas tout \u00e0 fait r\u00e9ussi. Tout d'abord, parce que le d\u00e9veloppeur, en plus de r\u00e9pondre aux exigences commerciales, devra consacrer beaucoup de temps \u00e0 la mise en \u0153uvre du m\u00e9canisme d\u00e9crit.<\/p>\n<p><\/p>\n<p>De plus, si la gestion des acc\u00e8s est activ\u00e9e sur le cluster Kafka, il faudra passer un certain temps \u00e0 cr\u00e9er des topics et \u00e0 garantir les acc\u00e8s n\u00e9cessaires \u00e0 ceux-ci. En outre, il sera n\u00e9cessaire de choisir le bon param\u00e8tre retention.ms pour chacun des topics de r\u00e9essai, afin que les messages puissent \u00eatre renvoy\u00e9s \u00e0 temps et ne disparaissent pas. La mise en \u0153uvre et la demande d'acc\u00e8s devront \u00eatre r\u00e9p\u00e9t\u00e9es pour chaque service existant ou nouveau.<\/p>\n<p><\/p>\n<p>Voyons maintenant quels m\u00e9canismes de traitement des messages en double nous propose spring en g\u00e9n\u00e9ral et spring-kafka en particulier. Spring-kafka a une d\u00e9pendance transitive sur spring-retry, qui fournit des abstractions pour g\u00e9rer diff\u00e9rentes BackOffPolicy. C'est un outil assez flexible, mais son inconv\u00e9nient majeur est le stockage des messages \u00e0 renvoyer en m\u00e9moire de l'application. Cela signifie qu'un red\u00e9marrage de l'application en raison d'une mise \u00e0 jour ou d'une erreur pendant l'exploitation entra\u00eenera la perte de tous les messages en attente de traitement \u00e0 nouveau. \u00c9tant donn\u00e9 que ce point est critique pour notre syst\u00e8me, nous avons choisi de ne pas le consid\u00e9rer davantage.<\/p>\n<p><\/p>\n<p>La biblioth\u00e8que spring-kafka propose plusieurs impl\u00e9mentations de ContainerAwareErrorHandler, par exemple <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>, qui permet de traiter le message plus tard sans d\u00e9placer l'offset en cas d'erreur. Depuis la version 2.3 de spring-kafka, il est possible de d\u00e9finir une BackOffPolicy.<\/p>\n<p><\/p>\n<p>Cette approche permet aux messages en r\u00e9essai de survivre au red\u00e9marrage de l'application, mais le m\u00e9canisme DLQ est toujours absent. C'est cette option que nous avons choisie au d\u00e9but de 2019, en pensant de mani\u00e8re optimiste que le DLQ ne serait pas n\u00e9cessaire (nous avons eu de la chance et il ne fut effectivement pas n\u00e9cessaire pendant quelques mois d'exploitation de l'application avec ce syst\u00e8me de r\u00e9essai). Des erreurs temporaires provoquaient l'activation de SeekToCurrentErrorHandler. Les autres erreurs \u00e9taient enregistr\u00e9es dans les logs, entra\u00eenant un d\u00e9calage de l'offset, et le traitement se poursuivait avec le message suivant.<\/p>\n<p><\/p>\n<h2 id=\"itogovoe-reshenie\">Solution finale<\/h2>\n<p><\/p>\n<p>La mise en \u0153uvre bas\u00e9e sur SeekToCurrentErrorHandler nous a pouss\u00e9s \u00e0 d\u00e9velopper notre propre m\u00e9canisme pour le renvoi des messages.<\/p>\n<p><\/p>\n<p>Avant tout, nous voulions tirer parti de l'exp\u00e9rience existante et l'\u00e9largir en fonction de la logique de l'application. Pour une application \u00e0 logique lin\u00e9aire, il serait optimal d'arr\u00eater la lecture de nouveaux messages pendant un court laps de temps d\u00e9fini dans la strat\u00e9gie de r\u00e9essai. Pour les autres applications, nous souhaiterions avoir un point central qui garantirait l'application de la strat\u00e9gie de r\u00e9essai. De plus, ce point unique doit disposer d'une fonctionnalit\u00e9 DLQ pour les deux approches.<\/p>\n<p><\/p>\n<p>La strat\u00e9gie de r\u00e9essai elle-m\u00eame doit \u00eatre stock\u00e9e dans l'application responsable de la r\u00e9cup\u00e9ration du prochain intervalle en cas d'erreur temporaire.<\/p>\n<p><\/p>\n<h3 id=\"ostanovka-consumera-dlya-prilozheniya-s-lineynoy-logikoy\">Arr\u00eat du Consumer pour une application \u00e0 logique lin\u00e9aire<\/h3>\n<p><\/p>\n<p>Lors de l'utilisation de spring-kafka, le code pour arr\u00eater le Consumer peut ressembler \u00e0 ceci :<\/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        \/\/ vers DLQ\n    }<\/code><\/pre>\n<p><\/p>\n<p>Dans cet exemple, retryAt est le moment auquel il faut red\u00e9marrer le MessageListenerContainer s'il est encore en cours d'ex\u00e9cution. Le red\u00e9marrage se fera dans un thread s\u00e9par\u00e9, lanc\u00e9 par TaskScheduler, dont l'impl\u00e9mentation est \u00e9galement fournie par spring. <\/p>\n<p><\/p>\n<p>Nous trouvons la valeur de retryAt de la mani\u00e8re suivante :<\/p>\n<p><\/p>\n<ol>\n<li>Nous recherchons la valeur du compteur de r\u00e9essais.<\/li>\n<li>En fonction de la valeur du compteur, l'intervalle de d\u00e9lai actuel est recherch\u00e9 dans la strat\u00e9gie de r\u00e9essai. La strat\u00e9gie est d\u00e9clar\u00e9e dans l'application elle-m\u00eame, et nous avons choisi le format JSON pour son stockage.<\/li>\n<li>L'intervalle trouv\u00e9 dans le tableau JSON contient le nombre de secondes apr\u00e8s lesquelles il faudra r\u00e9essayer le traitement. Ce nombre de secondes est ajout\u00e9 au temps actuel, formant ainsi la valeur pour retryAt.<\/li>\n<li>Si l'intervalle n'est pas trouv\u00e9, la valeur de retryAt est nulle et le message sera envoy\u00e9 dans la DLQ pour un traitement manuel.<\/li>\n<\/ol>\n<p><\/p>\n<p>Avec cette approche, il ne reste qu'\u00e0 conserver le nombre de tentatives pour chaque message actuellement en cours de traitement, par exemple dans la m\u00e9moire de l'application. Conserver le compteur de tentatives en m\u00e9moire n'est pas critique pour cette approche, car une application avec une logique lin\u00e9aire ne peut pas traiter l'ensemble. Contrairement \u00e0 spring-retry, le red\u00e9marrage de l'application ne conduira pas \u00e0 la perte de tous les messages \u00e0 r\u00e9it\u00e9rer, mais simplement \u00e0 un red\u00e9marrage de la strat\u00e9gie. <\/p>\n<p><\/p>\n<p>Cette approche permet de r\u00e9duire la charge sur le syst\u00e8me externe, qui peut \u00eatre indisponible en raison d'une tr\u00e8s forte charge. En d'autres termes, en plus de la r\u00e9it\u00e9ration, nous avons r\u00e9ussi \u00e0 mettre en \u0153uvre le mod\u00e8le. <noindex><a rel=\"nofollow\" href=\"https:\/\/microservices.io\/patterns\/reliability\/circuit-breaker.html\">disjoncteur<\/a><\/noindex>.<\/p>\n<p><\/p>\n<p>Dans notre cas, le seuil d'erreur est de seulement 1, et pour minimiser le temps d'arr\u00eat du syst\u00e8me en raison d'une panne de r\u00e9seau temporaire, nous utilisons une strat\u00e9gie de r\u00e9it\u00e9ration tr\u00e8s granulaire avec de courtes intervalles de d\u00e9lai. Cela peut ne pas convenir \u00e0 toutes les applications du groupe, donc le ratio entre le seuil d'erreur et la taille de l'intervalle doit \u00eatre ajust\u00e9 en fonction des caract\u00e9ristiques du syst\u00e8me.<\/p>\n<p><\/p>\n<h3 id=\"otdelnoe-prilozhenie-dlya-obrabotki-soobscheniy-ot-prilozheniy-s-nedeterminirovannoy-logikoy\">Une application distincte pour le traitement des messages provenant d'applications avec une logique non d\u00e9terministe<\/h3>\n<p><\/p>\n<p>Voici un exemple de code qui envoie un message \u00e0 une telle application (Retryer), qui effectuera une nouvelle tentative d'envoi vers le sujet DESTINATION lorsque le temps RETRY_AT sera atteint :<\/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>L'exemple montre qu'une grande quantit\u00e9 d'informations est transmise dans les en-t\u00eates. La valeur RETRY_AT est d\u00e9termin\u00e9e de la m\u00eame mani\u00e8re que pour le m\u00e9canisme de r\u00e9p\u00e9tition via l'arr\u00eat du Consumer. En plus de DESTINATION et RETRY_AT, nous transmettons :<\/p>\n<p><\/p>\n<ul>\n<li>GROUP_ID, par lequel nous groupons les messages pour une analyse manuelle et simplifions la recherche.<\/li>\n<li>ORIGINAL_PARTITION, afin d'essayer de conserver le m\u00eame Consumer pour le retraitement. Ce param\u00e8tre peut \u00eatre null, auquel cas une nouvelle partition sera obtenue par la cl\u00e9 record.key() du message original.<\/li>\n<li>La valeur mise \u00e0 jour du COUNTER, pour suivre la strat\u00e9gie de r\u00e9p\u00e9titions.<\/li>\n<li>SEND_TO \u2014 une constante qui indique s'il faut renvoyer le message pour retraitement lorsqu'on atteint RETRY_AT ou le placer dans la DLQ.<\/li>\n<li>REASON \u2014 la raison pour laquelle le traitement du message a \u00e9t\u00e9 interrompu.<\/li>\n<\/ul>\n<p><\/p>\n<p>Le Retryer conserve les messages pour un nouvel envoi et une analyse manuelle dans PostgreSQL. Une t\u00e2che est lanc\u00e9e selon un minuteur, qui trouve les messages dont le RETRY_AT est atteint et les renvoie dans la partition ORIGINAL_PARTITION du sujet DESTINATION avec la cl\u00e9 record.key().<\/p>\n<p><\/p>\n<p>Apr\u00e8s l'envoi, les messages sont supprim\u00e9s de PostgreSQL. L'analyse manuelle des messages se d\u00e9roule dans une interface simple qui interagit avec le Retryer via l'API REST. Ses principales caract\u00e9ristiques sont la r\u00e9exp\u00e9dition ou la suppression de messages de la DLQ, la consultation des informations d'erreur et la recherche de messages, par exemple, par nom d'erreur. <\/p>\n<p><\/p>\n<p>Comme l'acc\u00e8s est contr\u00f4l\u00e9 sur nos clusters, il est n\u00e9cessaire de demander l'acc\u00e8s au sujet \u00e9cout\u00e9 par le Retryer et de permettre au Retryer d'\u00e9crire dans le sujet DESTINATION. C'est peu pratique, mais, contrairement \u00e0 l'approche avec un sujet bas\u00e9 sur des intervalles, nous disposons d'une v\u00e9ritable DLQ et d'une interface utilisateur pour la g\u00e9rer.<\/p>\n<p><\/p>\n<p>Il arrive que le sujet entrant soit lu par plusieurs groupes de consommateurs diff\u00e9rents, dont les applications mettent en \u0153uvre une logique vari\u00e9e. La r\u00e9p\u00e9tition d'un message via le Retryer pour l'une de ces applications entra\u00eenera un doublon pour l'autre. Pour se prot\u00e9ger contre cela, nous cr\u00e9ons un sujet distinct pour le retraitement. Le sujet entrant et le sujet de r\u00e9p\u00e9tition peuvent \u00eatre lus par le m\u00eame Consumer sans aucune restriction. <\/p>\n<p><\/p>\n<p><img decoding=\"async\" alt=\"Reprocessing des \u00e9v\u00e9nements re\u00e7us de Kafka\" src=\"\/wp-content\/uploads\/2020\/02\/06f2355839c6dd812ee3304bf2c72226.jpg\" style=\"display:block;margin: 0 auto;\" \/><\/p>\n<p><\/p>\n<p>Par d\u00e9faut, cette approche ne fournit pas la possibilit\u00e9 d'un circuit breaker, mais cela peut \u00eatre ajout\u00e9 dans l'application \u00e0 l'aide de <noindex><a rel=\"nofollow\" href=\"https:\/\/spring.io\/projects\/spring-cloud-netflix\">spring-cloud-netflix<\/a><\/noindex> ou du nouveau <noindex><a rel=\"nofollow\" href=\"https:\/\/spring.io\/projects\/spring-cloud-circuitbreaker\">spring cloud circuit breaker<\/a><\/noindex>, en enveloppant les lieux d'appels aux services externes dans les abstractions appropri\u00e9es. De plus, cela permet de choisir une strat\u00e9gie pour <noindex><a rel=\"nofollow\" href=\"https:\/\/docs.microsoft.com\/en-us\/azure\/architecture\/patterns\/bulkhead\">bulkhead<\/a><\/noindex> un mod\u00e8le, ce qui peut \u00e9galement \u00eatre utile. Par exemple, dans spring-cloud-netflix, cela peut \u00eatre une pool de threads ou un s\u00e9maphore.<\/p>\n<p><\/p>\n<h2 id=\"vyvod\">Sortie<\/h2>\n<p><\/p>\n<p>En cons\u00e9quence, nous avons d\u00e9velopp\u00e9 une application distincte qui permet de r\u00e9p\u00e9ter le traitement d'un message en cas d'indisponibilit\u00e9 temporaire d'un syst\u00e8me externe.<\/p>\n<p><\/p>\n<p>L'un des principaux avantages de l'application est qu'elle peut \u00eatre utilis\u00e9e par des syst\u00e8mes externes op\u00e9rant sur le m\u00eame cluster Kafka, sans n\u00e9cessiter d'importantes modifications de leur part ! Cette application n'aura besoin que d'acc\u00e9der au topic retry, de remplir quelques en-t\u00eates Kafka et d'envoyer le message au Retryer. Il n'est pas n\u00e9cessaire de d\u00e9ployer d'infrastructure suppl\u00e9mentaire. De plus, pour r\u00e9duire le nombre de messages transf\u00e9r\u00e9s entre l'application et le Retryer, nous avons isol\u00e9 des applications avec une logique lin\u00e9aire et mis en place un traitement r\u00e9p\u00e9t\u00e9 via l'arr\u00eat du Consumer.<\/p>\n<p>Source : <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.2 - 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\/fr\/blog\/administrirovanie\/povtornaya-obrabotka-sobytij-poluchennyh-iz-kafka\" \/>\n\t<meta name=\"generator\" content=\"All in One SEO (AIOSEO) 5.0.2\" \/>\n\t\t<meta property=\"og:locale\" content=\"fr_FR\" \/>\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\/fr\/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\udd47 Traitement r\u00e9p\u00e9t\u00e9 des \u00e9v\u00e9nements re\u00e7us de Kafka | ProHoster","description":"Bonjour, Habr. R\u00e9cemment.","canonical_url":"https:\/\/prohoster.info\/fr\/blog\/administrirovanie\/povtornaya-obrabotka-sobytij-poluchennyh-iz-kafka","robots":"max-image-preview:large","keywords":"","webmasterTools":{"miscellaneous":""},"schema":null,"og:locale":"fr_FR","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\/fr\/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\/fr\/wp-json\/wp\/v2\/posts\/56186","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/prohoster.info\/fr\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/prohoster.info\/fr\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/prohoster.info\/fr\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/prohoster.info\/fr\/wp-json\/wp\/v2\/comments?post=56186"}],"version-history":[{"count":0,"href":"https:\/\/prohoster.info\/fr\/wp-json\/wp\/v2\/posts\/56186\/revisions"}],"wp:attachment":[{"href":"https:\/\/prohoster.info\/fr\/wp-json\/wp\/v2\/media?parent=56186"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/prohoster.info\/fr\/wp-json\/wp\/v2\/categories?post=56186"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/prohoster.info\/fr\/wp-json\/wp\/v2\/tags?post=56186"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}