Spring Cloud Gateway 作为云服务的 API 网关

微服务示例项目地址:https://github.com/14032/cloud

为什么微服务需要网关?

  1. 提供单个统一的 API 入口,客户端只需要与网关打交道,无需关心微服务内部组成和数量
  2. 将外部公共 API 与内部微服务 API 分开,区分聚合服务和原子服务,原子服务一般不对外暴露
  3. 网关统一认证鉴权,无需每个微服务都去实现一遍重复的功能,节省工作量
  4. 限流,防止大量的请求使服务器过载,导致服务不可用,提高服务的可用性与稳定性
  5. 熔断降级,发生特定异常的时候,对接口降级,由网关直接返回,防止引起整个系统的雪崩

云服务网关的实现有很多,下面是我会分享的三种,这章先介绍下 Spring Cloud Gateway。

  • Spring Cloud Gateway
  • Kubernetes Ingress
  • Istio Ingress Gateway

Spring Cloud Gateway 是 Spring Cloud 团队在 Spring Reactive 生态系统之上实现的 API 网关,该项目基于 Spring 5.0、Spring Boot 2.0 和 Project Reactor 等技术,旨在为微服务架构提供一种简单有效的、统一的 API 路由管理方式。

Spring Cloud Gateway 中涉及到三个基本概念:

  • Route : 网关路由的基础组成模块,由一个 ID、一个目标 URI、一组 predicates 和一组 filters 定义,如果 predicates 结果为 true,则成功匹配到该路由块。
  • Predicate : 通过匹配 HTTP 请求头或请求参数中的内容,制定路由转发的规则。
  • Filter : 可以在请求之前和响应之前,修改请求和响应信息,可以做统一鉴权。

我以下面这个 Route 路由块来解释 Spring Cloud Gateway 是如何工作的。然后会通过 Docker Compose、Kubernetes 来部署微服务示例项目,演示 gateway 角色在三个体系(Spring Cloud、Kubernetes、Istio)中的实现方式。

