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网关是两个不同层次的流量治理,各有明确的职责边界:

关键区别:

  1. 流量方向。网关治理南北向流量(外部→内部)。Mesh治理东西向流量(内部→内部)。网关是入口大门,Mesh是内部走廊。

  2. 治理粒度。网关是全局级——一条限流规则影响所有经过网关的请求。Mesh是服务级——一条限流规则只影响特定服务间的调用。

  3. 故障恢复。网关处理入口故障(上游服务整体不可用→返回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 # 为什么在网关校验而非Mesh:JWT是外部用户凭证, # Mesh内的服务间调用不需要用户级JWT认证 # 2. 全局限流——按IP+API维度 - 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_tolerance:Redis宕机时限流失效, # 100%严格会导致Redis故障时所有请求被限死 limit_by: ip # 按IP限流 # 为什么不按consumer:consumer维度需要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在网关而非Mesh:CORS是浏览器安全策略, # 只有外部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_consumer:per_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%实例 # 为什么maxEjectionPercent=50而非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而非PERMISSIVE:PERMISSIVE允许明文+密文混合, # 在全部服务接入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{service="order-service"}[5m])) by (le)) # Mesh内部延迟(网关→服务A→服务B) - title: 内部延迟(Mesh视角) query: | histogram_quantile(0.99, sum(rate(istio_request_duration_milliseconds_bucket{ destination_service="order-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 # 为什么需要NetworkPolicy:STRICT 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

已有网关架构,逐步引入Mesh:

  1. Phase 1:部署Istio sidecar,PERMISSIVE模式(允许明文+密文)。Mesh只做可观测性——收集内部调用指标。
  2. Phase 2:Mesh增加重试/熔断配置。网关保留认证/限流/CORS。两层功能开始分工。
  3. Phase 3:Mesh切换STRICT模式(强制mTLS)。删除网关层的内部流量路由(Mesh接管)。网关只负责入口治理。
  4. Phase 4:NetworkPolicy确保Ingress只接受网关流量。双层分工彻底确立。

为什么渐进而非一次性切换:一次性切换风险太大——Mesh配置错误可能导致全量内部调用失败。渐进式迁移每步可控、可回滚。

总结

Service Mesh和API网关不是替代关系,是互补关系。分工原则:

  1. 网关管南北(入口),Mesh管东西(内部)。网关治理外部→内部的流量,Mesh治理内部→内部的流量。
  2. 认证在网关,mTLS在Mesh。JWT校验是入口职责,服务身份由mTLS保证。不重复校验。
  3. 全局限流在网关,服务级限流在Mesh。网关防外部滥用,Mesh防内部风暴。
  4. 重试/熔断在Mesh。局部恢复策略由服务级配置驱动,网关不该替服务做重试决策。
  5. CORS在网关。浏览器安全策略只在外部HTTP入口需要。
  6. 可观测性双层互补。网关采集入口指标,Mesh采集内部指标。叠加数据看完整请求生命周期。
  7. 防绕过:STRICT mTLS + NetworkPolicy确保外部请求只能通过网关进入Mesh。

功能重叠是配置错误,不是架构选择。用OverlapChecker工具定期审计,发现双写立即清理,发现遗漏立即补齐。两层各司其职,不重叠不遗漏,才是生产级的流量治理架构。

资料说明

本文中的协议、版本、性能、成本和行业趋势应以可核验的一手资料为准。未标注统计口径的比例、时间表和预测仅作工程讨论,不应视为行业事实。可参考 0731 资料来源索引,并在发布前将具体来源贴到对应断言之后。