【Java】在你上一家公司中是如何如何实现蓝绿发布的?

news2024/11/18 23:30:41

一、问题解析

随着微服务应用的不断增加,各个微服务之间的依赖关系也变得比较复杂,各个微服务的更新、升级部署给整个服务域的稳定性带来很大挑战。怎么以不停机的方式部署升级微服务呢?

在进行技术方案的设计之前,我们先来了解一下生产环境的基本部署情况,如下图所示:

用户在面向用户端(下文通称 C 端)下单后,C 端订单系统需要远程调用订单域中的“创建订单“接口。同时,订单域、运单域相关服务都需要调用基础服务域,进行基础数据的查询服务。

从这里也可以看出,基础服务的稳定运行对整个微服务体系至关重要。那如何确保基础服务域不受版本的影响,始终能够提供稳定可控的服务呢?

20.1 设计方案

我们公司为了解决这个问题实现了蓝绿发布。那什么是蓝绿发布呢?

蓝绿发布指的是在蓝、绿两套环境中分别运行项目的两个版本的代码。但是在进行版本发布时只更新其中一个环境,这样方便另一个环境快速回滚。

接下来我们看一下蓝绿发布的基本流程。

如果系统采取蓝绿发布,在下一个版本(base-service v1.2.0)发布之前,会这样部署架构:

当前订单域调用流量进入基础服务域 GREEN 环境。团队计划在 12:00 发布新版本(base-service v1.2.0),这时我们通常会执行下面几个操作。

  • 将新版本 1.2.0 全部发布在 BLUE 环境上。因为此时 BLUE 环境没有任何流量,对运行中的系统无任何影响。
  • 在请求入口对流量进行切分。通常可以按照百分比分配流量,待系统运行良好后,再逐步将流量全部切换到新版本。
  • 如果发现新版本存在严重问题,可以将流量全部切换到原来的环境,实现版本快速回滚

这个过程可以用下面这张图表示:

这个思路听起来很简单,但是怎么实现呢?

这就不得不提到上节课专门提到的路由选择(Router)了,它是 Dubbo 服务调用中非常重要的一步。路由选择的核心思想是在客户端进行负载均衡之前,通过一定的过滤规则,只在服务提供者列表中选择符合条件的提供者。

我们再看上面的实例图,从订单域消费者的视角,服务提供者列表大概是下面这个样子:

然后呢,我们按照比例对入口流量进行分流。例如,80% 的请求颜色为 BLUE,20% 的请求颜色为 GREEN。那些颜色为 BLUE 的请求,在真正执行 RPC 服务调用时,只从服务提供者列表中选择“color=BLUE”的服务提供者。同样,颜色为 GREEN 的请求只选择“color=GREEN”的服务提供者,这就实现了流量切分。

具体的操作是,在 Dubbo 中为这个场景引入 Tag 路由机制。

首先,服务提供者在启动时需要通过“-Dubbo.provider.tag”系统参数来设置服务提供者所属的标签。

例如,在 192.168.3.100 和 192.168.3.101 这两台机器上启动 base-service 程序时,需要添加“-Dubbo.provider.tag=BLUE”系统参数;而在 192.168.4.100 和 192.168.4.101 这两台机器上启动 base-service 程序时,则要添加“-Dubbo.provider.tag=GREEN”系统参数,通过这个操作完成对服务提供者的打标。服务提供者启动后,生成的服务提供者 URL 连接如下所示:

dubbo://192.168.3.100:20880/net.codingw.demo.BaseUser?dubbo.tag=BLUE

下一步,在服务入口对流量进行染色,从而实现流量切分。

蓝绿发布的流量通常是在流量入口处进行染色的。例如,我们可以使用随机加权来实现流量切分算法,用它对流量进行染色,具体示范代码如下:

