SpringCloud详解 第五章 API网关服务zuul(二)
本章梳理路由详解
一、传统路由配置
单实例:
zuul: # prefix: /wuzz #统一的公共前缀 # ignored-services: feign-server # "*"所有忽略原有服务名 routes: feign: url: http://localhost:9012/ path: /fegin/**
这个配置代表着,访问zuul服务路径 :http://localhost:9110/fegin/hello ,会转发到 http://localhost:9012/hello 这个路径。
多实例的配置如下:
zuul: routes: feign: path: /fegin/** serviceId: fegin-server ribbon: eureka: enabled: false fegin-server: ribbon: listOfServers: http://localhost:8080/,http://localhost:8081/
该 配 置 实 现了对 符 合 /fegin/** 规 则 的 请 求 路 径 转 发 到http://localhost:8080/和http://localhost:8081/两个实例地址的路由规则。 它的配置 方式与服务 路由的配置 方式一 样,都采用了zuul.routes.< route>.path与zuul.routes. < route>.serviceId参数对的映射方式,只是这 里的 serviceId 是由用户手工命名 的 服 务 名 称 , 配 合 ribbon.listOfServers 参数实现服务与实例的维护。由于存在多个实例,API网关在进行路由转发时需要实现负载均衡策略, 于是这里还需要Spring CloudRibbon的配合。由千在Spring Cloud Zuul中自带了对Ribbon的依赖, 所以我们只需做 一 些配置即可,比如上面示例中关 于Ribbon的各个配置,它们的具体作用如下所示
- ribbon.eureka.enabled:由于zuul.routes. < route>.serviceId指定的是服务名称,默认清况下Ribbon会根据服务发现机制来 获取配置服务名对应的实例清单。 但是,该示例并没有整合类似Eureka之类的服务治理框架,所以需要将该参数设置为false, 否则配置 的serviceId获取不到对应实例的 清单。
- fegin-server.ribbon.listOfServers: 该 参数内容与 zuul.routes.<route>.serviceId 的配置相对 应, 开头的 fegin-server 对应了serviceId的值, 这两个参数的配置相当于在该应用内部手工维护了服务与实例的 对应关系。
二、面向服务的路由
(1)示例:
很显然, 传统 路由的配置方式对于我们来说并不友好, 它同样需要运维人员花费大量的时间来维护各个路由 path与url的关系 。 为了解决这个问题, SpringCloud Zuul实现了与Spring Cloud Eureka的无缝整合, 我们可以让路由的path不是映射具体的url, 而是让它映射到某个具体的服务 , 而具体的url则交给Eureka的服务发现机制去自动维护, 我们称这类路由为面向服务的路由。 在Zuul中使用服务 路由也同样简单, 只需做下面这些配置。
1.添加依赖:这个我们上面已经加进去了
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> </dependency>
2.修改配置:
zuul: routes: feign: serviceId: feign-server path: /fegin/**
这样子如果我们访问 http://localhost:9110/fegin/hello ,那么会转发到服务名为 feign-server 的节点上。
对于面向服务的路由配置,除 了使用path与serviceId映射的配置方式 之外,还有一 种更简洁的配置方式:zuul.routes.<serviceid>=<path>, 其中<serviceid>用来指定路由的具体服务名, <path>用来配置匹配的请求表达式。比如下面的例子, 它的路由规则等价于上面通过path与serviceId组合使用的配置方式。
zuul: routes: feign: feign-server: /fegin/**
(2)服务器路由默认规则
由于默认情况下所有Eureka上的服务都会被Zuul自动地创建映射关系来进行路由,这会使得 一 些我们不希望对外开放的服务也可能被外部访问到。 这个时候, 我们可以使用zuul.ignored-services参数来设置 一 个服务名匹配表达式来定义不自动创建路由的规则。 Zuul在自动创建服务路由的时候会根据该表达式来进行判断, 如果服务名匹配表达式 , 那 么 Zuul 将跳过 该 服 务 , 不 为 其创 建 路 由 规 则 。 比如, 设 置 为zuul.ignored-services=*的时候,Zuul将对所有的服务都不自动创建路由规则。 在这种情况下,我们就要在配置文件中逐个为需要路由的服务添加映射规则(可以使用path与 serviceId组合的配置方式, 也可使用更简洁的 zuul.routes.<serviceid>=<path>配置方式),只有在配置文件中出现的映射规则会被创建路由,而从Eureka中获取的其他服务,Zuul将不会再为它们创建路由规则。
(3)自定义路由映射规则
我们在构建微服务系统进行业务逻辑开发的时候, 为了兼容外部不同版本的客户端程序(尽量不强迫用户升级客户端),一 般都会采用开闭原则来进行设计与开发。 这使得系统在迭代过程中, 有时候会需要我们为 一 组互相配合的微服务定义 一 个版本标识来方便管理它们的版本关系,根据这个 标识我们可以 很容易地知道这些服务需要 一 起启动并配合使用。比如可 以 采 用 类 似这 样 的 命 名: userservice-v1、 userservice-v2、orderservice-v1、 orderservice-v2。 默认情况下,Zuul自动为服务创建的路由表达式会采用服务名作为前缀, 比如针对上面的userservice-v1 和userservice-v2,它会产生/userservice-v1 和/userservice-v2两个路径表达式来映射,但是这样生成出来的表达式规则较为单 一 , 不利于通过路径规则来进行管理。 通常的做法是为这些不同版 本的 微 服 务 应 用 生 成以版 本代号 作 为 路 由 前 缀 定 义 的 路 由 规 则 , 比如/v1/userservice/ 。 这时候, 通过这样具有版本号前缀的 URL 路径,我们就可以很容易地通过路径表达式来归类和管理这些具有版本信息的微服务了。针对上面所述的需求,如果我们的各个微服务应用都遵循了类似userservice-v1这样的命名规则,通过-分隔的规范来定义服务名和服务版本标识的话,那么,我们可以使用Zuul中自定义服务与路由映射 关系的功能,来实现为符合上述规则的微服务自动化地创建类似/v1/userservice/** 的路由匹配规则。 实现步骤非常简单, 只需在 API 网关程序中, 增加如下Bean的创建即可:
@Bean public PatternServiceRouteMapper serviceRouteMapper () { return new PatternServiceRouteMapper( "(?<name> A .+)-(?<version>v.+$)", "${version}/${name}"); }
(4)路径匹配:
不论是使用传统路由的配置方式还是服务路由的配置方式,我们都需要为每个路由规则定义匹配表达式, 也就是上面所说的path参数。 在Zuul中, 路由匹配的路径表达式采用了Ant风格定义。Ant风格的路径表达式使用起来非常简单,它 一 共有下面这三种通配符。
- ? :匹配任意单 个字符
- * :匹配任意数量的字符
- ** :匹配任意数址的字符, 支待多级目录
例如:
- /user-service/? 它可以匹 配/user-service/之后拼接一个任务字符的路径, 比如/user-service/a 、 /user-service/b 、 /user-service/c
- /user-service/* 它可以匹 配/user-service/之后拼接任 意字 符的路径, 比如/user-service/a 、 /user-service/aaa 、 /user-service/bbb。 但是它无法匹配/user-service/a/b
- /user-service/** 它可以匹 配/user-service/*包含的内容之外, 还可以匹 配形如/user-service/a/b的多级目录路径
那么如果一 个URL 路径可能会被多个不同路由的表达式匹配上。例如如下这么个配置:
zuul.routes.user-service.path=/user-service/** zuul.routes.user-service.serviceId=user-service zuul.routes.user-service-ext.path=/user-service/ext/** zuul.routes.user-service-ext.serviceId=user-service-ext
此时, 调用 user-service-ext 服务的 URL 路径实际上会同时被 /userservice/** 和 /user-service/ext/** 两个表达式所匹配。 在逻辑上, API 网关服务需要优先选择 /user-service/ext/** 路由,然后再匹配 /user-service/** 路由才能实现上述需求。但是如果使用上面的配置方式,实际上是无法保证这样的路由优先顺序的。从下面的路由匹配算法中, 我们可以看到它在使用路由规则匹配请求路径的时候是通过线性遍历的方式,在请求路径获取到第 一 个匹配的路由规则之后就返回并结束匹配过程。所以当存在多个匹配的路由规则时, 匹配结果完全取决于路由规则的保存顺序。定位到 SimpleRouteLocator
protected ZuulRoute getZuulRoute(String adjustedPath) { if (!matchesIgnoredPatterns(adjustedPath)) { for (Entry<String, ZuulRoute> entry : getRoutesMap().entrySet()) { String pattern = entry.getKey(); log.debug("Matching pattern:" + pattern); if (this.pathMatcher.match(pattern, adjustedPath)) { return entry.getValue(); } } } return null; }
下面所示的代码是基础的路由规则加载算法, 我们可以看到这些路由规则是通过LinkedHashMap保存的, 也就是说, 路由规则的保存是有序的, 而内容的加载是通过遍历配置文件中路由规则依次加入的,所以导致问题的根本原因是对配置文件中内容的读取。还是这个类:
protected Map<String, ZuulRoute> locateRoutes() { LinkedHashMap<String, ZuulRoute> routesMap = new LinkedHashMap<>(); for (ZuulRoute route : this.properties.getRoutes().values()) { routesMap.put(route.getPath(), route); } return routesMap; }
所以上面这个例子的正确配置方式就是把更加明确的路径匹配写的更前面,也就是:
zuul: routes: user-service-ext: path: /user-service/ext/** serviceld: user-service-ext user-service: path: /user-service/** serviceld: user-service
(5)忽略表达式
通过 path 参数定义的 Ant 表达式已经能够完成 API 网关上的路由规则配置功能,但是为了更细粒度和更为灵活地配置路由规则, Zuul 还提供了一 个忽略表达式参数 zuul.ignored-patterns 。 该参数可以用来设置不希望被 API 网关进行路由的 URL 表达式。如果不希望 /hello 接口被路由, 那么我们可以这样设置:zuul.ignored-patterns = /**/hello/** 。然后, 可以尝试通过网关来访间 fegin-server 的 /hello。虽然该访问路径完全符合 path 参数定义的规则,但是由于该路径符合 zuul.ignored-patterns 参数定义的规则,所以不会被正确路由。 同时,我们在控制台或日志中还能看到没有匹配路由的输出信息
(6)路由前缀
为了方便全局地为路由规则增加前缀信息,Zuul提供了 zuul.prefix参数来进行设置。比如,希望为网关上的路由规则都增加/api前缀,那么我们可以在配置文件中增加配 置:zuul.prefix=/api。另外,对于代理前缀会默认从路径中移除,我们可以通过设置zuul.stripPrefix=false来关闭该移除代理前缀的动作,也可以通过zuul. routes .<route>.strip-pref ix=true来对指定路由关闭移除代理前缀的动作。
(7)本地跳转
在 Zuul 实现的 API 网关路由功能中, 还支持 forward 形式的服务端跳转配置。 实现方式非常简单,只需通过使用 path 与 url 的配置方式就能完成,通过 url 中使用 forward来指定需要跳转的服务器资源路径。下面的配置实现了两个路由规则, api-a 路由实现了将符合 /api-a/** 规则的请求转发到 http://localhost:8001/; 而 api-b 路由则使用了本地跳转, 它实现了将符合/api-b/** 规则的请求转发到API网关中以 /local 为前缀的请求上,由API网关进行本地处理。 比如, 当 API 网关接收到请求 /api-b/hello, 它符合 api-b 的路由规则,所以该请求会被 API 网关转发到网关的 /local/hello 请求上进行本地处理。
zuul.routes.api-a.path=/api-a/** zuul.routes.api-a.url=http://localhost:8001/ zuul.routes.api-b.path=/api-b/** zuul.routes.api-b.url=forward:/local
这里要注意, 由于需要在 API 网关上实现本地跳转,所以相应的我们也需要为本地跳转实现对应的请求接口。
public class HelloController { @RequestMapping("/local/hello”) public String hello() ( return "Hello World Local"; } )
(8)Cookie与头信息
默认情况下, SpringCloud Zuul在请求路由时, 会过滤掉HTTP请求头信息中的一 些敏感 信 息, 防止它们被传 递到下游的外部服 务器。 默 认的 敏感 头 信 息 通 过 zuul.sensitiveHeaders参数定义,包括Cookie、Set-Cookie、Authorization三个属性。所以, 我们在开发Web项目时常用的Cookie在 SpringCloud Zuul网关 中默认是不会传递 的 , 这就会引发一 个常见的问题: 如果我们要将使用了Spring Security、 Shiro等安全框架构建的Web应用通过SpringCloud Zuul构建的网关来进行路由时,由于Cook迳信息无法传递, 我们的Web应用将无法实现登录和鉴权。 为 了解决这个问题, 配置的 方法有很多。
通过设置全局参数为空来覆盖默认值, 具体如下:zuul.sensitiveHeaders =
这种方法并不推荐, 虽然可以实现Cookie的传递, 但是破坏了默认设置的用意。在微服务架构的API网关之内, 对于无状态的RESTful API请求肯定是要远多于这些Web类应用请求的, 甚至还有 一 些架构 设计会 将Web类应用和App客户端一 样都归为API 网关之外的客户端应用。通过指定路由的参数来配置, 方法有下面两种。
#方法一:对指定路由开启自定义敏感头 zuul.routes.<router>.customSensitiveHeaders = true #方法二:将指定路由的敏感头设置为空 zuul.routes.<router>.sensitiveHeaders =
推荐使用这两种方法 , 仅对指定的Web应用开启对敏感信息的传递,影响范围小, 不至于引起其他服务的信息泄露间题。
(9)Hystrix 和 Ribbon 支持
spring-cloud-starter-zuul依赖,它自身就包含了对 spring-cloud-starter-hys七rix 和 spring-cloud-starter-ribbon模块的依赖, 所以 Zuul天生就拥有线程隔离和断路器的自我保护功能,以及 对服务调用的客户端负载均衡功能。 但是需要注意, 当使用path与url的映射关系来配置路由规则的时候, 对于路由转发的请求不会采用HystrixCommand来包装, 所以 这类路由请求没有线程隔离和断路器的保护,并且也不会有负载均衡的能力。因此,我们在使用Zuul的时候尽量使用path和serviceId的组合来进行配置, 这样不仅可以保证API网关的健壮和稳定,也能用到Ribbon的客户端负载均衡功能。
所以在 zuul服务中可以使用到 ribbon 以及 hystrix 的配置,比如下面这些参数的设置。
- hystrix.command.default.execution.isolation.thread.timeoutinMilliseconds : 该 参 数可以用 来 设 置API 网 关中路 由 转 发 请 求的HystrixCommand 执行超时时间, 单位为毫秒。 当路由转发请求的命令执行时间超过该配置值之后,Hystrix会将该执行命令标记为TIMEOUT并抛出异常
- ribbon.ConnectTimeout: 该参数用来设置路由转发请求的时候, 创建请求连接的超时时间。当ribbon.ConnectTimeout的配置值小于hystrix.command.default.execution.isolation.thread.timeoutlnMilliseconds 配置值的时候, 若出现路由请求出现连接超时, 会自动进行重试路由请求, 如果重试依然失败。如果 ribbon.ConnectTimeo江的配置值大于hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds配置值的时候,当出现路由请求连接超时时, 由于此时对于路由转发的请求命令已经超时, 所以不会进行重试路由请求, 而是直接按请求命令超时处理, 返回TIMEOUT的错误信息。
- ribbon.ReadTimeout: 该参数用来设置路由转发请求的超时时间。 它的处理与ribbon.ConnectTimeout类似, 只是它的超时是对请求连接建立之后的处理时间。 当 ribbon.ReadTimeout的配置值小于 hystrix.command.default.execution.isolation.thread.TimeoutinMilliseconds配置值的时候,若 路由请求的处理时间超过该配置值且依赖服务的请求还未响应的时候, 会自动进行 重 试 路 由 请 求 。
在使用Zuul的服务路由时,如果路由转发请求发生超时(连接超时或处理超时),只要超时时间的设置小于Hystrix的命令超时时间,那么它就会自动发起重试。 但是在有些情况下,我们可能需要关闭该 重试机制,那么可以通过下面的两个参数来进行设置:
zuul.retryable = false //全局关闭重试机制 zuul.routes.<route>.retryable = false //指定路由关闭重试机制