2024年11月

记得之前在别的网站上看到这个喜庆的春节灯笼效果,觉得非常吸引人。虽然网上有一些现成的API可以直接实现,比如这个
春节灯笼API
,但使用后我发现两个问题:一是手机端访问时灯笼没有自适应,二是灯笼上的“春节快乐”四个字不能自定义。

为了解决这些问题,我找到了
这篇文章
,并“借鉴”了其中的源代码,稍加修改后转换成JavaScript方式引入使用。下面有完整的JS代码。

原文可查看效果:
张苹果博客灯笼效果

一,引入使用

距离春节还有不到90天,赶紧试试吧!将以下JS在您的页面直接引入即可。

   <!-- 将以下js放入您的页面即可。text为灯笼文字,默认:新年快乐 -->
   <script src="https://www.vae.zhangweicheng.xyz/web/denglong.js?text=好好学习"> </script>

二,完整JS代码

 // 张苹果博客:https://zhangpingguo.com/
 // 创建并添加元素
function createDengContainer() {
    const container = document.createElement('div');
    container.className = 'deng-container';

    // 从当前脚本的 URL 获取参数
    const scriptSrc = document.currentScript.src;
    const urlParams = new URLSearchParams(scriptSrc.split('?')[1]); // 获取 '?'
    const customText = urlParams.get('text'); // 获取参数名为'text'的值

    // 将获取的文本分割为字符数组,如果没有提供文本,则使用默认的“新年快乐”
    const texts = customText ? customText.split('') : ['新', '年', '快', '乐'];

    texts.forEach((text, index) => {
        const box = document.createElement('div');
        box.className = `deng-box deng-box${index + 1}`;

        const deng = document.createElement('div');
        deng.className = 'deng';

        const xian = document.createElement('div');
        xian.className = 'xian';

        const dengA = document.createElement('div');
        dengA.className = 'deng-a';

        const dengB = document.createElement('div');
        dengB.className = 'deng-b';

        const dengT = document.createElement('div');
        dengT.className = 'deng-t';
        dengT.textContent = text;

        dengB.appendChild(dengT);
        dengA.appendChild(dengB);
        deng.appendChild(xian);
        deng.appendChild(dengA);

        const shuiA = document.createElement('div');
        shuiA.className = 'shui shui-a';

        const shuiC = document.createElement('div');
        shuiC.className = 'shui-c';
        const shuiB = document.createElement('div');
        shuiB.className = 'shui-b';

        shuiA.appendChild(shuiC);
        shuiA.appendChild(shuiB);
        deng.appendChild(shuiA);
        box.appendChild(deng);
        container.appendChild(box);
    });

    document.body.appendChild(container);
}

// 添加CSS样式
function addStyles() {
    const style = document.createElement('style');
    style.type = 'text/css';
    style.textContent = `
        .deng-container {
            position: relative;
            top: 10px;
            opacity: 0.9;
            z-index: 9999;
            pointer-events: none;
        }
        .deng-box {
            position: fixed;
            right: 10px;
        }
        .deng-box1 { position: fixed; top: 15px; left: 20px; }
        .deng-box2 { position: fixed; top: 12px; left: 130px; }
        .deng-box3 { position: fixed; top: 10px; right: 110px; }
        .deng {
            position: relative;
            width: 120px;
            height: 90px;
            background: rgba(216, 0, 15, .8);
            border-radius: 50% 50%;
            animation: swing 3s infinite ease-in-out;
            box-shadow: -5px 5px 50px 4px #fa6c00;
        }
        .deng-a { 
            width: 100px; 
            height: 90px; 
            background: rgba(216, 0, 15, .1); 
            border-radius: 50%;  
            border: 2px solid #dc8f03; 
            margin-left: 7px; 
            display: flex; 
            justify-content: center; 
        }
        .deng-b { 
            width: 65px; 
            height: 83px; 
            background: rgba(216, 0, 15, .1); 
            border-radius: 60%; 
            border: 2px solid #dc8f03; 
        }
        .xian { 
            position: absolute; 
            top: -20px; 
            left: 60px; 
            width: 2px; 
            height: 20px; 
            background: #dc8f03; 
        }
        .shui-a { 
            position: relative; 
            width: 5px; 
            height: 20px; 
            margin: -5px 0 0 59px; 
            animation: swing 4s infinite ease-in-out; 
            transform-origin: 50% -45px; 
            background: orange; 
            border-radius: 0 0 5px 5px; 
        }
        .shui-b { 
            position: absolute; 
            top: 14px; 
            left: -2px; 
            width: 10px; 
            height: 10px; 
            background: #dc8f03; 
            border-radius: 50%; 
        }
        .shui-c { 
            position: absolute; 
            top: 18px; 
            left: -2px; 
            width: 10px; 
            height: 35px; 
            background: orange; 
            border-radius: 0 0 0 5px; 
        }
        .deng:before, .deng:after { 
            content: " "; 
            display: block; 
            position: absolute; 
            border-radius: 5px; 
            border: solid 1px #dc8f03; 
            background: linear-gradient(to right, #dc8f03, orange, #dc8f03, orange, #dc8f03); 
        }
        .deng:before { 
            top: -7px; left: 29px; height: 12px; width: 60px; z-index: 999; 
        }
        .deng:after { 
            bottom: -7px; left: 10px; height: 12px; width: 60px; margin-left: 20px; 
        }
        .deng-t { 
            font-family: '华文行楷', Arial, Lucida Grande, Tahoma, sans-serif; 
            font-size: 3.2rem; 
            color: #dc8f03; 
            font-weight: 700; 
            line-height: 85px; 
            text-align: center; 
        }
        @media (max-width: 768px) { 
            .deng-t { font-size: 2.5rem; }  
            .deng-box { transform: scale(0.5); top: -15px; }  
            .deng-box1 { left: -22%; }  
            .deng-box2 { left: 0px; }  
            .deng-box3 { right: 50px; }  
            .deng-box4 { right: -10px; }  
        }
        @keyframes swing { 
            0% { transform: rotate(-10deg); }  
            50% { transform: rotate(10deg); }  
            100% { transform: rotate(-10deg); }  
        }
    `;
    document.head.appendChild(style);
}