public static String selectColor(String[] colorArr, int[] weightArr) {
        int length = colorArr.length;
        boolean sameWeight = true;
        int totalWeight = 0;
        for (int i = 0; i < length; i++) {
            int weight = weightArr[i];
            totalWeight += weight;
            if (sameWeight && totalWeight != weight * (i + 1)) {
                sameWeight = false;
            }
        }
        if (totalWeight > 0 && !sameWeight) {
            int offset = ThreadLocalRandom.current().nextInt(totalWeight);
            System.out.println("offset:" + offset);
            for (int i = 0; i < length; i++) {
                if (offset < weightArr[i]) {
                    return colorArr[i];
                }
            }
        }
        return colorArr[ThreadLocalRandom.current().nextInt(length)];
    }

    //测试代码
    public static void main(String[] args) {
        String[] colorArr = new String[]{"GREEN","BLUE"};
        int[] weightArr = new int[] {100,50};
        for(int i = 0; i < 20; i ++) {
            System.out.println(selectColor(colorArr, weightArr));
        }
    }

根据流量切分算法计算得到流量标识后,怎么在消费端跟进流量标识从而进行路由选择呢?我们通常会将染色标记放在 ThreadLocal 中,然后再编写 Filter,获取或者传递路由标签。

但这个只是一个流量的切分算法,那如何动态设置蓝绿的比例或者说权重呢?其实,我们可以为发布系统提供一个设置权重的页面,用户设置完权重后写入到配置中心 (ZooKeeper、Apollo),然后应用程序动态感知到变化,利用最新的权重进行流量切分。

通过流量切分算法计算出一个请求的流量标识后,通常会存储在 ThreadLocal 中,实现代码如下:

publicclassThreadLocalContext {
    privatestaticfinal ThreadLocal<String> tagContext  = newThreadLocal<>();

    publicstaticvoidsetTag(String tag) {
        tagContext.set(tag);
    }

    publicstatic String getTag() {
        return tagContext.get();
    }

    publicstaticvoidresetTag() {
        tagContext.remove();
    }
}


//在整个请求的入口Stringcolor= selectColor(colorArr, weightArr);
try {
  ThreadLocalContext.setTag(color);
  
  //执行第一个远程调用
  invokeRpc1();
  
  //执行另外一个远程调用
  invokeRpc2();
  
} finally {
  ThreadLocalContext.reset();
}

将请求的流量标识存储到线程本地变量之后,还需要将流量标识附加到 RPC 请求调用中,这样才能触发正确的路由选择,具体代码示例如下:

import org.apache.commons.lang3.StringUtils;
import org.apache.dubbo.common.extension.Activate;
import org.apache.dubbo.common.logger.Logger;
import org.apache.dubbo.common.logger.LoggerFactory;
import org.apache.dubbo.rpc.*;
import org.apache.dubbo.rpc.cluster.router.tag.TagRouter;

importstatic org.apache.dubbo.common.constants.CommonConstants.CONSUMER;
importstatic org.apache.dubbo.rpc.Constants.ACCESS_LOG_KEY;

@Activate(group = CONSUMER, value = "tagConsumerFilter")publicclassTagConsumerContextFilterimplementsFilter {

    privatestaticfinalLoggerlogger= LoggerFactory.getLogger(TagConsumerContextFilter.class);

    @Overridepublic Result invoke(Invoker<?> invoker, Invocation invocation)throws RpcException {
        try {
            Stringtag= ThreadLocalContext.getTag();
            if(StringUtils.isNotEmpty(tag)) {
                invocation.setAttachment(TagRouter.NAME, tag );
            }
        } catch (Throwable t) {
            logger.warn("Exception in TagConsumerContextFilter of service(" + invoker + " -> " + invocation + ")", t);
        }

        // 调用链传递return invoker.invoke(invocation);
    }
}

这样在 RPC 调用的过程中,服务调用者就能根据本地线程变量中存储的流量标记,选择不同机房的服务提供者,从而实现蓝绿发布了。

同时,在实际生产环境中,一个调用链条中往往会存在多个 RPC 调用,那第一个 RPC 中的路由标签能自动传递到第二个 RPC 调用吗?

答案是不可以,我们需要再写一个服务端生效的 Filter,示例代码如下:

