web-dev-qa-db-fra.com

Gérer les mauvais messages à l'aide de l'API Streams de Kafka

J'ai un flux de traitement de flux de base qui ressemble à

master topic -> my processing in a mapper/filter -> output topics

et je me demande quelle est la meilleure façon de gérer les "mauvais messages". Cela pourrait potentiellement être des choses comme des messages que je ne peux pas désérialiser correctement, ou peut-être que la logique de traitement/filtrage échoue d'une manière inattendue (je n'ai pas de dépendances externes donc il ne devrait pas y avoir d'erreurs transitoires de ce type).

J'envisageais d'encapsuler tout mon code de traitement/filtrage dans un catch catch et si une exception était levée, puis un routage vers un "sujet d'erreur". Ensuite, je peux étudier le message et le modifier ou corriger mon code selon le cas, puis le relire sur le maître. Si je laisse des exceptions se propager, le flux semble se bloquer et plus aucun message n'est récupéré.

  • Cette approche est-elle considérée comme la meilleure pratique?
  • Existe-t-il un moyen Kafka streams de gérer cela? Je ne pense pas qu'il existe un concept de DLQ ...
  • Quelles sont les autres façons d'arrêter Kafka jamming on a "bad message"?
  • Quelles sont les autres approches de gestion des erreurs?

Pour être complet, voici mon code (pseudo-ish):

class Document {
    // Fields
}

class AnalysedDocument {

    Document document;
    String rawValue;
    Exception exception;
    Analysis analysis;

    // All being well
    AnalysedDocument(Document document, Analysis analysis) {...}

    // Analysis failed
    AnalysedDocument(Document document, Exception exception) {...}

    // Deserialisation failed
    AnalysedDocument(String rawValue, Exception exception) {...}
}

KStreamBuilder builder = new KStreamBuilder();
KStream<String, AnalysedPolecatDocument> analysedDocumentStream = builder
    .stream(Serdes.String(), Serdes.String(), "master")
    .mapValues(new ValueMapper<String, AnalysedDocument>() {
         @Override
         public AnalysedDocument apply(String rawValue) {
             Document document;
             try {
                 // Deserialise
                 document = ...
             } catch (Exception e) {
                 return new AnalysedDocument(rawValue, exception);
             }
             try {
                 // Perform analysis
                 Analysis analysis = ...
                 return new AnalysedDocument(document, analysis);
             } catch (Exception e) {
                 return new AnalysedDocument(document, exception);
             }
         }
    });

// Branch based on whether analysis mapping failed to produce errorStream and successStream
errorStream.to(Serdes.String(), customPojoSerde(), "error");
successStream.to(Serdes.String(), customPojoSerde(), "analysed");

KafkaStreams streams = new KafkaStreams(builder, config);
streams.start();

Toute aide grandement appréciée.

28
bm1729

À l'heure actuelle, Kafka Streams ne propose que des capacités de gestion des erreurs limitées. Des travaux sont en cours pour simplifier cela. Pour l'instant, votre approche globale semble être un bon moyen de procéder.

Un commentaire sur la gestion des erreurs de/sérialisation: la gestion de ces erreurs manuellement, vous oblige à effectuer la dés/sérialisation "manuellement". Cela signifie que vous devez configurer ByteArraySerdes pour la clé et la valeur pour votre sujet d'entrée/sortie de votre application Streams et ajouter un map() qui effectue la dés/sérialisation (c'est-à-dire KStream<byte[],byte[]> -> map() -> KStream<keyType,valueType> - ou inversement si vous souhaitez également intercepter les exceptions de sérialisation). Sinon, vous ne pouvez pas try-catch exceptions de désérialisation.

Avec votre approche actuelle, vous validez "uniquement" que la chaîne donnée représente un document valide - mais cela pourrait être le cas, que le message lui-même est corrompu et ne peut pas être converti en String dans l'opérateur source de la première place. Ainsi, vous ne couvrez pas réellement l'exception de désérialisation avec votre code. Cependant, si vous êtes sûr qu'une exception de désérialisation ne peut jamais se produire, votre approche serait également suffisante.

Mise à jour

