SpringCloud之Zuul

附:SpringCloud之系列汇总跳转地址

Zuul简介

基于Netflix的开源框架zuul实现的各个微服务之间都不存在单点,并且都注册于Eureka ,基于此进行服务的注册与发现,再通过Ribbon进行服务调用,并具有客户端负载功能。

问题点?

  • 将我们具体的微服务地址和端口暴露出去?
  • 如果系统庞大,服务拆分的足够多那又由谁来维护这些路由关系呢?
  • 服务调用之间的一些鉴权、签名校验怎么做?
  • 由于服务端地址较多,客户端请求维护难度?

针对上述问题:SpringCloud 全家桶自然也有对应的解决方案: Zuul Spring Cloud Zuul 将自身注册为 Eureka 服务治理下的应用,从 Eureka 中获取服务实例信息,从而维护路由规则和服务实例。

API服务网关(API Gateway)服务

我们在所有的请求进来之前抽出一层网关应用,将服务提供的所有细节都进行了包装,这样所有的客户端都是和网关进行交互(统一入口),简化了客户端开发:

  • 将细粒度的服务组合起来提供一个粗粒度的服务
  • 所有请求都导入一个统一的入口,那么整个服务只需要暴露一个api
  • 对外屏蔽了服务端的实现细节,也减少了客户端与服务器的网络调用次数

zuul

通过Zuul可以完成以下功能

  • 客户端负载:Zuul 注册于 Eureka 并集成了 Ribbon ,所以自然也是可以从注册中心获取到服务列表进行客户端负载(这里的Zuul就相当于一个Eureka Client)
  • 动态路由参照Spring Cloud Config 动态刷新配置文件的机制
  • 监控与审查
  • 身份认证与安全
  • 压力测试: 逐渐增加某一个服务集群的流量,以了解服务性能
  • 金丝雀测试
  • 服务迁移
  • 负载剪裁: 为每一个负载类型分配对应的容量,对超过限定值的请求弃用
  • 静态应答处理

添加依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-zuul</artifactId>
</dependency>

spring-cloud-starter-zuul本身已经集成了hystrix和ribbon

注意点

  • (传统路由)当使用path与url的映射关系来配置路由规则时,对于路由转发的请求则不会采用HystrixCommand来包装,所以这类路由请求就没有线程隔离和断路器保护功能,并且也不会有负载均衡的能力
  • (服务路由)使用Zuul的时候尽量使用**path和serviceId**的组合进行配置,这样不仅可以保证API网关的健壮和稳定,也能用到Ribbon的客户端负载均衡功能。

开启服务注册

@EnableZuulProxy
@SpringBootApplication
public class ZuulApplication {
    public static void main(String[] args) {
        SpringApplication.run(ZuulApplication.class, args);
    }
}

路由配置顺序注意

  • 按照配置的顺序进行路由规则控制,则需要使用YAML
  • 如果是使用propeties文件,则会丢失顺序
server.port=8008

spring.application.name=zuul
eureka.client.serviceUrl.defaultZone=http://admin:123456@peer1:8001/eureka,http://admin:123456@peer2:8002/eureka
eureka.instance.prefer-ip-address=true
eureka.instance.instance-id=${spring.cloud.client.ipAddress}:${server.port}
eureka.instance.hostname=${spring.cloud.client.ipAddress}

