2024年1月

这两天有空,继续更新一篇有关 eBPF BCC 框架尾调用的内容。

eBPF 技术很新,能够参考的中文资料很少,而对于 BCC 框架而言,优秀的中文介绍和教程更是凤毛麟角。我尝试去网上检索有关尾调用的中文资料,BCC 框架的几乎没有。即使找到了,这些资料也难以给出可供参考和正确运行的例子。

BCC 框架的中文资料也就图一乐,真正有指导意义的,还得去看 Brendan Gregg 大神的博客和 bcc 项目。

既然如此,我来抛砖引玉,就简单介绍一下 eBPF 尾调用在 BCC 框架中是如何应用的吧。

1 何为尾调用?

引用
ebpf.io
网站的一句介绍:“尾调用允许 eBPF 调用和执行另一个 eBPF 并替换执行上下文,类似于一个进程执行
execve()
系统调用的方式。”

也就是说,尾调用之后,函数不会再返回给调用者了。

那么,
eBPF 为什么要使用尾调用呢?
这是因为,eBPF 的运行栈太有限了(仅有 512 字节),在递归调用函数时(实际上是向运行栈中一节一节地添加栈帧),很容易导致栈溢出。而尾调用恰恰允许在不增加堆栈的情况下,调用一系列函数。这是非常有效且实用的。

你可以使用下面的辅助函数来增加一个尾调用:

long bpf_tail_call(void *ctx, struct bpf_map *prog_array_map, u32 index)

其三个参数的含义分别是:

  • ctx
    向被调用者传递当前 eBPF 程序的上下文信息。
  • prog_array_map
    是一个程序数组(
    BPF_MAP_TYPE_PROG_ARRAY
    )类型的 eBPF
    map
    ,用于记录一组 eBPF 程序的文件描述符。
  • index
    为程序数组中需要调用的 eBPF 程序索引。

2 如何使用尾调用?

关于 BCC 框架,
reference_guide.md
给出了一个例子。见
27.map.call()

内核态程序:

// example.c
BPF_PROG_ARRAY(prog_array, 10);			// A)定义程序数组

int tail_call(void *ctx) {
    bpf_trace_printk("Tail-call\n");
    return 0;
}

int do_tail_call(void *ctx) {
    bpf_trace_printk("Original program\n");
    prog_array.call(ctx, 2);			// B)调用 ID 为 2 的函数
    return 0;
}

用户态程序:

b = BPF(src_file="example.c")
tail_fn = b.load_func("tail_call", BPF.KPROBE)		# C)尾调用函数定义
prog_array = b.get_table("prog_array")
prog_array[c_int(2)] = c_int(tail_fn.fd)		# D)绑定尾调用函数
b.attach_kprobe(event="some_kprobe_event", fn_name="do_tail_call")

代码解释:

A)尾调用的实现,基于
程序数组(BPF_PROG_ARRAY)
这一映射结构。程序数组也是一个键值对结构(废话,它也是 BPF_MAP 类型之一)。其 key 为自定义索引,用于寻找对应的调用程序;其 value 为尾调用函数的文件描述符
fd

B)调用
尾调用函数
需要执行
call()
方法,传入程序数组(BPF_PROG_ARRAY)中的 key,用来查找对应的函数 fd。

C)尾调用函数的定义在用户态完成。注意这里有一个易错点:
尾调用需要和父调用保持相同的程序类型(这里是
BPF.KPROBE
)。

D)绑定尾调用函数到程序数组中。不再赘述。

尾调用示意图如下图:
图片名称

3 实现一个尾调用程序

明白尾调用则怎么玩之后,接下来,我们一起实现一个稍微复杂一点的尾调用,用来监视系统调用。

例子改编自《Learning eBPF》一书。目前该书还没有中文版本。

内核态程序:

// tail_hello.c
BPF_PROG_ARRAY(syscall, 300);					// A

int hello(struct bpf_raw_tracepoint_args *ctx) {		// B
    int opcode = ctx->args[1];					// C
    syscall.call((void *)ctx, opcode);				// D
    return 0;
}

int hello_exec(void *ctx) {					// E
    bpf_trace_printk("Executing a program\n");
    return 0;
}

int hello_timer(struct bpf_raw_tracepoint_args *ctx) {		// F
    int opcode = ctx->args[1];
    switch (opcode) {
        case 222:
            bpf_trace_printk("Creating a timer\n");
            break;
        case 226:
            bpf_trace_printk("Deleting a timer\n");
            break;
        default:
            bpf_trace_printk("Some other timer operation\n");
            break;
    }
    return 0;
}

代码解释:

【A】
BPF_PROG_ARRAY
宏定义,对应映射类型
BPF_MAP_TYPE_PROG_ARRAY
。在这里,命名为
syscall
,容量为 300。

【B】即将被用户态代码绑定在
sys_enter
类别的
Tracepoint
上,即当有任何系统调用被执行时,都会触发这个函数。
bpf_raw_tracepoint_args
类型的结构体
ctx
存放上下文信息。

译者注:
sys_enter

raw_syscalls
类型的
Tracepoint
;同族还有
sys_exit

详细信息可查看文件:
/sys/kernel/debug/tracing/events/raw_syscalls/sys_enter/format

【C】对于
sys_enter
类型的追踪点,其参数第 2 项为操作码,即指代即将执行的系统调用号。这里赋值给变量
opcode

【D】这一步,我们把
opcode
作为索引,进行尾调用,执行下一个 eBPF 程序。

再次提醒,这里的写法是 BCC 优化,在真正编译前,BCC 最终会将其重写为
bpf_tail_call
辅助函数。

【E】
hello_execve()
,程序数组的一项,对应
execve()
系统调用。经由尾调用触发。

【F】
hello_timer()
,程序数组的一项,对应计时器相关的系统调用。经由尾调用触发。

现在,我们来看一下用户态的程序。

#!/usr/bin/python3
from bcc import BPF
import ctypes as ct

b = BPF(src_file="tail_hello.c")
b.attach_raw_tracepoint(tp="sys_enter", fn_name="hello")		# A

exec_fn = b.load_func("hello_exec", BPF.RAW_TRACEPOINT)			# B
timer_fn = b.load_func("hello_timer", BPF.RAW_TRACEPOINT)