Ce problème est résolu via KIP-161 et sera inclus dans la prochaine version 1.0.0. Il vous permet d'enregistrer un rappel via le paramètre default.deserialization.exception.handler. Le gestionnaire sera invoqué chaque fois qu'une exception se produit pendant la désérialisation et vous permet de renvoyer un DeserializationResponse (CONTINUE -> déposez l'enregistrement en cours de déplacement, ou FAIL qui est le défaut).

Mise à jour 2

Avec KIP-21 (fera partie de dans Kafka 1.1), il est également possible de gérer les erreurs côté producteur, comme dans la partie consommateur, en enregistrant un ProductionExceptionHandler via config default.production.exception.handler qui peut renvoyer CONTINUE.

24
Matthias J. Sax

Mise à jour du 23 mars 2018: Kafka 1.0 offre une gestion bien meilleure et plus facile des messages d'erreur erronés ( "pilules empoisonnées") via KIP-161 que ce que j'ai décrit ci-dessous. Voir default.deserialization.exception.handler dans le Kafka 1.0 docs.

Cela pourrait être des choses comme des messages que je ne peux pas désérialiser correctement [...]

Ok, ma réponse se concentre ici sur les problèmes de (dé) sérialisation car cela pourrait être le scénario le plus délicat à gérer pour la plupart des utilisateurs.

[...] ou peut-être que la logique de traitement/filtrage échoue d'une manière inattendue (je n'ai pas de dépendances externes donc il ne devrait pas y avoir d'erreurs transitoires de ce type).

La même réflexion (pour la désérialisation) peut également s'appliquer aux défaillances de la logique de traitement. Ici, la plupart des gens ont tendance à graviter vers l'option 2 ci-dessous (moins la partie de désérialisation), mais YMMV.

J'envisageais d'encapsuler tout mon code de traitement/filtrage dans un catch catch et si une exception était levée, puis un routage vers un "sujet d'erreur". Ensuite, je peux étudier le message et le modifier ou corriger mon code selon le cas, puis le relire sur le maître. Si je laisse des exceptions se propager, le flux semble se bloquer et plus aucun message n'est récupéré.

  • Cette approche est-elle considérée comme la meilleure pratique?

Oui, en ce moment c'est la voie à suivre. Essentiellement, les deux modèles les plus courants sont (1) ignorer les messages corrompus ou (2) envoyer des enregistrements corrompus à un sujet de quarantaine, alias une file d'attente de lettres mortes.

  • Existe-t-il un moyen Kafka streams de gérer cela? Je ne pense pas qu'il existe un concept de DLQ ...

Oui, il existe un moyen de gérer cela, y compris l'utilisation d'une file d'attente de lettres mortes. Cependant, ce n'est (au moins à mon humble avis) pas encore très pratique. Si vous avez des commentaires sur la façon dont l'API devrait vous permettre de gérer cela - par exemple via une méthode nouvelle ou mise à jour, un paramètre de configuration ("si la sérialisation/désérialisation échoue, envoyez l'enregistrement problématique à CE sujet de quarantaine") - veuillez nous le faire savoir. :-)

  • Quelles sont les autres façons d'arrêter Kafka jamming on a "bad message"?
  • Quelles sont les autres approches de gestion des erreurs?

Voir mes exemples ci-dessous.

FWIW, la communauté Kafka discute également de l'ajout d'un nouvel outil CLI qui vous permet d'ignorer les messages corrompus. Cependant, en tant qu'utilisateur de Kafka API Streams, je pense que dans l'idéal, vous souhaitez gérer ces scénarios directement dans votre code, et ne recourir aux utilitaires CLI qu'en dernier recours.

Voici quelques schémas pour le Kafka Streams DSL pour gérer les enregistrements/messages corrompus aka "poison pills". Ceci est tiré de http://docs.confluent.io/current/ streams/faq.html # manipulation-enregistrements-corrompus-et-erreurs de désérialisation-messages-pilule-poison

Option 1: Ignorer les enregistrements corrompus avec flatMap

C'est sans doute ce que la plupart des utilisateurs aimeraient faire.

  • Nous utilisons flatMap car il vous permet de sortir zéro, un ou plusieurs enregistrements de sortie par enregistrement d'entrée. Dans le cas d'un enregistrement corrompu, nous ne produisons rien (zéro enregistrement), ignorant/ignorant ainsi l'enregistrement corrompu.
  • Bénéficiez de cette approche par rapport aux autres listées ici: Nous devons désérialiser manuellement un enregistrement une seule fois!
  • Inconvénient de cette approche: flatMap "marque" le flux d'entrée pour un éventuel re-partitionnement des données, c'est-à-dire si vous effectuez une opération basée sur des clés telles que des regroupements (groupBy/groupByKey ) ou rejoint par la suite, vos données seront re-partitionnées dans les coulisses. Étant donné que cela pourrait être une étape coûteuse, nous ne voulons pas que cela se produise inutilement. Si vous SAVEZ que les clés d'enregistrement sont toujours valides OR que vous n'avez pas besoin d'opérer sur les clés (en les conservant ainsi comme des clés "brutes" dans byte[] format), vous pouvez passer de flatMap à flatMapValues, ce qui n'entraînera pas un nouveau partitionnement des données même si vous rejoignez/groupez/agrégez le flux ultérieurement.

Exemple de code:

Serde<byte[]> bytesSerde = Serdes.ByteArray();
Serde<String> stringSerde = Serdes.String();
Serde<Long> longSerde = Serdes.Long();

// Input topic, which might contain corrupted messages
KStream<byte[], byte[]> input = builder.stream(bytesSerde, bytesSerde, inputTopic);

// Note how the returned stream is of type KStream<String, Long>,
// rather than KStream<byte[], byte[]>.
KStream<String, Long> doubled = input.flatMap(
    (k, v) -> {
      try {
        // Attempt deserialization
        String key = stringSerde.deserializer().deserialize(inputTopic, k);
        long value = longSerde.deserializer().deserialize(inputTopic, v);

        // Ok, the record is valid (not corrupted).  Let's take the
        // opportunity to also process the record in some way so that
        // we haven't paid the deserialization cost just for "poison pill"
        // checking.
        return Collections.singletonList(KeyValue.pair(key, 2 * value));
      }
      catch (SerializationException e) {
        // log + ignore/skip the corrupted message
        System.err.println("Could not deserialize record: " + e.getMessage());
      }
      return Collections.emptyList();
    }
);

Option 2: file d'attente de lettres mortes avec branch

Par rapport à l'option 1 (qui ignore les enregistrements corrompus), l'option 2 conserve les messages corrompus en les filtrant du flux d'entrée "principal" et en les écrivant dans un sujet de quarantaine (pensez: file d'attente de lettres mortes). L'inconvénient est que, pour des enregistrements valides, nous devons payer le coût de désérialisation manuelle deux fois.

