{"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\/es\/blog\/administrirovanie\/povtornaya-obrabotka-sobytij-poluchennyh-iz-kafka","title":{"rendered":"Reprocesamiento de eventos recibidos de Kafka","gt_translate_keys":[{"key":"rendered","format":"text"}]},"content":{"rendered":"<p><img decoding=\"async\" alt=\"Reprocesamiento de eventos recibidos de Kafka\" src=\"\/wp-content\/uploads\/2020\/02\/d04319691af5c5de40ccfe4a2268558c.jpg\" style=\"display:block;margin: 0 auto;\" \/><\/p>\n<p><\/p>\n<p>Hola, Habr.<\/p>\n<p><\/p>\n<p>Recientemente compart\u00ed <noindex><a rel=\"nofollow\" href=\"https:\/\/habr.com\/ru\/company\/tinkoff\/blog\/481784\/\">mi experiencia<\/a><\/noindex> sobre los par\u00e1metros que utilizamos con m\u00e1s frecuencia en nuestro equipo para Kafka Producer y Consumer, con el fin de acercarnos a la entrega garantizada. En este art\u00edculo quiero contar c\u00f3mo organizamos el reprocesamiento de eventos recibidos de Kafka debido a la indisponibilidad temporal de un sistema externo.<\/p>\n<p><\/p>\n<p>Las aplicaciones modernas operan en un entorno muy complejo. La l\u00f3gica de negocio, envuelta en una pila tecnol\u00f3gica moderna, que funciona en una imagen de Docker gestionada por un orquestador como Kubernetes u OpenShift, y que se comunica con otras aplicaciones o soluciones empresariales a trav\u00e9s de una cadena de enrutadores f\u00edsicos y virtuales. En un entorno as\u00ed, siempre puede fallar algo, por lo que el reprocesamiento de eventos en caso de la indisponibilidad de uno de los sistemas externos es una parte importante de nuestros procesos de negocio.<\/p>\n<p><noindex><a rel=\"nofollow\" name=\"habracut\"><\/a><\/noindex><\/p>\n<h2 id=\"kak-bylo-do-kafka\">C\u00f3mo era antes de Kafka<\/h2>\n<p><\/p>\n<p>Anteriormente en el proyecto utiliz\u00e1bamos IBM MQ para la entrega as\u00edncrona de mensajes. Cuando se produc\u00eda alg\u00fan error durante la operaci\u00f3n del servicio, el mensaje recibido pod\u00eda ser colocado en una cola de mensajes muertos (DLQ) para su posterior revisi\u00f3n manual. La DLQ se creaba junto a la cola de entrada, y el traslado del mensaje ocurr\u00eda dentro de IBM MQ. <\/p>\n<p><\/p>\n<p>Si el error era temporal y pod\u00edamos determinarlo (por ejemplo, ResourceAccessException al hacer una llamada HTTP o MongoTimeoutException al hacer una consulta en MongoDb), se activaba la estrategia de reintentos. Independientemente de la ramificaci\u00f3n de la l\u00f3gica de la aplicaci\u00f3n, el mensaje original se trasladaba ya sea a una cola del sistema para env\u00edo diferido o a una aplicaci\u00f3n separada que se hab\u00eda creado hace tiempo para reenv\u00edo de mensajes. En este caso, se registraba en el encabezado del mensaje el n\u00famero de reenv\u00edo, que estaba vinculado al intervalo de retardo o al final de la estrategia a nivel de la aplicaci\u00f3n. Si lleg\u00e1bamos al final de la estrategia, pero el sistema externo segu\u00eda sin estar disponible, el mensaje se colocar\u00eda en la DLQ para revisi\u00f3n manual.<\/p>\n<p><\/p>\n<h2 id=\"poisk-resheniya\">B\u00fasqueda de soluci\u00f3n<\/h2>\n<p><\/p>\n<p><noindex><a rel=\"nofollow\" href=\"https:\/\/www.google.com\/search?q=kafka+retry+message\">Al buscar en Internet<\/a><\/noindex>, se puede encontrar lo siguiente <noindex><a rel=\"nofollow\" href=\"https:\/\/blog.pragmatists.com\/retrying-consumer-architecture-in-the-apache-kafka-939ac4cb851a\">soluci\u00f3n<\/a><\/noindex>. En resumen, se sugiere establecer un tema para cada intervalo de retardo y realizar en la parte de la aplicaci\u00f3n Consumers que leer\u00e1n los mensajes con el retraso necesario. <\/p>\n<p><\/p>\n<p><img decoding=\"async\" alt=\"Reprocesamiento de eventos recibidos de Kafka\" src=\"\/wp-content\/uploads\/2020\/02\/718c4c3f71ccf95b5a6f60e1b2c685d2.jpg\" style=\"display:block;margin: 0 auto;\" \/><\/p>\n<p><\/p>\n<p>A pesar de la gran cantidad de comentarios positivos, me parece que no es del todo adecuado. En primer lugar, porque al desarrollador, adem\u00e1s de implementar los requisitos comerciales, le llevar\u00e1 mucho tiempo implementar el mecanismo descrito.<\/p>\n<p><\/p>\n<p>Adem\u00e1s, si se habilita el control de acceso en el cl\u00faster de Kafka, se necesitar\u00e1 algo de tiempo para crear los temas y garantizar los accesos necesarios a ellos. Adem\u00e1s de esto, ser\u00e1 necesario elegir el par\u00e1metro correcto retention.ms para cada uno de los temas de reintento, de modo que los mensajes puedan ser reenviados a tiempo y no se pierdan. La implementaci\u00f3n y la solicitud de accesos deber\u00e1n repetirse para cada servicio existente o nuevo.<\/p>\n<p><\/p>\n<p>Ahora veamos qu\u00e9 mecanismos para el re-procesamiento de mensajes nos ofrece Spring en general y Spring-Kafka en particular. Spring-Kafka tiene una dependencia transitiva en Spring-Retry, que proporciona abstracciones para gestionar diferentes BackOffPolicy. Es una herramienta bastante flexible, pero su desventaja significativa es que almacena los mensajes para reenv\u00edo en la memoria de la aplicaci\u00f3n. Esto significa que el reinicio de la aplicaci\u00f3n debido a una actualizaci\u00f3n o un error durante la explotaci\u00f3n resultar\u00e1 en la p\u00e9rdida de todos los mensajes que esperan ser re-procesados. Dado que este aspecto es cr\u00edtico para nuestro sistema, no lo consideramos m\u00e1s adelante.<\/p>\n<p><\/p>\n<p>Spring-Kafka ofrece varias implementaciones de ContainerAwareErrorHandler, como por ejemplo <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 procesar el mensaje m\u00e1s tarde sin mover el offset en caso de un error. A partir de la versi\u00f3n 2.3 de Spring-Kafka, se introdujo la posibilidad de establecer BackOffPolicy.<\/p>\n<p><\/p>\n<p>Este enfoque permite que los mensajes re-procesados sobrevivan al reinicio de la aplicaci\u00f3n, pero el mecanismo DLQ sigue siendo ausente. Precisamente esta opci\u00f3n elegimos a principios de 2019, optimistamente pensando que no necesitar\u00edamos DLQ (tuvimos suerte y realmente no lo necesit\u00e1bamos durante varios meses de explotaci\u00f3n de la aplicaci\u00f3n con este sistema de re-procesamiento). Los errores temporales provocaban la activaci\u00f3n de SeekToCurrentErrorHandler. Los dem\u00e1s errores se imprim\u00edan en el registro, llevaban al desplazamiento del offset y el procesamiento continuaba con el siguiente mensaje.<\/p>\n<p><\/p>\n<h2 id=\"itogovoe-reshenie\">Soluci\u00f3n final<\/h2>\n<p><\/p>\n<p>La implementaci\u00f3n basada en SeekToCurrentErrorHandler nos llev\u00f3 a desarrollar nuestro propio mecanismo para el reenv\u00edo de mensajes.<\/p>\n<p><\/p>\n<p>Primero que nada, quer\u00edamos aprovechar la experiencia existente y ampliarla seg\u00fan la l\u00f3gica de la aplicaci\u00f3n. Para una aplicaci\u00f3n con una l\u00f3gica lineal, lo \u00f3ptimo ser\u00eda detener la lectura de nuevos mensajes durante un breve intervalo de tiempo, establecido dentro de la estrategia de reintentos. Para otras aplicaciones, nos gustar\u00eda tener un punto \u00fanico que garantizara la ejecuci\u00f3n de la estrategia de reintentos. Adem\u00e1s, este punto \u00fanico deber\u00eda contar con funcionalidad DLQ para ambos enfoques.<\/p>\n<p><\/p>\n<p>La estrategia de reintentos en s\u00ed debe almacenarse en la aplicaci\u00f3n responsable de obtener el siguiente intervalo en caso de un error temporal.<\/p>\n<p><\/p>\n<h3 id=\"ostanovka-consumera-dlya-prilozheniya-s-lineynoy-logikoy\">Detener el Consumer para una aplicaci\u00f3n con l\u00f3gica lineal<\/h3>\n<p><\/p>\n<p>Al trabajar con spring-kafka, el c\u00f3digo para detener el Consumer podr\u00eda verse aproximadamente as\u00ed:<\/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>En el ejemplo, retryAt es el momento en que se debe reiniciar el MessageListenerContainer, si todav\u00eda est\u00e1 funcionando. El reinicio se llevar\u00e1 a cabo en un hilo separado que se ejecuta en TaskScheduler, cuya implementaci\u00f3n tambi\u00e9n proporciona spring. <\/p>\n<p><\/p>\n<p>El valor de retryAt lo encontramos de la siguiente manera:<\/p>\n<p><\/p>\n<ol>\n<li>Se busca el valor del contador de reintentos.<\/li>\n<li>De acuerdo con el valor del contador, se busca el intervalo actual de retraso en la estrategia de reintentos. La estrategia se declara en la propia aplicaci\u00f3n, para su almacenamiento elegimos el formato JSON.<\/li>\n<li>El intervalo encontrado en el array JSON contiene la cantidad de segundos despu\u00e9s de los cuales se deber\u00e1 repetir el procesamiento. Esta cantidad de segundos se suma al tiempo actual, formando el valor para retryAt.<\/li>\n<li>Si no se encuentra el intervalo, el valor de retryAt es null y el mensaje se enviar\u00e1 a la DLQ para su an\u00e1lisis manual.<\/li>\n<\/ol>\n<p><\/p>\n<p>Con este enfoque, solo queda guardar la cantidad de intentos para cada mensaje que actualmente est\u00e1 en proceso, por ejemplo, en la memoria de la aplicaci\u00f3n. Mantener un contador de intentos en la memoria no es cr\u00edtico para este enfoque, ya que una aplicaci\u00f3n con l\u00f3gica lineal no puede procesar en su totalidad. A diferencia de spring-retry, el reinicio de la aplicaci\u00f3n no resultar\u00e1 en la p\u00e9rdida de todos los mensajes para re-procesamiento, sino simplemente en un reinicio de la estrategia. <\/p>\n<p><\/p>\n<p>Este enfoque ayuda a aliviar la carga de un sistema externo que puede estar inalcanzable debido a una carga excesiva. En otras palabras, adem\u00e1s de la re-procesamiento, hemos logrado implementar el patr\u00f3n <noindex><a rel=\"nofollow\" href=\"https:\/\/microservices.io\/patterns\/reliability\/circuit-breaker.html\">circuit breaker<\/a><\/noindex>.<\/p>\n<p><\/p>\n<p>En nuestro caso, el umbral de error es de solo 1, y para minimizar el tiempo de inactividad del sistema debido a una interrupci\u00f3n temporal de la red, utilizamos una estrategia de reintentos muy granular con peque\u00f1os intervalos de retraso. Esto puede no ser adecuado para todas las aplicaciones del grupo empresarial, por lo que la relaci\u00f3n entre el umbral de error y el tama\u00f1o del intervalo debe ajustarse seg\u00fan las caracter\u00edsticas del sistema.<\/p>\n<p><\/p>\n<h3 id=\"otdelnoe-prilozhenie-dlya-obrabotki-soobscheniy-ot-prilozheniy-s-nedeterminirovannoy-logikoy\">Una aplicaci\u00f3n separada para procesar mensajes de aplicaciones con l\u00f3gica no determin\u00edstica<\/h3>\n<p><\/p>\n<p>Aqu\u00ed hay un ejemplo de c\u00f3digo que env\u00eda un mensaje a tal aplicaci\u00f3n (Retryer), que volver\u00e1 a enviar al tema DESTINATION al alcanzar el tiempo 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>Del ejemplo se puede ver que se transmite mucha informaci\u00f3n en los encabezados. El valor RETRY_AT se encuentra tambi\u00e9n, al igual que para el mecanismo de reintento a trav\u00e9s de la detenci\u00f3n del Consumer.<\/p>\n<p><\/p>\n<ul>\n<li>GROUP_ID, por el cual agrupamos los mensajes para an\u00e1lisis manual y facilitar la b\u00fasqueda.<\/li>\n<li>ORIGINAL_PARTITION, para intentar mantener el mismo Consumer para el procesamiento posterior. Este par\u00e1metro puede ser nulo, en cuyo caso se obtendr\u00e1 una nueva partici\u00f3n por la clave record.key() del mensaje original.<\/li>\n<li>Valor actualizado de COUNTER, para seguir la estrategia de reintentos.<\/li>\n<li>SEND_TO \u2014 constante que indica si se debe reenv\u00edar el mensaje para su procesamiento nuevamente al alcanzar RETRY_AT o colocarlo en DLQ.<\/li>\n<li>REASON \u2014 raz\u00f3n por la cual el procesamiento del mensaje fue interrumpido.<\/li>\n<\/ul>\n<p><\/p>\n<p>Retryer guarda los mensajes para reenv\u00edo y an\u00e1lisis manual en PostgreSQL. Una tarea se activa por temporizador, que encuentra mensajes con RETRY_AT alcanzado y los env\u00eda de regreso a la partici\u00f3n ORIGINAL_PARTITION del tema DESTINATION con la clave record.key().<\/p>\n<p><\/p>\n<p>Despu\u00e9s de enviar, los mensajes se eliminan de PostgreSQL. El an\u00e1lisis manual de los mensajes se realiza en una interfaz simple, que interact\u00faa con Retryer a trav\u00e9s de REST API. Sus caracter\u00edsticas principales son reenv\u00edo o eliminaci\u00f3n de mensajes de DLQ, visualizaci\u00f3n de informaci\u00f3n de errores y b\u00fasqueda de mensajes, por ejemplo, por nombre de error. <\/p>\n<p><\/p>\n<p>Dado que en nuestros cl\u00fasteres se habilita el control de acceso, es necesario solicitar permisos adicionales para el tema que escucha Retryer, y permitir que Retryer escriba en el tema DESTINATION. Esto es inc\u00f3modo, pero, a diferencia del enfoque con un tema a intervalos, tenemos una DLQ completa y una interfaz para gestionarla.<\/p>\n<p><\/p>\n<p>Puede haber casos en los que el tema de entrada sea le\u00eddo por varios grupos de consumidores diferentes, cuyas aplicaciones implementan diferentes l\u00f3gicas. El reenv\u00edo del mensaje a trav\u00e9s de Retryer para una de estas aplicaciones resultar\u00e1 en un duplicado en otra. Para protegerse contra esto, creamos un tema separado para el reenv\u00edo. El consumidor puede leer tanto el tema de entrada como el tema de reintentos sin restricciones. <\/p>\n<p><\/p>\n<p><img decoding=\"async\" alt=\"Reprocesamiento de eventos recibidos de Kafka\" src=\"\/wp-content\/uploads\/2020\/02\/06f2355839c6dd812ee3304bf2c72226.jpg\" style=\"display:block;margin: 0 auto;\" \/><\/p>\n<p><\/p>\n<p>Por defecto, este enfoque no proporciona un mecanismo de cortafuegos, sin embargo, se puede agregar al aplicativo a trav\u00e9s de <noindex><a rel=\"nofollow\" href=\"https:\/\/spring.io\/projects\/spring-cloud-netflix\">spring-cloud-netflix<\/a><\/noindex> o del nuevo <noindex><a rel=\"nofollow\" href=\"https:\/\/spring.io\/projects\/spring-cloud-circuitbreaker\">spring cloud circuit breaker<\/a><\/noindex>, envolviendo los lugares de llamadas a servicios externos en las abstracciones correspondientes. Adem\u00e1s, se presenta la posibilidad de elegir la estrategia para el patr\u00f3n <noindex><a rel=\"nofollow\" href=\"https:\/\/docs.microsoft.com\/en-us\/azure\/architecture\/patterns\/bulkhead\">bulkhead<\/a><\/noindex> , lo que tambi\u00e9n puede ser \u00fatil. Por ejemplo, en spring-cloud-netflix puede ser un grupo de hilos o un sem\u00e1foro.<\/p>\n<p><\/p>\n<h2 id=\"vyvod\">Salida<\/h2>\n<p><\/p>\n<p>Como resultado, hemos creado una aplicaci\u00f3n independiente que permite repetir el procesamiento de mensajes ante la indisponibilidad temporal de alg\u00fan sistema externo.<\/p>\n<p><\/p>\n<p>Una de las principales ventajas de la aplicaci\u00f3n es que puede ser utilizada por sistemas externos que operan en el mismo cl\u00faster de Kafka, \u00a1sin necesidad de grandes modificaciones de su parte! Esta aplicaci\u00f3n solo necesitar\u00e1 acceder al tema de reintentos, llenar algunos encabezados de Kafka y enviar el mensaje al Retryer. No es necesario levantar infraestructura adicional. Adem\u00e1s, para reducir la cantidad de mensajes que se trasladan de la aplicaci\u00f3n al Retryer y viceversa, hemos aislado aplicaciones con l\u00f3gica lineal y hemos implementado el re-procesamiento mediante la detenci\u00f3n del Consumer.<\/p>\n<p>Fuente: <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\/es\/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=\"es_ES\" \/>\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\/es\/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\udd47Re-procesamiento de eventos recibidos de Kafka | ProHoster","description":"Hola, Habr. Recientemente.","canonical_url":"https:\/\/prohoster.info\/es\/blog\/administrirovanie\/povtornaya-obrabotka-sobytij-poluchennyh-iz-kafka","robots":"max-image-preview:large","keywords":"","webmasterTools":{"miscellaneous":""},"schema":null,"og:locale":"es_ES","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\/es\/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\/es\/wp-json\/wp\/v2\/posts\/56186","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/prohoster.info\/es\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/prohoster.info\/es\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/prohoster.info\/es\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/prohoster.info\/es\/wp-json\/wp\/v2\/comments?post=56186"}],"version-history":[{"count":0,"href":"https:\/\/prohoster.info\/es\/wp-json\/wp\/v2\/posts\/56186\/revisions"}],"wp:attachment":[{"href":"https:\/\/prohoster.info\/es\/wp-json\/wp\/v2\/media?parent=56186"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/prohoster.info\/es\/wp-json\/wp\/v2\/categories?post=56186"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/prohoster.info\/es\/wp-json\/wp\/v2\/tags?post=56186"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}