prog_array = b.get_table("syscall")					# C
prog_array[ct.c_int(59)] = ct.c_int(exec_fn.fd)
prog_array[ct.c_int(222)] = ct.c_int(timer_fn.fd)
prog_array[ct.c_int(223)] = ct.c_int(timer_fn.fd)
prog_array[ct.c_int(224)] = ct.c_int(timer_fn.fd)
prog_array[ct.c_int(225)] = ct.c_int(timer_fn.fd)
prog_array[ct.c_int(226)] = ct.c_int(timer_fn.fd)

b.trace_print()								# D

代码解释:

【A】与前文绑定到
kprobe
不同,这次用户态将
hello()
主 eBPF 程序绑定到
sys_enter
追踪点(
Tracepoint
)上.

【B】这些
load_func()
方法用来将每个尾调用函数载入内核,并返回尾调用函数的
文件描述符
。尾调用需要和父调用保持相同的程序类型(这里是
BPF.RAW_TRACEPOINT
)。

一定不要混淆,每个尾调用程序本身就是一个 eBPF 程序。

【C】接下来,向我们创建好的
syscall
程序数组中添充条目。大可不必全部填满,如果执行时遇到空的,那也没啥影响。同样的,将多个
index
指向同一个尾调用也是可以的(事实上这段程序就是这样做的,将计时器相关的系统调用指向同一个 eBPF 尾调用)。

译者注:这里的
ct.c_int()
来自 Python 的 ctypes 库,用于 Python 到 C 的类型转换。

【D】不断打印输出,直到用户终止程序。

4 运行这个尾调用程序

运行这个 Python 程序,恭喜你,你可能会得到一段报错:

/virtual/main.c:22:20: error: incompatible pointer to integer conversion passing 'void *' to parameter of type 'u64' (aka 'unsigned long long') [-Wint-conversion]
    bpf_tail_call_((void *)bpf_pseudo_fd(1, -1), ctx, opcode);
                   ^~~~~~~~~~~~~~~~~~~~~~~~~~~~
