Service Mesh 速率限制:本地限流与全局限流的架构组合
一、限流配在网关层,但服务 B 被恶意重试搞垮了——因为网关限流看不见服务侧的实际负载
速率限制(Rate Limiting)有两个典型的架构位置:网关层(入口限流)和服务侧(本地限流)。网关层限流保护整体系统不被外部过载压垮,基于请求的全局 QPS。服务侧限流保护单个服务实例不被内部调用方的异常行为(如重试风暴)压垮。
二者缺一不可。如果只在网关做限流,服务 A 调用服务 B 时如果出现无限重试(Bug 导致),网关看不到内部调用,服务 B 被内部流量压垮。如果只在服务侧做限流,外部突发流量到达网关后分散到服务实例——单实例的限流会触发大量 429 拒绝,浪费了其他健康实例的容量。
Service Mesh 在这两者的结合上有个独特优势:Mesh 的 sidecar(Envoy)同时承担了入口流量和出口流量的代理。可以在 sidecar 层面同时实现本地限流(单 sidecar 级别)和全局限流(基于全局状态)。
二、底层机制与原理剖析
两层限流的职责分工:
全局限流(网关层 + Envoy 集成):
- 保护的是"集群的整体容量"。QPS 1000 是集群最大承载量——网关把超过 1000 的请求拒之门外
- 基于全局状态:需要知道"当前全集群已经处理了多少请求"。需要外部状态存储(Redis)来记录全局计数
- 特征:Coarse-grained(粗粒度),基于外部 IP / API Key / 整体 QPS。响应用 429 + Retry-After
本地限流(服务侧):
- 保护的是"单个服务实例不被打垮"。实例最大处理 500 QPS(取决于 CPU/内存/数据库连接池)
- 基于本地状态:不需要外部存储。Envoy 的
local_rate_limitfilter 直接用令牌桶算法 - 特征:Fine-grained(细粒度),基于服务实例的实际容量。响应也用 429
三、生产级代码实现
# envoy-local-rate-limit.yaml # Envoy 本地限流配置 (EnvoyFilter) apiVersion: networking.istio.io/v1alpha3 kind: EnvoyFilter metadata: name: local-rate-limit namespace: production spec: workloadSelector: labels: app: agent-api configPatches: - applyTo: HTTP_FILTER match: context: SIDECAR_INBOUND listener: filterChain: filter: name: "envoy.filters.network.http_connection_manager" patch: operation: INSERT_BEFORE value: name: envoy.filters.http.local_ratelimit typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit stat_prefix: http_local_rate_limiter # 令牌桶配置 token_bucket: max_tokens: 500 # 突发容量:允许 500 个 token tokens_per_fill: 500 # 每次填充 500 个 token fill_interval: 1s # 每 1 秒填充一次 → 500 QPS # 返回 429 时的 header filter_enabled: runtime_key: local_rate_limit_enabled default_value: numerator: 100 denominator: HUNDRED filter_enforced: runtime_key: local_rate_limit_enforced default_value: numerator: 100 denominator: HUNDRED # 限流生效的请求条件(仅对 /api/ 路径限流) request_headers_to_add_when_not_enforced: - header: key: x-local-rate-limit value: "false"# envoy-global-rate-limit.yaml # Envoy 全局限流配置(集成外部 Rate Limit Service) apiVersion: networking.istio.io/v1alpha3 kind: EnvoyFilter metadata: name: global-rate-limit namespace: production spec: workloadSelector: labels: app: agent-api configPatches: - applyTo: HTTP_FILTER match: context: SIDECAR_INBOUND listener: filterChain: filter: name: "envoy.filters.network.http_connection_manager" patch: operation: INSERT_BEFORE value: name: envoy.filters.http.ratelimit typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.ratelimit.v3.RateLimit domain: agent-api failure_mode_deny: false # 限流服务不可用时放行(非安全场景) rate_limit_service: transport_api_version: V3 grpc_service: envoy_grpc: cluster_name: rate-limit-service # 基于 API Key 做全局限流 descriptors: - key: api_key value: "" # 动态填充 # 第二个 patch:配置 rate-limit-service 的 cluster - applyTo: CLUSTER match: context: SIDECAR_OUTBOUND patch: operation: ADD value: name: rate-limit-service type: STRICT_DNS connect_timeout: 0.5s lb_policy: ROUND_ROBIN load_assignment: cluster_name: rate-limit-service endpoints: - lb_endpoints: - endpoint: address: socket_address: address: rate-limit-service.istio-system.svc.cluster.local port_value: 8081# rate-limit-service.py """ 全局限流服务——基于 Redis 的分布式限流 Envoy 通过 gRPC 调用此服务进行全局速率检查 """ import grpc import redis import time import logging from concurrent import futures from typing import Dict # 注:需要安装 envoy_ratelimit proto 生成的 Python 代码 # pip install grpcio grpcio-tools logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) class GlobalRateLimiter: """ 基于 Redis 的全局限流实现 使用滑动窗口算法代替简单的固定窗口: - 固定窗口有"边界突发"问题(窗口最后 1 秒 + 下一个窗口开始 1 秒 = 2 倍 QPS) - 滑动窗口通过存储每次请求的时间戳来精确控制 """ def __init__(self, redis_host: str = "localhost", redis_port: int = 6379): self.redis = redis.Redis( host=redis_host, port=redis_port, decode_responses=True, socket_connect_timeout=2, socket_timeout=2, ) # 限流规则(从配置文件或管理 API 动态加载) self.rules: Dict[str, Dict] = { # 按 API Key 限流 "api-key": { "free": {"qps": 10, "burst": 20}, "pro": {"qps": 100, "burst": 200}, "enterprise": {"qps": 1000, "burst": 2000}, }, # 按服务限流 "service": { "agent-api": {"qps": 2000, "burst": 3000}, "agent-worker": {"qps": 500, "burst": 800}, }, } def check_rate_limit(self, domain: str, descriptors: list) -> bool: """ 检查速率限制 参数: domain: 限流域(如 "agent-api") descriptors: 限流描述符列表 [{"key": "api_key", "value": "sk-xxx"}, ...] 返回: True: 允许通过, False: 触发限流 """ for descriptor in descriptors: key = descriptor.get("key") value = descriptor.get("value") if not key or not value: continue # 查找对应的限流规则 rule = self._find_rule(key, value) if not rule: continue # 检查 Redis 中的滑动窗口计数 rl_key = f"ratelimit:{domain}:{key}:{value}" limit = rule["qps"] burst = rule.get("burst", limit * 2) allowed = self._check_sliding_window(rl_key, limit, burst) if not allowed: logger.debug("Rate limit hit: %s/%s QPS=%d", key, value, limit) return False return True def _find_rule(self, descriptor_key: str, descriptor_value: str) -> dict: """查找限流规则""" if descriptor_key == "api_key": # 根据 API Key 的 tier 查找 # 生产环境:从数据库查询 API Key 对应的 tier tier_map = self.rules.get("api-key", {}) # 简化:默认 free tier return tier_map.get("free", {"qps": 10, "burst": 20}) if descriptor_key == "service": service_rules = self.rules.get("service", {}) return service_rules.get(descriptor_value, None) return None def _check_sliding_window(self, key: str, limit: int, burst: int) -> bool: """ 滑动窗口限流检查 算法: 1. 使用 Redis Sorted Set,成员 = 请求时间戳(微秒),score = 时间戳 2. 每次检查时删除窗口外的旧数据 3. 统计窗口内的请求数 4. 如果超过限制 → 拒绝;否则 → 添加当前请求并允许 """ now_us = int(time.time() * 1_000_000) # 微秒精度 window_us = 1_000_000 # 1 秒窗口 pipe = self.redis.pipeline() # 1. 删除窗口外的旧数据 pipe.zremrangebyscore(key, 0, now_us - window_us) # 2. 统计窗口内的请求数 pipe.zcard(key) try: _, current_count = pipe.execute() except redis.RedisError as e: logger.error("Redis error in rate limiter: %s", e) # Redis 不可用时——放行(取决于 failure_mode_deny 配置) return True current_count = int(current_count) if current_count >= limit + burst: return False # 触发限流 # 3. 记录本次请求 try: self.redis.zadd(key, {str(now_us): now_us}) self.redis.expire(key, 2) # 2 秒后自动过期 except redis.RedisError: pass # 记录失败不阻塞请求 return True # --------------------------------------------------------------------------- # gRPC 服务 # --------------------------------------------------------------------------- class RateLimitServicer: """Envoy Rate Limit Service gRPC 接口实现""" def __init__(self, limiter: GlobalRateLimiter): self.limiter = limiter def ShouldRateLimit(self, request, context): """ 实现 Envoy RateLimitService.Check 接口 """ domain = request.domain descriptors = [] for desc in request.descriptors: for entry in desc.entries: descriptors.append({ "key": entry.key, "value": entry.value, }) # 全局限流检查 overall_code = self.limiter.check_rate_limit(domain, descriptors) # 构建 gRPC 响应(简化——实际需要导入 envoy proto 生成的代码) # response = RateLimitResponse() # response.overall_code = OK if overall_code else OVER_LIMIT # return response logger.debug("Rate limit check: domain=%s allowed=%s", domain, overall_code) return None # 实际返回 gRPC 响应对象 def start_server(limiter: GlobalRateLimiter, port: int = 8081): """启动限流 gRPC 服务""" server = grpc.server(futures.ThreadPoolExecutor(max_workers=10)) # rate_limit_pb2_grpc.add_RateLimitServiceServicer_to_server( # RateLimitServicer(limiter), server # ) server.add_insecure_port(f"[::]:{port}") server.start() logger.info("Rate limit service started on port %d", port) return server if __name__ == "__main__": limiter = GlobalRateLimiter(redis_host="redis.istio-system.svc") server = start_server(limiter) try: import time while True: time.sleep(3600) except KeyboardInterrupt: server.stop(0)四、边界分析与架构权衡
全局限流的 Redis 依赖:
- 如果 Redis 不可用,全局限流失效。Envoy 的
failure_mode_deny控制这种行为:true(安全优先——Redis 不可用时拒绝所有请求)或 false(可用优先——Redis 不可用时放行) - 对于 API 计费场景推荐 deny(防止免费用户绕过计费);对于内部服务调用推荐 allow(可用性优先)
本地限流的公平性:
- 多副本的本地限流在负载均衡不均时可能产生"不公平"——副本 A 被打到 500 QPS(触发限流),副本 B 只被打到 100 QPS(大量容量浪费)
- 如果负载均衡算法是 Round Robin 或 Least Connection,通常不会出现严重不均
两层限流的协调:
- 本地限流的值应该是全局限额 / 副本数。如果全局限额 2000 QPS、3 副本,每个副本本地限流 700 QPS(略高于 2000/3=666,留 buffer)
- 优先触发本地限流(延迟最低——不需要调 Redis),全局限流作为兜底(保护集群整体)
五、总结
Service Mesh 的两层限流:全局(网关 + Envoy + Redis)保护集群整体容量,本地(Envoy local_rate_limit filter)保护单个实例。全局限流用滑动窗口算法(基于 Redis Sorted Set)避免固定窗口的边界突发问题。本地限流的值 = 全局限额 / 副本数 + buffer。两层同时生效——本地优先触发(低延迟),全局作为兜底。Redis 不可用时根据场景选择 deny(安全优先)或 allow(可用优先)。