Spring Cloud GateWay-Netflix Zuul

本文详细介绍了Zuul网关的引入及其在微服务架构中的作用,涵盖了Zuul的服务搭建、路由配置、过滤器机制及配置管理。重点探讨了如何利用Zuul实现动态路由、过滤请求和响应,以及如何通过自定义过滤器实现AB测试和日志串联,同时提供了超时配置等实用技巧。

目录

1 网关引入与Zuul

1.1 网关引入

1.2 Zuul介绍

2  Zuul服务搭建

3 Zuul 路由

3.1 默认配置

3.1.1 DiscoverClient方式

3.1.2 SpringCloud增强版的Rest Template

3.1.3 Netflix Feign

3.2 手动配置

3.2.1   手动配置实例

3.2.3 其他配置

3.3 静态配置

3.4 动态配置

4 Zuul过滤器

5  前置过滤器

5.1 Zuul前置过滤器-生成TraceId

5.2 客户端使用TraceId

5.3 客户端RestTemplate拦截器

6 后置过滤器

7 路由过滤器

7.1 AB test的实现

7.1.1 定义后置过滤器

7.1.2 获取AB test的配置信息

7.1.3 路由策略

7.1.4 获取新服务地址

7.1.5 转发路由

7.1.6 测试

7.2 关于其他策略

8 Zull相关配置

8.1 超时配置


1 网关引入与Zuul

1.1 网关引入

1、两大功能

主要包括 路由和过滤器 两大功能。主要功能体现在过滤器,实现“横切关注点”水平切分各个服务的公共部分,如日志记录等功能,作为服务来提供。

2、Zull和nginx区别

(1)动态注册和注销 客户端服务时,网关服务可以立刻生效。Nginx需要通过修改配置文件才能生效。

(2)横切服务关注点

1.2 Zuul介绍

在没有网关之前,客户端服务包括两个步骤:

  • 从EurekaServer获取一个客户端服务的所有地址
  • 通过Ribbon负载均衡选择一个机器进行访问

 

引入网关之后,客户端通过 “http://gateway/服务ID/url” URL格式调用网关,网关执行包括两个步骤:

  • 使用服务ID查找客户端服务
  • 使用Netflix Ribbon对调用服务进行负载均衡。

 

2  Zuul服务搭建

1、pom.xml

1

2

3

4

<dependency>

<groupId>org.springframework.cloud</groupId>

<artifactId>spring-cloud-starter-zuul</artifactId>

</dependency>

2、使用注解@EnableZuulProxy

1

2

3

4

5

6

7

8

@SpringBootApplication(scanBasePackages = {"com.example"})

@EnableZuulProxy

public class DemoApplication {

 

    public static void main(String[] args) {

        SpringApplication.run(DemoApplication.class, args);

    }

}

3、配置文件

1

2

3

4

5

6

eureka.client.serviceUrl.defaultZone=http://localhost:8761/eureka/

server.port=8763

spring.application.name=gateway

eureka.instance.status-page-url=http://localhost:${server.port}/swagger-ui.html

 

management.security.enabled=false

4、启动之后

 

5、可以通过 http://localhost:8763/routes 查看网关当前路由信息

49450231

如果出现“There was an unexpected error (type=Unauthorized, status=401).”,解决该问题就是添加一个配置如下:

1

management.security.enabled=false

3 Zuul 路由

3.1 默认配置

URL的格式如下,默认情况下都是Eureka上注册的服务ID。

1

http://gateway/服务ID/url

  • gateway,网关服务在Eureka注册的服务ID
  • 服务ID,客户端服务在Eureka注册的服务ID
  • url,访问客户端服务的url。

3.1.1 DiscoverClient方式

1、使用DiscoveryClient

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

@Service

public class DiscoveryClientService {

    private static final Logger LOGGER = LoggerFactory.getLogger(DiscoveryClientService.class);

    @Autowired

    private DiscoveryClient discoveryClient;

 

    public String queryWithGateWay(){

        // 1.获取网关服务的IP

        List<ServiceInstance> instances = discoveryClient.getInstances("gateway");

        StringBuilder url = new StringBuilder("http://");

        url.append(instances.get(0).getHost() + ":" + instances.get(0).getPort());

        // 2.添加服务名称

        url.append("/service-prodvider1");

        // 3.添加查询url

        url.append("/query");

 

        RestTemplate restTemplate = new RestTemplate();

        String response = restTemplate.getForObject(url.toString(), String.class);

        LOGGER.info("query with discovery client :{}", response);

        return response;

    }

}