/virtual/include/bcc/helpers.h:552:25: note: passing argument to parameter 'map_fd' here
void bpf_tail_call_(u64 map_fd, void *ctx, int index) {

出现这个问题,说明你的系统上
clang
版本为 15。不信你可以看一下:

clang -v

我们可以在这个 issue 中找到问题描述(
https://github.com/iovisor/bcc/issues/4642
)。
可以在这个 issue 中找到解决方案(
https://github.com/iovisor/bcc/issues/4467
)。

大致意思就是,你尝试将一个
u64
类型的值转换成
void *
。这在
clang-14
中仅仅是一个
warning
,但是在
clang-15
中就会被认定为一个
error

你可以选择降低
clang
版本来解决这个问题。

运行截图如下所示:

图片名称

5 总结

尾调用的适当应用,能够使 eBPF 如虎添翼。

然而,内核 4.2 版本才开始支持尾调用,在很长的一段时间内,尾调用和 BPF 的编译过程不太兼容(尾调用需要 JIT 编译器的支持)。直到 5.10 版本才解決了这个问题。

你可以最多链接 33 个尾调用(而每个 eBPF 程序的指令复杂度最大支持 100w)。这样一来,eBPF 终于能够真正发挥出巨大潜力来了。

至此,BCC 框架中 BPF 映射就先告一段落了,后面看经历是否在增加其他 BPF 映射结构的应用。

如果您有问题,欢迎留言讨论!如果您觉得这篇文章还不错,请点一个推荐吧~

写在前面

两年前我做了第一个开源软件
DreamScene2
动态桌面,如今受到了很多人的喜欢,这增加了我继续做好开源软件的信心。之前的这个软件一直有人希望我加入一个设置屏保壁纸的功能,因为 DreamScene2 就是一个单纯的动态桌面的软件,所以一直没有加入这个功能。今天我带来一个新的开源软件,软件依然是小而强大,简洁并且快速。

欢迎 Star 和 Fork:
https://github.com/he55/SonomaWallpaper

介绍

Sonoma Wallpaper 是首款将 macOS Sonoma 4k 120 帧动态屏保壁纸带到 Windows 11 的软件,壁纸包含了自然景观、城市景观、水下景观和地球四个主题的屏保共有 134 个 4k 屏保壁纸。


功能

  • 4k 120 帧率视频播放
  • 多语言
  • 高 dpi 支持
  • 深色模式
  • 多任务下载
  • Windows 11 支持

如何使用

选中已下载的视频壁纸,然后打开
显示为屏幕保护程序
开关,程序会在 2 分钟系统无操作时自动播放屏保壁纸。

下载壁纸

  • 选择一个壁纸点击下载
  • 等待壁纸下载完成
  • 壁纸下载完成后,可以点击预览按钮看效果

视频播放功能代码

在解码 4k 120 帧视频的时候我试过 VLC、迅雷的 APlayer、mpv、ffmpeg、PotPlayer 这些引擎或播放器,发现在 Windows 10 上它们对 120 帧视频支持的都不好。最后发现 Windows 11 已经原生支持了 120 帧视频播放,但只有 UWP 和 WinUI3 程序原生支持。WPF 要使用 UWP 的控件需要安装
Microsoft.Toolkit.Wpf.UI.XamlHost
这个 NuGet 包,这个包可以让 Win32 程序调用 UWP 的控件。包安装后在 xaml 文件中添加名称空间
xmlns:xamlhost="clr-namespace:Microsoft.Toolkit.Wpf.UI.XamlHost;assembly=Microsoft.Toolkit.Wpf.UI.XamlHost"
,然后添加
WindowsXamlHost
控件用来显示 UWP 的
MediaPlayerElement
媒体播放控件。

完整代码如下

<Window x:Class="WpfApp21.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp21"
        xmlns:xamlhost="clr-namespace:Microsoft.Toolkit.Wpf.UI.XamlHost;assembly=Microsoft.Toolkit.Wpf.UI.XamlHost"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <xamlhost:WindowsXamlHost
        ChildChanged="WindowsXamlHost_ChildChanged"
        InitialTypeName="Windows.UI.Xaml.Controls.MediaPlayerElement" />
</Window>

添加
ChildChanged
事件用来处理
MediaPlayerElement
控件创建成功后的要做的事情

  • Stretch 设置视频拉伸模式
  • Source 设置播放文件路径
  • AutoPlay 设置自动播放
  • IsMuted 设置静音
  • IsLoopingEnabled 设置循环播放
private void WindowsXamlHost_ChildChanged(object sender, EventArgs e)
{
    MediaPlayerElement mediaPlayerElement = (MediaPlayerElement)((WindowsXamlHost)sender).Child;
    if (mediaPlayerElement != null)
    {
        mediaPlayerElement.Stretch = Windows.UI.Xaml.Media.Stretch.UniformToFill;
        mediaPlayerElement.Source = MediaSource.CreateFromUri(new Uri("C:\wallpapers\comp_LW_L001_C006_PSNK_DENOISE_v02_SDR_PS_FINAL_20180709_FRC_sdr_4k_qp20_240p_t2160_tsa.mov"));
        mediaPlayerElement.AutoPlay = true;
        mediaPlayerElement.MediaPlayer.IsMuted = true;
        mediaPlayerElement.MediaPlayer.IsLoopingEnabled = true;
    }
}

前言

关于解决echarts+ws多次数据刷新渲染,内存增长溢出的尝试。
记录一下,便于下次使用有参考

方法

  1. 关闭echarts动画
  2. tooltip的动画设置为false。(echarts动画会缓存,通过快照可以看出)
 tooltip: {
          axisPointer: {
            animation: false, //很重要!
          },
        },
  1. 单独设置频繁刷新的echarts dom,不使用组件式的封装
  2. echarts实例不挂载在data上。可以如下设置
 data() {
    return {};
  },
  created() {
    this.Eechart = null;
  },
  1. 数据更新使用
this.Eechart.setOption(fullOption, { replaceMerge: ["series"] });

使用其他方式都会造成内存的增长(通过跑f12性能查看),比如以下

1. this.Eechart.setOption(fullOption, true);
2. this.Eechart.setOption(fullOption);
3. this.Eechart.clear();
   this.Eechart.setOption(fullOption, true);
4.this.Eechart.clear(); this.Eechart.dispose();this.Eechart=null 重新init

  1. 卸载挂载,(事实证明还是会增长,后续有说明)
  if (!this.chart) {
     this.chart = echarts.init(this.$el, { renderer: "svg" });
   }
 this.$once("hook:beforeDestroy", () => {
     // //document.getElementById("mainEChart").removeAttribute('_echarts_instance_');
     this.Eechart.off("click");
     this.Eechart.clear();
     this.Eechart.dispose();

     console.log("是否清理?", this.Eechart.isDisposed);

     this.Eechart = null;
     window.removeEventListener("resize", this.handleWindowResize);
   });
  1. finished 与 click 同时使用,会影响click。有个粗暴方法不建议使用。
    当时为了解决100ms-200ms刷新,大量数据渲染坐标消失问题的解决方法。事实证明没用,渲染太快了,当时很多计算放在前端循环(数据量多的时候有100ms+的计算时长)。只有降低频率去解决。尝试过web worker,效果不是很理想,放弃。
      每次更新挂载,下一次更新卸载
       this.chart.on("finished", () => {
       });
      let cache = this.chart._$handlers.finished;
     cache.splice(0, cache.length - 1);
  1. 切换路由时,回到页面,会有内存增长影响
    见6,卸载与挂载。去issue搜了
    https://github.com/apache/echarts/issues/4105
    没看到解决方法。如果有,欢迎留言评论。
解决方式就是给路由加上keep-active

最后

经过以上,测试跑了性能,时间轴也基本全灰,都能回收。内存稳定在一个范围里。
image

问题

如果有更好的解决方式,欢迎大佬们留言。
因为是三维曲面图,需要点击功能,分级颜色。根据数据生成图形。没找到echarts外更好选择。如果有其他的方式或者库之类,请佬务必留言!

本文内容提炼于《
Python深度学习
》一书,整合了前 4 章的内容。

人工智能包含机器学习,而深度学习是机器学习的一个分支。机器学习只能用来记忆训练数据中存在的模式。只能识别出曾经见过的东西。在过去的数据上训练机器学习来预测未来,这里存在一个假设,就是未来的规律与过去相同。

在经典的程序设计(即符号主义人工智能的范式)中,人们输入的是规则(即程序)和需要根据这些规则进行处理的数据,系统输出的是答案。而利用机器学习,人们输入的是数据和从这些数据中预期得到的答案,系统输出的则是规则。这些规则随后可应用于新的数据,并使计算机自主生成答案。

一、深度学习

深度学习是机器学习的一个分支领域,它是从数据中学习表示的一种新方法,强调从连续的层(layer)中进行学习,这些层对应于越来越有意义的表示。在深度学习中,这些分层表示几乎总是通过叫作神经网络(neural network)的模型来学习得到的,神经网络的结构是逐层堆叠。如下图所示,一个多层网络如何对数字图像进行变换,以便识别图像中所包含的数字。

在下图中,这个网络将数字图像转换成与原始图像差别越来越大的表示,而其中关于最终结果的信息却越来越丰富。你可以将深度网络看作多级信息蒸馏操作:信息穿过连续的过滤器,其纯度越来越高(即对任务的帮助越来越大)。

深度学习从数据中进行学习时有两个基本特征:第一,通过渐进的、逐层的方式形成越来越复杂的表示;第二,对中间这些渐进的表示共同进行学习,每一层的变化都需要同时考虑上下两层的需要。

1)工作原理

下图描绘了深度学习的工作原理,神经网络中每层对输入数据所做的具体操作保存在该层的权重(weight)中,其本质是一串数字。想要控制神经网络的输出,就需要能够衡量该输出与预期值之间的距离。这是神经网络损失函数(loss function)的任务,该函数也叫目标函数(objective function)。损失函数的输入是网络预测值与真实目标值(即你希望网络输出的结果),然后计算一个距离值,衡量该网络在这个示例上的效果好坏。深度学习的基本技巧是利用这个距离值作为反馈信号来对权重值进行微调,以降低当前示例对应的损失值。这种调节由优化器(optimizer)来完成,它实现了所谓的反向传播(backpropagation)算法,这是深度学习的核心算法。

一开始对神经网络的权重随机赋值,因此网络只是实现了一系列随机变换。其输出结果自然也和理想值相去甚远,相应地,损失值也很高。但随着网络处理的示例越来越多,权重值也在向正确的方向逐步微调,损失值也逐渐降低。这就是训练循环(training loop),将这种循环重复足够多的次数(通常对数千个示例进行数十次迭代),得到的权重值可以使损失函数最小。具有最小损失的网络,其输出值与目标值尽可能地接近,这就是训练好的网络。这虽然是一个简单的机制,但是一旦具有足够大的规模,将会产生魔法般的效果。

