nacos+ribbon+feign+gateway设计实现灰度方案

一. 架构设计

微服务架构图

架构原理 

1. 微服务系统在启动时将自己注册到服务注册中心,同时对外发布 Http 接口供其它系统调用(一般都是基于Spring MVC)

2、服务消费者基于 Feign 调用服务提供者对外发布的接口,先对调用的本地接口加上注解@FeignClient,Feign会针对 加了该注解的接口生成动态代理,服务消费者会针对 Feign 生成的动态代理去调用方法时,在底层会生成Http协议格式的请求,类似 /stock/deduct?productId=100

3、Feign 最终会调用Ribbon从本地的Nacos注册表的缓存里根据服务名取出服务提供在机器的列表,然后进行负载均衡 并选择一台机器出来,对选出来的机器IP和端口拼接之前生成的url请求,生成类似调用http接口地址 http://192.168.0.60:9000/stock/deduct?productId=100,最后基于HTTPClient调用请求

 

基于微服务架构的原理,来设计灰度方案

 

概要流程:

1. 全局配置灰度是否启用--在nacos中配置, 动态更新 
2. 配置灰度规则, version=2.0, class="1234567" maxClassId="010-1234567"
3. 设置灰度服务器, 哪些服务器是灰度服务器。 为其打标签
4. 启动所有服务, 服务在nacos上进行注册
5. 客户端发起请求, 带着header参数
6. zuul进行过滤,判断是否符合灰度条件, 如果符合,打上灰度标签
7. 通过feign将灰度标签进行透传
8. 通过ribbon选择跳转的服务器, 可以指定负载均衡策略
9. 下一个服务器继续跳转,带上feign的灰度标签,继续请求。

 

以上是这个灰度方案实现的整体逻辑和思路

二. 具体操作及规划

2.1 灰度的目标

不同的流量过来, 根据元数据匹配, 走不同的微服务

 当流量请求过来以后, 根据其匹配的灰度规则的不同, 走的服务有所不同, 可以将其分为三种类型.

1. 不匹配任何灰度规则, 则走无灰度服务

2. 匹配灰度规则, 则走对应的灰度服务

3. 同时匹配多个灰度规则, 选择灰度服务

 

2.2 设置灰度规则

1. 全局灰度标签设置在nacos中, nacos配置的灰度标签的开闭, 可实时自动更新同步. 

2. 灰度管理后台, 管理后台主要有两大块内容.

  1)  配置灰度规则

    1. 根据需要设置灰度规则, 比如: 城市, 大班, 小班, 版本号, 学科等, 

  2) 设置灰度服务器

    1. 调用nacos接口, 获取所有微服务ip+port

    2. 为灰度服务器打灰度标签

    3. 做同步策略, 当灰度服务标签内容有变化, 通知网关, 做相应更新

 

  

 2.3. 网关设置--拦截请求, 为其打灰度标签

网关其实就是各种各样的过滤器, 常用的过滤器类型有:pre:前置过滤器, routing: 路由过滤器, post过滤器, error过滤器

这里我们定义一个前置过滤器, 过滤所有 过来的请求, 判断其是否匹配灰度规则

 执行步骤:

1. 初始化灰度规则, 我们首先判断nacos中灰度规则是否启用, 启用则去灰度管理服务器获取有效的灰度规则

2. 判断请求头是否和某一灰度规则匹配, 如果匹配, 则将请求header添加到请求上下文, 后续feign进行透传. 同时添加到ribbon请求上下文, 做服务选择. 

 

 4. ribbon设置 -- 根据灰度规则, 选择灰度服务器

ribbon是客户端负载均衡, 通过对ribbon上下文中的灰度标签和微服务列表中灰度标签的比较, 来选择一台服务器, 作为目标跳转服务器

5. 自定义Feign拦截器, 实现参数(灰度标签)的透传

feign的实质是拦截器, feign将拦截所有的请求跳转, 主要作用是用来做header参数透传, 保证服务间的调用也可以正确选择灰度服务器.

三. 各组件功能原理

3.1 zuul网关

1. 标准的过滤器类型