2、测试

1

2

3

4

5

6

7

8

9

@RequestMapping(value = "/testDiscoveryWithGateWay", method = RequestMethod.GET)

    @ResponseBody

    public ResponseDemo testDiscoveryWithGateWay() {

        String response = discoveryClientService.queryWithGateWay();

        ResponseDemo demo = new ResponseDemo();

        demo.setMessage(response);

        demo.setResultCode("000");

        return demo;

    }

3、执行结果

 

3.1.2 SpringCloud增强版的Rest Template

1、使用网关来访问微服务

1

2

3

4

5

6

7

8

9

10

11

12

13

14

    public String queryWithGateWay() {

 

        StringBuilder url = new StringBuilder("http://");

        // 1.添加网关

        url.append("gateway");

        // 2.添加服务ID,不需要具体的IP和端口号

        url.append("/service-prodvider1");

        // 3.下游服务的接口

        url.append("/query");

 

        String response = restTemplate.getForObject(url.toString(), String.class);

        return response;

 

    }

2、测试

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

#################

ClientController.java

#################

    @Autowired

    private SpringCloudRestTemplateService springCloudRestTemplateService;

 

  @RequestMapping(value = "/testSpringCloudRestWithGateWay", method = RequestMethod.GET)

    @ResponseBody

    public ResponseDemo testSpringCloudRestWithGateWay() {

        String response = springCloudRestTemplateService.queryWithGateWay();

        ResponseDemo demo = new ResponseDemo();

        demo.setMessage(response);

        demo.setResultCode("000");

        return demo;

    }

3、执行结果

51317884

3.1.3 Netflix Feign

1、使用Feign调用网关

1

2

3

4

5

6

7

8

9

// 通过FeignClient指定网关

@FeignClient("gateWay")

// 指定服务ID

@RequestMapping("/service-prodvider1")

public interface FeignClientWithGateWayServcie {

    // 服务查询url

    @RequestMapping(value = "/query", method = RequestMethod.GET, consumes = "application/json")

    String query();

}

2、测试

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

#################

ClientController.java

#################

   @Autowired

   private FeignClientWithGateWayServcie feignClientWithGateWayServcie;

 

   @RequestMapping(value = "/testFeignGateWayClient", method = RequestMethod.GET)

    @ResponseBody

    public ResponseDemo testFeignGateWayClient() {

        String response = feignClientWithGateWayServcie.query();

        ResponseDemo demo = new ResponseDemo();

        demo.setMessage(response);

        demo.setResultCode("000");

        return demo;

    }

3、结果

51263227

3.2 手动配置

3.2.1   手动配置实例

1、在网关服务中的配置文件中添加

1

2

## 使用provieder1标识服务ID为service-prodvider1