// 引入时调用
function init() {
    addStyles();
    createDengContainer();
}

// 调用初始化函数
init();

三,页面使用

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>春节灯笼</title>
</head>
<body>
    <!-- 引入js即可,text为灯笼文字,默认:新年快乐 -->
   <script src="./denglong.js?text=等着下班"> </script>
 </body></html>

四,效果图

一、介绍
今天是这个系列《C++之 Opencv 入门到提高》得第三篇文章。今天这篇文章也不难,主要介绍如何使用 Opencv 对图像进行掩膜处理,提高图像的对比度。在这个过程中,我们可以学到如何获取图像指针、如何处理像素值越界等问题。我们一步一个脚印的走,收获就会越来越多。虽然操作很简单,但是要下功夫理解每个技术点,把基础打扎实,才能让以后得学习过程更顺利一点。OpenCV 具体的简介内容,我就不多说了,网上很多,大家可以自行脑补。
OpenCV 的官网地址:
https://opencv.org/
,组件下载地址:
https://opencv.org/releases/

OpenCV 官网学习网站:
https://docs.opencv.ac.cn/4.10.0/index.html

我需要进行说明,以防大家不清楚,具体情况我已经罗列出来。
操作系统:Windows Professional 10(64位)
开发组件:OpenCV – 4.10.0
开发工具:Microsoft Visual Studio Community 2022 (64 位) - Current版本 17.8.3
开发语言:C++(VC16)

二、实例学习
这一节的内容不多,接口 API 其实也是挺简单的。但是我们学习接口,不能冷冰冰的只是学API、一些基础知识也是要掌握的。
图像中的掩膜(Mask)是什么?在图像处理中,掩膜(Mask)是一种用于控制图像处理区域或处理过程的特殊图像。它通常是一个与原始图像同样大小的二维矩阵,用于选择性地遮盖或显示图像的特定区域。掩膜可以用于多种图像处理任务,如图像分割、特征提取、增强等。
在数字图像处理中,掩膜通常是一个二进制图像,其中像素值为1的区域表示要保留的区域,像素值为0的区域表示要排除的区域。通过将掩膜与原始图像进行逻辑运算,可以创建新的图像,其中只有掩膜中标记为1的区域被保留,其他区域被排除。
掩膜在图像处理中有多种应用。例如,在图像分割中,掩膜可用于选择性地突出显示感兴趣的区域,以便进一步处理或分析。在特征提取中,掩膜可用于提取图像中的特定形状或结构。此外,掩膜还可以用于图像增强,例如通过模糊或锐化特定区域来改善图像质量。

数字图像处理中,掩模为二维矩阵数组,有时也用多值图像,图像掩模主要用于:
①提取感兴趣区,用预先制作的感兴趣区掩模与待处理图像相乘,得到感兴趣区图像,感兴趣区内图像值保持不变,而区外图像值都为0。
②屏蔽作用,用掩模对图像上某些区域作屏蔽,使其不参加处理或不参加处理参数的计算,或仅对屏蔽区作处理或统计。
③结构特征提取,用相似性变量或图像匹配方法检测和提取图像中与掩模相似的结构特征。
④特殊形状图像的制作。

什么是位图与掩膜的与运算?
其实就是原图中的每个像素和掩膜中的每个对应像素进行与运算。比如1 & 1 = 1;1 & 0 = 0;
比如一个3 * 3的图像与3 * 3的掩膜进行运算,得到的结果图像就是:

说白了,我们使用掩膜(mask)位图来选择哪个像素允许拷贝,哪个像素不允许拷贝。如果mask像素的值是非0的,我就拷贝它,也就是保留下,否则不拷贝,不保留。

在所有图像基本运算的操作函数中,凡是带有掩膜(mask)的处理函数,其掩膜都参与运算(输入图像运算完之后再与掩膜图像或矩阵运算)。

1 #include <opencv2/opencv.hpp>
2 #include <iostream>
3 #include <math.h>
4 
5 using namespacestd;6 using namespacecv;7 
8 
9 intmain()10 {11 Mat src, dst;12     src = imread("D:\\360MoveData\\Users\\Administrator\\Desktop\\TestImage\\4.jpg", IMREAD_UNCHANGED);13     if (!src.data)14 {15         cout << "图片加载错误!!!" <<endl;16         return -1;17 }18 
19     namedWindow("原始图像", WINDOW_AUTOSIZE);20     imshow("原始图像", src);21 
22     //1、获取图形像素指针23     //CV_Assert(myImage.depth()==CV_8U);24     //Mat.ptr<uchar>(int i=0)获取像素矩阵的指针,索引 i 表示第几行,从0开始计数。25     //获取当前行指针 const uchar* current=myImage.ptr<uchar>(row);26     //获取当前像素点 p(row,col) 的像素值 p(row,col)=current[col];27 
28     //2、像素范围处理 saturate_cast<uchar>29     //saturate_cast<uchar>(-100),返回 0.30     //saturate_cast<uchar>(288),返回 255,31     //saturate_cast<uchar>(100),返回 100.32     //这个函数的功能是确保 RGB 值的范围在 0-255 之间。
33 
34     double startDate =getTickCount();35 
36     //第一种实现对比度
37     /*int cols = (src.cols - 1) * src.channels();38 int offerts = src.channels();39 int rows = src.rows;40 
41 dst = Mat(src.size(), src.type());42 
43 for (int row = 1; row < (rows - 1); row++)44 {45 const uchar* previous = src.ptr<uchar>(row - 1);46 const uchar* current = src.ptr<uchar>(row);47 const uchar* next = src.ptr<uchar>(row + 1);48 uchar* output = dst.ptr<uchar>(row);49 for (int col = offerts; col < cols; col++)50 {51 output[col] = saturate_cast<uchar>(5 * current[col] - (current[col - offerts] + current[col + offerts] + previous[col] + next[col]));52 }53 }*/
54 
55     //3、定义掩膜56     //Mat kernel = (Mat_<char>(3, 3) << 0, -1, 0, -1, 5, -1, 0, -1, 0);57     //filter2D(src, dst, src.depth(), kernel):src 是原图,dst 是目标图,src.depth() 表示位图深度,有 32,24,8 等。58 
59     //第二种实现对比度
60     Mat kernel = (Mat_<char>(3, 3) << 0, -1, 0, -1, 5, -1, 0, -1, 0);61 filter2D(src, dst, src.depth(), kernel);62 
63     double totalTime = (getTickCount() - startDate) /getTickFrequency();64 
65     cout << "消费时间:"<< totalTime <<endl;66 
67     namedWindow("对比度图像", WINDOW_AUTOSIZE);68     imshow("对比度图像", dst);69 
70 
71     waitKey(0);72 
73     return 0;74 }

