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

本文涉及的产品
云数据库 RDS MySQL Serverless,0.5-2RCU 50GB
简介: nacos+ribbon+feign+gateway设计实现灰度方案 (下)

四.项目demo设计与规划


1187916-20200814112731762-809873863.png

五.关键代码实现



第一步: 启动nacos服务.


nacos使用的是本地mysql数据库, 而我们的mysql是放在docker上的,所以,

首先启动mysql

1187916-20200814114222138-2065411984.png

docker start bdc382d8f7f8


然后启动nacos,这里nacos使用的版本是1.2.1


 ./startup.sh -m standalone

在浏览器输入http://localhost:8848/nacos, 登录后进入nacos管理后台

本次我们使用的全局配置主要是global.yml

1187916-20200814114424154-1104114111.png

其实里面就定义了一个配置, 灰度标签是否启用. gray_enable. 如果标签值为1, 表示启用; 为0,表示不启用. 这是一个整体的灰度规则

1187916-20200814114451251-769067232.png


第二步: 启动灰度管理后台 -- 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

1187916-20200814123027619-1683780939.png


表二: gray_relation

1187916-20200814123216029-2012667868.png


6. 管理后台页面


管理后台需要启动, 使用的是fslayui


进入项目的根目录:

npm start


在页面浏览器输入一下地址

localhost:3000


a. 灰度规则管理页面

1187916-20200814124120249-1334120651.png


新增灰度规则,编辑规则, 启用/停用灰度规则,删除灰度规则

b. 微服务灰度服务管理页面--只显示设置了灰度规则的实例

1187916-20200814124646991-1791247329.png

这里可以关联灰度规则, 立即灰度, 服务灰度断流,服务去灰,优雅停服


7. 下面我们按照上面的设计规划来给服务器设置灰度规则

 1187916-20200814131218749-1059011106.png

 

一共启动了这些服务,是不是很神奇, 我的电脑太强大了, 可以一下子其懂8个服务.


然后将端口号为8102, 8202, 8302设置为灰度服务器


首先:关联灰度规则

1187916-20200814131434142-1083397866.png

然后点击"立即灰度", 启用灰度

1187916-20200814131539297-1992852342.png

查看nacos服务器, 我们可以看到已经给服务器打上了灰度标签

 

1187916-20200814131836602-116770762.png

这里的主要流程是, 通过页面操作给服务打上灰度标签.


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


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

相关实践学习
基于CentOS快速搭建LAMP环境
本教程介绍如何搭建LAMP环境,其中LAMP分别代表Linux、Apache、MySQL和PHP。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
2月前
|
Java Nacos 开发者
解决Spring Cloud整合Nacos与Gateway的探险之旅
解决Spring Cloud整合Nacos与Gateway的探险之旅
69 0
|
3月前
|
前端开发 jenkins 持续交付
前后端分离项目知识汇总(GateWay,Nacos配置中心,Jenkins自动化部署,项目总结)-3
前后端分离项目知识汇总(GateWay,Nacos配置中心,Jenkins自动化部署,项目总结)
66 0
|
3月前
|
jenkins Java 持续交付
前后端分离项目知识汇总(GateWay,Nacos配置中心,Jenkins自动化部署,项目总结)-2
前后端分离项目知识汇总(GateWay,Nacos配置中心,Jenkins自动化部署,项目总结)
52 0
|
3月前
|
前端开发 Java jenkins
前后端分离项目知识汇总(GateWay,Nacos配置中心,Jenkins自动化部署,项目总结)-1
前后端分离项目知识汇总(GateWay,Nacos配置中心,Jenkins自动化部署,项目总结)
53 0
|
4月前
|
前端开发 Java Nacos
spring cloud nacos整合gateway(二)
spring cloud nacos整合gateway
52 1
|
4月前
|
负载均衡 Java Nacos
spring cloud nacos整合gateway(一)
spring cloud nacos整合gateway
125 1
|
5月前
|
前端开发 jenkins 持续交付
前后端分离项目知识汇总(GateWay,Nacos配置中心,Jenkins自动化部署,项目总结)-3
前后端分离项目知识汇总(GateWay,Nacos配置中心,Jenkins自动化部署,项目总结)
73 0
|
1月前
|
Dubbo 关系型数据库 MySQL
nacos常见问题之命名空间配置数据上线修改如何解决
Nacos是阿里云开源的服务发现和配置管理平台,用于构建动态微服务应用架构;本汇总针对Nacos在实际应用中用户常遇到的问题进行了归纳和解答,旨在帮助开发者和运维人员高效解决使用Nacos时的各类疑难杂症。
93 1
|
1月前
|
存储 运维 监控
NACOS 配置中心和注册中心是分两个集群部署还是放在一个集群中
【2月更文挑战第33天】NACOS 配置中心和注册中心是分两个集群部署还是放在一个集群中
78 2
|
1月前
|
运维 Java Nacos
nacos常见问题之配置账号密码
Nacos是阿里云开源的服务发现和配置管理平台,用于构建动态微服务应用架构;本汇总针对Nacos在实际应用中用户常遇到的问题进行了归纳和解答,旨在帮助开发者和运维人员高效解决使用Nacos时的各类疑难杂症。
250 6