import org.apache.commons.lang3.StringUtils;
import org.apache.dubbo.common.logger.Logger;
import org.apache.dubbo.common.logger.LoggerFactory;
import org.apache.dubbo.common.extension.Activate;
import org.apache.dubbo.rpc.*;
import org.apache.dubbo.rpc.cluster.router.tag.TagRouter;

importstatic org.apache.dubbo.common.constants.CommonConstants.PROVIDER;

@Activate(group = PROVIDER, value = "tagProviderFilter")publicclassTagProviderContextFilterimplementsFilter {

    privatestaticfinalLoggerlogger= LoggerFactory.getLogger(TagProviderContextFilter.class);

    @Overridepublic Result invoke(Invoker<?> invoker, Invocation invocation)throws RpcException {
        try {
            Stringtag= invocation.getAttachment(TagRouter.NAME);
            if(StringUtils.isNotEmpty(tag)) {
                ThreadLocalContext.setTag(tag);
            }
        } catch (Throwable t) {
            logger.warn("Exception in TagProviderContextFilter of service(" + invoker + " -> " + invocation + ")", t);
        }
        // 调用链传递return invoker.invoke(invocation);
    }
}

也就是将调用链中的 tag 存储到服务端的线程本地上下文环境中,当服务端调用其他服务时,可以继续将 tag 传递到下一个 RPC 调用链中。

这样,我们的蓝绿发布就基本完成了。但这里还有一个问题。规模较大的公司的生产环境往往会运行很多微服务,我们无法将蓝绿机制一下引入到所有微服务当中,必然会存在一部分应用使用蓝绿发布,但其他应用没有使用蓝绿的情况。怎么做到兼容呢?

比方说,我们公司目前核心业务域的蓝绿部署情况如下:

这里,订单域接入了蓝绿发布;C 端应用需要调用订单域相关接口,因此也接入了蓝绿发布;但运单中心并未接入蓝绿发布。这时候,运单中心能调用订单域的服务吗?

要回答这个问题,我们要先看看 Dubbo 官方的降级策略。

  • 如果消费者侧设置了标签,那么如果集群中没有对应标签的服务提供者,默认可以选择不带任何标签的服务提供者进行服务调用。该行为可以通过设置 request.tag.force=true 来禁止,这就是说如果 request.tag.force 为 true,一旦没有对应标签的服务提供者,就会跑出“No Provider”异常。
  • 如果消费者侧没有设置标签,那就只能向集群中没有设置标签的服务提供者发起请求,如果不存在没有标签的服务提供者,则报“No Provider”异常。

回到上面的问题,运单中心由于未接入蓝绿发布,所以不带任何标签,它无法调用订单域的服务。为了解决这个问题,订单域还需要部署一些不带标签的服务。订单域最终的部署大概如下图所示:

也就是说,订单域为了兼容那些还没接入蓝绿发布的应用需要部署 3 套环境,一套为不设置标签的服务提供者,一套为蓝颜色的服务提供者,另一套为绿颜色的服务提供者。

蓝绿发布实践就介绍到这里了,在这节课的最后,我们再来学习一下蓝绿发布底层依托的原理。

20.2 实现原理

先来看一下 Dubbo 服务调用的基本时序图:

我建议你按照这张时序图跟踪一下源码,更加详细地了解 Dubbo 服务调用的核心流程与实现关键点,我在这里总结了几个要点:

  • Dubbo 的服务调用支持容错,对应的抽象类为 AbstractClusterInvoker,它封装了服务调用的基本流程。Dubbo 内置了 failover、failfast、failsafe、failback、forking 等失败容错策略,每一个策略对应 AbstractClusterInvoker 的一个实现;
  • 在调用 AbstractClusterInvoker 服务的时候,首先需要获取所有的服务提供者列表,这个过程我们称之为服务动态发现(具体实现类为 DynamicDirectory)。在获取路由信息之前,需要调用 RouterChain 的 route 方法,执行路由选择策略,筛选出服务动态发现的服务提供者列表。我们这一课的重点,标签路由的具体实现类 TagRouter 就是在这里发挥作用的。

