Service Mesh 与 API 网关的分工:流量入口治理的两个层次(续篇)
Service Mesh 与 API 网关的分工流量入口治理的两个层次续篇场景痛点团队同时部署了Istio Service Mesh和Kong API网关。两者功能重叠限流、认证、可观测性、流量路由。开发者困惑限流规则该写在Istio的EnvoyFilter还是Kong的Plugin认证JWT应该在网关层校验还是Mesh层校验结果双写——网关校验JWT后Istio又校验一次请求延迟增加20ms。更糟的情况限流规则只在Kong配置了Mesh内服务间调用没有限流——内部流量风暴时Mesh扛不住。或者认证只在Istio配置了外部请求绕过网关直接打到Mesh——未经认证的请求进入服务网格。核心矛盾网关和Mesh的治理职责边界模糊。功能重叠导致双写或遗漏职责不清导致安全漏洞或性能浪费。底层机制与原理剖析Service Mesh和API网关是两个不同层次的流量治理各有明确的职责边界关键区别流量方向。网关治理南北向流量外部→内部。Mesh治理东西向流量内部→内部。网关是入口大门Mesh是内部走廊。治理粒度。网关是全局级——一条限流规则影响所有经过网关的请求。Mesh是服务级——一条限流规则只影响特定服务间的调用。故障恢复。网关处理入口故障上游服务整体不可用→返回503。Mesh处理局部故障某个服务实例异常→重试其他实例熔断隔离。职责分工矩阵功能网关层Mesh层为什么JWT认证✅ 全权负责❌ 不重复认证是入口职责内部服务间调用已通过认证全局限流✅ IP/API级❌ 不重复全局流量入口只有一个点网关限流最高效服务级限流❌ 不负责✅ 单服务级保护单个服务不被内部流量打爆重试策略❌ 不负责✅ 单调用级重试是局部恢复策略网关不该替服务做重试决策熔断隔离❌ 不负责✅ 单服务级熔断基于局部健康状态网关看不到内部健康协议转换✅ 负责❌ 不负责外部HTTP→内部gRPC是入口职责可观测性✅ 入口指标✅ 内部指标两层各自采集数据互补TLS终止✅ 负责✅ 内部mTLS外部TLS在网关终止内部mTLS由Mesh管理生产级代码实现API网关配置Kong# kong-declarative-config.yml # 网关层只负责入口治理 _format_version: 3.0 _transform: true services: # 订单服务入口 - name: order-service url: http://order-service.internal:8080 # 转发到Mesh内部服务 routes: - name: order-api paths: - /api/orders methods: - GET - POST - PUT - DELETE plugins: # 1. JWT认证——网关层全权负责 - name: jwt config: uri_param_names: - jwt claims_to_verify: - exp # 过期时间校验 key_claim_name: iss secret_is_base64: false # 为什么在网关校验而非MeshJWT是外部用户凭证 # Mesh内的服务间调用不需要用户级JWT认证 # 2. 全局限流——按IPAPI维度 - name: rate-limiting config: minute: 100 # 每分钟100次按IP policy: redis # 使用Redis存储计数集群模式需要共享计数 # 为什么用Redis而非local计数多网关实例需要共享限流计数 # local计数导致每个实例独立限流总流量是N×100而非100 redis_host: redis-cluster redis_port: 6379 fault_tolerance_percent: 50 # Redis不可用时允许50%超限 # 为什么设fault_toleranceRedis宕机时限流失效 # 100%严格会导致Redis故障时所有请求被限死 limit_by: ip # 按IP限流 # 为什么不按consumerconsumer维度需要JWT解析 # 在JWT校验后限流增加了处理顺序依赖 # 3. 请求大小限制 - name: request-size-limiting config: allowed_size_mb: 5 # 最大5MB请求体 # 为什么限制请求大小超大请求体是DDoS的常见手段 # 网关层拦截比内部服务拦截更早、更安全 # 4. CORS——外部入口需要 - name: cors config: origins: - https://app.example.com methods: - GET - POST - PUT - DELETE max_age: 3600 # 为什么CORS在网关而非MeshCORS是浏览器安全策略 # 只有外部HTTP请求才需要Mesh内gRPC调用不需要CORS # 5. Prometheus指标导出——入口级指标 - name: prometheus config: metrics: - name: http_status stat_code: true - name: latency quantiles: - 0.5 - 0.9 - 0.99 per_consumer: false # 不按consumer维度——太细导致高基数 # 为什么不per_consumerper_consumer按JWT claims分维度 # 几千个用户导致指标基数爆炸Prometheus内存溢出 consumers: - username: app-client jwt_secrets: - key: app-client-issuer algorithm: RS256 rsa_public_key: -----BEGIN PUBLIC KEY-----\nMIIBIj...\n-----END PUBLIC KEY-----Service Mesh配置Istio# istio-service-level-policies.yaml # Mesh层只负责内部治理 # 服务级限流——保护order-service不被内部流量打爆 apiVersion: envoyproxy.io/v1alpha1 kind: EnvoyFilter metadata: name: order-service-local-rate-limit namespace: production spec: workloadSelector: labels: app: order-service configPatches: - applyTo: HTTP_FILTER match: context: SIDECAR_INBOUND # 只限制进入order-service的流量 # 为什么INBOUND而非OUTBOUND保护服务自己不被打爆 # OUTBOUND限制的是服务发出的流量——方向不对 patch: operation: INSERT_BEFORE value: name: envoy.filters.http.local_rate_limit typed_config: type: type.googleapis.com/envoy.extensions.filters.http.local_rate_limit.v3.LocalRateLimit stat_prefix: order_rate_limit token_bucket: max_tokens: 500 # 突发容量500 tokens_per_fill: 100 # 每秒补充100 fill_interval: 1s filter_enabled: runtime_key: local_rate_limit.enabled default_value: true # 为什么用local rate limit而非全局Redis限流 # 服务级限流是每个实例独立的local限流足够。 # 全局Redis限流增加网络延迟和外部依赖 # 重试策略——局部故障恢复 apiVersion: networking.istio.io/v1beta1 kind: DestinationRule metadata: name: order-service-retry namespace: production spec: host: order-service trafficPolicy: connectionPool: tcp: maxConnections: 100 # 连接池上限 # 为什么设上限无上限的连接池在流量高峰时耗尽服务端资源 http: h2UpgradePolicy: UPGRADE # HTTP/1→HTTP/2自动升级 maxRequestsPerConnection: 50 # 单连接最大请求数 # 为什么限制请求数长连接复用过多请求导致头部阻塞 outlierDetection: # 熔断配置 consecutive5xxErrors: 3 # 连续3次5xx→标记异常 interval: 30s # 30秒检测一次 baseEjectionTime: 60s # 异常实例隔离60秒 maxEjectionPercent: 50 # 最多隔离50%实例 # 为什么maxEjectionPercent50而非100隔离所有实例服务完全不可用 # 50%隔离保留部分容量同时给异常实例恢复时间 retries: attempts: 2 # 重试2次原始2次最多3次调用 perTryTimeout: 5s # 单次调用超时5秒 retryOn: 5xx,connect-failure,refused-stream # 为什么retryOn指定具体错误而非全部全部重试包括4xx客户端错误 # 4xx重试不会成功浪费资源。只重试服务端错误和连接故障 # 内部mTLS——Mesh内服务间通信加密 apiVersion: security.istio.io/v1beta1 kind: PeerAuthentication metadata: name: default namespace: production spec: mtls: mode: STRICT # 为什么STRICT而非PERMISSIVEPERMISSIVE允许明文密文混合 # 在全部服务接入Mesh后STRICT确保所有内部流量加密 # 防止内部窃听分工校验工具# tools/mesh_gateway_overlap_checker.py 检查网关和Mesh配置的功能重叠发现双写或遗漏 import yaml from typing import List, Dict OVERLAP_RULES [ { function: JWT认证, gateway_indicator: [jwt, oauth2, authentication], mesh_indicator: [RequestAuthentication, AuthorizationPolicy], expected_owner: gateway, reason: 认证是入口职责Mesh内服务间调用不需要用户级认证 }, { function: 全局限流, gateway_indicator: [rate-limiting, rate_limit], mesh_indicator: [local_rate_limit, rate_limit], expected_owner: gateway, reason: 全局限流在入口点执行最高效Mesh内局部限流不覆盖绕过网关的请求 }, { function: 服务级限流, gateway_indicator: [rate-limiting-per-service], mesh_indicator: [local_rate_limit], expected_owner: mesh, reason: 服务级限流保护特定服务Mesh内配置更精确 }, { function: 重试策略, gateway_indicator: [retries, retry], mesh_indicator: [DestinationRule.retries], expected_owner: mesh, reason: 重试是局部恢复策略网关不该替服务做重试决策 }, { function: 熔断隔离, gateway_indicator: [circuit-breaker, circuit_breaker], mesh_indicator: [outlierDetection], expected_owner: mesh, reason: 熔断基于局部健康状态网关看不到服务内部健康 }, { function: CORS, gateway_indicator: [cors], mesh_indicator: [], expected_owner: gateway, reason: CORS是浏览器安全策略只有外部HTTP请求需要 }, ] class OverlapChecker: def __init__(self): self.overlaps: List[Dict] [] self.missing: List[Dict] [] def check(self, gateway_config: Dict, mesh_configs: List[Dict]) - OverlapReport: 检查配置重叠和遗漏 for rule in OVERLAP_RULES: in_gateway any( self._contains_indicator(gateway_config, rule[gateway_indicator]) ) in_mesh any( self._contains_indicator(mesh_config, rule[mesh_indicator]) for mesh_config in mesh_configs ) if in_gateway and in_mesh: # 功能在两层都有配置——重叠 if rule[expected_owner] gateway: self.overlaps.append({ function: rule[function], issue: f两层都配置了{rule[function]}应只在网关层配置, reason: rule[reason], action: f从Mesh配置中移除{rule[function]} }) elif rule[expected_owner] mesh: self.overlaps.append({ function: rule[function], issue: f两层都配置了{rule[function]}应只在Mesh层配置, reason: rule[reason], action: f从网关配置中移除{rule[function]} }) if not in_gateway and not in_mesh: # 功能两层都没有配置——遗漏 self.missing.append({ function: rule[function], issue: f{rule[function]}没有在任何层配置, action: f在{rule[expected_owner]}层配置{rule[function]} }) # 网关有但Mesh也应该有互补功能 if rule[function] 可观测性: if not in_gateway: self.missing.append({ function: 入口可观测性, issue: 网关没有配置入口级指标采集, action: 在网关配置Prometheus指标导出 }) if not in_mesh: self.missing.append({ function: 内部可观测性, issue: Mesh没有配置内部调用指标采集, action: 在Istio配置Telemetry资源 }) return { overlaps: self.overlaps, missing: self.missing, overlap_count: len(self.overlaps), missing_count: len(self.missing) } def _contains_indicator(self, config: Dict, indicators: List[str]) - bool: 检查配置中是否包含指定功能的关键词 config_str yaml.dump(config) return any(indicator in config_str for indicator in indicators) # 使用示例 if __name__ __main__: checker OverlapChecker() # 加载网关配置 with open(kong-declarative-config.yml) as f: gateway_config yaml.safe_load(f) # 加载Mesh配置 mesh_configs [] for path in [istio-destination-rules.yaml, istio-envoy-filters.yaml]: with open(path) as f: for doc in yaml.safe_load_all(f): mesh_configs.append(doc) report checker.check(gateway_config, mesh_configs) print(f重叠问题: {report[overlap_count]}) for overlap in report[overlaps]: print(f - {overlap[function]}: {overlap[issue]}) print(f 建议: {overlap[action]}) print(f\n遗漏问题: {report[missing_count]}) for missing in report[missing]: print(f - {missing[function]}: {missing[issue]}) print(f 建议: {missing[action]})双层可观测性集成# Grafana dashboard: 网关Mesh双层指标 # 网关层指标入口视角 datasources: - name: Kong-Prometheus type: prometheus url: http://kong-prometheus:9090 # Mesh层指标内部视角 - name: Istio-Prometheus type: prometheus url: http://istio-prometheus:9090 # 双层面板配置 dashboard: panels: # 网关入口延迟客户端→网关→Mesh - title: 入口延迟网关视角 query: | histogram_quantile(0.99, sum(rate(kong_latency_bucket{serviceorder-service}[5m])) by (le)) # Mesh内部延迟网关→服务A→服务B - title: 内部延迟Mesh视角 query: | histogram_quantile(0.99, sum(rate(istio_request_duration_milliseconds_bucket{ destination_serviceorder-service}[5m])) by (le)) # 为什么需要双层面板网关延迟入口网络网关处理Mesh转发 # Mesh延迟服务间调用。两层数据叠加才能看到完整的请求生命周期边界分析与架构权衡网关和Mesh是否可以只用一个只用网关无Mesh限流、认证、路由全在网关。服务间调用没有sidecar代理——没有mTLS、没有内部可观测性、没有局部重试/熔断。适合小规模10服务架构。只用Mesh无网关Istio Ingress Gateway替代独立网关。功能够用但运维复杂——Istio的Ingress配置比Kong的声明式配置难维护。适合已有Istio且团队精通Istio的场景。两层并存功能最完整但成本最高两套系统运维。适合大规模20服务 安全要求严格外部认证内部mTLS的场景。认证的双层问题网关校验JWT后内部服务间调用是否需要再次认证不需要。网关校验通过后请求进入Mesh。Mesh内的mTLS保证调用来源可信只有Mesh内的sidecar才能发起mTLS连接。服务间调用不需要用户级JWT——服务身份由mTLS的证书保证。但某些场景需要传递用户身份服务B需要知道这是哪个用户的请求。解决方案网关校验JWT后提取user_id放入HTTP header传递到下游。Mesh内服务读取header获取用户身份不做JWT校验。限流的双层协同网关限流100次/分钟/IP。Mesh限流500次/秒/服务实例。两层限流是否冲突不冲突互补网关限流防外部滥用。单IP超100次→限流拒绝。正常用户不会超限。Mesh限流防内部风暴。服务A突发调用服务B 1000次/秒→Mesh限流保护服务B。外部请求不可能触发1000次/秒的单服务调用网关全局限流100次/分钟已拦截。两层限流的叠加效果外部请求先过网关限流粗粒度再过Mesh限流细粒度。内部请求只过Mesh限流网关不拦截内部调用。网关绕过的风险外部请求绕过网关直接打到Mesh的sidecar——网关的认证、限流、CORS全部失效。防御Istio的PeerAuthentication设置STRICT模式后只有Mesh内的sidecar能发起mTLS连接。外部客户端没有Mesh证书无法直接连接sidecar。但Ingress端口仍然暴露。必须确保Ingress只接受来自网关的流量——通过NetworkPolicy限制Ingress端口的来源IP为网关Pod的IP。apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: allow-only-gateway-to-ingress namespace: production spec: podSelector: matchLabels: istio: ingressgateway ingress: - from: - podSelector: matchLabels: app: kong # 只允许Kong网关Pod访问Ingress ports: - port: 8080 - port: 8443 # 为什么需要NetworkPolicySTRICT mTLS阻止非Mesh客户端连接sidecar # 但不阻止非Mesh客户端连接Ingress端口Ingress需要接受外部流量。 # NetworkPolicy在Pod网络层限制Ingress只接受网关流量gRPC网关的特殊处理外部客户端发HTTP/JSON请求。内部服务用gRPC通信。网关层做协议转换HTTP→gRPC。Kong的gRPC插件配置plugins: - name: grpc-gateway config: proto: /path/to/order-service.proto # protobuf定义文件路径 # 为什么需要proto文件HTTP→gRPC转换需要知道service/method映射 # proto文件定义了gRPC服务的接口结构Mesh内不需要协议转换——服务间直接gRPC通信。迁移路径从纯网关到网关Mesh已有网关架构逐步引入MeshPhase 1部署Istio sidecarPERMISSIVE模式允许明文密文。Mesh只做可观测性——收集内部调用指标。Phase 2Mesh增加重试/熔断配置。网关保留认证/限流/CORS。两层功能开始分工。Phase 3Mesh切换STRICT模式强制mTLS。删除网关层的内部流量路由Mesh接管。网关只负责入口治理。Phase 4NetworkPolicy确保Ingress只接受网关流量。双层分工彻底确立。为什么渐进而非一次性切换一次性切换风险太大——Mesh配置错误可能导致全量内部调用失败。渐进式迁移每步可控、可回滚。总结Service Mesh和API网关不是替代关系是互补关系。分工原则网关管南北入口Mesh管东西内部。网关治理外部→内部的流量Mesh治理内部→内部的流量。认证在网关mTLS在Mesh。JWT校验是入口职责服务身份由mTLS保证。不重复校验。全局限流在网关服务级限流在Mesh。网关防外部滥用Mesh防内部风暴。重试/熔断在Mesh。局部恢复策略由服务级配置驱动网关不该替服务做重试决策。CORS在网关。浏览器安全策略只在外部HTTP入口需要。可观测性双层互补。网关采集入口指标Mesh采集内部指标。叠加数据看完整请求生命周期。防绕过STRICT mTLS NetworkPolicy确保外部请求只能通过网关进入Mesh。功能重叠是配置错误不是架构选择。用OverlapChecker工具定期审计发现双写立即清理发现遗漏立即补齐。两层各司其职不重叠不遗漏才是生产级的流量治理架构。资料说明本文中的协议、版本、性能、成本和行业趋势应以可核验的一手资料为准。未标注统计口径的比例、时间表和预测仅作工程讨论不应视为行业事实。可参考 0731 资料来源索引并在发布前将具体来源贴到对应断言之后。