Spring Cloud Zuul

网关是介于客户端(外部调用方比如app,h5)和微服务的中间层。

Zuul是Netflix开源的微服务网关,核心是一系列过滤器。这些过滤器可以完成以下功能。

  1. 是所有微服务入口,进行分发。
  2. 身份认证与安全。识别合法的请求,拦截不合法的请求。
  3. 监控。在入口处监控,更全面。
  4. 动态路由。动态将请求分发到不同的后端集群。
  5. 压力测试。可以逐渐增加对后端服务的流量,进行测试。
  6. 负载均衡。也是用ribbon。
  7. 限流(望京超市)。比如我每秒只要1000次,1001次就不让访问了。
  8. 服务熔断

zuul默认集成了:ribbon和hystrix。

基本使用

1
2
3
4
5
6
7
8
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
1
2
3
4
5
6
7
@EnableZuulProxy
@SpringBootApplication
public class ZuulApplication {
public static void main(String[] args) {
SpringApplication.run(ApplicationTest.class, args);
}
}
1
2
3
eureka.client.service-url.defaultZone=http://euk1.com:7001/eureka/
spring.application.name=zuulserver
server.port=80

通过http://localhost:80/consumer/alive就能够达到负载均衡Consumer微服务的作用,其中consumer是注册eureka的服务名,aliveController的接口。

相关配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#修改consumer的负载均衡策略,consumer是注册微服务的名称
#xxxxxxx.ribbon.NFLoadBalancerRuleClassName
consumer.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.RandomRule

management.endpoints.web.exposure.include=*
management.endpoint.health.show-details=always
management.endpoint.health.enabled=true
management.endpoint.routes.enabled=true