2)突破

深度学习已经取得了以下突破,它们都是机器学习历史上非常困难的领域:

  • 接近人类水平的图像分类
  • 接近人类水平的语音识别
  • 接近人类水平的手写文字转录
  • 更好的机器翻译
  • 更好的文本到语音转换
  • 接近人类水平的自动驾驶
  • 更好的广告定向投放,Google、百度、必应都在使用
  • 更好的网络搜索结果
  • 能够回答用自然语言提出的问题
  • 在围棋上战胜人类

二、数学基础

要理解深度学习,需要熟悉很多简单的数学概念:张量、张量运算、微分、梯度下降等。我们来看一个具体的神经网络示例,使用 Python 的
Keras
库来学习手写数字分类。这里要解决的问题是,将手写数字的灰度图像(28 像素×28 像素)划分到 10 个类别中(0~9)。我们将使用
MNIST
数据集,它是机器学习领域的一个经典数据集。

Keras 是一个模型级(model-level)的库,为开发深度学习模型提供了高层次的构建模块。它不处理张量操作、求微分等低层次的运算。相反,它依赖于一个专门的、高度优化的张量库来完成这些运算,这个张量库就是 Keras 的后端引擎(backend engine)。目前,Keras 有三个后端实现:
TensorFlow
、Theano 和微软认知工具包(CNTK,Microsoft cognitive toolkit)。

首先由 train_images 和 train_labels 组成训练集(training set),模型将从这些数据中进行学习。然后在测试集(test set,即 test_images 和 test_labels)上对模型进行测试。

from keras.datasets importmnist
(train_images, train_labels), (test_images, test_labels)
= mnist.load_data()

接下来的工作流程如下:首先,将训练数据(train_images 和 train_labels)输入神经网络;其次,网络学习将图像和标签关联在一起;最后,网络对 test_images 生成预测,而我们将验证这些预测与 test_labels 中的标签是否匹配。下面我们来构建网络。

from keras importmodelsfrom keras importlayers

network
=models.Sequential()
network.add(layers.Dense(
512, activation='relu', input_shape=(28 * 28,)))
network.add(layers.Dense(
10, activation='softmax'))

神经网络的核心组件是层(layer),它是一种数据处理模块,你可以将它看成数据过滤器。进去一些数据,出来的数据变得更加有用。大多数深度学习都是将简单的层链接起来,从而实现渐进式的数据蒸馏(data distillation)。深度学习模型就像是数据处理的筛子,包含一系列越来越精细的数据过滤器(即层)。

本例中的网络包含 2 个 Dense 层,它们是密集连接(也叫全连接)的神经层。第二层(也是最后一层)是一个 10 路 softmax 层,它将返回一个由 10 个概率值(总和为 1)组成的数组。 每个概率值表示当前数字图像属于 10 个数字类别中某一个的概率。

要想训练网络,我们还需要选择编译(compile)步骤的三个参数:

  1. 损失函数(loss function):网络如何衡量在训练数据上的性能,即网络如何朝着正确的方向前进。
  2. 优化器(optimizer):基于训练数据和损失函数来更新网络的机制。
  3. 在训练和测试过程中需要监控的指标(metric):本例只关心精度,即正确分类的图像所占的比例。
network.compile(optimizer='rmsprop',
loss
='categorical_crossentropy',
metrics
=['accuracy'])

在开始训练之前,我们将对数据进行预处理,将其变换为网络要求的形状,并缩放到所 有值都在 [0, 1] 区间。比如,之前训练图像保存在一个 uint8 类型的数组中,其形状为(60000, 28, 28),取值区间为[0, 255]。我们需要将其变换为一个 float32 数组,其形状为(60000, 28 * 28),取值范围为 0~1。

train_images = train_images.reshape((60000, 28 * 28))
train_images
= train_images.astype('float32') / 255test_images= test_images.reshape((10000, 28 * 28))
test_images
= test_images.astype('float32') / 255

我们还需要对标签进行分类编码。

from keras.utils importto_categorical

train_labels
=to_categorical(train_labels)
test_labels
= to_categorical(test_labels)

现在我们准备开始训练网络,在 Keras 中这一步是通过调用网络的 fit 方法来完成的—— 我们在训练数据上拟合(fit)模型。

>>> network.fit(train_images, train_labels, epochs=5, batch_size=128)
Epoch
1/5 60000/60000 [=============================] - 9s - loss: 0.2524 - acc: 0.9273Epoch2/5 51328/60000 [=======================>.....] - ETA: 1s - loss: 0.1035 - acc: 0.9692

训练过程中显示了两个数字:一个是网络在训练数据上的损失(loss),另一个是网络在训练数据上的精度(acc)。我们很快就在训练数据上达到了 0.989(98.9%)的精度。测试集精度为 97.8%,比训练集精度低不少。训练精度和测试精度之间的这种差距是过拟合(overfit)造成的。过拟合是指机器学习模型在新数据上的性能往往比在训练数据上要差。

训练好网络之后,你希望将其用于实践。你可以用 predict 方法对未见过的数据进行预测。

network.predict(x_test)

你刚刚看到了如何构建和训练一个神经网络,用不到 20 行的 Python 代码对手写数字进行分类。

1)张量