routes:
  - id: user-server
    uri: lb://user-server
    predicates:
      - Path=/user-server/**
    filters:
      - SsoAuth=true
      - StripPrefix=1

uri 有两种书写方式:

  • http://192.168.0.108:9097
  • lb://user-server

如果 uri 以 lb:// 开头,则表示从注册中心中获取服务地址,后面跟的就是要转发到的服务的服务名称,这里会使用全局过滤器 LoadBalancerClientFilter 将服务名称 user-server 解析为实际的主机和端口,服务名称既是注册到 Eureka Server 中的服务名。

# LoadBalancerClientFilter
log.trace("LoadBalancerClientFilter url before: " + url);
# 从 RibbonLoadBalancerClient 中获取服务信息
final ServiceInstance instance = choose(exchange);

if (instance == null) {
  String msg = "Unable to find instance for " + url.getHost();
  if(properties.isUse404()) {
    throw new FourOFourNotFoundException(msg);
  }
  throw new NotFoundException(msg);
}

Spring Cloud Gateway 中有很多内置的路由 predicates factories, 这些 predicates 都可以和 HTTP 请求的不同属性进行匹配。内置的 predicates 如下:

Predicate Comments
After Route Predicate Factory 匹配在指定日期时间之后发生的请求
Before Route Predicate Factory 匹配在指定日期时间之前发生的请求
Between Route Predicate Factory 匹配在指定时间区域内发生的请求
Cookie Route Predicate Factory 匹配 cookie
Header Route Predicate Factory 匹配给定名称的请求头
Host Route Predicate Factory 匹配 Host 请求头
Method Route Predicate Factory 匹配 HTTP Method
Path Route Predicate Factory 匹配请求路径
Query Route Predicate Factory 匹配请求参数
RemoteAddr Route Predicate Factory 匹配 RemoteAddr
Weight Route Predicate Factory 权重路由

Spring Cloud Gateway 也内置了很多通用的 Filter,这些 Filter 从接口实现上分为两种:

  • GatewayFilter 局部过滤器,应用到单个路由规则
  • GlobalFilter 全局过滤器,应用到所有的路由上,无需显式配置

全局过滤器通过继承 GlobalFilter、Ordered 实现,注册顺序的设置重写 getOrder() 方法即可,值越大则优先级越低。

内置全局过滤器 功能 顺序
ForwardRoutingFilter scheme 包含 forward 时,将请求转发到当前网关实例 DispatcherHandler 2147483647
LoadBalancerClientFilter scheme 中包含 lb 时,经过此过滤器解析实际的主机和端口 10100
WebsocketRoutingFilter scheme 中包含 ws 或 wss 时,路由 WebScoket 请求 2147483646
ForwardPathFilter 0
GatewayMetricsFilter -2147473648

局部过滤器可以通过继承 AbstractGatewayFilterFactory 来自定义,大家可以去看下两个比较常用的官方内置 Filter 源码 StripPrefixGatewayFilterFactory 和 PrefixPathGatewayFilterFactory,实现非常简单 。

@Override
public GatewayFilter apply(Config config) {
    return new OrderedGatewayFilter((exchange, chain) -> {
        // ...
    }, 10);
}

Spring Cloud Gateway 约定过滤器前缀为 filters 配置的过滤器名称,格式 XxxGatewayFilterFactory

filters:
  # 添加 prefix
  - PrefixPath=/cloud
  # 去除 prefix
  - StripPrefix=1

过滤器参数的配置支持两种方式:

  • Shortcut Configuration 快捷方式

    filters:
      - StripPrefix=1
      - SsoAuth=true
    
  • Fully Expanded Arguments 完全展开方式

    filters:
      - name: StripPrefix
        args:
          parts: 1
      - name: SsoAuth
        args:
          enabled: true
    

SsoAuth 是我自定义的过滤器,用于 Token 认证,具体实现参看微服务示例 api-gateway 模块。

Spring Boot Admin 监控也很好的支持了 Gateway,可以直接在管理界面中查看相关的路由配置,包括添加或者删除路由,在我们微服务示例项目中 actuator-admin 模块集成了 Spring Boot Admin 监控,你可以去访问测试下。

可以通过 actuator endpoint 接口来观察 Spring Cloud Gateway 路由信息:

  • /actuator/gateway/globalfilters 全局路由

  • /actuator/gateway/routefilters 局部路由

  • /actuator/gateway/routes/{id} 路由块

完整的一个路由块数据结构,在做动态路由存储的时候,可以参考这个数据结构来做。

{
  "route_id": "user-server",
  "route_definition": {
      "id": "user-server",
      "predicates": [
          { "name": "Path", "args": {"_genkey_0": "/user-server/**"} }
      ],
      "filters": [
          { "name": "StripPrefix", "args": {"_genkey_0": "1"} },
          { "name": "SsoAuth", "args": {"_genkey_0": "true"} }
      ],
      "uri": "lb://user-server"
  },
  "order": 0
}

Spring Boot Admin 中提供的路由配置功能仅供学习参靠使用,线上断然不能这样用,路由配置如果没有落库,网关重启之后就会丢失,就达不到动态路由的作用,可以结合 redis + mysql 来将路由配置持久化。

微服务项目示例

下面介绍如何通过 Docker Compose 一键部署微服务演示项目,项目结构如下:

[图片上传失败...(image-e98476-1620222358649)]

  • api-gateway 使用 Spring Cloud Gateway 实现的网关。
  • eureka-server 注册中心。
  • swagger-stater 模块是将 Swagger 的功能抽取成了一个组件,各模块直接引用即可。
  • actuator-admin 通过 Spring Boot Admin 管理和监控 Spring Boot 应用程序。

在每个项目下均有制作镜像的 Dockerfile 文件和 Kubernetes 平台 API 对象描述文件 api-gateway.yml

FROM frolvlad/alpine-java:jdk8-slim

RUN set -eux && mkdir -p /home/api-gateway
RUN set -eux && mkdir -p /opt/logs/api-gateway
RUN set -eux && touch /opt/logs/api-gateway/api-gateway.log
ADD api-gateway.jar /home/api-gateway/api-gateway.jar

ENV JAVA_ENV="-Denv=docker"
ENV JAVA_OPTS="-server -Xmx256m -Xms256m -XX:+UseG1GC"

ENTRYPOINT ["sh", "-c", "java $JAVA_ENV $JAVA_OPTS -Dfile.encoding=UTF-8 -Djava.security.egd=file:/dev/./urandom -jar /home/api-gateway/api-gateway.jar" ]

各服务下有 application-docker.yml 和 application-local.yml 两个配置文件,在 bootstrap.yml 中通过指定环境变量来选择启用哪一个,要在本地 IDEA 中测试的话,各启动类配置 VM options:-Denv=local 即可。

为什么要需要两个配置文件?因为服务部署到容器中 IP 地址是不可预知的,所以部分配置使用了服务名,在 Docker Compose 中会通过内置的 DNS 服务来将容器名称解析成 IP 地址,在 Kubernetes 平台中则使用 CoreDNS 进行域名解析,下面是在 application-docker.yml 中的配置,使用的是服务名称。

  • eureka-server:http://aAuHAd:aAuHAd@eureka-server:18761/eureka/
  • actuator-admin:http://actuator-admin:5000

在项目父级 POM 文件中配置了 docker 外部访问地址 docker.host,用来在 IDEA 中通过 Maven 插件 docker-maven-plugin 自动化构建镜像,push.image 参数表示是否要推送镜像至开源仓库 https://hub.docker.com/repository.name 指定仓库名称。

<docker.host>http://47.95.xx.xx:2375</docker.host>
<push.image>false</push.image>
<repository.name>11060</repository.name>
<maven.build.timestamp.format>yyyyMMddHHmmss</maven.build.timestamp.format>

mvn 构建镜像命令:mvn clean package docker:build -Dmaven.test.skip=true -Pdocker

构建成功后,即可在指定的 docker.host 服务器上看到如下镜像:

[root@iZ2zece2l8yr2f8qhrnr3lZ ~]# docker images
REPOSITORY              TAG                 IMAGE ID           CREATED            SIZE
11060/user-server       20210219085131      4b3f6c1abc50       20 hours ago       214MB
11060/eureka-server     20210219084733      eb4a3a365bd5       20 hours ago       213MB
11060/order-server      20210219084538      70fe3a17caa5       20 hours ago       215MB
11060/auth-server       20210219084437      a2e8a2d99556       20 hours ago       214MB
11060/api-gateway       20210219083002      8b4415f557db       20 hours ago       220MB
11060/actuator-admin    20210220083538      9d338e1a3210       20 hours ago       219MB

构建时镜像的版本号,用的是当前构建时间,这样可以保证版本号不会冲突,配置信息如下:

<imageName>11060/${project.build.finalName}:${maven.build.timestamp}</imageName>

😕 镜像有个很让人迷惑的标签:latest。

如果你的 docker-compose 编排文件或者 Kubernetes 平台中的 Pod 模板 (.spec.template) 使用的镜像标签为 latest,执行下面的命令时,并不会去和远端镜像仓库比较当前所用是否是最新的镜像版本,也就是说不会去自动拉取最新镜像触发更新操作的。

  • docker-compose up -d
  • kubectl apply -f

在 Kubernetes 中即使你的 pod 模板使用的镜像拉取策略是 imagePullPolicy=Always,也不会触发更新,所以强烈建议使用递增版本号。

在项目根目录下,放置了 docker-compose.yml 文件,用来创建管理微服务容器,关于 Docker Compose 的使用介绍可以参看《Docker、Docker Compose、Harbor 的使用》章节。

🎉 项目中 docker-compose 编排文件和 Kubernetes 资源描述文件中用到的镜像版本号,我都已经推送到了开源仓库 dockerhub 上面,也就是说,你可以不用修改任何参数,直接拿来部署测试

version: '3.3'
services:
  eureka-server:
    image: 11060/eureka-server:20210228051833
    container_name: eureka-server
    restart: always
    privileged: true
    ports:
      - 18761:18761
    volumes:
      - ./logs:/opt/logs
    logging:
      options:
        max-size: "10M"
        max-file: "10"

...... 完整配置可 clone 项目后查看

通过 docker-compose -p cloud up -d 来创建启动所有容器,启动成功后,可以访问下面的一些站点测试。

[root@ cloud]# docker ps --format "table {{.ID}}\t{{.Image}}\t{{.Names}}\t{{.Ports}}\t{{.Status}}"
IMAGE                                NAMES           PORTS                      STATUS
11060/eureka-server:20210220051830   eureka-server   0.0.0.0:18761->18761/tcp   Up 32 seconds
11060/auth-server:20210220051823     auth-server     0.0.0.0:9096->9096/tcp     Up 32 seconds
11060/api-gateway:20210220053807     api-gateway     0.0.0.0:4000->4000/tcp     Up 32 seconds
11060/user-server:20210220051845     user-server     0.0.0.0:9097->9097/tcp     Up 33 seconds
11060/order-server:20210220051838    order-server    0.0.0.0:9099->9099/tcp     Up 32 seconds
11060/actuator-admin:20210220081014  actuator-admin  0.0.0.0:5000->5000/tcp     Up 32 seconds

Eureka Server 的访问端口为:18761,可以看到下面的注册列表信息。

微服务项目可能几十、上百,如果去访问每一个应用的 swagger 来进行 API 测试将是非常繁琐的,既然网关是微服务统一的入口点,那我们就希望网关也可以将所有的应用的 swagger 页面聚合在一起,这样我们测试接口时只需要访问网关的 Swagger UI 就可以了。

swagger 聚合的具体实现可参考微服务项目中 api-gateway 模块。

网关 swagger 的访问地址为:http://ip:4000/swagger-ui.html,已经聚合了其它微服务的 swagger 入口,如下:

网关还有很多功能,比如降级、熔断、限流、黑白名单等等,这些就留给你去探索了。

Spring Boot Admin 监控页面访问入口:http://ip:5000 账号:cloud 密码:cloud@monitor

示例中 actuator-admin 服务注册到了 Eureka 上,它会自动拉取服务列表,主动去注册,也就说客户端不需要引入 spring-boot-admin-starter-client 依赖,也不需要配置 actuator-admin 的地址。

下面是注册上来的微服务信息,可以看到是通过 source:discovery 来发现服务并主动注册上来的。

后面会介绍如何将这个微服务演示项目部署到 Kubernetes 平台,以及 Ingress 对象如何实现全局路由的。

~ 终 ~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容