Warum Kafka im Zahlungsverkehr
Apache Kafka ist das Rückgrat unserer VoP-Architektur. Die Anforderungen sind klar: hoher Durchsatz, niedrige Latenz, garantierte Zustellung und Skalierbarkeit. Kafka liefert das, aber nur mit der richtigen Konfiguration. Ein falsch partitioniertes Topic oder eine schlecht konfigurierte Consumer Group kann den Unterschied zwischen 50ms und 5s Verarbeitungszeit ausmachen.
Partitionsstrategien für VoP-Requests
Die Partitionierung bestimmt, wie Nachrichten auf Consumer verteilt werden. Für VoP-Requests gibt es verschiedene Ansätze mit jeweils eigenen Trade-offs.
Partitionierung nach IBAN-Prefix
VoP-Requests enthalten die Empfänger-IBAN. Eine Partitionierung nach dem BIC-Teil der IBAN (oder den ersten 8 Zeichen) stellt sicher, dass Anfragen für denselben PSP auf derselben Partition landen:
public class IbanPartitioner implements Partitioner {
@Override
public int partition(String topic, Object key, byte[] keyBytes,
Object value, byte[] valueBytes, Cluster cluster) {
String iban = (String) key;
String routingKey = iban.substring(4, 12); // BIC-relevant
int numPartitions = cluster.partitionCountForTopic(topic);
return Math.abs(routingKey.hashCode() % numPartitions);
}
}
Vorteile:
- Ordering pro PSP, Anfragen an denselben PSP werden in Reihenfolge verarbeitet
- Cache-Locality, Der Consumer kann Routing-Informationen für “seine” PSPs cachen
Nachteile:
- Ungleiche Verteilung, Große Banken erzeugen mehr Traffic als kleine, was zu Hot Partitions führt
Partitionierung nach Request-ID
Alternativ kann die Request-ID als Partition Key dienen. Das verteilt Nachrichten gleichmäßig, opfert aber die Ordering-Garantie pro PSP.
In der Praxis verwenden wir einen hybriden Ansatz: VoP-Requests werden nach IBAN-Prefix partitioniert, interne Processing-Events nach Request-ID.
Consumer Groups und horizontale Skalierung
Jede Partition wird von genau einem Consumer in einer Consumer Group verarbeitet. Das bedeutet: Die maximale Parallelität entspricht der Anzahl der Partitionen.
Dimensionierung
| Parameter | Test | QA | Produktion |
|---|---|---|---|
| Partitionen pro Topic | 6 | 12 | 48 |
| Consumer Instanzen | 2 | 4 | 16 |
| Partitionen pro Consumer | 3 | 3 | 3 |
| Durchsatz (Nachrichten/s) | ~500 | ~2.000 | ~20.000 |
Die Faustregel: 3-5 Partitionen pro Consumer. Mehr Partitionen pro Consumer erhöhen die Last auf einzelne Instanzen, weniger verschwenden Ressourcen.
Autoscaling mit KEDA
Kubernetes-basiertes Autoscaling der Consumer Instanzen über KEDA (Kubernetes Event-Driven Autoscaling):
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: vop-processor-scaler
spec:
scaleTargetRef:
name: vop-processor
minReplicaCount: 4
maxReplicaCount: 16
triggers:
- type: kafka
metadata:
bootstrapServers: kafka-prod:9093
consumerGroup: vop-processor
topic: vop-requests
lagThreshold: "1000"
Wenn der Consumer Lag 1000 Nachrichten übersteigt, skaliert KEDA automatisch neue Consumer-Instanzen hoch. Das funktioniert in der Praxis gut, erfordert aber ausreichend viele Partitionen, neue Consumer ohne zugewiesene Partition sind nutzlos.
Idempotenz, keine doppelten Transaktionen
Im Zahlungsverkehr ist Idempotenz nicht optional. Eine doppelt verarbeitete VoP-Anfrage führt bestenfalls zu Performance-Verschwendung, schlimmstenfalls zu inkonsistenten Zuständen.
Producer Idempotency
Kafka unterstützt idempotente Producer nativ:
spring:
kafka:
producer:
properties:
enable.idempotence: true
acks: all
retries: 3
max.in.flight.requests.per.connection: 5
Mit enable.idempotence: true garantiert Kafka, dass jede Nachricht exakt einmal geschrieben wird, selbst bei Netzwerk-Retries.
Consumer Idempotency
Auf Consumer-Seite ist Idempotenz aufwendiger. Wir verwenden einen Deduplication-Mechanismus basierend auf der Request-ID:
@Transactional
public void processVopRequest(VopRequest request) {
if (processedRequestRepository.existsById(request.getRequestId())) {
log.info("Duplicate request ignored: {}", request.getRequestId());
return;
}
var result = matchingService.match(request);
processedRequestRepository.save(new ProcessedRequest(request.getRequestId()));
kafkaTemplate.send("vop-responses", result);
}
Die Kombination aus Datenbankabfrage und Verarbeitung in einer Transaktion stellt sicher, dass jede Request-ID exakt einmal verarbeitet wird.
Dead Letter Queues
Nachrichten, die nicht verarbeitet werden können, sei es durch Validierungsfehler, fehlende Stammdaten oder technische Probleme, landen in einer Dead Letter Queue (DLQ):
@Bean
public DefaultErrorHandler errorHandler(KafkaTemplate<String, Object> template) {
var recoverer = new DeadLetterPublishingRecoverer(template,
(record, ex) -> new TopicPartition(
record.topic() + ".dlq",
record.partition()
));
var backoff = new ExponentialBackOffWithMaxRetries(3);
backoff.setInitialInterval(1000);
backoff.setMultiplier(2.0);
backoff.setMaxInterval(10000);
return new DefaultErrorHandler(recoverer, backoff);
}
DLQ-Strategie
- 3 Retries mit exponential Backoff, 1s, 2s, 4s. Behebt transiente Fehler wie DB-Timeouts
- DLQ Topic pro Source Topic,
vop-requests.dlqfür fehlgeschlagene VoP-Requests - Monitoring der DLQ, Alert wenn DLQ-Messages > 0. Eine nicht-leere DLQ erfordert manuelle Analyse
- Replay-Mechanismus, Werkzeug zum erneuten Einspielen von DLQ-Nachrichten nach Fehlerbehebung
Backpressure Handling
Was passiert, wenn mehr Nachrichten eintreffen als verarbeitet werden können? Ohne Backpressure-Strategie wächst der Consumer Lag unbegrenzt.
Maßnahmen auf verschiedenen Ebenen
| Ebene | Maßnahme | Wirkung |
|---|---|---|
| Producer | Rate Limiting am API Gateway | Eingangslast begrenzen |
| Kafka | Topic-spezifische Retention | Alte Nachrichten nach 24h verwerfen |
| Consumer | max.poll.records begrenzen | Batch-Größe pro Poll reduzieren |
| Consumer | Concurrency erhöhen | Mehr parallele Verarbeitung pro Instanz |
| Infrastruktur | KEDA Autoscaling | Mehr Consumer-Instanzen hochfahren |
Priorisierung
Nicht alle VoP-Requests haben dieselbe Priorität. Instant Payment VoP-Requests müssen innerhalb von Sekunden beantwortet werden, Batch-Prüfungen können warten. Wir verwenden separate Topics mit unterschiedlichen Consumer-Konfigurationen:
vop-requests-instant, Hohe Priorität, niedrige Latenz, mehr Consumervop-requests-batch, Niedrige Priorität, höherer Durchsatz, weniger Consumer
Monitoring und Metriken
Kafka-spezifische Metriken, die wir in Grafana überwachen:
- Consumer Lag, Die wichtigste Metrik. Steigender Lag bedeutet: Consumer können nicht mithalten
- Throughput (Messages/s), Pro Topic und pro Consumer Group
- Processing Time, Wie lange dauert die Verarbeitung einer einzelnen Nachricht?
- Error Rate, Anteil fehlgeschlagener Verarbeitungen
- DLQ Size, Anzahl der Nachrichten in Dead Letter Queues
# Consumer Lag Alert
kafka_consumer_records_lag_max{consumer_group="vop-processor"} > 5000
Fazit
Kafka ist das richtige Werkzeug für die Echtzeit-Transaktionsverarbeitung im Zahlungsverkehr, aber nur mit durchdachter Partitionierung, robuster Idempotenz und klaren Backpressure-Strategien. Die hier beschriebenen Patterns sind das Ergebnis aus dem Betrieb eines produktiven VoP-Systems. Jede Designentscheidung, von der Partition Key Wahl bis zur DLQ-Strategie, hat direkte Auswirkungen auf Zuverlässigkeit und Performance.