当前所有机器学习系统都使用张量(tensor)作为基本数据结构。张量对这个领域非常重要,重要到 Google 的 TensorFlow 都以它来命名。张量这一概念的核心在于,它是一个数据容器。它包含的数据几乎总是数值数据,因此它是数字的容器。你可能对矩阵很熟悉,它是二维张量。

  1. 仅包含一个数字的张量叫作标量(scalar),也叫标量张量、零维张量、0D 张量。
  2. 数字组成的数组叫作向量(vector)或一维张量(1D 张量),一维张量只有一个轴。
  3. 向量组成的数组叫作矩阵(matrix)或二维张量(2D 张量),矩阵有 2 个轴(通常叫作行和列),形状为:
    1. (samples, features)
      1. 人口统计数据集,其中包括每个人的年龄、邮编和收入。每个人可以表示为包含 3 个值的向量,而整个数据集包含 100 000 个人,因此可以存储在形状为 (100000, 3) 的 2D 张量中。
  4. 将多个矩阵组合成一个新的数组,可以得到一个 3D 张量,可以将其直观地理解为数字组成的立方体,形状为:
    1. (samples, timesteps, features)
      1. 每条推文编码为 280 个字符组成的序列,而每个字符又来自于 128 个字符组成的字母表,包含 100 万条推文的数据集则可以存储在一个形状为 (1000000, 280, 128) 的张量中。
  5. 将多个 3D 张量组合成一个数组,可以创建一个 4D 张量,形状为:
    1. (samples, height, width, channels)
      1. 如果图像大小为 256×256,那么 128 张灰度图像将保存在形状为 (128, 256, 256, 1) 的张量
    2. (samples, channels, height, width)
  6. 以此类推。深度学习处理的一般 是 0D 到 4D 的张量,但处理视频数据时可能会遇到 5D 张量,形状为:
    1. (samples, frames, height, width, channels)
      1. 一个以每秒 4 帧采样的 60 秒视频片段,尺寸为 144×256,共有 240 帧,4 个这样的视频片段将保存在形状为 (4, 240, 144, 256, 3) 的张量中
    2. (samples, frames, channels, height, width)

张量是由以下三个关键属性来定义的:

  1. 轴的个数(阶)。例如,3D 张量有 3 个轴,矩阵有 2 个轴。这在 Numpy 等 Python 库中也叫张量的 ndim。
  2. 形状。这是一个整数元组,表示张量沿每个轴的维度大小(元素个数)。例如,前面矩阵示例的形状为 (3, 5),3D 张量示例的形状为 (3, 3, 5)。向量的形状只包含一个元素,比如 (5,),而标量的形状为空,即 ()。
  3. 数据类型(在 Python 库中通常叫作 dtype)。这是张量中所包含数据的类型,例如,张量的类型可以是 float32、uint8、float64 等。在极少数情况下,你可能会遇到字符(char)张量。

2)张量运算

深度神经网络学到的所有变换也都可以简化为数值数据张量上的一些张量运算(tensor operation),例如加上张量、乘以张量等。在最开始的例子中,我们通过叠加 Dense 层来构建网络。

keras.layers.Dense(512, activation='relu')

这个层可以理解为一个函数,输入一个 2D 张量,返回另一个 2D 张量,即输入张量的新表示。具体而言,这个函数如下所示,其中 W 是一个 2D 张量,b 是一个向量,二者都是该层的属性。

output = relu(dot(W, input) + b)

这里有三个张量运算:输入张量和张量 W 之间的点积运算(dot)、得到的 2D 张量与向量 b 之间的加法运算(+)、最后的 relu 运算。relu(x) 是 max(x, 0)。relu 运算和加法都是逐元素(element-wise)的运算,即该运算独立地应用于张量中的每个元素。

W 和 b 都是张量,均为该层的属性。它们被称为该层的权重(weight)或可训练参数(trainable parameter),分别对应 kernel 和 bias 属性。一开始,这些权重矩阵取较小的随机值,这一步叫作随机初始化(random initialization)。虽然得到的表示是没有意义的,但这是一个起点。下一步则是根据反馈信号逐渐调节这些权重,这 个逐渐调节的过程叫作训练,也就是机器学习中的学习。

3)张量变形

张量变形(tensor reshaping)是指改变张量的行和列,以得到想要的形状。变形后的张量的元素总个数与初始 张量相同。

train_images = train_images.reshape((60000, 28 * 28))

对于张量运算所操作的张量,其元素可以被解释为某种几何空间内点的坐标,因此所有的张量运算都有几何解释。假设有这样两个点:A = [0.5, 1] 和 B = [1, 0.25],将 B 与 A 相加。从几何上来看,相当于将两个向量箭头连在一起,得到的位置表示两个向量之和对应的向量。

通常来说,仿射变换、旋转、缩放等基本的几何操作都可以表示为张量运算。你可以将神经网络解释为高维空间中非常复杂的几何变换,这种变换可以通过许多简单的步骤来实现。

神经网络(或者任何机器学习模型)要做的就是找到可以让纸球恢复平整的变换,从而能够再次让两个类别明确可分。通过深度学习,这一过程可以用三维空间中一系列简单的变换来实现,比如你用手指对纸球做的变换,每次做一个动作。深度网络的每一层都通过变换使数据解开一点点——许多层堆叠在一起,可以实现非常复杂的解开过程。

三、机器学习基础

二分类问题、多分类问题和标量回归问题。这三者都是监督学习(supervised learning)的例子,其目标是学习训练输入与训练目标之间的关系。监督学习是目前最常见的机器学习类型。给定一组样本(通常由人工标注),它可以学会将输入数据映射到已知目标,也叫标注(annotation)。一般来说,近年来广受关注的深度学习应用几乎都属于监督学习,比如光学字符识别、语音识别、图像分类和语言翻译。

1)数据预处理

数据预处理的目的是使原始数据更适于用神经网络处理,包括向量化、标准化、处理缺失值和特征提取。

神经网络的所有输入和目标都必须是浮点数张量(在特定情况下可以是整数张量)。无论处理什么数据(声音、图像还是文本),都必须首先将其转换为张量,这一步叫作数据向量化(data vectorization)。

一般来说,将取值相对较大的数据(比如多位整数,比网络权重的初始值大很多)或异质数据(heterogeneous data,比如数据的一个特征在 0~1 范围内,另一个特征在 100~200 范围内)输入到神经网络中是不安全的。这么做可能导致较大的梯度更新,进而导致网络无法收敛。为了让网络的学习变得更容易,输入数据应该具有以下特征:

  • 取值较小:大部分值都应该在 0~1 范围内。
  • 同质性(homogenous):所有特征的取值都应该在大致相同的范围内。

此外,下面这种更严格的标准化方法也很常见,而且很有用,虽然不一定总是必需的(例如,对于数字分类问题就不需要这么做)。

  • 将每个特征分别标准化,使其平均值为 0。
  • 将每个特征分别标准化,使其标准差为 1。

一般来说,对于神经网络,将缺失值设置为 0 是安全的,只要 0 不是一个有意义的值。网络能够从数据中学到 0 意味着缺失数据,并且会忽略这个值。

2)过拟合与欠拟合

机器学习的根本问题是优化和泛化之间的对立。优化(optimization)是指调节模型以在训练数据上得到最佳性能(即机器学习中的学习),而泛化(generalization)是指训练好的模型在前所未见的数据上的性能好坏。机器学习的目的当然是得到良好的泛化,但你无法控制泛化,只能基于训练数据调节模型。

