Skip to content

☀️ Nepxion Discovery is a solution for Spring Cloud with blue green, gray, route, limitation, circuit breaker, degrade, isolation, tracing, dye, failover 蓝绿、灰度、路由、限流、熔断、降级、隔离、追踪、流量染色、故障转移

6.x.x
Switch branches/tags
Code

README.md

Discovery【探索】云原生微服务解决方案

Total visits Total lines License Maven Central Javadocs Build Status Codacy Badge Stars Stars

Spring Boot Spring Cloud Spring Cloud Alibaba Nepxion Discovery

Discovery PPT Discovery WIKI Discovery Platform WIKI Polaris WIKI

           

如果您觉得本框架具有一定的参考价值和借鉴意义,请帮忙在页面右上角 [Star]

首席作者简介

  • Nepxion开源社区创始人
  • 2020年阿里巴巴中国云原生峰会出品人
  • 2020年被Nacos和Spring Cloud Alibaba纳入相关开源项目
  • 2021年阿里巴巴技术峰会上海站演讲嘉宾
  • 2021年荣获陆奇博士主持的奇绩资本,进行风险投资的关注和调研
  • 2021年入选Gitee最有价值开源项目
  • Nacos Group Member、Spring Cloud Alibaba Member
  • Spring Cloud Alibaba、Nacos、Sentinel、OpenTracing Committer & Contributor

商业化合作

① Discovery系列

框架名称 框架版本 支持Spring Cloud版本 使用许可
Discovery 1.x.x ~ 6.x.x Camden ~ Hoxton 开源,永久免费
DiscoveryX 7.x.x 202x 闭源,商业许可

② Polaris系列

Polaris为Discovery高级定制版,特色功能

  • 基于Nepxion Discovery集成定制
  • 多云、多活、多机房流量调配
  • 跨云动态域名、跨环境适配
  • DCN、DSU、SET单元化部署
  • 组件灵活装配、配置对外屏蔽
  • 极简低代码PaaS平台
框架名称 框架版本 支持Discovery版本 支持Spring Cloud版本 使用许可
Polaris 1.x.x 6.x.x Finchley ~ Hoxton 闭源,商业许可
Polaris 2.x.x 7.x.x 202x 闭源,商业许可

有商业版需求的企业和用户,请添加微信1394997,联系作者,洽谈合作事宜

Discovery【探索】企业级云原生微服务开源解决方案

① 解决方案

② 平台界面

③ 快速入门

④ 框架源码

⑤ 指南示例源码

⑥ 指南示例说明

  • 对于入门级玩家,参考6.x.x指南示例极简版,分支为6.x.x-simple
  • 对于熟练级玩家,参考6.x.x指南示例精进版,分支为6.x.x。除上述《极简版》功能外,涉及到指南篇里的绝大多数高级功能
  • 对于骨灰级玩家,参考6.x.x指南示例高级版,分支为6.x.x-complex。除上述《精进版》功能外,涉及到指南篇里的ActiveMQ、MongoDB、RabbitMQ、Redis、RocketMQ、MySQL等高级调用链和蓝绿灰度调用链的整合
  • 上述指南实例分支是针对Spring Cloud旧版本。对于Spring Cloud 202x版本,参考7.x.x指南示例精进版,分支为master

Polaris【北极星】企业级云原生微服务商业解决方案

① 解决方案

② 框架源码

③ 指南示例源码

④ 指南示例说明

Discovery【探索】和Polaris【北极星】架构体系

① Discovery【探索】和Polaris【北极星】联合架构图

② Discovery【探索】和Polaris【北极星】联合拓扑图

③ Polaris【北极星】分层架构图

④ Discovery【探索】实施方案图

⑤ Discovery【探索】域网关实施图

⑥ Discovery【探索】非域网关实施图

⑦ Discovery【探索】全局订阅实施图

⑧ Discovery【探索】配置中心发布订阅图

简介

诞生故事

  • 2017年12月开始筹划
  • 2018年03月开始编码
  • 2018年06月在GitHub开源
  • 2018年06月发布v1.0.0,支持Camden版
  • 2018年06月发布v2.0.0,支持Dalston版
  • 2018年07月发布v3.0.0,支持Edgware版
  • 2018年07月发布v4.0.0,支持Finchley版
  • 2019年04月发布v5.0.0,支持Greenwich版
  • 2020年04月发布v6.0.0,支持Hoxton版
  • 2021年04月完成v7.0.0,支持202x版

功能概述

Discovery【探索】微服务框架,基于Spring Cloud & Spring Cloud Alibaba,Discovery服务注册发现、Ribbon & Spring Cloud LoadBalancer负载均衡、Feign & RestTemplate & WebClient调用、Spring Cloud Gateway & Zuul过滤等组件全方位增强的企业级微服务开源解决方案,更贴近企业级需求,更具有企业级的插件引入、开箱即用特征

① 微服务框架支持的基本功能,如下

  • 支持阿里巴巴Spring Cloud Alibaba中间件生态圈
  • 支持阿里巴巴Nacos、Eureka、Consul和Zookeeper四个服务注册发现中心
  • 支持阿里巴巴Nacos、携程Apollo、Redis、Zookeeper、Consul和Etcd六个远程配置中心
  • 支持阿里巴巴Sentinel、Hystrix和Resilience4J三个熔断限流降级权限中间件
  • 支持OpenTracing和OpenTelemetry规范下的调用链中间件,Jaeger、SkyWalking和Zipkin等
  • 支持Prometheus Micrometer和Spring Boot Admin两个指标中间件
  • 支持Java Agent解决异步跨线程ThreadLocal上下文传递
  • 支持Spring Spel解决蓝绿灰度参数的驱动逻辑
  • 支持Spring Matcher解决元数据匹配的通配逻辑
  • 支持Spring Cloud Gateway、Zuul网关和微服务三大模块的蓝绿灰度发布等一系列功能
  • 支持和兼容Spring Cloud Edgware版、Finchley版、Greenwich版、Hoxton版和202x版以及更高的Spring Cloud版本
  • 支持和兼容Java8~Java16以及更高的SDK版本

② 微服务框架支持的应用功能,如下

  • 全链路蓝绿灰度发布
    • 全链路版本、区域、 IP地址和端口匹配蓝绿发布
    • 全链路版本、区域、 IP地址和端口权重灰度发布
    • 全链路蓝 | 绿 | 兜底、蓝 | 兜底的蓝绿路由类型
    • 全链路稳定、灰度的灰度路由类型
    • 全链路网关、服务端到端混合蓝绿灰度发布
    • 全链路域网关、非域网关部署
    • 全链路条件驱动、非条件驱动
    • 全链路前端触发后端蓝绿灰度发布
    • 全局订阅式蓝绿灰度发布
    • 全链路自定义网关、服务的过滤器、负载均衡策略类触发蓝绿灰度发布
    • 全链路动态变更元数据的蓝绿灰度发布
    • 全链路Header、Parameter、Cookie、域名、RPC Method等参数化规则策略驱动
    • 全链路本地和远程、局部和全局无参数化规则策略驱动
    • 全链路条件表达式、通配表达式支持
    • 全链路内置Header,支持定时Job的服务调用蓝绿灰度发布
  • 全链路蓝绿灰度发布编排建模和流量侦测
    • 全链路蓝绿发布编排建模
    • 全链路灰度发布编排建模
    • 全链路蓝绿发布流量侦测
    • 全链路灰度发布流量侦测
    • 全链路蓝绿灰度发布混合流量侦测
  • 全链路蓝绿灰度发布容灾
    • 发布失败下的版本故障转移
    • 并行发布下的版本偏好
  • 服务下线场景下全链路蓝绿灰度发布,实时性的流量绝对无损
    • 全局唯一ID屏蔽
    • IP地址和端口屏蔽
  • 异步场景下全链路蓝绿灰度发布
    • 异步跨线程Agent插件
    • Hystrix线程池隔离插件
  • 全链路数据库和消息队列蓝绿发布
    • 基于多DataSource的数据库蓝绿发布
    • 基于多Queue的消息队列蓝绿发布
  • 网关动态路由
    • 路由动态添加
    • 路由动态修改
    • 路由动态删除
    • 路由动态批量更新
    • 路由查询
    • 路由动态变更后的事件通知
  • 统一配置订阅执行器
  • 全链路规则策略推送
    • 基于远程配置中心的规则策略订阅推送
    • 基于Swagger和Rest的规则策略推送
    • 基于平台端和桌面端的规则策略推送
  • 全链路环境隔离和路由
    • 全链路环境隔离
    • 全链路环境路由
  • 全链路可用区亲和性隔离和路由
    • 全链路可用区亲和性隔离
    • 全链路可用区亲和性路由
  • 全链路服务隔离和准入
    • 消费端服务隔离
    • 提供端服务隔离
    • 注册发现隔离和准入
  • 全链路服务限流熔断降级权限
    • Sentinel基于服务名的防护
    • Sentinel基于组的防护
    • Sentinel基于版本的防护
    • Sentinel基于区域的防护
    • Sentinel基于环境的防护
    • Sentinel基于可用区的防护
    • Sentinel基于IP地址和端口的防护
    • Sentinel自定义Header、Parameter、Cookie的防护
    • Sentinel自定义业务参数的防护
    • Sentinel自定义组合式的防护
  • 全链路监控
    • 蓝绿灰度埋点和熔断埋点的调用链监控
    • 蓝绿灰度埋点和熔断埋点的日志监控
    • 熔断埋点的指标监控
  • 全链路服务侧注解
  • 全链路服务侧API权限
  • 元数据流量染色
    • Git插件自动化的元数据流量染色
    • 服务名前缀的元数据流量染色
    • 运维平台参数化的元数据流量染色
    • 注册中心动态化的元数据流量染色
    • 用户自定义的元数据流量染色
  • 多活、多云、多机房流量切换
  • Docker容器化和Kubernetes平台无缝支持部署
  • 自动化测试、压力测试

③ 微服务框架易用性表现,如下

  • 引入相关依赖到pom.xml
  • 元数据Metadata流量染色。5大元数据根据不同的使用场景按需设置
    • 定义所属组名 - metadata.group,也可以通过服务名前缀来自动产生服务组名
    • 定义版本号 - metadata.version,也可以通过Git插件方式自动产生版本号
    • 定义所属区域名 - metadata.region
    • 定义所属环境 - metadata.env
    • 定义所属可用区 - metadata.zone
  • 执行采用【约定大于配置】的准则,使用者根据不同的使用场景开启和关闭相关功能项或者属性值,达到最佳配置
  • 规则策略文件设置和推送,或者通过业务Header、Parameter、Cookie触发,并通过Json格式的Header路由策略全链路传递

版本列表

① 微服务框架版本兼容列表,如下

提醒:版本号右边, 表示>=该版本号, 表示<=该版本号

框架版本 框架分支 框架状态 Spring Cloud版本 Spring Boot版本 Spring Cloud Alibaba版本
7.0.0
商业版
master 202x.x.x 2.5.x
2.4.1
202x.x
6.12.1 6.x.x Hoxton.SR5
Hoxton
Greenwich
Finchley
2.3.x.RELEASE
2.2.x.RELEASE
2.1.x.RELEASE
2.0.x.RELEASE
2.2.x.RELEASE
2.2.x.RELEASE
2.1.x.RELEASE
2.0.x.RELEASE
5.6.0 5.x.x Greenwich 2.1.x.RELEASE 2.1.x.RELEASE
4.15.0 4.x.x Finchley 2.0.x.RELEASE 2.0.x.RELEASE
3.28.1 3.x.x Edgware 1.5.x.RELEASE 1.5.x.RELEASE
2.0.x 2.x.x Dalston 1.x.x.RELEASE 1.5.x.RELEASE
1.0.x 1.x.x Camden 1.x.x.RELEASE 1.5.x.RELEASE

表示维护中 | 表示不维护,但可用,强烈建议升级 | 表示不维护,不可用,已废弃

  • 7.x.x版本(适用于202x.x.x)将继续维护
  • 6.x.x版本(同时适用于Finchley、Greenwich和Hoxton)将继续维护
  • 5.x.x版本(适用于Greenwich)已废弃
  • 4.x.x版本(适用于Finchley)已废弃
  • 3.x.x版本(适用于Edgware)不维护,但可用,强烈建议升级
  • 2.x.x版本(适用于Dalston)已废弃
  • 1.x.x版本(适用于Camden)已废弃

② 相关中间件版本列表,如下

组件类型 组件版本
基础组件 Guava
Caffeine
Dom4J
Swagger
Swagger
注册配置组件 Apollo
Zookeeper Curator
Consul
JEtcd
Nacos
Eureka
Redis
防护组件 Sentinel
Hystrix
监控组件 OpenTelemetry
OpenTracing
OpenTracing%20Sping%20Cloud
OpenTracing%20Jaeger
OpenTracing%20Concurrent
SkyWalking
Spring Boot
Spring组件 Alibaba Spring
Spring Cloud
Spring Cloud Alibaba
Spring Boot

郑重致谢

  • 感谢阿里巴巴中间件Nacos、Sentinel和Spring Cloud Alibaba团队,尤其是Nacos负责人@彦林、@于怀,Sentinel负责人@宿何、@子衿,Spring Cloud Alibaba负责人@良名、@小马哥、@洛夜、@亦盏的技术支持
  • 感谢携程Apollo团队,尤其是@宋顺的技术支持
  • 感谢所有Committers和Contributors
  • 感谢所有帮忙分析和定位问题的同学
  • 感谢所有提出宝贵建议和意见的同学
  • 感谢支持和使用本框架的公司和企业

企业用户

不完全统计,目前社区开源项目(包括本框架以及关联框架或组件)已经被如下公司使用或者调研

为提供更好的专业级服务,请更多已经使用本框架的公司和企业联系我,并希望在Github Issues上登记

某大型银行信用卡新核心系统在生产环境接入Nepxion Discovery框架的服务实例数(包括异地双活,同城双活,多机房全部汇总)将近10000个

某大型互联网教育公司在生产环境接入Nepxion Discovery框架的服务实例数截至到2021年2月已达到2600多个,基本接入完毕

请联系我

微信、钉钉、公众号和文档

目录

主页链接

源码主页

Discovery源码主页

Polaris源码主页

发布主页

DiscoveryAgent

DiscoveryDesktop

指南主页

Discovery指南主页

Polaris指南主页

入门主页

Gitee Wiki

Github Wiki

博客主页

博客主页

工程架构

工程清单

① Discovery工程清单

工程名 描述
discovery-commons 通用模块目录
   discovery-common 通用模块
   discovery-common-apollo 封装Apollo通用配置操作逻辑
   discovery-common-nacos 封装Nacos通用配置操作逻辑
   discovery-common-redis 封装Redis通用配置操作逻辑
   discovery-common-zookeeper 封装Zookeeper通用配置操作逻辑
   discovery-common-consul 封装Consul通用配置操作逻辑
   discovery-common-etcd 封装Etcd通用配置操作逻辑
discovery-plugin-framework 基本框架目录
   discovery-plugin-framework-starter 基本框架的Starter
   discovery-plugin-framework-starter-parser 基本框架解析模块的Starter
discovery-plugin-register-center 注册中心目录
   discovery-plugin-register-center-starter 注册中心的Starter
   discovery-plugin-register-center-starter-eureka 注册中心的Eureka Starter
   discovery-plugin-register-center-starter-consul 注册中心的Consul Starter
   discovery-plugin-register-center-starter-zookeeper 注册中心的Zookeeper Starter
   discovery-plugin-register-center-starter-nacos 注册中心的Nacos Starter
discovery-plugin-config-center 配置中心目录
   discovery-plugin-config-center-starter 配置中心的Starter
   discovery-plugin-config-center-starter-apollo 配置中心的Apollo Starter
   discovery-plugin-config-center-starter-nacos 配置中心的Nacos Starter
   discovery-plugin-config-center-starter-redis 配置中心的Redis Starter
   discovery-plugin-config-center-starter-zookeeper 配置中心的Zookeeper Starter
   discovery-plugin-config-center-starter-consul 配置中心的Consul Starter
   discovery-plugin-config-center-starter-etcd 配置中心的Etcd Starter
discovery-plugin-admin-center 管理中心目录
   discovery-plugin-admin-center-starter 管理中心的Starter
discovery-plugin-strategy 策略目录
   discovery-plugin-strategy-starter 策略的Starter
   discovery-plugin-strategy-starter-service 策略在微服务端的Starter
   discovery-plugin-strategy-starter-zuul 策略在Zuul网关端的Starter
   discovery-plugin-strategy-starter-gateway 策略在Spring Cloud Gateway网关端的Starter
   discovery-plugin-strategy-starter-hystrix 策略的Hystrix线程池隔离模式插件的Starter
   discovery-plugin-strategy-starter-opentelemetry 策略的OpenTelemetry调用链的Starter
   discovery-plugin-strategy-starter-opentracing 策略的OpenTracing调用链的Starter
   discovery-plugin-strategy-starter-skywalking 策略的SkyWalking调用链的Starter
   discovery-plugin-strategy-starter-sentinel-datasource 策略的Sentinel配置中心的Starter
   discovery-plugin-strategy-starter-sentinel-limiter 策略的Sentinel Limiter高级限流熔断的Starter
   discovery-plugin-strategy-starter-sentinel-monitor 策略的Sentinel监控抽象的Starter
   discovery-plugin-strategy-starter-sentinel-opentelemetry 策略的Sentinel OpenTelemetry调用链的Starter
   discovery-plugin-strategy-starter-sentinel-opentracing 策略的Sentinel OpenTracing调用链的Starter
   discovery-plugin-strategy-starter-sentinel-skywalking 策略的Sentinel SkyWalking调用链的Starter
   discovery-plugin-strategy-starter-sentinel-micrometer 策略的Sentinel Micrometer指标的Starter
discovery-plugin-test 测试模块目录
   discovery-plugin-test-starter-automation 自动化测试的Starter
discovery-console 控制平台目录
   discovery-console-starter 控制平台的starter
   discovery-console-starter-apollo 控制平台的Apollo Starter
   discovery-console-starter-nacos 控制平台的Nacos Starter
   discovery-console-starter-redis 控制平台的Redis Starter
   discovery-console-starter-zookeeper 控制平台的Zookeeper Starter
   discovery-console-starter-consul 控制平台的Consul Starter
   discovery-console-starter-etcd 控制平台的Etcd Starter
discovery-springcloud-examples 示例目录
   discovery-springcloud-example-admin Spring Boot Admin服务台示例
   discovery-springcloud-example-console 控制平台示例
   discovery-springcloud-example-eureka Eureka服务器示例
   discovery-springcloud-example-service 微服务示例
   discovery-springcloud-example-zuul Zuul网关示例
   discovery-springcloud-example-gateway Spring Cloud Gateway网关示例

② DiscoveryPlatform工程清单

工程名 描述
discovery-platform-server 平台服务端模块目录
   discovery-platform-starter-server 平台服务端的Starter
   discovery-platform-starter-server-mysql 平台服务端数据库MySQL插件的Starter
   discovery-platform-starter-server-h2 平台服务端H2插件的Starter
   discovery-platform-starter-server-ldap 平台服务端Ldap插件的Starter
   discovery-platform-starter-server-ui 平台服务端界面
discovery-platform-client 平台服务端模块目录
   discovery-platform-starter-client 平台客户端的Starter
discovery-platform-common 平台通用模块目录
   discovery-platform-starter-common-dingding 封装钉钉通用操作逻辑的Starter
   discovery-platform-starter-common-mail 封装邮件通用操作逻辑的Starter
discovery-platform-application 平台服务端可执行应用

③ DiscoveryAgent工程清单

工程名 描述
discovery-agent-starter 异步跨线程Agent Starter
discovery-agent-starter-plugin-strategy 路由策略的异步跨线程Agent Plugin Starter
discovery-agent-starter-plugin-mdc MDC日志的异步跨线程Agent Plugin Starter
discovery-agent-example 异步跨线程示例

④ DiscoveryUI工程清单

工程名 描述
desktop Nepxion Discovery 服务治理平台前端桌面版
web Nepxion Discovery 服务治理平台前端Web版

⑤ DiscoveryContrib工程清单

工程名 描述
discovery-contrib-plugin-starter 第三方非微服务范畴中间件的蓝绿灰度发布Contrib Plugin Starter
discovery-contrib-plugin-starter-rocketmq RocketMQ的蓝绿灰度发布Contrib Plugin Starter
discovery-contrib-plugin-starter-shardingsphere ShardingSphere日志的蓝绿灰度发布Contrib Plugin Starter
discovery-contrib-example 第三方非微服务范畴中间件的蓝绿灰度发布示例

代码清单

仓库主分支 代码行数
Discovery Total lines
DiscoveryPlatform Total lines
DiscoveryGuide Total lines
DiscoveryAgent Total lines
DiscoveryUI Total lines
DiscoveryContrib Total lines

架构核心

  • 服务治理架构图

  • 模块结构图

依赖引入

① 服务注册发现依赖引入

服务注册发现中间件的四个插件,必须引入其中一个

<dependency>
    <groupId>com.nepxion</groupId>
    <artifactId>discovery-plugin-register-center-starter-nacos</artifactId>
    <artifactId>discovery-plugin-register-center-starter-eureka</artifactId>
    <artifactId>discovery-plugin-register-center-starter-consul</artifactId>
    <artifactId>discovery-plugin-register-center-starter-zookeeper</artifactId>
    <version>${discovery.version}</version>
</dependency>

② 配置中心依赖引入

配置中心中间件的六个插件,选择引入其中一个

