微服务-流量染色

1. 功能目的

通过设置请求头的方式将http请求优先打到指定的服务上,为微服务开发调试工作提供便利

  1. 请求报文难模拟:可以直接在测试环境页面上操作,流量直接打到本地IDEA进行debug
  2. 请求链路较长:本地开发无需启动所有服务,仅需要启动目标服务
  3. 协同开发:与其他人一同开发,并且依赖对方开发的接口,可以直接将自己本地服务的请求发到对方本地服务上
    这是目前主要的使用目的,当然也可以调整负载均衡逻辑,配合网关的一些自定义配置,扩充为灰度发布的效果

2. 实现原理

通过在网关以及Ribbon,实现自定义的负载均衡策略,将请求引流到本地。
PS:需要服务器能访问本地,需要类似OpenVPN这样赋予本地一个IP,供服务器网关能请求到本地
开启OpenVPN之后本机会有多个IP,通过配置指定注册到nacos上的IP

spring.cloud.nacos.discovery.network-interface: 10.0

2.1 本地服务调整

本地启动时,配置文件添加参数,设置一个元数据作为服务的流量标识
比如mdm服务配置参数

spring.cloud.nacos.discovery.metadata.request-mark: azhuzhu

服务启动之后,我们可以在nacos的服务列表里看到元数据
在这里插入图片描述

2.2 网关负载均衡

服务分类:

  1. 服务器服务:metadata中没有 requestMark 参数
  2. 本地服务:metadata中带有 requestMark 参数

网关实现自定义负载均衡策略:

  • 判断请求头中是否带有本地流量标识:requestMark
    • 有标识:判断有无metadata匹配的服务实例
      • 有:调用匹配的服务实例
      • 无:判断目标请求有没有服务器服务实例
        • 有:服务器服务随机数负载
        • 无:可用实例随机负载
    • 无标识:判断目标请求有没有服务器服务实例
      • 有:服务器服务随机数负载
      • 无:可用实例随机负载
        网关负载处理了第一目标服务,假如调用链路为 mdm -> commom-api,我们启动的是common-api,则需要在服务端的ribbon中做负载
        在这里插入图片描述

2.3 请求发起

比如,约定request-mark作为本地流量标识
请求头 request-mark=azhuzhu 表示流量优先打到带有元数据 request-mark=azhuzhu 的服务

  • 浏览器操作:通过浏览器插件ModHeader,在浏览器发起请求时,带上请求头
  • HTTP工具:带上自定义请求头

3. 具体代码介绍

以下所有注册的bean, 都通过指定的配置参数开启

@ConditionalOnProperty(name = "hg.request-mark.enable", havingValue = "true")

3.1 自定义负载均衡器

网关及其他客户端的流量染色具体的负载逻辑实现

