微服务示例项目地址:https://github.com/14032/cloud
为什么微服务需要网关?
- 提供单个统一的 API 入口,客户端只需要与网关打交道,无需关心微服务内部组成和数量
- 将外部公共 API 与内部微服务 API 分开,区分聚合服务和原子服务,原子服务一般不对外暴露
- 网关统一认证鉴权,无需每个微服务都去实现一遍重复的功能,节省工作量
- 限流,防止大量的请求使服务器过载,导致服务不可用,提高服务的可用性与稳定性
- 熔断降级,发生特定异常的时候,对接口降级,由网关直接返回,防止引起整个系统的雪崩
云服务网关的实现有很多,下面是我会分享的三种,这章先介绍下 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 对象如何实现全局路由的。
~ 终 ~