<dependency>
    <groupId>com.nepxion</groupId>
    <artifactId>discovery-plugin-config-center-starter-apollo</artifactId>
    <artifactId>discovery-plugin-config-center-starter-nacos</artifactId>
    <artifactId>discovery-plugin-config-center-starter-redis</artifactId>
    <artifactId>discovery-plugin-config-center-starter-zookeeper</artifactId>
    <artifactId>discovery-plugin-config-center-starter-consul</artifactId>
    <artifactId>discovery-plugin-config-center-starter-etcd</artifactId>
    <version>${discovery.version}</version>
</dependency>

③ 管理中心依赖引入

选择引入

<dependency>
    <groupId>${project.groupId}</groupId>
    <artifactId>discovery-plugin-admin-center-starter</artifactId>
    <version>${discovery.version}</version>
</dependency>

④ 路由策略依赖引入

微服务端、网关Zuul端和网关Spring Cloud Gateway端三个路由策略插件,选择引入其中一个

<dependency>
    <groupId>com.nepxion</groupId>
    <artifactId>discovery-plugin-strategy-starter-service</artifactId>
    <artifactId>discovery-plugin-strategy-starter-zuul</artifactId>
    <artifactId>discovery-plugin-strategy-starter-gateway</artifactId>
    <version>${discovery.version}</version>
</dependency>

⑤ 防护插件依赖引入

  • Sentinel防护的数据源插件
<dependency>
    <groupId>com.nepxion</groupId>
    <artifactId>discovery-plugin-strategy-starter-sentinel-datasource</artifactId>
    <version>${discovery.version}</version>
</dependency>
  • Sentinel防护的Sentinel Limiter高级限流熔断插件。只适用于Servlet模式
<dependency>
    <groupId>com.nepxion</groupId>
    <artifactId>discovery-plugin-strategy-starter-sentinel-limiter</artifactId>
    <version>${discovery.version}</version>
</dependency>
  • Hystrix防护插件。Hystrix线程池隔离模式下必须引入该插件
<dependency>
    <groupId>com.nepxion</groupId>
    <artifactId>discovery-plugin-strategy-starter-hystrix</artifactId>
    <version>${discovery.version}</version>
</dependency>

⑥ 控制台依赖引入

控制台对于配置中心中间件的四个插件,选择引入其中一个

<dependency>
    <groupId>com.nepxion</groupId>
    <artifactId>discovery-console-starter-apollo</artifactId>
    <artifactId>discovery-console-starter-nacos</artifactId>
    <artifactId>discovery-console-starter-redis</artifactId>
    <artifactId>discovery-console-starter-zookeeper</artifactId>
    <version>${discovery.version}</version>
</dependency>

⑦ 调用链插件依赖引入

支持微服务端、网关Zuul端和网关Spring Cloud Gateway端,选择引入其中一个

需要注意,该模块支持F版或更高版本

<dependency>
    <groupId>com.nepxion</groupId>
    <artifactId>discovery-plugin-strategy-starter-sentinel-opentelemetry</artifactId>
    <artifactId>discovery-plugin-strategy-starter-sentinel-opentracing</artifactId>
    <artifactId>discovery-plugin-strategy-starter-sentinel-skywalking</artifactId>
    <version>${discovery.version}</version>
</dependency>

⑧ 自动化测试插件依赖引入

<dependency>
    <groupId>com.nepxion</groupId>
    <artifactId>discovery-plugin-test-starter</artifactId>
    <version>${discovery.version}</version>
</dependency>

⑨ 异步跨线程Agent引入

-javaagent:/discovery-agent/discovery-agent-starter-${discovery.agent.version}.jar -Dthread.scan.packages=com.abc;com.xyz

准备工作

为了更好的阐述框架的各项功能,本文围绕指南示例展开,请使用者先进行下面的准备工作。指南示例以Nacos为服务注册中心和配置中心展开介绍,使用者可自行换成其它服务注册中心和配置中心

环境搭建

① 下载代码,Git clone https://github.com/Nepxion/DiscoveryGuide.git,分支为6.x.x-simple

② 代码导入IDE

③ 下载Nacos服务器

④ 启动Nacos服务器

  • Windows环境下,运行bin目录下的startup.cmd
  • Linux环境下,运行bin目录下的startup.sh

启动服务

  • 在IDE中,启动四个应用服务和两个网关服务,如下
类名 微服务 服务端口 版本 区域 环境 可用区
DiscoveryGuideServiceA1.java A1 3001 1.0 dev env1 zone1
DiscoveryGuideServiceA2.java A2 3002 1.1 qa common zone2
DiscoveryGuideServiceB1.java B1 4001 1.0 qa env1 zone1
DiscoveryGuideServiceB2.java B2 4002 1.1 dev common zone2
DiscoveryGuideGateway.java Gateway 5001 1.0
DiscoveryGuideZuul.java Zuul 5002 1.0
  • 部署拓扑图

全链路路径, 如下

API网关 -> 服务A(两个实例) -> 服务B(两个实例)

环境验证

通过Postman工具验证

  • 导入Postman的测试脚本postman.json(位于根目录下)

  • 在Postman中执行目录结构下〔Nepxion〕->〔Discovery指南网关接口〕->〔Gateway网关调用示例〕,调用地址为http://localhost:5001/discovery-guide-service-a/invoke/gateway,相关的Header值已经预设,供开发者修改。执行通过Spring Cloud Gateway网关发起的调用,结果为如下格式

gateway 
-> [ID=discovery-guide-service-a][T=service][P=Nacos][H=192.168.0.107:3001][V=1.0][R=dev][E=env1][Z=zone1][G=discovery-guide-group][TID=48682.7508.15870951148324081][SID=49570.77.15870951148480000] 
-> [ID=discovery-guide-service-b][T=service][P=Nacos][H=192.168.0.107:4001][V=1.0][R=qa][E=env1][Z=zone2][G=discovery-guide-group][TID=48682.7508.15870951148324081][SID=49571.85.15870951189970000]
  • 在Postman中执行目录结构下〔Nepxion〕->〔Discovery指南网关接口〕->〔Zuul网关调用示例〕,调用地址为http://localhost:5002/discovery-guide-service-a/invoke/zuul,相关的Header值已经预设,供开发者修改。执行通过Zuul网关发起的调用,结果为如下格式
zuul 
-> [ID=discovery-guide-service-a][T=service][P=Nacos][H=192.168.0.107:3001][V=1.0][R=dev][E=env1][Z=zone1][G=discovery-guide-group][TID=48682.7508.15870951148324081][SID=49570.77.15870951148480000] 
-> [ID=discovery-guide-service-b][T=service][P=Nacos][H=192.168.0.107:4001][V=1.0][R=qa][E=env1][Z=zone2][G=discovery-guide-group][TID=48682.7508.15870951148324081][SID=49571.85.15870951189970000]
  • 在Postman中多种同步和异步的调用方式,异步方式需要增加DiscoveryAgent,才能保证蓝绿发布路由调用的成功

〔Spring Cloud 202x版〕特别提醒

对于Spring Cloud 202x版,由于它采用的负载均衡Spring Cloud LoadBalancer是基于异步的WebFlux,所以必须加上DiscoveryAgent,如下方式

-javaagent:C:/opt/discovery-agent/discovery-agent-starter-${discovery.agent.version}.jar

URL 调用方式
/invoke/ 同步调用
/invoke-async/ @Async注解方式的异步调用
/invoke-thread/ 单线程方式的异步调用
/invoke-threadpool/ 线程池方式的异步调用
  • 上述步骤在下面每次更改规则策略的时候执行,并验证结果和规则策略的期望值是否相同

蓝绿灰度发布概念

蓝绿发布

蓝绿发布 Blue-Green Deployment

① 概念

不停机旧版本,部署新版本,通过用户标记将流量在新版本和老版本切换,属无损发布

② 优点

新版本升级和老版本回滚迅速。用户可以灵活控制流量走向

③ 缺点

成本较高,需要部署两套环境(蓝/绿)。新版本出现问题,切换不及时,会造成大面积故障

灰度发布

灰度发布 Gray Release(又名金丝雀发布 Canary Release)

① 概念

不停机旧版本,部署新版本,低比例流量(例如:5%)切换到新版本,高比例流量(例如:95%)走旧版本,通过监控观察无问题,逐步扩大范围,最终把所有流量都迁移到新版本上,下线旧版本。属无损发布

② 优点

灵活简单,不需要用户标记驱动。安全性高,新版本如果出现问题,只会发生在低比例的流量上

③ 缺点

流量配比递增的配置修改,带来额外的操作成本。用户覆盖狭窄,低比例流量未必能发现所有问题

滚动发布

滚动发布 Rolling Release

① 概念

每次只升级一个或多个服务,升级完成监控观察,不断执行这个过程,直到集群中的全部旧版本升级到新版本。停止旧版本的过程中,无法精确计算旧版本是否已经完成它正在执行的工作,需要靠业务自身去判断。属有损发布

② 优点

出现问题影响范围很小,只会发生在若干台正在滚动发布的服务上

③ 缺点

发布和回滚需要较长的时间周期。按批次停止旧版本,启动新版本,由于旧版本不保留,一旦全部升级完毕后才发现问题,则无法快速回滚,必须重新降级部署

全链路蓝绿灰度发布

全链路蓝绿发布

经典场景:当调用请求从网关或者服务发起的时候,通过Header | Parameter | Cookie一种或者几种参数进行驱动,在路由过滤中,根据这些参数,选择在配置中心配置的蓝路由 | 绿路由 | 兜底路由的规则策略(Json格式),并把命中的规则策略转化为策略路由Header(n-d-开头),实现全链路传递。每个端到端服务接收到策略路由Header后,执行负载均衡时,该Header跟注册中心的对应元数据进行相关比较,不符合条件的实例进行过滤,从而实现全链路蓝绿发布

实施概要:只涉及当前正在发布的服务,例如,对于 〔网关〕->〔A服务〕->〔B服务〕->〔C服务〕->〔D服务〕调用链来说,如果当前只是B服务和C服务正在实施发布,那么,只需要把B服务和C服务配置到规则策略中,其它则不需要配置。发布结束后,即B服务和C服务的所有实例都完全一致,例如,版本号都只有唯一一个,那么清除掉在配置中心配置的规则策略即可,从而进行下一轮全链路蓝绿发布

小贴士

n-d-的含义:n为Nepxion首字母,d为Discovery首字母

全链路版本匹配蓝绿发布

增加Spring Cloud Gateway的版本匹配蓝绿发布策略,Group为discovery-guide-group,Data Id为discovery-guide-gateway,策略内容如下,实现从Spring Cloud Gateway发起的调用全链路都走版本为1.0的服务

<?xml version="1.0" encoding="UTF-8"?>
<rule>
    <strategy>
        <version>1.0</version>
    </strategy>
</rule>

如果希望每个服务的版本分别指定,那么策略内容如下,实现从Spring Cloud Gateway发起的调用走1.0版本的a服务,走1.1版本的b服务

<?xml version="1.0" encoding="UTF-8"?>
<rule>
    <strategy>
        <version>{"discovery-guide-service-a":"1.0", "discovery-guide-service-b":"1.1"}</version>
    </strategy>
</rule>

当所有服务都选同一版本的时候,下面两条是等效的

<version>1.0</version>
<version>{"discovery-guide-service-a":"1.0", "discovery-guide-service-b":"1.0"}</version>

如果希望可调用的版本是多个,也可以表示成如下方式,即1.0版本和1.1版本的a服务和b服务都可以被调用到,下面两条是等效的

<version>1.0;1.1</version>
<version>{"discovery-guide-service-a":"1.0;1.1", "discovery-guide-service-b":"1.0;1.1"}</version>

如果上述表达式还未满足需求,也可以采用通配表达式方式(具体详细用法,参考Spring AntPathMatcher),通过Spring Matcher的通配表达式,支持多个通配*、单个通配?等全部标准表达式用法

* - 表示调用范围为所有版本
1.* - 表示调用范围为1开头的所有版本

例如

"discovery-guide-service-b":"1.*;1.2.?"

表示discovery-guide-service-b服务的调用范围是1开头的所有版本,或者调用范围是1.2开头的所有版本(末尾必须是1个字符),多个用分号隔开

提醒:非条件驱动下的全链路蓝绿发布跟Header驱动下的全链路蓝绿发布等效,例如

n-d-version=1.0
n-d-version={"discovery-guide-service-a":"1.0", "discovery-guide-service-b":"1.0"}

版本匹配蓝绿发布架构图

全链路区域匹配蓝绿发布

增加Zuul的区域匹配蓝绿发布策略,Group为discovery-guide-group,Data Id为discovery-guide-zuul,策略内容如下,实现从Zuul发起的调用全链路都走区域为dev的服务

<?xml version="1.0" encoding="UTF-8"?>
<rule>
    <strategy>
        <region>dev</region>
    </strategy>
</rule>

如果希望每个服务的版本分别指定,那么策略内容如下,实现从Zuul发起的调用走dev区域的a服务,走qa区域的b服务

<?xml version="1.0" encoding="UTF-8"?>
<rule>
    <strategy>
        <region>{"discovery-guide-service-a":"dev", "discovery-guide-service-b":"qa"}</region>
    </strategy>
</rule>

当所有服务都选同一区域的时候,下面两条是等效的

<region>dev</region>
<region>{"discovery-guide-service-a":"dev", "discovery-guide-service-b":"dev"}</region>

如果希望可调用区域是多个,也可以表示成如下方式,即dev区域和qa区域的a服务和b服务都可以被调用到,下面两条是等效的

<region>dev;qa</region>
<region>{"discovery-guide-service-a":"dev;qa", "discovery-guide-service-b":"dev;aq"}</region>

如果上述表达式还未满足需求,也可以采用通配表达式方式(具体详细用法,参考Spring AntPathMatcher),通过Spring Matcher的通配表达式,支持多个通配*、单个通配?等全部标准表达式用法

* - 表示调用范围为所有区域
d* - 表示调用范围为d开头的所有区域

例如

"discovery-guide-service-b":"d*;q?"

表示discovery-guide-service-b服务的调用范围是d开头的所有区域,或者调用范围是q开头的所有区域(末尾必须是1个字符),多个用分号隔开

提醒:非条件驱动下的全链路蓝绿发布跟Header驱动下的全链路蓝绿发布等效,例如

n-d-region=dev
n-d-region={"discovery-guide-service-a":"dev", "discovery-guide-service-b":"dev"}

区域匹配蓝绿发布架构图

全链路IP地址和端口匹配蓝绿发布

增加Zuul的IP地址和端口匹配蓝绿发布策略,Group为discovery-guide-group,Data Id为discovery-guide-zuul,策略内容如下,实现从Zuul发起的调用走指定IP地址和端口,或者指定IP地址,或者指定端口(下面策略以端口为例)的服务

<?xml version="1.0" encoding="UTF-8"?>
<rule>
    <strategy>
        <!-- <address>127.0.0.1:3001</address> -->
        <!-- <address>127.0.0.1</address> -->
        <address>3001</address>
    </strategy>
</rule>

如果希望每个服务的IP地址或者端口分别指定,那么策略内容如下,实现从Zuul发起的调用走3001端口的a服务,走4001端口的b服务

<?xml version="1.0" encoding="UTF-8"?>
<rule>
    <strategy>
        <address>{"discovery-guide-service-a":"3001", "discovery-guide-service-b":"4001"}</address>
    </strategy>
</rule>

当所有服务都选同一端口的时候,下面两条是等效的

<address>3001</address>
<address>{"discovery-guide-service-a":"3001", "discovery-guide-service-b":"3001"}</address>

如果希望可调用端口是多个,也可以表示成如下方式,即3001端口和4001端口的a服务和b服务都可以被调用到,下面两条是等效的

<address>3001;4001</address>
<address>{"discovery-guide-service-a":"3001;4001", "discovery-guide-service-b":"3001;4001"}</address>

如果上述表达式还未满足需求,也可以采用通配表达式方式(具体详细用法,参考Spring AntPathMatcher),通过Spring Matcher的通配表达式,支持多个通配*、单个通配?等全部标准表达式用法

* - 表示调用范围为所有端口
3* - 表示调用范围为3开头的所有端口

例如

"discovery-guide-service-b":"3*;400?"

表示discovery-guide-service-b服务的调用范围是3开头的所有端口,或者调用范围是400开头的所有端口(末尾必须是1个字符),多个用分号隔开

提醒:非条件驱动下的全链路蓝绿发布跟Header驱动下的全链路蓝绿发布等效,例如

n-d-address=3001
n-d-address={"discovery-guide-service-a":"3001", "discovery-guide-service-b":"3001"}

IP地址和端口匹配蓝绿发布架构图

全链路条件蓝绿发布

全链路版本条件匹配蓝绿发布

通过Header、Parameter、Cookie驱动参数和条件表达式结合,把业务定义的这三个驱动参数转化成全链路传递的策略路由Header,执行基于版本匹配的蓝、绿、兜底三条路由驱动,实现全链路版本条件匹配蓝绿发布

驱动参数

① Header、Parameter、Cookie参数传递。对于要驱动发布的参数,例如,业务参数user,可以选择Header、Parameter、Cookie其中任意一个传递,都是等效的

② Header、Parameter、Cookie参数优先级。对于要驱动发布的参数,例如,业务参数user,如果在这三者中都存在,且值不相同,那么取值优先级Parameter > Cookie > Header > 内置Header

③ Header、Parameter、Cookie参数混合。对于要驱动发布的参数,如果不止一个,例如,业务参数user、age、address,可以全部是Header或者Parameter或者Cookie,也可以是这三者混合传递:user通过Header传递,age通过Parameter传递,address通过Cookie传递

条件表达式

① Spring Spel的条件表达式,支持等于=、不等于!=、大于>、小于<、与&&、或||、匹配matches,以及加减乘除取模等全部标准表达式用法

小贴士

通过Spring Spel的matches条件表达式

  • 可通过如下表达式,判断入参是否为邮件格式
[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,4}
  • 可通过如下表达式,判断入参是否为三个字母,结尾等于2
[a-z]{3}2

② Spring Spel的条件表达式,整合驱动参数

例如,驱动参数分别为a、b、c,驱动条件为a等于1,b小于等于2,c不等于3,那么表达式可以写为

#H['a'] == '1' && #H['b'] <= '2' && #H['c'] != '3'

或者

#H['a'] == '1' and #H['b'] <= '2' and #H['c'] != '3'

其中,#H['a'],Spring Spel表达式用来表述驱动参数a的专有格式

小贴士

H的含义:H为Http首字母,即取值Http类型的参数,包括Header、Parameter、Cookie

③ Spring Spel的逻辑表达,需要注意

  • 任何值都大于null。当某个参数未传值,但又指定了该参数大于的表达逻辑,那么表达式结果为false。例如,#H['a'] > '2',但a未传递进来,a即null,则null > 2,表达式结果为false
  • null满足不等于。当某个参数未传值,但又指定了该该参数不等于的表达逻辑,那么表达式结果为true。例如,#H['a'] != '2',但a未传递进来,a即null,则null != 2,表达式结果为true

④ Spring Spel的符号

符号和等价符号作用相同,任选一个,等价符号不区分大小写

符号 等价符号 含义 备注
+
-
*
/ div
% mod 求余
== eq 等于 equal缩写
!= ne 不等于 not equal缩写
> gt 大于 greater than缩写
>= ge 大于等于 greater than equal缩写
< lt 小于 less than缩写
<= le 小于等于 less than equal缩写
&& and
|| or
! not
matches 正则表达式 #H['a'] matches '[a-z]{3}2'
contains 包含 #H['a'].contains('123')
between 区间 #H['a'] between {1, 2}
instanceof 实例表达式 #H['a'] instanceof 'T(String)'

⑤ Spring Spel的符号转义,对XML格式的规则策略文件,保存在配置中心的时候,需要对表达式中的特殊符号进行转义

符号 转义符 含义 备注
& &amp; 和符号 必须转义
< &lt; 小于号 必须转义
" &quot; 双引号 必须转义
> &gt; 大于号
' &apos; 单引号

表达式如果包含跟XML格式冲突的字符,就必须转义,例如

#H['a'] == '1' &amp;&amp; #H['b'] &lt;= '2' &amp;&amp; #H['c'] != '3'

规则策略配置

增加Spring Cloud Gateway的版本条件匹配蓝绿发布策略,Group为discovery-guide-group,Data Id为discovery-guide-gateway,策略内容如下

<?xml version="1.0" encoding="UTF-8"?>
<rule>
    <!-- 全局缺省路由,当兜底路由存在的时候,全局缺省路由不需要配置 -->
    <!-- <strategy>
        <version>{"discovery-guide-service-a":"1.0", "discovery-guide-service-b":"1.0"}</version>
    </strategy> -->

    <strategy-release>
        <conditions type="blue-green">
            <!-- 蓝路由,条件expression驱动 -->	
            <condition id="blue-condition" expression="#H['a'] == '1'" version-id="blue-route"/>
            <!-- 绿路由,条件expression驱动 -->
            <condition id="green-condition" expression="#H['a'] == '1' and #H['b'] == '2'" version-id="green-route"/>
            <!-- 兜底路由,无条件expression驱动 -->
            <condition id="basic-condition" version-id="basic-route"/>
        </conditions>

        <routes>
            <route id="blue-route" type="version">{"discovery-guide-service-a":"1.1", "discovery-guide-service-b":"1.1"}</route>	
            <route id="green-route" type="version">{"discovery-guide-service-a":"1.0", "discovery-guide-service-b":"1.0"}</route>
            <route id="basic-route" type="version">{"discovery-guide-service-a":"1.0", "discovery-guide-service-b":"1.0"}</route>
        </routes>
    </strategy-release>