KStream<byte[], byte[]> input = ...;

KStream<byte[], byte[]>[] partitioned = input.branch(
    (k, v) -> {
      boolean isValidRecord = false;
      try {
        stringSerde.deserializer().deserialize(inputTopic, k);
        longSerde.deserializer().deserialize(inputTopic, v);
        isValidRecord = true;
      }
      catch (SerializationException ignored) {}
      return isValidRecord;
    },
    (k, v) -> true
);

// partitioned[0] is the KStream<byte[], byte[]> that contains
// only valid records.  partitioned[1] contains only corrupted
// records and thus acts as a "dead letter queue".
KStream<String, Long> doubled = partitioned[0].map(
    (key, value) -> KeyValue.pair(
        // Must deserialize a second time unfortunately.
        stringSerde.deserializer().deserialize(inputTopic, key),
        2 * longSerde.deserializer().deserialize(inputTopic, value)));

// Don't forget to actually write the dead letter queue back to Kafka!
partitioned[1].to(Serdes.ByteArray(), Serdes.ByteArray(), "quarantine-topic");

Option 3: Ignorer les enregistrements corrompus avec filter

Je ne mentionne cela que pour être complet. Cette option ressemble à un mélange d'options 1 et 2, mais est pire que l'une ou l'autre. Par rapport à l'option 1, vous devez payer le coût de désérialisation manuelle pour les enregistrements valides deux fois (mauvais!). Par rapport à l'option 2, vous perdez la possibilité de conserver les enregistrements corrompus dans une file d'attente de lettres mortes.

KStream<byte[], byte[]> validRecordsOnly = input.filter(
    (k, v) -> {
      boolean isValidRecord = false;
      try {
        bytesSerde.deserializer().deserialize(inputTopic, k);
        longSerde.deserializer().deserialize(inputTopic, v);
        isValidRecord = true;
      }
      catch (SerializationException e) {
        // log + ignore/skip the corrupted message
        System.err.println("Could not deserialize record: " + e.getMessage());
      }
      return isValidRecord;
    }
);
KStream<String, Long> doubled = validRecordsOnly.map(
    (key, value) -> KeyValue.pair(
        // Must deserialize a second time unfortunately.
        stringSerde.deserializer().deserialize(inputTopic, key),
        2 * longSerde.deserializer().deserialize(inputTopic, value)));

Toute aide grandement appréciée.

J'espère que je pourrais aider. Si oui, j'apprécierais vos commentaires sur la façon dont nous pourrions améliorer l'API Kafka Streams pour gérer les échecs/exceptions de manière meilleure/plus pratique qu'aujourd'hui. :-)

23
Michael G. Noll