zuul.routes.api-ribbon.path=/api-ribbon/**
zuul.routes.api-ribbon.service-id=ribbon

zuul.routes.api-feign.path=/api-feign/**
zuul.routes.api-feign.service-id=feign

#是否开启重试功能
zuul.retryable=true
#对当前服务的重试次数
ribbon.MaxAutoRetries=2
#切换相同Server的次数
ribbon.MaxAutoRetriesNextServer=0

#zuul.TokenFilter.pre.disable=true

其实,Zuul在注册到Eureka服务中心之后,它会为Eureka中的每个服务都创建一个默认的路由规则,默认规则的path会使用serviceId配置的服务名作为请求前缀 :

zuul.routes.api-ribbon.path=/ribbon/**
zuul.routes.api-ribbon.service-id=ribbon

zuul.routes.api-feign.path=/feign/**
zuul.routes.api-feign.service-id=feign

Zuul容错与回退

  • Zuul的Hystrix监控的粒度微服务,而不是某个API
  • Zuul提供了一个ZuulFallbackProvider接口(最新版本建议直接继承类FallbackProvider,新版增加异常处理),实现该接口就可以为Zuul实现回退功能
package com.yj.zuul.provider;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.netflix.zuul.filters.route.ZuulFallbackProvider;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.stereotype.Component;

@Component
public class ZuulServiceFallbackProvider implements ZuulFallbackProvider {
    protected final Logger logger = LoggerFactory.getLogger(ZuulServiceFallbackProvider.class);
    @Override
    public String getRoute() {
        // 注意: 这里是route的名称,不是服务的名称,否则无法起到回退作用 
        return "ribbon";
    }

    @Override
    public ClientHttpResponse fallbackResponse() {
        return new ClientHttpResponse() {
            @Override
            public HttpHeaders getHeaders() {
                HttpHeaders headers = new HttpHeaders(); 
                headers.setContentType(MediaType.APPLICATION_JSON_UTF8);
                return headers;
            }

            @Override
            public InputStream getBody() throws IOException {
                logger.info("ribbon: 服务不可用");
                return new ByteArrayInputStream("该服务暂不可用,请稍后重试!".getBytes());
            }

            @Override
            public HttpStatus getStatusCode() throws IOException {
                return HttpStatus.OK;
            }

            @Override
            public int getRawStatusCode() throws IOException {
                return 200;
            }

            @Override
            public String getStatusText() throws IOException {
                return "OK";
            }

            @Override
            public void close() {

            }
        };
    }
}

路由重试或者启动备用服务来分散压力(待验证)

网络等原因导致服务不可用,需要进行重试,zuul结合Spring Retry 添加Spring Retry依赖

<dependency>
	<groupId>org.springframework.retry</groupId>
	<artifactId>spring-retry</artifactId>
</dependency>

开启重试

不使用重试,就必须考虑到是否能够接受单个服务实例关闭和eureka刷新服务列表之间带来的短时间的熔断(在未刷新之前还是会访问到熔断中的服务降级方法)

#是否开启重试功能
zuul.retryable=true
#对当前服务的重试次数
ribbon.MaxAutoRetries=2
#切换相同Server的次数
ribbon.MaxAutoRetriesNextServer=0

断路器的其中一个作用就是防止故障或者压力扩散。当压力过大是,一个服务的宕机,路由将压力转向其它服务时有可能也会压垮其它服务。断路器的形式更像是提供一种友好的错误信息,提高服务之间的容错性。

Zuul过滤器

Zuul大部分功能都是通过过滤器来实现的。Zuul中定义了四种标准过滤器类型:

  • PRE 路由之前。我们可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等。
  • ROUTING 路由之时。这种过滤器用于构建发送给微服务的请求,并使用Apache HttpClient或Netfilx Ribbon请求微服务。
  • POST 路由之后。这种过滤器可用来为响应添加标准的HTTP Header、收集统计信息和指标、将响应从微服务发送给客户端等。
  • ERROR 在其他阶段发生错误时执行该过滤器。

除了默认的过滤器类型,Zuul还允许我们创建自定义的过滤器类型。自定义(PRE)如下: ZuulFilter 是Zuul中核心组件,通过继承该抽象类,覆写几个关键方法达到自定义调度请求的作用

package com.yj.zuul.filter;

import java.util.Enumeration;
import java.util.Vector;

import javax.servlet.http.HttpServletRequest;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;

public class TokenFilter extends ZuulFilter {

	private final static Logger logger = LoggerFactory.getLogger(TokenFilter.class);

	@Override
	public boolean shouldFilter() {
		// 是否执行该过滤器,此处为true,说明需要过滤
		return true;
	}

	@Override
	public Object run() {
		logger.info("This is pre-type zuul filter.");
		RequestContext requestContext = RequestContext.getCurrentContext();
		HttpServletRequest request = requestContext.getRequest();
		
		requestContext.addZuulRequestHeader("aaa", "aaa");
		System.out.println("zuul:");
		Enumeration headerNames = request.getHeaderNames();
		while (headerNames.hasMoreElements()) {
			String key = (String) headerNames.nextElement();
			String value = request.getHeader(key);
			System.out.println(key + ":" + value);
		}
		String accessToken = request.getHeader("accessToken");
		if (StringUtils.isNotBlank(accessToken)) {
			requestContext.setSendZuulResponse(true); // 放行
			requestContext.setResponseStatusCode(200);
			requestContext.set("isSuccess", true);
			return null;
		} else {
			requestContext.setSendZuulResponse(false);// 使用Zuul来过滤这次请求
			requestContext.setResponseStatusCode(401);
			requestContext.setResponseBody("token is empty");
			requestContext.set("isSuccess", false);
			return null;
		}
	}

	@Override
	public String filterType() {
		// pre:路由之前
		// routing:路由中
		// post:路由后
		// error:发生错误时
		return "pre";
	}

	@Override
	public int filterOrder() {
		// filter执行顺序,通过数字指定 ,优先级为0,数字越大,优先级越低
		return 0;
	}

	public static void main(String[] args) {
		Vector v = new Vector();
		v.addElement("Lisa");
		v.addElement("Billy");
		v.addElement("Mr Brown");
		Enumeration e = v.elements();// 返回Enumeration对象
		while (e.hasMoreElements()) {
			String value = (String) e.nextElement();// 调用nextElement方法获得元素
			System.out.print(value);
		}
	}
}
  • filterType()方法是该过滤器的类型;
  • filterOrder()方法返回的是执行顺序;
  • shouldFilter()方法则是判断是否需要执行该过滤器;
  • run()则是所要执行的具体过滤动作。

过滤器之间并不会直接进行通信,而是通过RequestContext来共享信息,RequestContext是线程安全的。 开启过滤器 在程序的启动类 添加 Bean

@Bean
public TokenFilter tokenFilter() {
	return new TokenFilter();
}

禁用过滤器 格式为:zuul.[filter-name].[filter-type].disable=true 如:

zuul.TokenFilter.pre.disable=true

@EnableZuulServer和@EnableZuulProxy

  • @EnableZuulProxy包含@EnableZuulServer的功能,不会自动加载任何代理过滤器。
  • 当我们需要运行一个没有代理功能的Zuul服务,或者有选择的开关部分代理功能时,那么需要使用 @EnableZuulServer替代 @EnableZuulProxy。 这时候我们可以添加任何 ZuulFilter类型实体类都会被自动加载

Cookie和头部信息

默认情况下,Zuul在请求路由时不会过滤掉HTTP请求头信息中的一些敏感信息(包括Cookie、Set-Cookie、Authorization),这些敏感信息对于下游服务是没有用处的且易导致下游服务头信息混乱,可以通过zuul.sensitiveHeaders参数来过滤掉这些敏感信息,防止这些敏感信息回到调用者手中,可以设置的属性有Cookie、Set-Cookie、Authorization

1.全局设置

zuul: 
  sensitiveHeaders: Cookie,Set-Cookie,Authorization

2.指定路由名设置

 zuul:
  routes:
    users:
      path: /myusers/**
      sensitiveHeaders: Cookie,Set-Cookie,Authorization
      url: https://downstream

如果全局配置和路由配置均有不同粒度的过滤,那么采取就近原则,路由配置的过滤规则将生效

实践一下,我们在Zuul的application.properties文件,设置放行Authorization,即zuul.sensitiveHeaders属性中去掉Authorization

zuul.sensitiveHeaders=Cookie,Set-Cookie

 可以观察到,我们由Zuul请求Feign的路由时,Feign项目中可以从HttpServletRequest对象中获取到头部的Authorization了。

这里说的是从Zuul到下游项目的一次转发,在微服务系统中,为了保证微服务系统的安全,常常使用jwt来鉴权,但是服务内部的相互调用呢?采用Feign的拦截器。

在Feign中开启了hystrix,hystrix默认采用的是线程池作为隔离策略。线程隔离有一个难点需要处理,即隔离的线程无法获取当前请求线程的Jwt,这用ThredLocal类可以去解决,但是比较麻烦,所以我才用的是信号量模式。
在application.yml配置文件中使用一下配置:

hystrix.command.default.execution.isolation.strategy=SEMAPHORE

写一个Feign的拦截器,Feign在发送网络请求之前会执行以下的拦截器,代码如下:

import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.stereotype.Component;


@Component
public class JwtFeignInterceptor implements RequestInterceptor {

    private final String key = "Authorization";

    @Override
    public void apply(RequestTemplate template) {

        if (!template.headers().containsKey(key)) {
            String currentToken = UserUtils.getCurrentToken();
            if (!StrUtil.isEmpty(currentToken)){
                template.header(key, currentToken);
            }
        }
    }
}

微服务内部jwt的传递方式,https://blog.csdn.net/qq_24313635/article/details/103944694

动态路由

参照原先的Config-Client项目,其实就是动态刷新配置文件,开始改造。

①ZuulApplication文件,新增

@Bean
@RefreshScope
@ConfigurationProperties("zuul")
public ZuulProperties zuulProperties(){
    return new ZuulProperties();
}

②Zuul的pom.xml文件添加依赖

<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-config</artifactId>
	</dependency>
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>

 ③新建bootstrop.properties文件

server.port=8008

spring.application.name=zuul
eureka.client.serviceUrl.defaultZone=http://admin:123456@127.0.0.1:8001/eureka,http://admin:123456@127.0.0.1:8002/eureka
eureka.instance.prefer-ip-address=true
eureka.instance.instance-id=${spring.cloud.client.ipAddress}:${server.port}
eureka.instance.hostname=${spring.cloud.client.ipAddress}

spring.cloud.config.discovery.enabled=true
spring.cloud.config.discovery.service-id=config-server
spring.cloud.config.label=zuul
spring.cloud.config.profile=dev
spring.cloud.config.username=admin
spring.cloud.config.password=123456

spring.rabbitmq.host=192.168.190.131
spring.rabbitmq.port=5672
spring.rabbitmq.username=yj
spring.rabbitmq.password=123456

 ④配置中心的目标文件夹下,新建zuul文件夹,zuul文件夹内新建zuul-dev.properties文件

zuul.routes.api-feign.path=/feign666/**
zuul.routes.api-feign.service-id=feign

准备完毕,开始测试,我们在配置文件内新增路由配置,如图所示,然后调用配置中心的refresh端点,刷新配置,发现

http://192.168.174.1:8008/feign666/hiFeign?name=yj

 调用成功了,其实就是配置中心动态刷新配置文件的原理

Zuul 高可用

Zuul 现在既然作为了对外的第一入口,那肯定不能是单节点,对于 Zuul 的高可用有以下两种方式实现。

  1. Eureka 高可用:部署多个 Zuul 节点,并且都注册于 Eureka(有一个严重的缺点:那就是客户端也得注册到 Eureka 上才能对 Zuul 的调用做到负载,这显然是不现实的。)
  2. 基于 Nginx 高可用:在调用 Zuul 之前使用 Nginx 之类的负载均衡工具进行负载,这样 Zuul 既能注册到 Eureka ,客户端也能实现对 Zuul 的负载

zuul配合nginx实现高可用

①分别启动8008,8009两个端口的zuul实例

②nginx.conf配置

worker_processes  1;

events {
    worker_connections  1024;
}

http {
     upstream zuul{
	 server 192.168.190.131:8008;
	 server 192.168.190.131:8009;
    }
    server{
        listen       8010;
 
        location / {
            proxy_pass  http://zuul;
        }
    } 
}

③我们访问

http://192.168.37.142:8010/api-ribbon/hiRibbon?name=yj&token=123
或者
http://192.168.37.142:8010/api-feign/hiFeign?name=yj&token=123

显示hi yj ,i am from port:8004

附:SpringCloud之系列汇总跳转地址

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

猎户星座。

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值