</rule>

规则策略解释

特别提醒

为准确体现相关变量(例如上文中的 a )支持Header、Parameter、Cookie中的任意一个,表达式格式为 expression="#H['a'] == '1'"

① 当外部调用带有的Header/Parameter/Cookies中的值a=1同时b=2,执行绿路由

<condition>节点(id="blue-condition")中 expression="#H['a'] == '1' and #H['b'] == '2'" 对应的 version-id="green-route" ,找到下面<route>节点中 id="green-route" type="version" 的那项,那么路由即为

{"discovery-guide-service-a":"1.0", "discovery-guide-service-b":"1.0"}

② 当外部调用带有的Header/Parameter/Cookies中的值a=1,执行蓝路由

<condition>节点(id="green-condition")中 expression="#H['a'] == '1'" 对应的 version-id="blue-route" ,找到下面<route>节点中 id="blue-route" type="version" 的那项,那么路由即为

{"discovery-guide-service-a":"1.1", "discovery-guide-service-b":"1.1"}

③ 当外部调用带有的Header/Parameter/Cookies中的值都不命中,或者未传值,执行兜底路由

  • 执行<condition>节点(id="basic-condition")中的兜底路由,那么路由即为
{"discovery-guide-service-a":"1.0", "discovery-guide-service-b":"1.0"}

④ 如果兜底路由未配置

  • 执行<strategy>节点中的全局缺省路由,那么路由即为
{"discovery-guide-service-a":"1.0", "discovery-guide-service-b":"1.0"}

特别提醒

兜底路由和全局缺省路由配置一个即可

  • 如果上述配置都不存在,则执行Spring Cloud Ribbon轮询策略

⑤ 假如不愿意从网关外部传入Header/Parameter/Cookies,那么支持策略下内置Header来决策蓝绿发布,可以代替外部传入Header/Parameter/Cookies,参考如下配置

<header>{"a":"1", "b":"2", "c":"3"}</header>

内置Header一般使用场景为定时Job的服务定时去调用其它服务,希望实施蓝绿灰度发布。当服务侧配置了内置Header,而网关也传递给对应Header给该服务,通过开关来决定,网关传递的Header为优先还是服务侧内置的Header优先

需要注意,Spring Cloud Gateway在Finchley版不支持该方式

⑥ 路由类型支持如下

  • 蓝 | 绿 | 兜底,即上述提到的路由场景
  • 蓝 | 兜底,即绿路由缺省,那么兜底路由则为绿路由,逻辑更加简单的路由场景
  • 如果蓝路由和路由都缺省,那就只有兜底路由(全局缺省路由),即为全链路版本匹配蓝绿发布的路由场景

⑦ 策略总共支持5种,可以单独一项使用,也可以多项叠加使用

  • version 版本
  • region 区域
  • address IP地址和端口
  • version-weight 版本权重
  • region-weight 区域权重

⑧ 策略支持Spring Spel的条件表达式方式

⑨ 策略支持Spring Matcher的通配方式

上述方式,可以通过全链路蓝绿发布编排建模方式执行,并通过全链路蓝绿发布流量侦测进行验证

全链路区域条件匹配蓝绿发布

参考全链路版本条件匹配蓝绿发布

用法相似,只需要把规则策略中

  • 属性version-id替换成region-id
  • 属性type="version"替换成type="region"
  • 节点route对应的Json中版本替换成区域

全链路IP地址和端口条件匹配蓝绿发布

参考全链路版本条件匹配蓝绿发布

用法相似,只需要把规则策略中

  • 属性version-id替换成address-id
  • 属性type="version"替换成type="address"
  • 节点route对应的Json中版本替换成IP地址和端口

全链路灰度发布

经典场景:当调用请求从网关或者服务发起的时候,在路由过滤中,根据在配置中心配置的随机权重值,执行权重算法,选择灰度路由 | 稳定路由的规则策略(Json格式),并把命中的规则策略转化为策略路由Header(n-d-开头),实现全链路传递。每个端到端服务接收到策略路由Header后,执行负载均衡时,该Header跟注册中心的对应元数据进行相关比较,不符合条件的实例进行过滤,从而实现全链路灰度发布

实施概要:只涉及当前正在发布的服务,例如,对于 〔网关〕->〔A服务〕->〔B服务〕->〔C服务〕->〔D服务〕调用链来说,如果当前只是B服务和C服务正在实施发布,那么,只需要把B服务和C服务配置到规则策略中,其它则不需要配置。发布结束后,即B服务和C服务的所有实例都完全一致,例如,版本号都只有唯一一个,那么清除掉在配置中心配置的规则策略即可,从而进行下一轮全链路灰度发布

全链路版本权重灰度发布

增加Spring Cloud Gateway的版本权重灰度发布策略,Group为discovery-guide-group,Data Id为discovery-guide-gateway,策略内容如下,实现从Spring Cloud Gateway发起的调用全链路1.0版本流量权重为90%,1.1版本流量权重为10%

<?xml version="1.0" encoding="UTF-8"?>
<rule>
    <strategy>
        <version-weight>1.0=90;1.1=10</version-weight>
    </strategy>
</rule>

如果希望每个服务的版本权重分别指定,那么策略内容如下,实现从Spring Cloud Gateway发起的调用a服务1.0版本流量权重为90%,1.1版本流量权重为10%,b服务1.0版本流量权重为80%,1.1版本流量权重为20%

<?xml version="1.0" encoding="UTF-8"?>
<rule>
    <strategy>
        <version-weight>{"discovery-guide-service-a":"1.0=90;1.1=10", "discovery-guide-service-b":"1.0=80;1.1=20"}</version-weight>
    </strategy>
</rule>

当所有服务都选相同版本流量权重分配的时候,下面两条是等效的

<version-weight>1.0=90;1.1=10</version-weight>
<version-weight>{"discovery-guide-service-a":"1.0=90;1.1=10", "discovery-guide-service-b":"1.0=90;1.1=10"}</version-weight>

提醒:非条件驱动下的全链路灰度发布跟Header驱动下的全链路灰度发布等效,例如

n-d-version-weight=1.0=90;1.1=10
n-d-version-weight={"discovery-guide-service-a":"1.0=90;1.1=10", "discovery-guide-service-b":"1.0=90;1.1=10"}

全链路区域权重灰度发布

增加Zuul的区域权重灰度发布策略,Group为discovery-guide-group,Data Id为discovery-guide-zuul,策略内容如下,实现从Zuul发起的调用全链路dev区域流量权重为85%,qa区域流量权重为15%

<?xml version="1.0" encoding="UTF-8"?>
<rule>
    <strategy>
        <region-weight>dev=85;qa=15</region-weight>
    </strategy>
</rule>

如果希望每个服务的区域权重分别指定,那么策略内容如下,实现从Zuul发起的调用a服务dev区域流量权重为85%,qa区域流量权重为15%,b服务dev区域流量权重为75%,qa区域流量权重为25%

<?xml version="1.0" encoding="UTF-8"?>
<rule>
    <strategy>
        <region-weight>{"discovery-guide-service-a":"dev=85;qa=15", "discovery-guide-service-b":"dev=75;qa=25"}</region-weight>
    </strategy>
</rule>

当所有服务都选相同区域流量权重分配的时候,下面两条是等效的

<region-weight>dev=85;qa=15</region-weight>
<region-weight>{"discovery-guide-service-a":"dev=85;qa=15", "discovery-guide-service-b":"dev=85;qa=15"}</region-weight>

提醒:非条件驱动下的全链路灰度发布跟Header驱动下的全链路灰度发布等效,例如

n-d-region-weight=dev=85;qa=15
n-d-region-weight={"discovery-guide-service-a":"dev=85;qa=15", "discovery-guide-service-b":"dev=85;qa=15"}

全链路条件灰度发布

全链路版本条件权重灰度发布

规则策略配置

增加Zuul的版本条件权重灰度发布策略,Group为discovery-guide-group,Data Id为discovery-guide-zuul,策略内容如下

<?xml version="1.0" encoding="UTF-8"?>
<rule>
    <!-- 全局缺省路由,当兜底路由存在的时候,全局缺省路由不需要配置 -->
    <!-- 兜底路由如果把全局缺省路由的流量配比设置成100%,其它流量配比设置成0%,那么它等同于如下全局缺省路由 -->
    <!-- <strategy>
        <version>{"discovery-guide-service-a":"1.0", "discovery-guide-service-b":"1.0"}</version>
    </strategy> -->

    <strategy-release>
        <conditions type="gray">
            <!-- 灰度路由1,条件expression驱动 -->
            <!-- <condition id="gray-condition-1" expression="#H['a'] == '1'" version-id="gray-route=10;stable-route=90"/> -->
            <!-- 灰度路由2,条件expression驱动 -->
            <!-- <condition id="gray-condition-2" expression="#H['a'] == '1' and #H['b'] == '2'" version-id="gray-route=85;stable-route=15"/> -->
            <!-- 兜底路由,无条件expression驱动 -->
            <condition id="basic-condition" version-id="gray-route=0;stable-route=100"/>
        </conditions>

        <routes>
            <route id="gray-route" type="version">{"discovery-guide-service-a":"1.1", "discovery-guide-service-b":"1.1"}</route>
            <route id="stable-route" type="version">{"discovery-guide-service-a":"1.0", "discovery-guide-service-b":"1.0"}</route>
        </routes>
    </strategy-release>
</rule>

规则策略解释

网关随机权重调用服务,服务链路按照版本匹配方式调用

① 稳定版本路由和灰度版本路由流量权重分配

  • 稳定版本路由:a服务1.0版本向网关提供90%的流量,a服务1.0版本只能访问b服务1.0版本
  • 灰度版本路由:a服务1.1版本向网关提供10%的流量,a服务1.1版本只能访问b服务1.1版本

② gray-route链路配比10%的流量,stable-route链路配比90%的流量

③ 策略总共支持3种,可以单独一项使用,也可以多项叠加使用

  • version 版本
  • region 区域
  • address IP地址和端口

上述方式,可以通过全链路灰度发布编排建模方式执行,并通过全链路灰度发布流量侦测进行验证

全链路区域条件权重灰度发布

参考全链路版本条件权重灰度发布

用法相似,只需要把规则策略中

  • 属性version-id替换成region-id
  • 属性type="version"替换成type="region"
  • 节点route对应的Json中版本替换成区域

全链路IP地址和端口条件权重灰度发布

参考全链路版本条件权重灰度发布

用法相似,只需要把规则策略中

  • 属性version-id替换成address-id
  • 属性type="version"替换成type="address"
  • 节点route对应的Json中版本替换成IP地址和端口

全链路端到端混合实施蓝绿灰度发布

全链路端到端实施蓝绿灰度发布

前端 -> 网关 -> 服务全链路调用中,可以实施端到端蓝绿灰度发布

① 前端 -> 网关并行实施蓝绿灰度发布

当外界传值Header的时候,网关也设置并传递同名的Header,需要决定哪个Header传递到后边的服务去。需要通过如下开关做控制

# 当外界传值Header的时候,网关也设置并传递同名的Header,需要决定哪个Header传递到后边的服务去。如果下面开关为true,以网关设置为优先,否则以外界传值为优先。缺失则默认为true
spring.application.strategy.gateway.header.priority=false
# 当以网关设置为优先的时候,网关未配置Header,而外界配置了Header,仍旧忽略外界的Header。缺失则默认为true
spring.application.strategy.gateway.original.header.ignored=true

# 当外界传值Header的时候,网关也设置并传递同名的Header,需要决定哪个Header传递到后边的服务去。如果下面开关为true,以网关设置为优先,否则以外界传值为优先。缺失则默认为true
spring.application.strategy.zuul.header.priority=false
# 当以网关设置为优先的时候,网关未配置Header,而外界配置了Header,仍旧忽略外界的Header。缺失则默认为true
spring.application.strategy.zuul.original.header.ignored=true

② 网关 -> 服务并行实施蓝绿灰度发布

当网关传值Header的时候,服务也设置并传递同名的Header,需要决定哪个Header传递到后边的服务去。需要通过如下开关做控制

# 当外界传值Header的时候,服务也设置并传递同名的Header,需要决定哪个Header传递到后边的服务去。如果下面开关为true,以服务设置为优先,否则以外界传值为优先。缺失则默认为true
# spring.application.strategy.service.header.priority=true

全链路混合实施蓝绿灰度发布

网关 -> 服务全链路调用中,可以混合实施蓝绿灰度发布

① 网关上实施蓝绿发布,服务上实施灰度发布

② 网关上实施灰度发布,服务上实施蓝绿发布

上述两个发布场景,可以独立实施,互不影响,前提条件,需要控制服务上header.priority的开关

单节点混合实施蓝绿灰度发布

网关或者服务上的规则同时含有蓝绿灰度发布策略

本着蓝绿发布优先于灰度发布的原则,当前端传入参数a(Header、Parameter、Cookie其中一种)

  • 当a等于1,执行蓝绿发布,即a服务调用1.1版本,b服务调用1.1版本
  • 当a的值不命中,或者未传值,执行灰度发布,即服务a和b 1.1版本的链路流量分配为5%,服务a和b 1.0版本的链路流量分配为95%

分为如下两种方式,都可以达到相同的预期效果

① 通过蓝绿灰度混合方式

<?xml version="1.0" encoding="UTF-8"?>
<rule>
    <strategy-release>
        <conditions type="blue-green"> 
            <condition id="blue-condition" expression="#H['a'] == '1'" version-id="route-2"/>
        </conditions>

        <conditions type="gray">
            <condition id="gray-condition" version-id="route-1=95;route-2=5"/>
        </conditions>

        <routes>
            <route id="route-1" type="version">{"discovery-guide-service-a":"1.0", "discovery-guide-service-b":"1.0"}</route>
            <route id="route-2" type="version">{"discovery-guide-service-a":"1.1", "discovery-guide-service-b":"1.1"}</route>
        </routes>
    </strategy-release>
</rule>

② 通过纯灰度方式

把其中一个链路的流量分配为0%,达到蓝绿发布的效果

<?xml version="1.0" encoding="UTF-8"?>
<rule>
    <strategy-release>
        <conditions type="gray">
            <condition id="gray-condition-1" expression="#H['a'] == '1'" version-id="route-1=0;route-2=100"/>
            <condition id="gray-condition-2" version-id="route-1=95;route-2=5"/>
        </conditions>

        <routes>
            <route id="route-1" type="version">{"discovery-guide-service-a":"1.0", "discovery-guide-service-b":"1.0"}</route>
            <route id="route-2" type="version">{"discovery-guide-service-a":"1.1", "discovery-guide-service-b":"1.1"}</route>
        </routes>
    </strategy-release>
</rule>

混合蓝绿灰度发布的逻辑

<?xml version="1.0" encoding="UTF-8"?>
<rule>
    <strategy>
        <version>{"discovery-guide-service-a":"1.0", "discovery-guide-service-b":"1.0"}</version>
    </strategy>
    <strategy-release>
        <conditions type="blue-green">
            <condition id="condition-0" expression="#H['a'] == '1'" version-id="route-0"/>
            <condition id="condition-1" expression="#H['a'] == '2'" version-id="route-1"/>
            <condition id="basic-condition" version-id="route-0"/>
        </conditions>
        <conditions type="gray">
            <condition id="condition-0" expression="#H['a'] == '3'" version-id="route-0=10;route-1=90"/>
            <condition id="condition-1" expression="#H['a'] == '4'" version-id="route-0=40;route-1=60"/>
            <condition id="basic-condition" version-id="route-0=0;route-1=100"/>
        </conditions>
        <routes>
            <route id="route-0" type="version">{"discovery-guide-service-a":"1.0", "discovery-guide-service-b":"1.0"}</route>
            <route id="route-1" type="version">{"discovery-guide-service-a":"1.1", "discovery-guide-service-b":"1.1"}</route>
        </routes>
    </strategy-release>
</rule>

规则解读

原则:蓝绿规则优先于灰度规则

if (a == 1) {
    执行蓝绿发布blue-green的route-0下的路由
} else if (a == 2) {
    执行蓝绿发布blue-green的route-1下的路由
} else {
    执行蓝绿发布blue-green的route-0下的兜底路由
}

提醒:当蓝绿发布存在兜底策略(basic-condition),灰度发布永远不会被执行

如果删除掉蓝绿发布的兜底策略,那么执行逻辑则变为

if (a == 1) {
    执行蓝绿发布route-0下的路由
} else if (a == 2) {
    执行蓝绿发布route-1下的路由
} else if (a == 3) {
    执行灰度发布route-0=10;route-1=90下的流量百分比分配路由
} else if (a == 4) {
    执行灰度发布route-0=40;route-1=60下的流量百分比分配路由
} else {
    执行灰度发布route-0=0;route-1=100下的兜底路由
    由于赋予了route-0=0,那么流量会全部打到route-1上,相当于变种的蓝绿发布
}

如果删除掉蓝绿发布和灰度发布的兜底策略,那么执行逻辑则变为

if (a == 1) {
    执行蓝绿发布route-0下的路由
} else if (a == 2) {
    执行蓝绿发布route-1下的路由
} else if (a == 3) {
    执行灰度发布route-0=10;route-1=90下的流量百分比分配路由
} else if (a == 4) {
    执行灰度发布route-0=40;route-1=60下的流量百分比分配路由
} else {
    执行<strategy>下的全局兜底路由
}

蓝绿灰度混合发布执行逻辑图

全链路域网关和非域网关部署

全链路域网关部署

A部门服务访问B部门服务必须通过B部门网关

该部署模式下,本部门服务的蓝绿灰度发布只由本部门的网关来实施,其它部门无权对本部门服务实施蓝绿灰度发布,前提条件,需要控制网关上header.priority的开关

全链路非域网关部署

A部门服务直接访问B部门服务

该部署模式下,会发生本部门服务的蓝绿灰度发布会由其它部门的网关或者服务来触发,当本部门服务和其它部门服务在同一时刻实施蓝绿灰度发布的时候,会产生混乱。解决方案,参考并行发布下的版本偏好

全链路前端触发后端蓝绿灰度发布

前端可以直接触发后端蓝绿灰度发布,前提条件,需要控制网关和服务上header.priority的开关

全链路驱动方式

  • 版本匹配策略,Header格式如下任选一个
n-d-version=1.0
n-d-version={"discovery-guide-service-a":"1.0", "discovery-guide-service-b":"1.0"}
  • 版本权重策略,Header格式如下任选一个
n-d-version-weight=1.0=90;1.1=10
n-d-version-weight={"discovery-guide-service-a":"1.0=90;1.1=10", "discovery-guide-service-b":"1.0=90;1.1=10"}
  • 区域匹配策略,Header格式如下任选一个
n-d-region=qa
n-d-region={"discovery-guide-service-a":"qa", "discovery-guide-service-b":"qa"}
  • 区域权重策略,Header格式如下任选一个
n-d-region-weight=dev=99;qa=1
n-d-region-weight={"discovery-guide-service-a":"dev=99;qa=1", "discovery-guide-service-b":"dev=99;qa=1"}
  • IP地址和端口匹配策略,Header格式如下任选一个
n-d-address=3001;4002
n-d-address={"discovery-guide-service-a":"127.0.0.1:3001", "discovery-guide-service-b":"127.0.0.1:4002"}
n-d-address={"discovery-guide-service-a":"127.0.0.1", "discovery-guide-service-b":"127.0.0.1"}
n-d-address={"discovery-guide-service-a":"3001", "discovery-guide-service-b":"4002"}
  • 环境隔离下动态环境匹配策略
n-d-env=env1
  • 服务下线实时性的流量绝对无损,全局唯一ID屏蔽策略,Header格式如下任选一个
n-d-id-blacklist=20210601-222214-909-1146-372-698;20210601-222623-277-4978-633-279
n-d-id-blacklist={"discovery-guide-service-a":"20210601-222214-909-1146-372-698", "discovery-guide-service-b":"20210601-222623-277-4978-633-279"}
  • 服务下线实时性的流量绝对无损,IP地址和端口屏蔽策略,Header格式如下任选一个
n-d-address-blacklist=3001;4002
n-d-address-blacklist={"discovery-guide-service-a":"127.0.0.1:3001", "discovery-guide-service-b":"127.0.0.1:4002"}
n-d-address-blacklist={"discovery-guide-service-a":"127.0.0.1", "discovery-guide-service-b":"127.0.0.1"}
n-d-address-blacklist={"discovery-guide-service-a":"3001", "discovery-guide-service-b":"4002"}

全链路前端触发后端蓝绿灰度发布全景功能

全链路参数策略

① Header参数策略

基于标准Http传值方式

框架会默认把相关的Header,进行全链路传递,可以通过如下配置进行。除此之外,凡是以n-d-开头的任何Header,框架都会默认全链路传递

# 启动和关闭路由策略的时候,对REST方式的调用拦截。缺失则默认为true
spring.application.strategy.rest.intercept.enabled=true
# 启动和关闭Header传递的Debug日志打印,注意:每调用一次都会打印一次,会对性能有所影响,建议压测环境和生产环境关闭。缺失则默认为false
spring.application.strategy.rest.intercept.debug.enabled=true
# 路由策略的时候,对REST方式调用拦截的时候(支持Feign、RestTemplate或者WebClient调用),希望把来自外部自定义的Header参数(用于框架内置上下文Header,例如:trace-id, span-id等)传递到服务里,那么配置如下值。如果多个用“;”分隔,不允许出现空格
spring.application.strategy.context.request.headers=trace-id;span-id
# 路由策略的时候,对REST方式调用拦截的时候(支持Feign、RestTemplate或者WebClient调用),希望把来自外部自定义的Header参数(用于业务系统自定义Header,例如:mobile)传递到服务里,那么配置如下值。如果多个用“;”分隔,不允许出现空格
spring.application.strategy.business.request.headers=user;mobile;location