#配置consumer微服务前缀
zuul.routes.consumer=/xxoo/**

#类似nginx转发
zuul.routes.xx.path=/xx/**
zuul.routes.xx.url=http://mashibing.com

#忽略微服务
zuul.ignored-services=consumer

#所有微服务前缀指定
zuul.prefix=/api/v1

#是否带上前缀转发
zuul.strip-prefix=false

相关配置

1
2
3
4
5
6
7
8
9
10
11
12
13
#路由端点
management:
endpoints:
web:
exposure:
include: "*"
endpoint:
health:
##默认是never
show-details: ALWAYS
enabled: true
routes:
enabled: true
1
2
3
4
#负载均衡策略,默认是轮训
api-driver:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule

路由转发

1
2
3
4
#1.通过服务名配置
zuul:
routes:
api-driver: /zuul-api-driver/**
1
2
3
4
5
6
#2.自定义命名配置
zuul:
routes:
custom-zuul-name: #此处名字随便取
path: /zuul-custom-name/**
url: http://localhost:9002/ #这样的话 ribbon和hystrix 就都失效了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#3.基于2,恢复ribbon+hystrix
zuul:
routes:
#此处名字随便取
custom-zuul-name:
path: /zuul-custom-name/**
service-id: no-eureka-api-driver

no-eureka-api-driver:
ribbon:
listOfServers: localhost:9003,localhost:9002
ribbon:
eureka:
enabled: false
1
2
3
4
5
6
7
#4.指定serviceId
zuul:
routes:
#此处名字随便取
custom-zuul-name:
path: /zuul-custom-name/**
service-id: api-driver
1
2
3
4
5
6
#4.自己转发自己
zuul:
routes:
xxx:
path: /a-forward/**
url: forward:/myController
1
2
3
4
5
6
#忽略微服务serviceId访问,只能用url访问
zuul:
routes:
api-driver: /zuul-api-driver/**
ignored-services:
- api-driver
1
2
3
4
5
#前缀,访问时带上前缀,实际请求会将前缀去掉。
zuul:
prefix: /api
# 是否移除前缀
strip-prefix: true

三个问题

1
2
3
4
Zuul的三个问题:
1.Token不向后传 (配置sentinel)
2.老项目改造中路由问题
3.动态路由
1
2
3
4
5
#sensitive-headers是一个敏感header集合,在里面的key不会传到下游中
zuul:
#以下配置,表示忽略下面的key=token请求头信息,不向微服务传播
#以下配置为空表示:所有请求头都透传到后面微服务。
sensitive-headers: token

过滤器

Zuul的大部分功能都是有过滤器实现的。4种过滤器

1
2
3
4
5
6
7
8
9
10
11
PRE: 在请求被路由之前调用,可利用这种过滤器实现身份验证。选择微服务,记录日志、鉴权、限流

ROUTING:在将请求路由到微服务调用,用于构建发送给微服务的请求,并用http client(或者ribbon)请求微服务。
有三种类型的Filter:
1.RibbonRoutingFilter 路由到服务
2.SimpleHostRoutingFilter 路由到URL
3.SendForwardFilter 转发(转向Zuul自己)

POST:在调用微服务执行后。可用于添加header,记录日志,将响应发给客户端。

ERROR:在其他阶段发生错误是,走此过滤器。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//过滤器的执行顺序
try {
this.preRoute();
} catch (ZuulException var13) {
this.error(var13);
this.postRoute();
return;
}

try {
this.route();
} catch (ZuulException var12) {
this.error(var12);
this.postRoute();
return;
}

try {
this.postRoute();
} catch (ZuulException var11) {
this.error(var11);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
//自定义过滤器
@Component
public class MyFilter extends ZuulFilter {
//过滤器类型,四选一
@Override
public String filterType() {
return FilterConstants.ROUTE_TYPE;
}

//该过滤器的执行顺序,越小越先执行
@Override
public int filterOrder() {
return 0;
}

//返回该过滤器是否执行
@Override
public boolean shouldFilter() {
//通过它来判断是否继续执行过滤器
RequestContext ctx = RequestContext.getCurrentContext();
if (!ctx.sendZuulResponse()) {
return false;
}
return true;
}

//过滤器具体业务逻辑
@Override
public Object run() throws ZuulException {
RequestContext currentContext = RequestContext.getCurrentContext();
HttpServletRequest request = currentContext.getRequest();


// 设置这个请求最终不会被zuul转发到后端服务器
// 但是如果当前Filter后面还存在其他Filter,那么其他Filter仍然会被调用
// 所以shouldFilter会再做多一层判断,是否不继续执行filter
currentContext.setSendZuulResponse(false);
return null;
}
}

路由映射

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
//自定义过滤器
@Component
public class MyFilter extends ZuulFilter {
@Override
public String filterType() {
return FilterConstants.ROUTE_TYPE;
}

@Override
public int filterOrder() {
return 0;
}

@Override
public boolean shouldFilter() {
return true;
}

@Override
public Object run() throws ZuulException {
RequestContext currentContext = RequestContext.getCurrentContext();
HttpServletRequest request = currentContext.getRequest();

//将 serviceA/test/aaa请求 转发到 serviceA/test/bbb请求中
String uri = request.getRequestURI();
if(uri.contains("/aaa")){
//设置新的serviceName和url
currentContext.set(FilterConstants.SERVICE_ID_KEY,"serviceA");
currentContext.set(FilterConstants.REQUEST_URI_KEY,"/test/bbb");
//设置全路径映射
//currentContext.set(new URI("http://localhost:8003/test/bbb").toURL());
}
return null;
}
}

动态路由

1
2
3
ZUUL404找不到地址,需要排查请求地址的来源:
1.eureka服务
2.zuul的配置文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//自定义过滤器
@Component
public class MyFilter extends ZuulFilter {
@Override
public String filterType() {
return FilterConstants.ROUTE_TYPE;
}

@Override
public int filterOrder() {
return 0;
}

@Override
public boolean shouldFilter() {
return true;
}

@Override
public Object run() throws ZuulException {
RequestContext currentContext = RequestContext.getCurrentContext();
HttpServletRequest request = currentContext.getRequest();

//将 serviceA/test/aaa请求 转发到 serviceA/test/bbb请求中
String uri = request.getRequestURI();
if(uri.contains("/aaa")){
//设置全路径映射
currentContext.set(new URI("http://localhost:8003/test/bbb").toURL());
}
return null;
}
}

灰度发布

灰度发布(又名金丝雀发布)是指在黑与白之间,能够平滑过渡的一种发布方式。在其上可以进行A/B testing,即让一部分用户继续用产品特性A,一部分用户开始用产品特性B,如果用户对B没有什么反对意见,那么逐步扩大范围,把所有用户都迁移到B上面来。

网关级别

1
2
3
4
5
6
<!-- 实现灰度测试关键包 -->
<dependency>
<groupId>io.jmnarloch</groupId>
<artifactId>ribbon-discovery-filter-spring-cloud-starter</artifactId>
<version>2.1.0</version>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Component
public class GrayFilter extends ZuulFilter {
@Override
public String filterType() {
return FilterConstants.ROUTE_TYPE;
}

@Override
public int filterOrder() {
return 0;
}

@Override
public boolean shouldFilter() {
return true;
}

@Override
public Object run() throws ZuulException {
//查库,然后根据逻辑设置灰度
RibbonFilterContextHolder.getCurrentContext().add("version", "v1");
return null;
}
}
1
2
3
4
5
#下游服务配置
eureka:
instance:
metadataMap: #元信息
version: v1 #服务版本标识 会访问配置上相同的version的服务
1
2
3
4
5
注意事项:
- 通过以上配置,启动各服务,可以实现灰度测试和版本测试
- 在网关依据灰度和版本请求标识,Ribbon利用各服务的元信息进行匹配,以实现过滤和负载
- 服务中必须配置相应的请求标识,否则该请求无法负载,将会报错
- 关闭组件,ribbon.filter.metadata.enabled=false #默认true

微服务级别

1
2
3
4
5
6
public class GrayRibbonConfiguration {
@Bean
public IRule ribbonRule(){
return new GrayRule();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Aspect
@Component
public class RequestAspect {

@Pointcut("execution(* com.kuro.apipassenger.controller..*Controller*.*(..))")
private void anyMehtod(){
}

@Before(value = "anyMehtod()")
public void before(JoinPoint joinPoint){

HttpServletRequest request = ((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getRequest();
String version = request.getHeader("version");

//灰度规则匹配的地方...
if(version.trim().equals("v1")) {
//底层用了ThreadLocal存储信息
RibbonFilterContextHolder.getCurrentContext().add("version","v1");
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class GrayRule extends AbstractLoadBalancerRule {

/**
* 根据用户选出一个服务
* @param iClientConfig
* @return
*/
@Override
public void initWithNiwsConfig(IClientConfig iClientConfig) {
}