训练开始时,优化和泛化是相关的:训练数据上的损失越小,测试数据上的损失也越小。这时的模型是欠拟合(underfit)的,即仍有改进的空间,网络还没有对训练数据中所有相关模式建模。但在训练数据上迭代一定次数之后,泛化不再提高,验证指标先是不变,然后开始变差,即模型开始过拟合。这时模型开始学习仅和训练数据有关的模式,但这种模式对新数据来说是错误的或无关紧要的。这种降低过拟合的方法叫作正则化(regularization)。

3)特征工程

特征工程(feature engineering)是指将数据输入模型之前,利用你自己关于数据和机器学习算法(这里指神经网络)的知识对数据进行硬编码的变换(不是模型学到的),以改善模型的效果。多数情况下,一个机器学习模型无法从完全任意的数据中进行学习。呈现给模型的数据应该便于模型进行学习。

假设你想开发一个模型,输入一个时钟图像,模型能够输出对应的时间。

如果你选择用图像的原始像素作为输入数据,那么这个机器学习问题将非常困难。如果将 (x, y) 坐标转换为相对于图像中心的极坐标,这样输入就变成了每个时钟指针的角度 theta,那么现在的特征能使问题变得非常简单,根本不需要机器学习,因为简单的舍入运算和字典查找就足以给出大致的时间。

这就是特征工程的本质:用更简单的方式表述问题,从而使问题变得更容易。它通常需要深入理解问题。

4)通用工作流程

本节将介绍一种可用于解决任何机器学习问题的通用模板。

(1)定义问题,收集数据集,明确了输入、输出以及所使用的数据。

  • 假设输出是可以根据输入进行预测的。
  • 假设可用数据包含足够多的信息,足以学习输入和输出之间的关系。

你收集了包含输入 X 和目标 Y 的很多样例,并不意味着 X 包含足够多的信息来预测 Y。例如,如果你想根据某支股票最近的历史价格来预测其股价走势,那你成功的可能性不大,因为历史价格并没有包含很多可用于预测的信息。请记住,机器学习只能用来记忆训练数据中存在的模式。你只能识别出曾经见过的东西。在过去的数据上训练机器学习来预测未来,这里存在一个假设,就是未来的规律与过去相同,但事实往往并非如此。

(2)选择衡量成功的指标

衡量成功的指标将指引你选择损失函数,即模型要优化什么。它应该直接与你的目标(如业务成功)保持一致。

对于平衡分类问题(每个类别的可能性相同),精度和接收者操作特征曲线下面积(area under the receiver operating characteristic curve,ROC AUC)是常用的指标。对于类别不平衡的问题,你可以使用准确率和召回率。对于排序问题或多标签分类,你可以使用平均准确率均值(mean average precision)。

(3)确定评估方法

一旦明确了目标,你必须确定如何衡量当前的进展,推荐三种常见的评估方法:

  1. 留出验证集。数据量很大时可以采用这种方法。
  2. K 折交叉验证。如果留出验证的样本量太少,无法保证可靠性,那么应该选择这种方法。
  3. 重复的 K 折验证。如果可用的数据很少,同时模型评估又需要非常准确,那么应该使用这种方法。

只需选择三者之一。大多数情况下,第一种方法足以满足要求。

(4)准备数据

首先你应该将数据格式化,使其可以输入到机器学习模型中(这里假设模型为深度神经网络)。

  • 如前所述,应该将数据格式化为张量。
  • 这些张量的取值通常应该缩放为较小的值,比如在 [-1, 1] 区间或 [0, 1] 区间。 ‰
  • 如果不同的特征具有不同的取值范围(异质数据),那么应该做数据标准化。 ‰
  • 你可能需要做特征工程,尤其是对于小数据问题。

(5)开发比基准更好的模型

这一阶段的目标是获得统计功效(statistical power),即开发一个小型模型,它能够打败纯随机的基准(dumb baseline)。注意,不一定总是能获得统计功效。如果你尝试了多种合理架构之后仍然无法打败随机基准,那么原因可能是问题的答案并不在输入数据中。要记住你所做的两个假设。

  1. 假设输出是可以根据输入进行预测的。
  2. 假设可用的数据包含足够多的信息,足以学习输入和输出之间的关系。

如果一切顺利,你还需要选择三个关键参数来构建第一个工作模型。

  1. 最后一层的激活。它对网络输出进行有效的限制。例如,IMDB 分类的例子在最后一层使用了 sigmoid,回归的例子在最后一层没有使用激活,等等。
  2. 损失函数。它应该匹配你要解决的问题的类型。例如,IMDB 的例子使用 binary_ crossentropy、回归的例子使用 mse,等等。
  3. 优化配置。你要使用哪种优化器?学习率是多少?大多数情况下,使用 rmsprop 及其 默认的学习率是稳妥的。

(6)扩大模型规模:开发过拟合的模型

一旦得到了具有统计功效的模型,问题就变成了:模型是否足够强大?它是否具有足够多的层和参数来对问题进行建模?

请记住,机器学习中无处不在的对立是优化和泛化的对立,理想的模型是刚好在欠拟合和过拟合的界线上,在容量不足和容量过大的界线上。要搞清楚你需要多大的模型,就必须开发一个过拟合的模型,这很简单:

  1. 添加更多的层。
  2. 让每一层变得更大。
  3. 训练更多的轮次。

(7)模型正则化与调节超参数

这一步是最费时间的:你将不断地调节模型、训练、在验证数据上评估(这里不是测试数据)、 再次调节模型,然后重复这一过程,直到模型达到最佳性能。你应该尝试以下几项:

  • 添加 dropout。
  • 尝试不同的架构:增加或减少层数。
  • 添加 L1 和 / 或 L2 正则化。
  • 尝试不同的超参数(比如每层的单元个数或优化器的学习率),以找到最佳配置。
  • (可选)反复做特征工程:添加新特征或删除没有信息量的特征。

请注意,每次使用验证过程的反馈来调节模型,都会将有关验证过程的信息泄露到模型中。如果只重复几次,那么无关紧要;但如果系统性地迭代许多次,最终会导致模型对验证过程过拟合(即使模型并没有直接在验证数据上训练),这会降低验证过程的可靠性。

延伸资料:

TensorFlow.js 模型