② Parameter参数策略

基于标准Http传值方式

http://localhost:5001/discovery-guide-service-a/invoke/gateway?a=1

http://localhost:5001/discovery-guide-service-a/invoke/gateway?a=2

③ Cookie参数策略 基于标准Http传值方式

④ 域名参数策略 基于取值域名前缀等方式,即可实现既定功能

本地测试,为验证结果,请事先在hosts文件中配置如下

127.0.0.1 common.nepxion.com
127.0.0.1 env1.nepxion.com
127.0.0.1 env2.nepxion.com
  • 根据env1.nepxion.com域名路由到env1环境

  • 根据common.nepxion.com域名路由到common环境

参考全链路自定义过滤器触发蓝绿灰度发布示例,以根据域名全链路环境隔离为例,根据域名前缀中的环境名路由到相应的全链路环境中

⑤ RPC-Method参数策略

基于取值RPC调用中的方法入参等方式,即可实现既定功能,该方式只适用于服务侧

参考全链路自定义负载均衡策略类触发蓝绿灰度发布示例

全局订阅式蓝绿灰度发布

如果使用者不希望通过全链路传递Header实现蓝绿灰度发布,框架提供另外一种规避Header传递的方式,即全局订阅式蓝绿灰度发布,也能达到Header传递一样的效果。以全链路版本匹配蓝绿发布为例

增加版本匹配的蓝绿发布策略,Group为discovery-guide-group,Data Id为discovery-guide-group(全局发布,两者都是组名),策略内容如下,实现a服务走1.0版本,b服务走1.1版本

<?xml version="1.0" encoding="UTF-8"?>
<rule>
    <strategy>
        <version>{"discovery-guide-service-a":"1.0", "discovery-guide-service-b":"1.1"}</version>
    </strategy>
</rule>

如果采用上述方式,可以考虑关闭下面的开关

# 启动和关闭核心策略Header传递,缺失则默认为true。当全局订阅启动时,可以关闭核心策略Header传递,这样可以节省传递数据的大小,一定程度上可以提升性能。核心策略Header,包含如下
# 1. n-d-version
# 2. n-d-region
# 3. n-d-address
# 4. n-d-version-weight
# 5. n-d-region-weight
# 6. n-d-id-blacklist
# 7. n-d-address-blacklist
# 8. n-d-env (不属于蓝绿灰度范畴的Header,只要外部传入就会全程传递)
spring.application.strategy.gateway.core.header.transmission.enabled=true
spring.application.strategy.zuul.core.header.transmission.enabled=true
spring.application.strategy.feign.core.header.transmission.enabled=true
spring.application.strategy.rest.template.core.header.transmission.enabled=true
spring.application.strategy.web.client.core.header.transmission.enabled=true

全链路自定义蓝绿灰度发布

全链路自定义过滤器触发蓝绿灰度发布

下面代码既适用于Zuul和Spring Cloud Gateway网关,也适用于微服务。继承DefaultGatewayStrategyRouteFilter、DefaultZuulStrategyRouteFilter和DefaultServiceStrategyRouteFilter,覆盖掉如下方法中的一个或者多个,通过@Bean方式覆盖框架内置的过滤类

public String getRouteVersion();

public String getRouteRegion();

public String getRouteEnvironment();

public String getRouteAddress();

public String getRouteVersionWeight();

public String getRouteRegionWeight();

public String getRouteIdBlacklist();

public String getRouteAddressBlacklist();

GatewayStrategyRouteFilter示例

// 适用于A/B Testing或者更根据某业务参数决定蓝绿灰度路由路径。可以结合配置中心分别配置A/B两条路径,可以动态改变并通知
// 当Header中传来的用户为张三,执行一条路由路径;为李四,执行另一条路由路径
public class MyGatewayStrategyRouteFilter extends DefaultGatewayStrategyRouteFilter {
    private static final Logger LOG = LoggerFactory.getLogger(MyGatewayStrategyRouteFilter.class);

    private static final String DEFAULT_A_ROUTE_VERSION = "{\"discovery-guide-service-a\":\"1.0\", \"discovery-guide-service-b\":\"1.1\"}";
    private static final String DEFAULT_B_ROUTE_VERSION = "{\"discovery-guide-service-a\":\"1.1\", \"discovery-guide-service-b\":\"1.0\"}";
    private static final String DEFAULT_A_ROUTE_REGION = "{\"discovery-guide-service-a\":\"dev\", \"discovery-guide-service-b\":\"qa\"}";
    private static final String DEFAULT_B_ROUTE_REGION = "{\"discovery-guide-service-a\":\"qa\", \"discovery-guide-service-b\":\"dev\"}";
    private static final String DEFAULT_A_ROUTE_ADDRESS = "{\"discovery-guide-service-a\":\"3001\", \"discovery-guide-service-b\":\"4002\"}";
    private static final String DEFAULT_B_ROUTE_ADDRESS = "{\"discovery-guide-service-a\":\"3002\", \"discovery-guide-service-b\":\"4001\"}";

    @Value("${a.route.version:" + DEFAULT_A_ROUTE_VERSION + "}")
    private String aRouteVersion;

    @Value("${b.route.version:" + DEFAULT_B_ROUTE_VERSION + "}")
    private String bRouteVersion;

    @Value("${a.route.region:" + DEFAULT_A_ROUTE_REGION + "}")
    private String aRouteRegion;

    @Value("${b.route.region:" + DEFAULT_B_ROUTE_REGION + "}")
    private String bRouteRegion;

    @Value("${a.route.address:" + DEFAULT_A_ROUTE_ADDRESS + "}")
    private String aRouteAddress;

    @Value("${b.route.address:" + DEFAULT_B_ROUTE_ADDRESS + "}")
    private String bRouteAddress;

    // 自定义根据Header全链路版本匹配路由
    @Override
    public String getRouteVersion() {
        String user = strategyContextHolder.getHeader("user");

        LOG.info("自定义根据Header全链路版本匹配路由, Header user={}", user);

        if (StringUtils.equals(user, "zhangsan")) {
            LOG.info("执行全链路版本匹配路由={}", aRouteVersion);

            return aRouteVersion;
        } else if (StringUtils.equals(user, "lisi")) {
            LOG.info("执行全链路版本匹配路由={}", bRouteVersion);

            return bRouteVersion;
        }

        return super.getRouteVersion();
    }

    // 自定义根据Parameter全链路区域匹配路由
    @Override
    public String getRouteRegion() {
        String user = strategyContextHolder.getParameter("user");

        LOG.info("自定义根据Parameter全链路区域匹配路由, Parameter user={}", user);

        if (StringUtils.equals(user, "zhangsan")) {
            LOG.info("执行全链路区域匹配路由={}", aRouteRegion);

            return aRouteRegion;
        } else if (StringUtils.equals(user, "lisi")) {
            LOG.info("执行全链路区域匹配路由={}", bRouteRegion);

            return bRouteRegion;
        }

        return super.getRouteRegion();
    }

    // 自定义根据Cookie全链路IP地址和端口匹配路由
    @Override
    public String getRouteAddress() {
        String user = strategyContextHolder.getCookie("user");

        LOG.info("自定义根据Cookie全链路IP地址和端口匹配路由, Cookie user={}", user);

        if (StringUtils.equals(user, "zhangsan")) {
            LOG.info("执行全链路IP地址和端口匹配路由={}", aRouteAddress);

            return aRouteAddress;
        } else if (StringUtils.equals(user, "lisi")) {
            LOG.info("执行全链路IP地址和端口匹配路由={}", bRouteAddress);

            return bRouteAddress;
        }

        return super.getRouteAddress();
    }

    @Autowired
    private GatewayStrategyContextHolder gatewayStrategyContextHolder;

    // 自定义根据域名全链路环境隔离
    @Override
    public String getRouteEnvironment() {
        String host = gatewayStrategyContextHolder.getURI().getHost();
        if (host.contains("nepxion.com")) {
            LOG.info("自定义根据域名全链路环境隔离, URL={}", host);

            String environment = host.substring(0, host.indexOf("."));

            LOG.info("执行全链路环境隔离={}", environment);

            return environment;
        }

        return super.getRouteEnvironment();
    }

    // 自定义全链路版本权重路由
    /*@Override
    public String getRouteVersion() {
        LOG.info("自定义全链路版本权重路由");

        List<Pair<String, Integer>> weightList = new ArrayList<Pair<String, Integer>>();
        weightList.add(new ImmutablePair<String, Integer>(aRouteVersion, 30));
        weightList.add(new ImmutablePair<String, Integer>(bRouteVersion, 70));
        MapWeightRandom<String, Integer> weightRandom = new MapWeightRandom<String, Integer>(weightList);

        return weightRandom.random();
    }*/
}

在配置类里@Bean方式进行过滤类创建,覆盖框架内置的过滤类

@Bean
public GatewayStrategyRouteFilter gatewayStrategyRouteFilter() {
    return new MyGatewayStrategyRouteFilter();
}

ZuulStrategyRouteFilter示例

// 适用于A/B Testing或者更根据某业务参数决定蓝绿灰度路由路径。可以结合配置中心分别配置A/B两条路径,可以动态改变并通知
// 当Header中传来的用户为张三,执行一条路由路径;为李四,执行另一条路由路径
public class MyZuulStrategyRouteFilter extends DefaultZuulStrategyRouteFilter {
    private static final Logger LOG = LoggerFactory.getLogger(MyZuulStrategyRouteFilter.class);

    private static final String DEFAULT_A_ROUTE_VERSION = "{\"discovery-guide-service-a\":\"1.0\", \"discovery-guide-service-b\":\"1.1\"}";
    private static final String DEFAULT_B_ROUTE_VERSION = "{\"discovery-guide-service-a\":\"1.1\", \"discovery-guide-service-b\":\"1.0\"}";
    private static final String DEFAULT_A_ROUTE_REGION = "{\"discovery-guide-service-a\":\"dev\", \"discovery-guide-service-b\":\"qa\"}";
    private static final String DEFAULT_B_ROUTE_REGION = "{\"discovery-guide-service-a\":\"qa\", \"discovery-guide-service-b\":\"dev\"}";
    private static final String DEFAULT_A_ROUTE_ADDRESS = "{\"discovery-guide-service-a\":\"3001\", \"discovery-guide-service-b\":\"4002\"}";
    private static final String DEFAULT_B_ROUTE_ADDRESS = "{\"discovery-guide-service-a\":\"3002\", \"discovery-guide-service-b\":\"4001\"}";

    @Value("${a.route.version:" + DEFAULT_A_ROUTE_VERSION + "}")
    private String aRouteVersion;

    @Value("${b.route.version:" + DEFAULT_B_ROUTE_VERSION + "}")
    private String bRouteVersion;

    @Value("${a.route.region:" + DEFAULT_A_ROUTE_REGION + "}")
    private String aRouteRegion;

    @Value("${b.route.region:" + DEFAULT_B_ROUTE_REGION + "}")
    private String bRouteRegion;

    @Value("${a.route.address:" + DEFAULT_A_ROUTE_ADDRESS + "}")
    private String aRouteAddress;

    @Value("${b.route.address:" + DEFAULT_B_ROUTE_ADDRESS + "}")
    private String bRouteAddress;

    // 自定义根据Header全链路版本匹配路由
    @Override
    public String getRouteVersion() {
        String user = strategyContextHolder.getHeader("user");

        LOG.info("自定义根据Header全链路版本匹配路由, Header user={}", user);

        if (StringUtils.equals(user, "zhangsan")) {
            LOG.info("执行全链路版本匹配路由={}", aRouteVersion);

            return aRouteVersion;
        } else if (StringUtils.equals(user, "lisi")) {
            LOG.info("执行全链路版本匹配路由={}", bRouteVersion);

            return bRouteVersion;
        }

        return super.getRouteVersion();
    }

    // 自定义根据Parameter全链路区域匹配路由
    @Override
    public String getRouteRegion() {
        String user = strategyContextHolder.getParameter("user");

        LOG.info("自定义根据Parameter全链路区域匹配路由, Parameter user={}", user);

        if (StringUtils.equals(user, "zhangsan")) {
            LOG.info("执行全链路区域匹配路由={}", aRouteRegion);

            return aRouteRegion;
        } else if (StringUtils.equals(user, "lisi")) {
            LOG.info("执行全链路区域匹配路由={}", bRouteRegion);

            return bRouteRegion;
        }

        return super.getRouteRegion();
    }

    // 自定义根据Cookie全链路IP地址和端口匹配路由
    @Override
    public String getRouteAddress() {
        String user = strategyContextHolder.getCookie("user");

        LOG.info("自定义根据Cookie全链路IP地址和端口匹配路由, Cookie user={}", user);

        if (StringUtils.equals(user, "zhangsan")) {
            LOG.info("执行全链路IP地址和端口匹配路由={}", aRouteAddress);

            return aRouteAddress;
        } else if (StringUtils.equals(user, "lisi")) {
            LOG.info("执行全链路IP地址和端口匹配路由={}", bRouteAddress);

            return bRouteAddress;
        }

        return super.getRouteEnvironment();
    }

    @Autowired
    private ZuulStrategyContextHolder zuulStrategyContextHolder;

    // 自定义根据域名全链路环境隔离
    @Override
    public String getRouteEnvironment() {
        String requestURL = zuulStrategyContextHolder.getRequestURL();
        if (requestURL.contains("nepxion.com")) {
            LOG.info("自定义根据域名全链路环境隔离, URL={}", requestURL);

            String host = requestURL.substring("http://".length(), requestURL.length());
            String environment = host.substring(0, host.indexOf("."));

            LOG.info("执行全链路环境隔离={}", environment);

            return environment;
        }

        return super.getRouteEnvironment();
    }

    // 自定义全链路版本权重路由
    /*@Override
    public String getRouteVersion() {
        LOG.info("自定义全链路版本权重路由");

        List<Pair<String, Integer>> weightList = new ArrayList<Pair<String, Integer>>();
        weightList.add(new ImmutablePair<String, Integer>(aRouteVersion, 30));
        weightList.add(new ImmutablePair<String, Integer>(bRouteVersion, 70));
        MapWeightRandom<String, Integer> weightRandom = new MapWeightRandom<String, Integer>(weightList);

        return weightRandom.random();
    }*/
}

在配置类里@Bean方式进行过滤类创建,覆盖框架内置的过滤类

@Bean
public ZuulStrategyRouteFilter zuulStrategyRouteFilter() {
    return new MyZuulStrategyRouteFilter();
}

ServiceStrategyRouteFilter示例

// 适用于A/B Testing或者更根据某业务参数决定蓝绿灰度路由路径。可以结合配置中心分别配置A/B两条路径,可以动态改变并通知
// 当Header中传来的用户为张三,执行一条路由路径;为李四,执行另一条路由路径
public class MyServiceStrategyRouteFilter extends DefaultServiceStrategyRouteFilter {
    private static final Logger LOG = LoggerFactory.getLogger(MyServiceStrategyRouteFilter.class);

    private static final String DEFAULT_A_ROUTE_VERSION = "{\"discovery-guide-service-a\":\"1.0\", \"discovery-guide-service-b\":\"1.1\"}";
    private static final String DEFAULT_B_ROUTE_VERSION = "{\"discovery-guide-service-a\":\"1.1\", \"discovery-guide-service-b\":\"1.0\"}";
    private static final String DEFAULT_A_ROUTE_REGION = "{\"discovery-guide-service-a\":\"dev\", \"discovery-guide-service-b\":\"qa\"}";
    private static final String DEFAULT_B_ROUTE_REGION = "{\"discovery-guide-service-a\":\"qa\", \"discovery-guide-service-b\":\"dev\"}";
    private static final String DEFAULT_A_ROUTE_ADDRESS = "{\"discovery-guide-service-a\":\"3001\", \"discovery-guide-service-b\":\"4002\"}";
    private static final String DEFAULT_B_ROUTE_ADDRESS = "{\"discovery-guide-service-a\":\"3002\", \"discovery-guide-service-b\":\"4001\"}";

    @Value("${a.route.version:" + DEFAULT_A_ROUTE_VERSION + "}")
    private String aRouteVersion;

    @Value("${b.route.version:" + DEFAULT_B_ROUTE_VERSION + "}")
    private String bRouteVersion;

    @Value("${a.route.region:" + DEFAULT_A_ROUTE_REGION + "}")
    private String aRouteRegion;

    @Value("${b.route.region:" + DEFAULT_B_ROUTE_REGION + "}")
    private String bRouteRegion;

    @Value("${a.route.address:" + DEFAULT_A_ROUTE_ADDRESS + "}")
    private String aRouteAddress;

    @Value("${b.route.address:" + DEFAULT_B_ROUTE_ADDRESS + "}")
    private String bRouteAddress;

    // 自定义根据Header全链路版本匹配路由
    @Override
    public String getRouteVersion() {
        String user = strategyContextHolder.getHeader("user");

        LOG.info("自定义根据Header全链路版本匹配路由, Header user={}", user);

        if (StringUtils.equals(user, "zhangsan")) {
            LOG.info("执行全链路版本匹配路由={}", aRouteVersion);

            return aRouteVersion;
        } else if (StringUtils.equals(user, "lisi")) {
            LOG.info("执行全链路版本匹配路由={}", bRouteVersion);

            return bRouteVersion;
        }

        return super.getRouteVersion();
    }

    // 自定义根据Parameter全链路区域匹配路由
    @Override
    public String getRouteRegion() {
        String user = strategyContextHolder.getParameter("user");

        LOG.info("自定义根据Parameter全链路区域匹配路由, Parameter user={}", user);

        if (StringUtils.equals(user, "zhangsan")) {
            LOG.info("执行全链路区域匹配路由={}", aRouteRegion);

            return aRouteRegion;
        } else if (StringUtils.equals(user, "lisi")) {
            LOG.info("执行全链路区域匹配路由={}", bRouteRegion);

            return bRouteRegion;
        }

        return super.getRouteRegion();
    }

    // 自定义根据Cookie全链路IP地址和端口匹配路由
    @Override
    public String getRouteAddress() {
        String user = strategyContextHolder.getCookie("user");

        LOG.info("自定义根据Cookie全链路IP地址和端口匹配路由, Cookie user={}", user);

        if (StringUtils.equals(user, "zhangsan")) {
            LOG.info("执行全链路IP地址和端口匹配路由={}", aRouteAddress);

            return aRouteAddress;
        } else if (StringUtils.equals(user, "lisi")) {
            LOG.info("执行全链路IP地址和端口匹配路由={}", bRouteAddress);

            return bRouteAddress;
        }

        return super.getRouteEnvironment();
    }

    @Autowired
    private ServiceStrategyContextHolder serviceStrategyContextHolder;

    // 自定义根据域名全链路环境隔离
    @Override
    public String getRouteEnvironment() {
        String requestURL = serviceStrategyContextHolder.getRequestURL();
        if (requestURL.contains("nepxion.com")) {
            LOG.info("自定义根据域名全链路环境隔离, URL={}", requestURL);

            String host = requestURL.substring("http://".length(), requestURL.length());
            String environment = host.substring(0, host.indexOf("."));

            LOG.info("执行全链路环境隔离={}", environment);

            return environment;
        }

        return super.getRouteEnvironment();
    }

    // 自定义全链路版本权重路由
    /*@Override
    public String getRouteVersion() {
        LOG.info("自定义全链路版本权重路由");

        List<Pair<String, Integer>> weightList = new ArrayList<Pair<String, Integer>>();
        weightList.add(new ImmutablePair<String, Integer>(aRouteVersion, 30));
        weightList.add(new ImmutablePair<String, Integer>(bRouteVersion, 70));
        MapWeightRandom<String, Integer> weightRandom = new MapWeightRandom<String, Integer>(weightList);

        return weightRandom.random();
    }*/
}

在配置类里@Bean方式进行过滤类创建,覆盖框架内置的过滤类

@Bean
public ServiceStrategyRouteFilter serviceStrategyRouteFilter() {
    return new MyServiceStrategyRouteFilter();
}

全链路自定义负载均衡策略类触发蓝绿灰度发布

〔Spring Cloud 202x版〕特别提醒

对于Spring Cloud 202x版,由于它已经移除了Ribbon,所以apply(Server server)方法上的入参,com.netflix.loadbalancer.Server需要改成org.springframework.cloud.client.ServiceInstance

下面代码既适用于Zuul和Spring Cloud Gateway网关,也适用于微服务。继承DefaultDiscoveryEnabledStrategy,可以有多个,通过@Bean方式注入

// 实现了组合策略,版本路由策略+区域路由策略+IP地址和端口路由策略+自定义策略
public class MyDiscoveryEnabledStrategy extends DefaultDiscoveryEnabledStrategy {
    private static final Logger LOG = LoggerFactory.getLogger(MyDiscoveryEnabledStrategy.class);

