本文概览:介绍了Zuul服务搭建;Zuul的路由配置和过滤器两大功能,如下
- zuul四种路由配置。
- zuul三种过滤器:前置过滤器、路由过滤器、后置过滤器。三种过滤器只是执行点不同,都是继承自ZuulFilter。
- 使用zuul前置过滤器实现traceId
- 使用zuul路由过滤器实现Abtest
1 网关引入与Zuul
1.1 网关引入
1、两大功能
主要包括 路由和过滤器 两大功能。主要功能体现在过滤器,实现“横切关注点”,水平切分各个服务的公共部分,如日志记录等功能,作为服务来提供。
2、Zuul和nginx区别
zuul是在微服务建设中,和注册中心配合使用,即两者应用的场景是不同,zuul应用在微服务建设中,负责服务的路由转发(类似nginx),而且提供一些服务治理功能:
(1)在主从中心动态注册和注销 客户端服务时,网关服务可以立刻生效。Nginx需要通过修改配置文件才能生效。
(2)网关服务可以横切服务关注点,实现限流、熔断、划分机房、授权等服务治理功能。
3、网关服务和网关SDK比较
如果我们使用网关sdk,优点是节省了调用网关服务的时间,避免了网关服务成为瓶颈。缺点是sdk升级代价太大,需要引用SDK所有服务进行升级,那么每次一个限流SDK功能迭代都需要全部服务升级重启,显然是不合理的,而网关服务只需要升级自身服务就可以了。
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 查看网关当前路由信息
如果出现“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、执行结果
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、结果
3.2 手动配置
3.2.1 手动配置实例
1、在网关服务中的配置文件中添加
1 2 |
## 使用provieder1标识服务ID为service-prodvider1 zuul.routes.service-prodvider1:/provider1/** |
此时重新启动网关服务,访问网关服务的 /routes 接口,如下。发现除了默认的根据eureka注册服务ID的映射之外,还有一个是自己手动配置映射记录。
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 的策略
- 获取新的服务地址。需要考虑负载均衡
- 转发路由。
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、返回结果为
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超时 |