zuul.routes.service-prodvider1:/provider1/**

此时重新启动网关服务,访问网关服务的 /routes 接口,如下。发现除了默认的根据eureka注册服务ID的映射之外,还有一个是自己手动配置映射记录。

49450231

2、客户端使用

在使用网关时,此时只需要把网关名字由服务ID(service-prodvider1)改为自己定义的名字(provider1)就可以了。如下

1

2

3

4

5

6

7

8

9

10

11

12

13

  public String queryWithGateWay() {

        StringBuilder url = new StringBuilder("http://");

        url.append("gateway");

 

        // 添加服务ID,此时自己新定义的名字

        url.append("/provider1");

 

        url.append("/query");

 

        String response = restTemplate.getForObject(url.toString(), String.class);

        return response;

 

    }

3.2.3 其他配置

1、取消默认配置

(1)取消一个服务的默认配置

1

zuul.ignored-services='service-prodvider1'

(2)取消所有服务的自动配置

1

zuul.ignored-services='*'

3.3 静态配置

对于Spring mvc和非JVM(python等)服务并没有在Eureka上注册,此时在网关进行配置路由时只能指定一台服务,无法使用Ribbon进行负载均衡。

1

2

zuul.routes.noJvmService.path:/noJvmService/**

zuul.routes.noJvmService.url:http://IP:PORT

解决方案就是将这两类服务注册到Eureka。后续会介绍这两类服务的注册。

3.4 动态配置

使用spring cloud或者apollo来进行配置路由。

4 Zuul过滤器

过滤器是网关GateWay 引入最重要的一点,实现了横切关注点,即将服务的一些功能移动到网关服务中,如添加traceID、验证/授权等。目前主要有前置过滤器、路由过滤器和后置过滤器 三种过滤器。如下图:

  • 前置过滤器。在Zull网关调用服务之前,对请求request进行处理。如添加traceId,请求的验证和授权。
  • 路由过滤器。在Zull网关调用服务之前,重新设置新的路由规则,可以覆盖zuul的路由配置。如A/B 测试。
  • 后置过滤器。在Zull网关调用服务之后,对response进行处理。如添加traceId。

 

三种过滤器区别就在于执行时间点不同,对于前置过滤器和路由过滤器是在Zuul网关调用下游服务前;对于后置过滤器是在Zuul网关调用下游服务之后。

三种过滤器共同点都是继承自ZuulFilter,需要实现四个方法:

  • filterType。值有:pre/post/route,对应的常量值为FilterConstants.PRE_TYPE、FilterConstants.POST_TYPE、FilterConstants.POST_TYPE。
  • filterOrder。filter执行顺序
  • shouldFilter。是否需要执行此过滤器。
  • run。过滤器执行逻辑

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

public class XXXFilter extends ZuulFilter {

    // 过滤器类型

    @Override

    public String filterType() {

        return "pre";

    }

 

    // 过滤器顺序

    @Override

    public int filterOrder() {

        return 1;

    }

 

    // 是否需要执行过滤器

    @Override

    public boolean shouldFilter() {

        return true;

    }

 

    //过滤器的执行逻辑

    @Override

    public Object run() {....}

}

5  前置过滤器

使用前置过滤器可以为各服务生成一个TraceId为例,TracId可以将各个服务的日志串联起来,方便查询问题。实现此串联TrancId的功能主要包括网关和客户端两部分:

(1)网关。负责在请求中生成traceID

  • Zuul前置过滤器。从http request中获取traceID,保存到上下文RequestContext中;如果traceId不存在就生成一个traceId中。

(2)客户端。负责从请求获取traceID、使用traceID、传递TranceID到下游服务

  • 过滤器Filter。从HTTP Request的头部获取TraceId.
  • 执行服务的业务逻辑。使用RequestContext中traceId,如打印日志。
  • 添加RestTemplate拦截器。在请求下游服务时,添加traceId,把TraceID传递到下游服务。

 

5.1 Zuul前置过滤器-生成TraceId

1、生成TraceId

在请求达到网关服务之后,首先判断request的header中是否存在traceId,如果存在就直接跳过,否则生成一个traceId,放到request的header中。

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

/**

* 需要覆盖四个方法。

*/

@Component

public class TraceIdFilter extends ZuulFilter {

    private static final String TRANCE_ID_KEY = "TRANCE_ID_KEY";

 

     // 过滤器类型

    @Override

    public String filterType() {

        return FilterConstants.PRE_TYPE;

    }

 

    // 过滤器顺序

    @Override

    public int filterOrder() {

        return 1;

    }

 

    // 是否需要执行过滤器

    @Override

    public boolean shouldFilter() {

        return true;

    }

 

    // 过滤器的执行逻辑

    @Override

    public Object run() {

        HttpServletRequest request = RequestContext.getCurrentContext().getRequest();

        // 1.判断请求中是否存在。如果存在,立刻返回

        if (isExistTraceId(request)) {

            return null;

        }

 

        // 2.不存在就新生成一个

        String traceId = UUID.randomUUID().toString();

        RequestContext context = RequestContext.getCurrentContext();

        context.addZuulRequestHeader(TRANCE_ID_KEY, traceId);

        return null;

    }

 

    private boolean isExistTraceId(HttpServletRequest request) {

        String value = request.getHeader(TRANCE_ID_KEY);

        if (StringUtils.isBlank(value)) {

            return false;

        }

        return true;

    }

}

注意:

  • RequestContext使用的是zuul提供的,不是Spring提供的。