    // 对REST调用传来的Header参数(例如:mobile)做策略
    @Override
    public boolean apply(Server server) {
        String mobile = strategyContextHolder.getHeader("mobile");
        String serviceId = pluginAdapter.getServerServiceId(server);
        String version = pluginAdapter.getServerVersion(server);
        String region = pluginAdapter.getServerRegion(server);
        String environment = pluginAdapter.getServerEnvironment(server);
        String address = server.getHost() + ":" + server.getPort();

        LOG.info("负载均衡用户定制触发:mobile={}, serviceId={}, version={}, region={}, env={}, address={}", mobile, serviceId, version, region, environment, address);

        if (StringUtils.isNotEmpty(mobile)) {
            // 手机号以移动138开头,路由到1.0版本的服务上
            if (mobile.startsWith("138") && StringUtils.equals(version, "1.0")) {
                return true;
                // 手机号以联通133开头,路由到2.0版本的服务上
            } else if (mobile.startsWith("133") && StringUtils.equals(version, "1.1")) {
                return true;
            } else {
                // 其它情况,直接拒绝请求
                return false;
            }
        }

        return true;
    }
}

在配置类里@Bean方式进行策略类创建

@Bean
public DiscoveryEnabledStrategy discoveryEnabledStrategy() {
    return new MyDiscoveryEnabledStrategy();
}

服务除了支持网关那种基于Rest参数的方式之外,还支持基于Rpc方法参数的方式,它包括接口名、方法名、参数名或参数值等多种形式

// 实现了组合策略,版本路由策略+区域路由策略+IP地址和端口路由策略+自定义策略
public class MyDiscoveryEnabledStrategy implements DiscoveryEnabledStrategy {
    private static final Logger LOG = LoggerFactory.getLogger(MyDiscoveryEnabledStrategy.class);

    @Autowired
    private PluginAdapter pluginAdapter;

    @Autowired
    private ServiceStrategyContextHolder serviceStrategyContextHolder;

    @Override
    public boolean apply(Server server) {
        boolean enabled = applyFromHeader(server);
        if (!enabled) {
            return false;
        }

        return applyFromMethod(server);
    }

    // 根据REST调用传来的Header参数(例如:mobile),选取执行调用请求的服务实例
    private boolean applyFromHeader(Server server) {
        String mobile = serviceStrategyContextHolder.getHeader("mobile");
        String serviceId = pluginAdapter.getServerServiceId(server);
        String version = pluginAdapter.getServerVersion(server);
        String region = pluginAdapter.getServerRegion(server);
        String environment = pluginAdapter.getServerEnvironment(server);
        String address = server.getHost() + ":" + server.getPort();

        LOG.info("负载均衡用户定制触发:mobile={}, serviceId={}, version={}, region={}, env={}, address={}", mobile, serviceId, version, region, environment, address);

        if (StringUtils.isNotEmpty(mobile)) {
            // 手机号以移动138开头,路由到1.0版本的服务上
            if (mobile.startsWith("138") && StringUtils.equals(version, "1.0")) {
                return true;
                // 手机号以联通133开头,路由到2.0版本的服务上
            } else if (mobile.startsWith("133") && StringUtils.equals(version, "1.1")) {
                return true;
            } else {
                // 其它情况,直接拒绝请求
                return false;
            }
        }

        return true;
    }

    // 根据RPC调用传来的方法参数(例如接口名、方法名、参数名或参数值等),选取执行调用请求的服务实例
    // 本示例只作用在discovery-guide-service-a服务上
    @SuppressWarnings("unchecked")
    private boolean applyFromMethod(Server server) {
        Map<String, Object> attributes = serviceStrategyContextHolder.getRpcAttributes();
        String serviceId = pluginAdapter.getServerServiceId(server);
        String version = pluginAdapter.getServerVersion(server);
        String region = pluginAdapter.getServerRegion(server);
        String environment = pluginAdapter.getServerEnvironment(server);
        String address = server.getHost() + ":" + server.getPort();

        LOG.info("负载均衡用户定制触发:attributes={}, serviceId={}, version={}, region={}, env={}, address={}", attributes, serviceId, version, region, environment, address);

        if (attributes.containsKey(DiscoveryConstant.PARAMETER_MAP)) {
            Map<String, Object> parameterMap = (Map<String, Object>) attributes.get(DiscoveryConstant.PARAMETER_MAP);
            String value = parameterMap.get("value").toString();
            if (StringUtils.isNotEmpty(value)) {
                // 输入值包含dev,路由到dev区域的服务上
                if (value.contains("dev") && StringUtils.equals(region, "dev")) {
                    return true;
                    // 输入值包含qa,路由到qa区域的服务上
                } else if (value.contains("qa") && StringUtils.equals(region, "qa")) {
                    return true;
                } else {
                    // 其它情况,直接通过请求
                    return true;
                }
            }
        }

        return true;
    }
}

需要通过如下开关开启该功能

# 启动和关闭路由策略的时候,对RPC方式的调用拦截。缺失则默认为false
spring.application.strategy.rpc.intercept.enabled=true

全链路动态变更元数据的蓝绿灰度发布

利用注册中心的Open API接口动态变更服务实例的元数据,达到稳定版本和灰度版本流量灰度控制的目的

以Nacos注册中心的版本匹配路由为例

老的稳定版本的服务实例配置版本元数据,如下

spring.cloud.nacos.discovery.metadata.version=stable

新的稳定版本的服务实例配置版本元数据,如下

spring.cloud.nacos.discovery.metadata.version=gray

路由策略,如下

表示所有的服务流量走灰度版本

<?xml version="1.0" encoding="UTF-8"?>
<rule>
    <strategy>
        <version>gray</version>
    </strategy>
</rule>

表示a服务流量走灰度版本,b服务流量走稳定版本

<?xml version="1.0" encoding="UTF-8"?>
<rule>
    <strategy>
        <version>{"discovery-guide-service-a":"gray", "discovery-guide-service-b":"stable"}</version>
    </strategy>
</rule>

也可以通过全链路传递Header方式实现

n-d-version=gray
n-d-version={"discovery-guide-service-a":"gray", "discovery-guide-service-b":"stable"}

新上线的服务实例版本为gray,即默认是灰度版本。等灰度成功后,通过注册中心的Open API接口变更服务版本为stable,或者在注册中心界面手工修改

  • Nacos Open API变更元数据
curl -X PUT 'http://ip:port/nacos/v1/ns/instance?serviceName={appId}&ip={ip}&port={port}&metadata={"version", "stable"}'

Nacos Open API使用手册,参考https://nacos.io/zh-cn/docs/open-api.html

  • Eureka Open API变更元数据
curl -X PUT 'http://ip:port/eureka/apps/{appId}/{instanceId}/metadata?version=stable'
  • Consul Open API变更元数据

自行研究

  • Zookeeper Open API变更元数据

自行研究

需要注意

① 并非所有的注册中心都支持动态元数据变更方式,需要使用者自行研究

② 动态元数据变更方式利用第三方注册中心的Open API达到最终目的,其可能具有一定的延迟性,不如本框架那样具有蓝绿灰度发布实时生效的特征,但比本框架动态变更蓝绿灰度发布简单了一些

③ 动态元数据变更方式只是让新的元数据驻留在内存里,并不持久化。当服务重启后,服务的元数据仍旧会以初始值为准

全链路蓝绿灰度发布平台界面

请访问Discovery Platform WIKI

全链路蓝绿灰度发布图形化桌面

① 获取图形化桌面端

桌面端获取方式有两种方式

② 启动控制台

  • 通过https://github.com/Nepxion/DiscoveryPlatform下载最新版本的控制台
  • 导入IDE或者编译成Spring Boot程序运行
  • 运行之前,先修改src/main/resources/bootstrap.properties的相关配置,包括注册中心和配置中心的地址等

③ 启动图形化桌面端

  • 修改config/console.properties中的url,指向控制台的地址
  • 在Windows操作系统下,运行startup.bat,在Mac或者Linux操作系统下,运行startup.sh

④ 登录图形化桌面端

登录认证,用户名和密码为admin/admin或者nepxion/nepxion。控制台支持简单的认证,用户名和密码配置在上述控制台的bootstrap.properties中,使用者可以自己扩展AuthenticationResource并注入,实现更专业的认证功能

全链路编排建模

全链路编排建模工具,只提供最经典和最常用的蓝绿灰度发布场景功能,并不覆盖框架所有的功能

全链路蓝绿发布编排建模

① 导航栏上选择〔全链路服务蓝绿发布〕

② 〔全链路服务蓝绿发布〕界面的工具栏上,点击【新建】按钮,弹出【新建配置】对话框。确认下面选项后,点击【确定】按钮后,进行全链路蓝绿发布编排建模

  • 〔订阅参数〕项。选择〔局部订阅〕或者〔全局订阅〕,通过下拉菜单〔订阅组名〕和〔订阅服务名〕,〔订阅服务名〕可以选择网关(以网关为发布入口)或者服务(以服务为发布入口)。如果是〔全局订阅〕,则不需要选择〔订阅服务名〕
  • 〔部署参数〕项。选择〔域网关模式〕(发布界面上提供只属于〔订阅组〕下的服务列表)或者〔非域网模式〕(发布界面上提供所有服务列表)
  • 〔发布策略〕项。选择〔版本策略〕或者〔区域策略〕
  • 〔路由类型〕项。选择〔蓝 | 绿 | 兜底〕或者〔蓝 | 兜底〕

根据全链路版本条件匹配蓝绿发布示例中的场景

③ 在〔蓝绿条件〕中,分别输入〔蓝条件〕和〔绿条件〕

  • 〔蓝条件〕输入a==1
  • 〔绿条件〕输入a==1&&b==2

使用者可以通过〔条件校验〕来判断条件是否正确。例如,在〔绿条件〕区的校验文本框里,输入a=1,执行校验,将提示〔校验结果:false〕,输入a=1;b=2,将提示〔校验结果:true〕

④ 在〔蓝绿编排〕中,分别选择如下服务以及其版本,并点击【添加】按钮,把路由链路添加到拓扑图上

  • 服务discovery-guide-service-a,〔蓝版本〕=1.1,〔绿版本〕=1.0,〔兜底版本〕=1.0
  • 服务discovery-guide-service-b,〔蓝版本〕=1.1,〔绿版本〕=1.0,〔兜底版本〕=1.0

⑤ 如果希望内置Header参数,可以〔蓝绿参数〕的文本框中输入

⑥ 全链路编排建模完毕,点击工具栏上【保存】按钮进行保存,也可以先点击【预览】按钮,在弹出的【预览配置】对话框中,确认规则策略无误后再保存。使用者可以访问Nacos界面查看相关的规则策略是否已经存在

⑦ 对于已经存在的策略配置,可以通过点击工具栏上【打开】按钮,在弹出的【打开配置】对话框中,根据上述逻辑相似,确定〔订阅参数〕项后,选择〔打开远程配置〕(载入Nacos上对应的规则策略)或者〔打开本地配置〕(载入本地硬盘上规则策略文件rule.xml)

⑧ 对于已经存在的策略配置,如果想重置清除掉,点击工具栏上【重置】按钮进行重置清除

全链路灰度发布编排建模

① 导航栏上选择〔全链路服务灰度发布〕

根据全链路版本条件权重灰度发布示例中的场景

② 在〔灰度条件〕中,〔灰度条件〕(灰度流量占比)选择95%,〔稳定条件〕(稳定流量占比)会自动切换成5%

其它步骤跟全链路蓝绿发布编排建模相似,但比其简单

全链路流量侦测

全链路蓝绿发布流量侦测

① 导航栏上选择〔全链路服务流量侦测〕

② 在〔侦测入口〕中,操作如下

  • 〔类型〕项。选择〔网关〕或者〔服务〕,本示例的规则策略是配置在网关上,所以选择〔网关〕
  • 〔协议〕项。选择〔http://〕或者〔https://〕,视网关或者服务暴露出来的协议类型而定,本示例暴露出来的是http协议,所以选择〔http://〕
  • 〔服务〕项。选择一个网关名或者服务名,下拉菜单列表随着〔类型〕项的改变而改变,蓝绿发布规则策略是配置在discovery-guide-gateway上,所以选择它
  • 〔实例〕项。选择一个网关实例或者服务实例的IP地址和端口,下拉菜单列表随着〔服务〕的改变而改变

③ 在〔侦测参数〕中,操作如下

添加〔Header〕项和〔Parameter〕项,也可以〔Cookie〕项,使用者可以任意选择2个

  • 〔Header〕项。输入a=1
  • 〔Parameter〕项。输入b=2

④ 在〔侦测链路〕中,操作如下

  • 增加服务discovery-guide-service-a
  • 增加服务discovery-guide-service-b

⑤ 在〔侦测执行〕中,操作如下

  • 〔维护〕项。选择〔版本〕、〔区域〕、〔环境〕、〔可用区〕、〔地址〕或者〔组〕,维护表示在拓扑图上聚合调用场景的维度,本示例的规则策略是是基于版本维度进行发布,所以选择〔版本〕
  • 〔次数〕项。选择执行侦测的次数,基于网关和服务的性能压力,使用者需要酌情考虑调用次数
  • 〔次数〕项。选择执行侦测的同一时刻线程并发数,并发数是对于图形化桌面端而言的
  • 〔成功〕项。用来显示侦测成功的百分比
  • 〔失败〕项。用来显示侦测失败的百分比
  • 〔耗时〕项。用来显示侦测执行的消耗时间

⑥ 点击工具栏上【开始】按钮开始侦测,在侦测执行过程中,可以点击工具栏上【停止】按钮停止侦测

从上述截图中,可以看到

  • 在条件a==1&&b==2的〔绿条件〕下,执行〔网关〕->〔a服务1.0版本〕->〔b服务1.0版本〕的〔绿路由〕

⑦ 点击工具栏上【查看】按钮查看拓扑图上所有节点配置的规则策略,包括局部配置和全局配置

⑧ 支持直接n-d-version策略路由Header驱动的蓝绿发布流量侦测

全链路灰度发布流量侦测

① 导航栏上选择〔全链路服务流量侦测〕

② 在〔侦测入口〕中,操作如下

  • 〔服务〕项。灰度发布规则策略是配置在discovery-guide-zuul上,所以选择它

③ 在〔侦测参数〕中,不需要输入任何值

④ 在〔侦测执行〕中,〔次数〕项的值越大,灰度权重百分比越准确

其它步骤跟全链路蓝绿发布流量侦测相似,但比其简单

从上述截图中,可以看到

  • 执行〔网关〕->〔a服务1.1版本〕->〔b服务1.1版本〕的〔灰度路由〕权重百分比95%左右
  • 执行〔网关〕->〔a服务1.0版本〕->〔b服务1.0版本〕的〔稳定路由〕权重百分比5%左右

全链路蓝绿灰度发布混合流量侦测

① 全链路蓝绿发布 + 灰度发布混合模式下流量侦测

在网关上配置了蓝绿发布规则策略,在a服务上配置了灰度发布规则策略

② 全链路灰度发布 + 蓝绿发布混合模式下流量侦测

在网关上配置了灰度发布规则策略,在a服务上配置了蓝绿发布规则策略

③ 全链路流量侦测接口

通过discovery-plugin-admin-center-starter内置基于LoadBalanced RestTemplate的接口方法,实现全链路侦测,用于查看全链路中调用的各个服务的版本、区域、环境、可用区、IP地址和端口等是否符合和满足蓝绿灰度条件。使用方式,如下

服务的Rest Endpoint接口

操作 路径 参数 方式
网关为入口 http://[网关IP:PORT]/[A服务名]/inspector/inspect {"serviceIdList":["B服务名", "C服务名", ...]} POST
服务为入口 http://[A服务IP:PORT]/inspector/inspect {"serviceIdList":["B服务名", "C服务名", ...]} POST

提醒:内容项中服务名列表不分前后次序

全链路蓝绿灰度发布容灾

发布失败下的版本故障转移

版本故障转移,即无法找到相应版本的服务实例,路由到老的稳定版本的实例。其作用是防止蓝绿灰度版本发布人为设置错误,或者对应的版本实例发生灾难性的全部下线,导致流量有损

故障转移方式,对版本号进行排序,此解决方案的前置条件是版本号必须是规律的有次序,例如,以时间戳的方式。如果所有服务实例的版本号未设置,那么将转移到未设置版本号的实例上

需要通过如下开关开启该功能

# 启动和关闭版本故障转移。缺失则默认为false
spring.application.strategy.version.failover.enabled=true

并行发布下的版本偏好

版本偏好,即非蓝绿灰度发布场景下,路由到老的稳定版本的实例。其作用是防止多个网关上并行实施蓝绿灰度版本发布产生混乱,对处于非蓝绿灰度状态的服务,调用它的时候,只取它的老的稳定版本的实例;蓝绿灰度状态的服务,还是根据传递的Header版本号进行匹配

偏好方式,对版本号进行排序,此解决方案的前置条件是版本号必须是规律的有次序,例如,以时间戳的方式。如果所有服务实例的版本号未设置,那么将转移到未设置版本号的实例上

需要通过如下开关开启该功能

# 启动和关闭版本偏好。缺失则默认为false
spring.application.strategy.version.prefer.enabled=true

服务下线场景下全链路蓝绿灰度发布

服务下线场景下,由于Ribbon负载均衡组件存在着缓存机制,当被提供端服务实例已经下线,而消费端服务实例还暂时缓存着它,直到下个心跳周期才会把已下线的服务实例剔除,在此期间,如果发生调用,会造成流量有损

框架提供流量的实时性绝对无损策略。采用下线之前,把服务实例添加到屏蔽名单中,负载均衡不会去寻址该服务实例。下线之后,清除该名单。实现该方式,需要通过DevOps调用配置中心的Open API推送或者在配置中心界面手工修改

全局唯一ID屏蔽

全局唯一ID对应于元数据spring.application.uuid字段,框架会自动把该ID注册到注册中心,不需要用户自己配置,支持通配表达式方式

全局唯一ID的格式为

年月日(8位)-小时分钟秒(6位)-毫秒(3位)-随机数(4位)-随机数(3位)-随机数(3位)

前半部分精确到毫秒的设计,基本能保证ID的全局唯一,后半部分三重随机数,完全能保证ID的全局唯一。全局唯一失效的前提是,两个服务实例必须是毫秒级的同时启动,同时三次随机碰撞下来,得到完全三个相同的随机数后

增加Spring Cloud Gateway的全局唯一ID屏蔽策略,Group为discovery-guide-group,Data Id为discovery-guide-gateway,策略内容如下,实现从Spring Cloud Gateway发起的调用屏蔽指定全局唯一ID的服务

<?xml version="1.0" encoding="UTF-8"?>
<rule>
    <strategy-blacklist>
        <id>20210601-222214-909-1146-372-698</id>
    </strategy-blacklist>
</rule>

如果希望每个服务的全局唯一ID分别指定,那么策略内容如下,实现从Spring Cloud Gateway发起的调用屏蔽ID为20210601-222214-909-1146-372-698的a服务,屏蔽ID为20210601-222623-277-4978-633-279的b服务

<?xml version="1.0" encoding="UTF-8"?>
<rule>
    <strategy>
        <id>{"discovery-guide-service-a":"20210601-222214-909-1146-372-698", "discovery-guide-service-b":"20210601-222623-277-4978-633-279"}</id>
    </strategy>
</rule>

如果忽略服务名,也可以表示成如下方式,即ID为20210601-222214-909-1146-372-698和20210601-222623-277-4978-633-279的服务都被屏蔽

<id>20210601-222214-909-1146-372-698;20210601-222623-277-4978-633-279</id>

如果上述表达式还未满足需求,也可以采用通配表达式方式(具体详细用法,参考Spring AntPathMatcher),通过Spring Matcher的通配表达式,支持多个通配*、单个通配?等全部标准表达式用法

20210601* - 表示屏蔽范围是2021年06月01日注册的实例口
20210601-222214-909-1146-372-69? - 表示屏蔽范围是20210601-222214-909-1146-372-69开头ID的服务

例如

"discovery-guide-service-b":"20210601*;20210601-222214-909-1146-372-69?"

表示discovery-guide-service-b服务的屏蔽范围是2021年06月01日注册的实例,或者屏蔽范围是20210601-222214-909-1146-372-69开头ID的服务(末尾必须是1个字符),多个用分号隔开

提醒:跟Header驱动下的IP地址和端口屏蔽策略等效,例如

n-d-id-blacklist=20210601-222214-909-1146-372-698;20210601-222623-277-4978-633-279
n-d-id-blacklist={"discovery-guide-service-a":"20210601-222214-909-1146-372-698", "discovery-guide-service-b":"20210601-222623-277-4978-633-279"}

小贴士

利用通配符方式实现对指定日期上线的服务实例做屏蔽,示例内容如下,表示2021年6月1日(也可以精确到小时或者分钟)上线的a服务实例和b服务实例都会被屏蔽。该场景的使用意义是,在服务下线之前,使用者担心流量有损,同时使用者知道上一次服务发布的日期,只要该屏蔽策略一生效,负载均衡将实时过滤掉指定日期的服务实例。那么,使用者对这些服务实例无论是优雅停机,还是暴力下线,都不会造成任何流量有损,例如

<?xml version="1.0" encoding="UTF-8"?>
<rule>
    <strategy-blacklist>
        <id>{"discovery-guide-service-a":"20210601*", "discovery-guide-service-b":"20210601*"}</id>
    </strategy-blacklist>
</rule>

IP地址和端口屏蔽

通过IP地址或者端口或者IP地址+端口进行屏蔽,支持通配表达式方式

增加Zuul的IP地址和端口屏蔽策略,Group为discovery-guide-group,Data Id为discovery-guide-zuul,策略内容如下,实现从Zuul发起的调用屏蔽指定IP地址和端口,或者指定IP地址,或者指定端口(下面策略以端口为例)的服务

