flexway
FRENParler à un expert

Poser des quotas

GuideExploitationDepuis la version 0.1.0

Markdown

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 non

Derriè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 ou service.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ée

Les 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-for doit montrer des adresses publiques, pas celle du load balancer - et gardez server.trusted_proxies sur 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: write

Un 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 sensibles

Derriè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ée

Pas 127.0.0.1/32 : le proxy du maillage se connecte depuis l'IP du pod, pas depuis la boucle locale (mesuré avec Linkerd). local dé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 pas X-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 vocabulaire

Chaque 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ôte

Par 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) | sheddable

Magasin 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.

Étapes suivantes