1

com.netflix.zuul.context.RequestContext;

2、测试

在网关生成traceId之后,就可以在客户端通获取上面的traceId了

1

2

3

4

5

6

7

   @RequestMapping("/query")

   @ResponseBody

    public ViewVo query(HttpServletRequest request) {

        // 获取traceId

        String traceId = request.getHeader("TRANCE_ID_KEY");

        .......

    }

5.2 客户端使用TraceId

客户端可以通过过滤器获取请求head中的traceId,并把TraceId保存到上下文中;执行业务逻辑时就可以从这个上下文中获取traceId。

1、获取Http Request头部中的traceId

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

/**

* 从请求中获取关联ID,然后保存到上下文中

*/

@Component

public class ContextFilter implements Filter {

    @Override

    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,

                         FilterChain filterChain)

            throws IOException, ServletException {

 

        HttpServletRequest request = (HttpServletRequest) servletRequest;

        String traceId = request.getHeader("TRANCE_ID_KEY");

        if (!StringUtils.isEmpty(traceId)) {

            UserContextHolder.getContext().setLogId(traceId);

        }

        filterChain.doFilter(servletRequest, servletResponse);

    }

 

    @Override

    public void init(FilterConfig filterConfig) throws ServletException {

    }

 

    @Override

    public void destroy() {

    }

}

2、使用上下文中的traceId

1

String traceId = UserContextHolder.getContext().getLogId();

3、上下文和上下文Holder

(1)上下文

1

2

3

4

5

6

7

8

9

public class UserContext {

    private String logId;

    public String getLogId() {

        return logId;

    }

    public void setLogId(String logId) {

        this.logId = logId;

    }

}

(2)上下文Holder

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

public class UserContextHolder {

    private static ThreadLocal<UserContext> contextLocal = new ThreadLocal<>();

 

    public static UserContext getContext() {

        UserContext context = contextLocal.get();

        if (null == context) {

            UserContext ctx = new UserContext();

            contextLocal.set(ctx);

            return ctx;

        }

        return context;

    }

 

    public static void setContext(UserContext context) {

        contextLocal.set(context);

    }

}

5.3 客户端RestTemplate拦截器

1、定义SpringTemplate的拦截器,在发送的Request的头部添加tranceId

1

2

3

4

5

6

7

8

9

10

11

12

public class ContextInterceptor implements ClientHttpRequestInterceptor {

    @Override

    public ClientHttpResponse intercept(HttpRequest httpRequest, byte[] bytes,

                                        ClientHttpRequestExecution clientHttpRequestExecution) throws IOException {

        String traceId = UserContextHolder.getContext().getLogId();

        HttpHeaders headers = httpRequest.getHeaders();

        if (!StringUtils.isEmpty(traceId)) {

            headers.add("TRANCE_ID_KEY", traceId);

        }

        return clientHttpRequestExecution.execute(httpRequest, bytes);

    }

}

2、将新定义的拦截器加入到RestTemplate。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

@Configuration

public class RestTemplateConfig {

 

    @LoadBalanced

    @Bean

    public RestTemplate restTemplate() {

        RestTemplate restTemplate = new RestTemplate();

        List<ClientHttpRequestInterceptor> interceptors = restTemplate.getInterceptors();

        if (CollectionUtils.isEmpty(interceptors)) {

            restTemplate.setInterceptors(Lists.newArrayList(new ContextInterceptor()));

        } else {

            interceptors.add(new ContextInterceptor());

            restTemplate.setInterceptors(interceptors);

        }

        return restTemplate;

    }

}

3、测试

使用上面的RestTemplate,进行访问其他服务,如下代码。这个新定义的RestTemplate属于服务查找方式中“SpringCloud 增强版的RestTemplate”。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

@Service

public class SpringCloudRestTemplateService {

    @Autowired

    private RestTemplate restTemplate;

 

    public String query() {

 

        StringBuilder url = new StringBuilder("http://");

        // 添加服务ID,不需要具体的IP和端口号

        url.append("service-prodvider2");

        // 下游服务的接口

        url.append("/query");

 

        String response = restTemplate.getForObject(url.toString(), String.class);

        return response;

    }

}

6 后置过滤器

