前言:
当今,随着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: 10010logging:level:com.ecpmisrv: debugpattern:dateformat: MM-dd HH:mm:ss:SSSspring:application:name: gatewaycloud:nacos:server-addr: localhost:8848 # nacos地址gateway:routes:- id: user-service # 路由标示,必须唯一uri: lb://userservice # 路由的目标地址predicates:# 路由断言,判断请求是否符合规则- Path=/user/** # 路径断言,判断路径是否是以/user开头,如果是则符合- id: order-serviceuri: lb://orderservicepredicates:- 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 #允许带CookiemaxAge: 36000 #这次跨域请求有效期
pom.xml引用依赖:
cloud-democom.ecpmisrv.demo1.04.0.0gateway88com.alibaba.cloudspring-cloud-starter-alibaba-nacos-discoveryorg.springframework.cloudspring-cloud-starter-gatewayorg.springframeworkspring-contextio.projectreactorreactor-coreorg.springframework.cloudspring-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@Slf4jpublic class AuthorizeFilter implements GlobalFilter, Ordered {@Overridepublic 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. 判断参数值是否等于 adminif("YES".equals(auth)){// 拦截并设置灰度环境//将灰度标记放入请求头中,放到后续链路中判断是否继续走其他的灰度服务ServerHttpRequest tokenRequest = request.mutate()//将灰度标记传递过去param: versionvalue: 2.0.header("version","2.0").build();ServerWebExchange build = exchange.mutate().request(tokenRequest).build();System.out.println("网关服务build 将灰度标记放入请求头中:--"+build.toString());grayscale("2.0"); //设置本地 ThreadLocalreturn 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");}}@Overridepublic 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;@Slf4jpublic class GrayRule extends ZoneAvoidanceRule {@Overridepublic void initWithNiwsConfig(IClientConfig clientConfig) {}@Overridepublic 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@Slf4jpublic class FeignRequestInterceptor implements RequestInterceptor {@Overridepublic 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.1nacos验证与配置
本地安装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″ />
Orderservice服务8080 日志打印,显示已经选取userservice的灰度环境服务
5. 总结
技术架构最终是以业务实现为目标,具体业务场景如何定义,如何做到架构最优解,能满足后期迭代升级,都需要各位码农同仁努力,有任何问题欢迎交流评论。