TensorFlow.js - 根据 2D 数据进行预测

TensorFlow.js 入门指南

tensorflow.js 入门

pipcook

TensorFlow in JavaScript

前端入门机器学习 Tensorflow.js 简明教程

TensorFlow实践

使用 WebRTC 和 TensorFlow.js 监测摸脸次数

本文分享自华为云社区《
Prometheus配置Basic Auth进行安全防护,实现登录控制
》,作者:可以交个朋友。

一、Prometheus Basic Auth 使用背景

在日常prometheus的使用中是没有安全加密措施的,可能会导致监控信息,敏感信息遭遇泄漏。在这种情况下需要保护对Prometheus的访问。

二、方案简介

Prometheus于2.24版本(包括2.24)之后提供Basic Auth功能进行加密访问,在浏览器登录UI的时候需要输入用户密码,访问Prometheus api的时候也需要加上用户密码。

大致步骤如下

  1. 预制用户密码,其中密码使用python3工具包加密
  2. 创建对应用户密码配置文件,修改普罗启动命令(operator场景通过ngress-nginx方式)
  3. 由于Prometheus访问需要认证,如果普罗负载存在探针则修改普罗负载本身的探针配置
  4. 由于Prometheus访问需要认证,如果有grafana,需要修改grafana相关步骤

三、准备工作,预制用户密码,其中密码使用python3工具包加密

因为Basic Auth 需要user、password 信息。访问Prometheus API的所有行为都需要用户名和密码。普罗配置的密码需要加盐加密,可以使用python3工具进行生成,再配置到普罗的相关配置中,作为登录密码认证使用。

安装python3环境

apt install python3-bcrypt

Python 脚本如下

import getpass
import bcrypt

password
= getpass.getpass("password:")
hashed_password
= bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt())
print(hashed_password.decode())

运行脚本,假如我们需要的密码为: test。

执行脚本后,需要我们手动键入需要使用的密码,在终端键入的时候不显示

image-20230817144302442.png

保存密码备用:
$2b$12$kXxrZP74Fmjh6Wih0Ignu.uWSiojl5aKj4UnMvHN9s2h/Lc/ui0.S

四、prometheus操作配置实践操作

在prometheus的日常使用中,通常有三种方式:

  1. 容器化部署在集群中
  2. 通过kube-prometheus部署
  3. 二进制直接在虚机上部署

三种不同方式安装的prometheus,配置basic auth的方式也略有差异。本文将对这三种使用场景进行Basic auth的配置。

4.1 Prometheus容器化部署添加BasicAuth

默认无需用户、密码等认证方式,直接通过服务IP和端口就能访问到prometheus 的queryAPI和UI界面

image-20230817170721016.png

image-20230817170752121.png

如果需要给UI和 prometheus API 添加basic auth,那么该如何做呢?

确认prom的版本信息,低于2.24版本的prometheus 不支持配置Basic Auth

image-20230817171317031.png

存在该启动命令,即可配置basic auth。

创建configmap配置项

准备webconfig.yml文件

basic_auth_users:
admin: $2b$
12$kXxrZP74Fmjh6Wih0Ignu.uWSiojl5aKj4UnMvHN9s2h/Lc/ui0.S

检测webconfig.yml是否可用:

promtool check web-config webconfig.yml

image-20230825093808294.png

复用上述的webconfig.yml

kubectl -n monitoring create configmap webconfig --from-file=webconfig.yml

image-20230817171736361.png

将configmap挂载给prometheus 实例

有多处修改点。

需要修改volume配置,即增加configmap的挂载

volumes:-configMap:
name: webconfig
name: basic
-auth

修改后效果如下:

image-20230817172926583.png

需要将配置挂载给prometheus容器,添加挂载点

volumeMounts:- mountPath: /etc/prometheus/basicauth
name: basic
-auth

修改后效果如下:

image-20230817172953553.png

需要修改启动命令,添加 web.config.file

- --web.config.file=/etc/prometheus/basicauth/webconfig.yml

修改后效果如下:

image-20230817172844465.png

修改完上述配置后,还得看情况继续修改探针配置。

修改探针配置 (如果有健康检查相关配置的话)

同时还需要检查prometheus负载 是否有存活探针livenessProbe,和就绪探针readinessProbe相关配置。如果配置了探针,则需要对探针信息进行修改,添加访问头信息。否则会报错:

image-20230824204142073.png

因为kubelet探针需要访问prometheus接口,进行存活和就绪检测。如果配置了httpGet探针,不对探针进行httpHeaders配置,就会引起pod不断重启,无法正常运行。

image-20230824204620357.png

修改方式如下:

首先需要对 用户名和明文密码进行 Base64编码处理,例如我设置的basicauth信息是:
admin:test

则需要
echo -n "admin:test" |base64 -w0
,编码后的信息为:
YWRtaW46dGVzdA==
,保存备用

image-20230824205308281.png

然后继续编辑Prometheus负载配置:

image-20230824205740549.png

在livenessProbe.httpGet/readinessProbe.httpGet中添加:

httpGet: 
httpHeaders:
-name: Authorization
value: Basic YWRtaW46dGVzdA
==

image-20230824210728316.png
修改完成后,保存退出负载配置。然后手动重启pod实例。

修改完成后,查看prometheus 实例状态。实例就绪

image-20230824266666632082.png

访问prometheus的queryAPI,发现如果不带用户信息,则访问失败。basic auth生效

image-20230824211618671.png

访问Prometheus UI: 需要带用户鉴权,否则无法访问:

image-20230824211744429.png
image-20230824211839428.png

页面密码输入明文即可。

4.2 Prometheus-operator部署场景添加basic auth

当前在k8s 部署prometheus 大都选择kube-prometheus这种形式,配置文件的变更都是交由crd进行管理。统一由prometheus-operator进行识别转换。

image-20230817184612951.png

查看对应的prometheus实例,并访问http API

image-20230817190001543.png

默认是不带鉴权的。

查看prometheus crd 的配置,发现没有web.config.file 相关可以配置的地方

当前kube-prometheus 还不支持配置basic auth,详情可见https://github.com/prometheus-operator/prometheus-operator/issues/5765

规避方案

借助nginx-ingress的能力配置basic auth。nginx ingress 在这方面非常灵活。

image-20230817203143324.png
即创建ingress 来实现。再创建ingress之前,我们需要先准备auth-secret

执行下述命令之前 先安装 工具:
yum -y install httpd