通过后置过滤器可以处理Response。如在response中添加traceID,如下:

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 PostFilter extends ZuulFilter {

    //  过滤器类型

    @Override

    public String filterType() {

        return FilterConstants.POST_TYPE;

    }

 

    // 过滤器顺序

    @Override

    public int filterOrder() {

        return 1;

    }

 

    // 是否需要执行过滤器

    @Override

    public boolean shouldFilter() {

        return true;

    }

 

    // 过滤器执行逻辑

    @Override

    public Object run() {

        RequestContext ctx = RequestContext.getCurrentContext();

        // 1.从head中获取traceId

        HttpServletRequest httpRequest = ctx.getRequest();

        String traceId = httpRequest.getHeader("TRANCE_ID_KEY");

 

        // 2.添加traceId到response中

        if (!StringUtils.isEmpty(traceId)) {

            HttpServletResponse response = ctx.getResponse();

            response.addHeader("TRANCE_ID_KEY", traceId);

        }

        return null;

    }

}

7 路由过滤器

路由过滤器功能就是可以覆盖Zuul原来的路由配置,重新路由请求。在实际中,灰度发布、Abtest都可以通过这个过滤器实现。

7.1 AB test的实现

这里以实现A/B test为例。首先介绍下AB test:

A/B test,或者说灰度发布。对于一次升级,并不想要一次让所有用户看到,而是循序渐进的让更多用户看到,这样也方便优化。灰色,介于白色和黑色之间,寓意一个功能即不是全部给用户看到也不是所有用户看不到,而是部分用户可见。

如下是一个简单Abtest的实现流程,主要包括如下步骤:

  • 获取Ab Test的配置信息。如服务的灰度机器列表信息等。
  • 校验是否需要走新的路由。AB test 的策略
  • 获取新的服务地址。需要考虑负载均衡
  • 转发路由。

83054971

7.1.1 定义后置过滤器

如下是路由过滤器定义,需要注意:

  • filterOrder()。返回的是SIMPLE_HOST_ROUTING_FILTER_ORDER – 1。SimpleHostRoutingFilter中改返回值是SIMPLE_HOST_ROUTING_FILTER_ORDER, 所以这里要减一,保证此过滤器在SimpleHostRoutingFilter之前执行。

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

@Component

public class RouteFilter extends ZuulFilter {

    private static final Logger LOGGER = LoggerFactory.getLogger(RouteFilter.class);

    @Autowired

    private ProxyRequestHelper helper;

 

    /**

     * 过滤器类型

     *

     * @return

     */

    @Override

    public String filterType() {

        return FilterConstants.ROUTE_TYPE;

    }

 

    /**

     * 过滤器顺序

     *

     * @return

     */

    @Override

    public int filterOrder() {

        //  SimpleHostRoutingFilter的值是SIMPLE_HOST_ROUTING_FILTER_ORDER,

        // 所以这里要减一,保证在SimpleHostRoutingFilter之前执行。

        return SIMPLE_HOST_ROUTING_FILTER_ORDER - 1;

    }

 

    /**

     * 是否需要执行过滤器。参考SimpleHostRoutingFilter实现

     *

     * @return

     */

    @Override

    public boolean shouldFilter() {

        return true;

    }

 

    @Override

    public Object run() {

        // 1.查询配置信息

        AbTestRoute route = queryAbTestRoute();

 

        // 2.校验是否命中智能路由

        boolean needRoute = checkNeedRoute(route);

        if (!needRoute) {

            return null;

        }

 

        // 3.获取一个目的服务地址

        String address = getAddress(route);

 

        // 4.转发请求

        forward(address);

 

        return null;

    }

}

7.1.2 获取AB test的配置信息

为了测试方便,这里把配置信息当成常量来写了,其实应该是调用配置中心来获取的。这里需要注意的有:

  • 需要有一个灰度开关。在不需要灰度,可以关闭次开关。

1

2

3

4

5

6

7

8

9

10

  // 可以查询配置服务(SpringCloudConfig或Apollo)来获取查询信息,这里为了测试,写成一个常量。

    private AbTestRoute queryAbTestRoute() {

        AbTestRoute route = new AbTestRoute();

        route.setAddress(Lists.newArrayList("localhost:8765"));

        // 标识90%进入到新服务

        route.setWeight(9);

        // 灰度测试开关打开

        route.setRouteSwitch(true);

        return route;

    }