<?xml version="1.0" encoding="UTF-8"?>
<rule>
    <strategy-blacklist>
        <!-- <address>127.0.0.1:3001</address> -->
        <!-- <address>127.0.0.1</address> -->
        <address>3001</address>
    </strategy-blacklist>
</rule>

如果希望每个服务的IP地址或者端口分别指定,那么策略内容如下,实现从Zuul发起的屏蔽屏蔽3001端口的a服务,屏蔽4001端口的b服务

<?xml version="1.0" encoding="UTF-8"?>
<rule>
    <strategy>
        <address>{"discovery-guide-service-a":"3001", "discovery-guide-service-b":"4001"}</address>
    </strategy>
</rule>

如果忽略服务名,也可以表示成如下方式,即3001和4001端口的服务都被屏蔽

<address>3001;4001</address>

当所有服务都选同一端口的时候,下面两条是等效的

<address>3001</address>
<address>{"discovery-guide-service-a":"3001", "discovery-guide-service-b":"3001"}</address>

如果上述表达式还未满足需求,也可以采用通配表达式方式(具体详细用法,参考Spring AntPathMatcher),通过Spring Matcher的通配表达式,支持多个通配*、单个通配?等全部标准表达式用法

* - 表示屏蔽为所有端口
3* - 表示屏蔽范围为3开头的所有端口

例如

"discovery-guide-service-b":"3*;400?"

表示discovery-guide-service-b服务的屏蔽范围是3开头的所有端口,或者屏蔽范围是400开头的所有端口(末尾必须是1个字符),多个用分号隔开

提醒:跟Header驱动下的IP地址和端口屏蔽策略等效,例如

n-d-address-blacklist=3001
n-d-address-blacklist={"discovery-guide-service-a":"3001", "discovery-guide-service-b":"3001"}

异步场景下全链路蓝绿灰度发布

Discovery框架存在着如下全链路传递上下文的场景,包括

  • 策略路由Header全链路从网关传递到服务
  • 调用链埋点全链路从网关传递到服务
  • 业务自定义的上下文的传递

上述上下文会在如下异步场景中丢失,包括

  • WebFlux Reactor响应式异步
  • Spring异步,@Async注解异步
  • Hystrix线程池隔离模式异步
  • 线程,线程池异步
  • SLF4J日志异步

通过DiscoveryAgent,解决上述痛点。Discovery框架利用DiscoveryAgent字节码增强技术,完美解决各种调用场景下的异步,包括

  • Spring Cloud Gateway过滤器中的上下文传递
  • Zuul过滤器中的上下文传递
  • Feign拦截器中的上下文转发
  • RestTemplate拦截器中的上下文转发
  • WebClient拦截器中的上下文转发

异步场景下DiscoveryAgent解决方案

DiscoveryAgent不仅适用于Discovery框架,也适用于一切具有类似使用场景的基础框架(例如:Dubbo)和业务系统

ThreadLocal的作用是提供线程内的局部变量,在多线程环境下访问时能保证各个线程内的ThreadLocal变量各自独立。在异步场景下,由于出现线程切换的问题,例如,主线程切换到子线程,会导致线程ThreadLocal上下文丢失。DiscoveryAgent通过Java Agent方式解决这些痛点

涵盖所有Java框架的异步场景,解决如下8个异步场景下丢失线程ThreadLocal上下文的问题

  • WebFlux Reactor
  • @Async
  • Hystrix Thread Pool Isolation
  • Runnable
  • Callable
  • Single Thread
  • Thread Pool
  • SLF4J MDC

异步跨线程DiscoveryAgent获取

插件获取方式有两种方式

异步跨线程DiscoveryAgent清单

① discovery-agent-starter-${discovery.version}.jar为Agent引导启动程序,JVM启动时进行加载

② agent.config为基准扫描目录配置文件

绝大多数情况下不需要修改,当然使用者也可以增加和删除agent.config的基准扫描目录。默认配置如下

# Base thread scan packages
agent.plugin.thread.scan.packages=reactor.core.publisher;org.springframework.aop.interceptor;com.netflix.hystrix

基准扫描目录,含义如下

  • WebFlux Reactor异步场景下的扫描目录对应为reactor.core.publisher
  • @Async场景下的扫描目录对应为org.springframework.aop.interceptor
  • Hystrix线程池隔离场景下的扫描目录对应为com.netflix.hystrix

③ plugin/discovery-agent-starter-plugin-strategy-${discovery.version}.jar插件,解决Nepxion Discovery上下文异步场景

④ plugin/discovery-agent-starter-plugin-mdc-${discovery.version}.jar插件,解决SLF4J MDC日志上下文异步场景

⑤ 业务系统可以自定义plugin,解决业务自己定义的上下文异步场景

异步跨线程DiscoveryAgent使用

① 使用示例

  • 通过如下-javaagent启动,基本格式,如下
-javaagent:C:/opt/discovery-agent/discovery-agent-starter-${discovery.agent.version}.jar -Dthread.scan.packages=com.nepxion.discovery.guide.service.feign

② 参数说明

  • C:/opt/discovery-agent:Agent所在的目录,需要对应到实际的目录上
  • -Dthread.scan.packages:Runnable/Callable/Thread/ThreadPool等异步类所在的扫描目录,该目录下的异步类都会被装饰
    • 扫描目录最好精细和准确,目录越详细,越可以减少被装饰的对象数,从一定程度上可以提高性能
    • 扫描目录如果有多个,用“;”分隔
    • 扫描目录如果含有“;”,可能会在某些操作系统中无法被识别,请用""进行引入,例如,-Dthread.scan.packages="com.abc;com.xyz"
    • 扫描目录下没有Runnable/Callable/Thread/ThreadPool等异步类存在,那么thread.scan.packages也不需要配置,最终启动命令行简化为-javaagent:C:/opt/discovery-agent/discovery-agent-starter-${discovery.agent.version}.jar
  • -Dthread.gateway.enabled:Spring Cloud Gateway端策略Header输出到异步子线程。默认开启
  • -Dthread.zuul.enabled:Zuul端策略Header输出到异步子线程。默认开启
  • -Dthread.service.enabled:服务端策略Header输出到异步子线程。默认开启
  • -Dthread.mdc.enabled:SLF4J MDC日志输出到异步子线程。默认开启
  • -Dthread.request.decorator.enabled:异步调用场景下在服务端的Request请求的装饰,当主线程先于子线程执行完的时候,Request会被Destory,导致Header仍旧拿不到,开启装饰,就可以确保拿到。默认为开启,根据实践经验,大多数场景下,需要开启该开关

异步跨线程DiscoveryAgent扩展

  • 根据规范开发一个插件,插件提供了钩子函数,在某个类被加载的时候,可以注册一个事件到线程上下文切换事件当中,实现业务自定义ThreadLocal的跨线程传递
  • plugin目录为放置需要在线程切换时进行ThreadLocal传递的自定义插件。业务自定义插件开发完后,放入到plugin目录下即可

具体步骤介绍,如下

① SDK侧工作

  • 新建ThreadLocal上下文类
public class MyContext {
    private static final ThreadLocal<MyContext> THREAD_LOCAL = new ThreadLocal<MyContext>() {
        @Override
        protected MyContext initialValue() {
            return new MyContext();
        }
    };

    public static MyContext getCurrentContext() {
        return THREAD_LOCAL.get();
    }

    public static void clearCurrentContext() {
        THREAD_LOCAL.remove();
    }

    private Map<String, String> attributes = new HashMap<>();

    public Map<String, String> getAttributes() {
        return attributes;
    }

    public void setAttributes(Map<String, String> attributes) {
        this.attributes = attributes;
    }
}

② Agent侧工作

  • 新建一个模块,引入如下依赖
<dependency>
    <groupId>com.nepxion</groupId>
    <artifactId>discovery-agent-starter</artifactId>
    <version>${discovery.agent.version}</version>
    <scope>provided</scope>
</dependency>
  • 新建一个ThreadLocalHook类继承AbstractThreadLocalHook
public class MyContextHook extends AbstractThreadLocalHook {
    @Override
    public Object create() {
        // 从主线程的ThreadLocal里获取并返回上下文对象
        return MyContext.getCurrentContext().getAttributes();
    }

    @Override
    public void before(Object object) {
        // 把create方法里获取到的上下文对象放置到子线程的ThreadLocal里
        if (object instanceof Map) {
            MyContext.getCurrentContext().setAttributes((Map<String, String>) object);
        }
    }

    @Override
    public void after() {
        // 线程结束,销毁上下文对象
        MyContext.clearCurrentContext();
    }
}
  • 新建一个Plugin类继承AbstractPlugin
public class MyContextPlugin extends AbstractPlugin {
    private Boolean threadMyPluginEnabled = Boolean.valueOf(System.getProperty("thread.myplugin.enabled", "false"));

    @Override
    protected String getMatcherClassName() {
        // 返回存储ThreadLocal对象的类名,由于插件是可以插拔的,所以必须是字符串形式,不允许是显式引入类
        return "com.nepxion.discovery.example.sdk.MyContext";
    }

    @Override
    protected String getHookClassName() {
        // 返回ThreadLocalHook类名
        return MyContextHook.class.getName();
    }

    @Override
    protected boolean isEnabled() {
        // 通过外部-Dthread.myplugin.enabled=true/false的运行参数来控制当前Plugin是否生效。该方法在父类中定义的返回值为true,即缺省为生效
        return threadMyPluginEnabled;
    }
}
  • 定义SPI扩展,在src/main/resources/META-INF/services目录下定义SPI文件

名称为固定如下格式

com.nepxion.discovery.agent.plugin.Plugin

内容为Plugin类的全路径

com.nepxion.discovery.example.agent.MyContextPlugin
  • 执行Maven编译,把编译后的包放在discovery-agent/plugin目录下

  • 给服务增加启动参数并启动,如下

-javaagent:C:/opt/discovery-agent/discovery-agent-starter-${discovery.agent.version}.jar -Dthread.scan.packages=com.nepxion.discovery.example.application -Dthread.myplugin.enabled=true

③ Application侧工作

  • 执行MyApplication,它模拟在主线程ThreadLocal放入Map数据,子线程通过DiscoveryAgent获取到该Map数据,并打印出来
@SpringBootApplication
@RestController
public class MyApplication {
    private static final Logger LOG = LoggerFactory.getLogger(MyApplication.class);

    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);

        invoke();
    }

    public static void invoke() {
        RestTemplate restTemplate = new RestTemplate();

        for (int i = 1; i <= 10; i++) {
            restTemplate.getForEntity("http://localhost:8080/index/" + i, String.class).getBody();
        }
    }

    @GetMapping("/index/{value}")
    public String index(@PathVariable(value = "value") String value) throws InterruptedException {
        Map<String, String> attributes = new HashMap<String, String>();
        attributes.put(value, "MyContext");

        MyContext.getCurrentContext().setAttributes(attributes);

        LOG.info("【主】线程ThreadLocal:{}", MyContext.getCurrentContext().getAttributes());

        new Thread(new Runnable() {
            @Override
            public void run() {
                LOG.info("【子】线程ThreadLocal:{}", MyContext.getCurrentContext().getAttributes());

                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                LOG.info("Sleep 5秒之后,【子】线程ThreadLocal:{} ", MyContext.getCurrentContext().getAttributes());
            }
        }).start();

        return "";
    }
}

输出结果,如下

2020-11-09 00:08:14.330  INFO 16588 --- [nio-8080-exec-1] c.n.d.example.application.MyApplication  : 【主】线程ThreadLocal:{1=MyContext}
2020-11-09 00:08:14.381  INFO 16588 --- [       Thread-4] c.n.d.example.application.MyApplication  : 【子】线程ThreadLocal:{1=MyContext}
2020-11-09 00:08:14.402  INFO 16588 --- [nio-8080-exec-2] c.n.d.example.application.MyApplication  : 【主】线程ThreadLocal:{2=MyContext}
2020-11-09 00:08:14.403  INFO 16588 --- [       Thread-5] c.n.d.example.application.MyApplication  : 【子】线程ThreadLocal:{2=MyContext}
2020-11-09 00:08:14.405  INFO 16588 --- [nio-8080-exec-3] c.n.d.example.application.MyApplication  : 【主】线程ThreadLocal:{3=MyContext}
2020-11-09 00:08:14.406  INFO 16588 --- [       Thread-6] c.n.d.example.application.MyApplication  : 【子】线程ThreadLocal:{3=MyContext}
2020-11-09 00:08:14.414  INFO 16588 --- [nio-8080-exec-4] c.n.d.example.application.MyApplication  : 【主】线程ThreadLocal:{4=MyContext}
2020-11-09 00:08:14.414  INFO 16588 --- [       Thread-7] c.n.d.example.application.MyApplication  : 【子】线程ThreadLocal:{4=MyContext}
2020-11-09 00:08:14.417  INFO 16588 --- [nio-8080-exec-5] c.n.d.example.application.MyApplication  : 【主】线程ThreadLocal:{5=MyContext}
2020-11-09 00:08:14.418  INFO 16588 --- [       Thread-8] c.n.d.example.application.MyApplication  : 【子】线程ThreadLocal:{5=MyContext}
2020-11-09 00:08:14.421  INFO 16588 --- [nio-8080-exec-6] c.n.d.example.application.MyApplication  : 【主】线程ThreadLocal:{6=MyContext}
2020-11-09 00:08:14.422  INFO 16588 --- [       Thread-9] c.n.d.example.application.MyApplication  : 【子】线程ThreadLocal:{6=MyContext}
2020-11-09 00:08:14.424  INFO 16588 --- [nio-8080-exec-7] c.n.d.example.application.MyApplication  : 【主】线程ThreadLocal:{7=MyContext}
2020-11-09 00:08:14.425  INFO 16588 --- [      Thread-10] c.n.d.example.application.MyApplication  : 【子】线程ThreadLocal:{7=MyContext}
2020-11-09 00:08:14.427  INFO 16588 --- [nio-8080-exec-8] c.n.d.example.application.MyApplication  : 【主】线程ThreadLocal:{8=MyContext}
2020-11-09 00:08:14.428  INFO 16588 --- [      Thread-11] c.n.d.example.application.MyApplication  : 【子】线程ThreadLocal:{8=MyContext}
2020-11-09 00:08:14.430  INFO 16588 --- [nio-8080-exec-9] c.n.d.example.application.MyApplication  : 【主】线程ThreadLocal:{9=MyContext}
2020-11-09 00:08:14.431  INFO 16588 --- [      Thread-12] c.n.d.example.application.MyApplication  : 【子】线程ThreadLocal:{9=MyContext}
2020-11-09 00:08:14.433  INFO 16588 --- [io-8080-exec-10] c.n.d.example.application.MyApplication  : 【主】线程ThreadLocal:{10=MyContext}
2020-11-09 00:08:14.434  INFO 16588 --- [      Thread-13] c.n.d.example.application.MyApplication  : 【子】线程ThreadLocal:{10=MyContext}
2020-11-09 00:08:19.382  INFO 16588 --- [       Thread-4] c.n.d.example.application.MyApplication  : Sleep 5秒之后,【子】线程ThreadLocal:{1=MyContext} 
2020-11-09 00:08:19.404  INFO 16588 --- [       Thread-5] c.n.d.example.application.MyApplication  : Sleep 5秒之后,【子】线程ThreadLocal:{2=MyContext} 
2020-11-09 00:08:19.406  INFO 16588 --- [       Thread-6] c.n.d.example.application.MyApplication  : Sleep 5秒之后,【子】线程ThreadLocal:{3=MyContext} 
2020-11-09 00:08:19.416  INFO 16588 --- [       Thread-7] c.n.d.example.application.MyApplication  : Sleep 5秒之后,【子】线程ThreadLocal:{4=MyContext} 
2020-11-09 00:08:19.418  INFO 16588 --- [       Thread-8] c.n.d.example.application.MyApplication  : Sleep 5秒之后,【子】线程ThreadLocal:{5=MyContext} 
2020-11-09 00:08:19.422  INFO 16588 --- [       Thread-9] c.n.d.example.application.MyApplication  : Sleep 5秒之后,【子】线程ThreadLocal:{6=MyContext} 
2020-11-09 00:08:19.425  INFO 16588 --- [      Thread-10] c.n.d.example.application.MyApplication  : Sleep 5秒之后,【子】线程ThreadLocal:{7=MyContext} 
2020-11-09 00:08:19.428  INFO 16588 --- [      Thread-11] c.n.d.example.application.MyApplication  : Sleep 5秒之后,【子】线程ThreadLocal:{8=MyContext} 
2020-11-09 00:08:19.432  INFO 16588 --- [      Thread-12] c.n.d.example.application.MyApplication  : Sleep 5秒之后,【子】线程ThreadLocal:{9=MyContext} 
2020-11-09 00:08:19.434  INFO 16588 --- [      Thread-13] c.n.d.example.application.MyApplication  : Sleep 5秒之后,【子】线程ThreadLocal:{10=MyContext} 

如果不加异步Agent,则输出结果,如下,可以发现在子线程中ThreadLocal上下文全部都丢失

2020-11-09 00:01:40.133  INFO 16692 --- [nio-8080-exec-1] c.n.d.example.application.MyApplication  : 【主】线程ThreadLocal:{1=MyContext}
2020-11-09 00:01:40.135  INFO 16692 --- [       Thread-8] c.n.d.example.application.MyApplication  : 【子】线程ThreadLocal:{}
2020-11-09 00:01:40.158  INFO 16692 --- [nio-8080-exec-2] c.n.d.example.application.MyApplication  : 【主】线程ThreadLocal:{2=MyContext}
2020-11-09 00:01:40.159  INFO 16692 --- [       Thread-9] c.n.d.example.application.MyApplication  : 【子】线程ThreadLocal:{}
2020-11-09 00:01:40.162  INFO 16692 --- [nio-8080-exec-3] c.n.d.example.application.MyApplication  : 【主】线程ThreadLocal:{3=MyContext}
2020-11-09 00:01:40.163  INFO 16692 --- [      Thread-10] c.n.d.example.application.MyApplication  : 【子】线程ThreadLocal:{}
2020-11-09 00:01:40.170  INFO 16692 --- [nio-8080-exec-5] c.n.d.example.application.MyApplication  : 【主】线程ThreadLocal:{4=MyContext}
2020-11-09 00:01:40.170  INFO 16692 --- [      Thread-11] c.n.d.example.application.MyApplication  : 【子】线程ThreadLocal:{}
2020-11-09 00:01:40.173  INFO 16692 --- [nio-8080-exec-4] c.n.d.example.application.MyApplication  : 【主】线程ThreadLocal:{5=MyContext}
2020-11-09 00:01:40.174  INFO 16692 --- [      Thread-12] c.n.d.example.application.MyApplication  : 【子】线程ThreadLocal:{}
2020-11-09 00:01:40.176  INFO 16692 --- [nio-8080-exec-6] c.n.d.example.application.MyApplication  : 【主】线程ThreadLocal:{6=MyContext}
2020-11-09 00:01:40.177  INFO 16692 --- [      Thread-13] c.n.d.example.application.MyApplication  : 【子】线程ThreadLocal:{}
2020-11-09 00:01:40.179  INFO 16692 --- [nio-8080-exec-8] c.n.d.example.application.MyApplication  : 【主】线程ThreadLocal:{7=MyContext}
2020-11-09 00:01:40.180  INFO 16692 --- [      Thread-14] c.n.d.example.application.MyApplication  : 【子】线程ThreadLocal:{}
2020-11-09 00:01:40.182  INFO 16692 --- [nio-8080-exec-7] c.n.d.example.application.MyApplication  : 【主】线程ThreadLocal:{8=MyContext}
2020-11-09 00:01:40.182  INFO 16692 --- [      Thread-15] c.n.d.example.application.MyApplication  : 【子】线程ThreadLocal:{}
2020-11-09 00:01:40.185  INFO 16692 --- [nio-8080-exec-9] c.n.d.example.application.MyApplication  : 【主】线程ThreadLocal:{9=MyContext}
2020-11-09 00:01:40.186  INFO 16692 --- [      Thread-16] c.n.d.example.application.MyApplication  : 【子】线程ThreadLocal:{}
2020-11-09 00:01:40.188  INFO 16692 --- [io-8080-exec-10] c.n.d.example.application.MyApplication  : 【主】线程ThreadLocal:{10=MyContext}
2020-11-09 00:01:40.189  INFO 16692 --- [      Thread-17] c.n.d.example.application.MyApplication  : 【子】线程ThreadLocal:{}
2020-11-09 00:01:45.136  INFO 16692 --- [       Thread-8] c.n.d.example.application.MyApplication  : Sleep 5秒之后,【子】线程ThreadLocal:{} 
2020-11-09 00:01:45.160  INFO 16692 --- [       Thread-9] c.n.d.example.application.MyApplication  : Sleep 5秒之后,【子】线程ThreadLocal:{} 
2020-11-09 00:01:45.163  INFO 16692 --- [      Thread-10] c.n.d.example.application.MyApplication  : Sleep 5秒之后,【子】线程ThreadLocal:{} 
2020-11-09 00:01:45.171  INFO 16692 --- [      Thread-11] c.n.d.example.application.MyApplication  : Sleep 5秒之后,【子】线程ThreadLocal:{} 
2020-11-09 00:01:45.174  INFO 16692 --- [      Thread-12] c.n.d.example.application.MyApplication  : Sleep 5秒之后,【子】线程ThreadLocal:{} 
2020-11-09 00:01:45.177  INFO 16692 --- [      Thread-13] c.n.d.example.application.MyApplication  : Sleep 5秒之后,【子】线程ThreadLocal:{} 
2020-11-09 00:01:45.181  INFO 16692 --- [      Thread-14] c.n.d.example.application.MyApplication  : Sleep 5秒之后,【子】线程ThreadLocal:{} 
2020-11-09 00:01:45.183  INFO 16692 --- [      Thread-15] c.n.d.example.application.MyApplication  : Sleep 5秒之后,【子】线程ThreadLocal:{} 
2020-11-09 00:01:45.187  INFO 16692 --- [      Thread-16] c.n.d.example.application.MyApplication  : Sleep 5秒之后,【子】线程ThreadLocal:{} 
2020-11-09 00:01:45.190  INFO 16692 --- [      Thread-17] c.n.d.example.application.MyApplication  : Sleep 5秒之后,【子】线程ThreadLocal:{} 

