服务网关和路由

服务网关

分布式系统的横切关注点

  • cross-cutting concern

    image-20220418163309843
  • 安全

  • 日志记录

  • 用户跟踪

  • 服务网关(service gateway)

服务网关

  • 服务网关位于服务客户端和相应的服务实例之间

  • 所有服务调用(内部和外部)都应流经服务网关

  • 服务网关提供的能力

    • 静态路由

    • 动态路由

    • 验证和授权

    • 度量数据收集和日志记录

服务网关的实现Zuul

Zuul is a gateway service that provides dynamic routing, monitoring, resiliency, security, and more.

  • 将应用程序中的所有服务的路由映射到一个URL
  • 过滤器

image-20220418163912021

分布式系统关系图

image-20220418164423818

网关作用:一个中转站(外部访问、内部调用都要通过网关),所以普通用户调用时用的是网关的端口加上zuul.prefix

通过服务发现自动映射路由

  • 服务ID

  • 需要访问Eureka,有服务才会创建路由

  • 默认的映射路由为:(通过GEThttp://localhost:5555/routes获取)

    /服务ID/**: 服务ID

使用服务发现手动映射路由

  • 手动映射:confsvr中,修改zuulservice.yml配置:

    1
    2
    3
    4
    zuul.prefix:  /api
    zuul.routes.organizationservice: /organization/**
    zuul.routes.licensingservice: /licensing/**
    zuul.routes.authenticationservice: /auth/**

    此时的映射路由会添加(注意原先的还在,而且加上了前缀api

    /api/服务ID去掉service后缀/**:服务ID

    也就是对URL完成了缩短

使用静态URL手动映射路由

适用场景:非Java开发的项目(没有注册到服务发现中)

  • 静态URL是指向未通过Eureka服务发现引擎注册的服务的URL

  • 禁用Ribbon与Eureka集成,手动指定负载均衡的服务实例

1
2
zuul.routes.licensestatic.path: /注意加上zuul.prefix(如果有的话)/otherService/**
zuul.routes.licensestatic.url: http://otherService:8081
  • 负载均衡
1
2
3
4
5
6
# 定义一个 服务ID
zuul.routes.licensestatic.serviceId: licensestatic
# 多个实例(可能是非Java开发的)
zuul.routes.licensestatic.ribbon.listOfServers: http://licenseservice-static1:8081, http://licenseservice-static2:8082
# 此时就不需要ribbon了,所以要加上false
ribbon.eureka.enabled: false

动态重新加载路由配置

如果属性文件放在git仓库中,那么可以到仓库中来手动修改配置,且让Zuul能够重新加载这些配置,就可以发送POST请求到

POST:http://localhost:5555/refresh来让微服务主动刷新配置

设置超时

原因:

  • Hystrix的超时时间为1秒
  • Ribbon的超时时间为5秒
  • Ribbon的懒加载导致第一次调用慢

剩下的这些配置没讲

1
2
3
4
5
6
zuul.sensitiveHeaders: Cookie,Set-Cookie
zuul.debug.request: true
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds: 2500
#hystrix.command.licensingservice.execution.isolation.thread.timeoutInMilliseconds: 2
#licensingservice.ribbon.ReadTimeout: 2
signing.key: "345345fsdfsf5345"

过滤器

  • 使用Zuul和Zuul过滤器允许开发人员为通过Zuul路由的所有服务实现横切关注点

  • ZuulFilter

    • 前置过滤器,在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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
@Component
public class TrackingFilter extends ZuulFilter{
private static final int FILTER_ORDER = 1;
private static final boolean SHOULD_FILTER=true;
private static final Logger logger = LoggerFactory.getLogger(TrackingFilter.class);

@Autowired
FilterUtils filterUtils;

@Override
public String filterType() {
return FilterUtils.PRE_FILTER_TYPE;
}

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

public boolean shouldFilter() {
return SHOULD_FILTER;
}

private boolean isCorrelationIdPresent(){
if (filterUtils.getCorrelationId() !=null){
return true;
}

return false;
}

private String generateCorrelationId(){
return java.util.UUID.randomUUID().toString();
}

public Object run() {
// 获取关联ID(手动定义的)
if (isCorrelationIdPresent()) {
logger.debug("tmx-correlation-id found in tracking filter: {}. ", filterUtils.getCorrelationId());
}
else{ // 没有的话就设置一个(随机生成)
filterUtils.setCorrelationId(generateCorrelationId());
logger.debug("tmx-correlation-id generated in tracking filter: {}.", filterUtils.getCorrelationId());
}
// 获取请求上下文
RequestContext ctx = RequestContext.getCurrentContext();
logger.debug("Processing incoming request for {}.", ctx.getRequest().getRequestURI());
logger.debug("====incoming=========ServiceId=={}",filterUtils.getServiceId());
return null;
}
}

路由过滤器

动态路由

没细讲,跳过

后置过滤器

通过filterType来区分前置与后置

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
@Component
public class ResponseFilter extends ZuulFilter{
private static final int FILTER_ORDER=1;
private static final boolean SHOULD_FILTER=true;
private static final Logger logger = LoggerFactory.getLogger(ResponseFilter.class);

@Autowired
FilterUtils filterUtils;

@Override
public String filterType() {
return FilterUtils.POST_FILTER_TYPE;
}

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

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

@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();

logger.debug("Adding the correlation id to the outbound headers. {}", filterUtils.getCorrelationId());
ctx.getResponse().addHeader(FilterUtils.CORRELATION_ID, filterUtils.getCorrelationId());

logger.debug("Completing outgoing request for {}.", ctx.getRequest().getRequestURI());
logger.debug("====outgoing=========ServiceId=={}",filterUtils.getServiceId());
return null;
}
}

过滤器总结

主要功能:

  1. 当用户请求头中没有关联ID时,zuulservice会在前置过滤器随机生成一个(设为X),并传给licensing
  2. licensing的filter会获取到X并保存到线程中。
    • 若licensing需要向organization调用服务(我们只考虑这种情况,否则也不需要关联ID了),则在发送请求之前会由Hystrix获取线程中的X(见[2.2 licensing-service](### licensing-service)的第二点),然后再发送请求给zuulservice让他帮忙联系organization。(如果organization长时间未响应则会触发@HystrixCommand中具体参数的响应)
  3. zuulservice会在前置过滤器再次判断,但是这时候有关联ID了,就是这个X,然后带着这个X向organization发送请求
  4. organization接收到请求,像licensing一样保存这个IDX,返回数据,且带上X给zuulservice
  5. zuulservice此时会在后置过滤器中处理这个X,并传给licensing
  6. licensing处理完用户请求,并再次带着X返回结果给zuulservice
  7. licensing的处理结果会带着X再次进入zuulservice的后置过滤器,zuulservice会在最终返回给用户的response带上这个X
  8. 若用户请求头有关联ID也同理

具体实现

zuulsvr

  1. 添加依赖

    1
    2
    3
    4
     <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-zuul</artifactId>
    </dependency>
  2. 启动类加注解

    @EnableZuulProxy

  3. 编写相关配置

    application.yml

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    server:
    port: 5555
    #Setting logging levels
    logging:
    level:
    com.netflix: WARN
    org.springframework.web: WARN
    com.thoughtmechanix: DEBUG

    eureka:
    instance:
    preferIpAddress: true
    client:
    registerWithEureka: true # 服务网关也要注册到服务发现(erueka)中去
    fetchRegistry: true
    serviceUrl:
    defaultZone: http://localhost:8761/eureka/

    bootstrap.yml

    1
    2
    3
    4
    5
    6
    7
    8
    9
    spring:
    application:
    name: zuulservice
    profiles:
    active:
    default
    cloud:
    config:
    enabled: true

licensing-service

  1. 在调用organization-service的地方做出一些修改

    原先的调用代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public Organization getOrganization(String organizationId){
    ResponseEntity<Organization> restExchange =
    restTemplate.exchange(
    "http://organizationservice/v1/organizations/{organizationId}",
    HttpMethod.GET,
    null, Organization.class, organizationId);

    return restExchange.getBody();
    }

    修改成:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public Organization getOrganization(String organizationId){
    ResponseEntity<Organization> restExchange =
    restTemplate.exchange(
    // 这个URL要根据具体情况来改变
    // 如是否有api(即zuul.prefix)
    // 如organization是否有后缀(即手动配置路由))
    // 但是/v1及以后的是固定的(即真正的organization服务地址)
    "http://zuulservice/api/organization/v1/organizations/{organizationId}",
    HttpMethod.GET,
    null, Organization.class, organizationId);
    return restExchange.getBody();
    }
  2. 为了让Hystrix能够获取到线程中的关联ID,需要取消上节课中【支持Ribbon的RestTemplate的拦截器】(即UserContextInterceptor)前取消@Component注解;然后在启动类中主动添加@Bean@LoadBalanced

    看不懂也没关系,代码如下:

    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
    @SpringBootApplication
    @EnableEurekaClient
    @EnableCircuitBreaker
    public class Application {

    @LoadBalanced
    @Bean
    public RestTemplate getRestTemplate(){
    RestTemplate template = new RestTemplate();
    List interceptors = template.getInterceptors();
    if (interceptors==null){
    template.setInterceptors(Collections.singletonList(new UserContextInterceptor()));
    }
    else{
    interceptors.add(new UserContextInterceptor());
    template.setInterceptors(interceptors);
    }

    return template;
    }

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