- 通过之前技术的积累,终于开始了本文的编写,如果对灰度、负载均衡、上下文传递、网关不太理解,可以先学习博主的以下博客内容。共勉:
- 企业级 Java 应用灰度发布设计方案与实践全解析
- 《Spring 中上下文传递的那些事儿》 Part 1:ThreadLocal、MDC、TTL 原理与实践
- Spring Cloud LoadBalancer 详解-负载均衡
- 【Spring Cloud Gateway 实战系列】基础篇:路由、断言、过滤器、负载均衡深度解析
- 本系统采用Spring Cloud微服务架构,通过网关层+负载均衡器实现了灵活可控的灰度发布方案。
- 本系统是xx地区互联网医院的实现方案,已经成功上线并运行中,现已接入几十家医院
- 本文档主要分享实现思路(代码仅参考),互相学习,共同进步
一、灰度发布架构概览
1.1 什么是灰度发布?
灰度发布(Gray Release)是一种平滑过渡的发布策略(渐进式发布策略),通过将新功能先开放给部分用户验证,逐步扩大范围,最终全量上线,从而降低新版本发布风险。在医疗系统中,这种方式可有效保障核心业务(如在线问诊、电子处方)的稳定性。通过灰度发布,可以:
- 降低发布风险:新功能先在少量用户中验证,避免全量发布可能带来的系统性风险
- 快速回滚:发现问题可立即切换回稳定版本
- A/B测试:对比新老版本的性能和用户体验
- 数据收集:收集用户使用新功能的数据,为后续优化提供依据
- 平滑过渡:让用户逐步适应新功能,减少用户感知的突兀
- 合规要求:满足医疗行业对系统稳定性的严格要求
1.2 项目整体结构分析
基于工作空间目录树,项目采用多层微服务架构,主要模块划分如下:
xxxx-medical-ihm/
├── api/ # 各服务API接口定义
├── apps/ # 应用服务实现
├── business/ # 业务逻辑层
├── commons/ # 公共组件(含灰度发布模块)
│ └── xxxx-medical-ihm-common-grayrelease/ # 灰度发布核心模块
├── gateway/ # 网关服务
└── mpc/ # 领域模型与核心服务
核心技术栈:Spring Cloud微服务生态(nacos/Feign/Loadbalancer/gateway)、Spring Boot自动配置、拦截器模式
1.3灰度发布设计思路与方案选型
1.3.1设计理念
该项目基于Spring Cloud生态的网关层灰度发布方案 ,采用请求头驱动的流量路由模式,核心设计思路是通过在网关层拦截请求并注入灰度标识,结合自定义负载均衡策略实现流量分发。并利用拦截器机制确保灰度上下文在微服务调用链中传递。整体架构遵循以下原则:
- 轻量级集成 :无侵入式集成现有微服务架构,不引入独立服务网格组件,基于
Spring Cloud
原生能力扩展 - 请求头驱动 :支持多维度灰度标识(版本号、开发者模式),通过
application_version
和developer
请求头标识灰度流量 - 上下文传递 :使用
TTL(TransmittableThreadLocal)
存储灰度上下文,确保跨服务调用时标识透传(确保灰度上下文在服务调用链中透传) - 安全降级 :当灰度规则匹配失败时自动降级到非灰度实例
1.3.2方案选型
本项目采用网关层灰度方案,属于业界六种主流方案中的第三种,与其他方案对比:
方案类型 | 实现方式 | 本项目适配度 |
---|---|---|
代码硬编码 | 业务代码中嵌入灰度逻辑 | ❌ 侵入性高,已排除 |
配置中心灰度 | 动态配置推送灰度规则 | ⚠️ 未集成,但可扩展 |
网关层灰度 | 拦截器+负载均衡器实现 | ✅ 当前采用方案 |
服务网格灰度 | Istio/Linkerd等专用组件 | ❌ 架构过重,目前未采用 ⚠️ 微服务的下一阶段云原生 |
K8s Ingress灰度 | 基于Ingress Controller | ❌ 依赖K8s基础设施 ⚠️ 本项目灰度实现后才引入了k8s,后续可以考虑优化 |
JavaAgent灰度 | 字节码增强技术 | ❌ 运维复杂度高 |
1.4 灰度架构详解
1.4.1 核心流程图
1.4.2 配置管理层
- BusinessGrayEnvironmentController: 运营平台管理灰度配置的REST接口
- GatewayApi: 网关配置管理API,负责配置的CRUD操作
- Redis: 配置存储中心,支持实时更新和发布订阅
1.4.3 网关层 - GrayscaleGlobalFilter
- 作用: 网关入口的灰度路由决策引擎
- Order: 1 (最高优先级)
- 功能:
- 从Redis实时获取灰度配置
- 实现多维度灰度判断:用户白名单、医院编码、域名匹配
- 设置Application-Version请求头
- 支持开发调试模式
1.4.4 负载均衡层 - GrayRoundRobinLoadBalancer
- 作用: 基于灰度版本的智能负载均衡器
- 核心算法:
- 原子计数器实现线程安全的轮询选择
- 版本精确匹配:metadata.version与目标版本完全一致
- 自动降级机制:无灰度实例时回退到正式版本
- 执行流程:
- 获取当前请求的灰度版本(从GrayReleaseContextHolder)
- 筛选匹配版本的服务实例
- 使用轮询算法选择最终实例
1.4.5 业务服务层 - GrayReleaseContextInterceptor
- 作用: 业务服务内部的灰度上下文管理
- 执行时机: 每个HTTP请求进入业务服务时
- 功能:
- 提取Application-Version请求头
- 调用GrayReleaseContextHolder,存储到TransmittableThreadLocal,供后续Feign调用使用
- 请求完成后自动清理,防止内存泄漏
1.4.6 服务间调用 - GrayReleaseFeignRequestInterceptor
- 作用: 微服务间灰度标识的透传
- 执行时机: 每次Feign调用发起时
- 功能:
- 调用GrayReleaseContextHolder,从TransmittableThreadLocal获取当前灰度版本
- 自动注入到Feign请求头
- 支持开发调试模式的详细日志
1.5关键数据流转路径
-
配置更新路径:
运营平台 → GatewayApi → Redis → 网关配置缓存 → 实时生效 -
请求处理路径:
客户端 → 网关灰度判断 → 负载均衡选择 → 业务服务 → 上下文管理 → Feign透传 -
版本标识传递:
网关设置 → 请求头传递 → ThreadLocal存储 → Feign注入 → 下游服务继承 -
异常降级机制:
无灰度实例 → 自动降级到正式版本
实例不可用 → 熔断降级机制
配置缺失 → 使用默认正式版本
二、核心实现类详解
2.1 配置管理层—gateway网关相关
2.1.1 yml配置
gateway.yml:其中whitelist白名单配置,后续在WhiteListProperties类中获取
## 端口
#server.port: 8888
spring:servlet:multipart:max-file-size: 500MBmax-request-size: 500MB
## 监控
management:endpoint:health:show-details: alwaysendpoints:jmx:exposure:include: '*'web:exposure:include: '*'gateway:enabled: falseserver:port: -1## 是否生成新的token
sso.isNewToken: true
## 是否打开接口权限校验
api.isOpenPower: false## 网关白名单
gateway.whiteUrl: /sso/tool/getImageCaptcha|/sso/tool/getRandom|/sso/auth/login|/system/auth/login|/system/tool/getImageCaptcha|/system/tool/getRandom|/system/user/v1/security/queryAnonymousRandomSecretKey|/bigdata/hsb/v1/route/api/doc|/bigdata/qc/v1/qcRuleExecuteResult/internal/receive|/bigdata/qc/v1/qcReport/record/content|/system/auth/findPasswordSms|/system/auth/validateSms|/system/auth/forgetPassword|/system/auth/loginSms|/system/auth/loginByMobile|/system/auth/v1/getToken|/system/auth/v1/testAccept|/health-h5/**||^.*/cdm-nbbl-patient/.*$|^.*/region/queryMap.*$|^.*/openApi/auth.*$|^.*/openApi/checkTokenAndEmpiId.*$|^.*/cdrs-doctor/.*$|^.*/cdrs-patient/.*$|^.*/resource/monitor/.*$|^.*/equipment/dict/get.*$|^.*/equipment/event/.*$|^.*/doc.html|^.*/cdm-nb-doctor/.*$|^.*/cdm-nb-patient/.*$|^.*/cdm-screen-api/.*$|^.*/cdm-nb/screen/.*$|/cdm-nb/screen/query
## 身份白名单
identity.whiteUrl: 1
## 接口权限校验白名单
api.whiteUrl: 1
## xss白名单
xss.whiteUrl: 2## 跨域白名单
cors.white.list: '*'## 白名单配置
whitelist:identity:- /external/queryByTicketgray:- /portal/external/logistics/mrds/route/callback- /hospital/dept/queryAllOnlineDeptsByHosId- /portal/tenant-callback/commonQuery- /portal/tenant-callback/.*- /portal/medical/.*- /portal/api/.*- /operate/.*- /patient/consultation/queryOrderDetailByRoomNo- /patient/tenantConfig/fetchHospitalGlobalConfig- /portal/3-payment/.*- /(.*?)/api/hos/.*- /(.*?)/api/inter-hos/.* - ^/.*\/v3\/api-docs/.* auditblacks:- /portal/tenant-callback/saveApiLog- /hospital/doctor/getDoctorLogo/.*- /hospital/getHosLogo/.*- /portal/operate/saveUserBehaviorLog- /patient/queryPatientByCurrentUser- /portal/operate/viewBuryingPoint- /patient/homePage/myDoctors- /portal/heartbeat- /hospital/queryAgreeBook- /portal/getUserInfoByToken- /patient/user/getImParams
WhiteListProperties:获取白名单配置
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.stereotype.Component;import java.util.List;@Data
@Component
@ConfigurationProperties(prefix = "whitelist")
@RefreshScope
public class WhiteListProperties {/*** 网关白名单*/private List<String> gateway;
// /**
// * 获取接口验证白名单
// */
// private List<String> api;/*** 获取身份验证白名单*/private List<String> identity;private List<String> gray;private List<String> auditblacks;
// /**
// * 跨域白名单配置
// */
// private List<String> cors;
// /**
// * Xss白名单
// */
// private List<String> xss;
}
gateway-route.yml:各服务的网关路由配置,简单看一下就可以
spring:cloud:gateway:discovery:locator:enabled: trueroutes:- id: portaluri: lb://portalpredicates:- Path=/portal/**filters:- RewritePath=/portal/(?<segment>.*), /$\{segment}- id: portal-hisuri: lb://portalpredicates:- Path=/{hisOrgCode}/api/inter-hos/**filters:- StripPrefix=1- RewritePath=/api/inter-hos/(?<segment>.*), /inbound/$\{segment}- AddRequestHeadersIfNotPresent=orgcode:{hisOrgCode} - id: hospitaluri: lb://hospitalpredicates:- Path=/hospital/**filters:- RewritePath=/hospital/(?<segment>.*), /$\{segment}- id: patienturi: lb://patientpredicates:- Path=/patient/**filters:- RewritePath=/patient/(?<segment>.*), /$\{segment}- id: mpcuri: lb://mpc-serverpredicates:- Path=/rbac/**filters:- RewritePath=/rbac/(?<segment>.*), /$\{segment} - id: gatewayuri: lb://gatewaypredicates:- Path=/gateway/**filters:- RewritePath=/gateway/(?<segment>.*), /$\{segment}- id: ordersuri: lb://orderspredicates:- Path=/orders/**filters:- RewritePath=/orders/(?<segment>.*), /$\{segment}
2.1.2 GatewayApi
@FeignClient(name = "gateway")
public interface GatewayApi {@GetMapping("/infoByUserId")Result<JSONObject> infoByUserId(@RequestParam String userId);@PostMapping("/setting")Result<GatewaySettingDto> save(@RequestBody GatewaySettingDto dto);@GetMapping("/syn")Result<GatewaySettingDto> syn();@GetMapping("/isGrayUser")Result<Boolean> isGrayUser(@RequestParam String userId);@PostMapping("/version/info")Result<GatewayVersionDto> info();@PostMapping("/version/update")Result update(@RequestBody GatewayVersionDto dto);
}
2.1.3 GatewayController
Spring Boot网关控制器,主要功能如下:
-
灰度发布控制:通过queryInfo()方法根据用户ID、医院、域名等条件判断是否启用灰度版本,返回对应的版本号和角色
-
配置管理:
- syn()同步网关配置
- save()保存网关设置
- /version/info:获取版本配置【运营平台中使用,提供界面配置化】
- /version/update:更新版本配置【运营平台中使用,提供界面配置化】
-
用户查询:
-
infoByUserId()通过用户ID查询信息
-
isGrayUser()判断是否为灰度用户
-
所有接口均封装在GatewayController类中,通过GatewaySettingService操作配置数据。
@RestController
@RequestMapping("/")
public class GatewayController {private Logger log = LoggerFactory.getLogger(this.getClass());@Resourceprivate GatewaySettingService gatewaySettingService;//infoByUserId()通过用户ID查询信息@GetMapping("/infoByUserId")public Result<JSONObject> infoByUserId(@RequestParam String userId) {FeignClientsConfiguration d;GatewayDto gatewayDto = new GatewayDto();gatewayDto.setCustomerId(userId);return Result.success(queryInfo(gatewayDto));}@GetMapping("info")public Result<JSONObject> info(GatewayDto dto,@RequestHeader("Domainsign") String domainsign) { // 查询域名或者医院的版本号try {dto.setDomainSign(domainsign);JSONObject info = queryInfo(dto);return Result.success(info);} catch (Exception e) {log.error("handle info error", e);return Result.error();}}//灰度发布控制:通过queryInfo()方法根据用户ID、医院、域名等条件判断是否启用灰度版本,返回对应的版本号和角色private JSONObject queryInfo(GatewayDto dto) {// 网关配置GatewaySettingDto s = gatewaySettingService.find();String domainSign = dto.getDomainSign();StringBuffer platFrom = new StringBuffer("");if (StringUtils.isNotEmpty(domainSign)) {platFrom.append(domainSign.split("_")[0]);}else {platFrom.append("JTP");}boolean garyUser4Cust = Optional.ofNullable(s.getGrayUserIds()).map(map -> map.get(platFrom.toString())).filter(set -> set.contains(dto.getCustomerId())).isPresent();Boolean gray =(Objects.nonNull(dto.getHospital()) && s.getGrayHospitals().contains(dto.getHospital()))// 管理平台使用domain确定version|| (StrUtil.isNotBlank(dto.getDomain()) && s.getGrayVersionDomains().contains(dto.getDomain()))|| (StrUtil.isNotBlank(dto.getCustomerId()) && garyUser4Cust)|| (StrUtil.isNotBlank(dto.getCustomerId()) && s.getGrayCustomerIds().contains(dto.getCustomerId()));String version = gray ? s.getGrayVersion() : s.getReleaseVersion();JSONObject info = new JSONObject();info.putOpt(GrayConstant.VERSION, version);info.putOpt(GrayConstant.ROLE, gray ? GrayConstant.GRAY_ROLE : GrayConstant.SIMPLE_ROLE);return info;}//syn()同步网关配置@GetMapping("syn")public Result<GatewaySettingDto> syn() {try {// 网关配置GatewaySettingDto s = gatewaySettingService.find();return Result.success(s);} catch (Exception e) {log.error("handle info error", e);return Result.error();}}//save()保存网关设置@PostMapping("setting")public Result<GatewaySettingDto> save(@RequestBody GatewaySettingDto dto) {GatewaySettingDto gatewaySettingDto = gatewaySettingService.save(dto);dto.setPassword(null);return Result.success(gatewaySettingDto);}//isGrayUser()判断是否为灰度用户@GetMapping("/isGrayUser")public Result<Boolean> isGrayUser(@RequestParam String userId) {// 网关配置GatewaySettingDto s = gatewaySettingService.find();return Result.success(StrUtil.isNotBlank(userId) && s.getGrayCustomerIds().contains(userId));}@Operation(summary = "获取版本号")@PostMapping("/version/info")public Result<GatewayVersionDto> info() {GatewaySettingDto gatewaySettingDto = gatewaySettingService.find();ValidatorUtil.validateNotEmpty(gatewaySettingDto,"网关配置为空,请先配置");GatewayVersionDto gatewayVersionDto = new GatewayVersionDto();gatewayVersionDto.setGrayVersion(gatewaySettingDto.getGrayVersion());gatewayVersionDto.setReleaseVersion(gatewaySettingDto.getReleaseVersion());return Result.success(gatewayVersionDto);}@Operation(summary = "更新版本号")@PostMapping("/version/update")public Result update(@RequestBody GatewayVersionDto dto) {log.info("更新版本号 start--:{}", JSONUtil.toJsonPrettyStr(dto));dto.check();GatewaySettingDto gatewaySettingDto = gatewaySettingService.find();ValidatorUtil.validateNotEmpty(gatewaySettingDto,"网关配置为空,请先配置");gatewaySettingDto.setGrayVersion(dto.getGrayVersion());gatewaySettingDto.setReleaseVersion(dto.getReleaseVersion());log.info("更新版本号 end--:{}", JSONUtil.toJsonPrettyStr(gatewaySettingDto));gatewaySettingService.save(gatewaySettingDto);return Result.success();}}
2.1.4 GatewaySettingService
import cn.hutool.json.JSONUtil;
import com.chinaunicom.medical.ihm.model.GatewaySettingDto;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;/*** 网关配置*/
@Service
public class GatewaySettingService {private Logger log = LoggerFactory.getLogger(this.getClass());@Autowiredprivate RedisTemplate<String, String> stringRedisTemplate;public GatewaySettingDto save(GatewaySettingDto dto) {try {// updatestringRedisTemplate.boundValueOps(GatewaySettingDto.GATEWAY_SETTING_REDIS).set(JSONUtil.toJsonPrettyStr(dto));// notifystringRedisTemplate.convertAndSend(GatewaySettingDto.GATEWAY_SETTING_TOPIC, String.valueOf(System.currentTimeMillis()));return dto;} catch (Exception e) {throw new RuntimeException(e);}}//本地缓存private GatewaySettingDto cache = null;public GatewaySettingDto find() {if (cache == null) {syn();}return cache;}//主动同步public void syn() {try {String json = stringRedisTemplate.boundValueOps(GatewaySettingDto.GATEWAY_SETTING_REDIS).get();log.info("gateway syn result " + json);if (null != json) {cache = JSONUtil.toBean(json,GatewaySettingDto.class) ;return;}log.warn("gateway has no config ,setting [" + GatewaySettingDto.GATEWAY_SETTING_REDIS + "] !");} catch (Exception e) {log.error("gateway syn error", e);}}}
2.1 灰度常量定义(GrayConstant.java)
public class GrayConstant {public static final String VERSION = "application_version"; // 版本标识public static final String HOSIPITAL = "Application-Hospital-Source-Code"; // 医院编码public static final String USER = "application_user_mobile"; // 用户手机号public static final String CUST_ID = "Application-Cust-Id"; // 客户IDpublic static final String ROLE = "application_role"; // 角色标识public static final String SIMPLE_ROLE = "1"; // 简单角色public static final String GRAY_ROLE = "2"; // 灰度角色public static final String DEVELOPER = "developer"; // 开发者标识
}
2.2 网关层灰度流量标记(GrayscaleGlobalFilter)
-
网关层的GrayscaleGlobalFilter是整个灰度系统的入口,负责识别和标记灰度流量。
-
该过滤器实现Spring Cloud Gateway的GlobalFilter接口(order=1,优先级最高),核心逻辑如下:
@Component
@RefreshScope
@Slf4j
public class GrayscaleGlobalFilter implements GlobalFilter, Ordered {@Resourceprivate GatewaySettingService gatewaySettingService;@Resourceprivate WhiteListProperties whiteListProperties;@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {try {ServerHttpRequest request = exchange.getRequest();MultiValueMap querystring = request.getQueryParams();var headers = request.getHeaders();var path = request.getURI().getPath();var setting = gatewaySettingService.find();if (null == setting) {log.error("setting is null .");return chain.filter(exchange);}var version = setting.getReleaseVersion();// 默认正式版本var metadata = new Metadata();// 1. 基于客户ID的灰度判断,从请求头获取灰度标识String custId = headers.getFirst(GrayConstant.CUST_ID);String domainSign = headers.getFirst(Constants.REQUEST_HEADER_DOMAIN_SIGN);List<String> list = Optional.ofNullable(setting.getGrayUserIds().get("JTP")).orElse(List.of());//解决b端请求头中的 Application-Cust-Id 问题if (StrUtil.isNotEmpty(custId) && CollUtil.isNotEmpty(list) && list.contains(custId)&& StrUtil.isNotEmpty(domainSign) && domainSign.endsWith("CUST")) {version = setting.getGrayVersion(); // 切换到灰度版本}log.info(">>>>>>path:{},customerId:{},version:{}>>>>>>", path, custId, version);if (StrUtil.isNotEmpty(custId) && setting.getGrayCustomerIds().contains(custId)&& StrUtil.isNotEmpty(domainSign) && domainSign.endsWith("CUST")) {version = setting.getGrayVersion(); // 切换到灰度版本}log.info("path:{},cust:{},version:{}", path, custId, version);// 2. 基于B端用户ID的灰度判断String applicationBusiId = headers.getFirst(Constants.APPLICATION_BUSI_ID);if (StrUtil.isNotEmpty(applicationBusiId)) {String appCode = domainSign.split("_")[0];boolean gray = Optional.ofNullable(setting.getGrayUserIds()).map(grayUserMap -> grayUserMap.get(appCode)).map(userIds -> {if (CollUtil.isEmpty(userIds)) {return false;}return userIds.contains(applicationBusiId);}).orElse(false);version = gray ? setting.getGrayVersion() : setting.getReleaseVersion();}log.info("path:{},applicationBusiId:{},version:{}", path, applicationBusiId, version);// 3. 基于医院编码的灰度判断String hospitalCode = headers.getFirst(GrayConstant.HOSIPITAL);if (StrUtil.isNotEmpty(hospitalCode) && setting.getGrayHospitals().contains(hospitalCode)) {version = setting.getGrayVersion();}log.info("path:{},hospital:{},version:{}", path, hospitalCode, version);// 4. 基于路径匹配的灰度判断if (isMatchGrayPath(request)) {version = setting.getGrayVersion();}if (isMatchReleasePath(path, setting)) {version = setting.getReleaseVersion();log.info("path:{} -> 配置为Release版本", path);}// // 设置灰度上下文,设置线程是为了传到后面的filtermetadata.setVersion(version + "");GrayReleaseContextHolder.set(metadata);// 添加灰度请求头,传递灰度标记到下游服务,用于 httpClient.request 传递到实例apiServerHttpRequest.Builder mutate = exchange.getRequest().mutate();// 设置头有两个作用,1.loadbalancer的时候使用.2.传递到微服务mutate.header(GrayConstant.VERSION, version + "");// 系统版本return chain.filter(exchange.mutate().request(request).build());} catch (Exception exception) {log.error("GrayscaleGlobalFilter Error.", exception);return chain.filter(exchange);}}@Overridepublic int getOrder() {return 1;}private boolean isMatchGrayPath(ServerHttpRequest request) {List<String> grayPaths = whiteListProperties.getGray();// grayPaths空指针处理if (CollUtil.isEmpty(grayPaths)) {return false;}for (String identity : grayPaths) {if (request.getPath().toString().matches(identity)) {return true;}}return false;}private boolean isMatchReleasePath(String path, GatewaySettingDto setting) {Map<String, String> releasePaths = setting.getReleasePaths();if (CollUtil.isEmpty(releasePaths)) {return false;}if (releasePaths.containsKey(path)) {return true;}return false;}}
关键功能:
- 支持用户ID、医院编码、路径匹配等多维度灰度判断
- 通过GrayReleaseContextHolder维护线程上下文
- 为下游服务添加VERSION头用于灰度路由
2.3 运营平台灰度用户管理(BusinessGrayEnvironmentController)
运营平台提供了完整的灰度用户管理接口,接口示例:
关键功能:
- 新增灰度用户
- 删除灰度用户
- 选择用户
- 查询用户
@Tag(name = "运营平台-B端灰度环境管理")
@RestController
@Slf4j
@RequestMapping("/businessGrayEnvironment")
public class BusinessGrayEnvironmentController {@Resourceprivate UserInfoService userInfoService;@Resourceprivate GatewayApi gatewayApi;@Resourceprivate UserApi userApi;@Operation(summary = "新增灰度用户")@PostMapping("/save")public Result save(@RequestBody GrayUserDTO dto) {//新增时,必须选择应用Validator.validateNotNull(dto.getAppCode(), "应用编码不能为空");Validator.validateNotNull(dto.getUserIds(), "userIds不能为空");// 查询灰度用户GatewaySettingDto gatewaySettingDto = gatewayApi.syn().assertData();if (Objects.nonNull(gatewaySettingDto)) {Map<String, List<String>> grayUserMap = Optional.ofNullable(gatewaySettingDto.getGrayUserIds()).orElseGet(HashMap::new);Set<String> set = new HashSet<>(dto.getUserIds());Optional.ofNullable(grayUserMap.get(dto.getAppCode())).ifPresent(set::addAll);grayUserMap.put(dto.getAppCode(), new ArrayList<>(set));gatewaySettingDto.setGrayUserIds(grayUserMap);}return Result.success(gatewayApi.save(gatewaySettingDto).assertData());}@Operation(summary = "删除(移除)灰度用户")@PostMapping("/delete")public Result delete(@RequestBody GrayUserDTO dto) {Validator.validateNotNull(dto.getAppCode(), "应用编码不能为空");Validator.validateNotNull(dto.getUserIds(), "userIds不能为空");// 查询灰度用户GatewaySettingDto gatewaySettingDto = gatewayApi.syn().assertData();Map<String, List<String>> grayUserMap = gatewaySettingDto.getGrayUserIds();if (Objects.nonNull(gatewaySettingDto) && CollUtil.isNotEmpty(grayUserMap)) {List<String> list = grayUserMap.get(dto.getAppCode());Set set = new HashSet<>();set.addAll(list);set.removeAll(dto.getUserIds());grayUserMap.put(dto.getAppCode(), new ArrayList<>(set));gatewaySettingDto.setGrayUserIds(grayUserMap);}return Result.success(gatewayApi.save(gatewaySettingDto).assertData());}@PostMapping("/selectUser")@Operation(summary = "选择用户")public Result<PageData<UserVo>> selectUser(@RequestBody GrayUserPageDTO dto) {GatewaySettingDto gatewaySettingDto = gatewayApi.syn().assertData();Map<String, List<String>> grayUserMap = gatewaySettingDto.getGrayUserIds();List<String> grayUserIds;if (Objects.nonNull(gatewaySettingDto) && CollUtil.isNotEmpty(grayUserMap)) {grayUserIds = grayUserMap.get(dto.getAppCode());} else {grayUserIds = null;}//患者端 单独处理if (dto.getAppCode().equals("JTP")) {//查询ihm_user_info表IPage<UserInfo> page = userInfoService.lambdaQuery().select(UserInfo::getId, UserInfo::getName, UserInfo::getPhone).notIn(CollUtil.isNotEmpty(grayUserIds), UserInfo::getId, grayUserIds).like(StrUtil.isNotEmpty(dto.getName()), UserInfo::getName, dto.getName()).eq(StrUtil.isNotEmpty(dto.getPhone()), UserInfo::getPhone, DesensitizedUtils.encryption(dto.getPhone())).page(new Page<>(dto.getCurrent(), dto.getSize()));List<UserVo> userVos = page.getRecords().stream().map(userInfo -> {UserVo userVo = BeanUtil.copyProperties(userInfo, UserVo.class);userVo.setMobile(DesensitizedUtils.decrypt(userInfo.getPhone()));return userVo;}).collect(Collectors.toList());Page<UserVo> returnPage = new Page<>(dto.getCurrent(), dto.getSize());BeanUtil.copyProperties(page, returnPage);returnPage.setRecords(userVos);return Result.success(new PageData<>(returnPage));}//查询u_user表下的所有用户UserQueryParam userQueryParam = new UserQueryParam();userQueryParam.setName(dto.getName());userQueryParam.setMobile(dto.getPhone());userQueryParam.setCurrent(dto.getCurrent());userQueryParam.setSize(dto.getSize());userQueryParam.setUserIds(grayUserIds);Page<UserDto> userDtoPage = userApi.page(userQueryParam).assertData();List<UserVo> userVos = userDtoPage.getRecords().stream().map(userDto -> BeanUtil.copyProperties(userDto, UserVo.class)).collect(Collectors.toList());Page<UserVo> returnPage = new Page<>(dto.getCurrent(), dto.getSize());BeanUtil.copyProperties(userDtoPage, returnPage);returnPage.setRecords(userVos);return Result.success(new PageData<>(returnPage));}@Operation(summary = "分页查询")@PostMapping("/page")public Result<PageData<GrayUserVo>> page(@RequestBody GrayUserPageDTO dto) {Integer current = dto.getCurrent();Integer size = dto.getSize();String name = dto.getName();String phone = dto.getPhone();GatewaySettingDto gatewaySettingDto = gatewayApi.syn().assertData();Map<String, List<String>> grayUserMap = Optional.ofNullable(gatewaySettingDto).map(GatewaySettingDto::getGrayUserIds).orElse(Collections.emptyMap());//患者端 单独处理if (StrUtil.equals("JTP", dto.getAppCode())) {List<Long> userIds = grayUserMap.get(dto.getAppCode()).stream().map(Long::valueOf).collect(Collectors.toList());if (CollUtil.isEmpty(userIds)) {return Result.success(new PageData<>());} else {IPage<UserInfo> page = userInfoService.lambdaQuery().select(UserInfo::getId, UserInfo::getName, UserInfo::getPhone).in(Objects.nonNull(userIds), UserInfo::getId, userIds).like(StrUtil.isNotEmpty(name), UserInfo::getName, dto.getName()).eq(StrUtil.isNotEmpty(phone), UserInfo::getPhone, DesensitizedUtils.encryption(dto.getPhone())).page(new Page<>(dto.getCurrent(), dto.getSize()));if (page.getRecords().size() > 0) {for (UserInfo userInfo : page.getRecords()) {userInfo.setPhone(DesensitizedUtils.decrypt(userInfo.getPhone()));}}List<GrayUserVo> grayUserVos = page.getRecords().stream().map(userInfo -> {GrayUserVo grayUserVo = BeanUtil.copyProperties(userInfo, GrayUserVo.class);grayUserVo.setUserId(String.valueOf(userInfo.getId()));grayUserVo.setAppCode(dto.getAppCode());return grayUserVo;}).collect(Collectors.toList());Page<GrayUserVo> returnPage = new Page<>(dto.getCurrent(), dto.getSize());BeanUtil.copyProperties(page, returnPage);returnPage.setRecords(grayUserVos);return Result.success(new PageData<>(returnPage));}}List<GrayUserVo> grayUserVos = CollUtil.newArrayList();List<Long> userIds = grayUserMap.get(dto.getAppCode()).stream().map(Long::valueOf).collect(Collectors.toList());if (CollUtil.isNotEmpty(userIds)) {IdsDto idsDto = new IdsDto();idsDto.setIds(userIds);List<UserDto> userDtos = userApi.queryByIds(idsDto).assertData();grayUserVos = userDtos.stream().map(userDto -> {GrayUserVo grayUserVo = new GrayUserVo();grayUserVo.setUserId(String.valueOf(userDto.getId()));grayUserVo.setName(userDto.getName());grayUserVo.setPhone(userDto.getMobile());grayUserVo.setAppCode(dto.getAppCode());return grayUserVo;}).collect(Collectors.toList());}List<GrayUserVo> subList = grayUserVos.stream().filter(x ->
// (StrUtil.isEmpty(dto.getAppCode()) || Objects.equals(x.getAppCode(), dto.getAppCode())) &&(StrUtil.isEmpty(name) || StrUtil.contains(x.getName(), name)) &&(StrUtil.isEmpty(phone) || StrUtil.contains(x.getPhone(), phone))).skip((current - 1) * size).limit(size).collect(Collectors.toList());int totalPages = (int) Math.ceil((double) grayUserVos.size() / size);return Result.success(new PageData<>(subList, current, subList.size(), size, totalPages));}
}
2.4 负载均衡层过滤服务实例(GrayRoundRobinLoadBalancer)
自定义负载均衡器,基于灰度版本选择服务实例:
/*** 灰度发布增强版轮询负载均衡器* 实现基于版本号和开发者标识的服务实例筛选与路由* 继承Spring Cloud ReactorServiceInstanceLoadBalancer接口,支持响应式负载均衡*/
public class GrayRoundRobinLoadBalancer implements ReactorServiceInstanceLoadBalancer {private static final Logger logger = LoggerFactory.getLogger(GrayRoundRobinLoadBalancer.class);/** 轮询计数器,使用原子整数保证线程安全 */final AtomicInteger position;/** 目标服务ID */final String serviceId;/** 服务实例列表提供者,用于获取可用服务实例 */ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;/*** 构造函数:使用随机种子初始化轮询位置。* 为什么这样做?* 原因:在高并发的分布式系统中,可能会同时创建多个 GrayRoundRobinLoadBalancer 实例。如果这些实例都从 0 开始轮询服务实例,* 就可能出现多个请求同时访问同一个服务实例的情况,无法充分利用所有可用的服务实例,造成负载不均衡。* 通过设置随机的初始轮询位置,不同的负载均衡器实例会从不同的位置开始轮询,使得服务实例的请求分布更加均匀,提高系统的负载均衡效果。* @param serviceId 服务ID* @param serviceInstanceListSupplierProvider 服务实例列表提供者*/public GrayRoundRobinLoadBalancer(String serviceId, ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider) {this(new Random().nextInt(1000), serviceId, serviceInstanceListSupplierProvider);}/*** 构造函数:指定初始轮询位置* @param seedPosition 初始轮询位置* @param serviceId 服务ID* @param serviceInstanceListSupplierProvider 服务实例列表提供者*/public GrayRoundRobinLoadBalancer(int seedPosition, String serviceId, ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider) {this.position = new AtomicInteger(seedPosition);this.serviceId = serviceId;this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider;}/*** 核心负载均衡方法:选择合适的服务实例* @param request 负载均衡请求对象,包含请求上下文信息* @return 封装服务实例的响应对象*/public Mono<Response<ServiceInstance>> choose(Request request) {ServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider.getIfAvailable(NoopServiceInstanceListSupplier::new);return supplier.get(request).next().map(serviceInstances -> processInstanceResponse(supplier, serviceInstances, request));}/*** 处理服务实例响应* @param supplier 服务实例列表提供者* @param serviceInstances 服务实例列表* @param request 请求对象* @return 封装服务实例的响应对象*/private Response<ServiceInstance> processInstanceResponse(ServiceInstanceListSupplier supplier, List<ServiceInstance> serviceInstances, Request request) {Response<ServiceInstance> serviceInstanceResponse = this.getInstanceResponse(serviceInstances, request);if (supplier instanceof SelectedInstanceCallback && serviceInstanceResponse.hasServer()) {((SelectedInstanceCallback) supplier).selectedServiceInstance((ServiceInstance) serviceInstanceResponse.getServer());}return serviceInstanceResponse;}/*** 获取实例响应:实现灰度筛选和轮询选择* @param instances 原始服务实例列表* @param request 请求对象* @return 封装服务实例的响应对象*/private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances, Request request) {if (instances.isEmpty()) {if (logger.isWarnEnabled()) {logger.warn("No servers available for service: " + this.serviceId);}return new EmptyResponse();}instances = getInstances(instances, request);// Do not move position when there is only 1 instance, especially some suppliers// have already filtered instancesif (instances.size() == 1) {return new DefaultResponse(instances.get(0));}// Ignore the sign bit, this allows pos to loop sequentially from 0 to Integer.MAX_VALUEint pos = this.position.incrementAndGet() & Integer.MAX_VALUE;ServiceInstance instance = instances.get(pos % instances.size());return new DefaultResponse(instance);}/*** 灰度实例筛选核心逻辑* @param instances 原始服务实例列表* @param request 请求对象* @return 筛选后的服务实例列表*/private List<ServiceInstance> getInstances(List<ServiceInstance> instances, Request request) {DefaultRequest<RequestDataContext> defaultRequest = Convert.convert(new TypeReference<DefaultRequest<RequestDataContext>>() {}, request);RequestDataContext dataContext = defaultRequest.getContext();RequestData requestData = dataContext.getClientRequest();HttpHeaders headers = requestData.getHeaders();String[] version = new String[] {""} ;if(StrUtil.isEmpty(version[0])){version[0] = headers.getFirst(GrayConstant.VERSION) ; // 网关由于是Nio架构,所以input和output不是同一个线程, context是拿不到version的}GrayReleaseContextHolder.clear();if(StrUtil.isEmpty(version[0])){List<ServiceInstance> list = instances.stream().filter(i-> StrUtil.isBlank(i.getMetadata().get(GrayConstant.DEVELOPER))).collect(Collectors.toList()) ;return list;}List<ServiceInstance> list = instances.stream().filter(i->{String instanceVersion = i.getMetadata().get(GrayConstant.VERSION) ;if(StrUtil.isEmpty(instanceVersion)){return false ;}if(StrUtil.isNotEmpty(headers.getFirst(GrayConstant.DEVELOPER))){logger.info("本地开发调试:{}",headers.getFirst(GrayConstant.DEVELOPER));return StrUtil.equals(instanceVersion, version[0])&&StrUtil.equals(i.getMetadata().get(GrayConstant.DEVELOPER) , headers.getFirst(GrayConstant.DEVELOPER));}else {return StrUtil.equals(instanceVersion, version[0])&&StrUtil.isBlank(i.getMetadata().get(GrayConstant.DEVELOPER)) ;}}).collect(Collectors.toList()) ;logger.info("version:{} ,instances url:{} , list:{}",version[0], requestData.getUrl() , JSONUtil.toJsonStr(list));if(CollectionUtil.isEmpty(list)){list = instances.stream().filter(i-> StrUtil.isBlank(i.getMetadata().get(GrayConstant.DEVELOPER))).collect(Collectors.toList()) ;return list;}else return list ;}
}
该类实现了灰度发布与轮询策略结合的负载均衡器,核心功能如下:
- 灰度筛选:
- 从请求头获取灰度版本号(GrayConstant.VERSION)
- 根据版本号和开发者ID(GrayConstant.DEVELOPER)过滤服务实例
- 优先匹配相同版本且无开发者标签的实例
- 轮询策略:
- 使用原子整数position保证线程安全
- 通过取模运算实现均匀轮询
- 使用随机种子初始化轮询位置,避免请求集中
- 动态路由:
- 支持开发环境调试(通过开发者ID直连特定实例)
- 无灰度实例时自动降级到普通轮询
- 记录日志监控路由决策
- 响应式编程:
- 实现Spring Cloud ReactorServiceInstanceLoadBalancer接口
- 支持非阻塞异步处理
完整处理流程:请求→获取/解析灰度参数→筛选实例列表→轮询选择实例→返回负载均衡结果。
2.5 灰度上下文管理(GrayReleaseContextHolder)
采用**TransmittableThreadLocal
**实现跨线程上下文传递,确保灰度标记在异步调用中正确传递:
为什么用TransmittableThreadLocal?
普通ThreadLocal在异步线程中会丢失上下文,而医疗系统存在大量异步处理(如处方审核通知),使用阿里开源的TTL(TransmittableThreadLocal)可确保上下文在线程池环境中正确传递。
public class GrayReleaseContextHolder {private static final Logger logger = LoggerFactory.getLogger(GrayReleaseContextHolder.class);private static final TransmittableThreadLocal<Metadata> CONTEXT = new TransmittableThreadLocal<>();private static TransmittableThreadLocal<Metadata> currentRequestContext() {if (Objects.isNull(CONTEXT.get())) {Metadata systemDto = new Metadata();CONTEXT.set(systemDto);}return CONTEXT;}public static void clear() {currentRequestContext().remove();}public static void set(Metadata metadata) {currentRequestContext().set(metadata);}public static Metadata get() {return currentRequestContext().get();}
}
public class Metadata {private String version ;public String getVersion() {return version;}public void setVersion(String version) {this.version = version;}
}
该类实现了灰度发布上下文管理功能,核心逻辑如下:
-
线程上下文管理:使用TransmittableThreadLocal存储Metadata对象,确保线程池/异步场景下上下文传递
-
上下文初始化:currentRequestContext()方法确保首次访问时自动创建默认Metadata实例
-
核心操作方法:
-
set():设置当前线程上下文
-
get():获取当前上下文
-
clear():清除上下文防止内存泄漏
三、灰度发布完整流程
3.1 配置阶段
-
运营平台配置
- 管理员登录运营平台
- 选择应用和灰度用户
- 配置灰度规则(用户ID、医院编码、路径等)
-
服务注册
- 灰度服务实例启动时携带
version=gray
元数据 - 正式服务实例携带
version=release
元数据
- 灰度服务实例启动时携带
-
配置下发
- 网关配置实时更新(支持@RefreshScope热刷新)
3.2 请求处理阶段
请求头传递示例:
流量识别:用户发起请求,携带用户ID、医院编码等标识信息
GET /api/patient/list HTTP/1.1
Host: medical.chinaunicom.com
Application-Cust-Id: 123456789
Application-Hospital-Source-Code: 110101
application_version: gray
网关标记:GrayscaleGlobalFilter根据请求信息和配置的灰度规则,判断是否为灰度用户
上下文设置:如果是灰度用户,在请求上下文中设置相应的灰度标识
GrayReleaseContextHolder.get().setVersion("gray");
// 向下游传递版本头
request.mutate().header("VERSION", "gray").build();
负载均衡:GrayRoundRobinLoadBalancer根据上下文中的灰度标识,选择合适的灰度服务实例
服务实例列表:
- instance-1: metadata={version=release}
- instance-2: metadata={version=gray} ✅ 被选中
- instance-3: metadata={version=gray}
服务处理与上下文清理:请求被路由到对应的灰度服务实例进行处理,完成后通过拦截器清除上下文:
@Component
public class GrayReleaseContextInterceptor implements HandlerInterceptor {@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {GrayReleaseContextHolder.clear(); // 防止线程复用导致上下文污染}
}
四、关键技术点解析
4.1 多维度灰度判断
系统支持多种维度的灰度判断:
- 用户维度:根据用户ID判断是否为灰度用户
- 医院维度:根据医院编码判断是否为灰度医院
- 路径维度:根据请求路径判断是否进入灰度流程
- 角色维度:根据用户角色判断是否为灰度用户
4.2 动态配置更新
通过GatewaySettingService实现版本配置动态更新:
@RefreshScope // Spring Cloud配置自动刷新注解
@Component
public class GatewaySettingService {@Value("${gray.version:release}")private String grayVersion;// 实时获取最新灰度版本配置public String getGrayVersion() {return grayVersion;}
}
修改Nacos配置中心的gray.version
参数,网关会自动感知并更新路由策略,无需重启服务。
- 实时生效:使用Spring Cloud Config和@RefreshScope
- 零停机:配置修改无需重启服务
- 版本回退:随时切换回正式版本
4.3 线程安全设计(上下文传递TransmittableThreadLocal)
医疗系统存在大量异步场景(如消息推送、报表生成),通过三层保障确保线程安全:
- TransmittableThreadLocal:上下文跨线程传递
- 拦截器自动清除:请求结束时调用clear()
- AtomicInteger计数器:负载均衡轮询无锁实现
在分布式系统中,特别是在使用异步处理(如线程池、CompletableFuture等)时,普通的ThreadLocal无法正确传递上下文信息。系统采用阿里巴巴开源的TransmittableThreadLocal来解决这个问题:
// 使用TransmittableThreadLocal保证线程安全
private static final TransmittableThreadLocal<Metadata> CONTEXT = new TransmittableThreadLocal<>();
TransmittableThreadLocal能够自动传递线程上下文,确保在异步处理过程中也能正确获取到灰度标识。
TransmittableThreadLocal的作用:
- 解决异步线程上下文传递问题
- 支持Hystrix、CompletableFuture等异步场景
- 避免内存泄漏:每次请求结束后清理上下文
五、前端传值与后端判断示例
5.1 前端传值示例
场景1:B端用户灰度测试
// 前端axios配置
axios.get('/api/business/data', {headers: {'Application-Busi-Id': 'user123456', // B端用户ID'Application-Hospital-Source-Code': '110101', // 医院编码'Domain-Sign': 'JTP_CUST' // 域名标识}
})
场景2:医院维度灰度
// 医院维度灰度
axios.post('/api/medical/record', data, {headers: {'Application-Hospital-Source-Code': '310104' // 北京某医院}
})
5.2 后端判断逻辑
灰度判断流程:
// 网关层判断逻辑简化版
private boolean isGrayUser(ServerHttpRequest request) {String custId = request.getHeaders().getFirst("Application-Cust-Id");String hospitalCode = request.getHeaders().getFirst("Application-Hospital-Source-Code");String busiId = request.getHeaders().getFirst("Application-Busi-Id");// 1. 检查客户ID是否在灰度列表if (grayCustomerIds.contains(custId)) {return true;}// 2. 检查医院是否在灰度列表if (grayHospitals.contains(hospitalCode)) {return true;}// 3. 检查B端用户是否在灰度列表return grayUserIds.get(appCode).contains(busiId);
}
六、常见问题与解决方案
6.1 灰度实例不可用的降级策略
问题描述:当灰度实例出现故障时,如何保证服务的可用性?
解决方案:在GrayRoundRobinLoadBalancer中实现了降级机制:
// GrayRoundRobinLoadBalancer中的降级逻辑
if (CollectionUtil.isEmpty(filteredInstances)) {// 降级到正式版本实例return instances.stream().filter(i -> StrUtil.isBlank(i.getMetadata().get(GrayConstant.DEVELOPER))).collect(Collectors.toList());
}
6.2 上下文传递问题
问题描述:在异步处理场景中,如何保证灰度上下文的正确传递?
解决方案:使用TransmittableThreadLocal替代普通的ThreadLocal,并在请求结束时清理上下文:
// 在拦截器中清理上下文
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {GrayReleaseContextHolder.clear();
}
6.3 灰度实例不匹配
问题:服务实例元数据未正确设置version
解决:在服务启动参数中添加-Dspring.cloud.nacos.discovery.metadata.version=gray
6.4 动态配置不生效
问题:@RefreshScope未生效
解决:确保配置类被Spring容器管理,且配置中心监听正确
6.5 配置同步延迟问题
- 问题:配置更新后,网关实例可能延迟感知
- 解决:使用Spring Cloud Bus实现配置变更广播
6.6 灰度用户列表过大
- 问题:用户列表过大导致内存占用高
- 解决:使用Redis缓存+本地缓存的二级缓存策略
七、实际应用场景
7.1 新功能AB测试
为不同用户群展示不同问诊流程:
GET /api/consultation/process
CUST_ID: 123456 // A流程(新界面)
CUST_ID: 654321 // B流程(旧界面)
场景:新挂号功能上线
- 灰度用户:内部员工+试点医院
- 灰度比例:10% → 30% → 100%
- 监控指标:接口响应时间、错误率、用户满意度
7.2 重大版本升级
场景:医保接口升级
- 灰度策略:按医院逐步切换
- 回滚策略:一键切换回老版本
- 验证周期:2周观察期
7.3 性能压测
场景:双十一前性能测试
- 灰度用户:压测机器人账号
- 灰度实例:独立的压测环境
- 数据隔离:压测数据写入影子库
八、灰度设计亮点
- 低侵入性:通过过滤器和拦截器实现,不侵入业务代码
- 无业务侵入:业务代码无需修改
- 配置驱动:通过配置实现灰度控制
- 插件化:可插拔的灰度组件
- 多维度控制:支持用户/医院/路径等多场景灰度
- 用户维度:根据用户ID判断是否为灰度用户
- 医院维度:根据医院编码判断是否为灰度医院
- 路径维度:根据请求路径判断是否进入灰度流程
- 角色维度:根据用户角色判断是否为灰度用户
- 完善的监控体系-全链路追踪:VERSION头贯穿整个调用链,便于问题定位
- 实时日志:每个灰度决策都有日志记录
- 指标监控:灰度流量占比、错误率监控
- 告警机制:灰度异常自动告警
- 动态调整:配置中心实时更新,无需重启服务
- 降级机制-安全兜底:无灰度实例时自动降级到生产环境
- 易于扩展:模块化设计,便于添加新的灰度判断维度
- 线程安全:使用TransmittableThreadLocal解决异步场景下的上下文传递问题
九、总结与扩展建议
本项目实现的灰度发布系统,通过网关层标记、负载均衡层路由、上下文层传递的三层架构,结合多维度灰度判断和动态配置能力,有效支撑了医疗系统的平稳发布需求。特别是TransmittableThreadLocal的应用,解决了异步场景下的上下文传递难题。
9.1 核心优势
- 技术架构先进:基于Spring Cloud原生实现
- 运维友好:可视化配置,支持热更新
- 安全可靠:多重降级机制,确保系统稳定
- 扩展性强:支持多种灰度策略
9.2 未来扩展方向
- 智能灰度:基于机器学习预测灰度效果
- 监控告警:添加灰度流量占比、响应时间监控,异常时自动熔断,增加灰度流量的监控指标,实时观察灰度发布的效果
- 灰度报告:自动生成灰度发布报告
- 权限细化:支持按功能模块,支持设备类型、地理位置等更多维度的灰度控制
- 规则引擎:引入开源规则引擎(如Drools),支持更复杂的灰度策略
- 流量比例控制:支持按百分比分配灰度流量,而非仅通过用户列表控制
- 跨语言支持:支持Dubbo、gRPC等协议
- 自动化扩缩容:结合Kubernetes等容器编排平台,实现灰度实例的自动扩缩容
9.3 最佳实践建议
- 灰度比例控制:建议从5%开始,逐步扩大
- 监控覆盖:灰度期间加强监控密度
- 回滚预案:制定详细的回滚方案
- 用户沟通:提前告知灰度用户可能的影响