完整示例,请参考https://github.com/Nepxion/DiscoveryAgent/tree/master/discovery-agent-example。上述自定义插件的方式,即可解决使用者在线程切换时丢失ThreadLocal上下文的问题

异步场景下Hystrix线程池隔离解决方案

全链路策略路由Header和调用链Span在Hystrix线程池隔离模式(信号量模式不需要引入)下传递时,通过线程上下文切换会存在丢失Header的问题,通过下述步骤解决,同时适用于网关端和服务端

① Pom引入

<!-- 当服务用Hystrix做线程隔离的时候,才需要导入下面的包 -->
<dependency>
    <groupId>com.nepxion</groupId>
    <artifactId>discovery-plugin-strategy-starter-hystrix</artifactId>
    <version>${discovery.version}</version>
</dependency>

② 配置开启

# 开启服务端实现Hystrix线程隔离模式做服务隔离时,必须把spring.application.strategy.hystrix.threadlocal.supported设置为true,同时要引入discovery-plugin-strategy-starter-hystrix包,否则线程切换时会发生ThreadLocal上下文对象丢失。缺失则默认为false
spring.application.strategy.hystrix.threadlocal.supported=true

该方案也可以通过异步场景下DiscoveryAgent解决方案解决

全链路数据库和消息队列蓝绿发布

通过订阅相关参数的变化,实现参数化蓝绿发布,可用于如下场景

① 基于多DataSource的数据库蓝绿发布

② 基于多Queue的消息队列蓝绿发布

增加参数化蓝绿发布规则,Group为discovery-guide-group,Data Id为discovery-guide-group(全局发布,两者都是组名),规则内容如下,实现功能

① 服务a在版本为1.0的时候,数据库的数据源指向db1;服务a在版本为1.1的时候,数据库的数据源指向db2

② 服务b在区域为dev的时候,消息队列指向queue1;服务b在区域为qa的时候,消息队列指向queue2

③ 服务c在环境为env1的时候,数据库的数据源指向db1;服务c在环境为env2的时候,数据库的数据源指向db2

④ 服务d在可用区为zone1的时候,消息队列指向queue1;服务d在可用区为zone2的时候,消息队列指向queue2

⑤ 服务c在IP地址和端口为192.168.43.101:1201的时候,数据库的数据源指向db1;服务c在IP地址和端口为192.168.43.102:1201的时候,数据库的数据源指向db2

<?xml version="1.0" encoding="UTF-8"?>
<rule>
    <parameter>
        <service service-name="discovery-guide-service-a" tag-key="version" tag-value="1.0" key="ShardingSphere" value="db1"/>
        <service service-name="discovery-guide-service-a" tag-key="version" tag-value="1.1" key="ShardingSphere" value="db2"/>
        <service service-name="discovery-guide-service-b" tag-key="region" tag-value="dev" key="RocketMQ" value="queue1"/>
        <service service-name="discovery-guide-service-b" tag-key="region" tag-value="qa" key="RocketMQ" value="queue2"/>
        <service service-name="discovery-guide-service-c" tag-key="env" tag-value="env1" key="ShardingSphere" value="db1"/>
        <service service-name="discovery-guide-service-c" tag-key="env" tag-value="env2" key="ShardingSphere" value="db2"/>
        <service service-name="discovery-guide-service-d" tag-key="zone" tag-value="zone1" key="RocketMQ" value="queue1"/>
        <service service-name="discovery-guide-service-d" tag-key="zone" tag-value="zone2" key="RocketMQ" value="queue2"/>
        <service service-name="discovery-guide-service-e" tag-key="address" tag-value="192.168.43.101:1201" key="ShardingSphere" value="db1"/>
        <service service-name="discovery-guide-service-e" tag-key="address" tag-value="192.168.43.102:1201" key="ShardingSphere" value="db2"/>
    </parameter>
</rule>

通过事件总线方式,对参数改变动态实现监听,并在此类里自行对接相关的数据库和消息队列中间件的切换和驱动

@EventBus
public class MySubscriber {
    @Autowired
    private PluginAdapter pluginAdapter;

    @Subscribe
    public void onParameterChanged(ParameterChangedEvent parameterChangedEvent) {
        ParameterEntity parameterEntity = parameterChangedEvent.getParameterEntity();
        String serviceId = pluginAdapter.getServiceId();
        List<ParameterServiceEntity> parameterServiceEntityList = null;
        if (parameterEntity != null) {
            Map<String, List<ParameterServiceEntity>> parameterServiceMap = parameterEntity.getParameterServiceMap();
            parameterServiceEntityList = parameterServiceMap.get(serviceId);
        }
        // parameterServiceEntityList为动态参数列表
    }
}

使用者可以通过如下开关,决定在服务启动过程中,读到参数配置的时候,是否要发送一个事件触发数据库和消息队列中间件的切换

# 启动和关闭在服务启动的时候参数订阅事件发送。缺失则默认为true
spring.application.parameter.event.onstart.enabled=true

参考https://github.com/Nepxion/DiscoveryContrib里的实现方式

网关动态路由

网关动态路由功能,主要包括

  • 路由动态添加
  • 路由动态修改
  • 路由动态删除
  • 路由动态批量更新
  • 路由查询
  • 路由动态变更后,通过事件总线方式发出事件通知

上述操作,可以通过

  • 网关暴露Rest Endpoint接口实施
  • 控制台暴露Rest Endpoint接口,对同一个网关下若干个实例批量实施
  • 网关订阅配置中心(包括Nacos、Apollo、Consul、Etcd、Redis、Zookeeper)批量实施

Spring-Cloud-Gateway网关动态路由

提醒:Spring Cloud Gateway网关在自动路由模式下,动态路由不能工作

支持Spring Cloud Gateway网关官方断言器和过滤器,也支持用户自定义断言器和过滤器

Spring-Cloud-Gateway网关动态路由配置

① 精简配置

[
    {
        "id": "route0", 
        "uri": "lb://discovery-guide-service-a", 
        "predicates": [
            "Path=/discovery-guide-service-a/**,/x/**,/y/**"
        ], 
        "filters": [
            "StripPrefix=1"
        ]
    }
]

② 完整配置

[
    {
        "id": "route0", 
        "uri": "lb://discovery-guide-service-a", 
        "predicates": [
            "Path=/discovery-guide-service-a/**,/x/**,/y/**"
        ], 
        "filters": [
            "StripPrefix=1"
        ], 
        "order": 0,
        "metadata": {}
    }
]

Spring-Cloud-Gateway网关自定义动态路由配置

① 自定义方式描述网关内置断言器和过滤器

提醒:自定义方式描述网关内置断言器和过滤器的Key必须遵循如下规则

  • 对于没有显式args定义的配置,类似Path、StripPrefix这种配置,args名称必须是_genkey_序号格式。例如,"_genkey_0": "/discovery-guide-service-a/**"
  • 对于显式args定义的配置,类似Header、Cookie、Query这种配置,args名称遵照Spring Cloud Gateway内置格式,请查看相关文档或者源码。例如,Header的KV格式为header -> regexp,Cookie的KV格式为name->regexp,Query的KV格式为param->regexp
[
    {
        "id": "route0", 
        "uri": "lb://discovery-guide-service-a",
        "userPredicates": [
            {
                "name": "Path",
                "args": {
                    "_genkey_0": "/discovery-guide-service-a/**",
                    "_genkey_1": "/x/**",
                    "_genkey_2": "/y/**"
                }
            },
          {
                "name": "Header",
                "args": {
                    "header": "a",
                    "regexp": "1"
                }
            },
            {
                "name": "Header",
                "args": {
                    "header": "b",
                    "regexp": "2"
                }
            },
            {
                "name": "Cookie",
                "args": {
                    "name": "c",
                    "regexp": "3"
                }
            },
            {
                "name": "Cookie",
                "args": {
                    "name": "d",
                    "regexp": "4"
                }
            },
            {
                "name": "Query",
                "args": {
                    "param": "e",
                    "regexp": "5"
                }
            },
            {
                "name": "Query",
                "args": {
                    "param": "f",
                    "regexp": "6"
                }
            }
        ],
        "userFilters": [
            {
                "name": "StripPrefix",
                "args": {
                    "_genkey_0": "1"
                }
            }
        ]
    }
]

在DiscoveryPlatform界面上,格式为

Path={"_genkey_0":"/discovery-guide-service-a/**", "_genkey_1":"/x/**", "_genkey_2":"/y/**"}
StripPrefix={"_genkey_0":"1"}

Header={"header":"a","regexp":"1"}
Header={"header":"b","regexp":"2"}
Cookie={"name":"c","regexp":"3"}
Cookie={"name":"d","regexp":"4"}
Query={"param":"e","regexp":"5"}
Query={"param":"f","regexp":"6"}

② 自定义方式描述用户扩展的断言器和过滤器

提醒:自定义方式描述用户扩展的断言器和过滤器的Key必须遵循如下规则

  • List结构,args名称必须是list的变量名.序号格式。例如,"whiteList.0": "* swagger-ui.html"
  • Map<String, String>结构,args名称必须是map的变量名.map的key格式。例如,"userMap.name": "jason"
[
    {
        "id": "route0", 
        "uri": "lb://discovery-guide-service-a", 
        "predicates": [
            "Path=/discovery-guide-service-a/**,/x/**,/y/**"
        ], 
        "filters": [
            "StripPrefix=1"
        ], 
        "userPredicates": [],
        "userFilters": [
            {
                "name": "Authentication",
                "args": {
                    "secretKey": "abc",
                    "whiteList.0": "* swagger-ui.html",
                    "whiteList.1": "* /swagger-resources/**",
                    "whiteList.2": "* /doc.html",
                    "userMap.name": "jason",
                    "userMap.age": "20",
                    "authInfoCarryStrategy": "AuthWriteToHeader"
                }
            }
        ]
    }
]

在DiscoveryPlatform界面上,格式为

Authentication={"secretKey":"abc", "whiteList.0":"* swagger-ui.html", "whiteList.1":"* /swagger-resources/**", "whiteList.2":"* /doc.html", "userMap.name":"jason", "userMap.age":"20", "authInfoCarryStrategy":"AuthWriteToHeader"}

Spring-Cloud-Gateway网关Rest-Endpoint

① Spring Cloud Gateway网关的Rest Endpoint接口

操作 路径 参数 方式
增加网关路由 http://[网关IP:PORT]/spring-cloud-gateway-route/add 单个动态路由配置 POST
修改网关路由 http://[网关IP:PORT]/spring-cloud-gateway-route/modify 单个动态路由配置 POST
删除网关路由 http://[网关IP:PORT]/spring-cloud-gateway-route/delete/{routeId} DELETE
更新全部网关路由 http://[网关IP:PORT]/spring-cloud-gateway-route/update-all 多个动态路由配置 POST
根据路由Id查看网关路由 http://[网关IP:PORT]/spring-cloud-gateway-route/view/{routeId} GET
查看全部网关路由 http://[网关IP:PORT]/spring-cloud-gateway-route/view-all GET

② 控制台的Rest Endpoint接口

操作 路径 参数 方式
增加网关路由 http://[控制台IP:PORT]/route/add/spring-cloud-gateway/{serviceId} 单个动态路由配置 POST
修改网关路由 http://[控制台IP:PORT]/route/modify/spring-cloud-gateway/{serviceId} 单个动态路由配置 POST
删除网关路由 http://[控制台IP:PORT]/route/delete/spring-cloud-gateway/{serviceId}/{routeId} DELETE
更新全部网关路由 http://[控制台IP:PORT]/route/update-all/spring-cloud-gateway/{serviceId} 多个动态路由配置 GET
查看全部网关路由 http://[控制台IP:PORT]/route/view-all/spring-cloud-gateway/{serviceId} GET

Spring-Cloud-Gateway网关订阅配置中心

网关订阅配置中心的使用方式,如下

  • Key为
    • Nacos、Redis、Zookeeper配置中心,Group为{group},DataId为{网关serviceId}-dynamic-route
    • Apollo、Consul、Etcd配置中心,Key的格式为{group}-{网关serviceId}-dynamic-route
    • {group}为注册中心元数据group值
  • Value参考Spring-Cloud-Gateway网关动态路由配置

支持如下开关开启该动能,默认是关闭的

# 开启和关闭网关订阅配置中心的动态路由策略。缺失则默认为false
spring.application.strategy.gateway.dynamic.route.enabled=true

配置中心配置的网关动态路由推送到网关后,网关会自动根据已经存在的路由表进行判断后实施增删改操作,而不是全部清空后再全部插入,这样有助于提高性能和安全性。网关控制台上会打印出如下日志

--- Gateway Dynamic Routes Update Information ----
Total count=3
Added count=1
Modified count=1
Deleted count=1
--------------------------------------------------

Spring-Cloud-Gateway网关事件总线通知的订阅

@EventBus
public class MySubscriber {
    @Subscribe
    public void onGatewayStrategyRouteAdded(GatewayStrategyRouteAddedEvent gatewayStrategyRouteAddedEvent) {
        System.out.println("增加网关路由=" + gatewayStrategyRouteAddedEvent.getGatewayStrategyRouteEntity());
    }

    @Subscribe
    public void onGatewayStrategyRouteModified(GatewayStrategyRouteModifiedEvent gatewayStrategyRouteModifiedEvent) {
        System.out.println("修改网关路由=" + gatewayStrategyRouteModifiedEvent.getGatewayStrategyRouteEntity());
    }

    @Subscribe
    public void onGatewayStrategyRouteDeleted(GatewayStrategyRouteDeletedEvent gatewayStrategyRouteDeletedEvent) {
        System.out.println("删除网关路由=" + gatewayStrategyRouteDeletedEvent.getRouteId());
    }

    @Subscribe
    public void onGatewayStrategyRouteUpdatedAll(GatewayStrategyRouteUpdatedAllEvent gatewayStrategyRouteUpdatedAllEvent) {
        System.out.println("更新全部网关路由=" + gatewayStrategyRouteUpdatedAllEvent.getGatewayStrategyRouteEntityList());
    }
}

Zuul网关动态路由

提醒:Zuul网关在自动路由模式下,动态路由可以工作

Zuul网关动态路由配置

① 精简配置

[
    {
        "id": "route0",
        "serviceId": "discovery-guide-service-a",
        "path": "/discovery-guide-service-a/**"
    },
    {
        "id": "route1",
        "serviceId": "discovery-guide-service-a",
        "path": "/x/**"
    },
    {
        "id": "route2",
        "serviceId": "discovery-guide-service-a",
        "path": "/y/**"
    }
]

如果希望一个服务只映射一个动态路由路径,则不需要id,可以简化为

[
    {
        "serviceId": "discovery-guide-service-a",
        "path": "/x/**"
    }
]

② 完整配置

[
    {
        "id": "route0",
        "serviceId": "discovery-guide-service-a",
        "path": "/discovery-guide-service-a/**",
        "url": null,
        "stripPrefix": true,
        "retryable": null,
        "sensitiveHeaders": [],
        "customSensitiveHeaders": false
    },
    {
        "id": "route1",
        "serviceId": "discovery-guide-service-a",
        "path": "/x/**",
        "url": null,
        "stripPrefix": true,
        "retryable": null,
        "sensitiveHeaders": [],
        "customSensitiveHeaders": false
    },
    {
        "id": "route2",
        "serviceId": "discovery-guide-service-a",
        "path": "/y/**",
        "url": null,
        "stripPrefix": true,
        "retryable": null,
        "sensitiveHeaders": [],
        "customSensitiveHeaders": false
    }
]

Zuul网关Rest-Endpoint

① Zuul网关的Rest Endpoint接口

操作 路径 参数 方式
增加网关路由 http://[网关IP:PORT]/zuul-route/add 单个动态路由配置 POST
修改网关路由 http://[网关IP:PORT]/zuul-route/modify 单个动态路由配置 POST
删除网关路由 http://[网关IP:PORT]/zuul-route/delete/{routeId} DELETE
更新全部网关路由 http://[网关IP:PORT]/zuul-route/update-all 多个动态路由配置 POST
根据路由Id查看网关路由 http://[网关IP:PORT]/zuul-route/view/{routeId} GET
查看全部网关路由 http://[网关IP:PORT]/zuul-route/view-all GET

② 控制台的Rest Endpoint接口

操作 路径 参数 方式
增加网关路由 http://[控制台IP:PORT]/route/add/zuul/{serviceId} 单个动态路由配置 POST
修改网关路由 http://[控制台IP:PORT]/route/modify/zuul/{serviceId} 单个动态路由配置 POST
删除网关路由 http://[控制台IP:PORT]/route/delete/zuul/{serviceId}/{routeId} DELETE
更新全部网关路由 http://[控制台IP:PORT]/route/zuul/update-all/{serviceId} 多个动态路由配置 GET
查看全部网关路由 http://[控制台IP:PORT]/route/zuul/view-all/{serviceId} GET

Zuul网关订阅配置中心

网关订阅配置中心的使用方式,如下

  • Key为
    • Nacos、Redis、Zookeeper配置中心,Group为{group},DataId为{网关serviceId}-dynamic-route
    • Apollo、Consul、Etcd配置中心,Key的格式为{group}-{网关serviceId}-dynamic-route
    • {group}为注册中心元数据group值
  • Value参考Zuul网关动态路由配置

支持如下开关开启该动能,默认是关闭的

# 开启和关闭网关订阅配置中心的动态路由策略。缺失则默认为false
spring.application.strategy.zuul.dynamic.route.enabled=true

配置中心配置的网关动态路由推送到网关后,网关会自动根据已经存在的路由表进行判断后实施增删改操作,而不是全部清空后再全部插入,这样有助于提高性能和安全性。网关控制台上会打印出如下日志

----- Zuul Dynamic Routes Update Information -----
Total count=3
Added count=1
Modified count=1
Deleted count=1
--------------------------------------------------

Zuul网关事件总线通知的订阅

@EventBus
public class MySubscriber {
    @Subscribe
    public void onZuulStrategyRouteAdded(ZuulStrategyRouteAddedEvent zuulStrategyRouteAddedEvent) {
        System.out.println("增加网关路由=" + zuulStrategyRouteAddedEvent.getZuulStrategyRouteEntity());
    }

    @Subscribe
    public void onZuulStrategyRouteModified(ZuulStrategyRouteModifiedEvent zuulStrategyRouteModifiedEvent) {
        System.out.println("修改网关路由=" + zuulStrategyRouteModifiedEvent.getZuulStrategyRouteEntity());
    }

    @Subscribe
    public void onZuulStrategyRouteDeleted(ZuulStrategyRouteDeletedEvent zuulStrategyRouteDeletedEvent) {
        System.out.println("删除网关路由=" + zuulStrategyRouteDeletedEvent.getRouteId());
    }

    @Subscribe
    public void onZuulStrategyRouteUpdatedAll(ZuulStrategyRouteUpdatedAllEvent zuulStrategyRouteUpdatedAllEvent) {
        System.out.println("更新全部网关路由=" + zuulStrategyRouteUpdatedAllEvent.getZuulStrategyRouteEntityList());
    }
}

统一配置订阅执行器

统一配置订阅执行器,基于Nacos、Apollo、Consul、Etcd、Redis、Zookeeper六种配置中心,通过封装适配成同样的写法,通过切换继承类,可切换配置中心,无须修改其它代码

Spring Cloud配置动态刷新机制固化在一个比较单一的场景(例如,通过@Value方式)里,无法满足更灵活更高级的订阅场景,例如,Spring Cloud Gateway和Zuul网关通过改变配置中心的路由信息无法动态刷新路由路径

本框架提供更简单灵活的实现方式,以Nacos为例子,使用者先确定订阅的Group和DataId,在Nacos界面填入这两个参数对应的配置内容,然后通过回调方法处理业务逻辑。具体使用方式,如下

// 把继承类(extends)换成如下任何一个,即可切换配置中心,代码无需任何变动
// 1. NacosProcessor
// 2. ApolloProcessor
// 3. ConsulProcessor
// 4. EtcdProcessor
// 5. ZookeeperProcessor
// 6. RedisProcessor
// Group和DataId自行决定,需要注意
// 1. 对于Nacos、Redis、Zookeeper配置中心,Group和DataId需要和界面相对应
// 2. 对于Apollo、Consul、Etcd配置中心,Key的格式为Group-DataId
// 可以同时支持多个配置中心的订阅,需要同时创建多个不同的Processor,同时@Bean方式进入到Spring容器
public class MyConfigProcessor extends NacosProcessor {
    @Override
    public void beforeInitialization() {
        System.out.println("订阅器初始化之前,可以做一些工作");
    }

    @Override
    public void afterInitialization() {
        System.out.println("订阅器初始化之后,可以做一些工作");
    }

    @Override
    public String getGroup() {
        return "b";
    }

    @Override
    public String getDataId() {
        return "a";
    }

