Quotas des clients Kafka
Freiner producteurs et consommateurs Kafka par principal et par tenant, sans jamais couper, et lire les signaux de l'écouteur.
GuideExploitationDepuis la version 1.6.0
Sur cette page
Guide d'exploitation : configurer, déployer et observer la passerelle. Ce que fait la fonctionnalité côté produit : guide produit ; l'appeler : guide développeur.
Les quotas de l'écouteur Kafka se déclarent sous listeners.kafka.quotas ; ses signaux disent ensuite qui est freiné, refusé ou fermé, et pourquoi.
Étapes
Quotas : l'étranglement de KIP-219
Octets produits, octets lus et requêtes par seconde, pour chaque principal vérifié et pour chaque
tenant (listeners.kafka.quotas). Jamais une coupure : au-delà, la réponse
part tout de suite avec throttle_time_ms, puis la passerelle ne lit plus la connexion du client
pendant ce délai (KIP-219). Un client qui ignore le
champ - librdkafka ne l'a jamais appliqué (librdkafka#5592) -
est freiné quand même : ce qu'il envoie attend dans la socket. Sans quotas, rien n'est étranglé.
| Clé | Pourquoi | Défaut |
|---|---|---|
client.producer_byte_rate |
Octets produits par seconde (requêtes Produce, en-têtes compris) par un principal : le sujet du jeton, la clé d'API ou le certificat - jamais le client.id, qu'un client choisit librement |
aucune borne |
client.consumer_byte_rate |
Octets lus par seconde (réponses Fetch) par un principal |
aucune borne |
client.request_rate |
Requêtes par seconde d'un principal, toutes API, une fois authentifié | aucune borne |
tenant.producer_byte_rate, tenant.consumer_byte_rate, tenant.request_rate |
Les mêmes pour tous les principaux d'un tenant ensemble : un tenant n'achète pas plus de débit en multipliant les clés | aucune borne |
window |
La rafale admise d'un coup : un seau tient débit x window (les fenêtres de quota de Kafka font environ une seconde) |
1s (de 100ms à 10s) |
max_throttle |
Plafond de throttle_time_ms - et du silence qui suit : sous le request.timeout.ms des clients (30 s par défaut chez Kafka), qu'un étranglement ne doit jamais faire expirer |
10s (de 100ms à 30s ; défaut provisoire, à fixer par les mesures de performance) |
quota |
Le nom d'une politique quota (Poser des quotas) : le budget jour ou mois de requêtes Produce du tenant, compté sous ce nom comme en HTTP ; épuisé, le Produce est refusé POLICY_VIOLATION (44, non réessayable) et rien n'est écrit |
aucun |
Toutes ces clés sont rechargeables : le débit en vigueur s'applique à la requête suivante, ce qu'un principal a dépensé est gardé. Avec server.counters,
les seaux sont partagés entre répliques comme ceux des limites HTTP : chaque réplique décide seule et se
réconcilie toutes les sync (un débit tenu à une fenêtre sync près ; écart pas encore mesuré). Sans,
chaque réplique compte pour elle.
| Cas | Ce qui se passe |
|---|---|
| Client qui respecte KIP-219 (Java, franz-go) | Réponse immédiate avec throttle_time_ms ; il attend ce délai avant sa requête suivante |
| Client qui ignore le champ (librdkafka : confluent-kafka-go, Python, .NET) | Même réponse ; il renvoie aussitôt, mais sa requête suivante n'est servie qu'à l'échéance : le débit tient sans sa coopération |
Fetch d'un principal ou d'un tenant déjà en dette |
Réponse vide immédiate avec throttle_time_ms, rien n'est demandé au courtier |
| Courtier qui étrangle aussi la passerelle (ses propres quotas sur le principal de la passerelle) | throttle_time_ms = le plus grand des deux délais ; le silence du courtier est respecté : rien ne lui est envoyé sur cette connexion avant son terme |
| Quota jour ou mois du tenant épuisé | Refus POLICY_VIOLATION sur Produce (cause quota), pas un étranglement ; les lectures (Fetch) ne sont pas encore comptées au budget : leur code de refus doit d'abord être vérifié contre les principales bibliothèques clientes (un consommateur Java lève une exception sur un code inattendu) |
Par tenant
Un tenant ne prend pas toute la bande passante des courtiers, quel que soit son nombre de clés :
listeners:
kafka:
quotas:
tenant: { producer_byte_rate: 10MiB, consumer_byte_rate: 40MiB, request_rate: 2000 }Par client
Chaque principal (jeton, clé, certificat) a son seau, sous le seau de son tenant :
listeners:
kafka:
quotas:
client: { producer_byte_rate: 1MiB, consumer_byte_rate: 4MiB, request_rate: 200 }
tenant: { producer_byte_rate: 10MiB, consumer_byte_rate: 40MiB }
window: 2s # des rafales de deux secondes de débit
max_throttle: 5s # des clients au request.timeout.ms court
server:
counters: { store: "${COUNTERS_URL}" } # le débit tenu sur toutes les répliques ensembleAvec les quotas du courtier
Rien à déclarer : la passerelle relaie le plus grand des deux délais. Le
courtier voit le principal de la passerelle (upstreams.<nom>.sasl.username) et l'identifiant client du
tenant (acme.app-1, Les noms des groupes et des transactions) ; un quota du courtier sur
ce principal freine tous les tenants ensemble, ceux de la passerelle chacun le sien :
kafka-configs --bootstrap-server kafka-0.kafka:9093 --command-config admin.properties \
--alter --entity-type users --entity-name flexway --add-config producer_byte_rate=104857600Budget jour ou mois
Le même forfait qu'en HTTP, compté sur les Produce :
listeners:
kafka:
quotas: { quota: plans }
server:
counters: { store: "${COUNTERS_URL}" } # exigé par toute politique quota
policies:
definitions:
plans: { type: quota, config: { limits: { day: 100000, month: 2000000 } } }Vérifier
Un client librdkafka
Rien à changer côté client ; ce qu'il voit au-delà du quota :
# confluent-kafka-go, Python, .NET : des propriétés ordinaires
bootstrap.servers=bootstrap.shop.kafka.example.com:9093
security.protocol=SASL_SSL
sasl.mechanisms=PLAIN
sasl.username=acme-orders-producer
sasl.password=${FLEXWAY_KAFKA_KEY}
# au-delà du quota : le producteur avance au débit déclaré ; sa statistique "throttle" (statistics.interval.ms)
# montre throttle_time_ms ; request.timeout.ms (30 s) reste au-dessus de max_throttleLire les signaux
Noms Prometheus :
# part des requêtes étranglées, par cluster virtuel
sum by (cluster) (rate(flexway_kafka_requests_total{decision="throttled"}[5m]))
/ sum by (cluster) (rate(flexway_kafka_requests_total[5m]))
# refus par cause (hors quota jour ou mois)
sum by (cause) (rate(flexway_kafka_requests_total{decision="refused", cause!="quota"}[5m]))
# connexions refusées par borne ; fermetures normales à part
sum by (bound) (rate(flexway_kafka_connections_refused_total[5m]))
sum by (cause) (rate(flexway_kafka_connections_closed_total[5m]))
# attente p95 de la part d'un tenant
histogram_quantile(0.95, sum by (le, tenant) (rate(flexway_kafka_tenant_budget_wait_seconds_bucket[5m])))Signaux : métriques, traces, audit
Les métriques flexway.kafka.* partent en OTLP avec les autres (même chemin, service.instance.id par
réplique) ; chaque dimension est fermée : clusters du contrat, noms de la table des API, jeux ci-dessous,
et tenant sous la règle de toutes les dimensions de tenant - 200 tenants gardent leur série, les
suivants partagent _other. Jamais un sujet, une clé, un certificat, un client.id, un sujet Kafka
physique ni une adresse.
| Métrique | Dimensions | Ce qu'elle dit |
|---|---|---|
flexway.kafka.connections (jauge) |
cluster, tenant, mechanism (oauthbearer, api_key, mtls, none avant l'authentification ; _none et _anonymous avant le SNI) |
Connexions ouvertes : sur une réplique, l'occupation de max_connections - qui ne refuse rien, une connexion de plus attend dans la file du noyau |
flexway.kafka.requests |
cluster, tenant, api, decision (allowed, throttled, refused), cause (acl, admin_api, quota, unknown_topic, authentication, kms_unavailable, unsupported, key_destroyed ; none) |
Requêtes jugées ; refused dès qu'une partie est refusée par la passerelle |
flexway.kafka.bytes |
cluster, tenant, direction (produce, fetch) |
Les octets que comptent les quotas |
flexway.kafka.throttle (histogramme, s) |
cluster, tenant, quota (producer_byte_rate, consumer_byte_rate, request_rate, broker), scope (client, tenant, broker) |
throttle_time_ms de chaque réponse étranglée |
flexway.kafka.connections.refused |
bound (max_unauthenticated_connections, max_connections_per_ip, max_connections_per_tenant, max_connection_rate_per_tenant), tenant pour les deux bornes du tenant seulement |
Connexions refusées par une borne ; jamais avant l'authentification un tenant, jamais une adresse ; le refus par max_connections_per_tenant n'est pas compté aussi en authentication |
flexway.kafka.connections.closed |
cause (auth_timeout sans tenant, idle_timeout, max_session_lifetime), tenant |
Fermetures normales, jamais mêlées aux refus ; max_session_lifetime : session arrivée à son terme (la borne, ou exp, ou NotAfter) sans ré-authentification |
flexway.kafka.tenant_budget |
cluster, tenant, outcome (waited, closed) |
Réponses qui ont attendu la part du tenant (max_tenant_in_flight_bytes, mode wait) ; connexions fermées à elle (mode close) |
flexway.kafka.tenant_budget.wait (histogramme, s) |
cluster, tenant |
Combien de temps une réponse a attendu cette part |
flexway.kafka.encryption |
cluster, tenant, op (encrypt, decrypt), outcome (ok, kms_unavailable, key_destroyed) |
Chiffrement par champ |
flexway.kafka.upstream.connections |
cluster, broker (identifiant de nœud, ou bootstrap), outcome (ok, failed) |
Connexions de la passerelle à chaque courtier : la santé par courtier |
| Clé | Pourquoi | Défaut |
|---|---|---|
observability.kafka.budget_wait_buckets |
Bornes de flexway.kafka.tenant_budget.wait, croissantes, 20 au plus : à resserrer quand les attentes normales d'un parc tiennent sous la milliseconde. Non rechargeable (les bornes d'un histogramme se fixent à sa création) |
1ms, 5ms, 10ms, 50ms, 100ms, 500ms, 1s, 5s, 10s, 30s (le délai d'écriture) |
observability.kafka.closures |
Compter les fermetures normales : false sur un parc de clients éphémères où elles ne disent rien. Rechargeable |
true |
observability:
kafka:
budget_wait_buckets: [100us, 1ms, 10ms, 100ms, 1s, 10s, 30s]
closures: falseTraces
Un span par requête authentifiée échantillonnée (kafka <API>, attributs kafka.cluster,
tenant, auth.mechanism, kafka.decision, kafka.cause, kafka.throttle_time_ms) - jamais un sujet ni
un nom de sujet Kafka.
Audit
Chaque requête refusée et chaque requête
d'administration (permise ou refusée), dans la même chaîne que HTTP - host = cluster virtuel,
method = KAFKA, operation = l'API, code = la cause, le sujet haché ; les requêtes de données
permises sont comptées, pas journalisées une à une.