对应配置信息类为:

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

public class AbTestRoute {

    // 灰度流量百分比

    private int weight;

    // 格式为 "IP:端口号"

    private List<String> address = Lists.newArrayList();

    // 灰度开关

    private boolean routeSwitch;

 

    public int getWeight() {

        return weight;

    }

 

    public void setWeight(int weight) {

        this.weight = weight;

    }

 

    public List<String> getAddress() {

        return address;

    }

 

    public void setAddress(List<String> address) {

        this.address = address;

    }

 

    public boolean getRouteSwitch() {

        return routeSwitch;

    }

 

    public void setRouteSwitch(boolean routeSwitch) {

        this.routeSwitch = routeSwitch;

    }

}

7.1.3 路由策略

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

// 生成随机数1到10随机数,设置的是9,如果随机数小于9就通过,所以此时通过率为90%

    private boolean checkNeedRoute(AbTestRoute route) {

        // 如果灰度开关关闭,则直接返回

        if(!route.getRouteSwitch()){

            return false;

        }

 

        Random random = new Random();

        int value = random.nextInt(10) + 1;

 

        if (route.getWeight() > value) {

            return true;

        } else {

            return false;

        }

    }

7.1.4 获取新服务地址

1

2

3

4

  // 从灰度服务的机器列表中,选择一个。这里需要负载均衡,为了方便直接返回1个

    private String getAddress(AbTestRoute route) {

        return route.getAddress().get(0);

    }

7.1.5 转发路由

用到了OkHttpClient,需要引入下面的maven包

1

2

3

4

5

<dependency>

    <groupId>com.squareup.okhttp</groupId>

    <artifactId>okhttp</artifactId>

    <version>2.7.5</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

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

/**

     * 转发请求到目的地址

     *

     * @param address 格式为:"IP:端口号"

     */

    private void forward(String address) {

        try {

            OkHttpClient httpClient = new OkHttpClient();

 

            RequestContext context = RequestContext.getCurrentContext();

            HttpServletRequest request = context.getRequest();

            String method = request.getMethod();

            String uri = this.helper.buildZuulRequestURI(request);

 

            // 对URI进行处理todo,修改为http://IP:端口/uri

            uri = "http://" + address + uri;

 

            Headers.Builder headers = new Headers.Builder();

            Enumeration<String> headerNames = request.getHeaderNames();

            while (headerNames.hasMoreElements()) {

                String name = headerNames.nextElement();

                Enumeration<String> values = request.getHeaders(name);

 

                while (values.hasMoreElements()) {

                    String value = values.nextElement();

                    headers.add(name, value);

                }

            }

            InputStream inputStream = request.getInputStream();

            RequestBody requestBody = null;

            if (inputStream != null && HttpMethod.permitsRequestBody(method)) {

                MediaType mediaType = null;

                if (headers.get("Content-Type") != null) {

                    mediaType = MediaType.parse(headers.get("Content-Type"));

                }

                requestBody = RequestBody.create(mediaType, StreamUtils.copyToByteArray(inputStream));

            }

            Request.Builder builder = new Request.Builder()

                    .headers(headers.build())

                    .url(uri)

                    .method(method, requestBody);

            Response response = httpClient.newCall(builder.build()).execute();

 

            LinkedMultiValueMap<String, String> responseHeaders = new LinkedMultiValueMap<>();

 

            for (Map.Entry<String, List<String>> entry : response.headers().toMultimap().entrySet()) {

                responseHeaders.put(entry.getKey(), entry.getValue());

            }

 

            this.helper.setResponse(response.code(), response.body().byteStream(),

                    responseHeaders);

 

            // prevent SimpleHostRoutingFilter from running

            // 阻塞SimpleHostRoutingFilter执行

            context.setRouteHost(null);

 

        } catch (Exception e) {

            LOGGER.error("执行错误:{}", e);

        }

    }

7.1.6 测试

1、客户端

1

2

3

4

5

6

7

8

9

