在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
简单路由提供一个连接池的属性,请求如果过多,会被放到连接池中依次执行,保证请求的效率。在使用简单路由的时候,我们可以通过设置zuul.host.maxTotalConnections(默认200)和zuul.host.maxPerRouteConnections(默认20),来调整目标主机最大连接数和每个主机的初始连接数。
注意:简单路由在配置path的时候是以“http”或“https”开头的。
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网关的路由规则了:
源码点击这里
Spring Cloud系列:
Spring Cloud服务管理框架Eureka简单示例(三)