前言:
当今,随着web2.0移动互联网的兴起,用户量的暴涨,各类网站应用的、各种APP规模也实现跨越式增长,随之而来的是各种高并发,海量数据处理的头疼问题,此时的系统架构为了使用时代,也被迫推陈出新。从互联网早期到现在,系统架构大体经历了下面几个过程:
单体应用架构--------垂直应用架构--------分布式架构--------SOA架构--------微服务架构
由于工作原因,需要对微服务灰度发布方面进行技术的预研与验证,顺便整理并形成实际文章,以便有所帮助。微服务涉及到的关键组件的功能在本案例不多做叙述。
灰度发布(又名金丝雀发布)是指在黑与白之间,能够平滑过渡的一种发布方式。在其上可以进行A/B testing,即让一部分用户继续用产品特性A,一部分用户开始用产品特性B,如果用户对B没有什么反对意见,那么逐步扩大范围,把所有用户都迁移到B上面来。灰度发布可以保证整体系统的稳定,在初始灰度的时候就可以发现、调整问题,以保证其影响度。
灰度发布开始到结束期间的这一段时间,称为灰度期。灰度发布能及早获得用户的意见反馈,完善产品功能,提升产品质量,让用户参与产品测试,加强与用户互动,降低产品升级所影响的用户范围。
注:相关代码已上传到资源里,可在本人主页资源内下载源码进行测试技术交流
本次方案选择SpringCloudAlibaba技术架构,具体采用 的是nacos+feign+SpringCloudGateway组合来实现灰度发布。当然也可以考虑采用 Dubbo+zookeeper方式进行服务的治理,来实现分布式服务的灰度发布,这里不多做体现。
2. 具体方案:
2.1 微服相关系统访问流程图解
下面基于 GateWay和 Nacos实现微服务架构灰度发布方案,首先对生产的服务和灰度环境的服务统一注册到 Nacos中,但是版本不同,比如生产环境版本为 1.0,灰度环境版本为 2.0,请求经过网关后,判断携带的用户是否为灰度用户,如果是将请求转发至 2.0的服务中,否则转发到 1.0的服务中,并且微服务之间的访问也能按照此规则进行,如果没有灰度环境,则默认选择正式环境。本方案技术代码与nacos安装说明已打包放在主页资源中,需要时可下载。
2.2 具体技术实现方案流程图解:
3. 源码文件:
所用工具: IDEA,mysql,nacos
3.1 整体构造
pom.xml引用内容可以从本人资源中下载查看
3.2 网关服务
具体代码可以从本人资源中下载
网关 application.yml配置:
server:
port: 10010
logging:
level:
com.ecpmisrv: debug
pattern:
dateformat: MM-dd HH:mm:ss:SSS
spring:
application:
name: gateway
cloud:
nacos:
server-addr: localhost:8848 # nacos地址
gateway:
routes:
- id: user-service # 路由标示,必须唯一
uri: lb://userservice # 路由的目标地址
predicates: # 路由断言,判断请求是否符合规则
- Path=/user/** # 路径断言,判断路径是否是以/user开头,如果是则符合
- id: order-service
uri: lb://orderservice
predicates:
- Path=/order/**
# filters:
# - AddRequestHeader=Truth,Itcast is-freaking awesome!
default-filters:
- AddRequestHeader=Truth,victory!
globalcors:
add-to-simple-url-handler-mapping: true #解决options 请求被拦截问题
cors-configurations:
'/[**]':
allowedOrigins: #允许哪些网站跨域请求
- "http://localhost:8090"
- "http://www.baidu.com"
allowedMethods:
- "GET"
- "POST"
- "DELETE"
- "PUT"
- "OPTIONS"
allowedHeaders: "*" #允许请求头中带的头信息
allowedCredenties: true #允许带Cookie
maxAge: 36000 #这次跨域请求有效期
pom.xml引用依赖:
cloud-demo
com.ecpmisrv.demo
1.0
4.0.0
gateway
8
8
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-discovery
org.springframework.cloud
spring-cloud-starter-gateway
org.springframework
spring-context
io.projectreactor
reactor-core
org.springframework.cloud
spring-cloud-starter-loadbalancer
网关 全链路流量标记
package com.ecpmisrv.gateway;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
//@Order {-1} // 也可以采用实现 Orderd 接口方式来实现
@Component
@Slf4j
public class AuthorizeFilter implements GlobalFilter, Ordered {
@Override
public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 1. 获取请求参数
ServerHttpRequest request = exchange.getRequest();
//获取请求头
HttpHeaders headers = request.getHeaders();
System.out.println("-网关服务------headers-----"+headers.toString());
// 2.或者判断是否是灰度用户 根据参数判断
//------------针对请求体中参数进行校验---------
// 2.1 使用客户端上传的版本参数或者使用redis缓存参数统一判断
// 2.2 如果针对指定用户,可以加载白名单,配置数据库链接进行数据查询,与客户端传递过来的用户信息比对,一致的则打上灰度标记
// 2.3 如果不针对特定用户或测试人员无法满足生产测试要求,可以采用nacos权重机制,分流,等待生产用户验证
MultiValueMap params = request.getQueryParams();
log.info("----params---"+params.toString());
// 2. 获取参数中的 authorization 参数
String auth = params.getFirst("grayUserFlag");
System.out.println("-网关服务---获取请求参数,判断是否灰度用户:"+auth);
// 3. 判断参数值是否等于 admin
if("YES".equals(auth)){
// 拦截并设置 灰度环境
//将灰度标记放入请求头中,放到后续链路中判断是否继续走其他的灰度服务
ServerHttpRequest tokenRequest = request.mutate()
//将灰度标记传递过去 param: version value: 2.0
.header("version","2.0")
.build();
ServerWebExchange build = exchange.mutate().request(tokenRequest).build();
System.out.println("网关服务build 将灰度标记放入请求头中:--"+build.toString());
grayscale("2.0"); //设置本地 ThreadLocal
return chain.filter(build);
}else {
// 放行 正常环境
return chain.filter(exchange);
}
// // 否 拦截
// exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
// return exchange.getResponse().setComplete();
}
/**
* 灰度流程
*/
private void grayscale(String version) {
if (StringUtils.isEmpty(version)) {
return;
}
if ("2.0".equals(version)) {
// 设置当前用户灰度的环境
GrayscaleThreadLocalEnvironment.setCurrentEnvironment("2.0");
}else{
// 设置当前环境为正式环境
GrayscaleThreadLocalEnvironment.setCurrentEnvironment("1.0");
}
}
@Override
public int getOrder() {
return -1;
}
}
负载均衡策略-全服务统一
package com.ecpmisrv.gateway.config;
import com.ecpmisrv.gateway.GrayscaleThreadLocalEnvironment;
import com.alibaba.cloud.nacos.ribbon.NacosServer;
import com.google.common.base.Optional;
import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.Server;
import com.netflix.loadbalancer.ZoneAvoidanceRule;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import java.util.ArrayList;
import java.util.List;
@Slf4j
public class GrayRule extends ZoneAvoidanceRule {
@Override
public void initWithNiwsConfig(IClientConfig clientConfig) {
}
@Override
public Server choose(Object key) {
try {
//从ThreadLocal中获取灰度标记
String version = GrayscaleThreadLocalEnvironment.getCurrentEnvironment();
System.out.println("网关服务 从ThreadLocal中获取灰度标记: "+version);
//获取所有服务
List serverList = this.getLoadBalancer().getAllServers();
System.out.println("-网关服务--------serverList---------"+serverList.toString());
//灰度发布的服务
List grayServerList = new ArrayList();
//正常的服务
List normalServerList = new ArrayList();
for(Server server : serverList) {
NacosServer nacosServer = (NacosServer) server;
//从nacos中获取元素剧进行匹配
if(nacosServer.getMetadata().containsKey("version")
&& nacosServer.getMetadata().get("version").equals("2.0")) {
grayServerList.add(server);
} else {
normalServerList.add(server);
}
}
System.out.println("-网关服务---grayServerList----"+grayServerList.toString());
System.out.println("-网关服务---normalServerList----"+normalServerList.toString());
//如果被标记为灰度发布,则调用灰度发布的服务
if("2.0".equals(version)) {
Server grayServer = originChoose(grayServerList,key);
if(null == grayServer || StringUtils.isEmpty(grayServer)){
log.info("无灰度服务或灰度服务列表中没有可用的服务,为保证服务能够正常进行,则将正式环境服务返回");
grayServer = originChoose(normalServerList,key);
}
return grayServer;
} else {
return originChoose(normalServerList,key);
}
} finally {
//清除灰度标记
GrayscaleThreadLocalEnvironment.setCurrentEnvironment("1.0");
}
}
private Server originChoose(List noMetaServerList, Object key) {
Optional server = getPredicate().chooseRoundRobinAfterFiltering(noMetaServerList, key);
System.out.println("-网关服务--noMetaServerList: "+noMetaServerList.toString());
if (server.isPresent()) {
return server.get();
} else {
return null;
}
}
}
3.3 订单服务
bootstrap.yml配置
spring:
application:
name: orderservice
# profiles:
# active: dev #环境空间
cloud:
nacos:
server-addr: localhost:8848 #nacos地址
discovery:
metadata:
version: 2.0 # 指定 是否灰度版本
# config:
# file-extension: yaml #文件格式
# namespace: c145eeab-fd60-408e-91c6-b94d2910422f
feign拦截器-灰度流量标记
package com.ecpmisrv.order.config;
import com.ecpmisrv.feign.config.reliance.GrayscaleThreadLocalEnvironment;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.Enumeration;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
@Component
@Slf4j
public class FeignRequestInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
HttpServletRequest httpServletRequest =getHttpServletRequest();
Map headers = getHeaders(httpServletRequest);
log.info("服务端微服务之间httpServletRequesteign调用headers: "+headers.toString());
for (Map.Entry entry : headers.entrySet()) {
//② 设置请求头到新的Request中
template.header(entry.getKey(), entry.getValue());
}
}
//获取请求对象
private HttpServletRequest getHttpServletRequest() {
try {
return ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
} catch (Exception e) {
//log.info("REQ1001","请求信息不能为空");
return null;
}
}
/**
* 获取原请求头
*/
private Map getHeaders(HttpServletRequest request) {
Map map = new LinkedHashMap<>();
Enumeration enumeration = request.getHeaderNames();
if (enumeration != null) {
while (enumeration.hasMoreElements()) {
String key = enumeration.nextElement();
String value = request.getHeader(key);
//将灰度标记的请求头透传给下个服务
if (key.equals("version")&&"2.0".equals(value)){
//① 保存灰度发布的标记
GrayscaleThreadLocalEnvironment.setCurrentEnvironment("2.0");
map.put(key, value);
}
}
}
return map;
}
}
3.4 用户服务
3.5 公共依赖
建议下载资源后,导入验证
4.测试验证
4.1 nacos验证与配置
本地安装nacos,注意本验证方案设置nacos端口号为8847。默认是8848。可以配置
本方案采用的是单个nacos,后期采用集群化管理,通过ngix发起调用,本方案中不多做验证
设置元数据
也可以通过各个服务代码中的yml文件配置来实现
4.2 测试客户端发起
正式环境
http://localhost:10010/order/101
测试效果
代码验证效果-网关服务日志打印,客户端对服务端orderservice正式环境服务进行调用
代码验证效果-orderserice服务8079接口日志打印,8080接口无日志打印。
同时orderservice服务又对userservice服务进行调用,获取正式环境服务
灰度环境
http://localhost:10010/order/101?grayUserFlag=YES
网关服务日志,验证请求的是orderservice灰度环境服务
Orderservice服务8080 日志打印,显示已经选取userservice的灰度环境服务
5. 总结
技术架构最终是以业务实现为目标,具体业务场景如何定义,如何做到架构最优解,能满足后期迭代升级,都需要各位码农同仁努力,有任何问题欢迎交流评论。