我们也详细拆解一下 TagRouter 的 route 方法。因为这个方法的实现代码比较多,我们还是分步讲解。

第一步,执行静态路由过滤机制,代码如下:

finalTagRouterRuletagRouterRuleCopy= tagRouterRule;
if (tagRouterRuleCopy == null || !tagRouterRuleCopy.isValid() || !tagRouterRuleCopy.isEnabled()) {
    return filterUsingStaticTag(invokers, url, invocation);
}

如果路由规则为空,则根据 tag 进行过滤。我们顺便也看一下基于 tag 的静态过滤机制是如何实现的:

private <T> List<Invoker<T>> filterUsingStaticTag(List<Invoker<T>> invokers, URL url, Invocation invocation) {
   List<Invoker<T>> result;
   Stringtag= StringUtils.isEmpty(invocation.getAttachment(TAG_KEY)) ? url.getParameter(TAG_KEY) 
            :invocation.getAttachment(TAG_KEY)
   if (!StringUtils.isEmpty(tag)) {
        result = filterInvoker(invokers, invoker -> tag.equals(invoker.getUrl().getParameter(TAG_KEY)));
        if (CollectionUtils.isEmpty(result) && !isForceUseTag(invocation)) {
                result = filterInvoker(invokers, invoker ->     
                                       StringUtils.isEmpty(invoker.getUrl().getParameter(TAG_KEY)));
        }
   } else {
        result = filterInvoker(invokers, invoker -> 
                               StringUtils.isEmpty(invoker.getUrl().getParameter(TAG_KEY)));
   }
   return result;
}

尝试从 Invocation(服务调用上下文)中或者从 URL 中获取 tag 的值,根据 tag 是否为空,执行两种不同的策略:

  • 如果 tag 不为空,首先按照 tag 找到服务提供者列表中打了同样标签的服务提供者列表,如果 dubbo.force.tag 的设置为 false,则查找服务提供者列表,筛查出没有打标签的服务提供者列表。
  • 如果 tag 为空,则直接查找没有打标签的服务提供者列表。

我们继续回到 TagRouter 的 route 方法。第二步操作是,按照路由规则进行筛选,具体代码如下:

// if we are requesting for a Provider with a specific tagif (StringUtils.isNotEmpty(tag)) {
   List<String> addresses = tagRouterRuleCopy.getTagnameToAddresses().get(tag);
   if (CollectionUtils.isNotEmpty(addresses)) {
      result = filterInvoker(invokers, invoker -> addressMatches(invoker.getUrl(), addresses));
      if (CollectionUtils.isNotEmpty(result) || tagRouterRuleCopy.isForce()) {
           return result;
      }
   } else {
     result = filterInvoker(invokers, invoker -> tag.equals(invoker.getUrl().getParameter(TAG_KEY)));
   }
   if (CollectionUtils.isNotEmpty(result) || isForceUseTag(invocation)) {
      return result;
   } else {
      List<Invoker<T>> tmp = filterInvoker(invokers, invoker -> addressNotMatches(invoker.getUrl(),
                        tagRouterRuleCopy.getAddresses()));
      return filterInvoker(tmp, invoker -> StringUtils.isEmpty(invoker.getUrl().getParameter(TAG_KEY)));
   }
} 

上面这段代码比较简单,它的过滤思路和静态 tag 过滤是相似的。不同点是,这里可以通过 YAML 格式配置单个服务的路由规则。具体的配置格式如下:

force:trueenabled:truepriority:1key:demo-provider(服务名称)tags:-name:tag1addresses: [ip1, ip2]
  -name:tag2addresses: [ip3, ip4]

这些数据都会记录在注册中心,并在发生变化后实时通知 TagRouter,从而实现路由规则的动态配置。

二、粉丝福利

最近很多同学问我有没有java学习资料,我根据我从小白到架构师多年的学习经验整理出来了一份80W字面试解析文档、简历模板、学习路线图、java必看学习书籍 、 需要的小伙伴斯我一下

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