pre:前置过滤器

  在请求被路由到原服务器之前, 要执行的过滤器

  • 认证 : 认证安全, 是否符合条件, 认证为安全的才能放过
  • 选路由: 当前这个请求来了, 应该调用后面的哪个微服务呢? A还是B
  • 请求日志: 请求日志, 日志来了, 写日志, 对其进行监控

routing: 路由过滤器

  处理将请求发送到源服务器的过滤器

post过滤器

  在响应从源服务器返回时要被执行的过滤器

  • 对响应增加http请求头: 要增加调试的header日志
  • 收集统计和度量: 这次请求, 它的性能如何, 有没有出错? 可以搜集一些信息
  • 将响应以流的方式返回客户端

error过滤器

2. 请求处理的生命周期

 

 

 

 

1) 请求过来了, 首先会进入一系列的前置过滤器pre filter.

2)前置过滤器处理完了, 进入routing filter路由过滤器, routing filter路由过滤器是真正的向后台服务发起请求, 接收响应的过滤器

3) 经过routing filter路由过滤器, 最后会传递过post filter 后置过滤器,进行一些后续的处理, 这时候已经拿到响应了, 然后在返回给客户端.

4) 在这三个过滤器过滤的过程中,任何一个环节发生错误, 都会进入error filter. 有error filter进行统一的错误处理. error filter错误过滤器会发送给post filter, 也是以响应的方式发回给客户端.

这是一个请求, 在网关处理的生命周期.

3. 自定义路由拦截器

这个过滤器extends ZuulFilter

package com.lxl.www.gateway.filter;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import javax.servlet.http.HttpServletRequest;

@Component
public class TokenFilter extends ZuulFilter {
    @Override
    public String filterType() {
        return "pre";
    }

    @Override
    public int filterOrder() {
        return 1;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    /**
     * 实现token 拦截验证
     * @return
     * @throws ZuulException
     */
    @Override
    public Object run() throws ZuulException {
        
        return null;
    }
}

这个过滤器extends自ZuulFilter, 后面的案例多了, 我们就发现, 其实zuul网关的本质就是拦截器, zuul的各种功能,也是通过拦截器来实现的

filterType() : 拦截器的类型是前置拦截器.

filterOrder(): 执行顺序是第一个执行. 

shouldFilter(): 过滤器执行的条件, 这里是所有的连接都需要过这个拦截器, 所以直接设置为true

run(): 拦截器的核心逻辑.  

 4.  spring-cloud-zuul已经实现的过滤器

pre过滤器: 

PreDecorationFilter
ServletDetectionFilter
FormBodyWrapperFilter
DebugFilter
Servlet30WrapperFilter

routing 过滤器

RibbonRoutingFilter
SimpleHostRoutingFilter

post过滤器

SendResponseFilter
SendForwardFilter

error过滤器

SendErrorFilter

 

3.2 ribbon

 1. ribbon是一个客户端负载均衡

 

 

ribbon的实现原理 原来我们的http请求是 http://ip:port/**** 使用ribbon: 需要使用项目名/ 那么也就是根据项目名 寻找一台服务 然后将项目名定位到一台服务的过程

2、 重新定义ribbon负载均衡策略

public class TheSameClusterPriorityRule extends AbstractLoadBalancerRule {
    @Override
    public void initWithNiwsConfig(IClientConfig iClientConfig) {

    }

    /**
     * 主要实现choose方法
     *
     */
    @Override
    public Server choose(Object o) {

    } 

3.3 feign

1、 feign也是客户端负载均衡

Ribbon VS Feign

feign和ribbon是Spring Cloud的Netflix中提供的两个实现软负载均衡的组件,Ribbon和Feign都是用于 调用其他服务的,方式不同。Feign则是在Ribbon的基础上进行了一次改进,采用接口的方式。 将需要调用的其他服务的方法定义成抽象方法即可,不需要自己构建 http 请求

2、 自定义feign拦截器

 

四.项目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. 整体调用流程及效果

 下面我们模拟一个请求调用

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

as

posted @ 2020-07-21 09:41  盛开的太阳  阅读(4274)  评论(1编辑  收藏  举报