Poser des quotas
GuideExploitationDepuis la version 0.1.0
Guide d'exploitation : configurer, déployer et observer la passerelle. Ce que fait la capacité côté produit : guide produit ; l'appeler : guide développeur.
Objectif : limiter par appelant, pas par adresse du pod qui relaie, et qu'un amont lent ou mort ne devienne pas une panne générale.
Commande - une politique rate-limit, attachée globalement ou par hôte :
policies:
definitions:
per-caller:
type: rate-limit
config:
key: caller # le sujet du jeton, sinon l'adresse du client
classes:
read: { rps: 50, burst: 100 } # GET, HEAD, OPTIONS
write: { rps: 10, burst: 20 } # tout le reste
chains:
global: [per-caller]
server:
trusted_proxies: [10.0.0.0/8] # le bord : son X-Forwarded-For est cru, celui des autres nonDerrière un load balancer cloud, le bord peut ne pas voir le client non plus. Un load balancer de couche 4 se connecte au bord depuis sa propre adresse privée : toutes les requêtes arrivent alors au bord - puis à la passerelle - d'une seule adresse, et tous les anonymes partagent un seau. Activez le proxy protocol aux deux bouts : le load balancer l'envoie (annotation du Service propre au fournisseur, par exemple
service.beta.kubernetes.io/scw-loadbalancer-proxy-protocol-v2: "*"chez Scaleway ouservice.beta.kubernetes.io/aws-load-balancer-proxy-protocol: "*"chez AWS) et les listeners HTTPS du bord l'exigent, ce qui empêche aussi un client de choisir son adresse. Avec Envoy Gateway :apiVersion: gateway.envoyproxy.io/v1alpha1 kind: ClientTrafficPolicy metadata: { name: public-client, namespace: envoy-gateway-system } spec: targetRefs: - { group: gateway.networking.k8s.io, kind: Gateway, name: public, sectionName: https } proxyProtocol: {} # le load balancer l'envoie ; une connexion sans lui est refuséeLes deux réglages vont ensemble : l'un sans l'autre coupe le trafic. Vérifiez le résultat dans le journal d'accès du bord -
x-forwarded-fordoit montrer des adresses publiques, pas celle du load balancer - et gardezserver.trusted_proxiessur la seule plage du bord.
Une opération change de classe dans le contrat - la classe doit exister dans la politique, sinon la passerelle refuse de démarrer :
x-flexway-rate-class: writeUn seau par opération (1.0.0) : par défaut (scope: class), un appelant a un seau
par classe, que toutes les opérations de la classe vident - un parcours qui enchaîne trois écritures
dans la seconde consomme trois jetons du même seau. scope: operation donne à chaque opération son
propre seau par appelant, la classe en fixant la taille - ce que comptent la plupart des
passerelles. Une classe peut surcharger la portée de la politique ; les compteurs partagés
(server.counters) suivent sans réglage de plus.
config:
scope: operation # défaut : class
classes:
read: { rps: 50, burst: 100 }
write: { rps: 10, burst: 20 }
sensitive: { rps: 1, burst: 3, scope: class } # un seau pour toutes les opérations sensiblesDerrière un client serveur (1.0.0) : un BFF relaie des anonymes, tous depuis son adresse.
La clé header compte chacun à part, d'après un en-tête que le BFF pose - cru seulement si le
pair a prouvé une identité de trusted. Sinon l'en-tête est ignoré et l'adresse reste la clé :
une clé qu'un appelant choisit n'en est pas une. Un sujet authentifié reste toujours la clé.
policies:
definitions:
per-user:
type: rate-limit
config:
key: header
header: X-Client-Key
trusted: [web-bff.web.serviceaccount.identity.linkerd.cluster.local]
classes: { read: { rps: 20, burst: 40 } }
server:
trusted_proxies: [local] # le proxy du maillage : les adresses propres du pod
peer_identity_header: l5d-client-id # l'identité mTLS qu'il a vérifiéePas
127.0.0.1/32: le proxy du maillage se connecte depuis l'IP du pod, pas depuis la boucle locale (mesuré avec Linkerd).localdésigne les adresses propres du pod, résolues au chargement - jamais un CIDR de pods, qui croirait chaque pod. Derrière un maillage, comptez les anonymes par identité de pair comme ci-dessus, jamais par adresse : le maillage ne nettoie pasX-Forwarded-For.
L'identité du pair vient du certificat vérifié par l'écouteur TLS de la passerelle, sinon - d'un pair
de server.trusted_proxies seulement - de server.peer_identity_header s'il est déclaré (et alors
de lui seul), sinon de x-forwarded-client-cert. Précondition de peer_identity_header : le proxy local écrase
cet en-tête quand un appelant l'envoie ; sans cette garantie, n'importe quel pod forge la clé.
Validation et erreurs (1.0.0) : la passerelle valide paramètres et corps (full, défaut).
Un service qui reste l'autorité de son corps demande params - le corps est relayé sans être
lu, ni validé ni borné par max_body - ou off - rien n'est validé. Aucun mode n'ouvre la liste
blanche : un paramètre non déclaré reste refusé. Le type des erreurs pointe vers votre portail :
hosts:
api.example.com: { default_upstream: orders, validation: params } # tout l'hôte
errors:
type_base: https://docs.example.com/errors/ # type = <base><code>
field_codes: { minLength: too_short, minimum: out_of_range } # errors[].code dans votre vocabulaireChaque entrée de errors porte field, rule (pour un humain) et code, ce sur quoi un
client aiguille : le mot-clé JSON Schema enfreint (required, type, enum, minLength,
maximum, additionalProperties...), ou invalid quand aucun schéma n'a pu être appliqué (corps
illisible). errors.field_codes renomme un mot-clé vers le jeton de votre contrat ; un mot-clé
absent de la table garde son nom. Une propriété manquante ou en trop est nommée comme champ
(body.age), et chaque règle enfreinte a son entrée.
x-flexway-validation: full # dans le contrat : l'opération l'emporte sur l'hôtePar amont, le cloisonnement et le coupe-circuit (actif par défaut, valeurs montrées = défauts) :
upstreams:
orders:
url: http://orders:8080
max_in_flight: 200 # sans plafond si absent
circuit_breaker: { failure_ratio: 0.5, min_requests: 20, window: 10s, cooldown: 5s }Vérif :
| Situation | Réponse |
|---|---|
| Seau vide | 429, Retry-After, RateLimit-Remaining: 0 - deux utilisateurs derrière un même client serveur ont des seaux distincts |
| Toute réponse limitée | RateLimit-Limit, -Remaining, -Reset |
| Corps ou paramètre hors du schéma | 422, l'extension errors nomme chaque champ et son code - en params, le corps n'est plus examiné |
Méthode non déclarée sur un chemin déclaré, QUERY ou jeton d'extension compris |
405 avec Allow |
Corps au-delà de server.limits.max_body (1 Mio) |
413 |
| Amont en panne, circuit ouvert | 503 immédiat, Retry-After, Flexway-Degraded: circuit-open |
| Amont saturé | 503 immédiat, Flexway-Degraded: upstream-saturated |
| Quota du tenant dépensé | 429 quota_exceeded, Retry-After jusqu'à minuit ou la fin du mois (UTC) |
| Passerelle saturée | 503 overloaded sur le délestable d'abord, Flexway-Degraded: shedding ; critical jamais refusé |
Partager entre répliques, vendre des forfaits, délester (1.0.0) - sans server.counters, les
compteurs sont locaux à chaque réplique : trois répliques donnent jusqu'à trois fois le débit.
Avec, chaque réplique décide seule et se réconcilie avec le magasin toutes les sync - aucun
aller-retour sur le chemin, un plafond tenu à une fenêtre sync près :
server:
counters: { store: "${COUNTERS_URL}", sync: 1s } # Redis, Dragonfly ou Valkey ; non rechargeable
shedding: { target: 20ms } # facultatif ; non rechargeable
in_flight: { max: 2048, queue: 0 } # facultatif - dérivé de la limite mémoire du conteneur s'il est absent ; non rechargeable
policies:
definitions:
plans:
type: quota # par tenant ; exige server.counters
config: { limits: { day: 100000, month: 2000000 } }
chains:
global: [per-caller, plans] x-flexway-priority: sheddable # dans le contrat : critical | normal (défaut) | sheddableMagasin perdu : la passerelle sert, en compteurs locaux, et le dit (flexway_counters_store_up à 0,
une ligne WARN). Le délestage suit la latence d'ordonnancement du runtime Go - la saturation de la
passerelle elle-même, pas la lenteur d'un amont.