    @Override
    public String getDescription() {
        // description为日志打印显示而设置,作用是帮助使用者在日志上定位订阅器是否在执行
        return "My subscription";
    }

    @Override
    public void callbackConfig(String config) {
        // config为配置中心对应键值的内容变更,使用者可以根据此变更对业务模块做回调处理
        System.out.println("监听配置改变:config=" + config);
    }
}

统一配置订阅执行器可以单独运行在Spring Boot应用上,它是一个通用的解决方案

  • 如果使用者希望脱离Nepxion Discovery以及Spring Cloud框架,使用者只需要引入如下依赖之一即可
  • 如果使用者正在使用Nepxion Discovery框架,则跟随它的内置引入即可,不需要额外引入如下依赖之一
<dependency>
    <groupId>${project.groupId}</groupId>
    <artifactId>discovery-common-nacos</artifactId>
    <version>${discovery.version}</version>
</dependency>

<dependency>
    <groupId>${project.groupId}</groupId>
    <artifactId>discovery-common-apollo</artifactId>
    <version>${discovery.version}</version>
</dependency>

<dependency>
    <groupId>${project.groupId}</groupId>
    <artifactId>discovery-common-redis</artifactId>
    <version>${discovery.version}</version>
</dependency>

<dependency>
    <groupId>${project.groupId}</groupId>
    <artifactId>discovery-common-zookeeper</artifactId>
    <version>${discovery.version}</version>
</dependency>

<dependency>
    <groupId>${project.groupId}</groupId>
    <artifactId>discovery-common-consul</artifactId>
    <version>${discovery.version}</version>
</dependency>

<dependency>
    <groupId>${project.groupId}</groupId>
    <artifactId>discovery-common-etcd</artifactId>
    <version>${discovery.version}</version>
</dependency>

具体用法和配置,请参考6.x.x指南示例配置版,分支为6.x.x-config

规则策略定义

规则策略格式定义

需要注意,服务名大小写规则

  • 在配置文件(application.properties、application.yaml等)里,定义服务名(spring.application.name)不区分大小写
  • 在规则文件(XML、Json)里,引用的服务名必须小写
  • 在Nacos、Apollo、Redis等远程配置中心的Key,包含的服务名必须小写

规则策略内容定义

规则策略的格式是XML或者Json,存储于本地文件或者远程配置中心,可以通过远程配置中心修改的方式达到规则策略动态化。其核心代码参考discovery-plugin-framework以及它的扩展、discovery-plugin-config-center以及它的扩展和discovery-plugin-admin-center等

规则策略示例

XML最全的示例如下,Json示例见源码discovery-springcloud-example-service工程下的rule.json

<?xml version="1.0" encoding="UTF-8"?>
<rule>
    <!-- 如果不想开启相关功能,只需要把相关节点删除即可,例如不想要黑名单功能,把blacklist节点删除 -->
    <register>
        <!-- 服务注册的黑/白名单注册过滤,只在服务启动的时候生效。白名单表示只允许指定IP地址前缀注册,黑名单表示不允许指定IP地址前缀注册。每个服务只能同时开启要么白名单,要么黑名单 -->
        <!-- filter-type,可选值blacklist/whitelist,表示白名单或者黑名单 -->
        <!-- service-name,表示服务名 -->
        <!-- filter-value,表示黑/白名单的IP地址列表。IP地址一般用前缀来表示,如果多个用“;”分隔,不允许出现空格 -->
        <!-- 表示下面所有服务,不允许10.10和11.11为前缀的IP地址注册(全局过滤) -->
        <blacklist filter-value="10.10;11.11">
            <!-- 表示下面服务,不允许172.16和10.10和11.11为前缀的IP地址注册 -->
            <service service-name="discovery-springcloud-example-a" filter-value="172.16"/>
        </blacklist>

        <!-- <whitelist filter-value="">
            <service service-name="" filter-value=""/>
        </whitelist>  -->

        <!-- 服务注册的数目限制注册过滤,只在服务启动的时候生效。当某个服务的实例注册达到指定数目时候,更多的实例将无法注册 -->
        <!-- service-name,表示服务名 -->
        <!-- filter-value,表示最大实例注册数 -->
        <!-- 表示下面所有服务,最大实例注册数为10000(全局配置) -->
        <count filter-value="10000">
            <!-- 表示下面服务,最大实例注册数为5000,全局配置值10000将不起作用,以局部配置值为准 -->
            <service service-name="discovery-springcloud-example-a" filter-value="5000"/>
        </count>
    </register>

    <discovery>
        <!-- 服务发现的黑/白名单发现过滤,使用方式跟“服务注册的黑/白名单过滤”一致 -->
        <!-- 表示下面所有服务,不允许10.10和11.11为前缀的IP地址被发现(全局过滤) -->
        <blacklist filter-value="10.10;11.11">
            <!-- 表示下面服务,不允许172.16和10.10和11.11为前缀的IP地址被发现 -->
            <service service-name="discovery-springcloud-example-b" filter-value="172.16"/>
        </blacklist>

        <!-- 服务发现的多版本灰度匹配控制 -->
        <!-- service-name,表示服务名 -->
        <!-- version-value,表示可供访问的版本,如果多个用“;”分隔,不允许出现空格 -->
        <!-- 版本策略介绍 -->
        <!-- 1. 标准配置,举例如下 -->
        <!--    <service consumer-service-name="a" provider-service-name="b" consumer-version-value="1.0" provider-version-value="1.0;1.1"/> 表示消费端1.0版本,允许访问提供端1.0和1.1版本 -->
        <!-- 2. 版本值不配置,举例如下 -->
        <!--    <service consumer-service-name="a" provider-service-name="b" provider-version-value="1.0;1.1"/> 表示消费端任何版本,允许访问提供端1.0和1.1版本 -->
        <!--    <service consumer-service-name="a" provider-service-name="b" consumer-version-value="1.0"/> 表示消费端1.0版本,允许访问提供端任何版本 -->
        <!--    <service consumer-service-name="a" provider-service-name="b"/> 表示消费端任何版本,允许访问提供端任何版本 -->
        <!-- 3. 版本值空字符串,举例如下 -->
        <!--    <service consumer-service-name="a" provider-service-name="b" consumer-version-value="" provider-version-value="1.0;1.1"/> 表示消费端任何版本,允许访问提供端1.0和1.1版本 -->
        <!--    <service consumer-service-name="a" provider-service-name="b" consumer-version-value="1.0" provider-version-value=""/> 表示消费端1.0版本,允许访问提供端任何版本 -->
        <!--    <service consumer-service-name="a" provider-service-name="b" consumer-version-value="" provider-version-value=""/> 表示消费端任何版本,允许访问提供端任何版本 -->
        <!-- 4. 版本对应关系未定义,默认消费端任何版本,允许访问提供端任何版本 -->
        <!-- 特殊情况处理,在使用上需要极力避免该情况发生 -->
        <!-- 1. 消费端的application.properties未定义版本号,则该消费端可以访问提供端任何版本 -->
        <!-- 2. 提供端的application.properties未定义版本号,当消费端在xml里不做任何版本配置,才可以访问该提供端 -->
        <version>
            <!-- 表示网关g的1.0,允许访问提供端服务a的1.0版本 -->
            <service consumer-service-name="discovery-springcloud-example-gateway" provider-service-name="discovery-springcloud-example-a" consumer-version-value="1.0" provider-version-value="1.0"/>
            <!-- 表示网关g的1.1,允许访问提供端服务a的1.1版本 -->
            <service consumer-service-name="discovery-springcloud-example-gateway" provider-service-name="discovery-springcloud-example-a" consumer-version-value="1.1" provider-version-value="1.1"/>
            <!-- 表示网关z的1.0,允许访问提供端服务a的1.0版本 -->
            <service consumer-service-name="discovery-springcloud-example-zuul" provider-service-name="discovery-springcloud-example-a" consumer-version-value="1.0" provider-version-value="1.0"/>
            <!-- 表示网关z的1.1,允许访问提供端服务a的1.1版本 -->
            <service consumer-service-name="discovery-springcloud-example-zuul" provider-service-name="discovery-springcloud-example-a" consumer-version-value="1.1" provider-version-value="1.1"/>
            <!-- 表示消费端服务a的1.0,允许访问提供端服务b的1.0版本 -->
            <service consumer-service-name="discovery-springcloud-example-a" provider-service-name="discovery-springcloud-example-b" consumer-version-value="1.0" provider-version-value="1.0"/>
            <!-- 表示消费端服务a的1.1,允许访问提供端服务b的1.1版本 -->
            <service consumer-service-name="discovery-springcloud-example-a" provider-service-name="discovery-springcloud-example-b" consumer-version-value="1.1" provider-version-value="1.1"/>
            <!-- 表示消费端服务b的1.0,允许访问提供端服务c的1.0和1.1版本 -->
            <service consumer-service-name="discovery-springcloud-example-b" provider-service-name="discovery-springcloud-example-c" consumer-version-value="1.0" provider-version-value="1.0;1.1"/>
            <!-- 表示消费端服务b的1.1,允许访问提供端服务c的1.2版本 -->
            <service consumer-service-name="discovery-springcloud-example-b" provider-service-name="discovery-springcloud-example-c" consumer-version-value="1.1" provider-version-value="1.2"/>
        </version>

        <!-- 服务发现的多区域灰度匹配控制 -->
        <!-- service-name,表示服务名 -->
        <!-- region-value,表示可供访问的区域,如果多个用“;”分隔,不允许出现空格 -->
        <!-- 区域策略介绍 -->
        <!-- 1. 标准配置,举例如下 -->
        <!--    <service consumer-service-name="a" provider-service-name="b" consumer-region-value="dev" provider-region-value="dev"/> 表示dev区域的消费端,允许访问dev区域的提供端 -->
        <!-- 2. 区域值不配置,举例如下 -->
        <!--    <service consumer-service-name="a" provider-service-name="b" provider-region-value="dev;qa"/> 表示任何区域的消费端,允许访问dev区域和qa区域的提供端 -->
        <!--    <service consumer-service-name="a" provider-service-name="b" consumer-region-value="dev"/> 表示dev区域的消费端,允许访问任何区域的提供端 -->
        <!--    <service consumer-service-name="a" provider-service-name="b"/> 表示任何区域的消费端,允许访问任何区域的提供端 -->
        <!-- 3. 区域值空字符串,举例如下 -->
        <!--    <service consumer-service-name="a" provider-service-name="b" consumer-region-value="" provider-region-value="dev;qa"/> 表示任何区域的消费端,允许访问dev区域和qa区域的提供端 -->
        <!--    <service consumer-service-name="a" provider-service-name="b" consumer-region-value="dev" provider-region-value=""/> 表示dev区域的消费端,允许访问任何区域的提供端 -->
        <!--    <service consumer-service-name="a" provider-service-name="b" consumer-region-value="" provider-region-value=""/> 表示任何区域的消费端,允许访问任何区域的提供端 -->
        <!-- 4. 区域对应关系未定义,默认表示任何区域的消费端,允许访问任何区域的提供端 -->
        <!-- 特殊情况处理,在使用上需要极力避免该情况发生 -->
        <!-- 1. 消费端的application.properties未定义区域值,则该消费端可以访问任何区域的提供端 -->
        <!-- 2. 提供端的application.properties未定义区域值,当消费端在xml里不做任何区域配置,才可以访问该提供端 -->
        <region>
            <!-- 表示dev区域的消费端服务a,允许访问dev区域的提供端服务b -->
            <service consumer-service-name="discovery-springcloud-example-a" provider-service-name="discovery-springcloud-example-b" consumer-region-value="dev" provider-region-value="dev"/>
            <!-- 表示qa区域的消费端服务a,允许访问qa区域的提供端服务b -->
            <service consumer-service-name="discovery-springcloud-example-a" provider-service-name="discovery-springcloud-example-b" consumer-region-value="qa" provider-region-value="qa"/>
            <!-- 表示dev区域的消费端服务b,允许访问dev区域的提供端服务c -->
            <service consumer-service-name="discovery-springcloud-example-b" provider-service-name="discovery-springcloud-example-c" consumer-region-value="dev" provider-region-value="dev"/>
            <!-- 表示qa区域的消费端服务b,允许访问qa区域的提供端服务c -->
            <service consumer-service-name="discovery-springcloud-example-b" provider-service-name="discovery-springcloud-example-c" consumer-region-value="qa" provider-region-value="qa"/>
        </region>

        <!-- 服务发现的多版本或者多区域的灰度权重控制 -->
        <!-- service-name,表示服务名 -->
        <!-- weight-value,表示版本对应的权重值,格式为"版本/区域值=权重值",如果多个用“;”分隔,不允许出现空格 -->
        <!-- 版本权重策略介绍 -->
        <!-- 1. 标准配置,举例如下 -->
        <!--    <service consumer-service-name="a" provider-service-name="b" provider-weight-value="1.0=90;1.1=10"/> 表示消费端访问提供端的时候,提供端的1.0版本提供90%的权重流量,1.1版本提供10%的权重流量 -->
        <!--    <service provider-service-name="b" provider-weight-value="1.0=90;1.1=10"/> 表示所有消费端访问提供端的时候,提供端的1.0版本提供90%的权重流量,1.1版本提供10%的权重流量 -->
        <!-- 2. 局部配置,即指定consumer-service-name,专门为该消费端配置权重。全局配置,即不指定consumer-service-name,为所有消费端配置相同情形的权重。当局部配置和全局配置同时存在的时候,以局部配置优先 -->
        <!-- 3. 尽量为线上所有版本都赋予权重值 -->
        <!-- 全局版本权重策略介绍 -->
        <!-- 1. 标准配置,举例如下 -->
        <!--    <version provider-weight-value="1.0=85;1.1=15"/> 表示版本为1.0的服务提供85%的权重流量,版本为1.1的服务提供15%的权重流量 -->
        <!-- 2. 全局版本权重可以切换整条调用链的权重配比 -->
        <!-- 3. 尽量为线上所有版本都赋予权重值 -->

        <!-- 区域权重策略介绍 -->
        <!-- 1. 标准配置,举例如下 -->
        <!--    <service consumer-service-name="a" provider-service-name="b" provider-weight-value="dev=85;qa=15"/> 表示消费端访问提供端的时候,区域为dev的服务提供85%的权重流量,区域为qa的服务提供15%的权重流量 -->
        <!--    <service provider-service-name="b" provider-weight-value="dev=85;qa=15"/> 表示所有消费端访问提供端的时候,区域为dev的服务提供85%的权重流量,区域为qa的服务提供15%的权重流量 -->
        <!-- 2. 局部配置,即指定consumer-service-name,专门为该消费端配置权重。全局配置,即不指定consumer-service-name,为所有消费端配置相同情形的权重。当局部配置和全局配置同时存在的时候,以局部配置优先 -->
        <!-- 3. 尽量为线上所有版本都赋予权重值 -->
        <!-- 全局区域权重策略介绍 -->
        <!-- 1. 标准配置,举例如下 -->
        <!--    <region provider-weight-value="dev=85;qa=15"/> 表示区域为dev的服务提供85%的权重流量,区域为qa的服务提供15%的权重流量 -->
        <!-- 2. 全局区域权重可以切换整条调用链的权重配比 -->
        <!-- 3. 尽量为线上所有区域都赋予权重值 -->
        <weight>
            <!-- 权重流量配置有如下六种方式,优先级分别是由高到底,即先从第一种方式取权重流量值,取不到则到第二种方式取值,以此类推,最后仍取不到则忽略。使用者按照实际情况,选择一种即可 -->
            <!-- 表示消费端服务b访问提供端服务c的时候,提供端服务c的1.0版本提供90%的权重流量,1.1版本提供10%的权重流量 -->
            <service consumer-service-name="discovery-springcloud-example-b" provider-service-name="discovery-springcloud-example-c" provider-weight-value="1.0=90;1.1=10" type="version"/>
            <!-- 表示所有消费端服务访问提供端服务c的时候,提供端服务c的1.0版本提供90%的权重流量,1.1版本提供10%的权重流量 -->
            <service provider-service-name="discovery-springcloud-example-c" provider-weight-value="1.0=90;1.1=10" type="version"/>
            <!-- 表示所有版本为1.0的服务提供90%的权重流量,版本为1.1的服务提供10%的权重流量 -->
            <version provider-weight-value="1.0=90;1.1=10"/>

            <!-- 表示消费端服务b访问提供端服务c的时候,提供端服务c的dev区域提供85%的权重流量,qa区域提供15%的权重流量 -->
            <service consumer-service-name="discovery-springcloud-example-b" provider-service-name="discovery-springcloud-example-c" provider-weight-value="dev=85;qa=15" type="region"/>
            <!-- 表示所有消费端服务访问提供端服务c的时候,提供端服务c的dev区域提供85%的权重流量,qa区域提供15%的权重流量 -->
            <service provider-service-name="discovery-springcloud-example-c" provider-weight-value="dev=85;qa=15" type="region"/>
            <!-- 表示所有区域为dev的服务提供85%的权重流量,区域为qa的服务提供15%的权重流量 -->
            <region provider-weight-value="dev=85;qa=15"/>
        </weight>
    </discovery>

    <!-- 基于Http Header传递的策略路由,全局缺省路由(第三优先级) -->
    <strategy>
        <!-- 版本路由 -->
        <version>{"discovery-springcloud-example-a":"1.0", "discovery-springcloud-example-b":"1.0", "discovery-springcloud-example-c":"1.0;1.2"}</version>
        <!-- <version>1.0</version> -->
        <!-- 区域路由 -->
        <region>{"discovery-springcloud-example-a":"qa;dev", "discovery-springcloud-example-b":"dev", "discovery-springcloud-example-c":"qa"}</region>
        <!-- <region>dev</region> -->
        <!-- IP地址和端口路由 -->
        <address>{"discovery-springcloud-example-a":"192.168.43.101:1100", "discovery-springcloud-example-b":"192.168.43.101:1201", "discovery-springcloud-example-c":"192.168.43.101:1300"}</address>
        <!-- 权重流量配置有如下四种方式,优先级分别是由高到底,即先从第一种方式取权重流量值,取不到则到第二种方式取值,以此类推,最后仍取不到则忽略。使用者按照实际情况,选择一种即可 -->
        <!-- 版本权重路由 -->
        <version-weight>{"discovery-springcloud-example-a":"1.0=90;1.1=10", "discovery-springcloud-example-b":"1.0=90;1.1=10", "discovery-springcloud-example-c":"1.0=90;1.1=10"}</version-weight>
        <!-- <version-weight>1.0=90;1.1=10</version-weight> -->
        <!-- 区域权重路由 -->
        <region-weight>{"discovery-springcloud-example-a":"dev=85;qa=15", "discovery-springcloud-example-b":"dev=85;qa=15", "discovery-springcloud-example-c":"dev=85;qa=15"}</region-weight>
        <!-- <region-weight>dev=85;qa=15</region-weight> -->
    </strategy>

    <!-- 基于Http Header传递的策略路由,支持蓝绿发布和灰度发布两种模式。如果都不命中,则执行上面的全局缺省路由 -->
    <strategy-release>
        <!-- Spel表达式在XML中的转义符:-->
        <!-- 和符号 & 转义为 &amp; 必须转义 -->
        <!-- 小于号 < 转义为 &lt; 必须转义 -->
        <!-- 双引号 " 转义为 &quot; 必须转义 -->
        <!-- 大于号 > 转义为 &gt; -->
        <!-- 单引号 ' 转义为 &apos; -->

        <!-- 全链路蓝绿发布:条件命中的匹配方式(第一优先级),支持版本匹配、区域匹配、IP地址和端口匹配、版本权重匹配、区域权重匹配 -->
        <!-- Expression节点允许缺失,当含Expression和未含Expression的配置并存时,以含Expression的配置为优先 -->
        <conditions type="blue-green">
            <condition id="1" expression="#H['a'] == '1' and #H['b'] == '2'" version-id="a-1" region-id="b-1" address-id="c-1" version-weight-id="d-1" region-weight-id="e-1"/>
            <condition id="2" expression="#H['c'] == '3'" version-id="a-2" region-id="b-2" address-id="c-2" version-weight-id="d-2" region-weight-id="e-2"/>
            <condition id="3" version-id="a-2" region-id="b-2" address-id="c-2" version-weight-id="d-2" region-weight-id="e-2"/>
        </conditions>

        <!-- 全链路灰度发布:条件命中的随机权重(第二优先级),支持版本匹配、区域匹配、IP地址和端口匹配 -->
        <!-- Expression节点允许缺失,当含Expression和未含Expression的配置并存时,以含Expression的配置为优先 -->
        <conditions type="gray">
            <condition id="1" expression="#H['a'] == '1' and #H['b'] == '2'" version-id="a-1=10;a-2=90" region-id="b-1=20;b-2=80" address-id="c-1=30;c-2=70"/>
            <condition id="2" expression="#H['c'] == '3'" version-id="a-1=90;a-2=10" region-id="b-1=80;b-2=20" address-id="c-1=70;c-2=30"/>
            <condition id="3" version-id="a-1=5;a-2=95" region-id="b-1=5;b-2=95" address-id="c-1=5;c-2=95"/>
        </conditions>

        <routes>
            <route id="a-1" type="version">{"discovery-springcloud-example-a":"1.0", "discovery-springcloud-example-b":"1.0", "discovery-springcloud-example-c":"1.0;1.2"}</route>
            <route id="a-2" type="version">{"discovery-springcloud-example-a":"1.1", "discovery-springcloud-example-b":"1.1", "discovery-springcloud-example-c":"1.2"}</route>
            <route id="b-1" type="region">{"discovery-springcloud-example