四.项目demo设计与规划
五.关键代码实现
第一步: 启动nacos服务.
nacos使用的是本地mysql数据库, 而我们的mysql是放在docker上的,所以,
首先启动mysql
docker start bdc382d8f7f8
然后启动nacos,这里nacos使用的版本是1.2.1
./startup.sh -m standalone
在浏览器输入http://localhost:8848/nacos, 登录后进入nacos管理后台
本次我们使用的全局配置主要是global.yml
其实里面就定义了一个配置, 灰度标签是否启用. gray_enable. 如果标签值为1, 表示启用; 为0,表示不启用. 这是一个整体的灰度规则
第二步: 启动灰度管理后台 -- gray-admin
我们的管理后台是gray-admin. 按照上面的规划
1. 注册到nacos, 并读取nacos灰度管理标签
2. 端口号设置为9000
3. bootstrap.yml配置文件
spring: cloud: nacos: config: server-addr: 127.0.0.1:8848 file-extension: yml namespace: 482c42bd-fba1-4147-a700-5b678d7c0747 group: ZUUL_TEST extension-configs[0]: data-id: global.yml group: GLOBAL_CONFIG
这里主要是golbal.yml配置文件
4. application.yml配置文件
server: port: 9000 spring: application: name: gray-admin datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3306/gray-admin?useUnicode=true&characterEncoding=utf-8 username: root password: 123456 # 服务模块 devtools: restart: # 热部署开关 enabled: true livereload: enabled: true # MyBatis mybatis: # 搜索指定包别名 typeAliasesPackage: com.lxl.admin # 配置mapper的扫描,找到所有的mapper.xml映射文件 mapperLocations: classpath*:mapper/*Mapper.xml # 加载全局的配置文件 configLocation: classpath:mapper/mybatis-config.xml
通过配置文件, 我们看到, 引入了mysql,也就是最终灰度规则等信息是保存在mysql进行持久化
5. 接下来看一下mysql的数据结构
一共有两张表: gray_rule和gray_relation
gray_rule: 保存的是灰度规则.
设计这张表的目的是: 对灰度规则进行统一管理. 目前都有哪些规则, 可以修改哪些规则
gray_relation: 保存的是服务器当前使用的灰度规则
设计这张表的目的是: 方便对服务器灰度规则进行管理. 比如服务器1, 当前使用的是什么规则? 已经对服务器进行立即灰度, 去灰, 断流, 优雅停服等操作.
表一: gray_rule
表二: gray_relation
6. 管理后台页面
管理后台需要启动, 使用的是fslayui
进入项目的根目录:
npm start
在页面浏览器输入一下地址
localhost:3000
a. 灰度规则管理页面
新增灰度规则,编辑规则, 启用/停用灰度规则,删除灰度规则
b. 微服务灰度服务管理页面--只显示设置了灰度规则的实例
这里可以关联灰度规则, 立即灰度, 服务灰度断流,服务去灰,优雅停服
7. 下面我们按照上面的设计规划来给服务器设置灰度规则
一共启动了这些服务,是不是很神奇, 我的电脑太强大了, 可以一下子其懂8个服务.
然后将端口号为8102, 8202, 8302设置为灰度服务器
首先:关联灰度规则
然后点击"立即灰度", 启用灰度
查看nacos服务器, 我们可以看到已经给服务器打上了灰度标签
这里的主要流程是, 通过页面操作给服务打上灰度标签.
3. 网关关键代码实现
package com.lxl.credit.gray; import com.fasterxml.jackson.databind.ObjectMapper; import com.lxl.credit.client.GrayClient; import com.netflix.zuul.ZuulFilter; import com.netflix.zuul.context.RequestContext; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpServletRequest; import java.util.*; import com.lxl.ribbon.config.*; import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.PRE_TYPE; /** * 判断请求是否应该走灰度服务器 */ public class GrayZuulFilter extends ZuulFilter { private static Logger log = LoggerFactory.getLogger(GrayZuulFilter.class); // 是否初始化过灰度规则, 全局只需要存一份 public static boolean initGray = false; // 是否启用灰度规则, 全局存一份 //@Value("${gray_enable}") public static int grayEnable=1; @Autowired GrayClient grayClient; // 灰度规则 public static List<Map<String, String>> grayRules = new ArrayList<>(); private static ObjectMapper mapper = new ObjectMapper(); /* @Autowired GrayService grayService;*/ /** * 这是一个前置过滤器 * @return */ @Override public String filterType() { return PRE_TYPE; } /** * 过滤的顺序是1 * @return */ @Override public int filterOrder() { return 1; } /** * 过滤器执行的条件 * 所有url链接全部需要走这个过滤器 * @return */ @Override public boolean shouldFilter() { return true; } /** * 过滤器执行的内容 * @return */ @Override public Object run() { // 第一步: 初始化灰度规则 if (!initGray) { //初始化灰度规则 getGrayRules(); } // 第二步: 获取请求头(包括请求的来源url和method) Map<String, String> headerMap = getHeadersInfo(); log.info("headerMap:{},grayRules:{}", headerMap, grayRules); // 删除之前的路由到灰度的标记 /* if (RibbonFilterContextHolder.getCurrentContext().getAttributes().get(GrayConstant.GRAY_TAG) != null) { RibbonFilterContextHolder.getCurrentContext().remove(GrayConstant.GRAY_TAG); }*/ //灰度开关关闭 -- 无需走灰度, 执行正常的ribbon负载均衡转发策略 if (grayEnable == 0) { log.info(">>>>>>>>>灰度开关已关闭<<<<<<<<<"); return null; } if (!grayRules.isEmpty()) { for (Map<String, String> grayRuleMap : grayRules) { try { // 获取本次灰度的标签,标签的内容是灰度的规则内容 String grayTag = grayRuleMap.get(GrayConstant.GRAY_TAG); // 第三步: 过滤有效的灰度标签 Map<String, String> resultGrayRuleMap = new HashMap<>(); //去掉值为空的灰度规则 grayRuleMap.forEach((K, V) -> { if (StringUtils.isNotBlank(V)) { resultGrayRuleMap.put(K, V); } }); resultGrayRuleMap.remove(GrayConstant.GRAY_TAG); //将灰度标签(规则)小写化 Map<String, String> lowerGrayRuleMap = transformUpperCase(resultGrayRuleMap); // 第四步: 判断请求头是否匹配灰度规则 if (headerMap.entrySet().containsAll(resultGrayRuleMap.entrySet()) || headerMap.entrySet().containsAll(lowerGrayRuleMap.entrySet())) { // 这是网关通讯使用的全局对象RequestContext RequestContext requestContext = RequestContext.getCurrentContext(); // 把灰度规则添加到网关请求头, 后面的请求都可以使用该参数 requestContext.addZuulRequestHeader(GrayConstant.GRAY_HEADER, grayTag); // 将灰度规则添加到ribbon的上下文 RibbonFilterContextHolder.getCurrentContext().add(GrayConstant.GRAY_TAG, grayTag); log.info("添加灰度tag成功:lowerGrayRuleMap:{},grayTag:{}", lowerGrayRuleMap, grayTag); } } catch (Exception e) { log.error("灰度匹配失败", e); } } } return null; } /** * 初始化灰度规则 */ private synchronized void getGrayRules() { try { if (!initGray) { // 未启用灰度规则, 返回 if (grayEnable == 0) { initGray = true; return; } // 获取在gray-admin-view中配置的所有可用的灰度规则 // 这里可能有多套灰度规则,所以是一个list // [{"areaCode":"010", "version": "1.0", "grayTag": "areaCode=010&version=1.0"}] grayRules = grayClient.getCurrentCrayRules(); initGray = true; } } catch (Exception e) { e.printStackTrace(); } } /** * 获取header map * 将请求头转换成map, 同时增加-path参数和-method参数 * @return */ private Map<String, String> getHeadersInfo() { Map<String, String> map = new HashMap<>(); /** * 获取请求的参数 */ RequestAttributes ra = RequestContextHolder.getRequestAttributes(); ServletRequestAttributes sra = (ServletRequestAttributes) ra; HttpServletRequest request = sra.getRequest(); map.put("-path", String.valueOf(request.getRequestURL())); map.put("-method", request.getMethod()); Enumeration headerNames = request.getHeaderNames(); while (headerNames.hasMoreElements()) { String key = (String) headerNames.nextElement(); String value = request.getHeader(key); map.put(key, value); } return map; } public static Map<String, String> transformUpperCase(Map<String, String> orgMap) { Map<String, String> resultMap = new HashMap<>(); if (orgMap == null || orgMap.isEmpty()) { return resultMap; } Set<String> keySet = orgMap.keySet(); for (String key : keySet) { String newKey = key.toLowerCase(); newKey = newKey.replace("_", ""); resultMap.put(newKey, orgMap.get(key)); } return resultMap; } }
这里的逻辑很清晰了
首先: 获取灰度规则标签. 什么时候获取呢? 第一次请求过来的时候, 去请求灰度标签. 放到全局的map集合中. 后面, 直接拿来就用
第二: 获取请求过来的header, 和灰度规则进行匹配, 如果匹配上了, 那么打灰度标签, 将其灰度请求头添加到请求上下文, 同时添加到ribbon请求的上下文中
接下来, 走feign实现header透传
4. feign关键代码实现
package com.lxl.ribbon.interceptor; import com.alibaba.fastjson.JSON; import com.lxl.ribbon.config.RibbonFilterContextHolder; import com.lxl.ribbon.constants.GrayConstant; import feign.RequestInterceptor; import feign.RequestTemplate; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpServletRequest; @Component public class FeignRequestInterceptor implements RequestInterceptor { private static Logger log = LoggerFactory.getLogger(FeignRequestInterceptor.class); @Override public void apply(RequestTemplate requestTemplate) { RequestAttributes ra = RequestContextHolder.getRequestAttributes(); //处理特殊情况 if (null == ra) { return; } ServletRequestAttributes sra = (ServletRequestAttributes) ra; HttpServletRequest request = sra.getRequest(); //处理特殊情况 if (null == request) { return; } log.info("[feign拦截器] ribbon上下文属性:{}", JSON.toJSONString(RibbonFilterContextHolder.getCurrentContext().getAttributes())); if (RibbonFilterContextHolder.getCurrentContext().getAttributes().get(GrayConstant.GRAY_TAG) != null) { RibbonFilterContextHolder.getCurrentContext().remove(GrayConstant.GRAY_TAG); } if (StringUtils.isNotBlank(request.getHeader(GrayConstant.GRAY_HEADER))) { log.info("灰度feign收到header:{}", request.getHeader(GrayConstant.GRAY_HEADER)); RibbonFilterContextHolder.getCurrentContext().add(GrayConstant.GRAY_TAG, request.getHeader(GrayConstant.GRAY_HEADER)); requestTemplate.header(GrayConstant.GRAY_HEADER, request.getHeader(GrayConstant.GRAY_HEADER)); } } }
其实feign的主要作用就是透传, 为什么要透传了呢? 微服务之间的请求, 不只是是首次定向的服务需要进行灰度, 那么后面服务内部相互调用也可能要走灰度, 那么最初请求的请求头就很重要了. 要一直传递下去.
而requestTemplate.header(GrayConstant.GRAY_HEADER, request.getHeader(GrayConstant.GRAY_HEADER));就可以实现参数在整个请求进行透传.
请求的参数带好了, 下面就要进行服务选择了, 有n台服务器, 到底要选择哪台服务器呢? 就是ribbon的负载均衡选择了
5. ribbon关键代码实现
package com.lxl.ribbon.rules; import com.alibaba.cloud.nacos.NacosDiscoveryProperties; import com.alibaba.cloud.nacos.ribbon.NacosServer; import com.alibaba.nacos.api.naming.NamingService; import com.alibaba.nacos.api.naming.pojo.Instance; import com.lxl.ribbon.config.RibbonFilterContext; import com.lxl.ribbon.config.RibbonFilterContextHolder; import com.lxl.ribbon.constants.GrayConstant; import com.lxl.ribbon.loadbalance.WeightedBalancer; import com.netflix.client.config.IClientConfig; import com.netflix.loadbalancer.AbstractLoadBalancerRule; import com.netflix.loadbalancer.BaseLoadBalancer; import com.netflix.loadbalancer.Server; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import java.util.*; /** * 根据元数据进行灰度规则匹配 */ public class MetadataBalancerRule extends AbstractLoadBalancerRule { private static Logger log = LoggerFactory.getLogger(MetadataBalancerRule.class); private static Random r = new Random(); @Autowired private NacosDiscoveryProperties nacosDiscoveryProperties; @Override public void initWithNiwsConfig(IClientConfig iClientConfig) { } /** * 实现父类的负载均衡规则 * * @param key * @return */ @Override public Server choose(Object key) { //return choose(getLoadBalancer(), key); try { // 调用父类方法, 获取当前的负载均衡器 BaseLoadBalancer loadBalancer = (BaseLoadBalancer) this.getLoadBalancer(); //获取当前的服务名 String serviceName = loadBalancer.getName(); log.info("[ribbon负载均衡策略] 当前服务名: {}", serviceName); //获取服务发现客户端 NamingService namingService = nacosDiscoveryProperties.namingServiceInstance(); // 获取指定的服务实例列表 List<Instance> allInstances = namingService.getAllInstances(serviceName); log.info("[ribbon负载均衡策略] 可用的服务实例: {}", allInstances); if (allInstances == null || allInstances.size() == 0) { log.warn("没有可用的服务器"); return null; } RibbonFilterContext context = RibbonFilterContextHolder.getCurrentContext(); log.info("MetadataBalancerRule RibbonFilterContext:{}", context.getAttributes()); Set<Map.Entry<String, String>> ribbonAttributes = context.getAttributes().entrySet(); /** * 服务分为三种类型 * 1. 设置为灰度的服务 --- 灰度服务 * 2. 先设置了灰度, 后取消了灰度的服务 --- 去灰服务 * 3. 普通服务-非灰服务 */ // 可供选择的灰度服务 List<Instance> grayInstances = new ArrayList<>(); // 非灰服务 List<Instance> noneGrayInstances = new ArrayList<>(); Instance toBeChooseInstance; if (!context.getAttributes().isEmpty()) { for (Instance instance : allInstances) { Map<String, String> metadata = instance.getMetadata(); if (metadata.entrySet().containsAll(ribbonAttributes)) { log.info("进行灰度匹配,已匹配灰度服务:{},灰度tag为:{}", instance, context.getAttributes().get(GrayConstant.GRAY_TAG)); grayInstances.add(instance); } else if (!StringUtils.isBlank(metadata.get(GrayConstant.GRAY_TAG))) { // 非灰度服务 noneGrayInstances.add(instance); } } } log.info("[ribbon负载均衡策略] 灰度服务: {}, 非灰服务:{}", grayInstances, noneGrayInstances); // 如果灰度服务不为空, 则走灰度服务 if (grayInstances != null && grayInstances.size() > 0) { // 走灰度服务 -- 从本集群中按照权重随机选择一个服务实例 toBeChooseInstance = WeightedBalancer.chooseInstanceByRandomWeight(grayInstances); log.info("[ribbon负载均衡策略] 灰度规则匹配成功, 匹配的灰度服务是: {}", toBeChooseInstance); return new NacosServer(toBeChooseInstance); } // 灰度服务为空, 走非断灰的服务 if (noneGrayInstances != null && noneGrayInstances.size() > 0) { // 走非灰服务 -- 从本集群中按照权重随机选择一个服务实例 toBeChooseInstance = WeightedBalancer.chooseInstanceByRandomWeight(noneGrayInstances); log.info("[ribbon负载均衡策略] 不走灰度, 匹配的非灰度服务是: {}", toBeChooseInstance); return new NacosServer(toBeChooseInstance); } else { log.info("未找到可匹配服务,实际服务:{}", allInstances); toBeChooseInstance = WeightedBalancer.chooseInstanceByRandomWeight(allInstances); log.info("[ribbon负载均衡策略] 未找到可匹配服务, 随机选择一个: {}", toBeChooseInstance); return new NacosServer(toBeChooseInstance); } } catch (Exception e) { e.printStackTrace(); } return null; } }
这里自定义实现了负载均衡策略. 首先判断这个请求是否走灰度? 然后从服务器中选择一台, 根据nacos的加权随机原则, 随机选择一台服务器
6. order服务关键代码
package com.lxl.order.controller; import com.lxl.order.client.CreditClient; import com.lxl.order.client.StockClient; import com.lxl.order.client.WmsClient; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @Slf4j @RestController @RequestMapping("order/") public class OrderController { @Autowired private CreditClient creditClient; @Autowired private StockClient stockClient; @Autowired private WmsClient wmsClient; @GetMapping("create") public String createOrder() { // 创建一个订单 log.info("[创建了一个订单]"); // 通知库存减库存 stockClient.reduceCredit(); // 通知积分系统加积分 creditClient.addCredit(); // 通知仓储系统发货 // wmsClient.pull(); return "订单创建完毕"; } }
order就是一个请求, 他通过feign调用了其他服务.
7.stock关键代码实现
下面我们模拟一个请求调用
package com.lxl.admin.controller; import com.lxl.admin.client.CreditClient; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @Slf4j @RequestMapping("stock/") public class StockController { @Autowired private CreditClient creditClient; @PostMapping("reduce") public String reduceCredit() { log.info("[减库存] 库存减1"); // 通知积分系统加积分 creditClient.addCredit(); return "减库存完成"; } }
这里就模拟了加积分分操作
也就是订单请求进来,调用了库存服务, 库存服务又调用了积分服务. 我们来观察是否会都走灰度服务器
8. 整体调用流程及效果
下面我们模拟一个请求调用