生成的效果图如下:

当然了,在源码中,提供了两种实现,效果都是一样的。一种是自己实现的,一种是通过调用接口实现的。这也说明了一个问题,只要你掌握的够深入,和接口效果一样的问题,你也可以写得出。

如果我们吧注释的代码打开,把【第二种实现】注释掉,源码:

1 int cols = (src.cols - 1) *src.channels();2 int offerts =src.channels();3 int rows =src.rows;4 
5 dst =Mat(src.size(), src.type());6 
7 for (int row = 1; row < (rows - 1); row++)8 {9     const uchar* previous = src.ptr<uchar>(row - 1);10     const uchar* current = src.ptr<uchar>(row);11     const uchar* next = src.ptr<uchar>(row + 1);12     uchar* output = dst.ptr<uchar>(row);13     for (int col = offerts; col < cols; col++)14 {15         output[col] = saturate_cast<uchar>(5 * current[col] - (current[col - offerts] + current[col + offerts] + previous[col] +next[col]));16 }17 }

而且,自己写的性能更好。对比如图:

再看看我们自己写的运行时间:

我们自己写实现的效果:



三、总结
这是 C++ 使用 OpenCV 的第三篇文章,概念挺难懂的,其实操作起来也没那么难,当然了,这只是我们入门的开始,那就继续吧。皇天不负有心人,不忘初心,继续努力,做自己喜欢做的,开心就好。

本文分享自
《华为云DTSE》第五期开源专刊
,作者:聂子雄 华为云高级工程师、李来 华为云高级工程师。

微服务是一种用于构建应用的架构方案,可使应用的各个部分既能独立工作,又能协同配合,微服务的治理模式在适应云原生的方向也逐步在演进中。本文以汽车行业DMS系统在微服务应用发布时面临的挑战为切入点,介绍了基于微服务SDK框架与JavaAgent技术的全链路灰度发布,整体方案能够有效提升微服务应用发布的效率。

1、微服务应用在发布时面临的挑战

微服务架构因其小而独立的特点受到广大开发者欢迎,我们生活中很多常见的应用也是基于微服务的架构。微服务架构强调应用拆分为一系列责任单一的小型服务单元,各个服务单元可进行独立部署,相互协作配合,这种架构模式极大的提高了IT团队的开发效率。

微服务应用在发布的时候一般会采用一种叫灰度发布的策略,它将新版本的软件逐步地推送给一小部分用户,以便在全面发布之前测试和验证新版本的稳定性和可靠性。这种发布策略可以减少潜在的风险和影响,因为只有一小部分用户受到影响,而其他用户仍然可以使用旧版本的软件。

常见的灰度发布一般只针对某个单点的服务进行实施,在很多情况下,这种方法可以大大提高应用发布的效率及稳定性。当然,单点灰度发布实际上也存在一些缺点,下面以汽车行业中的DMS系统为例进行分析:

DMS全称为汽车经销商管理系统(Dealer Management System),专门为汽车经销商和售后服务提供商设计的软件系统,帮助汽车经销商实现业务数字化、自动化和智能化,提高业务效率、降低成本、提升服务质量。很多厂商的DMS系统都做过微服务化改造,在提高了团队开发效率的同时,也遇到了一些业务发布场景的挑战:

  • 经销商想在某一个门店A上线自己的新业务,作为业务试点门店,比如新品汽车销售,或者打折促销活动等。和新业务相关的流量只会流入试点门店B。

  • 为了节约成本以及降低部署服务工作量,希望能够实现逻辑上的环境隔离。例如,测试环境有部分服务复用生产环境上的模块,开发测试人员只需要聚焦于需要测试的服务模块。

  • 经销商的交易、商品服务有新的业务要上线,新上线的功能间有依赖和交互,要在上线前做一些测试工作。

  1. 计划让测试人员专门账号来进行现网测试。

  2. 引入少量比例的生产流量进行验证。

针对上述问题,一般的思路是通过灰度发布去解决,通过灰度发布,可以引入部分的测试流量到新业务模块,也能控制带有具体特征的流量只流入到对应的测试模块,其余流量保持原有方式不动。

但是经过仔细考虑,就会发现如果只做单点灰度发布,其实是无法完善地解决以上场景的痛点问题,主要体现在:

业务特征时常只在第一跳,也就是特征只在入口,传递过程中会丢失。

除了第一跳入口,后续微服务之间进行调用的时候也会把特征给丢失。

因此,仅仅依靠单点灰度发布的能力是不够的,还需要能够做到整条微服务调用链的可灰度,也就是全链路灰度的能力,这样就可以灵活解决类似DMS系统遇到的问题。

后续我们将以全链路灰度发布的场景来展示微服务SDK框架和JavaAgent如何相互结合,解决真实场景中的服务发布问题。

2、微服务治理方案的选择

在提出具体解决方案之前,我们可以先了解基于微服务SDK框架以及基于JavaAgent技术的治理模式。微服务SDK进行治理是常见的一种形态,我们常见的Spring Cloud、Dubbo都属于微服务SDK架构,这种方式通常可以较为方便的通过外部依赖的方式集成各种服务治理功能。JavaAgent则可以通过非侵入的方式引入微服务治理功能,下面将对这两种模式进行解析。

2.1 基于微服务SDK框架的微服务治理

2.1.1 原理和优势

服务治理是一个宽泛的概念,通常来说,保证微服务可靠运行的策略,都可以称为服务治理。这些策略涵盖开发态、运行态和运维态等微服务生命周期。可以从两个不同的角度进行描述服务治理:

  • 从管理流程上,可以分为进行业务定义和设置治理规则两个步骤。系统架构师将请求流量根据特征打上标记,用于区分一个或者一组代表具体含义的业务,然后对这些业务设置治理规则。

  • 从处理过程上,可以分为下发配置和应用治理规则两个步骤。可以通过配置文件、配置中心、环境变量等常见的配置管理手段下发配置。微服务SDK框架负责读取配置,解析治理规则,实现治理效果。

  • 由于微服务应用是基于微服务SDK框架开发的,开发者可以明显可以感知微服务SDK框架的存在,此外SDK和应用实例是运行在同一个进程中,这些特点也给微服务的治理带来了一些好处,包括但不限于以下:

  • 更轻量级:微服务SDK框架在运行时不需要起单独的进程,因此资源开销较小。

  • 治理粒度更精细:微服务SDK可以直接对应用实例的某个方法直接进行管理,因此能够满足各种治理场景要求。

  • 性能高,时延低:微服务实例之间的链路不存在代理,访问的时候是直接点对点的调用,因此时延低,并且微服务SDK框架可以提供高性能的RPC,保障数据的高效传输。

华为云也致力于为开发者提供全面开放,方便高效的微服务SDK框架,目前对外开源的稳定成熟框架主要有Spring Cloud Huawei和Java Chassis。

2.1.2 Spring Cloud Huawei框架

自从Netflix开源出最早的Spring Cloud微服务SDK框架,Spring Cloud在这个领域的发展非常迅速,目前Spring Cloud已经是业界广泛使用的微服务SDK框架之一。为了让开发者能够更加方便、高效地使用Spring Cloud开发可靠的微服务应用,基于Spring Cloud和华为云服务生态体系,华为云提供了Spring Cloud Huawei微服务SDK框架,为开发者提供了一站式的开发、部署、运维、监控、治理等全生命周期的服务。

使用Spring Cloud Huawei,开发者可以不用熟悉和了解Spring Cloud,只需要熟悉Spring和Spring Boot,就能够按照微服务架构模式开发应用。相对于Spring Cloud,Spring Cloud Huawei能够更好的支持快速微服务开发,提供开箱即用的微服务治理能力。

2.1.3 Java Chassis框架

Java Chassis框架是Apache ServiceComb项目下面向Java语言的微服务框架。ServiceComb项目最早源于华为微服务引擎(CSE),在2017年12月捐赠给Apache基金会,目前已经形成了庞大的微服务生态,而Java Chassis是其中重要一环,着重解决微服务面临的如下问题:

  • 微服务通信性能

  • 微服务运维和治理

  • 遗留系统改造

  • 配套DevOps

Java Chassis框架包含服务契约、编程模型、运行模型与通信模型四个部分,具备负载均衡、容错熔断、限流降级、调用链追踪等全面微服务治理能力。下面是总体的架构设计图:

为了支持软件工程实践, Java Chassis 的运行时架构是一个哑铃结构, 两端分别是“编程模型”和“通信模型”,中间是“运行模型”。“编程模型”面向开发者写服务接口的习惯,“通信模型”面向微服务之间的高效编码和通信,“运行模型”基于“契约”,提供一种服务无关的插拔机制,能够让开发者独立于业务实现开发治理功能,并且灵活的移除和增加功能,以及调整这些治理功能的处理顺序。“运行模型”的核心抽象接口是 Handler ,这个接口是一个异步的定义,Java Chassis 运行时模型采用纯异步的实现,让整个系统运行非常高效。

Java Chassis和Spring Cloud都实现了微服务架构模式,相比而言,Java Chassis 是一个更加紧凑的实现,开箱即用,而 Spring Cloud则是相对松散的实现,整合了大量的Netflix组件。

2.2 基于JavaAgent的非侵入微服务治理

JavaAgent是如何实现能服务治理能力的?其技术核心在于Java进程启动时,可以挂载JavaAgent来执行字节码的增强逻辑。JVM启动后,JavaAgent运行于Java应用之前,可以修改原应用的目标类和方法的字节码,做到非侵入地增强,原应用中被增强的类在JVM中实例化的对象都是已经被JavaAgent处理过的,因此在业务应用的代码执行时,我们的服务治理逻辑就能悄无声息地通过这种方法实现注入。

例如,我们可以增强服务发现过程,对这一过程中的某个关键函数进行拦截增强,插入一段路由筛选服务提供者实例的逻辑,根据服务实例的元数据和下发的路由规则选定对应的服务提供者,以此来完成灰度发布的功能。下图简化地介绍了字节码JavaAgent字节码增强的核心原理:

类似地,其他的服务治理能力的开发都可以通过这种方式,用户的Java业务应用无需修改,业务应用的开发者也无需理解其中的深层原理,只要把实现了服务治理功能的JavaAgent挂载上即可一键非侵入接入服务治理。

基于上述原理,华为云开源团队开发了无代理服务网格Sermant项目,目前已经在Github开源。Sermant专注于利用Java字节码增强技术为宿主应用程序提供服务治理功能,以解决大规模微服务场景中的服务治理问题。Sermant构建了非侵入、高性能、插件化核心框架,并在框架中提供了对接动态配置中心、事件上报、链路标记、心跳等服务治理通用能力。

当前开源仓库中提供的插件涵盖了服务注册、标签路由、流量控制、服务监控等常见的服务治理场景。Sermant的后端观测、动态配置中心、Agent等组件提供的一套完整的解决方案如下:

利用Sermant来进行微服务治理能够集合上文提到的各种服务架构的优势:

  • 高性能:对比传统的服务治理边车,由于没有边车进程,因此有更高的性能和更低的资源损耗。

  • 非侵入:对比微服务 SDK 架构架构,无需代码修改,升级云原生架构无需代码改造。

  • 插件化架构:服务治理功能作为插件嵌入到JavaAgent内部,可动态部署,且内部充分类隔离,和业务代码无类冲突。

3、方案详解

结合以上两种治理模式的各自特点,便可设计出理想的全链路灰度发布方案。目前要实现全链路灰度,一般要考虑这些问题的处理:

  • 在第一跳的地方(一般是网关),我们需要能选中各种类型的流量,把这部分流量染色,再路由到正确的目标。

  • 除了第一跳,剩下调用链路中的各个微服务能够识别染色标,透传染色标,并路由到正确的目标。

  • 能对异常情况进行妥善处理。

3.1 全链路灰度发布方案的具体流程

针对以上问题,我们有一套相对完善的全链路灰度发布方案,整体方案如下:

  • 在前端部分,请求会统一携带流量标签参数发到网关上面。

  • 网关会选中各种类型的流量,将这些流量根据需求分别染色,比如通过请求header进行标记染色。

  • 网关会将染色后的流量转发到带有不同tag的后端微服务实例,tag可以由应用发布流水线注入到相应发布的微服务实例当中。

  • 借助微服务实例上运行的SDK/JavaAgent,接收到应用网关流量的微服务实例会通过SDK/JavaAgent提供的标签路由能力将流量特征保留并转发到合适的下一跳微服务实例。

  • 对于后续链路上的微服务实例,都可以通过微服务实例上面的SDK/Agent进行特征的传递。

3.2 SDK/JavaAgent如何助力全链路灰度发布?

由于在每个微服务实例都运行着SDK/JavaAgent,因此SDK/JavaAgent可以对每个实例进行细粒度的服务治理,包括限流,熔断,降级,标签路由等功能。在全链路灰度发布过程中,对于每条链路上的微服务实例,可以借助SDK/JavaAgent的标签路由能力实现流量特征的保留以及传递到下一跳微服务实例。进行标签路由的全流程如下:

3.3 SDK/JavaAgent如何搭配使用?

从上面的方案介绍可以知道无论是SDK还是JavaAgent的方式,其实都可以非常有效地助力全链路灰度发布方案的落地,那遇到具体业务场景对全链路灰度发布有诉求的时候,我们该如何去选择呢?

对于用户规划的新业务,用户一般会统一技术栈,因此直接使用微服务SDK框架自带的能力去实现全链路灰度发布会更加方便高效。

对于用户已有的业务,并且业务内部技术栈也不统一,这时候直接采用非侵入式的JavaAgent去做全链路灰度发布会极大程度地降低改造成本,因为业务代码本身不需要做变动,只需要在运行实例上挂载一个JavaAgent即可。当然目前的JavaAgent其实是基于Java 语言的,因此对于别的编程语言,还是得依靠微服务SDK框架来实现全链路灰度发布,但是考虑到目前Java属于使用量第一的编程语言,因此JavaAgent这种方式基本上还是能够覆盖绝大多数的场景。

总的来看,两种方式其实适用于不同的业务场景,它们之间可以相互补充,形成一整套完善的全链路灰度发布解决方案。

3.4 全链路灰度发布方案带来的优势

微服务应用通过全链路灰度发布的方式可以显著提高发布的效率以及稳定性,关键优势如下:

  • 在开发测试过程中,客户可以根据需求在逻辑上划分出一套属于自己的服务链路,只需要关注自己设定的特征流量即可,这种模式可以为客户省去搭建系统中一些共用的模块时间以及节约环境资源。

  • 在发布过程中,方便地将带有试点特征的流量引入到含有自己试点应用的链路环境当中。客户还可以根据需要把一部分生产流量引入到自己的新版本业务链路环境当中,完成新版本的验证。

4、总结

微服务治理架构的形态一直在演进,各种形态有其适用的场景和优缺点。对于企业用户和开发者来说,如何尽可能以较低的成本、较好的效率来解决微服务治理过程中的各个问题是永恒的目标。

以上我们主要对主流的微服务SDK架构和JavaAgent非侵入治理架构作了解析,对于Java应用场景来说,这两种治理模式可以在不影响性能的前提解决绝大多数场景的治理问题。

华为云在微服务治理方向的持续探索也孵化出了Spring Cloud Huawei、Java Chassis、Sermant等优秀的开源项目,并且将持续演进,丰富微服务治理领域的开源生态。


华为开发者空间,汇聚鸿蒙、昇腾、鲲鹏、GaussDB、欧拉等各项根技术的开发资源及工具,致力于为每位开发者提供一台云主机、一套开发工具及云上存储空间,让开发者基于华为根生态创新。
点击链接
,免费领取您的专属云主机

点击关注,第一时间了解华为云新鲜技术~

当集群中需要升级 Mount Pod 时,目前推荐的方式是更新配置后重新挂载应用 Pod 进行滚动升级,但这种升级方式的问题在于需要业务重启。

如果对业务的使用模式很清楚时,比如没有数据写入等,也可以选择手动重建 Mount Pod 的方式。在更新配置后,手动删除已有的 Mount Pod,并等待其重建,同时依赖 CSI 对挂载点的自动恢复功能,等待应用 Pod 中挂载点的恢复。这种升级的过程有几个问题:

  1. 操作繁琐
    :需要用户自己使用 kubectl 命令,通过应用 Pod 找到对应的 Mount Pod;
  2. 升级时业务中断
    :CSI 需要先等待旧的 Mount Pod 完全被删除,才会去创建新的 Mount Pod。这就导致了在旧的 Mount Pod 删除后、新的 Mount Pod 起来前,业务处于不可用状态;
  3. 挂载点自动恢复对原 I/O 操作的影响
    :由于 CSI 使用了重新 bind 应用挂载点的方式实现了自动恢复,所以即使在恢复后,原先的 I/O 操作都会报错。

为了解决现有的升级过程遇到的问题,JuiceFS CSI Driver 在 v0.25.0 版本中,实现了 Mount Pod 的平滑升级,即在应用不停服的情况下升级 Mount Pod。

相比有损升级的方式,使用平滑升级的好处在于:

  1. 操作简单:在 CSI Dashboard 中,可以轻易的在应用 Pod 的页面找到 Mount Pod,且点击按钮即可触发平滑升级;
  2. 业务不中断:升级过程中,业务不会受到影响,用户也可以使用平滑升级功能来进行 Mount Pod 的参数和配置修改。

01 CSI 如何实现 Mount Pod 的平滑升级

目前 JuiceFS CSI 支持两种平滑升级方式,即二进制升级和 Pod 重建升级。

二进制升级

二进制升级不会重建 Mount Pod,而是升级 Mount Pod 中的客户端二进制。其依赖 JuiceFS 客户端自身的守护进程,即社区版版本在 v1.2.0 以上,商业版版本在 v5.0.0 以上。

整个二进制升级的过程如下:

  1. 在触发平滑升级后,CSI Node 启动一个使用新镜像的 Job,在 Job 中将 JuiceFS 客户端二进制 copy 到 Mount Pod 中;
  2. 等 Job 完成后,CSI Node 向 Mount Pod 发送 SIGHUP 信号;
  3. Mount Pod 接收到 SIGHUP 信号后,将目前的 I/O 请求状态信息保存到临时文件中,此时服务进程退出;
  4. Mount Pod 的服务进程退出后,守护进程会用新的二进制再次启动新的服务进程;
  5. 新的服务进程启动后读取中间状态文件,并继续处理之前的请求;
  6. 新的服务进程向守护进程拿当前的 FUSE fd。

二进制升级使用于仅需要升级客户端的情况。但升级后查看 Pod 的 yaml,其镜像依然是旧的。由于没有重建 Pod,这种升级的好处在于速度快且风险小,缺点在于不能更新 Mount Pod 的其他配置。

Pod 重建升级

Pod 重建升级指的是重建 Mount Pod 进行平滑升级。这种升级方式依赖 JuiceFS 本身的平滑升级功能,即社区版版本在 v1.2.1 以上,商业版版本在 v5.1.0 以上。

整个 Pod 重建升级的过程如下:

  1. 每个 Mount Pod 在启动后,都会将自身使用的 FUSE fd 传给 CSI Node,CSI Node 维护了每个 Mount Pod 使用的 FUSE fd 的关系表;
  2. 在触发了平滑升级后,CSI Node 会先起一个与新 Mount Pod 相同配置的空 Job,这一步的作用是提前将新镜像 pull 在节点上,从而节省升级时间;
  3. 等 Job 完成后,CSI Node 向旧的 Mount Pod 发送 SIGHUP 信号;
  4. 旧的 Mount Pod 接收到 SIGHUP 信号后,将目前的 I/O 状态信息保存到临时文件中,并退出。此时 Mount Pod 变为 Complete 状态;
  5. CSI Node 根据 ConfigMap 中的配置,创建新的 Mount Pod;
  6. 新的 Mount Pod 起来后,读取临时文件中的中间状态信息,从而继续处理之前的请求;
  7. 新的 Mount Pod 向 CSI Node 拿当前的 FUSE fd。

其中 Mount Pod 和 CSI Node 之间通过
Unix domain socket
来传递文件句柄。当某个 FUSE 请求未能在升级期间完成,会被强制中断,建议在负载比较低的时候进行升级操作。

可以看到整个平滑升级的过程,与
宿主机上客户端的平滑升级
类似,唯一的区别在于由 CSI Node 向旧的服务进程发送 SIGHUP 信号以及新的服务进程启动后向 CSI Node 拿 FUSE fd。这是因为 Mount Pod 在重建后,其中的守护进程无法向旧的守护进程发送 SIGHUP 信号以及无法通过 Unix domain socket 传递文件句柄,所以在 K8s 环境中这些工作就交由 CSI Node 来完成。

这种升级方式由于重建了 Pod,缺点在于如果集群环境比较复杂,重建 Pod 的过程中出错的风险比较大;其优点在于可以更新 Mount Pod 的其他配置,且查看 Pod 的 yaml,其镜像是新的。

02 如何触发平滑升级

目前平滑升级可以在 CSI Dashboard 或者 kubectl 插件中触发。

Dashboard 中触发

首先在 CSI Dashboard 中,点击「配置」按钮,更新 Mount Pod 需要升级的新镜像版本。

其次,在 Mount Pod 的详情页,有两个升级按钮,分别是「Pod 重建升级」和「二进制升级」。

  • Pod 重建升级:Mount Pod 会重建,可用于更新镜像、调整挂载参数、调整 pod 资源等。Mount Pod 的最低版本要求为 1.2.1(社区版)或 5.1.0(企业版);
  • 二进制升级:Mount Pod 不重建,只升级其中的二进制,不可变更其他配置,且升级完成后 Pod yaml 中所看到的依然是原来的镜像。Mount Pod 的最低版本要求为 1.2.0(社区版)或 5.0.0(企业版)。

点击升级按钮,即可触发 Mount Pod 的平滑升级。

触发后可以看到整个过程,完成后页面会自动跳转到新的 Mount Pod 的详情页。

kubectl 插件中触发

使用 kubectl 在 CSI ConfigMap 配置中更新 Mount Pod 所需要升级的镜像版本。

apiVersion: v1
data:
   config.yaml: |
      mountPodPatch:
         - ceMountImage: juicedata/mount:ce-v1.2.0
           eeMountImage: juicedata/mount:ee-5.1.1-ca439c2
kind: ConfigMap

使用 JuiceFS kubectl plugin 触发 Mount Pod 的平滑升级。

# Pod 重建升级
kubectl jfs upgrade juicefs-kube-node-1-pvc-52382ebb-f22a-4b7d-a2c6-1aa5ac3b26af-ebngyg --recreate
# 二进制升级
kubectl jfs upgrade juicefs-kube-node-1-pvc-52382ebb-f22a-4b7d-a2c6-1aa5ac3b26af-ebngyg

03 总结

鉴于目前的有损升级方案存在诸多缺陷,JuiceFS CSI Driver 在 v0.25.0 版本中,支持了 Mount Pod 的平滑升级。CSI 提供了两种平滑升级方案,包括二进制升级和 Pod 重建升级。二进制升级风险小,但不支持更新 Mount Pod 的其他配置;Pod 重建升级在集群环境较复杂的情况下有失败的风险,但支持更新 Mount Pod 的其他配置,比如可以根据 Mount Pod 的实际资源使用情况,动态调整资源配比等。用户可以根据需要,选择更适合的升级方式,同时建议在负载比较低的时候进行升级操作。

本人之前对C#开发非常喜欢,也从事开发C#开发桌面开发、Web后端、Vue前端应用开发多年,最近一直在研究使用Python,希望能够把C#的一些好的设计模式、开发便利经验引入到Python开发中,很多时候类似的开发方式,可以极大提高我们开发的效率,本篇随笔对wxpython控件实现类似C#扩展函数处理的探究总结。

1、C#扩展函数特点及便利性回顾

C# 的扩展方法具有以下几个特点和便利性:

  1. 语法简洁
    :扩展方法允许在不修改原始类型的情况下,向现有类型添加新功能。调用时看起来像是实例方法。

  2. 易于使用
    :可以在调用扩展方法时使用点语法,这使得代码更易读且更自然。例如,对于字符串类型,可以直接调用扩展方法而不是传递实例。

  3. 静态类和静态方法
    :扩展方法必须定义在静态类中,并且本身也是静态方法。第一个参数指定了要扩展的类型,并使用
    this
    关键字修饰。

  4. 提升代码组织性
    :将相关功能组织到扩展方法中,可以减少主类中的代码量,提高可维护性。

  5. 与 LINQ 的结合
    :扩展方法在 LINQ 中的应用非常广泛,使得集合操作更加直观和简洁。

  6. 支持多种数据类型
    :可以为基本类型、集合类型甚至自定义类型添加扩展方法,从而提供更广泛的功能。

总的来说,扩展方法在提高代码可读性和可维护性方面具有明显的优势,是C#语言设计中的一项重要特性。

我在开发C#Winform应用前端的时候,在自己的公用类库上实现了很多扩展方法,特别是对于一些控件,增加了很多如绑定数据列表、绑定字典大类名称后直接加载数据列表等,以及一些对数据类型的通用处理,如字符串的格式判断或者裁剪等等。

如在随笔《
使用扩展函数方式,在Winform界面中快捷的绑定树形列表TreeList控件和TreeListLookUpEdit控件
》中介绍过,对于一些常规控件的数据绑定处理。

对于常规的列表绑定,我们可以用简单的一个扩展函数实现,如下所示。

    //常规类别绑定
    this.txtProjectList4.BindDictItems(list, "Text", "Value", true, columns.ToArray());

定义了扩展方法,就很容易实现数据的绑定,减少涉及控件处理的细节。

那么对于Python如何使用类似的方式实现呢,我们需要对Python 的语言特性进行了解和实际测试下。

2、基于Python实现数据的快速绑定

我们让ChatGPT来进行解答,它给出的代码答案是如下所示。

1)属性绑定方式

importwx#定义扩展方法
defBindDictItems(self, items_dict):
self.Clear()
#清空现有项 for key, value initems_dict.items():
self.Append(value, key)
#添加项,key 为用户数据,value 为显示内容 #将扩展方法绑定到 wx.ComboBox 类 setattr(wx.ComboBox, "BindDictItems", BindDictItems)#测试应用 classMyApp(wx.App):defOnInit(self):
frame
= wx.Frame(None, title="ComboBox BindDictItems Example", size=(300, 200))
panel
=wx.Panel(frame)

combo_box
= wx.ComboBox(panel, style=wx.CB_DROPDOWN, pos=(10, 10))#使用扩展方法绑定字典项 items ={"item1": "Item One","item2": "Item Two","item3": "Item Three"}
combo_box.BindDictItems(items)

frame.Show()
returnTrueif __name__ == "__main__":
app
=MyApp()
app.MainLoop()

解释

  1. 定义
    BindDictItems
    方法

    :这个方法接受一个字典,将字典中的项绑定到
    ComboBox
    中。
    • 使用
      self.Clear()
      清空现有项。
    • 遍历字典,并使用
      Append
      方法将项添加到
      ComboBox
  2. 使用
    setattr
    绑定方法

    :通过
    setattr
    将定义的方法添加到
    wx.ComboBox
    类中。
  3. 测试应用
    :在
    MyApp
    中创建一个
    wx.ComboBox
    并调用
    BindDictItems
    方法。

这样,你就可以为
wx.ComboBox
提供一个
BindDictItems
方法。

测试发现,对于功能实现没有问题,但是对于开发的时候,combo_box 没有函数提示,也就是没有强类型的函数提示,这样只是进行了动态绑定,无法在开发的时候,获得IDE的智能提示,很容易写错。

另外也可以使用下面代码实现类似setattr的处理,相当于直接附加方法,同样在使用的时候也是没有函数的智能提示,但是能够正常工作,俗称
猴子补丁

#使用猴子补丁将方法添加到 wx.ComboBox
wx.ComboBox.BindDictItems = bind_dict_items

2)自定义子类方式

对该结果不满意,继续追问实现该功能的其他途径,给出的另一个解决方案是继承父类,在子类实现更多的自定义函数。