$ htpasswd -c auth admin
New password: test
New password:
Re
-type newpassword:
Adding password
for user admin

效果如下:

image-20230817203541970.png

使用auth文件创建secret:

kubectl create secret generic basic-auth --from-file=auth

image-20230817203814812.png

给prometheus创建ingress访问入口

由于我使用的k8s版本是1.21,ingress的写法会有所不同,1.22以后,
ingress的api也不再是networking.k8s.io/v1beta1

# ingress-prom.yaml
apiVersion: networking.k8s.io
/v1beta1
kind: Ingress
metadata:
annotations:
nginx.ingress.kubernetes.io
/auth-realm: Authentication Required
nginx.ingress.kubernetes.io
/auth-secret: basic-auth
nginx.ingress.kubernetes.io
/auth-type: basic
kubernetes.io
/ingress.class: nginx
name: prometheus
-k8snamespace: monitoring
spec:
rules:
-host: prometheus.example.com
http:
paths:
-backend:
serviceName: prometheus
-k8s
servicePort:
9090path:/pathType: Prefix

image-20230817204202987.png

访问queryAPI&prometheus UI

可以发现queryAPI直接访问已经被限制登录

image-20230817204801398.png

如果带鉴权用户访问则可以获取监控数据

image-20230817204704246.png

同时也可以发现,访问UI已经限制登录鉴权

image-20230817204801398.png

输入用户和密码后即可登录

image-20230817204935160.png

高版本的k8s ingress写法可参考

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
nginx.ingress.kubernetes.io
/auth-realm: Authentication Required
nginx.ingress.kubernetes.io
/auth-secret: basic-auth
nginx.ingress.kubernetes.io
/auth-type: basic
kubernetes.io
/ingress.class: nginx
name: prometheus
-k8snamespace: monitoring
spec:
rules:
-host: prometheus.example.com
http:
paths:
-backend:
service:
name: prometheus
-k8s
port:
name: web
path:
/pathType: Prefix

4.3 Prometheus裸机方式部署添加Basic Auth

创建webconfig.yml

basic_auth_users:
admin: $2b$
12$kXxrZP74Fmjh6Wih0Ignu.uWSiojl5aKj4UnMvHN9s2h/Lc/ui0.S

image-20230817144833637.png

启动prometheus

./prometheus --web.config.file=webconfig.yml --config.file=prometheus.yml

其中 --web.config.file=webconfig.yml 为关键配置,prometheus 启动后会要求 带密码访问
启动成功:

image-20230817145731245.png

访问prometheus UI

要求输入用户密码信息

image-20230817145854252.png

访问prometheus http API接口

image-20230817150044916.png

带用户信息访问:

image-20230817150204288.png

五、Grafana 如何对接鉴权之后的Prometheus

正常来说,如果对Prometheus配置了Basic Auth后,所有需要访问Prometheus的组件均需做出调整,否则无法获取数据。Grafana也不例外

登录Grafana UI, 初次登录需要填写grafana的用户密码,默认是
admin:admin

image-20230824212703932.png

确认数据源配置,并对数据源进行配置

image-20230824213031845.png

配置完成后,点击正下方绿色按钮Test,测试数据源的联通性(不出意外,连接失败)

如果没失败,就万事大吉,无需在往下看了。恭喜你完成对接。

失败了请看下面,如何解决:

image-20230824213249209.png

查看monitoring命名空间中grafana-datasource的configmap配置

kubectl get cm -n monitoring |grep grafana-datasources

image-20230824213541927.png

编辑该配置项:

kubectl edit cm grafana-datasources -n monitoring


editable: false
设置为 :
editable: true

修改的原因是因为 grafana内置了默认的数据源,且数据源不允许修改。我们需要调整这个默认的规定

image-20230824213702984.png

手动重启grafana实例

cm修改完成后,无法动态加载配置,需要手动重启grafana实例加载新配置。

如果grafana没做持久化处理,之前手动配置的dashboard可能会因为重启grafana实例而丢失,建议备份dashboard,dashboard可由grafana页面以json格式导出

image-20230824214658953.png

再次访问Grafana UI

image-20230824215020714.png

查看dashboard信息:

image-20230824215208599.png

Ok,大功告成!

六、总结建议

容器化部署的prometheus 修改逻辑其实和二进制相同。将对应的Basic Auth 信息传递给prometheus,然后启动加载就可以了。低版本Promethues加载失败,会打印错误日志:

unknow long flag '--web.config.file'

要求Prometheus版本不低于2.24。

如果prometheus在部署的时候配置了存活探针和就绪探针,不对探针进行httpHeaders的配置,则会造成prometheus实例无法正常运行。

七 拓展内容

7.1 Grafana 如何取消匿名登录

正常情况下,Grafana的监控信息应该需要用户才能登录显示对接数据源指标的显示。如果谁都能登录查看,容器造成信息泄漏等安全问题

image-20230824215619716.png

如何才能实现Grafana的安全登录功能呢?

查看集群中monitoring ns下的grafana-conf 配置项

kubectl get cm -n monitoring |grep grafana-conf

image-20230824220225466.png

编辑该配置项

kubectl edit cm -n monitoring grafana-conf

根据关键字找到
auth.anonymous
配置,将enabled = true 设置为 fale

image-20230824220449427.png

重启grafana 实例

修改配置后,需要重启grafana实例,加载配置(如果没做持久化处理,注意备份相关dashboard)

刷新grafana页面,重新登录

可以发现,再次登录页面,都需要填写用户信息了

image-20230824220929490.png

7.2 Prometheus BasicAuth 添加多用户

现实场景下,Prometheus的Baisc Auth信息需要配置多个提供给不同团队。

例如我之前添加的basic auth 只用一个鉴权用户:
admin: test
; 现在如果想添加新的鉴权用户:
mike: hello

我们先用python3 工具生成hello 字段加密后的样子:
$2b$12$qhdgpdq669cXNW4DLqRfI.JIBJ0KIvvf0I.I3ccie/tn8d4BxzqV2

此时只需要前往之前设置的webconfig 这个configmap中添加该信息即可:

kubectl edit cm webconfig -n monitoring

image-20230824222610149.png

修改完成后,使用prometheus的热加载命令加载新配置:

curl -u "admin:test" -XPOST http://ip:9090/-/reload

image-20230824223258717.png

多用户访问生效

image-20230824223456609.png

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