{"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\/pt\/blog\/administrirovanie\/povtornaya-obrabotka-sobytij-poluchennyh-iz-kafka","title":{"rendered":"Reprocessamento de eventos recebidos do Kafka","gt_translate_keys":[{"key":"rendered","format":"text"}]},"content":{"rendered":"<p><img decoding=\"async\" alt=\"Reprocessamento de eventos recebidos do Kafka\" src=\"\/wp-content\/uploads\/2020\/02\/d04319691af5c5de40ccfe4a2268558c.jpg\" style=\"display:block;margin: 0 auto;\" \/><\/p>\n<p><\/p>\n<p>Ol\u00e1, Habr.<\/p>\n<p><\/p>\n<p>Recentemente, eu <noindex><a rel=\"nofollow\" href=\"https:\/\/habr.com\/ru\/company\/tinkoff\/blog\/481784\/\">compartilhei minha experi\u00eancia<\/a><\/noindex> sobre quais par\u00e2metros usamos com mais frequ\u00eancia em nossa equipe para Kafka Producer e Consumer, a fim de nos aproximarmos da entrega garantida. Neste artigo, quero explicar como organizamos a reprocessamento de eventos recebidos do Kafka em resultado da indisponibilidade tempor\u00e1ria de um sistema externo.<\/p>\n<p><\/p>\n<p>Aplicativos modernos operam em um ambiente muito complexo. A l\u00f3gica de neg\u00f3cios, envolta em uma pilha tecnol\u00f3gica moderna, funcionando em uma imagem Docker, que \u00e9 gerenciada por um orquestrador como Kubernetes ou OpenShift, e se comunicando com outros aplicativos ou solu\u00e7\u00f5es empresariais atrav\u00e9s de uma cadeia de roteadores f\u00edsicos e virtuais. Em tal ambiente, sempre pode haver algo que quebre, portanto o reprocessamento de eventos em caso de indisponibilidade de um dos sistemas externos \u00e9 uma parte importante dos nossos processos de neg\u00f3cios.<\/p>\n<p><noindex><a rel=\"nofollow\" name=\"habracut\"><\/a><\/noindex><\/p>\n<h2 id=\"kak-bylo-do-kafka\">Como era antes do Kafka<\/h2>\n<p><\/p>\n<p>Anteriormente, no projeto, utiliz\u00e1vamos IBM MQ para entrega ass\u00edncrona de mensagens. Quando ocorria algum erro no processo de funcionamento do servi\u00e7o, a mensagem recebida poderia ser colocada em uma dead-letter-queue (DLQ) para posterior an\u00e1lise manual. A DLQ era criada ao lado da fila de entrada, e a realoca\u00e7\u00e3o da mensagem ocorria dentro do IBM MQ. <\/p>\n<p><\/p>\n<p>Se o erro era tempor\u00e1rio e consegu\u00edamos identific\u00e1-lo (por exemplo, ResourceAccessException durante uma chamada HTTP ou MongoTimeoutException durante uma consulta no MongoDb), ent\u00e3o entrava em vigor a estrat\u00e9gia de tentativas. Independentemente da ramifica\u00e7\u00e3o da l\u00f3gica do aplicativo, a mensagem original era realocada para uma fila sist\u00eamica para envio adiado ou para um aplicativo separado que havia sido criado h\u00e1 muito tempo para reenviar mensagens. Nesse caso, o n\u00famero da tentativa de reenvio era gravado no cabe\u00e7alho da mensagem, vinculado ao intervalo de atraso ou ao fim da estrat\u00e9gia a n\u00edvel de aplicativo. Se cheg\u00e1ssemos ao fim da estrat\u00e9gia, mas o sistema externo ainda estivesse indispon\u00edvel, a mensagem seria colocada na DLQ para an\u00e1lise manual.<\/p>\n<p><\/p>\n<h2 id=\"poisk-resheniya\">Busca pela solu\u00e7\u00e3o<\/h2>\n<p><\/p>\n<p><noindex><a rel=\"nofollow\" href=\"https:\/\/www.google.com\/search?q=kafka+retry+message\">Pesquisando na internet<\/a><\/noindex>, pode-se encontrar o seguinte <noindex><a rel=\"nofollow\" href=\"https:\/\/blog.pragmatists.com\/retrying-consumer-architecture-in-the-apache-kafka-939ac4cb851a\">uma solu\u00e7\u00e3o<\/a><\/noindex>. Em resumo, prop\u00f5e-se criar um t\u00f3pico para cada intervalo de atraso e implementar, do lado do aplicativo, Consumers que ler\u00e3o mensagens com o atraso necess\u00e1rio. <\/p>\n<p><\/p>\n<p><img decoding=\"async\" alt=\"Reprocessamento de eventos recebidos do Kafka\" src=\"\/wp-content\/uploads\/2020\/02\/718c4c3f71ccf95b5a6f60e1b2c685d2.jpg\" style=\"display:block;margin: 0 auto;\" \/><\/p>\n<p><\/p>\n<p>Apesar de haver muitos coment\u00e1rios positivos, n\u00e3o me parece totalmente satisfat\u00f3rio. Principalmente porque o desenvolvedor, al\u00e9m de implementar os requisitos de neg\u00f3cios, ter\u00e1 que investir muito tempo na implementa\u00e7\u00e3o do mecanismo descrito.<\/p>\n<p><\/p>\n<p>Al\u00e9m disso, se o gerenciamento de acesso estiver habilitado no cluster Kafka, ser\u00e1 necess\u00e1rio dedicar algum tempo para criar t\u00f3picos e garantir os acessos necess\u00e1rios a eles. Al\u00e9m disso, ser\u00e1 preciso escolher o par\u00e2metro correto retention.ms para cada um dos t\u00f3picos de retry, para que as mensagens consigam ser reenviadas a tempo e n\u00e3o desapare\u00e7am. A implementa\u00e7\u00e3o e a solicita\u00e7\u00e3o de acessos ter\u00e3o que ser repetidas para cada servi\u00e7o existente ou novo.<\/p>\n<p><\/p>\n<p>Vamos agora ver quais mecanismos de reprocessamento de mensagens o Spring nos oferece em geral e o spring-kafka em particular. O spring-kafka tem uma depend\u00eancia transitiva do spring-retry, que fornece abstra\u00e7\u00f5es para gerenciar diferentes BackOffPolicy. \u00c9 uma ferramenta bastante flex\u00edvel, mas sua desvantagem significativa \u00e9 o armazenamento de mensagens para reenvio na mem\u00f3ria da aplica\u00e7\u00e3o. Isso significa que a reinicializa\u00e7\u00e3o da aplica\u00e7\u00e3o devido a uma atualiza\u00e7\u00e3o ou a um erro durante a execu\u00e7\u00e3o resultar\u00e1 na perda de todas as mensagens que est\u00e3o aguardando reprocessamento. Como este ponto \u00e9 cr\u00edtico para o nosso sistema, n\u00e3o continuamos a explor\u00e1-lo.<\/p>\n<p><\/p>\n<p>O pr\u00f3prio spring-kafka oferece v\u00e1rias implementa\u00e7\u00f5es de ContainerAwareErrorHandler, por exemplo, <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>, que permite que, sem deslocar o offset em caso de erro, a mensagem seja processada mais tarde. A partir da vers\u00e3o spring-kafka 2.3, surgiu a possibilidade de definir BackOffPolicy.<\/p>\n<p><\/p>\n<p>Esta abordagem permite que as mensagens reprocessadas sobrevivam \u00e0 reinicializa\u00e7\u00e3o da aplica\u00e7\u00e3o, mas o mecanismo DLQ ainda est\u00e1 ausente. Foi essa op\u00e7\u00e3o que escolhemos no in\u00edcio de 2019, otimistamente acreditando que o DLQ n\u00e3o seria necess\u00e1rio (tivemos sorte e ele realmente n\u00e3o foi necess\u00e1rio durante v\u00e1rios meses de opera\u00e7\u00e3o da aplica\u00e7\u00e3o com esse sistema de reprocessamento). Erros tempor\u00e1rios resultaram na ativa\u00e7\u00e3o do SeekToCurrentErrorHandler. Os demais erros eram registrados no log, resultavam no deslocamento do offset, e o processamento continuava com a pr\u00f3xima mensagem.<\/p>\n<p><\/p>\n<h2 id=\"itogovoe-reshenie\">Solu\u00e7\u00e3o final<\/h2>\n<p><\/p>\n<p>A implementa\u00e7\u00e3o baseada em SeekToCurrentErrorHandler nos levou a desenvolver nosso pr\u00f3prio mecanismo para reenvio de mensagens.<\/p>\n<p><\/p>\n<p>Acima de tudo, quer\u00edamos aproveitar a experi\u00eancia existente e expandi-la de acordo com a l\u00f3gica do aplicativo. Para um aplicativo com l\u00f3gica linear, seria ideal interromper a leitura de novas mensagens por um curto per\u00edodo determinado na estrat\u00e9gia de tentativas. Para outros aplicativos, gostar\u00edamos de ter um \u00fanico ponto que garantisse a execu\u00e7\u00e3o da estrat\u00e9gia de tentativas. Al\u00e9m disso, esse \u00fanico ponto deve ter a funcionalidade de DLQ para ambas as abordagens.<\/p>\n<p><\/p>\n<p>A pr\u00f3pria estrat\u00e9gia de tentativas deve ser armazenada no aplicativo que \u00e9 respons\u00e1vel por obter o pr\u00f3ximo intervalo em caso de um erro tempor\u00e1rio.<\/p>\n<p><\/p>\n<h3 id=\"ostanovka-consumera-dlya-prilozheniya-s-lineynoy-logikoy\">Parar o Consumer para aplicativos com l\u00f3gica linear<\/h3>\n<p><\/p>\n<p>Ao trabalhar com spring-kafka, o c\u00f3digo para parar o Consumer pode parecer assim:<\/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        \/\/ para DLQ\n    }<\/code><\/pre>\n<p><\/p>\n<p>No exemplo, retryAt \u00e9 o tempo em que o MessageListenerContainer deve ser reiniciado, se ainda estiver em execu\u00e7\u00e3o. O rein\u00edcio ocorrer\u00e1 em uma thread separada, iniciada no TaskScheduler, cuja implementa\u00e7\u00e3o tamb\u00e9m \u00e9 fornecida pelo spring. <\/p>\n<p><\/p>\n<p>O valor de retryAt \u00e9 encontrado da seguinte forma:<\/p>\n<p><\/p>\n<ol>\n<li>Buscamos o valor do contador de tentativas.<\/li>\n<li>De acordo com o valor do contador, buscamos o intervalo de atraso atual na estrat\u00e9gia de tentativas. A estrat\u00e9gia \u00e9 declarada no pr\u00f3prio aplicativo, para seu armazenamento escolhemos o formato JSON.<\/li>\n<li>O intervalo encontrado no array JSON cont\u00e9m o n\u00famero de segundos ap\u00f3s os quais a reprocessamento deve ser repetido. Esse n\u00famero de segundos \u00e9 adicionado ao tempo atual, formando o valor para retryAt.<\/li>\n<li>Se o intervalo n\u00e3o for encontrado, o valor de retryAt ser\u00e1 null e a mensagem ser\u00e1 enviada para a DLQ para an\u00e1lise manual.<\/li>\n<\/ol>\n<p><\/p>\n<p>Com essa abordagem, resta apenas manter a contagem de chamadas repetidas para cada mensagem que est\u00e1 atualmente em processamento, por exemplo, na mem\u00f3ria do aplicativo. Manter o contador de tentativas na mem\u00f3ria n\u00e3o \u00e9 cr\u00edtico para essa abordagem, uma vez que um aplicativo com l\u00f3gica linear n\u00e3o pode realizar o processamento como um todo. Ao contr\u00e1rio do spring-retry, a reinicializa\u00e7\u00e3o do aplicativo n\u00e3o resultar\u00e1 na perda de todas as mensagens para reprocessamento, mas simplesmente relan\u00e7ar\u00e1 a estrat\u00e9gia. <\/p>\n<p><\/p>\n<p>Essa abordagem ajuda a aliviar a carga sobre o sistema externo, que pode estar indispon\u00edvel devido a uma carga muito grande. Em outras palavras, al\u00e9m do reprocessamento, conseguimos implementar o padr\u00e3o. <noindex><a rel=\"nofollow\" href=\"https:\/\/microservices.io\/patterns\/reliability\/circuit-breaker.html\">circuit breaker<\/a><\/noindex>.<\/p>\n<p><\/p>\n<p>No nosso caso, o limite de erro \u00e9 de apenas 1, e para minimizar o tempo de inatividade do sistema devido a uma interrup\u00e7\u00e3o tempor\u00e1ria na rede, usamos uma estrat\u00e9gia de tentativas de chamadas muito granular com pequenos intervalos de atraso. Isso pode n\u00e3o ser adequado para todas as aplica\u00e7\u00f5es do grupo, portanto, a rela\u00e7\u00e3o entre o limite de erro e o tamanho do intervalo precisa ser ajustada com base nas caracter\u00edsticas do sistema.<\/p>\n<p><\/p>\n<h3 id=\"otdelnoe-prilozhenie-dlya-obrabotki-soobscheniy-ot-prilozheniy-s-nedeterminirovannoy-logikoy\">Um aplicativo separado para processar mensagens de aplicativos com l\u00f3gica n\u00e3o determin\u00edstica.<\/h3>\n<p><\/p>\n<p>Aqui est\u00e1 um exemplo de c\u00f3digo que envia uma mensagem para tal aplicativo (Retryer), que reenvia para o t\u00f3pico DESTINATION ao atingir o 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>Do exemplo, pode-se ver que muitas informa\u00e7\u00f5es s\u00e3o transmitidas nos cabe\u00e7alhos. O valor RETRY_AT est\u00e1 presente da mesma forma que para o mecanismo de repeti\u00e7\u00e3o atrav\u00e9s da parada do Consumer. Al\u00e9m do DESTINATION e RETRY_AT, tamb\u00e9m transmitimos:<\/p>\n<p><\/p>\n<ul>\n<li>GROUP_ID, pelo qual agrupamos mensagens para an\u00e1lise manual e facilitamos a busca.<\/li>\n<li>ORIGINAL_PARTITION, para tentar manter o mesmo Consumer para reprocessamento. Este par\u00e2metro pode ser nulo, caso em que uma nova partition ser\u00e1 obtida pela chave record.key() da mensagem original.<\/li>\n<li>Valor atualizado de COUNTER, para seguir a estrat\u00e9gia de reenvios.<\/li>\n<li>SEND_TO \u2014 constante que indica se a mensagem deve ser reenviada para reprocessamento ao atingir RETRY_AT ou colocada em DLQ.<\/li>\n<li>REASON \u2014 a raz\u00e3o pela qual o processamento da mensagem foi interrompido.<\/li>\n<\/ul>\n<p><\/p>\n<p>Retryer armazena mensagens para reenvio e an\u00e1lise manual no PostgreSQL. Um cron job \u00e9 acionado para encontrar mensagens cujo RETRY_AT j\u00e1 foi atingido e envi\u00e1-las de volta para a partition ORIGINAL_PARTITION do t\u00f3pico DESTINATION com a chave record.key().<\/p>\n<p><\/p>\n<p>Ap\u00f3s o envio, as mensagens s\u00e3o removidas do PostgreSQL. A an\u00e1lise manual das mensagens ocorre em uma interface simples, que interage com o Retryer atrav\u00e9s da API REST. Suas principais caracter\u00edsticas incluem reenvio ou remo\u00e7\u00e3o de mensagens da DLQ, visualiza\u00e7\u00e3o de informa\u00e7\u00f5es de erro e busca de mensagens, por exemplo, por nome de erro. <\/p>\n<p><\/p>\n<p>Como o controle de acesso est\u00e1 ativado em nossos clusters, \u00e9 necess\u00e1rio solicitar permiss\u00f5es adicionais para o t\u00f3pico que o Retryer escuta e permitir que o Retryer escreva no t\u00f3pico DESTINATION. Isso \u00e9 inconveniente, mas, ao contr\u00e1rio da abordagem de t\u00f3pico por intervalo, temos uma DLQ completa e uma interface para gerenci\u00e1-la.<\/p>\n<p><\/p>\n<p>Existem situa\u00e7\u00f5es em que o t\u00f3pico de entrada \u00e9 lido por v\u00e1rios grupos de consumidores diferentes, cujos aplicativos implementam diferentes l\u00f3gicas. O reprocessamento de uma mensagem atrav\u00e9s do Retryer para um desses aplicativos resultar\u00e1 em um duplicado para o outro. Para se proteger contra isso, criamos um t\u00f3pico separado para reprocessamento. O t\u00f3pico de entrada e o t\u00f3pico de retry podem ser lidos pelo mesmo Consumer sem restri\u00e7\u00f5es. <\/p>\n<p><\/p>\n<p><img decoding=\"async\" alt=\"Reprocessamento de eventos recebidos do Kafka\" src=\"\/wp-content\/uploads\/2020\/02\/06f2355839c6dd812ee3304bf2c72226.jpg\" style=\"display:block;margin: 0 auto;\" \/><\/p>\n<p><\/p>\n<p>Por padr\u00e3o, essa abordagem n\u00e3o fornece a funcionalidade de um disjuntor, mas ela pode ser adicionada ao aplicativo usando <noindex><a rel=\"nofollow\" href=\"https:\/\/spring.io\/projects\/spring-cloud-netflix\">spring-cloud-netflix<\/a><\/noindex> ou o novo <noindex><a rel=\"nofollow\" href=\"https:\/\/spring.io\/projects\/spring-cloud-circuitbreaker\">spring cloud circuit breaker<\/a><\/noindex>, envolvendo os pontos de chamada dos servi\u00e7os externos nas abstra\u00e7\u00f5es correspondentes. Al\u00e9m disso, h\u00e1 a possibilidade de escolher a estrat\u00e9gia para o <noindex><a rel=\"nofollow\" href=\"https:\/\/docs.microsoft.com\/en-us\/azure\/architecture\/patterns\/bulkhead\">bulkhead<\/a><\/noindex> padr\u00e3o, o que tamb\u00e9m pode ser \u00fatil. Por exemplo, em spring-cloud-netflix isso pode ser um thread pool ou um sem\u00e1foro.<\/p>\n<p><\/p>\n<h2 id=\"vyvod\">Sa\u00edda<\/h2>\n<p><\/p>\n<p>Como resultado, temos um aplicativo separado que permite repetir o processamento de mensagens quando um sistema externo est\u00e1 temporariamente indispon\u00edvel.<\/p>\n<p><\/p>\n<p>Uma das principais vantagens do aplicativo \u00e9 que sistemas externos operando no mesmo cluster Kafka podem utiliz\u00e1-lo sem grandes adapta\u00e7\u00f5es de sua parte! O aplicativo precisa apenas acessar o t\u00f3pico de retry, preencher alguns cabe\u00e7alhos Kafka e enviar a mensagem para o Retryer. N\u00e3o \u00e9 necess\u00e1rio implantar nenhuma infraestrutura adicional. Para reduzir o n\u00famero de mensagens deslocadas entre o aplicativo e o Retryer, isolamos os aplicativos com l\u00f3gica linear e implementamos a reprocessamento atrav\u00e9s da pausa do 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.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\/pt\/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=\"pt_PT\" \/>\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\/pt\/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\udd47Reprocessamento de eventos recebidos do Kafka | ProHoster","description":"Ol\u00e1, Habr. Recentemente.","canonical_url":"https:\/\/prohoster.info\/pt\/blog\/administrirovanie\/povtornaya-obrabotka-sobytij-poluchennyh-iz-kafka","robots":"max-image-preview:large","keywords":"","webmasterTools":{"miscellaneous":""},"schema":null,"og:locale":"pt_PT","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\/pt\/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\/pt\/wp-json\/wp\/v2\/posts\/56186","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/prohoster.info\/pt\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/prohoster.info\/pt\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/prohoster.info\/pt\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/prohoster.info\/pt\/wp-json\/wp\/v2\/comments?post=56186"}],"version-history":[{"count":0,"href":"https:\/\/prohoster.info\/pt\/wp-json\/wp\/v2\/posts\/56186\/revisions"}],"wp:attachment":[{"href":"https:\/\/prohoster.info\/pt\/wp-json\/wp\/v2\/media?parent=56186"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/prohoster.info\/pt\/wp-json\/wp\/v2\/categories?post=56186"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/prohoster.info\/pt\/wp-json\/wp\/v2\/tags?post=56186"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}