import com.google.common.collect.Lists;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.reactive.DefaultResponse;
import org.springframework.cloud.client.loadbalancer.reactive.EmptyResponse;
import org.springframework.cloud.client.loadbalancer.reactive.Request;
import org.springframework.cloud.client.loadbalancer.reactive.Response;
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClients;
import org.springframework.cloud.loadbalancer.core.NoopServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.core.ReactorLoadBalancer;
import org.springframework.cloud.loadbalancer.core.ReactorServiceInstanceLoadBalancer;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.env.Environment;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import reactor.core.publisher.Mono;import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.stream.Collectors;/*** 自定义负载均衡器 用于开发环境的流量染色** @author 阿猪 2024-08-09 11:45*/
@Slf4j
@Configuration
@SuppressWarnings("deprecation")
@ConditionalOnProperty(name = "hg.request-mark.enable", havingValue = "true")
@LoadBalancerClients(defaultConfiguration = {ReqMarkLoadBalancer.class})
public class ReqMarkLoadBalancer {public static final String REQUEST_MARK = "request-mark";/*** 开启流量染色时 替换默认的负载器** @param environment               环境信息* @param loadBalancerClientFactory 负载器工厂* @return 自定义负载器*/@Bean@Primarypublic ReactorLoadBalancer<ServiceInstance> reactorServiceInstanceLoadBalancer(Environment environment,LoadBalancerClientFactory loadBalancerClientFactory) {String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);return new RandomLoadBalancer(loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class), name);}static class RandomLoadBalancer implements ReactorServiceInstanceLoadBalancer {private final ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;private final String serviceId;public RandomLoadBalancer(ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider,String serviceId) {this.serviceId = serviceId;this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider;}@Overridepublic Mono<Response<ServiceInstance>> choose(Request request) {ServiceInstanceListSupplier supplier =serviceInstanceListSupplierProvider.getIfAvailable(NoopServiceInstanceListSupplier::new);String requestMark = getRequestMark(request);return supplier.get().next().map(serviceInstances -> processInstanceResponseByReqMark(serviceInstances, requestMark));}private String getRequestMark(Request<?> request) {// 客户端的负载 直接从 RequestContextHolder 拿请求头ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();if (attributes != null) {return attributes.getRequest().getHeader(REQUEST_MARK);}// 网关的负载从 request 取值(网关覆盖了默认实现 把context塞进去了 不然拿到是 null 跟spring boot版本有关系)if (!(request.getContext() instanceof ServerHttpRequest)) {return null;}ServerHttpRequest context = (ServerHttpRequest) request.getContext();if (context.getHeaders().containsKey(REQUEST_MARK)) {return context.getHeaders().getFirst(REQUEST_MARK);}return null;}/*** 默认的随机数负载** @param instances 可用服务实例* @return 命中实例*/private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances) {if (CollectionUtils.isEmpty(instances)) {log.warn("No instance available {}", serviceId);return new EmptyResponse();}Random random = new Random();ServiceInstance instance = instances.get(random.nextInt(instances.size()));return new DefaultResponse(instance);}private Response<ServiceInstance> processInstanceResponseByReqMark(List<ServiceInstance> instances, String requestMark) {if (instances.isEmpty()) {return new EmptyResponse();}ServiceInstance sameClusterNameInst = selectInstanceByReqMark(instances, requestMark);return new DefaultResponse(sameClusterNameInst);}private ServiceInstance selectInstanceByReqMark(List<ServiceInstance> instances, String requestMark) {// 元数据不带请求标识的服务, 标识为服务器上的服务List<ServiceInstance> serverInstances = instances.stream().filter(instance -> {Map<String, String> metadata = instance.getMetadata();return MapUtils.isEmpty(metadata) || !metadata.containsKey(REQUEST_MARK);}).collect(Collectors.toList());if (StringUtils.isBlank(requestMark)) {if (CollectionUtils.isEmpty(serverInstances)) {return instances.get(new Random().nextInt(instances.size()));}return serverInstances.get(new Random().nextInt(serverInstances.size()));}List<ServiceInstance> matchInstances = Lists.newArrayList();for (ServiceInstance instance : instances) {Map<String, String> metadata = instance.getMetadata();if (MapUtils.isEmpty(metadata)) {continue;}if (metadata.containsKey(REQUEST_MARK) && requestMark.equals(metadata.get(REQUEST_MARK))) {matchInstances.add(instance);}}Random random = new Random();// 优先匹配到的服务  最后是随机if (CollectionUtils.isNotEmpty(matchInstances)) {return matchInstances.get(random.nextInt(matchInstances.size()));}// 然后是无标识服务(服务器上的服务)if (CollectionUtils.isNotEmpty(serverInstances)) {return serverInstances.get(random.nextInt(serverInstances.size()));}// 前两者都没有就随机获取return instances.get(random.nextInt(instances.size()));}}}

3.2 流量标识请求头透传

这里使用Feign进行内部服务调用,需要将原请求的流量标识 请求头继续传递下去,保证后续的服务链路也能有流量染色的效果。

import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;/*** 流量染色 - 流量标识请求头透传** @author 阿猪 2024-09-25 17:11*/
@Component
@ConditionalOnProperty(name = "hg.request-mark.enable", havingValue = "true")
public class ReqMarkRequestInterceptor implements RequestInterceptor {@Overridepublic void apply(RequestTemplate requestTemplate) {// 从 request 中获取流量标识, 设置到 feign 的 requestTemplate中ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();if (attributes != null) {requestTemplate.header(ReqMarkLoadBalancer.REQUEST_MARK, attributes.getRequest().getHeader(ReqMarkLoadBalancer.REQUEST_MARK));}}}

3.3 网关负载均衡-请求信息获取

由于这个方案中,负载均衡是依靠 请求头 判断的,详见上面请求头的获取ReqMarkLoadBalancer.getRequestMark
在spring boot 2.3.2 版本中 request.getContext是个空的,没法获取请求信息
2.6.x 后面没有这个问题,但需要关注下这个context的类型,调整代码
以下是覆盖默认实现,为 request 的 context 设置请求信息。
实际上是复制 ReactiveLoadBalancerClientFilter的源码稍作修改,看倒数最后两行

import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.reactive.DefaultRequest;
import org.springframework.cloud.client.loadbalancer.reactive.Request;
import org.springframework.cloud.client.loadbalancer.reactive.Response;
import org.springframework.cloud.gateway.config.LoadBalancerProperties;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.ReactiveLoadBalancerClientFilter;
import org.springframework.cloud.gateway.support.DelegatingServiceInstance;
import org.springframework.cloud.gateway.support.NotFoundException;
import org.springframework.cloud.loadbalancer.core.ReactorLoadBalancer;
import org.springframework.cloud.loadbalancer.core.ReactorServiceInstanceLoadBalancer;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;import java.net.URI;import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.*;/*** 覆盖默认的负载均衡器 改了choose方法 将request信息传入** @see ReactiveLoadBalancerClientFilter* @author 阿猪 2024-08-09 17:06*/
@Slf4j
@Component
@ConditionalOnProperty(name = "hg.request-mark.enable", havingValue = "true")
public class CustomLoadBalancerClientFilter extends ReactiveLoadBalancerClientFilter {private static final int LOAD_BALANCER_CLIENT_FILTER_ORDER = 10150;private final LoadBalancerClientFactory clientFactory;private final LoadBalancerProperties properties;public CustomLoadBalancerClientFilter(LoadBalancerClientFactory clientFactory,LoadBalancerProperties properties) {super(null, null);this.clientFactory = clientFactory;this.properties = properties;}@Overridepublic int getOrder() {return LOAD_BALANCER_CLIENT_FILTER_ORDER;}@Override@SuppressWarnings("Duplicates")public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {URI url = exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR);String schemePrefix = exchange.getAttribute(GATEWAY_SCHEME_PREFIX_ATTR);if (url == null|| (!"lb".equals(url.getScheme()) && !"lb".equals(schemePrefix))) {return chain.filter(exchange);}// preserve the original urladdOriginalRequestUrl(exchange, url);if (log.isTraceEnabled()) {log.trace(ReactiveLoadBalancerClientFilter.class.getSimpleName()+ " url before: " + url);}return choose(exchange).doOnNext(response -> {if (!response.hasServer()) {throw NotFoundException.create(properties.isUse404(),"Unable to find instance for " + url.getHost());}ServiceInstance retrievedInstance = response.getServer();URI uri = exchange.getRequest().getURI();// if the `lb:<scheme>` mechanism was used, use `<scheme>` as the default,// if the loadbalancer doesn't provide one.String overrideScheme = retrievedInstance.isSecure() ? "https" : "http";if (schemePrefix != null) {overrideScheme = url.getScheme();}DelegatingServiceInstance serviceInstance = new DelegatingServiceInstance(retrievedInstance, overrideScheme);URI requestUrl = reconstructURI(serviceInstance, uri);if (log.isTraceEnabled()) {log.trace("LoadBalancerClientFilter url chosen: " + requestUrl);}exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, requestUrl);}).then(chain.filter(exchange));}@SuppressWarnings("deprecation")private Mono<Response<ServiceInstance>> choose(ServerWebExchange exchange) {URI uri = exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR);ReactorLoadBalancer<ServiceInstance> loadBalancer = this.clientFactory.getInstance(uri.getHost(), ReactorServiceInstanceLoadBalancer.class);if (loadBalancer == null) {throw new NotFoundException("No loadbalancer available for " + uri.getHost());}// 就改了这里 仅调整参数传入 保持原有逻辑(原代码传入了空的request)Request<?> request = new DefaultRequest<>(exchange.getRequest());return loadBalancer.choose(request);}}

将bean覆盖掉,替换掉原本的 ReactiveLoadBalancerClientFilter

import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.gateway.config.LoadBalancerProperties;
import org.springframework.cloud.gateway.filter.ReactiveLoadBalancerClientFilter;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;/*** 流量染色 - 将ReactiveLoadBalancerClientFilter覆盖掉 为了获取到http请求头** @author 阿猪 2024-08-09 17:12*/
@Configuration
public class CustomLoadBalancerConfig {@Bean@ConditionalOnProperty(name = "hg.request-mark.enable", havingValue = "true")public ReactiveLoadBalancerClientFilter gatewayLoadBalancerClientFilter(LoadBalancerClientFactory clientFactory,LoadBalancerProperties properties){return new CustomLoadBalancerClientFilter(clientFactory, properties);}}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/433067.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

[附源码]网上订餐系统+SpringBoot+前后端分离

今天带来一款优秀的项目&#xff1a;网上订餐系统源码 。 系统采用的流行的前后端分离结构&#xff0c;包含了“管理端”&#xff0c;“商家管理端”&#xff0c;“用户购买端” 如果您有任何问题&#xff0c;也请联系小编&#xff0c;小编是经验丰富的程序员&#xff01; 一.…

【Python语言初识(五)】

一、文件和异常 在Python中实现文件的读写操作其实非常简单&#xff0c;通过Python内置的open函数&#xff0c;我们可以指定文件名、操作模式、编码信息等来获得操作文件的对象&#xff0c;接下来就可以对文件进行读写操作了。这里所说的操作模式是指要打开什么样的文件&#…

SpringSecurity -- 入门使用

文章目录 什么是 SpringSesurity &#xff1f;细节使用方法 什么是 SpringSesurity &#xff1f; 在我们的开发中&#xff0c;安全还是有些必要的 用 拦截器 和 过滤器 写代码还是比较麻烦。 SpringSecurity 是 SpringBoot 的底层安全默认选型。一般我们需要认证和授权&#xf…

程序编译的四个阶段

程序编译的四个阶段 #include <stdio.h>int main(){printf("Hello World~");return 0; } hello.c程序的生命周期从一个高级C语言程序开始&#xff0c;这种形式容易被人读懂。 但这无法直接被计算机读懂。为了在系统上运行hello.c程序&#xff0c;每条C语言都…

mysql数据库的基本管理

目录 一.数据库的介绍 二.mariadb的安装 三.软件基本信息 四.数据库开启 五.数据库的安全初始化 六.数据库的基本管理 七.数据密码管理 八.用户授权 九.数据库的备份 十.web控制器 一.数据库的介绍 1.什么是数据库 数据库就是个高级的表格软件 2.常见数据库 Mysql Oracl…

[大语言模型] 情感认知在大型语言模型中的近期进展-2024-09-26

[大语言模型] 情感认知在大型语言模型中的近期进展-2024-09-26 论文信息 Title: Recent Advancement of Emotion Cognition in Large Language Models Authors: Yuyan Chen, Yanghua Xiao https://arxiv.org/abs/2409.13354 情感认知在大型语言模型中的近期进展 《Recent A…

JVM 垃圾回收算法细节

目录 前言 GC Root 可达性分析 根节点枚举 安全点 安全区域 记忆集与卡表 写屏障 并行的可达性分析 前言 学习了几种垃圾收集算法之后&#xff0c; 我们再来看看它们在具体实现上有什么细节之处&#xff0c;我们所能看到的理论很简单&#xff0c;但是实现起来那…

如何把PDF样本册转换为网址链接

​随着互联网的普及&#xff0c;将纸质或PDF格式的样本册转化为网址链接&#xff0c;以便于在线浏览和分享&#xff0c;变得越来越重要。本文将为您详细讲解如何将PDF样本册转换为网址链接&#xff0c;让您轻松实现线上展示和分享。 一、了解PDF样本册与网址链接 1. PDF样本册…

详解电力物联网通常使用哪些通信规约?

在电力物联网行业中&#xff0c;通信规约是关键的技术之一&#xff0c;用于实现电网设备与控制中心之间的数据通信和信息管理。本篇就为大家简单说明电力物联网通常使用哪些通信规约。 1、IEC 60870-5-101/104 这是由国际电工委员会&#xff08;IEC&#xff09;制定的一系列标…

99%的人都不知道的AI绘图变现赚钱秘诀,都在这里了!

AI绘画发展至今&#xff0c;已经有很多实际落地的应用场景&#xff0c;这里介绍几种AI绘图热门变现方式 AI儿童绘本 各大平台上故事绘本、幼儿园儿歌、英文绘本、古诗词&#xff0c;从下图里&#xff0c;可以看出需求量很大 AI儿童绘本 实现方式 \1. gpt\2. leonardo.ai\3.…

Arduino的wifi连接,如何关闭低功耗模式?

&#x1f3c6;本文收录于《全栈Bug调优(实战版)》专栏&#xff0c;主要记录项目实战过程中所遇到的Bug或因后果及提供真实有效的解决方案&#xff0c;希望能够助你一臂之力&#xff0c;帮你早日登顶实现财富自由&#x1f680;&#xff1b;同时&#xff0c;欢迎大家关注&&am…

linuxC命令5

目录 2.1概念 2.2格式 5.1根据宏是否定义 5.2根据宏值 5.3防止头文件重复包含 指针函数 2.1概念 本质上是函数&#xff0c;返回值是指针类型 2.2格式 数据类型 * 函数名&#xff08;参数列表&#xff09; { 函数体&#xff1b; return 地址&#xff1b;//失败一般会返回NULL } …

虚幻蓝图Ai随机点移动

主要函数: AI MoveTo 想要AI移动必须要有 导航网格体边界体积 (Nav Mesh Bounds Volume) , 放到地上放大 , 然后按P键 , 可以查看范围 然后创建一个character类 这样连上 AI就会随机运动了 为了AI移动更自然 , 取消使用控制器旋转Yaw 取消角色移动组件 的 使用控制器所需的…

AI数字人直播爆火,数字人虚拟主播成品牌闲时直播最佳选择!

近年来&#xff0c;随着互联网的普及和发展&#xff0c;电商和直播平台在我国迅速崛起。根据中国网络信息中心的数据显示&#xff0c;我国直播用户7.5亿&#xff0c;使用率已经超过70%&#xff0c;直播已经成为企业重要的营销和销售通道。 一、在经历了几年的爆发式增长后&…

C++冷门知识点1

1.特殊情况汇总&#xff1a; 负数&#xff0c;空指针&#xff0c;叶节点&#xff0c;INT_MAX和INT_MIN 2.双指针法(快慢指针&#xff0c;头尾指针)&#xff0c;三数指针法(链表逆序那块) 3.一定要注意极端情况 2.e后边可以跟负数&#xff0c;但是不能跟小数 3.string的push_bac…

Git 使用方法

简介 Git常用命令 Git 全局设置 获取Git 仓库 方法二用的比较多 将仓库链接复制 在 git base here ----> git clone 仓库链接 工作区、暂存区、版本库 Git 工作区中文件中的状态 本地仓库的操作 远程仓库操作 git pull 将代码推送到远程仓库 1. git add 文件名 ---放…

Visual Studio导出动态库

1、创建新项目&#xff0c;选择如下 2、工程目录结构如下 3、编写pch.h文件&#xff0c;内容如下 // pch.h: 这是预编译标头文件。 // 下方列出的文件仅编译一次&#xff0c;提高了将来生成的生成性能。 // 这还将影响 IntelliSense 性能&#xff0c;包括代码完成和许多代码浏…

实现简易 vuedraggable 的拖拽排序功能

一、案例效果 拖拽计数4实现手动排序 二、案例代码 <draggable:list"searchResult.indicator":group"{ name: indicators }"item-key"field"handle".drag-handle-icon"><divclass"field-item"v-for"(item…

JAVA一键预约品质生活尽在掌握高效家政服务系统小程序源码

一键预约&#xff0c;品质生活尽在掌握 —— 高效家政服务系统 &#x1f3e0;【开篇&#xff1a;告别繁琐&#xff0c;拥抱品质生活】&#x1f3e0; 在这个快节奏的时代&#xff0c;我们总在为生活奔波&#xff0c;却往往忽略了家的温馨与整洁。你是否也曾为堆积如山的家务而烦…

禁止吸烟监测系统 基于图像处理的吸烟检测系统 YOLOv7

吸烟是引发火灾的重要原因之一。烟头在未熄灭的情况下&#xff0c;其表面温度可达200℃-300℃&#xff0c;中心温度甚至能高达700℃-800℃。在易燃、易爆的生产环境中&#xff0c;如化工厂、加油站、仓库等&#xff0c;一个小小的烟头就可能引发灾难性的火灾&#xff0c;造成巨…