自研轻量级API网关hermes-gate:设计、实现与部署实战
1. 项目概述一个轻量级API网关的诞生最近在重构一个老项目的微服务架构发现服务间的调用链路越来越复杂认证、限流、日志这些横切关注点散落在各个服务里每次改动都像在拆弹。当时就在想能不能有一个足够轻量、能快速集成、并且配置起来不费劲的API网关来统一处理这些公共逻辑市面上成熟的网关产品很多功能强大但随之而来的就是学习成本高、资源占用大对于中小型项目或者快速验证的场景总感觉有点“杀鸡用牛刀”。于是我动手搭建了hermes-gate。这个名字来源于希腊神话中的信使赫尔墨斯寓意着快速、可靠的消息传递。它的核心定位就是一个轻量级、高性能、易于扩展的API网关。它不是要替代Spring Cloud Gateway或者Kong这类重型选手而是瞄准了那些对网关有基础需求但又希望部署简单、代码可控的场景。比如内部工具链的接口聚合、给前端提供统一的后端入口、或者作为微服务架构演进的第一个试验田。如果你正在为服务治理的公共逻辑分散而头疼或者需要一个能快速上手的网关来统一管理API那么hermes-gate的设计思路和实现细节或许能给你带来一些直接的参考价值。接下来我会从设计思路、核心实现、到如何一步步把它跑起来以及过程中踩过的坑毫无保留地分享给你。2. 整体架构与核心设计思路2.1 为什么选择自研轻量级网关在决定动手之前我仔细评估过几个主流方案。Spring Cloud Gateway功能齐全但与Spring生态绑定深Kong基于OpenResty性能强悍但需要维护数据库和集群Nginx配置灵活但用原生配置写复杂路由和过滤逻辑维护成本不低。对于我的场景——一个大约十几个服务的内部系统——这些方案都显得有些“重”。我需要的是快速启动最好能一个Jar包几条配置就能跑起来。易于理解和调试代码结构清晰出了问题能快速定位而不是在黑盒里摸索。核心功能够用路由、负载均衡、认证、限流、日志这几样是刚需。易于扩展当有新的公共逻辑需要加入时我能以最小的成本接入。基于这些考虑我选择了以Netty作为网络通信框架来构建。Netty的异步高性能特性足以支撑网关这个IO密集型的应用而且其Handler链式的处理模型与网关的“请求处理管道”概念天然契合一个过滤器就是一个ChannelHandler。2.2 核心架构设计hermes-gate采用了经典的分层和插件化设计整体架构可以概括为“一个核心两条管道多种插件”。一个核心指的是路由核心Route Core。它负责在启动时加载所有路由规则目前支持从YAML配置文件读取并在运行时根据请求的路径、方法等信息快速匹配到目标后端服务Upstream。路由规则支持权重、超时、重试等基本配置。两条管道是请求处理管道Request Pipeline和响应处理管道Response Pipeline。这是网关逻辑执行的地方借鉴了责任链模式。请求管道在将请求转发给后端之前会依次执行一系列“前置过滤器”Pre Filter比如身份认证、参数校验、限流检查、请求头改写等。响应管道在收到后端响应后会依次执行“后置过滤器”Post Filter比如统一响应格式封装、添加特定响应头、记录访问日志、错误信息转换等。多种插件则是具体功能的实现。每个过滤器、负载均衡策略都是一个独立的插件。例如AuthFilter、RateLimitFilter、LoggingFilter。它们实现统一的接口通过配置动态加载到处理管道中。这种设计使得功能扩展变得非常容易你需要一个新的全局参数校验写一个ValidationFilter插件配置进去就行了。负载均衡方面第一版实现了最常用的轮询Round Robin和随机Random算法后续可以很方便地加入一致性哈希等更复杂的策略。设计心得在网关设计中清晰的生命周期划分至关重要。我明确将请求处理分为PRE_ROUTE、ROUTE、POST_ROUTE、COMPLETE/ERROR几个阶段。每个插件都需要声明自己作用于哪个阶段这避免了过滤器执行顺序的混乱也让日志跟踪和问题排查更加清晰。3. 核心模块深度解析3.1 路由配置与匹配引擎路由是网关的交通规则。在hermes-gate中路由配置采用YAML格式因为它比Properties更易于表达层次结构比JSON更简洁无需括号。一个典型的路由配置如下routes: - id: user_service_route uri: lb://user-service predicates: - Path/api/v1/users/** filters: - name: Auth args: type: JWT header: Authorization - name: RateLimit args: key: ${remoteAddr} replenishRate: 10 burstCapacity: 20 metadata: timeout: 5000 retries: 2id: 路由唯一标识用于日志和监控。uri: 目标服务地址。lb://前缀表示这是一个需要负载均衡的服务名网关会从注册中心或静态列表里查找对应的实例。也支持直接写http://host:port。predicates断言: 决定当前请求是否匹配该路由的条件集合。目前核心实现了Path路径匹配这是最常用的。设计上预留了Method、Header、Query等断言接口后续可以快速扩展。filters过滤器: 作用于该路由的过滤器链。每个过滤器需要指定name和可选的args参数。网关启动时会根据name从插件工厂中实例化对应的过滤器对象。metadata: 路由元数据这里可以配置一些通用参数比如超时时间毫秒、重试次数等。匹配引擎的工作流程网关启动时RouteLocator组件会解析YAML配置将每一段路由规则转换成内存中的Route对象并按照配置顺序或优先级存入一个列表。当收到一个HTTP请求时RouteMatcher会遍历这个路由列表。对每个Route依次执行其所有的Predicate。只有当所有断言都返回true时才算匹配成功。匹配成功后立即返回该Route对象不再继续遍历。因此路由的顺序很重要更具体的路径应该放在前面。避坑指南路由匹配的性能是关键。在路由数量较多时线性遍历可能成为瓶颈。在实际项目中如果路由超过上百条可以考虑使用Path前缀树Trie或哈希表进行初步筛选将匹配复杂度从O(N)降低到接近O(1)。第一版为了简洁没做但这是性能优化的明确方向。3.2 插件化过滤器体系过滤器是网关的“肌肉”负责执行各种业务逻辑。hermes-gate的过滤器体系设计遵循了开闭原则。核心接口GatewayFilterpublic interface GatewayFilter { // 过滤器名称用于配置 String name(); // 过滤器执行的顺序值越小越先执行 int order(); // 核心处理方法 MonoVoid filter(ServerWebExchange exchange, GatewayFilterChain chain); }过滤器链GatewayFilterChain 它维护了一个有序的过滤器列表。处理请求时会依次调用每个过滤器的filter方法。过滤器可以选择中断链例如认证失败直接返回401或者调用chain.filter(exchange)将请求传递给下一个过滤器。如何实现一个自定义过滤器假设我们要添加一个简单的请求耗时统计过滤器新建类RequestDurationFilter实现GatewayFilter接口。在filter方法中在调用chain.filter之前记录开始时间之后计算耗时并打印日志。通过Component注解将其声明为Spring Bean如果使用Spring容器管理或者在自定义的FilterProvider中手动注册。在路由配置的filters部分添加- name: RequestDuration即可启用。内置的几个关键过滤器AuthFilter (JWT)从指定Header如Authorization中提取JWT令牌使用公钥或密钥进行验证并将解析出的用户信息如userId放入请求上下文供下游服务使用。RateLimitFilter (基于令牌桶)对请求来源IP、用户ID进行限流。配置参数replenishRate每秒补充的令牌数和burstCapacity桶容量。使用Guava的RateLimiter或Redis分布式限流实现。LoggingFilter在POST_ROUTE阶段记录详细的访问日志包括请求/响应信息、耗时、后端服务地址等并可以输出到控制台、文件或ELK。实操心得过滤器的order需要谨慎设计。通常全局性的、应该最早执行的过滤器如限流、黑白名单order值较小与业务关联紧密的如添加业务头order值较大。一个建议的顺序是全局限流 - 认证 - 路径改写 - 业务头处理 - 日志。在hermes-gate中我通过一个FilterOrder常量类来明确定义这些顺序避免配置时的混乱。3.3 异步转发与响应处理网关的核心职能是转发请求。hermes-gate使用异步非阻塞模型来保证高并发下的性能。请求转发流程构建代理请求根据匹配到的路由和原始请求使用Netty的HttpClient或Spring的WebClient构建一个新的请求对象。这个过程会处理URI重写根据路由规则将请求路径/api/v1/users/123重写为后端服务路径/users/123。请求头处理移除或添加特定的请求头。例如通常会移除Host头防止干扰后端服务将经过JWT解析得到的用户ID以X-User-Id的形式传递给后端。负载均衡选择如果URI是lb://serviceName则调用LoadBalancer从可用实例列表中选择一个如轮询替换为真实的http://ip:port。异步执行调用使用异步HTTP客户端发送代理请求。这里设置连接超时、读取超时等参数取自路由配置的metadata.timeout。处理响应收到后端响应后进入响应处理管道。执行POST_ROUTE阶段的过滤器如日志记录。处理响应头可能需要移除或修改一些后端服务返回的敏感头信息。错误处理如果后端返回4xx/5xx错误或者发生网络超时、连接异常网关需要决定是直接返回错误响应还是进行重试根据metadata.retries配置。重试逻辑需要小心对于非幂等的POST、PUT请求通常不重试。写回响应将最终处理后的响应通过Netty的ChannelHandlerContext写回给客户端。关键代码片段使用WebClientpublic MonoClientResponse forwardRequest(ServerWebExchange exchange, Route route, String realUrl) { // 1. 获取原始请求 ServerHttpRequest request exchange.getRequest(); // 2. 构建WebClient请求 return webClient .method(request.getMethod()) .uri(realUrl) .headers(headers - copyHeaders(request, headers, route)) // 复制并处理请求头 .body(BodyInserters.fromDataBuffers(request.getBody())) .exchangeToMono(clientResponse - { // 3. 将后端响应放入交换器供后续过滤器处理 exchange.getAttributes().put(CLIENT_RESPONSE_ATTR, clientResponse); return Mono.just(clientResponse); }) .timeout(Duration.ofMillis(route.getMetadata().getTimeout())) // 超时控制 .onErrorResume(TimeoutException.class, e - handleTimeout(exchange, route)) // 超时处理 .retryWhen(Retry.backoff(route.getMetadata().getRetries(), Duration.ofMillis(100)) // 重试逻辑 .filter(throwable - isRetryable(throwable, request.getMethod()))); // 仅对可重试的异常和方法重试 }性能与资源陷阱异步编程虽然性能好但容易导致内存泄漏和资源未释放。要特别注意DataBuffer请求/响应体数据的释放。在WebFlux或Netty中如果没有被消费必须手动调用DataBufferUtils.release(buffer)。我曾在压测时发现内存缓慢增长后来就是通过检查所有可能的DataBuffer泄露点解决的。建议使用NettyLeakDetector或类似的工具进行排查。4. 从零开始部署与配置实战4.1 环境准备与项目构建系统与环境要求JDK 11 或以上版本推荐JDK 17以获得更好的容器支持。Maven 3.6 或 Gradle。可选Docker用于容器化部署。获取与构建代码# 1. 克隆代码仓库 git clone https://github.com/LehaoLin/hermes-gate.git cd hermes-gate # 2. 使用Maven打包 mvn clean package -DskipTests # 打包成功后会在 target 目录下生成可执行的JAR文件例如 hermes-gate-1.0.0-SNAPSHOT.jar4.2 核心配置文件详解配置文件application.yml或application.properties是网关的大脑。下面拆解关键部分server: port: 8080 # 网关自身服务的端口 hermes: gateway: # 路由配置可以指向一个独立的YAML文件 routes-config: classpath:routes/routes.yml # 全局默认过滤器对所有路由生效 global-filters: - name: GlobalLogging - name: GlobalCors # 全局跨域配置 args: allowed-origins: * allowed-methods: GET,POST,PUT,DELETE # HTTP客户端配置用于转发请求 http-client: connect-timeout: 3000 # 连接超时(ms) response-timeout: 10000 # 响应超时(ms) max-connections: 1000 # 连接池最大连接数 pool: max-idle-time: 60000 # 连接最大空闲时间(ms) # 负载均衡配置 loadbalancer: default-rule: round_robin # 默认规则 # 服务实例列表静态配置也可集成Nacos/Eureka service-instances: user-service: - uri: http://192.168.1.101:8081 weight: 80 - uri: http://192.168.1.102:8081 weight: 20 order-service: - uri: http://192.168.1.103:8082routes.yml路由规则文件 这个文件建议放在src/main/resources/routes/目录下与主配置分离更清晰。内容如第3.1节所示。4.3 启动、验证与基本测试启动网关# 在项目根目录下 java -jar target/hermes-gate-1.0.0-SNAPSHOT.jar # 或者指定外部配置文件生产环境推荐 java -jar -Dspring.config.locationfile:/opt/hermes/config/application.yml target/hermes-gate-1.0.0-SNAPSHOT.jar验证网关是否工作健康检查访问http://localhost:8080/actuator/health如果集成了Spring Boot Actuator应返回{status:UP}。路由测试假设你配置了一个指向http://httpbin.org的测试路由。# routes.yml - id: test_route uri: http://httpbin.org predicates: - Path/test/**启动后访问http://localhost:8080/test/get网关应该将请求转发到http://httpbin.org/get并返回结果。集成测试示例 使用curl或Postman进行测试。测试路由与负载均衡连续访问网关端点观察请求是否被轮询转发到不同的后端实例查看后端服务日志。测试JWT认证# 1. 不带Token应返回401 curl -v http://localhost:8080/api/v1/users/me # 2. 携带有效Token curl -v -H Authorization: Bearer your-jwt-token http://localhost:8080/api/v1/users/me测试限流使用工具如ab,wrk快速并发访问一个配置了限流的路由观察超出限制的请求是否收到429 Too Many Requests响应。4.4 生产环境部署考量高可用至少部署两个网关实例前面通过Nginx或云负载均衡器如AWS ALB 腾讯云CLB做流量分发形成集群。配置中心将application.yml和routes.yml放到配置中心如Nacos Config Apollo实现动态刷新无需重启网关即可修改路由规则。服务发现将静态的service-instances配置替换为集成Nacos、Eureka或Consul实现后端服务的自动发现与健康检查。监控与告警指标集成Micrometer暴露Prometheus格式的指标如请求量、耗时、错误率、各路由流量。日志访问日志输出为JSON格式通过Filebeat收集到ELK栈便于查询和分析。链路追踪集成Sleuth/Zipkin为每个请求生成Trace ID贯穿网关和后端服务便于排查分布式问题。安全管理端点保护确保/actuator等管理端点不被公网访问或添加IP白名单、基础认证。防止内部服务暴露确保网关是访问内部服务的唯一入口后端服务应部署在内网。5. 常见问题排查与性能调优5.1 启动与配置类问题问题1启动报错Failed to bind properties under hermes.gateway原因配置文件中的属性与ConfigurationProperties注解的类字段类型不匹配或拼写错误。排查检查application.yml的缩进是否正确YAML对缩进敏感。检查hermes.gateway.routes-config指向的文件路径是否存在。查看完整的启动日志Spring Boot通常会给出详细的绑定失败信息。问题2路由配置不生效请求返回404原因路由匹配失败。排查步骤检查路由加载日志启动时网关会打印加载的路由信息。确认你的路由是否被正确加载。检查断言Predicate确认请求的路径、方法是否完全匹配路由中predicates的配置。注意Path匹配的规则/api/**可以匹配/api和/api/xxx但/api/*只能匹配一级路径。启用调试日志在application.yml中设置logging.level.com.lehaolin.gateway: DEBUG可以看到详细的请求匹配过程。5.2 运行时问题问题3网关转发请求超时可能原因后端服务响应慢。网关配置的超时时间http-client.response-timeout过短。网络问题。排查首先直接调用后端服务确认其响应时间。适当增加response-timeout值。检查网关和后端服务之间的网络延迟和带宽。查看网关日志确认超时是发生在连接阶段还是读取响应阶段。问题4内存使用率持续升高潜在内存泄漏可能原因未正确释放Netty的ByteBuf或Spring WebFlux的DataBuffer。过滤器或业务代码中创建了大量对象且未及时回收。HTTP客户端连接池配置不当。排查与调优使用jmap -histo:live pid或VisualVM监控对象创建情况查看是否有特定类实例数异常增长。添加JVM参数-Dio.netty.leakDetectionLevelPARANOID进行Netty内存泄漏检测对性能有影响仅用于调试。检查所有自定义过滤器中是否确保了请求体和响应体数据流的正确消费或释放。调整HTTP客户端连接池参数避免连接数无限增长。问题5高并发下响应变慢或出错性能调优点线程池Netty默认的事件循环线程数通常是CPU核心数*2。如果网关有大量阻塞操作如同步的HTTP调用、复杂的数据库查询需要将这些操作移到单独的、有界的业务线程池中执行避免阻塞IO线程。HTTP客户端调整http-client.max-connections和pool相关参数。连接数不是越大越好需要根据后端服务的承受能力和网络状况调整。可以先用一个适中的值如500-1000进行压测。JVM参数根据机器内存设置合理的堆大小-Xms,-Xmx新生代与老年代比例。对于网关这种生命周期短的对象多的应用可以适当调大新生代-XX:NewRatio。操作系统限制检查Linux服务器的文件描述符限制ulimit -n高并发连接下需要调大这个值如65535。5.3 功能相关问题问题6JWT认证失败但Token看起来正确排查检查Token是否已过期。检查网关配置的JWT签名密钥或公钥与生成Token的密钥是否一致。检查Token提取的Header名称是否正确默认是Authorization。检查Token格式是否包含了Bearer前缀。过滤器逻辑通常是去除Bearer后再解析。问题7限流不准确单机 vs 分布式现象部署了多个网关实例每个实例单独限流导致总体限流阈值变为实例数 * 阈值。解决方案需要实现分布式限流。方案一推荐将RateLimitFilter的计数器令牌桶存储在Redis中所有网关实例共享同一个Redis。使用Redis的INCR和EXPIRE命令或Lua脚本来实现原子操作。方案二使用一个中心化的限流服务所有网关实例将限流请求发往该服务决策。这会增加网络开销和复杂度。注意分布式限流需要处理好时钟同步、网络延迟带来的精度问题通常允许一定的误差。问题8需要添加新的断言或过滤器扩展步骤新增断言实现Predicate接口在test方法中编写匹配逻辑。在RouteDefinitionLocator中增加对新断言名称如Header的解析支持。新增过滤器实现GatewayFilter接口。如果是全局过滤器实现GlobalFilter接口并标注Component。如果是路由级过滤器需要在FilterProvider中注册其名称与类的对应关系。更新配置解析确保YAML配置中的name能够正确映射到你新写的类。测试编写单元测试和集成测试验证新功能。开发hermes-gate的过程是一个不断权衡取舍的过程。在轻量与功能、灵活与复杂、性能与可维护性之间寻找平衡点。它现在可能还不完美但作为一个从实际痛点出发、亲手构建的工具你对它的每一个细节都了如指掌这种掌控感是使用现成组件无法比拟的。当你在深夜收到告警能迅速定位是某个过滤器的逻辑问题还是路由配置写错了这种解决问题的效率才是自研组件最大的价值。