opencv_核心操作

图像基本操作 访问和修改像素值 import numpy as np import cv2 img cv2.imread(c:/Users/HP/Downloads/basketball.png) h,w,c img.shape #图像大小 print(h,w,c)### 841 1494 3# 通过行和列坐标访问像素值 img[100,100]### 231 ### array([231, 140, 146], dtypeuint8)# …

WordPress 站点 Wp-Login.Php 登录页面,防止被机器扫描爆破终极方法

最近好像不少的 WordPress 站点又迎来了一波疯狂的 wp-login.php 登录页面暴力破解,明月使用的 Dragon 主题也迅速的做出了安全防范响应,第一时间就增强了代码上的安全防御能力。明月代运维的几个站点也在这方面加强了防范措施,基本上实现了永久解决这一顽疾的目标,今天明月…

智能编程支持分析本地工程文件,项目级开发效率再度提升!

CodeGeeX的智能问答新增关联本地文件的系列功能&#xff0c;用户在智能问答的输入框中输入&#xff0c;就可以在弹出框中选择需要使用的指令。从而能够针对所关联的文件给出更有针对性的问答和代码建议。 下面我们将分别介绍在编程工作中&#xff0c;如何关联本地文件&#xff…

Shell脚本01

一、shell脚本 脚本就是可运行的代码的集合&#xff0c;脚本语言&#xff08;计算机语言&#xff09;。 脚本的特点&#xff1a;从上到下&#xff0c;按行执行。 shell 脚本就是在shell环境&#xff08;bin/bash&#xff09;bash就是shell解释器&#xff0c;linux环境下的编…

pytest+requests+allure自动化测试接入Jenkins学习

&#x1f345; 视频学习&#xff1a;文末有免费的配套视频可观看 &#x1f345; 点击文末小卡片 &#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 最近在这整理知识&#xff0c;发现在pytest的知识文档缺少系统性&#xff0c;这里整理一下&…

MCU为什么上电不启动

相信很多朋友们都遇到过&#xff0c;自信满满的将程序下载到板子上&#xff0c;发现MCU居然没启动。 那这个现象可能有很多问题会导致&#xff0c;让我们来看看会有哪些原因。 1、BOOT引脚电平不对&#xff1a; 在GD32 MCU上&#xff0c;BOOT引脚决定了MCU的启动方式&#x…

zabbix监控深信服AD案例+自动发现虚拟服务和链路

文章目录 前言监控功能告警功能 环境准备操作步骤深信服AD开启SNMPSNMP V2SNMP V3 Zabbix导入模版Zabbix中添加深信服AD 前言 深信服AD&#xff0c;应用交付网关&#xff0c;是较为常见的应用发布、负载均衡设备&#xff0c;常用于网络出口。 本案例是通过zabbix的snmp监控深…

拥抱AI-图片学习中的卷积神经算法详解

一、定义 卷积神经算法&#xff08;Convolutional Neural Networks, CNN&#xff09;是深度学习领域中的一种重要算法&#xff0c;特别适用于处理图像相关的任务。以下是卷积神经算法的详细解释&#xff1a; 1. 基本概念 定义&#xff1a;卷积神经网络是一类包含卷积计算且具…

网络代理加速器:太阳HTTP的新一代解决方案(网络代理IP)

在当今数字化时代&#xff0c;网络速度与稳定性是企业和个人用户最为关注的重要问题之一。面对不稳定的网络连接&#xff0c;延迟高和速度慢等问题&#xff0c;传统的网络代理已经难以满足用户的需求。 为了解决这一问题&#xff0c;太阳HTTP推出了全新一代的网络代理加速器&a…

SQL Server 2016导入.bak文件到数据库里面步骤

1、打开SSMS管理器 选择数据库 右键 然后点击还原数据库。 2、选择设备 然后点击三个点 找到本地bak文件&#xff0c;然后点击确定 3、点击确定&#xff0c;会自动弹出来一个成功的提示。

消费增值模式引领业绩飙升与用户活跃