@RequestMapping(value = "/testSpringCloudRestWithGateWay", method = RequestMethod.GET)

    @ResponseBody

    public ResponseDemo testSpringCloudRestWithGateWay() {

        String response = springCloudRestTemplateService.queryWithGateWay();

        ResponseDemo demo = new ResponseDemo();

        demo.setMessage(response);

        demo.setResultCode("000");

        return demo;

    }

默认是请求客户端服务Provider1,如下

1

2

3

4

5

6

7

8

9

10

11

12

13

14

public String queryWithGateWay() {

 

        StringBuilder url = new StringBuilder("http://");

        // 1.添加网关

        url.append("gateway");

        // 2.添加服务ID,不需要具体的IP和端口号

        url.append("/prodvider1");

        // 3.下游服务的接口

        url.append("/query");

 

        String response = restTemplate.getForObject(url.toString(), String.class);

        return response;

 

    }

2、Provider1提供的query接口逻辑如下

1

2

3

4

5

6

7

8

  @RequestMapping("/query")

    @ResponseBody

    public ViewVo query(HttpServletRequest request) {

        ViewVo vo = new ViewVo();

        vo.setName("provider1");

        vo.setDecription("provider1");

        return vo;

    }

3、Provider2提供的query接口逻辑如下

1

2

3

4

5

6

7

8

   @RequestMapping("/query")

    @ResponseBody

    public ViewVo query(HttpServletRequest request) {

        ViewVo vo = new ViewVo();

        vo.setName("provider2");

        vo.setDecription("provider2");

        return vo;

    }

4、返回结果为

81328642

7.2 关于其他策略

1、根据用户ID后两位实现AB test的路由策略

对于部分用户可以看到新功能或者新服务,除了上面的随机概率方式,我们还可以根据userId的后两位,如为00,10,20,30…90结尾用户可见,此时就占10%的用户可以看到了新服务了。

1

2

3

4

5

6

7

8

9

10

11

12

public class AbTestRoute {

    // 保存用户后两位信息,00,10,..90。

    private List<String> rule = Lists.newArrayList;

    // 格式为 "IP:端口号"

    private List<String> address = Lists.newArrayList();

    // 灰度开关

    private boolean routeSwitch;

    

    ..................

    getter和setter方法

    ..................

}

2、项目中也常会遇到,对于上线新产品,需要开小流量,即只有部分用户可以看到这个产品,这种场景就不需要通过Zull来完成了,只需要在代码添加开关就可以了。因为这些代码需要全量上线的。对于代码只上线部分机器,并进行观察的场景,可以考虑Zuul实现灰度

8 Zull相关配置

8.1 超时配置

1、全部服务

1

hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=2000  // 设置2s超时

2、指定服务

如果服务ID为provider1,则替换default,如下

1

hystrix.command.provider1.execution.isolation.thread.timeoutInMilliseconds=2000  // 设置2s超时