@Override
public Server choose(Object key) {
return choose(getLoadBalancer(),key);
}

public Server choose(ILoadBalancer lb, Object key){

//获取灰度规则
String version = RibbonFilterContextHolder.getCurrentContext().get("version");

// 获取所有可达的服务
List<Server> reachableServers = lb.getReachableServers();

// 根据用户选服务
for (int i = 0; i < reachableServers.size(); i++) {
Server server = reachableServers.get(i);
// 获取调用方的metadata配置
//eureka.instance.metadata-map.version = v1
Map<String, String> metadata = ((DiscoveryEnabledServer) server).getInstanceInfo().getMetadata();
String version1 = metadata.get("version");

// 服务的meta也有了,用户的version也有了。
if (version.trim().equals(version1)){
return server;
}
}
return null;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class ApiPassengerApplication {

public static void main(String[] args) {
SpringApplication.run(ApiPassengerApplication.class, args);
}

@LoadBalanced
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}

@GetMapping("/test")
public String test(){
System.out.println("api-passenger-test");
return "api-passenger";
}
}
1
2
3
4
5
#下游服务配置
eureka:
instance:
metadataMap: #元信息
version: v1 #服务版本标识 会访问配置上相同的version的服务

容错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
@Component
public class MyFallback implements FallbackProvider{

/**
* 表明为哪个微服务提供回退
* 服务Id ,若需要所有服务调用都支持回退,返回null 或者 * 即可
*/
@Override
public String getRoute() {
return "*";
}

@Override
public ClientHttpResponse fallbackResponse(String route, 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;
return HttpStatus.BAD_REQUEST;
}

@Override
public int getRawStatusCode() throws IOException {
//return status.value();
return HttpStatus.BAD_REQUEST.value();
}

@Override
public String getStatusText() throws IOException {
//return status.getReasonPhrase();
//return HttpStatus.BAD_REQUEST.name();
return HttpStatus.BAD_REQUEST.getReasonPhrase();
}

@Override
public void close() {
}

@Override
public InputStream getBody() throws IOException {
String msg = "{\"msg\":\"服务故障\"}";
return new ByteArrayInputStream(msg.getBytes());
}

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

网关限流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import com.google.common.util.concurrent.RateLimiter;
@Component
public class LimitFilter extends ZuulFilter {

@Override
public String filterType() {
return FilterConstants.PRE_TYPE;
}

@Override
public int filterOrder() {
return -10;
}

@Override
public boolean shouldFilter() {
return true;
}
// 下发令牌
// 2 qps(1秒 2个 请求 Query Per Second 每秒查询量)
private static final RateLimiter RATE_LIMITER = RateLimiter.create(50);

@Override
public Object run() throws ZuulException {
RequestContext currentContext = RequestContext.getCurrentContext();

if (RATE_LIMITER.tryAcquire()){
System.out.println("通过");
return null;
}else {
// 被流控的逻辑
System.out.println("被限流了");
currentContext.setSendZuulResponse(false);
currentContext.setResponseStatusCode(HttpStatus.TOO_MANY_REQUESTS.value());
}
return null;
}
}

服务限流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import com.google.common.util.concurrent.RateLimiter;
//Servlet的过滤器
@Component
public class LimitFilter implements Filter {

// 2=每秒2个;0.1 = 10秒1个
private static final RateLimiter RATE_LIMITER = RateLimiter.create(1);

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {

// 限流的业务逻辑
if (RATE_LIMITER.tryAcquire()){
filterChain.doFilter(servletRequest,servletResponse);
}else {

servletResponse.setCharacterEncoding("utf-8");
servletResponse.setContentType("text/html; charset=utf-8");

PrintWriter pw = null;

pw = servletResponse.getWriter();
pw.write("限流了");
pw.close();
}
}

@Override
public void init(FilterConfig filterConfig) throws ServletException {
}

@Override
public void destroy() {
}
}

Sentinel网关限流

1
2
3
4
5
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-annotation-aspectj</artifactId>
<version>1.4.1</version>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@SpringBootApplication
@EnableZuulProxy
public class CloudZuulApplication {
public static void main(String[] args) {
init(); SpringApplication.run(CloudZuulApplication.class, args);
}

private static void init() {
//所有限流规则的合集
List<FlowRule> rules = new ArrayList<>();
FlowRule rule = new FlowRule();
//资源名称
rule.setResource("hello");
//限流类型
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
//2 qps
rule.setCount(2);

rules.add(rule);
FlowRuleManager.loadRules(rules);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import com.alibaba.csp.sentinel.Entry;
@Component
public class SentinelFilter extends ZuulFilter {

@Override
public String filterType() {
return FilterConstants.PRE_TYPE;
}

@Override
public int filterOrder() {
return -10;
}

@Override
public boolean shouldFilter() {
return true;
}


@Override
public Object run() throws ZuulException {
//限流使用的令牌
Entry entry = null;
try {
entry = SphU.entry("hello");
//业务逻辑
System.out.println("正常请求");
} catch (Exception e) {
System.out.println("阻塞住了");
//限流逻辑
} finally {
if (entry != null) {
entry.exit();
}
}
return null;
}
}

Service限流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@SpringBootApplication
@EnableZuulProxy
public class CloudZuulApplication {
public static void main(String[] args) {
init(); SpringApplication.run(CloudZuulApplication.class, args);
}

private static void init() {
//所有限流规则的合集
List<FlowRule> rules = new ArrayList<>();
FlowRule rule = new FlowRule();
//资源名称
rule.setResource("hello");
//限流类型
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
//2 qps
rule.setCount(2);

rules.add(rule);
FlowRuleManager.loadRules(rules);
}

@Bean
public SentinelResourceAspect sentinelResourceAspect() {
return new SentinelResourceAspect();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
@Service
public class SentinelService {

@SentinelResource(value = "SentinelService.success", blockHandler = "fail")
public String success() {
return "正常请求";
}

public String fail(BlockException e) {
return "阻塞请求";
}
}

Spring Cloud Zipkin

sleuth用来收集分布式微服务的调用链。

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>

sleuth收集跟踪信息通过http请求发送给zipkin server,zipkin将跟踪信息存储,以及提供RESTful API接口,zipkin ui通过调用api进行数据展示,Zipkin内部集成了sleuth

默认内存存储,可以用mysql,ES等存储。

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
1
2
3
4
5
6
7
8
spring:
#zipkin
zipkin:
base-url: http://localhost:9411/
#采样比例1 代表100%的请求都采集
sleuth:
sampler:
rate: 1

官网下载zipkin的服务端后启动,默认端口就是9411,通过访问http://localhost:9411/即可。

Spring Cloud Admin

采集日志默认使用logback。

服务端

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-server</artifactId>
<version>2.2.1</version>
</dependency>
<!-- Admin 界面 -->
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-server-ui</artifactId>
</dependency>
1
2
3
4
5
6
7
8
@SpringBootApplication
@EnableAdminServer
public class AdminApplication {

public static void main(String[] args) {
SpringApplication.run(AdminApplication.class, args);
}
}

客户端

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-client</artifactId>
<version>2.2.1</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
1
2
3
management.endpoints.web.exposure.include=*
management.endpoint.health.show-details=always
spring.boot.admin.client.url=http://localhost:8080

邮件通知

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
spring.mail.host=
spring.mail.username=
spring.mail.password=
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true
spring.mail.properties.mail.smtp.starttls.required=true
# 发送给谁
spring.boot.admin.notify.mail.to=
# 是谁发送出去的
spring.boot.admin.notify.mail.from=

management.health.mail.enabled=false

最后更新: 2021年01月24日 21:58

原始链接: https://midkuro.gitee.io/2020/06/27/springcloud-zuul/

× 请我吃糖~
打赏二维码