大家好&#xff0c;我是吴军&#xff0c;致力于为您揭示私域电商领域的独特魅力与机遇。 今日&#xff0c;我很高兴与大家分享一个激动人心的成功案例。我们的客户在短短一个月的时间里&#xff0c;业绩就飙升至上百万级别&#xff0c;其用户活跃度更是居高不下&#xff0c;日…

express+宝塔实现文件上传服务

文章目录 服务器部分开启存放文件的端口配置nginx该端口入口手动在/www/wwwroot/file目录下存放一张图片进行访问 express接口部分代码测试 服务器部分 开启存放文件的端口 我这里以83为例 先到对应的服务商开启端口&#xff0c;比如我这里是阿里云 测试&#xff0c;比如这里…

武汉凯迪正大—绝缘强度的测试设备 工频高压耐压交流试验仪 工频耐压试验机

武汉凯迪正大电气有限公司生产KDJS-8A全自动工频耐压试验系统&#xff08;10kVA全自动操作台&#xff09;&#xff0c;是根据国家行业试验标准而设计的试验设备&#xff0c;其安全可靠、功能强、使用方便、维护简单。主要用于对各种电器产品、电气元件、绝缘材料等进行规定电压…

vue3根据按钮切换更新echarts对应的数据

效果图 初始化注意 setOption的函数定义&#xff0c;option是指图表的配置项和数据&#xff0c;notMerge是指是否不跟之前设置的 option 进行合并。默认为 false。即表示合并。如果为 true&#xff0c;表示所有组件都会被删除&#xff0c;然后根据新option 创建所有新组件 //…

如何学习Django4?看这16堂课就够了

目录 写在前面 推荐图书 内容简介 作者简介 前言/序言 改编说明 推荐理由 写在后面 写在前面 本期博主给大家推荐一本关于Python Django4的图书&#xff08;2024年3月刚出版&#xff09;&#xff0c;感兴趣的小伙伴快来看看吧&#xff01; 推荐图书 《Python Django…

网络安全比赛-网络安全事件响应-Server2216(解析)

B-5:网络安全事件响应 任务环境说明: 服务器场景:Server2216(开放链接) 用户名:root密码:123456 1、黑客通过网络攻入本地服务器,通过特殊手段在系统中建立了多个异常进程,找出启动异常进程的脚本,并将其绝对路径作为Flag值提交; 2、黑客通过网络攻入本地服务器,…

72、AndroidStudio 导入项目Connect timed out错误解决

一、背景&#xff1a; 开发过程中难免会 clone 其他的项目&#xff0c;clone 或者下载成功之后。使用 android studio 打开项目时经常遇到 Connect timed out错误如图所示&#xff1a; 二、分析原因&#xff1a; 1、既然链接超时&#xff0c;肯定是 android studio 在运行…

python 魔术方法备忘录

python 魔术方法备忘录 网上收集了一些&#xff0c;列出了比较常用的&#xff0c;特别是第一张。 Python中的魔术方法&#xff08;Magic Methods&#xff09;&#xff0c;也被称为特殊方法&#xff08;Special Methods&#xff09;或双下划线方法&#xff08;Dunder Methods&a…

开放式耳机哪个品牌质量比较好?2024热门王炸品牌推荐!

开放式耳机市场繁杂&#xff0c;品质参差不齐。网红推荐、广告轰炸&#xff0c;让人眼花缭乱。但音频工程师告诉你&#xff0c;音质和舒适度才是关键。我根据多款开放式耳机测评结果&#xff0c;为大家提供选购指南&#xff0c;助你避开陷阱&#xff0c;找到心仪之选。 1、购买…

老版_zabbix安装与grafana可视化的安装(zabbix插件4.1.4)

grafana()的安装 查询、可视化和理解数据&#xff0c;并获取数据警报&#xff0c;无论数据存储在何处。在 Grafana&#xff0c;您可以通过美观、灵活的数据面板创建、探索和共享所有数据。 1. 网络下载安装 [itwisenode2 ]$ cd /opt/software/ #进入下载目录 #下载wegt安装命…