<think>嗯,用户现在遇到了Zuul路由转发错误和Eureka服务实例不可用的两个问题。之前他们提到了IllegalStateException: No instances available for eurekaprovider,现在结合新的引用内容,可能需要考虑多个方面。首先,用户引用了关于Zuul使用Ribbon的解决方法,这可能意味着需要检查Ribbon的配置以及与Eureka的集成是否正确。另外,引用[2]提到如果同时存在spring-boot-starter-web和Spring Cloud Gateway会导致冲突,但用户现在问的是Zuul的问题,可能需要注意依赖冲突的问题,尤其是是否错误引入了Gateway的依赖或者WebMvc相关的模块。 用户之前的问题中已经给出了Eureka服务不可用的解决方案,比如检查服务注册、消费者配置、依赖是否正确等。现在结合新的引用,可能需要考虑Zuul本身的配置是否正确,特别是Ribbon和Eureka的整合部分。比如,Zuul在转发请求时是否使用了服务名称,是否启用了负载均衡,以及是否有正确的Eureka客户端配置。 另外,用户提到的RibbonRoutingFilter forwarding error可能涉及到Zuul的路由配置是否正确,比如路由规则是否匹配,服务名称是否与Eureka中注册的一致。还需要检查Zuul的依赖是否完整,比如是否包含了Eureka和Ribbon的相关依赖,版本是否兼容。可能还需要考虑超时设置,如果服务实例存在但响应超时,也可能导致转发错误。 同时,用户之前的问题中提到了排除旧版本Ribbon依赖,这里可能需要确认Zuul所使用的Spring Cloud版本是否与Ribbon版本兼容,是否存在冲突的依赖需要排除。例如,如果使用的是较新的Spring Cloud版本,可能需要调整依赖管理,确保Zuul、Eureka和Ribbon的版本一致。 另外,用户提到的错误信息中是否有更详细的日志,比如具体的转发路径、服务实例的状态等,这些信息可以帮助进一步排查问题。例如,查看Zuul的日志确认转发请求的目标URL是否正确,是否成功从Eureka获取到可用的服务实例列表。 总结下来,解决这两个问题需要从以下几个方面入手:检查服务提供者是否成功注册到Eureka,确保Zuul的配置正确,特别是服务名称和路由规则,验证依赖是否正确且无冲突,检查Ribbon的配置和超时设置,以及确认Zuul和Eureka的版本兼容性。此外,查看详细的日志信息,定位具体的错误原因,比如是服务发现失败还是请求转发时的网络问题。</think>以下是解决Zuul路由转发错误与$java.lang.IllegalStateException: No instances available for eurekaprovider$异常的综合方案,结合您提供的引用内容和历史问题分析: --- ### 一、核心问题诊断 1. **服务实例不可用问题** - Zuul依赖Ribbon进行服务发现[^1],出现"No instances"表明未找到可用服务实例 - 常见原因:服务未注册、注册名不匹配、Eureka客户端配置错误 2. **路由转发错误** - 可能由以下原因引起: ```mermaid graph TD A[Zuul转发错误] --> B[依赖冲突] A --> C[路由配置错误] A --> D[Ribbon超时] A --> E[服务实例状态异常] ``` --- ### 二、分步解决方案 #### (一) 基础验证 1. **检查Eureka服务注册状态** ```bash # 查看eurekaprovider是否注册成功 curl http://localhost:8761/eureka/apps/EUREKAPROVIDER ``` 预期响应应包含实例元数据,如: ```xml <application> <name>EUREKAPROVIDER</name> <instance> <instanceId>host1:provider:8080</instanceId> <status>UP</status> </instance> </application> ``` 2. **验证Zuul配置** ```yaml zuul: routes: demo-service: path: /api/** serviceId: eurekaprovider # 必须与Eureka注册名一致 ribbon-isolation-strategy: THREAD # 推荐线程隔离模式[^3] ``` #### (二) 关键配置修正 1. **强制指定Ribbon配置** ```yaml eurekaprovider: ribbon: NIWSServerListClassName: com.netflix.niws.loadbalancer.DiscoveryEnabledNIWSServerList ConnectTimeout: 3000 ReadTimeout: 5000 MaxAutoRetries: 1 listOfServers: localhost:8080 # 开发环境应急配置 ``` 2. **排除依赖冲突**(针对引用[2]) ```xml <!-- zuul服务端需排除webmvc依赖 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-zuul</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </exclusion> </exclusions> </dependency> ``` #### (三) 代码层修复 1. **强制刷新服务列表(应急方案)** ```java @Autowired private DiscoveryClient discoveryClient; @GetMapping("/refresh") public void refreshServices() { discoveryClient.getServices().forEach(service -> discoveryClient.getInstances(service).forEach(instance -> System.out.println(instance.getServiceId() + ": " + instance.getUri()) ) ); } ``` --- ### 三、增强监控配置 ```yaml management: endpoints: web: exposure: include: "*" metrics: tags: application: ${spring.application.name} # Hystrix监控配置 hystrix: command: default: execution: isolation: thread: timeoutInMilliseconds: 10000 ``` --- ### 四、版本兼容矩阵 | 组件 | 推荐版本 | 注意事项 | |------|---------|----------| | Spring Cloud | Hoxton.SR12 | Zuul最后支持版本[^4] | | Ribbon | 2.3.0 | 需显式声明版本 | | Eureka Client | 1.10.17 | 保持与Server版本一致 | --- ### 五、高级排查工具 ```bash # 查看Ribbon服务列表 curl http://zuul-host:port/actuator/ribbon/serverList/eurekaprovider # 强制触发心跳(服务提供者) POST http://eureka-server:8761/eureka/apps/EUREKAPROVIDER/host1:provider:8080/status?value=UP ```
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值