前面两篇微服务讲的是netflix生态中的eureka(注册中心),hystrix(熔断器),也各自介绍了他们的作用,现在我们已经讲了微服务治理中的服务注册发现,服务熔断(防止服务不可用的级联扩散)也顺带提到了feign(对http请求的封装)。还有服务的负载均衡ribbon,微服务配置中心,以及本文马上要讲的zuul网关。很显然,在前面提到eureka时,说到eureka是将被调用的服务在注册中心注册自己的一些元信息,方便在服务变动后而不需要对服务消费端改变即可继续进行访问。但是这个前提都是所有被调用的服务都必须在注册中心(eureka server)注册信息,但在现实中,我们不能要求我们所有调用的服务都必须在我们自己的注册中心注册,那么,那些无法在我们的注册中心注册的服务怎么管理呢?一个很好的途径便是通过zuul网关管理,它可以屏蔽后端的细节。当然,zuul的作用不仅于此,我来把官方对zuul介绍翻一下,大家可以理解下:
zuul是一种设备和web站点访问后端微服务集群的入口,作为一个边缘性服务,zuul可以提供动态路由,弹性伸缩和安全认证以及监控。
更详细的,我们通常在以下几种情况下可以使用zuul:
1,身份认证和安全。可以对不同后端资源进行认证过滤那些不满足条件的请求。
2,监控。在边缘跟踪有意义的数据和统计数据,以便为我们提供生产线上的准确视图
3,动态路由。能够根据需求将请求分发到后端集群。
4,压力测试。逐渐增加集群的流量,以衡量性能
5,限流。每个服务都有处理请求的能力上限,当请求达到上限后,将会丢弃这些请求,以避免后端服务被击垮。
6,构造静态响应。在某些情况下可以直接构造静态响应返回而不用将这些请求传到后端服务。
7,多区域弹性。跨AWS区域路由请求,以使ELB使用多样化,并使我们的优势更接近我们的成员
zuul实现上述功能一大利器是这一组件可以提供一系列过滤器filter。而这些filter主要分为四类:pre filter(请求发给服务前),route filter(路由时的过滤器,通常可以将原来的请求包装成代理请求,将代理请求对应的response包装到原来请求对应的response),post filter(请求返回的过滤器),error filter(网关代理发生错误时过滤器)。
下面详细讲述spring boot使用zuul的过程以及zuul的核心功能过滤器的使用,这里我新建另一个项目专门作为网关服务器。
一,zuul的使用
首先是pom.xml引入对应版本的zuul,
org.springframework.boot
spring-boot-starter-tomcat
provided
org.springframework.boot
spring-boot-starter
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-test
test
org.springframework.cloud
spring-cloud-starter-netflix-zuul
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
1.4.2.RELEASE
org.springframework.cloud
spring-cloud-dependencies
Finchley.BUILD-SNAPSHOT
pom
import
启动类:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
@SpringBootApplication
@EnableDiscoveryClient
@EnableZuulProxy
public class ZuulApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulApplication.class, args);
}
}
application.yml
server:
tomcat:
uri-encoding: UTF-8
max-threads: 1000
min-spare-threads: 30
port: 8008
servlet:
context-path: /
zuul:
routes:
firstRoute:
path: /user/**
serviceId: eureka-client1
stripPrefix: true
secondRoute:
path: /user1/**
url: http://127.0.0.1:8003
thirdRoute:
path: /user2/**
serviceId: eureka-client1
eureka-client1:
ribbon:
eureka:
enabled: false
listOfServers: http://127.0.0.1:8003,http://127.0.0.1:8005
eureka:
client:
serviceUrl:
defaultZone: http://root:123456@localhost:8001/eureka,http://root:123456@localhost:8002/eureka
spring:
application:
name: zuul-server
这里解释下application.yml,看zuul.routes下我建了三个代理服务的代理(事实上他们代理的是同一个服务,接着前两篇我讲的微服务),第一个是一个很普通的通过serviceId来定位代理服务的,这个时候用到了注册中心,因为这个serviceId对应的就是eureka client中向eureka server注册的服务的应用名称。那么以后只要是请求路径带/user的请求路径就会被路由到eureka-client下面的服务路径,比如你有一个http://127.0.0.1:8003/user/test/test1那么他就会被路由到你注册到注册中心名为eureka-client1下的路径为/test/test1的接口,这里看到stripPrefix这个属性就是去掉请求路径中/user的意思,如果你把它设置为false,那么就会路由到eureka-client1下的路径为/user/test/test1的接口,默认是true;第二个代理是通过url寻找被代理服务,这个不需要注册中心eureka的参与,这也是我前面说的,当别人服务不能在你的注册中心注册时,你就可以通过这种方式统一管理;第三个是通过serviceId,但是,配置文件往下继续看,会发现我在这里面添加了ribbon负载均衡且这里面禁用了eureka,直接设置ribbon的listservers。效果,你可以自己演示看,确实可以。
下面给出原来我服务消费方的写法:
@Autowired
private ZuulHystrixCommandService zuulHystrixCommandService;
/**
* 测试zuul网关,通过eureka注册中心获取服务地址
* @return
*/
@RequestMapping("/getInfo6")
public String test2() {
// ServiceInstance si = balanceClient.choose("eureka-client1");
// System.out.println("host:"+si.getHost()+" port:"+si.getPort());
// return "host:"+si.getHost()+" port:"+si.getPort();
// return restTemplate.getForEntity("http://eureka-client1/user/getInfo", String.class).getBody().toString();
RestTemplate r =new RestTemplate();
URI uri;
try {
uri = new URI("http://127.0.0.1:8008/user/user/getInfo");
ResponseEntity body = r.exchange(uri, HttpMethod.GET, null, String.class);
return body.getBody().toString();
} catch (URISyntaxException e) {
e.printStackTrace();
return "111111";
}
}
/**
* 测试zuul网关,不通过eureka注册中心获取服务地址
* @return
*/
@RequestMapping("/getInfo7")
public String test3() {
// ServiceInstance si = balanceClient.choose("eureka-client1");
// System.out.println("host:"+si.getHost()+" port:"+si.getPort());
// return "host:"+si.getHost()+" port:"+si.getPort();
// return restTemplate.getForEntity("http://eureka-client1/user/getInfo", String.class).getBody().toString();
RestTemplate r =new RestTemplate();
URI uri;
try {
uri = new URI("http://127.0.0.1:8008/user1/user/getInfo");
ResponseEntity body = r.exchange(uri, HttpMethod.GET, null, String.class);
return body.getBody().toString();
} catch (URISyntaxException e) {
e.printStackTrace();
return "111111";
}
}
/**
* 测试zuul网关,不通过eureka注册中心获取服务地址
* @return
*/
@RequestMapping("/getInfo8")
public String test4() {
RestTemplate r =new RestTemplate();
URI uri;
try {
uri = new URI("http://127.0.0.1:8008/user2/user/getInfo");
ResponseEntity body = r.exchange(uri, HttpMethod.GET, null, String.class);
return body.getBody().toString();
} catch (URISyntaxException e) {
e.printStackTrace();
return "111111";
}
}
在上面我还注入了一个ZuulHystrixCommandService,这个是为了,研究zuul和hystrix结合后效果。它的代码如下:
import java.net.URI;
import java.net.URISyntaxException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
/**
* 调用zuul网关服务接口的hystrix,看看情况
* @author jumprn
*
*/
@Component
public class ZuulHystrixCommandService {
//简单的hystrix熔断机制,复杂的和以前讲的一样。
@HystrixCommand(fallbackMethod="fallback")
public String getMessage() throws URISyntaxException {
RestTemplate r =new RestTemplate();
URI uri;
uri = new URI("http://127.0.0.1:8008/user2/user/getInfo");
ResponseEntity body = r.exchange(uri, HttpMethod.GET, null, String.class);
return body.getBody().toString();
}
public String fallback() {
return "error 了!";
}
}
这里的uri就是我们的网关中第三个被代理服务的访问网关地址。然后,hystrix也可以对此进行熔断管控了。当然,zuul也有自己的失败过滤器以及FallbackProvider(当zuul中给定路由的电路断开时,可以通过创建fallbackprovider类型的bean来提供回退响应。在这个bean中,您需要指定回退的路由ID,并提供一个clienthttpresponse作为回退返回。以下示例显示了相对简单的FallbackProvider实现:)。
如下:
class MyFallbackProvider implements FallbackProvider {
@Override
public String getRoute() {
return "customers";
}
@Override
public ClientHttpResponse fallbackResponse(String route, final Throwable cause) {
if (cause instanceof HystrixTimeoutException) {
return response(HttpStatus.GATEWAY_TIMEOUT);
} else {
return response(HttpStatus.INTERNAL_SERVER_ERROR);
}
}
private ClientHttpResponse response(final HttpStatus status) {
return new ClientHttpResponse() {
@Override
public HttpStatus getStatusCode() throws IOException {
return status;
}
@Override
public int getRawStatusCode() throws IOException {
return status.value();
}
@Override
public String getStatusText() throws IOException {
return status.getReasonPhrase();
}
@Override
public void close() {
}
@Override
public InputStream getBody() throws IOException {
return new ByteArrayInputStream("fallback".getBytes());
}
@Override
public HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
return headers;
}
};
}
}
getRoute()方法返回就是你的要回退路由的id,比如上面返回的就对应你的application.yml中下面的路由:
zuul:
routes:
customers: /customers/**
当然,你可以让这个方法返回为null或者"*"那么,这个FallbackProvider就是为你所有的路由所准备的默认失败响应了。
同样,你可以设置zuul的超时时间以便判断连接失败早些释放连接,如果你使用了serviceId去标明代理,你可以通过设置ribbon.ReadTimeout
和 ribbon.SocketTimeout;如果你是通过url去标明代理的服务,你可以通过设置zuul.host.connect-timeout-millis
和 zuul.host.socket-timeout-millis 来设置超时时间。
当zuul作为web站点与前端用户和设备交互借接口时,你可能会遇到资源返回3xx的情况,此时也许你不想让前端直接重定向到后端服务而是重定向到zuul server,通过它去访问服务,这时你可以提供LocationRewriteFilter 去重写http头部的location字段。如下写法:
@Configuration
public class ZuulConfig {
@Bean
public LocationRewriteFilter locationRewriteFilter() {
return new LocationRewriteFilter();
}
}
默认的,zuul是可以允许所有跨站请求的,但是你也可以通过配置WebMvcConfigurer
设置过滤一些跨站请求。
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/path-1/**")
.allowedOrigins("http://allowed-origin.com")
.allowedMethods("GET", "POST");
}
};
}
这个配置就是只允许从http://allowed-origin.com 这个网址通过get和post去访问属于/path-1的这个代理。
二,zuul中的filter使用
在spring cloud中使用zuul有两种注释方法@EnableZuulServer和@EnableZuulProxy,其中,后者包括前者实现的所有功能,换句话说,后者拥有前者拥有的所有filter。下面详细介绍:
zuul最基本的就是动态路由功能,@EnableZuulServer使用了 SimpleRouteLocator从配置文件中拉取route配置进行简单的路由。
同时也默认配置了以下三类filter:pre filter——ServletDetectionFilter(检查请求是否是通过spring dispatcher,设置一个bool值给FilterConstants.IS_DISPATCHER_SERVLET_REQUEST_KEY),FormBodyWrapperFilter(把传过来的值重新encode一下传给下游服务);post filter————SendResponseFilter(把代理后的连接返回的response包装到原来被代理前的连接的response中);
Error filters———SendErrorFilter(
Forwards to /error
(by default) if RequestContext.getThrowable()
is not null. You can change the default forwarding path (/error
) by setting the error.path
property)
而@EnableZuulProxy创建了一个DiscoveryClientRouteLocator去实现动态路由,这个组件能够与类似eureka 合作除了拉取自己的配置文件配置的route数据外,还能拉取注册在rureka server的服务访问路径数据,当eureka server的注册信息发生变化时,变化也会应用到DiscoveryClientRouteLocator这里实现实时的动态路由。@
EnableZuulProxy包含@EnableZuulServer所有的filter,除此之外,还有下面的filter:pre filter———PreDecorationFilter(他会根据上面配置的RouteLocator去决定路由方式,同时也设置了代理经常设置的http头部信息给下游服务);route filter————RibbonRoutingFilter(它可以结合ribbon,hystrix和内置的http client去发送请求。同时它可以使用不同的http client,只需要你将响应http client配置到classpath下,并采取一些配置
Apache HttpClient: The default client.
Squareup OkHttpClient v3: Enabled by having the com.squareup.okhttp3:okhttp library on the classpath and setting ribbon.okhttp.enabled=true.
Netflix Ribbon HTTP client: Enabled by setting ribbon.restclient.enabled=true. This client has limitations, including that it does not support the PATCH method, but it also has built-in retry.
)。说到这里,不知道读者还记不记得上面我将hystrix与zuul结合,其实那种结合现在看来并不算真正的结合(固然,他有一定的应用场景)你如果想要真正实现在zuul中使用hystrix,只需要将zuul所在项目添加hystrix即可,按照前面讲解hystrix引入hystrix且在zuulserver中使用@EnableZuulProxy且在配置路由时使用serviceId去指定被代理服务方法而不是通过url方式,而回退操作就是通过上面讲的实现FallbackProvider,当然如果你想整合ribbon,一样的。
这里有https://cloud.spring.io/spring-cloud-static/Greenwich.RELEASE/single/spring-cloud.html#_custom_zuul_filter_examples 几种filter的详细使用方法,大家可以参考这个。
好了,zuul学习暂告一段落.....