为什么微服务一定要有网关?
为什么微服务一定要有网关?
微服务拆得越细,有一个问题就越无法回避:谁来管门?
系统从单体变成几十个服务之后,每个服务都有自己的端口,自己的认证逻辑,自己的限流开关,自己的日志格式。客户端要查一个订单详情,可能要同时打会员服务、订单服务、商品服务三个接口——而这三个请求全部直接暴露在公网上。
这不是架构,这是散兵游勇。
网关就是用来解决这个问题的。它在所有服务前面立一道统一入口,把认证、路由、限流、日志这些和业务无关但又每个服务都要做的事情,统一收到一处处理。
这篇文章结合生产实践代码,把"为什么需要网关"这件事讲清楚——不是背概念,是真实遇到过的问题。
一、没有网关会怎样?
问题一:认证逻辑每个服务写一遍
用户登录之后拿到一个 JWT 令牌。后端有 20 个服务,每个服务都要做解析、验签、查黑名单这三件事。就是说同一段代码要写 20 遍,过一段时间必然各写各的。将来有一天解析逻辑换了、黑名单逻辑改了,必须把 20 个服务全部改一遍、全部重新部署。漏改一处就是安全漏洞。
有人会想:把认证逻辑抽到一个公共包里,大家共同依赖。道理是对的,但有两个副作用:一是所有服务的 jar 包都会变大(Docker 镜像加载变慢);二是只要公共包有任何变动,所有依赖它的服务全得重新编译部署。就为了改一个验证逻辑,把上下游全拆了一遍,成本太高。
问题二:限流、日志、跨域到处散落
这些是典型的横切关注点(Cross-Cutting Concerns),跟业务逻辑本质上没关系,但每个服务又都要处理。结果就是:每个团队各写各的,标准不统一,出了问题排查困难。
A 团队的日志是 JSON,B 团队的日志是纯文本,C 团队的限流用的 Guava RateLimiter,D 团队用的自己写的滑动窗口…… 线上出了问题想动全局限流?找不到统一的地方改。想看所有请求的访问日志?要把几十个服务的日志全归拢一遍。
问题三:运维没有全局视角
想做灰度发布?想做 AB 测试?想全局拦截某类请求?没有网关,这些操作都需要修改每个服务。有网关,一个地方配置就能全局生效。
问题四:Nginx 能不能顶替
很多人第一反应是:我前面挂一个 Nginx 不就行了?
Nginx 确实占据了路由这块,性能强、稳定、久经考验。但它做不了 JWT 解析、做不了熔断降级、做不了和注册中心的动态路由。Nginx 的配置文件改了还需要 reload,路由变动频繁就扑不过来。
最关键的一点:Nginx 跟不上注册中心。微服务的服务实例是动态变化的,扩容或缩容时 IP 与端口全在变。Nginx 需要手动维护上游节点列表,而 Spring Cloud Gateway 这类微服务网关可以直接从 Nacos/Eureka 拉取服务列表、自动负载均衡,新小组成员上线完全无感。
一句话结论:Nginx 负责最外层的流量接入,微服务网关负责内部的流量治理,两者分工而不是替代关系。
二、网关是什么?
网关的本质其实只有两个能力:
网关 = 路由转发 + 过滤器链路由转发:接收所有外部请求,根据请求路径、请求头、参数等规则将其转发到对应的微服务上。
过滤器链:请求在进入和离开网关的过程中,经过一系列过滤器(Filter)的加工处理。认证、限流、日志、熔断、跨域——这些都是各种过滤器在负责。
网关自己就是一个微服务,它注册到注册中心(Nacos/Eureka),可以动态感知到后端所有服务实例的变动。这是它和 Nginx 最大的不同:它是体系内的成员,不是掉在体系外的连接层。
三、网关的核心职责一:统一认证
认证是网关价値最高的能力。把 JWT 解析和校验收敛到网关层,下游服务一行相关代码都不用写。
下面是一个生产环境中实际跑着的 JWT 认证过滤器核心流程:
// 从请求头取出令牌
String token = request.getHeaders().getFirst("Authorization");
// 检查令牌是否已被登出(Redis 黑名单)
Mono<Boolean> logoutInRedis = reactiveStringRedisTemplate
.opsForValue()
.get(LOGOUT_REDIS_KEY_PREFIX + pureToken)
.map(s -> true)
.switchIfEmpty(Mono.just(false));
// 解析 JWT,提取用户信息
Jws<Claims> claimsJws = Jwts.parser()
.setSigningKey(secretBase64)
.parseClaimsJws(pureToken);
Claims body = claimsJws.getBody();
String subject = body.getSubject();
String channel = body.get("channel", String.class);校验通过后,网关把用户信息塞进请求头,转发给下游服务:
// 先清理外部传入的自定义头(防伪造)
httpHeaders.remove("X-Subject");
httpHeaders.remove("X-Channel");
httpHeaders.remove("X-Primary-Account-Id");
// 再注入网关解析到的用户信息
requestBuilder.header("X-Subject", subject);
requestBuilder.header("X-Channel", channel);
requestBuilder.header("X-Primary-Account-Id", primaryAccountId);这个设计有几个好处。秘钥只有网关知道,不用散发给每个服务;登出逻辑集中在网关的 Redis 黑名单里,改一处全局生效;下游服务代码更干净,直接从请求头取 X-Subject 就知道当前是谁。
安全细节:网关必须先将外部传入的
X-Subject等头字段清除,再注入自己解析到的结果,否则恶意请求可以伪造用户身份。
四、网关的核心职责二:路由与负载均衡
路由是网关最基础的能力,也是最直观的功能。客户端只需要知道网关的地址,不需要关心背后有多少服务、部署在哪里。
Spring Cloud Gateway 的路由配置平均长这样:
spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb://user-service # lb:// 代表利用注册中心做负载均衡
predicates:
- Path=/api/user-service/**
- id: order-service
uri: lb://order-service
predicates:
- Path=/api/order-service/**
- id: pay-service
uri: lb://pay-service
predicates:
- Path=/api/pay-service/**lb:// 前缀代表从注册中心拉取服务实例做负载均衡。所有以 /api/user-service/ 开头的请求自动转发到用户服务,以 /api/order-service/ 开头的转到订单服务。后端服务扩缩容、IP 变更,客户端完全无感。
除了基础路由,网关还能做 API 聚合:客户端发一个请求到网关,网关内部并行打多个微服务之后将结果合并返回。这尤其适用于移动端项目:互联网高延迟,少发一次请求总比发两次好得多。
五、网关的核心职责三:限流与熔断
限流和熔断是网关在高并发场景下不可缺少的能力。
5.1 高精度限流:Redis + Lua 令牌桶
网关的限流不是简单地给整个系统设一个 QPS 上限,而是可以做到 API 级别的精准控制。比如下单接口限 500 QPS,查询接口限 2000 QPS,完全按业务需要来配。
实现原理是 Redis + Lua 脚本的令牌桶算法。Lua 脚本的核心逻辑:
-- 获取桶里当前的令牌数
local last_tokens = tonumber(redis.call("get", tokens_key))
if last_tokens == nil then
last_tokens = capacity
end
-- 计算从上次到现在应该补充多少令牌
local delta = math.max(0, now - last_refreshed)
local filled_tokens = math.min(capacity, last_tokens + (delta * rate))
-- 判断令牌是否够用
local allowed = filled_tokens >= requestedrate 是每秒补充的令牌数,capacity 是桶的最大容量。每个请求来了先算一下桶里还有多少令牌,够就放行并扣减,不够就拒绝。整个计算在 Redis 里原子执行,不存在并发问题。
为什么用 Lua 而不是 Java 代码做判断?Redis 执行 Lua 脚本是原子性的,高并发场景下不会出现竞态条件。比应用层用分布式锁做限流,性能好很多。
限流配置存在 Redis 里,支持动态调整,不需要重启网关。突发流量来了,运维可以实时调整某个接口的限流阈值。
5.2 熔断降级:防止故障扩散
下游服务如果出了问题(响应超时、错误率飙升),网关需要有能力快速切断请求,避免故障扩散拖嫦整个系统。
一个典型的 URL 级别熔断配置(基于 Hystrix):
// 按 URL 维度初始化断路器
Setter setter = Setter
.withGroupKey(HystrixCommandGroupKey.Factory.asKey(getClass().getSimpleName()))
.andCommandKey(HystrixCommandKey.Factory.asKey(uriAsCommandKey));
// 断路器打开时发布事件,联动限流策略
if (command.isCircuitBreakerOpen()) {
circuitBreakerOpenEventBus.post(command);
}每个 URL 对应一个独立的断路器。某个接口出问题,只会熔断这一个接口的请求,不影响其他接口。这比服务级别的粗粒度熔断精细得多。
目前深受欢迎的方案是 Sentinel(Spring Cloud Alibaba),它提供的控制台可以可视化配置限流熔断规则,配置实时生效,不用重新部署代码。
六、网关的核心职责四:日志与链路追踪
网关是所有请求的必经之路,在这里收集访问日志,天然覆盖所有接口。不需要每个服务自己实现。
异步日志发送,不阻塞主线程:
// 请求日志 -> MQ 异步发送
accessLogService.logRequestBody(exchange, bodyStr);
// 响应日志 -> MQ 异步发送
accessLogService.logResponseBody(exchange, responseBody);请求日志和响应日志分开发送,走不同的队列,不互相阻塞。
链路追踪 TraceId,网关在响应头里注入 TraceId。线上出了问题,前端把 TraceId 贴出来,后端就能在日志系统里追溯到整个请求链路。这就是集成 Zipkin/Sleuth 的好处:一条 TraceId 贯穿所有服务。
日志范围可配置,支持按路径匹配和排除。文件上传接口这类应该排除在外,避免日志量爆炸。
七、主流网关横向对比
微服务领域的网关选择不少,最常见的几种对比如下:
| Nginx | ||
| Spring Cloud Gateway | ||
| Kong | ||
| Apache APISIX | ||
| Envoy |
对于 Java 技术栈,首选 Spring Cloud Gateway,跟整个 Spring Cloud 体系无缝整合。对于高性能要求或多语言技术栈,可以考虑 APISIX。如果现有系统已经运行 Kong,也没必要强行迁移。
八、生产实践
很多小伙伴运行的系统只有一个网关,一了百了。但实际大流量系统往往会将 C 端和 B 端进行拆分。
以一个实际用户超过 6000 万的系统为例,同时跑着 C 端网关(小程序、App)和 B 端网关(商家后台、运营系统)。拆就是因为两端在以下几个维度差异太大,担在一个网关里会彼此干扰。
认证策略不同。C 端直接:JWT 解析 + SSO 单点登出状态检查,所有请求必带令牌。B 端则在 JWT 基础上多了两种模式:其一是游客模式(部分接口允许未登录访问),网关生成一个虚拟游客令牌;其二是 RSA 签名验证(对接 POS 机、第三方平台),这是 C 端不需要的能力,塑到一起只会增加复杂度和攻击面。
// B 端网关的游客令牌
private static final TokenDetail FAKE_GUEST_TOKEN = new TokenDetail()
.setSub("-1")
.setChannel("weixin")
.setIsGuest(true)
.setSource("mini_program");
// B 端的 RSA 签名验证
String clientId = request.getClientId();
String payload = request.getPayload();
String sign = request.getSign();
// 校验时间戳防重放
if (Math.abs(nowTimestamp - timestamp) > signApiV2Properties.getTtl()) {
throw new SignApiV2AccessNotAllowException("request timestamp too late or early");
}
SignUtils.checkSign(request, "sign", clientProperties.getPublicKey());限流策略不同。C 端标准的 API 精准限流(令牌桶)就够了,流量就是大头。B 端在此基础上加了智能限流:一旦下游服务处现熔断,网关自动推送事件,联动调整限流策略,给系统喘息的时间。
路由管理方式不同。C 端路由配置走 Apollo 配置中心,改路由在 Apollo 上操作。B 端路由存在 Redis 里,支持通过 API 动态修改,还做了本地快照——Redis 挂了,网关自动降级到本地快照,不影响路由转发。这是因为 B 端的路由变更频率更高,需要更灵活的管理方式。
九、常见问题
网关应该做哪些事?认证、限流、路由、日志、跨域、熔断、请求改写、响应标准化。这些都是和业务无关的横切关注点,天然适合放在网关层。
网关不应该做哪些事?业务逻辑。 网关里不要做数据聚合、不要调用数据库、不要做复杂的业务判断。网关是通道,不是业务处理节点。一旦网关里塞了业务代码,它的稳定性就和业务耦合了,出问题影响面是全局的。
什么时候需要拆分多个网关? 当不同的客户端群体有明显不同的认证方式、流量特征、安全要求时。比如 C 端面向消费者用 JWT,B 端面向商家用签名认证,把它们塞进一个网关会导致过滤器链过于复杂,互相干扰。流量隔离也是一个重要原因,C 端的突发流量不应该影响 B 端的稳定性。
网关会不会成为单点故障? 会的,这是引入网关之后不得不面对的问题。常见做法是在网关前面再挂一层 Nginx 或 LVS,网关服务多实例部署,通过注册中心健康检测。网关最好尽量载量化,不要塑入太多业务逻辑,做好压力测试估算容量也是关键。
十、总结
网关在微服务架构里扮演的角色,有点像公司的安保加前台。安保负责把门(认证验身),前台负责流量分发(路由),而后面的各个部门只管自己的事,不用每个人都跟陌生人验身、都重复登记。
没有网关的微服务系统,标准不统一、维护成本高、安全风险大,最终都会拖延到不得不自己手建一套。与其等到火烧眉毛呈六神无主的时候才来折腾,不如一开始就把网关建起来。
不过记住,网关就是网关,它不是业务系统。掌握好这个原则,才能建一个高可用、易维护的微服务架构。
点下方的“❤”支持我们,非常感谢!