在Eureka集群中使用Zuul
在Spring Cloud中集群中使用Zuul网关,那么Zuul也是集群的一部分,所以它也应该是一个Eureka项目,如图所示,我们搭建一个最简单的集群,通过网关来分发浏览器发起的请求。
依旧是从Spring Cloud服务管理框架Eureka简单示例(三)这篇博客底部拿到我们的源码,这三个项目对应我们架构图中底部的三个项目,可以启动三个项目的*App启动类,测试项目是否能够正常使用。
接下来创建eureka-zuul网关项目,作为一个Eureka项目,引入Eureka相关依赖,再加入zuul依赖和httpClient依赖,完整pom.xml的依赖:
<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Dalston.SR5</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-config</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-zuul</artifactId> </dependency> <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> <version>4.5.5</version> </dependency> </dependencies>
接下来在src/main/resources目录下新建application.yml文件,配置网关项目的启动端口、服务名称、注册地址以及zuul网关路由规则:
server: port: 9090 spring: application: name: eureka-zuul eureka: client: serviceUrl: defaultZone: http://localhost:8761/eureka/ zuul: routes: users: path: /users/** serviceId: eureka-consumer
路由规则配置了凡是访问以users作为前缀的请求都会转发到eureka-consumer项目进行处理。
接下来在src/main/java目录下创建com.init.springCloud包,并创建启动类ZuulApp.class,开启zuul服务代理:
package com.init.springCloud; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.zuul.EnableZuulProxy; @SpringBootApplication @EnableZuulProxy public class ZuulApp { public static void main(String[] args) { SpringApplication.run(ZuulApp.class, args); } }
启动eureka-zuul项目,访问:http://localhost:9090/users/router,可以看到浏览器返回了来自eureka-consumer的服务调用,相当于是访问了:http://localhost:8081/router
这样我们就完成了在一个最简单的eureka集群中使用zuul网关的效果,可以看到,我们全程是没有去对原来的项目进行改动的,只是在zuul的配置中设置了简单的规则之后,就实现了路由。
底层请求转发的方式:httpClient与okhttp
微服务分布式架构的出现,让http请求成为RPC(Remote Procedure Call,远程过程调用)的首选方式,虽然java平台本身提供了HttpURLConnection来作为http通讯,但是其本身api有限,功能不够强大,所以才有了一系列的http请求工具库出现,这里主要Zuul支持的三种http请求方式:
httpClient:由Apache提供,来作为标准开源http请求客户端,提供了丰富的api,以及强大的功能库,已经是java平台中默认的http请求客户端。在我们的网关中默认就是用了httpClient。
okhttp:由square公司开发,主要目标是高效,专注于提供网络连接效率。它能实现同一ip和端口的请求重用一个socket,这种方式能大大降低网络连接的时间,和每次请求都建立socket,再断开socket的方式相比,降低了服务器服务器的压力。提供对http(http/2)、https以及SPDY的支持。
RestClient:过去zuul使用的分布式rest风格客户端,它基于Ribbon,不过现在已经废弃了。
切换到okhttp或者RestClient,只需要分别设置ribbon.okhttp.enabled=true或者设置ribbon.restclient.enabled=true。
ribbon: httpclient: enabled: false okhttp: enabled: true # restclient: # enabled: true
另外,如果引用的是okhttp,还需要加入依赖:
<dependency> <groupId>com.squareup.okhttp3</groupId> <artifactId>okhttp</artifactId> </dependency>运行结果是一致的,我这里就不贴出具体结果了。
路由配置
我们先回顾zuul的生命周期:
http请求首先进入“pre”过滤器,实现身份认证、调试信息等;之后进入“routing”过滤器,将我们的请求转发到微服务中心,并获取响应;接着进入“post”过滤器,对响应的内容进行修饰;最后把响应返回给请求方。
1.简单路由:SimpleHostRoutingFilter
在“routing”过滤器阶段,zuul默认使用SImpleHostRoutingFilter将我们的请求封装后转发到源服务,源服务返回响应,再把结果给“post”过滤器处理,可以查看它的核心部分源码:
@Override public Object run() { //省略部分代码 String uri = this.helper.buildZuulRequestURI(request); this.helper.addIgnoredHeaders(); try { CloseableHttpResponse response = forward(this.httpClient, verb, uri, request, headers, params, requestEntity); setResponse(response); } catch (Exception ex) { throw new ZuulRuntimeException(ex); } return null; }
SImpleHostRoutingFilter主要处理不走Eureka的proxy,底层使用httpclient来转发请求。配置如下:
zuul: routes: baidu: url: https://www.baidu.com simple: path: /simple/** url: http://www.163.com
2.跳转路由:SendForwordFilter
直接使用RequestDispatcher的forward方法将地址进行跳转,这个时候,虽然我们请求的是网关,但是产生了响应之后,并不会响应到网关,而是我们给出的跳转地址。
我们在eureka-zuul项目下创建MyController,提供一个外部服务:
package com.init.springCloud; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; @RestController public class MyController { @RequestMapping(value = "/sayHi/{name}", method = RequestMethod.GET) public String sendForword(@PathVariable String name){ return "Hi,"+name; } }
浏览器访问:http://localhost:9090/sayHi/spirit,可以看到服务正常提供:
接着配置一个路由规则,让zuul使用sendForwardFilter来完成转发:
zuul: routes: myForword: path: /mf/** url: forward:/sayHi
配置好以后重启项目,访问:http://localhost:9090/mf/spirit,可以看到转发已经完成:
注意:跳转路由的path后面会跟着一个“forward:”,之后才是接的服务
3.Ribbon路由:RibbonRoutingFilter
如果写配置的时候用的是ServiceId,则用这个routing过滤器,这个过滤器可以用Ribbon 做负载均衡,用hystrix做熔断。在我们之前测试的时候已经这样配置过了:
zuul: routes: users: path: /users/** serviceId: eureka-consumer
我们也可以将这个serviceId替换zuul规则的名称,只提供一个path,这样最终达到的效果是一样的:
zuul: routes: eureka-consumer: path: /users/**注意:除了简单路由和跳转路由的path配置方式,其余的格式都会被当成是ribbon路由来处理,也就是当成一个serviceId。
如果一个请求是在我们的路由规则中不存在的,那么我们可以为它配置一个“遗留”请求的规则,如同我们在java中使用switch时设置的default一样,下面给出了整个路由规则的配置状况:
zuul: routes: users: path: /users/** serviceId: eureka-consumer baidu: url: https://www.baidu.com simple: path: /simple/** url: http://www.163.com myForword: path: /mf/** url: forward:/sayHi legacy: path: /** url: https://www.toutiao.com
我们这里就把所有其他的请求都导向到了头条的新闻页,访问:http://localhost:9090看看结果:
自定义路由规则
1.PatternServiceRouteMapper
在eureka-zuul项目下新建MyFilterConf类,通过PatternServiceRouteMapper编写正则表达式,匹配我们的路由规则:
package com.init.springCloud; import org.springframework.cloud.netflix.zuul.filters.discovery.PatternServiceRouteMapper; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class MyFilterConf { //配置访问"module/**"的项目,都转发到"eureka-module/**"进行处理 @Bean public PatternServiceRouteMapper patternServiceRouteMapper(){ return new PatternServiceRouteMapper( "(eureka)-(?<module>.+)", "${module}/**"); } }
重启项目,访问:http://localhost:9090/consumer/router,浏览器也是正常返回了结果:
自定义规则是很简单的,麻烦的地方在于正则表达式的编写。
2.zuul.ignoredServices
忽略那些不匹配的服务,譬如:
zuul: ignoredServices: '*' routes: users: /myusers/**
在这个例子中,所有的服务都将被忽略,除了users。
3.zuul.ignoredPatterns
如果想要加大忽略配置规则的粒度,除了使用忽略服务ID的方式,还可以用具体的规则来匹配需要忽略的服务:
zuul: ignoredPatterns: /**/admin/** routes: users: /myusers/**这意味着所有的调用,如“/myusers/101”将被转发到“users”服务的“/101”。但是包括“/admin/”的调用将不会解决。
敏感头部信息设置
在相同的系统中共享服务之间是可以的,但是你可能不希望敏感的头部向外部服务器泄漏。我们可以指定一个被忽略的头部列表作为routing配置的一部分。cookie是一个特殊的角色,因为它在浏览器中有明确的语义,而且应该总是被视为敏感的。
如果调用网关的服务中只有一个设置了cookie,那么你就可以让它们从后端流到服务提供者(对应我们的consumer)。除非你在代理中设置了cookie,并且所有的后台服务都是同一个系统的一部分,那么简单地共享它们是很自然的(例如,使用Spring会话将它们连接到某个共享状态)。除此之外,任何由调用网关的服务设置的cookie可能对服务提供者都不是很有用,所以建议至少要设置"Cookie"和"Set-Cookie"。
zuul: routes: users: path: /myusers/** sensitiveHeaders: Cookie,Set-Cookie,Authorization serviceId: eureka-consumer
譬如上面的示例,是每个规则下设置的默认敏感头部信息,包括了Cookie,Set-Cookie和Authorization,我们不用再去指定。除非你想让某些被禁止的项目被发送,譬如全部都不限制,可以将敏感头置为空:
zuul: routes: users: path: /myusers/** sensitiveHeaders: serviceId: eureka-consumer
忽略请求头设置
除了每个路由规则上面的敏感头部信息设置,我们还可以在网关与外部服务交互的时候,用一个全局的设置zuul.ignoredHeaders,去除那些我们不想要的http头部信息(包括请求和响应的)。在默认情况下,zuul是不会去除这些信息的。如果Spring Security不在类路径上的话,它们就会被初始化为一组众所周知的“安全”头部信息(例如,涉及缓存),这是由Spring Security指定的。在这种情况下,假设请求网关的服务也会添加头部信息,我们又要得到这些代理头部信息,就可以设置zuul.ignoreSecurityHeaders为false,同时保留Spring Security的安全头部信息和代理的头部信息。当然,我们也可以不设置这个值,仅仅获取来自代理的头部信息。路由端点
Actuator提供了一个可以查看路由规则的端点/routes,我们引入Actuator依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> <version>1.5.4.RELEASE</version> </dependency>
再把安全验证关闭,让我们可以访问到这个端点:
management: security: enabled: false
这里,遗留请求的路由规则会影响到我们访问这个端点,先注释掉这个路由规则:
# legacy: # path: /** # url: https://www.toutiao.com
之后,重启项目,访问:http://localhost:9090/routes,我们便能看到zuul网关的路由规则了: