2024年4月

1. 项目介绍

NVIDIA® TensorRT™ 是一款用于高性能深度学习推理的 SDK,包括深度学习推理优化器和运行时,可为推理应用程序提供低延迟和高吞吐量。基于 NVIDIA TensorRT 的应用程序在推理过程中的执行速度比纯 CPU 平台快 36 倍,使您能够优化在所有主要框架上训练的神经网络模型,以高精度校准低精度,并部署到超大规模数据中心、嵌入式平台或汽车产品平台。

TensorRT 基于 NVIDIA CUDA® 并行编程模型构建,使您能够在 NVIDIA GPU 上使用量化、层和张量融合、内核调整等技术来优化推理。TensorRT 提供 INT8 使用量化感知训练和训练后量化和浮点 16 (FP16) 优化,用于部署深度学习推理应用程序,例如视频流、推荐、欺诈检测和自然语言处理。低精度推理可显著降低延迟,这是许多实时服务以及自主和嵌入式应用所必需的。TensorRT 与 PyTorch 和 TensorFlow 集成,因此只需一行代码即可实现 6 倍的推理速度。TensorRT 提供了一个 ONNX 解析器,因此您可以轻松地将 ONNX 模型从常用框架导入 TensorRT。它还与 ONNX 运行时集成,提供了一种以 ONNX 格式实现高性能推理的简单方法。

基于这些优势,TensorRT目前在深度模型部署应用越来越广泛。但是TensorRT目前只提供了C++与Python接口,对于跨语言使用十分不便。目前C#语言已经成为当前编程语言排行榜上前五的语言,也被广泛应用工业软件开发中。为了能够实现在C#中调用TensorRT部署深度学习模型,我们在之前的开发中开发了TensorRT C# API。虽然实现了该接口,但由于数据传输存在问题,当时开发的版本在应用时存在较大的问题。

基于此,我们开发了TensorRT C# API 2.0版本,该版本在开发时充分考虑了上一版本应用时出现的问题,并进行了改进。为了更加方便开发者使用,在本次更新中增加了对动态输入模型的支持,将在本技术文中详细介绍本次更新内容以及应用案例。

  • TensorRT C# API 项目源码:
https://github.com/guojin-yan/TensorRT-CSharp-API.git
  • TensorRT C# API 项目应用源码:
https://github.com/guojin-yan/TensorRT-CSharp-API-Samples.git

2. 更新回顾

由于该项目目前还没有完全开发完成,为了更好的方便大家使用,因此会在最新更新后提供给大家最新的资讯。如果大家在使用时有任何疑问,可以阅读之前发布的技术博客:

3. 动态输入模型支持

在上一版本中,支持了多Bath推理,单其实现方式是导出的推理模型是多Bath的,因此模型推理的Bath是不可更改的。但是目前TensorRT已经支持了动态模型输入,所以更新了对动态输入模型的支持。下面将对更新的API接口以及推理流程进行简单的介绍:

3.1 新增API

  • public static void OnnxToEngine(string modelPath, int memorySize, string nodeName, Dims minShapes, Dims optShapes, Dims maxShapes)


    • 模型转换接口:
      可以调用封装的TensorRT中的ONNX 解释器,对ONNX模型进行转换,并根据本机设备信息,编译本地模型,将模型转换为TensorRT 支持的engine格式,该接口支持动态输入模型。
    • string modelPath:
      本地ONNX模型地址,只支持ONNX格式,且ONNX模型必须为确定的输入输出,暂不支持动态输入。
    • int memorySize:
      模型转换时分配的内存大小。
    • string nodeName:
      模型输入节点名称,该节点维度确定但是形状是动态的,一般为: [-1, 3 640, 640],某一维度或其中几个维度大小为“-1”。
    • Dims minShapes:
      动态尺寸的最小允许值
    • Dims optShapes:
      优化(内核选择)中使用的值、动态尺寸的最优值
    • Dims maxShapes:
      动态尺寸等的最大允许值
  • public Nvinfer(string modelPath, int maxBatahSize)


    • Nvinfer 初始化接口:
      初始化Nvinfer类,主要初始化封装的推理引擎,该推理引擎中封装了比较重要的一些类和指针。

    • string modelPath:
      engine模型路径。

    • int maxBatahSize:
      推理推理支持的最大的Bath。

  • public void SetBindingDimensions(int index, Dims dims)/SetBindingDimensions(string nodeName, Dims dims)


    • 设置节点维度接口:
      通过端口编号或者端口名称,获取绑定的端口的形状信息。
    • int index:
      绑定端口的编号。
    • string nodeName:
      绑定端口的名称。
    • Dims dims:
      需要设置绑定端口的维度。

3.2 推理流程

对于固定输入模型的推理流程,主要包括以下四个步骤:

  • Nvinfer初始化
  • 加载推理数据
  • 模型推理
  • 获取推理结果

而当我们使用动态输入模型时,其推理流程发生了变化,如下图所示:

当部署动态输入模型时,推理流程为:

  • Nvinfer初始化
  • 设置本次推理模型输入大小
  • 加载推理数据
  • 模型推理
  • 获取推理结果

与常规的规定输入模型的推理流程相比,主要是增加了
设置本次推理模型输入大小
这一步,其他步骤并未发生较大的变化。此外,如果下一次推理输入数据形状大小发生了改变,就需要重新进行设置,如果输入形状大小并未对发生变化,则无需进行再次设置。

4. 接口应用

关于该项目的调用方式在上一篇文章中已经进行了详细介绍,具体使用可以参考
《最新发布!TensorRT C# API :基于C#与TensorRT部署深度学习模型》
,下面结合Yolov8-cls模型详细介绍一下更新的接口使用方法。

4.1 创建并配置C#项目

首先创建一个简单的C#项目,然后添加项目配置。

首先是添加TensorRT C# API 项目引用,如下图所示,添加上文中C#项目生成的dll文件即可。

接下来添加OpenCvSharp,此处通过NuGet Package安装即可,此处主要安装以下两个程序包即可:

配置好项目后,项目的配置文件如下所示:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
    <RootNamespace>TensorRT_CSharp_API_demo</RootNamespace>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="OpenCvSharp4.Extensions" Version="4.9.0.20240103" />
    <PackageReference Include="OpenCvSharp4.Windows" Version="4.9.0.20240103" />
  </ItemGroup>

  <ItemGroup>
    <Reference Include="TensorRtSharp">
      <HintPath>E:\GitSpace\TensorRT-CSharp-API\src\TensorRtSharp\bin\Release\net6.0\TensorRtSharp.dll</HintPath>
    </Reference>
  </ItemGroup>

</Project>

4.2 添加推理代码

此处演示一个简单的图像分类项目,以Yolov8-cls项目为例:

(1) 转换engine模型

动态输入的模型在进行格式转换时,需要指定模型推理形状至此的范围,
minShapes
表示模型推理支持的最小形状,
optShapes
表示模型推理支持的最佳形状,
maxShapes
表示模型推理支持的最大形状,模型转换需要消耗较多时间,最终转换成功后会在模型同级目录下生成相同名字的
.engine
文件。

Dims minShapes = new Dims(1, 3, 224, 224);
Dims optShapes = new Dims(10, 3, 224, 224);
Dims maxShapes = new Dims(20, 3, 224, 224);
Nvinfer.OnnxToEngine(onnxPath, 20, "images", minShapes, optShapes, maxShapes);

(2) 定义模型预测方法

下面代码是定义的Yolov8-cls模型的预测方法,该方法支持动态Bath输入模型推理,可以根据用户输入图片数量,自动设置输入Bath,然后进行推理。

下面代码与上一篇文章中的代码差异主要是增加了
predictor.SetBindingDimensions("images", new Dims(batchNum, 3, 224, 224));
这一句代码。同时在初始化时,设置最大支持20Bath,这与上文模型转换时设置的一致。

public class Yolov8Cls
{
    public Dims InputDims;
    public int BatchNum;
    private Nvinfer predictor;
    public Yolov8Cls(string enginePath)
    {
        predictor = new Nvinfer(enginePath, 20);
        InputDims = predictor.GetBindingDimensions("images");
    }
    public void Predict(List<Mat> images)
    {
        BatchNum = images.Count;
        for (int begImgNo = 0; begImgNo < images.Count; begImgNo += BatchNum)
        {
            DateTime start = DateTime.Now;
            int endImgNo = Math.Min(images.Count, begImgNo + BatchNum);
            int batchNum = endImgNo - begImgNo;
            List<Mat> normImgBatch = new List<Mat>();
            int imageLen = 3 * 224 * 224;
            float[] inputData = new float[BatchNum * imageLen];
            for (int ino = begImgNo; ino < endImgNo; ino++)
            {
                Mat input_mat = CvDnn.BlobFromImage(images[ino], 1.0 / 255.0, new OpenCvSharp.Size(224, 224), 0, true, false);
                float[] data = new float[imageLen];
                Marshal.Copy(input_mat.Ptr(0), data, 0, imageLen);
                Array.Copy(data, 0, inputData, ino * imageLen, imageLen);
            }
            predictor.SetBindingDimensions("images", new Dims(batchNum, 3, 224, 224));
            predictor.LoadInferenceData("images", inputData);
            DateTime end = DateTime.Now;
            Console.WriteLine("[ INFO ] Input image data processing time: " + (end - start).TotalMilliseconds + " ms.");
            predictor.infer();
            start = DateTime.Now;
            predictor.infer();
            end = DateTime.Now;
            Console.WriteLine("[ INFO ] Model inference time: " + (end - start).TotalMilliseconds + " ms.");
            start = DateTime.Now;

            float[] outputData = predictor.GetInferenceResult("output0");
            for (int i = 0; i < batchNum; ++i)
            {
                Console.WriteLine(string.Format("[ INFO ] Classification Top {0} result : ", 2));
                float[] data = new float[1000];
                Array.Copy(outputData, i * 1000, data, 0, 1000);
                List<int> sortResult = Argsort(new List<float>(data));
                for (int j = 0; j < 2; ++j)
                {
                    string msg = "";
                    msg += ("index: " + sortResult[j] + "\t");
                    msg += ("score: " + data[sortResult[j]] + "\t");
                    Console.WriteLine("[ INFO ] " + msg);
                }
            }
            end = DateTime.Now;
            Console.WriteLine("[ INFO ] Inference result processing time: " + (end - start).TotalMilliseconds + " ms.\n");
        }
    }
    public static List<int> Argsort(List<float> array)
    {
        int arrayLen = array.Count;
        List<float[]> newArray = new List<float[]> { };
        for (int i = 0; i < arrayLen; i++)
        {
            newArray.Add(new float[] { array[i], i });
        }
        newArray.Sort((a, b) => b[0].CompareTo(a[0]));
        List<int> arrayIndex = new List<int>();
        foreach (float[] item in newArray)
        {
            arrayIndex.Add((int)item[1]);
        }
        return arrayIndex;
    }
}

(3) 预测方法调用

下面是上述定义的预测方法,为了测试不同Bath性能,此处读取了多张图片,并分别预测不同张数图片,如下所示:

Yolov8Cls yolov8Cls = new Yolov8Cls("E:\\Model\\yolov8\\yolov8s-cls_b.engine");
Mat image1 = Cv2.ImRead("E:\\ModelData\\image\\demo_4.jpg");
Mat image2 = Cv2.ImRead("E:\\ModelData\\image\\demo_5.jpg");
Mat image3 = Cv2.ImRead("E:\\ModelData\\image\\demo_6.jpg");
Mat image4 = Cv2.ImRead("E:\\ModelData\\image\\demo_7.jpg");
Mat image5 = Cv2.ImRead("E:\\ModelData\\image\\demo_8.jpg");

yolov8Cls.Predict(new List<Mat> { image1, image2 });

yolov8Cls.Predict(new List<Mat> { image1, image2, image3 });

yolov8Cls.Predict(new List<Mat> { image1, image2, image3, image4 });

yolov8Cls.Predict(new List<Mat> { image1, image2, image3, image4, image5 });

4.3 项目演示

配置好项目并编写好代码后,运行该项目,项目输出如下所示:

[ INFO ] Input image data processing time: 5.5277 ms.
[ INFO ] Model inference time: 1.3685 ms.
[ INFO ] Classification Top 2 result :
[ INFO ] index: 386     score: 0.8754883
[ INFO ] index: 385     score: 0.08013916
[ INFO ] Classification Top 2 result :
[ INFO ] index: 293     score: 0.89160156
[ INFO ] index: 276     score: 0.05480957
[ INFO ] Inference result processing time: 3.0823 ms.

[ INFO ] Input image data processing time: 2.7356 ms.
[ INFO ] Model inference time: 1.4435 ms.
[ INFO ] Classification Top 2 result :
[ INFO ] index: 386     score: 0.8754883
[ INFO ] index: 385     score: 0.08013916
[ INFO ] Classification Top 2 result :
[ INFO ] index: 293     score: 0.89160156
[ INFO ] index: 276     score: 0.05480957
[ INFO ] Classification Top 2 result :
[ INFO ] index: 14      score: 0.99853516
[ INFO ] index: 88      score: 0.0006980896
[ INFO ] Inference result processing time: 1.5137 ms.

[ INFO ] Input image data processing time: 3.7277 ms.
[ INFO ] Model inference time: 1.5285 ms.
[ INFO ] Classification Top 2 result :
[ INFO ] index: 386     score: 0.8754883
[ INFO ] index: 385     score: 0.08013916
[ INFO ] Classification Top 2 result :
[ INFO ] index: 293     score: 0.89160156
[ INFO ] index: 276     score: 0.05480957
[ INFO ] Classification Top 2 result :
[ INFO ] index: 14      score: 0.99853516
[ INFO ] index: 88      score: 0.0006980896
[ INFO ] Classification Top 2 result :
[ INFO ] index: 294     score: 0.96533203
[ INFO ] index: 269     score: 0.0124435425
[ INFO ] Inference result processing time: 2.7328 ms.

[ INFO ] Input image data processing time: 4.063 ms.
[ INFO ] Model inference time: 1.6947 ms.
[ INFO ] Classification Top 2 result :
[ INFO ] index: 386     score: 0.8754883
[ INFO ] index: 385     score: 0.08013916
[ INFO ] Classification Top 2 result :
[ INFO ] index: 293     score: 0.89160156
[ INFO ] index: 276     score: 0.05480957
[ INFO ] Classification Top 2 result :
[ INFO ] index: 14      score: 0.99853516
[ INFO ] index: 88      score: 0.0006980896
[ INFO ] Classification Top 2 result :
[ INFO ] index: 294     score: 0.96533203
[ INFO ] index: 269     score: 0.0124435425
[ INFO ] Classification Top 2 result :
[ INFO ] index: 127     score: 0.9008789
[ INFO ] index: 128     score: 0.07745361
[ INFO ] Inference result processing time: 3.5664 ms.

通过上面输出可以看出,不同Bath模型推理时间在1.3685~1.6947ms,大大提升了模型的推理速度。

5. 总结

在本项目中,我们扩展了TensorRT C# API 接口,使其支持动态输入模型。并结合分类模型部署流程向大家展示了TensorRT C# API 的使用方式,方便大家快速上手使用。

为了方便各位开发者使用,此处开发了配套的演示项目,主要是基于Yolov8开发的目标检测、目标分割、人体关键点识别、图像分类以及旋转目标识别,并且支持动态输入模型,用户可以同时推理任意张图像。

  • Yolov8 Det 目标检测项目源码:
https://github.com/guojin-yan/TensorRT-CSharp-API-Samples/blob/master/model_samples/yolov8_custom_dynamic/Yolov8Det.cs
  • Yolov8 Seg 目标分割项目源码:
https://github.com/guojin-yan/TensorRT-CSharp-API-Samples/blob/master/model_samples/yolov8_custom_dynamic/Yolov8Seg.cs
  • Yolov8 Pose 人体关键点识别项目源码:
https://github.com/guojin-yan/TensorRT-CSharp-API-Samples/blob/master/model_samples/yolov8_custom_dynamic/Yolov8Pose.cs
  • Yolov8 Cls 图像分类项目源码:
https://github.com/guojin-yan/TensorRT-CSharp-API-Samples/blob/master/model_samples/yolov8_custom_dynamic/Yolov8Cls.cs
  • Yolov8 Obb 旋转目标识别项目源码:
https://github.com/guojin-yan/TensorRT-CSharp-API-Samples/blob/master/model_samples/yolov8_custom_dynamic/Yolov8Obb.cs

同时对本项目开发的案例进行了时间测试,以下时间只是程序运行一次的时间,测试环境为:

  • CPU:i7-165G7

  • CUDA型号:12.2

  • Cudnn:8.9.3

  • TensorRT:8.6.1.6

Model Batch 数据预处理 (ms) 模型推理 (ms) 结果后处理 (ms)
Yolov8s-Det 1 16.6 4.6 13.1
4 38.0 12.4 32.4
8 70.5 23.0 80.1
Yolov8s-Obb 1 28.7 8.9 17.7
4 81.7 25.9 67.4
8 148.4 44.6 153.0
Yolov8s-Seg 1 15.4 5.4 67.4
4 37.3 15.5 220.6
8 78.7 26.9 433.6
Yolov8s-Pose 1 15.1 5.2 8.7
4 39.2 13.2 14.3
8 67.8 23.1 27.7
Yolov8s-Cls 1 9.9 1.3 1.9
4 14.7 1.5 2.3
8 22.6 2.0 2.9

最后如果各位开发者在使用中有任何问题,欢迎大家与我联系。

前言

一直有小伙伴在微信公众号后台留言让我分享一下.NET MAUI相关的UI框架,今天大姚分享一个.NET MAUI开源、免费的UI工具包:Uranium。

Uranium介绍

Uranium是一个.NET MAUI开源免费的UI工具包。它提供了一组用于构建现代应用程序的控件和实用程序,它构建在.NET MAUI基础架构之上,并提供一组控件和布局来构建现代UI。它还提供了用于在其上构建自定义控件和主题的基础设施。

什么是.NET MAUI?

.NET 多平台应用 UI (.NET MAUI) 是一个跨平台框架,用于使用 C# 和 XAML 创建本机移动和桌面应用。 使用 .NET MAUI,可从单个共享代码库开发可在 Android、iOS、macOS 和 Windows 上运行的应用。

UraniumUI项目源码查看

设置
UraniumApp
为启动项目运行

Windows Machine
运行:

Android Emulator(安卓模拟器)
运行:

安卓模拟器一直卡在不动:

在某些情况下,在“打开或关闭 Windows 功能”对话框中启用 Hyper-V 和 Windows 虚拟机监控程序平台后可能无法正确启用Hyper-V。

我就是开启Hyper-V才把安卓模拟器运行起来的。

假如设置了还是不行可以看看微软官方教程:
https://learn.microsoft.com/zh-cn/dotnet/maui/android/emulator/troubleshooting?view=net-maui-8.0

错误APT2000系统找不到指定的文件:

文件目录中不能包含中文!!!

安卓模拟器系统版本需要高版本:

注意假如安卓模拟器系统版本太低也有可能运行不起来,我选的是最新版!!!

安卓模拟器运行效果:

安卓模拟器运行效果部分截图

Windows运行效果部分截图

项目源码地址



更多项目实用功能和特性欢迎前往项目开源地址查看

echarts图表X轴

在柱状图中,X轴类目名如果数据太长;
echarts会默认进行隐藏部分字段;
如果我们想让每一个类目名都显示出来,需要进行额外的处理

X轴类目名太长时,默认只显示一部分类目名

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>4.9.0</title>
    <script src="https://cdn.staticfile.net/echarts/4.9.0-rc.1/echarts-en.common.js"></script>
</head>

<body>
  <div style="width: 400px;height: 300px;"></div>
  <script>
    let myChart = echarts.init(document.querySelector('div'))
    let colors = ['#4C98FB', '#83CCE7', '#26C7C8', '#73DEBD'];
    let option = {
      xAxis: {
        type: 'category',
        data: ['我是字段1cccccccc', '我是字段2', '我是字段3', '我是字段4', '我是字段5', '我是字段6', '我是字段7']
      },
      yAxis: {
        type: 'value'
      },
      series: [
        {
          data: [120, 200, 150, 80, 70, 110, 130],
          type: 'bar'
        }
      ]
    };
    myChart.setOption(option);
  </script>
</body>
</html>

分析原因

通过上面的现象,我们发现:
展示不出来的原因是水平标签过多导致;
我们如果可以让它倾斜的话,说不定可以全部展示出来;
我们可以使用 xAxis下的 axisLabel中的 rotate属性来解决;
rotate:刻度标签旋转角度;这个值在 【90,-90】的范围类
在类目轴的类目标签显示不下的时候可以通过旋转防止标签之间重叠。

使用倾斜角度让每一个类目名显示出来

xAxis: {
  type: 'category',
  data: ['我是字段1cccccccc2', '我是字段2', '段3', '我是字段4', '我是字段5', '我', '我是字段7'],
  axisLabel: {  
    interval:0,  
    rotate:-20  // 表示倾斜的角度
  }  
},

interval这个属性的简单介绍

interval:坐标轴刻度标签的显示间隔,在类目轴中有效。
默认会采用标签不重叠的策略显示标签。
可以设置成0表示强制显示所有标签。
如果设置为 1,表示『隔一个标签』
可以用数值表示间隔的数据,也可以通过回调函数控制。
回调函数格式如下:
interval:(index:number, value: string) => {
  // index表示该类目名的下标
  // string表示该类目名
  console.log(index,string)
  return 1
},
如果返回的是true,表示显示该类目名;
也就是说:可以返回数字或者布尔值
let option = {
  xAxis: {
    type: 'category',
    axisLabel: {  
      interval:2, // x轴间隔2个类目名
    },
    data: ['我是1', '我是2', '我是3', '我是4', '我是5', '我是6', '我是7']
  },
}

换行\n来处理这个问题

我们通过倾斜可以完全的把这个问题处理了;
可是有些时候,我们ui不想倾斜;那还有其他办法吗?可以换行
换行的话我们有两种方式;
第1种:直接在data中通过\n换行
不推荐第1种这样的方式去做;是因为如果图表还有tooltip的话,会影响。
第2种:在formatter函数中去处理
xAxis: {
  type: 'category',
  data: ['我是\n字段1', '我是\n字段2', '段3', '我是\n字段4', 
    '我是\n字段5', '我是\n字段6', '我是\n字段7'
  ],
},

在data中通过\n换行会在 tooltip 会产生一个空格【不推荐有有副作用】

tooltip: {
  // 使用formatter回调函数自定义显示内容
  formatter: function (params) {
    // params是包含数据信息的对象
    return params.name + ': ' + params.value;
  }
},
xAxis: {
  type: 'category',
  data: ['我是\n字段1', '我是\n字段2', '段3', '我是\n字段4', 
    '我是\n字段5', '我是\n字段6', '我是\n字段7'
  ],
},

通过 axisLabel中的formatter函数来换行 【推荐】没有副作用

我们可以通过xAxis下的axisLabel下的formatter函数来进行换行,
这样做不会改变原始数组,tooltip也不会出现任何问题;
如果像上面那样做;改变原始数据;
 tooltip: {
    // 使用formatter回调函数自定义显示内容
    formatter: function (params) {
      // params是包含数据信息的对象
      return params.name + ': ' + params.value;
    }
  },
 xAxis: {
  interval: 0, 
  type: 'category',
  data: ['我是字段1', '我是字段2', '我是字段3', 
  '我是字段4', '我是字段5', '我是字段6', '我是字段7'],
  axisLabel: {  
    formatter: function (params) {
      console.log('x',params)
      return params.substring(0,2) + '\n' + params.substring(2)
    }
  }
},

超出进行隐藏部分名称,hover显示全部

现在我们要做这样一个效果,
X轴中的类目名默认显示2个字,超出部分隐藏,hover的时候显示全部;
我们需要使用到echarts中的 mouseover 事件;
同时设置xAxis中的triggerEvent为true
xAxis: {
  triggerEvent: true,
  interval: 0, 
  type: 'category',
  ...其他配置项...
}
如果我们不设置triggerEvent: true鼠标移入X轴的类名不会被触发

myChart.on('mouseover', (e) => {
  console.log('鼠标移入X轴的类名不会被触发',e)
})
        

实现的思路

我们需要动态创建一个dom节点,通过createElement来实现
注册鼠标移入事件 myChart.on('mouseover',(e)=>{ })
通过e.event可以拿到offsetX和offsetY
紧接着将这个元素赋值(X轴类的全名称),添加到html页面中
鼠标的移出事件 myChart.on('mouseout',(e)=>{ })
let option = {
  xAxis: {
    // X轴的类目名必须设置这个属性,移入事件才会被触发
    triggerEvent: true,
    interval: 0, 
    type: 'category',
    data: ['我是字段666666666666', '我是字段222222', '我是字段33333', '我是字段4', '我是字段5', '我是字段6', '我是字段7'],
    axisLabel: {  
      formatter: function (params) {
        return params.substring(0,2) + '...'
      }
    }
  },
  ... 其他配置项
}
myChart.setOption(option);

myChart.on('mouseover', (e) => {
  console.log('鼠标移入',e)
  if(e.componentType === "xAxis"){
    // 我们这里先判断一个创建的dom节点是否存在
    let tipNameDom = document.getElementById('tipNameDom')
    console.log(1, tipNameDom)
    // 如果不存在我们创建一个dom节点
    if(!tipNameDom){
      // 创建一个元素
      var createDivElement = document.createElement('div')
      // 给这个元素设置属性
      createDivElement.setAttribute('id', 'tipNameDom')
      // // 设置元素的位置
      createDivElement.style.display = 'block'
      createDivElement.style.position = 'absolute'
      // 获取当前位置
      createDivElement.style.top =  e.event.offsetY + 15 + 'px'
      createDivElement.style.left = e.event.offsetX - 10 + 'px'
      // 这里需要使用 innerHTML,因为我们设置了一样html的属性
      createDivElement.innerHTML = e.value
      // document.querySelector('body').appendChild(createDivElement)
      document.querySelector('body').appendChild(createDivElement)
    }else {
      tipNameDom.style.display = 'block'
      tipNameDom.style.position = 'absolute'
      // 获取当前位置
      tipNameDom.style.top =  e.event.offsetY + 15 + 'px'
      tipNameDom.style.left = e.event.offsetX - 10 + 'px'
      // 这里需要使用 innerHTML,因为我们设置了一样html的属性
      tipNameDom.innerHTML = e.value
    }
  }
})

//  移入事件如果被多次触发,则hover的时候无法显示全部
myChart.on('mouseout', function(params) {
  console.log('移除元素',params )
  if (params.componentType === 'xAxis') {
    let elementDiv = document.querySelector('#tipNameDom')
    console.log('elementDiv', elementDiv)
    elementDiv.style.display = 'none'
  }  
})

是不是这样就OK了?

其实,并不是的;
如果小伙伴们多次移入移出;
偶尔会出现光标明明是移入的状态,但是类目名并没有全部显示出来;
此时已发现了移入事件被多次触发;
怎么解决这个问题呢?
目前的我,并不知道如何去解决。我感觉是echarts的bug;
如果知道的大佬;可以解答一下,万分感激; 


全部代码

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>4.9.0</title>
  <!-- <script src="https://cdn.staticfile.org/echarts/4.3.0/echarts.min.js"></script> -->
  <script src="https://cdn.staticfile.net/echarts/4.9.0-rc.1/echarts-en.common.js"></script>
  <script>
    window.onload =function (){
      let myChart = echarts.init(document.querySelector('.echars'))
      let colors = ['#4C98FB', '#83CCE7', '#26C7C8', '#73DEBD'];
      let option = {
        tooltip: {
          // 使用formatter回调函数自定义显示内容
          formatter: function (params) {
            // params是包含数据信息的对象
            return params.name + ': ' + params.value;
          }
        },
        xAxis: {
          // X轴的类目名必须设置这个属性,移入事件才会被触发
          triggerEvent: true,
          interval: 0, 
          type: 'category',
          data: ['我是字段666666666666', '我是字段222222', '我是字段33333', '我是字段4', '我是字段5', '我是字段6', '我是字段7'],
          axisLabel: {  
            formatter: function (params) {
              return params.substring(0,2) + '...'
            }
          }
        },
        yAxis: {
          type: 'value'
        },
        series: [
          {
            data: [120, 200, 150, 80, 70, 110, 130],
            type: 'bar'
          }
        ]
      };
      myChart.setOption(option);

      myChart.on('mouseover', (e) => {
        console.log('鼠标移入',e)
        if(e.componentType === "xAxis"){
          // 我们这里先判断一个创建的dom节点是否存在
          let tipNameDom = document.getElementById('tipNameDom')
          console.log(1, tipNameDom)
          // 如果不存在我们创建一个dom节点
          if(!tipNameDom){
            // 创建一个元素
            var createDivElement = document.createElement('div')
            // 给这个元素设置属性
            createDivElement.setAttribute('id', 'tipNameDom')
            // // 设置元素的位置
            createDivElement.style.display = 'block'
            createDivElement.style.position = 'absolute'
            // 获取当前位置
            createDivElement.style.top =  e.event.offsetY + 15 + 'px'
            createDivElement.style.left = e.event.offsetX - 10 + 'px'
            // 这里需要使用 innerHTML,因为我们设置了一样html的属性
            createDivElement.innerHTML = e.value
            // document.querySelector('body').appendChild(createDivElement)
            document.querySelector('body').appendChild(createDivElement)
          }else {
            tipNameDom.style.display = 'block'
            tipNameDom.style.position = 'absolute'
            // 获取当前位置
            tipNameDom.style.top =  e.event.offsetY + 15 + 'px'
            tipNameDom.style.left = e.event.offsetX - 10 + 'px'
            // 这里需要使用 innerHTML,因为我们设置了一样html的属性
            tipNameDom.innerHTML = e.value
          }
        }
      })

      //  移入事件如果被多次触发,则hover的时候无法显示全部
      myChart.on('mouseout', function(params) {
        console.log('移除元素',params )
        if (params.componentType === 'xAxis') {
          let elementDiv = document.querySelector('#tipNameDom')
          console.log('elementDiv', elementDiv)
          elementDiv.style.display = 'none'
        }  
      })
  }
</script>
</head>
<body>
  <div style="width: 400px;height: 300px;" class="echars"></div>
</body>

</html>

尾声

如果你觉得我写的不错的话,点一下推荐。感谢了
我已经几个月没有人给我推荐了。
听说打赏的小哥哥都追到女朋友了,
咦!你不信,不信你给我打赏看一下!
保准你追到到喜欢的Ta

本文分享自华为云社区《
基于istio实现多集群流量治理
》,作者: 可以交个朋友。

一 背景

对多云、混合云等异构基础设施的服务治理是Istio重点支持的场景之一。为了提高服务的可用性,避免厂商锁定,企业通常会选择将应用部署在多个地域的多个集群,甚至多云、混合云等多种云环境下,多集群的方案逐步成为企业应用部署的最佳选择。因此越来越多的用户对跨集群的服务治理有着强烈的需求,在此背景下Istio作为ServiceMesh领域的事实标准,推出了多种多集群管理方案。

二 简介

目前Istio支持4种多集群模型。

  1. 扁平网络单控制面模型
  2. 扁平网络多控制面模型
  3. 非扁平网络单控制面模型
  4. 非扁平网络多控制面模型

多集群的单控制面模型是指多个集群共用同一套Istio控制面,多集群的多控制面模型指每个集群都要独立使用一套Istio控制面,无论是单控制面还是多控制面模型,每套Istio控制面(istiod)都要连接所有集群的Kube-apiserver,并且List-Watch获取所有集群的
Service、Endpoint、Pod 、Node
,并控制面集群内或集群间的服务访问,但是只监听主集群的
VirtualService、DestinationRule、Gateway
等Istio API对象。

根据集群间网络是否扁平,Istio又对两种控制面模型进行了细分:

  • 扁平网络:多集群容器网络通过VPN等技术打通,Pod跨集群访问直通。
  • 非扁平网络:每个集群的容器网络都相互隔离,跨集群的访问不能直通,必须通过东西向网关

生产环境上在选择 Istio 多集群模型时,当然需要结合自己的实际场景来决定。如果集群之间的网络是扁平的,那么可以选择扁平网络模型,如果集群之间的网络是隔离的,那么可以选择非扁平网络模型。如果集群规模较小,那么可以选择单控制面模型,如果集群规模较大,那么可以选择多控制面模型。

本文档选择非扁平网络多控制面模型来进行安装说明:安装模型如下所示
image.png
非扁平网络多控制面模型有如下特点。

  1. 不同的集群不需要在一张大网下,即容器网络不需要三层打通,跨集群的服务访问通过
    Istio East-West Gateway
    转发。
  2. 每个kubernetes集群的Pod地址范围与服务地址范围没有限制,可以与其他集群重叠,不同集群之间互不干扰
  3. 每个Kubernetes集群的Sidecar仅连接到本集群的Istio控制面,通信效率更高。
  4. Istiod只监听主集群的Istio配置,因此
    VirtualService、DestinationRule、Gateway
    等资源存在冗余复制问题
  5. 同一集群内部服务访问: Pod之间直接连接;跨集群的服务访问:依赖DNS代理解析其他集群的服务域名,由于集群之间的网络相互隔离,所以依赖Remote集群的
    East-west Gateway
    中转流量。

三 ClusterMesh 环境搭建

搭建 cluster1 和 cluster2 两个集群,然后每个集群上安装 Istio 控制平面, 且将两者均设置为主集群(primary cluster)。 集群 cluster1 在 network1 网络上,而集群 cluster2 在 network2 网络上。

3.1 前提条件

本次搭建环境信息如下: 使用Kind搭建Kubernetes集群,Kind版本为v0.19.0。 Kubernetes 版本为1.27.3 ; Istio 版本为 1.20.1。

image.png

在搭建k8s 集群之前确保Linux节点已安装docker kubectl 和 kind。

下载istioctl二进制

curl -L https://istio.io/downloadIstio | ISTIO_VERSION=1.20.1 TARGET_ARCH=x86_64 sh -
将 istioctl 客户端添加到路径
image.png

3.2 Kubernetes集群安装

cluster1和cluster2集群安装脚本如下

# create-cluster.sh
# This script handles the creation of multiple clusters
usingkind and the
# ability to create and configure an insecure container registry.
set -o xtraceset -o errexitset -o nounsetset -o pipefail

# shellcheck source
=util.sh
NUM_CLUSTERS
="${NUM_CLUSTERS:-2}"KIND_IMAGE="${KIND_IMAGE:-}"KIND_TAG="${KIND_TAG:-v1.27.3@sha256:3966ac761ae0136263ffdb6cfd4db23ef8a83cba8a463690e98317add2c9ba72}"OS="$(uname)"function create-clusters() {
local num_clusters
=${1}

local image_arg
="" if [[ "${KIND_IMAGE}"]]; then
image_arg
="--image=${KIND_IMAGE}"elif [["${KIND_TAG}"]]; then
image_arg
="--image=kindest/node:${KIND_TAG}"fifor i in $(seq "${num_clusters}"); dokind create cluster--name "cluster${i}" "${image_arg}"fixup-cluster "${i}"echo

done
}

function fixup
-cluster() {
local i
=${1} # cluster numif [ "$OS" != "Darwin"];then
# Set container IP address
as kube API endpoint in order for clusters to reach kube API servers inother clusters.
local docker_ip
docker_ip
=$(docker inspect --format='{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "cluster${i}-control-plane")
kubectl config
set-cluster "kind-cluster${i}" --server="https://${docker_ip}:6443"fi

# Simplify context name
kubectl config rename
-context "kind-cluster${i}" "cluster${i}"}
echo
"Creating ${NUM_CLUSTERS} clusters"create-clusters "${NUM_CLUSTERS}"kubectl config use-context cluster1

echo
"Kind CIDR is $(docker network inspect -f '{{$map := index .IPAM.Config 0}}{{index $map"Subnet"}}' kind)"echo"Complete"

以上集群安装的过程中,为了istiod能够访问对方集群的
apiserver
地址,集群
kube-apiserver
的地址设置为master节点的地址。因为是kind部署的集群,两个集群的master节点本质上都是同个宿主机上的docker运行的容器。

image.png

确认cluster1和cluster2 是否就绪

image.png

3.3 使用MetalLB为网关分配ExternalIP

由于使用的是kind部署多集群,istio南北向网关和东西向网关创建需要创建LoadBalencer service,均需要使用到ExternalIP。这里借助metalLB 实现LB ip地址的分发和宣告。
查看kind搭建集群使用节点子网网段:
172.18.0.0/16
采用metalLB L2模式进行部署。

cluster1中的metalLB配置清单: metallb-config-1.yaml

### forcluster1
##配置IPAddressPool,用于lbip地址的分配。L2模式下,ippool地址和worker节点处于同一子网即可
apiVersion: metallb.io
/v1beta1
kind: IPAddressPool
metadata:
name: first
-poolnamespace: metallb-system
spec:
addresses:
- 172.18.1.230-172.18.1.240 ---##配置L2Advertisement,用于地址宣告
apiVersion: metallb.io
/v1beta1
kind: L2Advertisement
metadata:
name: first
-advnamespace: metallb-system
spec:
ipAddressPools:
- first-pool

cluster2集群中的metalLB配置清单:metallb-config-2.yaml

### forcluster2
##配置IPAddressPool,用于lbip地址的分配。L2模式下,ippool地址和worker节点处于同一子网即可
apiVersion: metallb.io
/v1beta1
kind: IPAddressPool
metadata:
name: second
-poolnamespace: metallb-system
spec:
addresses:
- 172.18.1.241-172.18.1.252 ---##配置L2Advertisement,用于地址宣告
apiVersion: metallb.io
/v1beta1
kind: L2Advertisement
metadata:
name: second
-advnamespace: metallb-system
spec:
ipAddressPools:
- second-pool

使用脚本进行安装

#!/usr/bin/env bashset -o xtraceset -o errexitset -o nounsetset -o pipefail

NUM_CLUSTERS
="${NUM_CLUSTERS:-2}" for i in $(seq "${NUM_CLUSTERS}"); doecho"Starting metallb deployment in cluster${i}"kubectl apply-f https://raw.githubusercontent.com/metallb/metallb/v0.13.10/config/manifests/metallb-native.yaml --context "cluster${i}" kubectl create secret generic -n metallb-system memberlist --from-literal=secretkey="$(openssl rand -base64 128)" --context "cluster${i}"## 增加等待时间,如果metallb负载没部署起来,创建IPAddressPool L2Advertisement 会报错
sleep
10kubectl apply-f ./metallb-config-${i}.yaml --context "cluster${i}"echo"----"done

确认metalLB部署情况

image.png

确认IPAddressPool信息:

image.png

3.4 集群共享根CA 配置信任关系

为了支持安全的跨集群mTLS通信,多控制面模型要求每个集群的控制面Istiod都使用相同的CA机构颁发的中间CA证书,供Citatel签发证书使用,以支持跨集群的TLS双向认证。
image.png
Istio东西向网关(跨集群访问)工作时使用基于SNI的路由,它根据TLS请求的SNI,自动将其路由到SNI对应的Cluster,因此非扁平网络的跨网络访问要求所有流量都必须经过TLS加密。

在集群中插入证书和密钥,脚本如下(需要将该脚本移动到istio的安装包目录下):

#!/usr/bin/env bashset -o xtrace
#
set -o errexitset -o nounsetset -o pipefail
NUM_CLUSTERS
="${NUM_CLUSTERS:-2}"##在istio安装包的顶层目录下 创建目录 用来存放证书和密钥
mkdir
-p certs
pushd certs

##生成根证书和密钥
make
-f ../tools/certs/Makefile.selfsigned.mk root-cafor i in $(seq "${NUM_CLUSTERS}"); do##对于每个集群,为 Istio CA 生成一个中间证书和密钥
make
-f ../tools/certs/Makefile.selfsigned.mk "cluster${i}-cacerts"##对于每个集群,创建istio-system 命名空间
kubectl create
namespace istio-system --context "cluster${i}"## 对于每个集群,通过给istio系统命名空间打上topology.istio.io/network 标签添加网络标识
kubectl
--context="cluster${i}" label namespace istio-system topology.istio.io/network="network${i}"##对于每个集群,给工作节点node打上地域和可用区标签,便于istio实现地域故障转移、地域负载均衡
kubectl
--context="cluster${i}" label node "cluster${i}-control-plane" topology.kubernetes.io/region="region${i}"kubectl--context="cluster${i}" label node "cluster${i}-control-plane" topology.kubernetes.io/zone="zone${i}"#在每个集群中,创建一个私密 cacerts,使用所有输入文件 ca-cert.pem, ca-key.pem,root-cert.pem 和 cert-chain.pem。
kubectl delete secret cacerts
-n istio-system --context "cluster${i}"kubectl create secret generic cacerts-n istio-system --context "cluster${i}"\--from-file="cluster${i}/ca-cert.pem"\--from-file="cluster${i}/ca-key.pem"\--from-file="cluster${i}/root-cert.pem"\--from-file="cluster${i}/cert-chain.pem"echo"----"done
执行脚本,将会生成根证书和中间证书等文件

image.png

image.png

3.5 Istio服务网格安装

为cluster1,和cluster2 集群安装多控制面istio网格。

将cluster1 设置为主集群,在istio的安装目录下执行如下命令

cat <<EOF >cluster1.yaml
apiVersion: install.istio.io
/v1alpha1
kind: IstioOperator
spec:
values:
global:
meshID: mesh1
multiCluster: ##开启多集群配置
clusterName: cluster1 #指定k8s集群名称
network: network1 #指定网络标识
logging:
level: debug
EOF

将cluster2 设置为主集群,在istio的安装目录下执行如下命令

cat <<EOF >cluster2.yaml
apiVersion: install.istio.io
/v1alpha1
kind: IstioOperator
spec:
values:
global:
meshID: mesh2
multiCluster: ##开启多集群配置
clusterName: cluster2 #指定k8s集群名称
network: network2 #指定网络标识
logging:
level: debug
EOF
编写自动化安装脚本
#!/usr/bin/env bashset -o xtraceset -o errexitset -o nounsetset -o pipefail

OS
="$(uname)"NUM_CLUSTERS="${NUM_CLUSTERS:-2}" for i in $(seq "${NUM_CLUSTERS}"); doecho"Starting istio deployment in cluster${i}"istioctl install--force --context="cluster${i}" -f "cluster${i}.yaml"echo"Generate eastwest gateway in cluster${i}"## 在每个集群中安装东西向网关。
bash samples
/multicluster/gen-eastwest-gateway.sh \--mesh "mesh${i}" --cluster "cluster${i}" --network "network${i}" |\
istioctl
--context="cluster${i}" install -y -f -echo

done

执行脚本,进行istio的安装部署

image.png

稍等片刻后,等待安装完成

image.png

可以发现每个集群中的网关使用的ExternalIP信息为配置的metalLB设置的IPPool中的地址。

3.6 在东西向网关开放服务

因为集群位于不同的网络中,所以我们需要在两个集群东西向网关上开放所有服务(*.local)。 虽然此网关在互联网上是公开的,但它背后的服务只能被拥有可信 mTLS 证书的服务访问, 就像它们处于同一网络一样。执行下面的命令在两个集群中暴露服务:

apiVersion: networking.istio.io/v1beta1
kind: Gateway
metadata:
name: cross
-network-gateway
spec:
selector:
istio: eastwestgateway # 专用于东西向流量的网关
servers:
-port:
number:
15443# 已经声明了
name: tls
protocol: TLS
tls:
mode: AUTO_PASSTHROUGH # 东西向网关工作模式是 TLS AUTO_PASSTHROUGH
hosts:
- "*.local" # 暴露所有的服务

分别在每个集群中应用上述Gateway配置:
kubectl -n istio-system --context=cluster${i} apply -f samples/multicluster/expose-services.yaml
image.png

3.7 配置secret以便istiod访问远程集群apiserver

每个k8s集群中的 istiod 需要 List-Watch 其他集群的 Kube-APIServer,使用 K8s 集群的凭据来创建 Secret 对象,以允许 Istio 访问远程 Kubernetes apiserver。

#!/usr/bin/env bashset -o xtraceset -o errexitset -o nounsetset -o pipefail
OS
="$(uname)"NUM_CLUSTERS="${NUM_CLUSTERS:-2}" for i in $(seq "${NUM_CLUSTERS}"); do for j in $(seq "${NUM_CLUSTERS}"); do if [ "$i" -ne "$j"]
then
echo
"Enable Endpoint Discovery between cluster${i} and cluster${j}" if [ "$OS" == "Darwin"]
then
# Set container IP address
as kube API endpoint in order for clusters to reach kube API servers inother clusters.
docker_ip
=$(docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "cluster${i}-control-plane")
istioctl create
-remote-secret \--context="cluster${i}"\--server="https://${docker_ip}:6443"\--name="cluster${i}" |\
kubectl apply
--validate=false --context="cluster${j}" -f - elseistioctl create-remote-secret \--context="cluster${i}"\--name="cluster${i}" |\
kubectl apply
--validate=false --context="cluster${j}" -f -fi
fi
done
done

执行以上脚本:remote secret创建完成。

image.png

查看istiod日志发现已经监听远程集群了

image.png

四 Istio多集群流量治理实践

image.png

每个集群创建sample 命名空间,并设置sidecar自动注入

kubectl create --context=cluster1 namespacesample
kubectl create
--context=cluster2 namespacesample

kubectl label
--context=cluster1 namespacesample \
istio
-injection=enabled
kubectl label
--context=cluster2 namespacesample \
istio
-injection=enabled

kubectl apply
--context=cluster1 \-f samples/helloworld/helloworld.yaml \-l service=helloworld -n sample
kubectl apply
--context=cluster2 \-f samples/helloworld/helloworld.yaml \-l service=helloworld -n sample

分别在不同集群部署不同版本的服务

把应用 helloworld-v1 部署到 cluster1:

kubectl apply --context=cluster1 \-f samples/helloworld/helloworld.yaml \-l version=v1 -n sample

把应用 helloworld-v2 部署到 cluster2:

kubectl apply --context=cluster2 \-f samples/helloworld/helloworld.yaml \-l version=v2 -n sample

部署测试客户端

kubectl apply --context=cluster1 \-f samples/sleep/sleep.yaml -n sample
kubectl apply
--context=cluster2 \-f samples/sleep/sleep.yaml -n sample

确认负载实例部署成功,并且sidecar已经注入

image.png

4.1 验证跨集群流量

用 Sleep pod 重复调用服务 HelloWorld。 为了确认负载均衡按预期工作,需要从所有集群调用服务 HelloWorld。

从 cluster1 中的 Sleep pod 发送请求给服务 HelloWorld

image.png

从 cluster2 中的 Sleep pod 发送请求给服务 HelloWorld

image.png

4.3 验证从网关访问

通过网关访问服务端Helloworld

创建virtualservice、gateway等istio资源,配置清单如下

# helloworld-gateway.yaml
apiVersion: networking.istio.io
/v1beta1
kind: Gateway
metadata:
name: helloworld
-gateway
spec:
selector:
istio: ingressgateway # use istio
defaultcontroller
servers:
-port:
number:
80name: http
protocol: HTTP
hosts:
- "*" ---apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: helloworld
spec:
hosts:
- "*"gateways:- helloworld-gateway
http:
-match:-uri:
exact:
/hello
route:
-destination:
host: helloworld
port:
number:
5000

注意: 两个集群都需要应用该配置

访问效果如下:

image.png

4.3 验证地域负载均衡

对流量进行更精细的控制,将
region1 -> zone1

region1 -> zone2
两个地区的权重分别为 80% 和 20%,使用 DestinationRule 来配置权重分布

# locality-lb-weight.yaml
apiVersion: networking.istio.io
/v1beta1
kind: DestinationRule
metadata:
name: helloworld
namespace: sample
spec:
host: helloworld.sample.svc.cluster.local
trafficPolicy:
connectionPool:
http:
maxRequestsPerConnection:
1loadBalancer:
simple: ROUND_ROBIN
localityLbSetting:
enabled:
truedistribute:- from: region1/*to:
"region1/*": 80
"region2/*": 20
- from: region2/*
to:
"region2/*": 80
"region1/*": 20
outlierDetection:
consecutive5xxErrors: 1
interval: 1s
baseEjectionTime: 1m

注意: 两个集群都需要应用该配置

从 cluster1 中通过网关发送请求给服务 HelloWorld

image.png

从 cluster2中通过网关发送请求给服务 HelloWorld

image.png

4.4 验证地域故障转移

当多个地区/区域部署多个服务实例时,如果某个地区/区域的服务实例不可用,可以将流量转移到其他地区/区域的服务实例上,实现地域故障转移,这样就可以保证服务的高可用性。

# locality-lb-failover.yaml
apiVersion: networking.istio.io
/v1beta1
kind: DestinationRule
metadata:
name: helloworld
namespace: sample
spec:
host: helloworld.sample.svc.cluster.local
trafficPolicy:
connectionPool:
http:
maxRequestsPerConnection:
1 # 关闭 HTTP Keep-Alive,强制每个HTTP请求使用一个新连接的策略
loadBalancer:
simple: ROUND_ROBIN
localityLbSetting: # 地域负载均衡配置,开启异常点检测后,默认开启。
enabled:
truefailover: # 地域故障转移策略- from: region1
to: region2
- from: region2
to: region1
outlierDetection:
consecutive5xxErrors:
1 # 连续 1次 5xx 错误
interval: 1s # 检测间隔 1s
baseEjectionTime: 1m # 基础驱逐时间 1m

注意: 两个集群都需要应用该配置

从 cluster1 中通过网关发送请求给服务 HelloWorld

image.png

模拟故障,手动将cluster1集群中Helloworld V1版本设置故障

image.png

再次访问,故障检测生效,触发故障转移,并验证响应中的 version 始终为 v2,也就是说我们访问的是 region2 的 helloworld 服务,这样就实现了地域故障转移。

image.png

故障转移的前提是当前region内,所有实例都不可用时,才会转移到到目前region,否则流量还会发往当前region的其他可用实例。

五 备注

参考文献如下:

  1. istio开源社区(跨网络多主架构的安装说明):
    https://istio.io/latest/zh/docs/setup/install/multicluster/multi-primary_multi-network/

  2. kind安装集群脚本参考:
    https://github.com/cnych/multi-cluster-istio-kind/tree/main/kind-create

  3. 多集群证书管理参考:
    https://istio.io/latest/zh/docs/tasks/security/cert-management/plugin-ca-cert/

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

大家好,我是
知微

作为一个程序员,写代码就跟厨师做菜一样,如果没有一些好的方法和习惯,做出来的菜肯定又慢又难吃。

下面分享一些优秀的编程习惯,用了都说好!

1、规范的命名

命名是代码清晰度的关键。

变量、函数和类的命名需简洁明了,并遵循项目中约定的命名规则,如驼峰命名法或下划线分隔法。这样你的代码会更易于理解和维护。

下面展示一下不规范和规范命名的C语言例子

不规范的命名:

// 不明确意图的命名方式
int a;          // 无法表达变量用途
float b;        // 缺乏描述性
void doSomething(); // 动作描述不清

int dta;        // 缩写可能令人困惑,不容易理解
void procData();    // "proc" 是处理的缩写,可读性不强

// 示例代码片段
int rfq4351d; // 没有任何意义的命名

void hndlrq() {
    int qwk = 10; // 很难猜到 qwk 是什么
    // ... 更多的代码
}

规范的命名:

// 清晰、具有描述性的命名
int accountBalance;    // 表明这是一个账户余额
float interestRate;    // 表明这是利率
void processOrder();   // 动作和目的明确

int customerData; // 用于存储顾客数据
void processData();   // 更清晰的函数命名

// 示例代码片段
int requestId; // 明确表达了这是一个请求标识

void handleRequest() {
    int maxRetries = 10; // 清晰表明这是最大重试次数
    // ... 更多的代码
}

2、合理编写注释

程序员最讨厌别人的代码不写注释,因为阅读起来特别困难。

虽然前段时间很流行防御式编程,不写注释,代码也尽量让他人难读懂。

但是该写注释的,咱还是得写,这样可以提高我们的效率。否则时间长了,自己不根本记得。

图片

但是切记,注释不要写太细,因为后面代码改动,以前的注释可能就不适用了,如果没有及时更新,反而会误导自己。

3. 随时保存代码并测试

修改代码后,应立即进行自测试以确保正确性。这叫步步为营,慢就是快。

不要等写完一大段代码后才提交,这可以减少调试的难度并及时发现问题。

我以前就犯过这样的错误,觉得修改的代码逻辑不算特别复杂,就是量大一些而已。结果全部改完了之后,编译运行。出来意想不到的结果,然后进入漫长的多线程调试,简直就是灾难。

4、学会先思考逻辑后写代码

编程更多的是思考,梳理逻辑,写代码只是将你的想法表达出来。如果一上来就开始写代码,而不去思考和设计,可以100%确定,写出来的代码一定很烂。

5、阅读官方文档

​ 官方的文档,你也可以理解为说明书。有谁能比设计产品的人更了解产品呢?这些文档不仅质量非常高,通常内容也都是最新的。

所以,当你遇到问题的时候,有时候看看官方文档,反而比用搜索引擎寻找解决办法更快。

6、不要重复造轮子

​ 每次你辛苦编码想要实现的功能,很可能在开源社区已经有了特别棒的解决办法。如果你能够善用这些现成的资源和各种开源的代码库,你的工作效率可以大大提高,轻松许多。

一个好的开源工具或库通常具备以下特点:

  • 开放源代码,并且许可证较为宽松
    :这样的轮子能够让你自由使用,甚至是在商业项目中,而不用担心法律问题。
  • 完善的文档和规范的代码
    :有详尽文档的轮子易于理解和使用,规范的代码则保证了可读性和可维护性。
  • 友好的接口,最好还附有示例
    :接口设计得好的轮子可以简化开发流程,如果附带示例,能使你快速学会如何使用。

比如我
这篇文章(点击跳转)
里提到的定时器模块,日志模块,多功能按键模块,你稍稍改改就能用上。

7、多读多写代码

怎么理解多读呢?这点就跟写作一样,没有大量的输入,你很难有好的输出。

一学就会,一练就废。要想摆脱这种情况,就需要多写了。

8、预留开发时间

新的项目立项之初,一定会评估开发时间。

你要是不会评估开发时间,拍脑袋随便定一个。时间长了,老板肯定不同意。时间短了,开发中可能遇到的各种问题,到时候天天加班,紧赶慢赶,心神俱疲。一头乌黑浓密的头发,没多久就成了地中海。

所以,合理的预留开发时间,可以确保交付的质量,也能让自己和团队轻松一点。

9、大胆重构

​ 不要害怕重构代码,好的设计不是一开始就有的,而且逐渐重构出来的。

通过重构,你可以改进代码结构,提高代码的可读性和可维护性 。而这种活,应该贯彻整个开发过程,你也会因为重构,使自己的编程能力得到极大的提升。

10、 善用代码管理工具,定时备份

我接触到的有些程序员,每写一版代码,都是复制一份,而且不备份。说实话,我看着都担心,如果某一天硬盘坏了,那么之前的努力就全部白费了。碰到项目紧急的时候,你就算007都不一定来得及了。


git
或者
svn
这样的软件来管理代码,一方面可以防止数据丢失,另一方面可以在程序出现错误的时候方便数据回滚。

以上就是今天要分享的内容了!