为了实现更好的智能提示和类型检查,可以通过使用类型注解和创建一个自定义子类来扩展
wx.ComboBox
。这样可以确保 IDE 提供正确的代码补全和提示。

给出的代码如下所示。

importwxfrom typing importDictclassMyComboBox(wx.ComboBox):def BindDictItems(self, items_dict: Dict[str, str]) ->None:"""Bind a dictionary of items to the ComboBox."""self.Clear()#清空现有项
        for key, value initems_dict.items():
self.Append(value, key)
#添加项,key 为用户数据,value 为显示内容 classMyApp(wx.App):defOnInit(self):
frame
= wx.Frame(None, title="ComboBox BindDictItems Example", size=(300, 200))
panel
=wx.Panel(frame)

combo_box
= MyComboBox(panel, style=wx.CB_DROPDOWN, pos=(10, 10))#使用扩展方法绑定字典项 items ={"item1": "Item One","item2": "Item Two","item3": "Item Three"}
combo_box.BindDictItems(items)

frame.Show()
returnTrueif __name__ == "__main__":
app
=MyApp()
app.MainLoop()

通过这种方式,可以确保在使用
BindDictItems
时能够获得智能提示,从而减少错误。

这样的方式,确实有强类型的函数提示了。虽然可以获得智能提示,但是也就是破坏了界面类的代码,也就是需要修改相关的使用代码,而非C#扩展方法那样,隐式的扩展了。

3)通过中介辅助类的方式实现

有些情况下,不适合继承关系,不可能为每个类都提供一个子类来封装,有时候提供一些辅助类可能更具有性价比。

要在不继承父类的情况下实现类似 C# 的扩展方法,并确保获得智能提示,推荐使用类型注解和一个中介类来包装扩展方法。通过这种方式,IDE 可以识别这些扩展方法并提供智能提示。

创建一个名为
ComboBoxExtensions.py
的文件,其中定义扩展方法。

#combo_box_extensions.py
from typing importDictimportwxclassComboBoxExtensions:
@staticmethod
def bind_dict_items(combo_box: wx.ComboBox, items_dict: Dict[str, str]) ->None:"""Bind a dictionary of items to the ComboBox."""combo_box.Clear()#清空现有项 for key, value initems_dict.items():
combo_box.Append(value, key)
#添加项,key 为用户数据,value 为显示内容

在主应用程序中,导入扩展类并使用其方法。

importwxfrom combo_box_extensions import ComboBoxExtensions  #导入扩展类

classMyApp(wx.App):defOnInit(self):
frame
= wx.Frame(None, title="ComboBox Extensions Example", size=(300, 200))
panel
=wx.Panel(frame)

combo_box
= wx.ComboBox(panel, style=wx.CB_DROPDOWN, pos=(10, 10))#使用扩展方法绑定字典项 items ={"item1": "Item One","item2": "Item Two","item3": "Item Three"}
ComboBoxExtensions.bind_dict_items(combo_box, items)
#这里应该有智能提示 frame.Show()returnTrueif __name__ == "__main__":
app
=MyApp()
app.MainLoop()

ComboBoxExtensions
类包含一个静态方法
bind_dict_items
,该方法接受
wx.ComboBox
实例和字典作为参数。

在主应用程序中,调用
ComboBoxExtensions.bind_dict_items(combo_box, items)
,这将获得智能提示。

4)使用协议类型的方式处理,并在使用的时候转换为协议类

为了确保在不继承的情况下实现扩展方法并获得智能提示,最佳方案是结合类型注解和一个特定的函数注册过程。以下是一个经过验证的方式,确保能够在实例上调用扩展方法,同时获得 IDE 的智能提示。


combo_box_extensions.py
中定义扩展函数,并使用
cast
来确保类型正确。

#combo_box_extensions.py
from typing importDict, Protocolimportwxfrom typing importcastclassComboBoxWithBindDictItems(Protocol):def BindDictItems(self, items_dict: Dict[str, str]) ->None:
...
def bind_dict_items(self: wx.ComboBox, items_dict: Dict[str, str]) ->None:"""Bind a dictionary of items to the ComboBox."""self.Clear()#清空现有项 for key, value initems_dict.items():
self.Append(value, key)
#添加项,key 为用户数据,value 为显示内容 #将扩展方法绑定到 wx.ComboBox wx.ComboBox.BindDictItems = bind_dict_items

在主应用程序中调用扩展方法,并确保正确使用类型注解。

importwxfrom combo_box_extensions import ComboBoxWithBindDictItems  #导入协议类型

classMyApp(wx.App):defOnInit(self):
frame
= wx.Frame(None, title="ComboBox Extensions Example", size=(300, 200))
panel
=wx.Panel(frame)

combo_box
= wx.ComboBox(panel, style=wx.CB_DROPDOWN, pos=(10, 10))#确保类型为 ComboBoxWithBindDictItems,以获得智能提示 cast(ComboBoxWithBindDictItems, combo_box).BindDictItems({"item1": "Item One","item2": "Item Two","item3": "Item Three"})#这里应该有智能提示 frame.Show()returnTrueif __name__ == "__main__":
app
=MyApp()
app.MainLoop()

可以看到,通过cast的方式转换后,具有函数代码的智能提示了。

协议类型
:定义
ComboBoxWithBindDictItems
,它确保
BindDictItems
方法存在。

使用
cast

:在调用
BindDictItems
方法时,使用
cast
来明确指定
combo_box
的类型为
ComboBoxWithBindDictItems
,这样 IDE 能够识别并提供智能提示。

智能提示
:通过类型注解和
cast
,IDE 能够识别扩展方法并提供智能提示。

无继承
:避免了复杂的继承结构,同时实现了功能扩展。

以上几种在Python开发中,对于实现C#扩展函数方法的实现,不过总体来说,虽然能够实现类似的方式,却没有C#那种简洁明了,不知道以后Python发展后解决或者是我没有研究透彻的原因,很多时候如果要实现自定义函数的处理方式,估计我只能结合子类继承和辅助类的方式一起解决这个问题。