Spring Cloud分区发布实践(6)--灰度服务-根据Header选择实例区域
此文是一个完整的例子, 包含可运行起来的源码.
此例子包含以下部分:
- 网关层实现自定义LoadBalancer, 根据Header选取实例
- 服务中的Feign使用拦截器, 读取Header
- Feign的LoadBalancer也是用网关一样的实现
- 使用Web Filter来统一设置header变量, 于业务解耦
自定义LoadBalancer, 读取Header
首先创建一个新模块 hello-mybalancerbyheader, pom文件如下
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.cnscud.betazone</groupId>
<artifactId>betazone-root</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>hello-mybalancerbyheader</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>hello-mybalancerbyheader</name>
<description>Demo project for Spring Boot</description>
<dependencies>
<dependency>
<groupId>com.cnscud.betazone</groupId>
<artifactId>hello-pubtool</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies>
</project>
创建一个 application.yml,
server:
port: 9200
spring:
application:
name: betazone-hello-mybalancerbyheader
main:
allow-bean-definition-overriding: true
cloud:
gateway:
discovery:
locator:
lowerCaseServiceId: true
enabled: true
routes:
- id: remotename
uri: lb://betazone-hello-remotename
predicates:
- Path=/remoteapi/**
filters:
- StripPrefix=1
loadbalancer:
ribbon:
enabled: false
eureka:
instance:
prefer-ip-address: true
client:
register-with-eureka: true
fetch-registry: true
prefer-same-zone-eureka: true
service-url:
defaultZone: http://localhost:8001/eureka/
logging:
level:
org.springframework.cloud: debug
com.cnscud.betazone: debug
代理了后面的betazone-hello-remotename服务, 启动, 访问 http://localhost:9200/remoteapi/remote/id/2 说明正常, 后面实例是不停轮询的方式来变化的.
那我们来如何根据header访问后面不同的实例哪? 方法有很多, 我们采用最简洁的办法, 抄袭一个Spring自己的 RoundRobinLoadBalancer,
(代码放在hello-pubtool模块, 一会要复用)
package com.cnscud.betazone.pub.zonebyheader;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.DefaultResponse;
import org.springframework.cloud.client.loadbalancer.EmptyResponse;
import org.springframework.cloud.client.loadbalancer.Request;
import org.springframework.cloud.client.loadbalancer.RequestDataContext;
import org.springframework.cloud.client.loadbalancer.Response;
import org.springframework.cloud.loadbalancer.core.NoopServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.core.ReactorServiceInstanceLoadBalancer;
import org.springframework.cloud.loadbalancer.core.SelectedInstanceCallback;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import org.springframework.http.HttpHeaders;
import reactor.core.publisher.Mono;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 根据header里面的workzone选择合适的实例, 如果没有发现, 则返回所有实例.
*
* A Round-Robin-based implementation of {@link ReactorServiceInstanceLoadBalancer}.
*
* @author Spencer Gibb
* @author Olga Maciaszek-Sharma
*/
public class MyBetaMainByHeaderLoadBalancer implements ReactorServiceInstanceLoadBalancer {
private static final Log log = LogFactory.getLog(MyBetaMainByHeaderLoadBalancer.class);
final String defaultKey = "default";
final AtomicInteger position;
final Map<String, AtomicInteger> postionMap = new HashMap<>();
final String serviceId;
ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;
/**
* @param serviceInstanceListSupplierProvider a provider of
* {@link ServiceInstanceListSupplier} that will be used to get available instances
* @param serviceId id of the service for which to choose an instance
*/
public MyBetaMainByHeaderLoadBalancer(ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider,
String serviceId) {
this(serviceInstanceListSupplierProvider, serviceId, new Random().nextInt(1000));
}
/**
* @param serviceInstanceListSupplierProvider a provider of
* {@link ServiceInstanceListSupplier} that will be used to get available instances
* @param serviceId id of the service for which to choose an instance
* @param seedPosition Round Robin element position marker
*/
public MyBetaMainByHeaderLoadBalancer(ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider,
String serviceId, int seedPosition) {
this.serviceId = serviceId;
this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider;
this.position = new AtomicInteger(seedPosition);
postionMap.put(defaultKey, this.position);
}
@SuppressWarnings("rawtypes")
@Override
// see original
// https://github.com/Netflix/ocelli/blob/master/ocelli-core/
// src/main/java/netflix/ocelli/loadbalancer/RoundRobinLoadBalancer.java
public Mono<Response<ServiceInstance>> choose(Request request) {
//read headers
HttpHeaders headers = ((RequestDataContext) request.getContext()).getClientRequest().getHeaders();
ServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider
.getIfAvailable(NoopServiceInstanceListSupplier::new);
return supplier.get(request).next()
.map(serviceInstances -> processInstanceResponse(supplier, serviceInstances, headers));
}
private Response<ServiceInstance> processInstanceResponse(ServiceInstanceListSupplier supplier,
List<ServiceInstance> serviceInstances,
HttpHeaders headers ) {
Response<ServiceInstance> serviceInstanceResponse = getInstanceResponse(serviceInstances, headers);
if (supplier instanceof SelectedInstanceCallback && serviceInstanceResponse.hasServer()) {
((SelectedInstanceCallback) supplier).selectedServiceInstance(serviceInstanceResponse.getServer());
}
return serviceInstanceResponse;
}
private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances,
HttpHeaders headers) {
if (instances.isEmpty()) {
if (log.isWarnEnabled()) {
log.warn("No servers available for service: " + serviceId);
}
return new EmptyResponse();
}
String workzone = headers.getFirst("workzone");
log.info("getInstanceResponse: workzone-> " + workzone);
String positionKey = defaultKey;
Map<String,String> zoneMap = new HashMap<>();
zoneMap.put("zone",workzone);
final Set<Map.Entry<String,String>> attributes =
Collections.unmodifiableSet(zoneMap.entrySet());
List<ServiceInstance> lastInstanceList = instances;
if(StringUtils.isNotBlank(workzone)) {
lastInstanceList = new ArrayList<>();
for (ServiceInstance instance : instances) {
Map<String, String> metadata = instance.getMetadata();
//根据zone头部判断
if (metadata.entrySet().containsAll(attributes)) {
lastInstanceList.add(instance);
}
}
//此处如果没有发现任何一个instance, 返回所有instance: 请根据自己情况定义
if(lastInstanceList.size() <=0){
lastInstanceList = instances;
}
else {
positionKey = workzone;
}
}
AtomicInteger mypos = postionMap.get(positionKey);
if( mypos == null) {
mypos = new AtomicInteger(new Random().nextInt(1000));
postionMap.put(positionKey, mypos);
}
int pos = Math.abs(mypos.incrementAndGet());
ServiceInstance instance = lastInstanceList.get(pos % lastInstanceList.size());
return new DefaultResponse(instance);
}
}
代码首先定义了一个postionMap, 用来存放不同区域的上次访问的位置, 避免每次都访问第一个实例. 然后获取Header
HttpHeaders headers = ((RequestDataContext) request.getContext()).getClientRequest().getHeaders();
然后读取 "workzone" 字段, 如果存在, 则遍历instances, 发现实例的metadata的zone如果和当前字段一样, 则加到列表里.
检查完成, 使用命中的列表进行轮询...如果没有实例, 则使用所有实例进行轮询.
实例化LoadBalancer(为了复用, 放在hello-pubtool模块里)
/**
* 定义配置, 引入LoadBalancer.
*
* @author Felix Zhang 2021-06-09 16:46
* @version 1.0.0
*/
public class MyBetaMainByHeaderLoadBalancerConfiguration {
@Bean
public ReactorLoadBalancer<ServiceInstance> reactorServiceInstanceLoadBalancer(Environment environment,
LoadBalancerClientFactory loadBalancerClientFactory) {
String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
return new MyBetaMainByHeaderLoadBalancer(
loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class), name);
}
}
声明配置(在hello-mybalancerbyheader模块里使用):
/**
* 声明配置类.
*/
@Configuration(proxyBeanMethods = false)
@LoadBalancerClients(defaultConfiguration = MyBetaMainByHeaderLoadBalancerConfiguration.class)
public class LoadBalancerAutoConfiguration {
}
启动应用, 我们来测试一下,
localhost:cnscud.github.io felixzhang$ curl -H 'workzone:beta' "http://localhost:9200/remoteapi/remote/id/2"
World [remotename: 127.0.0.1:9002]
localhost:cnscud.github.io felixzhang$ curl -H 'workzone:beta' "http://localhost:9200/remoteapi/remote/id/2"
World [remotename: 127.0.0.1:9002]
发现返回的都是beta区域的实例, 说明代码正确.
Feign实现根据Header访问不同区域的实例
第一步都是简单的, 我们看看Feign如何也能继承上一层的效果哪? 启用同样的LoadBalancer, 但是从哪读Header哪, 因为不是同一个Request, 显然是读不到的.
为了把代码区分出来, 不破坏原来的例子, 我们复制一个hello-nameservice模块到 hello-nameservicebyheader.
先打开上面一样的配置:
/**
* 配置声明类: .
*/
@Configuration(proxyBeanMethods = false)
@LoadBalancerClients(defaultConfiguration = MyBetaMainByHeaderLoadBalancerConfiguration.class)
public class LoadBalancerByHeaderAutoConfiguration {
}
但是这没什么用, 因为读不到Header.
然后我去网上搜了搜, 读了5*5=25篇文章之后.....中间过程省略50000字......
实现个Feign的拦截器
我们先看看原来的Controller代码,
@RequestMapping("/id/{userid}")
public String helloById(@PathVariable("userid") String userid, HttpServletRequest request) {
logger.debug("call helloById with " + userid);
logger.info("[nameservice] workzone header:" + request.getHeader("workzone"));
if (StringUtils.isNotBlank(userid) && StringUtils.isNumeric(userid)) {
return "hello " + feignRemoteNameService.readName(Integer.parseInt(userid)) + getServerName();
}
return "hello guest" + getServerName();
}
里面是可以读取到request.getHeader("workzone")的, 但是进入到feign的服务里面, 显然就读不到了, 而Feign的拦截器可以帮我们把Header放进去.
所以我们首先找个地方把这个header存起来,
因为不知道服务什么情况下会被调用, 有可能是异步, 有可能是线程池, 有可能....总之, 此处省略10000字, 我们实现一个:
实现一个Holder, 存放变量
/**
* Holder: store a String.
* !!!! 仅供参考, 没有全面测试过.
*
* 注意: 测试的几种情况要考虑: 父子线程, 线程池, 高QPS等等: InheritableThreadLocal, TransmittableThreadLocal, HystrixRequestVariableDefault
*
* @author Felix Zhang 2021-06-10 10:37
* @version 1.0.0
*/
public class RequestHeaderHolder {
//如果存Map, 好像有问题, 此处用String
private static final ThreadLocal<String> MYLOCAL;
static {
MYLOCAL = new InheritableThreadLocal();
//new TransmittableThreadLocal();
}
public static String get() {
return MYLOCAL.get();
}
public static void set(String value) {
MYLOCAL.set(value);
}
public static void remove() {
MYLOCAL.remove();
}
}
至于有没有问题, 因为不太好验证各种各种极限情况, 所以在实战中迎接战火吧, 总之也就InheritableThreadLocal/TransmittableThreadLocal/HystrixRequestVariableDefault这几个东西了.
然后我们在Controller把header存下来:
RequestHeaderHolder.set(request.getHeader("workzone"));
此处实现一个拦截器:
/**
* Feign Interceptor for transfer header.
*
* @author Felix Zhang 2021-06-10 10:04
* @version 1.0.0
*/
public class FeignRequest4ZoneHeaderInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
//获取之前设置的header
String workzone = RequestHeaderHolder.get();
if(workzone !=null){
template.header("workzone", workzone);
}
}
}
超级简单的拦截器, 把workzone这个header放到RequestTemplate的header里面去.
然后实例化拦截器
/**
* Interceptor configuration 声明拦截器配置.
*
* @author Felix Zhang 2021-06-10 11:09
* @version 1.0.0
*/
public class FeignByZoneHeaderConfig {
@Bean("myInterceptor")
public RequestInterceptor getRequestInterceptor() {
return new FeignRequest4ZoneHeaderInterceptor();
}
}
启用配置(全局声明):
@EnableFeignClients(basePackages = "com.cnscud.betazone.hellonameservicebyheader", defaultConfiguration = FeignByZoneHeaderConfig.class)
或者单独声明
@FeignClient(value = "betazone-hello-remotename", configuration = FeignByZoneHeaderConfig.class)
hello-mybalancerbyheader模块里添加路由:
- id: default
uri: lb://betazone-hello-nameservicebyheader
predicates:
- Path=/api/**
filters:
- StripPrefix=1
重新启动应用, 访问 , 发现生效了
localhost:cnscud.github.io felixzhang$ curl -H 'workzone:beta' "http://localhost:9200/api/remote/id/2"
hello World [remotename: 127.0.0.1:9002] [nameservice:127.0.0.1:8203]
localhost:cnscud.github.io felixzhang$ curl -H 'workzone:beta' "http://localhost:9200/api/remote/id/2"
hello World [remotename: 127.0.0.1:9002] [nameservice:127.0.0.1:8203]
返回的一直是beta区域的实例.
来个Web Filter
我们回想一下, 刚才
RequestHeaderHolder.set(request.getHeader("workzone"));
是写在Controller里面的, 这显然不合适, 这个谁也看不懂, 和业务也没啥关系, 那我们可以放到Filter里面去, Filter就很容易了, 就是个普通的Web Filter就行.
在hello-pubtool模块里创建ZoneHeaderFilter, 为了别处公用:
/**
* Web Filter.
*
* @author Felix Zhang 2021-06-10 16:45
* @version 1.0.0
*/
public class ZoneHeaderFilter extends OncePerRequestFilter {
private final Log logger = LogFactory.getLog(this.getClass());
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
logger.info("[ZoneHeaderFilter] workzone header:" + request.getHeader("workzone"));
//给Feign Client用的, 使用位置: FeignRequest4ZoneHeaderInterceptor
RequestHeaderHolder.set(request.getHeader("workzone"));
filterChain.doFilter(request, response);
}
}
这也太简单了....
在Feign的模块里声明 WebZoneHeaderFilterConfig:
/**
* 注入Filter.
*
* @author Felix Zhang 2021-06-10 17:05
* @version 1.0.0
*/
@Configuration
public class WebZoneHeaderFilterConfig {
@Bean
public FilterRegistrationBean<ZoneHeaderFilter> zoneheaderFilter() {
FilterRegistrationBean<ZoneHeaderFilter> registrationBean
= new FilterRegistrationBean<>();
registrationBean.setFilter(new ZoneHeaderFilter());
registrationBean.addUrlPatterns("/*");
return registrationBean;
}
}
收工了....
删除掉Controller里面的代码, 重新启动应用, 访问, 发现依然生效了
localhost:cnscud.github.io felixzhang$ curl -H 'workzone:beta' "http://localhost:9200/api/remote/id/2"
hello World [remotename: 127.0.0.1:9002] [nameservice:127.0.0.1:8203]
localhost:cnscud.github.io felixzhang$ curl -H 'workzone:beta' "http://localhost:9200/api/remote/id/2"
hello World [remotename: 127.0.0.1:9002] [nameservice:127.0.0.1:8203]
返回的一直是beta区域的实例.
收工.
感想:
网上的方法非常多, 概念非常多...可能是因为Spring Cloud的可定制性太强了, 组件太多了, 拦截器/Filter/各种配置/Rule/AOP/LoadBalancer各种都可以定制....
反观我的实现, 大部分是抄袭Spring自己的实现, 自己的代码没有几行, 这样我就放心多了...
注意
每个人的情况都不一样, 不同的组件, 不同的实现方式, 都可能造成不一样的效果, 所以仅供参考, 学到手才算真的好!
所有教程里的项目源码: https://github.com/cnscud/javaroom/tree/main/betazone2
感谢网上的各种文章, 太多了, 就不一一贴出来了. (很多写的不全, 过时, 所以读的时候也很费劲, 知识爆炸的时代学习成本也很高, 选择多了也不见得都是好事, 当然也是好事)
感谢
感谢感谢, 感谢帮助我的朋友, 家人. 感谢你们. 2021.6.10
Spring Cloud分区发布实践(6)--灰度服务-根据Header选择实例区域的更多相关文章
- Spring Cloud分区发布实践(2) 微服务
我们准备一下用于查询姓名的微服务. 首先定义一下服务的接口, 新建一个空的Maven模块hello-remotename-core, 里面新建一个类: public interface RemoteN ...
- Spring Cloud分区发布实践(1) 环境准备
最近研究了一下Spring Cloud里面的灰度发布, 看到各种各样的使用方式, 真是纷繁复杂, 眼花缭乱, 不同的场景需要不同的解决思路. 那我们也来实践一下最简单的场景: 区域划分: 服务分为be ...
- Spring Cloud分区发布实践(4) FeignClient
上面看到直接通过网关访问微服务是可以实现按区域调用的, 那么微服务之间调用是否也能按区域划分哪? 下面我们使用FeignClient来调用微服务, 就可以配合LoadBalancer实现按区域调用. ...
- Spring Cloud分区发布实践(3) 网关和负载均衡
注意: 因为涉及到配置测试切换, 中间环节需按此文章操作体验, 代码仓库里面的只有最后一步的代码 准备好了微服务, 那我们就来看看网关+负载均衡如何一起工作 新建一个模块hello-gateway, ...
- Spring Cloud分区发布实践(5)--定制ServiceInstanceListSupplier
现在我们简单地来定制二个 ServiceInstanceListSupplier, 都是zone-preference的变种. 为了方便, 我重新调整了一下项目的结构, 把一些公用的类移动到hello ...
- spring boot 2.0.3+spring cloud (Finchley)7、服务链路追踪Spring Cloud Sleuth
参考:Spring Cloud(十二):分布式链路跟踪 Sleuth 与 Zipkin[Finchley 版] Spring Cloud Sleuth 是Spring Cloud的一个组件,主要功能是 ...
- 厉害了,Spring Cloud Alibaba 发布 GA 版本!
? 小马哥 & Josh Long ? 喜欢写一首诗一般的代码,更喜欢和你共同 code review,英雄的相惜,犹如时间沉淀下来的对话,历久方弥新. 相见如故,@杭州. 4 月 18 日, ...
- Spring Cloud Alibaba发布第二个版本,Spring 发来贺电
还是熟悉的面孔,还是熟悉的味道,不同的是,这次的配方升级了. 今年10月底,Spring Cloud联合创始人Spencer Gibb在Spring官网的博客页面宣布:阿里巴巴开源 Spring Cl ...
- 基于Spring Cloud和Netflix OSS构建微服务,Part 2
在上一篇文章中,我们已使用Spring Cloud和Netflix OSS中的核心组件,如Eureka.Ribbon和Zuul,部分实现了操作模型(operations model),允许单独部署的微 ...
随机推荐
- 安装nodejs版本模块报错notsup Unsupported platform for n
使用npm install -g n报错 如果出现npm ERR! notsup Unsupported platform for n@6.7.0: wanted {"os":&q ...
- 关于equals()和hashcode()的一些约定
本文章主要讨论和回答一下几个问题: equals()的四大特性 equals()和hashcode()之间的关系,为什么我们经常说这两个方法要么都重写,要么都不重写? HashMap.HashSet等 ...
- [Usaco2018 Dec]Teamwork 题解
题目描述 题目描述 在Farmer John最喜欢的节日里,他想要给他的朋友们赠送一些礼物.由于他并不擅长包装礼物,他想要获得他的 奶牛们的帮助.你可能能够想到,奶牛们本身也不是很擅长包装礼物,而Fa ...
- 【译】Go:程序如何恢复?
原文:https://medium.com/a-journey-with-go/go-how-does-a-program-recover-fbbbf27cc31e 当程序不能正确处理错误时, 会 ...
- NameServer 核心原理解析
在之前的文章中,已经把 Broker.Producer 和 Conusmer 的部分源码和核心的机制介绍的差不多了,但是其实 RocketMQ 中还有一个比较关键但是我们平时很容易忽略的组件--Nam ...
- docker下创建redis cluster集群
概述 在Redis中,集群的解决方案有三种 主从复制 哨兵机制 Cluster Redis Cluster是Redis的分布式解决方案,在 3.0 版本正式推出. 准备工作 1.确定本机IP地址 2. ...
- svn创建新分支报错:svn: E155015: Aborting commit: XXX remains in conflict
用diea在对svn创建新分支的时候报错,错误为 svn: E155015: Aborting commit: XXX remains in conflict 百度和查阅资料后得知,此错误为分支被拉取 ...
- Sentinel流控与熔断
参考: https://thinkwon.blog.csdn.net/article/details/103770879 项目结构 com.guo ├── guo-sentinel ...
- 什么是BSE
BSE (bridge system engineer) 是外包开发人员和客户之前的桥梁. 主要是将客户的需求准确的理解并传达给外包的开发人员,一般情况下也兼开发的 leader 工作. 参考: ht ...
- Acunetix在SDLC中的安全性测试
DevOps只是害怕尝试新事物.它们用于Selenium测试,这些测试占用了管道并提供了难以解释的结果,但是与此同时,它们经常避开了DAST测试,这远没有那么麻烦. 由于他们的应用程序是完全用Java ...