2024年6月

最近使用 STM32 测试了一下 lwip 和 esp8266 的网络连接问题,使用 RTThread 的自动网卡时,发现不能很好的自动切换默认网卡,不能满足需求,所以自己简单的改了一下。

一、准备材料

  • MCU:STM32F103ZT6
  • RTThread:5.0.2
  • 工具:RTThread studio
  • 网络芯片:DM9000(FSMC连接)
  • WIFI:ESP8266(uart连接)

二、测试现象

  • 如果默认网卡断开是,无其他网卡联网时,其他网卡重新联网,不会自动切换默认网卡
  • 初始化时,如果默认网卡为初始成功,即时存在能联网的网卡时,也不会切换默认网卡

三、问题原因

遇到问题的时候,我们首先查看一下官方的说明文档:
https://www.rt-thread.org/document/site/#/rt-thread-version/rt-thread-standard/programming-manual/netdev/netdev

找到默认网卡的说明,如下图所示:

从图中不难看出,他只有网卡从 up → down 的时候,才会去切换状态,所以也能解释上面我测试到的现象了

四、解决办法

这里有两个办法可以解决,方式一,直接就该源码;方式二,从新添加相应的处理程序,如下所示:

方式一

程序源码的文件是 rt-thread/components/net/netdev/src/netdev.c,

  1. 先来看一下自动切换网卡的实现方式, 程序如下

    /* Change to the first link_up network interface device automatically */
    static void netdev_auto_change_default(struct netdev *netdev)
    {
        struct netdev *new_netdev = RT_NULL;
    
        if (netdev->flags & NETDEV_FLAG_LINK_UP)
        {
            if (!(netdev_default->flags & NETDEV_FLAG_LINK_UP))
            {
                netdev_set_default(netdev);
            }
            return;
        }
        if (rt_memcmp(netdev, netdev_default, sizeof(struct netdev)) == 0)
        {
            new_netdev = netdev_get_first_by_flags(NETDEV_FLAG_LINK_UP);
            if (new_netdev)
            {
                netdev_set_default(new_netdev);
            }
        }
    }
    

    程序中不难看出切换网卡的思路,确实很厉害,代码也比较简洁,所以切换部分的代码没有问题

  2. 调用的自动切换部分的程序

    /**
     * This function will set network interface device status.
     * @NOTE it can only be called in the network interface device driver.
     *
     * @param netdev the network interface device to change
     * @param is_up the new status
     */
    void netdev_low_level_set_status(struct netdev *netdev, rt_bool_t is_up)
    {
        if (netdev && netdev_is_up(netdev) != is_up)
        {
            if (is_up)
            {
                netdev->flags |= NETDEV_FLAG_UP;
            }
            else
            {
                netdev->flags &= ~NETDEV_FLAG_UP;
    
    #ifdef NETDEV_USING_AUTO_DEFAULT
                /* change to the first link_up network interface device automatically */
                netdev_auto_change_default(netdev);
    #endif /* NETDEV_USING_AUTO_DEFAULT */
            }
    
            /* execute  network interface device status change callback function */
            if (netdev->status_callback)
            {
                netdev->status_callback(netdev, is_up ? NETDEV_CB_STATUS_UP : NETDEV_CB_STATUS_DOWN);
            }
        }
    }
    
    
    /**
     * This function will set network interface device active link status.
     * @NOTE it can only be called in the network interface device driver.
     *
     * @param netdev the network interface device to change
     * @param is_up the new link status
     */
    void netdev_low_level_set_link_status(struct netdev *netdev, rt_bool_t is_up)
    {
        if (netdev && netdev_is_link_up(netdev) != is_up)
        {
            if (is_up)
            {
                netdev->flags |= NETDEV_FLAG_LINK_UP;
    
    #ifdef RT_USING_SAL
                /* set network interface device flags to internet up */
                if (netdev_is_up(netdev) && !ip_addr_isany(&(netdev->ip_addr)))
                {
                    sal_check_netdev_internet_up(netdev);
                }
    #endif /* RT_USING_SAL */
            }
            else
            {
                netdev->flags &= ~NETDEV_FLAG_LINK_UP;
    
                /* set network interface device flags to internet down */
                netdev->flags &= ~NETDEV_FLAG_INTERNET_UP;
    
    #ifdef NETDEV_USING_AUTO_DEFAULT
                /* change to the first link_up network interface device automatically */
                netdev_auto_change_default(netdev);
    #endif /* NETDEV_USING_AUTO_DEFAULT */
            }
    
            /* execute link status change callback function */
            if (netdev->status_callback)
            {
                netdev->status_callback(netdev, is_up ? NETDEV_CB_STATUS_LINK_UP : NETDEV_CB_STATUS_LINK_DOWN);
            }
        }
    }
    

    从程序中不难看出,它是只有当网络状态为 down 的时候,才去调用切换网络状态函数

  3. 解决办法
    只需要将 netdev_low_level_set_link_status 函数中的调用位移,移除 down 的判断条件即可解决这个问题。这个问题解决很简单,但是为啥会是这样的设计我还没弄明白,也能是开发人员粗心了(按理说不应该),也有可能是另有深意,但是不满足我得需求,所以还是需要更改一下。这里我没有改源程序,而是从新写了一个函数去实现的,具体流程如下

方式二

通过提供的 API 重写了一下默认网卡切换的方法,程序如下

#include <rtthread.h>
/* 当需要网卡操作是,需要包含这两个头文件 */
#include <arpa/inet.h>
#include <netdev.h>

#define DBG_TAG "app_net_monitor"
#include <rtdbg.h>

typedef struct net_device_info
{
    const char *name;    // 设备名称
    struct netdev *dev;  // 网络设备指针
} net_device_info_t;

/* 初始网卡设备,第一个为默认网卡 */
static net_device_info_t net_device[] =
{
        {"e0", RT_NULL},
        {"esp0", RT_NULL}
};

/* 默认网卡设备  */
struct netdev *network_default = RT_NULL;

rt_uint16_t net_dev_number = sizeof(net_device)/sizeof(net_device_info_t);


/**
 * @brief 自动更改为第一个link_up网络接口设备
 *
 * @param netdev 网络设备对象
 */
static void network_auto_change_default(struct netdev *netdev)
{
    struct netdev *new_netdev = RT_NULL;

    if (netdev->flags & NETDEV_FLAG_LINK_UP)
    {
        if (!(network_default->flags & NETDEV_FLAG_LINK_UP))
        {
            netdev_set_default(netdev);
            network_default = netdev;
        }
        return;
    }
    if (rt_memcmp(netdev, network_default, sizeof(struct netdev)) == 0)
    {
        new_netdev = netdev_get_first_by_flags(NETDEV_FLAG_LINK_UP);
        if (new_netdev)
        {
            netdev_set_default(new_netdev);
            network_default = new_netdev;
        }
    }
}


/**
 * @brief 网络设备状态回调函数
 *
 * @param netdev 网络设备对象
 * @param type 网络状态类型
 */
void network_status_callback(struct netdev *netdev, enum netdev_cb_type type)
{
    switch (type)
    {
    /* 网络断开 */
    case NETDEV_CB_STATUS_LINK_DOWN:
        network_auto_change_default(netdev);
        break;

    /* 网络连接 */
    case NETDEV_CB_STATUS_INTERNET_UP:
        network_auto_change_default(netdev);
        break;

    default:
        break;
    }
}

/**
 * @brief 返回设备联网状态
 *
 * @return RT_EOK 表示网络连接
 */
rt_bool_t network_is_up()
{
    return netdev_is_link_up(network_default);
}

/**
 * @brief 初始化自动网卡设置
 *
 */
static int app_network_init(void)
{

    for (int i = 0; i < net_dev_number; i++)
    {
        /* 获取网卡设备 */
        net_device[i].dev = netdev_get_by_name(net_device[i].name);
        if (net_device[i].dev == RT_NULL)
        {
            LOG_E("not find network interface device name(%s).\n", net_device[i].name);
            continue;
        }

        /* 设置网络设备状态变化的回调函数 */
        netdev_set_status_callback(net_device[i].dev, network_status_callback);

        /* 设置一个默认网卡  */
        if (network_default == RT_NULL)
        {
            network_default = net_device[i].dev;
            netdev_set_default(network_default);
        }

    }

    return RT_EOK;
}

INIT_APP_EXPORT(app_network_init);

2024年6月17日,我们的运维团队突然收到了一连串的告警。监控大屏上,代表着不同
Sealos
可用区的绿点中,零星地闪烁起了一两个红点。

“奇怪,
怎么有几台服务器突然 hang 住了?
” 值班的小辉皱起了眉头。

这次故障的诡异之处在于它的随机性。并非所有节点都受到影响,而是在不同可用区中,时不时地有个别服务器突然 “失联”。更令人不解的是,这些突然 hang 住的服务器资源都很充足,CPU、内存、磁盘 IO 等各项指标都处于健康水平。

“正常情况下它们不是都会自愈么?” 小谢问道。

“不会,我们已经观察了10分钟,还是没有恢复正常。” 小杨摇摇头,“这不太正常。”

常规手段失效,真凶难寻

面对这种间歇性但严重的故障,我们立即启动了应急预案。首先,
我们祭出了我们的 “传家宝”——dmesg 命令
,希望能从系统日志中找到一些蛛丝马迹。

然而,这次 dmesg 没有给我们任何有价值的信息。日志中看不到任何异常,服务器看起来完全正常,太诡异了。我们对比了之前遇到的所有已知问题,发现这次的情况与以往都不同,问题主要发生在
北京

广州
节点,但发生的概率较低,这更增加了排查的难度。

我们继续深入排查,检查了网络连接、系统负载、进程状态等多个方面,但所有指标都显示正常。

“等等,我发现了一个共同点!” 小李突然喊道,“
所有出问题的节点,内核版本都是 5.15.0-91-generic!

这个发现让我们眼前一亮,难道真凶就藏在这个特定的内核版本中?

意外的 “内核恐慌”

为了进一步确认猜测,我们联系了云服务提供商的技术支持团队。在他们的协助下,我们终于在串口日志中发现了关键线索。

就在系统 hang 住之前,日志中打印了一段特殊的堆栈信息:

这段堆栈信息清楚地显示,系统触发了一个内核 bug,导致了严重的内核恐慌。正常情况下,当发生内核 panic 时,系统应该会崩溃并进入 kdump 流程,将 Vmcore 写入磁盘,然后重新启动。但是,我们的系统却陷入了持续的 hang 状态,这显然不太对劲。

进一步分析 kdump 的日志,我们发现了问题的真相:

原来,kdump 在启动第二内核时出现了异常。我们怀疑,这可能是由于 crashkernel 配置的内存不足,导致第二内核启动失败,系统最终陷入了 hang 状态。

好家伙,
这不就像是消防车在赶往火灾现场的路上自己也出了故障。。。

内核升级大法

既然找到了问题的根源,接下来就是对症下药的时候了。我们的解决方案很直接——升级内核到已经修复了这个 bug 的最新版本:

apt install linux-image-5.15.0-112-generic

然而,事情并没有像我们预想的那样一帆风顺。在升级内核后的恢复过程中,我们又遇到了一个新的挑战——Cilium (我们使用的网络方案) 开始频繁重启,并报出了这样的错误:

Cilium 的小插曲

仔细查看 Cilium 的错误日志,我们发现问题出在一个叫做 “kube-ipvs0” 的网络接口上。这个 “kube-ipvs0” 设备其实是 Kubernetes 的 kube-proxy 组件在使用 IPVS 模式时创建的。

等等,我们的集群不是已经不再使用 kube-proxy 了吗?

经过一番排查后发现,原来
在我们迁移到 Cilium 网络方案的过程中,忘记了清理这个遗留的网络接口
。就是这个小小的疏忽导致了 Cilium 无法正确获取节点上的 IPv4 地址,进而引发了我们遇到的错误。

Cilium 源码中的相关片段:

// https://github.com/cilium/cilium/blob/8c7e442ccd48b9011a10f34a128ec98751d9a80e/pkg/datapath/loader/loader.go#L183
if option.Config.EnableIPv4Masquerade && bpfMasqIPv4Addrs != nil {
    ipv4 := bpfMasqIPv4Addrs[ifName] // nil checking is missing here
    opts["IPV4_MASQUERADE"] = uint64(byteorder.NetIPv4ToHost32(ipv4)) // ipv4 is nil
}

这段代码中,Cilium 尝试获取网络设备的 IPv4 地址,但是由于 “kube-ipvs0” 设备并不包含有效的 IP 地址,导致了空指针异常,最终就会导致 ip 进行类型转换时溢出:

func NetIPv4ToHost32(ip net.IP) uint32 {
        ipv4 := ip.To4()
        _ = ipv4[3] // Assert length of ipv4.
        return Native.Uint32(ipv4)
}

参考 issue:
https://github.com/cilium/cilium/issues/30746

解决方法很简单:删除这个多余的网络接口。

ip link delete kube-ipvs0

完成这个操作后,Cilium 终于恢复了正常,我们的服务也重新上线了。

总结

这次的故障给我们上了很重要的一课:
要保持警惕,不放过任何细节。

即使是一个看似无害的遗留网络设备,也可能成为系统稳定性的隐患。在进行重大架构变更时,我们需要更加细致地清理旧的组件和配置。

defineModel 同学已经转正

defineModel 在vue3.3中还是一个实验性功能,
但是经过一个学期的努力,该同学已经转正。

defineModel的简单介绍

defineModel() 返回的值是一个 ref。
它可以像其他 ref 一样被访问以及修改。
它能起到在父组件和当前变量之间的双向绑定的作用。
它的 .value 和父组件的 v-model 的值同步。
当它被子组件改变时,会触发父组件绑定的值一起更新。

我们都知道 props 的设计原则是单项数据流。
子组件默认情况下是无法更改父组件传递过来的数据。
如果要更改vue3.3以前是通过 $emit 来实现的。
下面我们来对比一下使用 $emit 和 defineModel 来更新数据

使用 $emit更新父组件传递过来的数据(vue3.2)

// 父页面
<template>
  <div>
    <div class="father-box">
      <p>我是父页面-此时:son组件的值{{ flag }}</p>
      <button @click="openHandler"> 显示子组件</button>
    </div>
    <!-- 控制子组件是否显示 -->
    <son v-model:flag="flag"></son>
  </div>
</template>
<script setup lang="ts">
// vue3.2开始组件引入后,就不需要注册啦。变量也不需要暴露出去啦
import son from '@/components/son.vue'
import { ref } from 'vue';
let flag = ref(false)
// 点击事件,更改值,让组件显示出来
const openHandler = ()=>{
  flag.value = true;
}
</script>
<style>
.father-box{
  background: palegreen;
}
</style>
// son组件
<template>
  <div class="son-box" v-if="flag">
    <h1 >我是son组件</h1>
    <button @click="hideHandler"> 关闭组件:更改父组件传递过来的值</button>
  </div>
</template>
<script setup lang="ts">
import { defineProps, defineEmits } from "vue";
// 接收传递过来的值
defineProps({
  flag: {
    type: Boolean,
    default: false,
  }
});
// 注册事件
const emits = defineEmits(["update:flag"]);
// 去更新父组件中的flag值,更改为false
const hideHandler = () => {
  emits("update:flag", false)
}
</script>
<style>
.son-box{
   background: pink;
}
</style>

使用 defineModel 更新父组件传递过来的数据(vue3.4)

// 父页面代码
<template>
  <div>
    <div class="father-box">
      <p>我是父页面-此时:son1组件的值{{ flag }}</p>
      <button @click="openHandler"> 显示子组件</button>
    </div>
    <!-- 控制子组件是否显示 -->
    <son1 v-model:flag="flag"></son1>
  </div>
</template>
<script setup lang="ts">
// vue3.2开始组件引入后,就不需要注册啦。变量也不需要暴露出去啦
import son1 from '@/components/son1.vue'
import { ref } from 'vue';
let flag = ref(false)
// 点击事件,更改值,让组件显示出来
const openHandler = ()=>{
  flag.value = true;
}
</script>
<style>
.father-box{
  background: palegreen;
}
</style>
// son1组件
<template>
  <div class="son-box" v-if="flagBool">
    <h1 >我是son1组件</h1>
    <button @click="hideHandler"> 关闭组件:更改父组件传递过来的值</button>
  </div>
</template>

<script setup lang="ts">
import { defineModel } from "vue";
// defineModel('flag')中的flag必须与传递过来的属性保持一致
// flagBool 是接受的控制变量,可以是任意的
const flagBool = defineModel('flag')
const hideHandler = () => {
  flagBool.value = false
}
</script>
<style>
.son-box{
   background: pink;
}
</style>

采访一下:使用 defineModel 后的感觉

现在使用 defineModel 进行数据的双向绑定更加友好。
比原来更加方便了。爽歪歪!
原来需要再一个合适的时机(事件触发)写上:
emits("update:flag", false)
而现在直接写上 const 变量名 = defineModel('双向绑定的值')
不需要考虑时机

defineModel 传递多个v-model

// 父页面
<template>
  <div>
    <div class="father-box">
      <p>我是父页面-此时:son3组件的值{{ titleName }} {{ address }}</p>
    </div>
    <!-- defineModel 传递多个v-model -->
    <son3 v-model:titleName="titleName" v-model:address="address"></son3>
  </div>
</template>
<script setup lang="ts">
// vue3.2开始组件引入后,就不需要注册啦。变量也不需要暴露出去啦
import son3 from '@/components/son3.vue'
import { ref } from 'vue'
let titleName = ref('少玩手机多看报')
let address = ref('我在这个红绿灯旁边')
</script>
<style>
.father-box{
  background: palegreen;
}
</style>
// 组件son3
<template>
  <div class="son-box">
    <h1 >我是son1组件</h1>
    <input type="text" v-model="titleName"> 
    <input type="text" v-model="address"> 
  </div>
</template>

<script setup lang="ts">
import { defineModel } from "vue";
const titleName = defineModel('titleName')
const address = defineModel('address')
</script>
<style>
.son-box{
   background: pink;
}
</style>

defineModel 设置默认值

// 父页面
<template>
  <div>
    <div class="father-box">
      <!-- 这里获取不到子组件的值 -->
      <p>我是父页面-此时:son3组件的值{{ obj }} </p>
    </div>
    <!-- 没有给子组件传递值 -->
    <son3></son3>
  </div>
</template>
<script setup lang="ts">
import son3 from '@/components/son3.vue'
import { ref } from 'vue'
const obj = ref()
</script>
<style>
.father-box{
  background: palegreen;
}
</style>
<template>
  <div class="son-box">
    <h1 >我是son3组件</h1>
    <h2>{{  detailsObj.name }}</h2>
    <h2>{{  detailsObj.age }}</h2>
    <button @click="changeHandler">改变值</button>
  </div>
</template>

<script setup lang="ts">
import { defineModel } from "vue";
// 定义 参数如类型、默认值
let detailsObj = defineModel('obj', {
  // 初始渲染的时候会显示默认值
  default: { name :'张三', age: 10},
  type: Object,
});
// 改变值
const changeHandler = ()=>{
  console.log(detailsObj)
  detailsObj.value.name = '我是王五',
  detailsObj.value.age = 20
}
</script>
.son-box{
   background: pink;
}
</style>


发现2个问题:父子数据不同步,子组件数据不更新

我们通过给defineModel设置了默认值。
子组件也正确显示了默认值,但是父页面获取不到子组件的值。
这导致导致父组件与子组件之间的数据不同步。这个是我们发现的第1个问题
第2个问题是:设置默认值后,子组件数据在视图中不更新。
这里我们大胆的猜想,是不是跟数据类型有关?
引用数据类型不更新,基本数据类型会更新。

验证: 子组件引用数据类型不更新,基本数据类型会更新

// 父组件
<template>
  <div>
    <div class="father-box">
      <!-- 这里获取不到子组件的值 -->
      <p>我是父页面-此时:son3组件的值{{ age }} </p>
    </div>
    <!-- 没有给子组件传递值 -->
    <son3></son3>
  </div>
</template>
<script setup lang="ts">
import son3 from '@/components/son3.vue'
import { ref } from 'vue'
const age = ref()
</script>
// 子组件
<template>
  <div class="son-box">
    <h1 >我是son3组件</h1>
    <h2>{{  ageValue }}</h2>
    <button @click="changeHandler">改变值</button>
  </div>
</template>
<script setup lang="ts">
import { defineModel } from "vue";
// 这次是一个基本数据类型
let ageValue = defineModel('age', {
  // 初始渲染的时候会显示默认值
  default: 10,
  type: Number,
});
// 改变值,页面会更新吗?
const changeHandler = ()=>{
  console.log(ageValue)
  ageValue.value = 100
}
</script>

总结: defineModel使用默认值会造成2个影响

defineModel使用默认值后:
1.导致父组件与子组件之间的数据不同步
2.如何默认值是应用数据类型,子组件数据在视图中不更新,
  如果默认值是基本数据类型,子组件数据在视图中会更新。
ps: 尽量不要在defineModel中使用默认值。

处理 v-model 修饰符

我们知道了 v-model 有一些内置的修饰符。
例如 .trim, .number, .lazy
有些时候,我们想自定义修饰符。
如:将v-model 绑定输入的字符串值第一个字母转为大写。
我们可以使用 defineModel 的 get 和 set 选项。

v-model 修饰符实现:第一个字母转为大写

// 父组件
<template>
  <div>
    <div class="father-box">
      <!-- 这里获取不到子组件的值 -->
      <p>我是父页面-此时:child组件的值{{ surName }} </p>
    </div>
    <!-- 修饰符 titleCase 在 defineModel 解构的第2个参数中可以拿到 -->
    <child  v-model.titleCase="surName"></child>
  </div>
</template>
<script setup lang="ts">
import child from '@/components/child.vue'
import { ref } from 'vue'
const surName = ref()
</script>
<style>
.father-box{
  background: palegreen;
}
</style>
// 子组件
<template>
  <input type="text" v-model="modelValue" />
</template>
<script setup>
import { defineModel } from 'vue'
// 解构
const [modelValue, modifiers] = defineModel({
  set(value) {
    // 正则表达输入的是否是26个英文
    const regex = /^[a-zA-Z]+$/
    if(regex.test(value) && modifiers.titleCase){
      return value.charAt(0).toUpperCase() + value.slice(1)
    } else {
      // 如果不符合要求返回空字符
      return value
    }
  }
})
console.log(modelValue)
console.log(modifiers)
</script>
<style>
.son-box{
   background: pink;
}
</style>

v-bind的简写语法

// 以前的
<img  :src="src" :alt="alt">
//3.4版本可以简写为
<img  :src :alt>

v-bind的简写语法用在组件传递值上

<template>
  <div>
    // 简写语法
    <son3 :userName :age></son3>
    <!-- 等价与以前这样写 -->
    <!-- <son3 :userName="userName" :age="age"></son3> -->
  </div>
</template>
<script setup lang="ts">
import son3 from '@/components/son3.vue'
import { ref } from 'vue'
const userName = ref('李四')
const age = ref(18)
</script>

更高效的反应性系统 watchEffect

<template>
  <div>
    <h1>分数{{ fraction }}</h1>
    <button @click="onChangeFraction">努力学习,改变分数</button>
  </div>
</template>
<script setup lang="ts">
import { ref ,watchEffect} from 'vue'
const fraction = ref(540)
// 现在fraction的值不发生变化
const onChangeFraction = () => {
  fraction.value = 540
}
// 不会触发watchEffect的回调
watchEffect(() => console.log(fraction.value))
</script>
在 vue3.4 之前,即使计算结果(fraction)保持不变
每次 fraction.value 都将触发 watchEffect 的回调。
而现在只要fraction的值不变化,不会触发watchEffect的回调

编译器性能优化

解析速度提高一倍
解析器从头开始重写,速度快了一倍。
与旧模板相比,它在一半的时间内解析相同的模板。
旧的解析器是一个递归下降解析器,它使用大量正则表达式和低效的前瞻搜索。
新的解析器使用 htmlparser2。
它以线性方式迭代输入,具有最少的前瞻和回溯。
并在很大程度上消除了对正则表达式的依赖。

删除了已弃用的功能

1.全局 JSX 命名空间
从 3.4 开始,Vue 默认不再注册全局 JSX 命名空间。
这是避免与 React 发生全局命名空间冲突,
以便两个库的 TSX 可以共存于同一个项目中。
这应该不会影响使用最新版本的 Volar 的 SFC 的用户。

如果您使用的是 TSX,则有两种选择:
第1种:在升级到 3.4 之前,
在tsconfig.json 中将 jsxImportSource 显式设置为 'vue'。
您还可以通过在文件顶部添加 /* @jsxImportSource vue */ 注释来选择加入每个文件。

第2种:如果您的代码依赖于全局 JSX 命名空间的存在,
例如使用 JSX.Element 类型等.
则可以通过显式引用 vue/jsx 来保留 3.4 之前的确切全局行为,
这将注册全局 JSX 命名空间。
2.其他已删除的功能
1,反应性转换在 3.3 中被标记为不推荐使用,现在在 3.4 中被删除。
2,app.config.unwrapInjectedRef 已被删除。
3,@vnode-xx模板中的事件侦听器现在是编译器错误,而不是弃用警告。请改用 @vue:XXX 侦听器。
4,v-is 指令已被删除。它在 3.3 中已弃用。请改用带 vue: 前缀的 is 属性。

监听子组件的生命周期:@vnode-xx更改为@vue:xxx

在已经删除的功能中,第2点:@vnode-xx更改为@vue:xxx。
有些时候,我们需要去监听子组件的生命周期。
有2种办法:第1种,在子组件的各个生命周期中使用emit抛出方法,然后父组件调用
缺点:第3方组件必须如果没有提供emit的话,我们就可以使用下面这一种
第2种: 在组件中使用@vnode-xx(3.4之前)
现在@vnode--更改为@vue:xxx

监听子组件的生命(vue3.4之前)

<template>
  <div>
    <!-- 在vue3.4之前监听子组件的生命周期可以使用 @vnode-mounted="fn" -->
    <son3 @vnode-mounted="sonMounted"></son3>
  </div>
</template>
<script setup lang="ts">
import son3 from '@/components/son3.vue'
const sonMounted = () =>{
  console.log('组件dom渲染完成')
}
</script>

监听子组件的生命(vue3.4)

<template>
  <div>
    <!-- 现在使用@vue:mounted="fn" @vue:后面是生命周期-->
    <son3 @vue:mounted="mountedDoThing" ></son3>
  </div>
</template>
<script setup lang="ts">
import son3 from '@/components/son3.vue'
const mountedDoThing = () =>{
  console.log('子组件的挂载阶段完成')
}
</script>

监听子组件的生命-奇怪的地方

细心的小伙伴刚刚可能发现了
<son3 @vue:mounted="mountedDoThing" ></son3>
@vue:后面的生命周期是原来vue2的mounted。
为啥不使用vue3的onMounted呢?
因为:如果使用的是onMounted的话,
将无法监听子组件(son3)是否在页面中被挂载了。
这里大家是否会觉得奇怪?
我想了很久,也没有找到原因。机智的小伙伴可以帮我解惑一下
下面我们看下使用vue3的生命周期是否会出发

奇怪的地方:如果使用vue3的生命周期将不会被触发

<template>
  <div>
    <!-- 这里是vue3的onMounted生命周期,
      onMountedDoThing函数将不会被触发
        如果使用的是mounted将会被触发  -->
    <son3  @vue:onMounted="onMountedDoThing"></son3>
  </div>
</template>
<script setup lang="ts">
import son3 from '@/components/son3.vue'
const onMountedDoThing = () => {
  console.log('onMounted不会被触发')
}
</script>

v-is 指令已被删除,改用带vue:前缀的is属性

在vue3.4中,v-is 指令已被删除。
它在 3.3 中已弃用。请改用 is="vue:想替换成的标签"
有些时候,我们想替换某个原生元素。
这个时候我们就可以is来实现。
下面我们将tr标签和p标签替换成li标签
<template>
  <div>
    <ul>
      <tr is="vue:li">tr变成li标签</tr>
      <p is="vue:li">p变成li标签</p>
    </ul>
  </div>
</template>

Vue 3.4 发布地址

https://blog.vuejs.org/posts/vue-3-4#removed-deprecated-features

尾声

如果你觉得我写的不错的话,
请给我点一个推荐,
周末都在写这个,有能力可以给我打赏(手动狗头)
最近想吃亲嘴烧,最好可以喝一瓶水,因为辣条有点辣(手动狗头)

玩区块链,手上没靓号怎么行。用网上的靓号生成器有一定的风险性,思来想去决定自己写一个。
首先需要导入波场官方
编辑

<!--        引用本地Maven仓库-->
        <dependency>
            <groupId>org.tron.trident</groupId>
            <artifactId>utils</artifactId>
            <version>0.8.0</version>
        </dependency>
        <dependency>
            <groupId>org.tron.trident</groupId>
            <artifactId>abi</artifactId>
            <version>0.8.0</version>
        </dependency>

生成很简单,就几行代码。

 /*** 生成地址
*
@return */ public static Map<String, String>createAddress() {
KeyPair keyPair
=KeyPair.generate();
String privateKey
=keyPair.toPrivateKey();
String base58CheckAddress
=keyPair.toBase58CheckAddress();
Map
<String, String> stringMap = new HashMap<>();
stringMap.put(
"address", base58CheckAddress);
stringMap.put(
"privateKey", privateKey);returnstringMap;


启动看了下大概一秒能生成几百个,速度太慢,写多线程来增加速度。
/*** 新建一个线程生成地址
*
@paramminLength*/ public static void runGenerate(intminLength ) {new Thread(()->{while (true){
Map
<String, String> address =createAddress();if ( isPrettyAddress(address.get("address"), minLength) ){
writeText(address.toString());
System.out.println(
"address = " +address);
}
}
}).start();

}
/*** start
*
@paramnum
*
@paramminLength*/ public static void start(int num, intminLength) {for (int i = 1; i <= num; i++) {
System.out.println(
"启动第"+i+"个线程");
runGenerate(minLength);
}
}

通过Java启动参数来控制线程数量以及连号规则。

public static voidmain(String[] args) {if ( args.length<2){
System.out.println(
"请在运行参数上输入线程数量和连号数,请输入整形并以空格分开。如 1 2");
System.exit(
0);
}
int num = Integer.parseInt(args[0]); //线程数量 int minLength = Integer.parseInt(args[1]); //读取一行文本 System.out.println("进程数量:"+num+",生成最小连号:"+minLength+"。程序开始运行。");
AddressService.start(num, minLength);
}

效果图如下:

一.系统环境

本文主要基于Kubernetes1.22.2和Linux操作系统Ubuntu 18.04。

服务器版本 docker软件版本 Kubernetes(k8s)集群版本 CPU架构
Ubuntu 18.04.5 LTS Docker version 20.10.14 v1.22.2 x86_64

Kubernetes集群架构:k8scludes1作为master节点,k8scludes2,k8scludes3作为worker节点。

服务器 操作系统版本 CPU架构 进程 功能描述
k8scludes1/192.168.110.128 Ubuntu 18.04.5 LTS x86_64 docker,kube-apiserver,etcd,kube-scheduler,kube-controller-manager,kubelet,kube-proxy,coredns,calico k8s master节点
k8scludes2/192.168.110.129 Ubuntu 18.04.5 LTS x86_64 docker,kubelet,kube-proxy,calico k8s worker节点
k8scludes3/192.168.110.130 Ubuntu 18.04.5 LTS x86_64 docker,kubelet,kube-proxy,calico k8s worker节点

二.前言

在当今的云计算时代,容器编排技术已成为企业部署和管理应用程序的关键组成部分。Kubernetes 作为最流行的容器编排平台,提供了强大的功能来部署、管理和扩展容器化应用程序。然而,随着应用程序的日益复杂和部署环境的扩大,确保 Kubernetes 集群的安全性变得越来越重要。在这篇博客中,我们将深入了解 Kubernetes 审计的概念,并探讨如何在 Kubernetes 环境中实施审计策略。

使用Kubernetes 审计(Auditing)的
前提
是已经有一套可以正常运行的Kubernetes集群,关于Kubernetes(k8s)集群的安装部署,可以查看博客《Ubuntu 安装部署Kubernetes(k8s)集群》
https://www.cnblogs.com/renshengdezheli/p/17632858.html。

三.Kubernetes 审计简介

Kubernetes 审计(Auditing) 功能提供了与安全相关的、按时间顺序排列的记录集, 记录每个用户、使用 Kubernetes API 的应用以及控制面自身引发的活动。

审计功能使得集群管理员能够回答以下问题:

  • 发生了什么?
  • 什么时候发生的?
  • 谁触发的?
  • 活动发生在哪个(些)对象上?
  • 在哪观察到的?
  • 它从哪触发的?
  • 活动的后续处理行为是什么?

审计记录最初产生于 kube-apiserver 内部。每个请求在不同执行阶段都会生成审计事件;这些审计事件会根据特定策略被预处理并写入后端。 策略确定要记录的内容和用来存储记录的后端,当前的后端支持
日志文件

webhook

每个请求都可被记录其相关的阶段(stage),
阶段(stage)可以理解为什么时候记录
。。已定义的阶段有:

  • RequestReceived
    - 此阶段对应审计处理器接收到请求后, 并且在委托给其余处理器之前生成的事件。
  • ResponseStarted
    - 在响应消息的头部发送后,响应消息体发送前生成的事件。 只有长时间运行的请求(例如 watch)才会生成这个阶段。
  • ResponseComplete
    - 当响应消息体完成并且没有更多数据需要传输的时候。
  • Panic
    - 当 panic 发生时生成。

审计日志记录功能会增加 API server 的内存消耗,因为需要为每个请求存储审计所需的某些上下文。 内存消耗取决于审计日志记录的配置。

Kubernetes 审计是一种监控和记录 Kubernetes 集群中资源操作的方法,用于确保集群的安全性和符合性。通过审计,管理员可以跟踪对集群资源的访问和修改,以便在发生安全事件时进行调查和响应。Kubernetes 提供了审计日志记录的框架,允许管理员自定义审计策略,以确定哪些资源操作应该被记录。

四.审计策略简介

审计策略定义了关于应记录哪些事件以及应包含哪些数据的规则。 审计策略对象结构定义在 audit.k8s.io API 组。 处理事件时,将按顺序与规则列表进行比较。第一个匹配规则设置事件的审计级别(Audit Level),
审计级别(Audit Level)可以理解为记录什么?
已定义的审计级别有:

  • None - 符合这条规则的日志将不会记录。
  • Metadata - 记录请求的元数据(请求的用户、时间戳、资源、动词等等), 但是不记录请求或者响应的消息体。
  • Request - 记录事件的元数据和请求的消息体,但是不记录响应的消息体。 这不适用于非资源类型的请求。
  • RequestResponse - 记录事件的元数据,请求和响应的消息体。这不适用于非资源类型的请求。

你可以使用 --audit-policy-file 标志将包含策略的文件传递给 kube-apiserver。 如果不设置该标志,则不记录事件。 注意 rules 字段必须在审计策略文件中提供。没有(0)规则的策略将被视为非法配置。

审计策略定义了哪些资源操作应该被审计以及审计记录的格式。在 Kubernetes 中,审计策略通过 Admission Controllers 实现,可以通过 Webhook 的方式进行集成。审计策略可以根据资源的类型、操作的类型和用户身份等信息进行过滤,以满足不同场景下的审计需求。

五.启用审计

5.1 引入审计

本次使用etcd2机器作为kubernetes集群的客户端,kctom是kubeconfig文件,etcd2机器使用kctom连接kubernetes集群,现在用户tom没有权限,需要先授权。kubeconfig文件在博客《
Kubernetes(k8s)访问控制:身份认证
》里已经详细介绍过了,这里不再赘述。

[root@etcd2 ~]# ls kc* -lh
-rw------- 1 root root 5.5K 5月   5 17:46 kctom

[root@etcd2 ~]# kubectl get node --kubeconfig=kctom
Error from server (Forbidden): nodes is forbidden: User "tom" cannot list resource "nodes" in API group "" at the cluster scope

去kubernetes集群给tom用户授予cluster-admin权限,也可以授予部分权限。关于授权的详细信息,请查看博客《
Kubernetes(k8s)访问控制:权限管理之RBAC授权/鉴权
》。

root@k8scludes1:~# kubectl create clusterrolebinding crbindtom --clusterrole=cluster-admin --user=tom
clusterrolebinding.rbac.authorization.k8s.io/crbindtom created

授予cluster-admin权限之后,客户端就具有权限了。

[root@etcd2 ~]# kubectl get node --kubeconfig=kctom
NAME         STATUS   ROLES                  AGE   VERSION
k8scludes1   Ready    control-plane,master   66d   v1.22.2
k8scludes2   Ready    <none>                 65d   v1.22.2
k8scludes3   Ready    <none>                 65d   v1.22.2

编辑pod配置文件,表示使用nginx镜像创建pod。

root@k8scludes1:~# vim pod.yaml 

root@k8scludes1:~# cat pod.yaml 
apiVersion: v1
kind: Pod
metadata:
  creationTimestamp: null
  labels:
    run: podtest
  name: podtest
spec:
  #当需要关闭容器时,立即杀死容器而不等待默认的30秒优雅停机时长。
  terminationGracePeriodSeconds: 0
  containers:
  - image: hub.c.163.com/library/nginx:latest
    #imagePullPolicy: IfNotPresent:表示如果本地已经存在该镜像,则不重新下载;否则从远程 Docker Hub 下载该镜像
    imagePullPolicy: IfNotPresent
    name: podtest
    resources: {}
  dnsPolicy: ClusterFirst
  restartPolicy: Always
status: {}

在admissioncontr命名空间创建一个pod。

root@k8scludes1:~# kubectl apply -f pod.yaml 
pod/podtest created

在客户端etcd2机器可以看到这个pod。

[root@etcd2 ~]# kubectl get pod --kubeconfig=kctom -n admissioncontr
NAME      READY   STATUS    RESTARTS   AGE
podtest   1/1     Running   0          34s

在客户端删除pod。

[root@etcd2 ~]# kubectl delete pod podtest --kubeconfig=kctom -n admissioncontr
pod "podtest" deleted

kubectl get ev (查看事件),可以看到pod被删除掉了,但是看不到pod被谁删除掉了!

[root@etcd2 ~]# kubectl get ev --kubeconfig=kctom -n admissioncontr -o wide
LAST SEEN   TYPE     REASON    OBJECT        SUBOBJECT                  SOURCE                MESSAGE                                                                            FIRST SEEN   COUNT   NAME
3m37s       Normal   Pulled    pod/podtest   spec.containers{podtest}   kubelet, k8scludes3   Container image "hub.c.163.com/library/nginx:latest" already present on machine   3m37s        1       podtest.16fab43e14342a3f
3m37s       Normal   Created   pod/podtest   spec.containers{podtest}   kubelet, k8scludes3   Created container podtest                                                          3m37s        1       podtest.16fab43e1db484f4
3m36s       Normal   Started   pod/podtest   spec.containers{podtest}   kubelet, k8scludes3   Started container podtest                                                          3m36s        1       podtest.16fab43e3315ada7
2m47s       Normal   Killing   pod/podtest   spec.containers{podtest}   kubelet, k8scludes3   Stopping container podtest                                                         2m47s        1       podtest.16fab449c468f9af

我们的k8s集群会被各种用户连接使用,我们想知道连接k8s集群的各个用户做了什么操作?应该怎么办?通过审计解决!

5.2 启用审计

创建audit目录存放yaml文件。

root@k8scludes1:~# mkdir audit

root@k8scludes1:~# cd audit/

root@k8scludes1:~/audit# pwd
/root/audit

创建命名空间audit。

root@k8scludes1:~/audit# kubectl create ns audit
namespace/audit created

切换到命名空间audit。

root@k8scludes1:~/audit# kubens audit
Context "kubernetes-admin@kubernetes" modified.
Active namespace is "audit".

root@k8scludes1:~/audit# kubectl get pod 
No resources found in audit namespace.

kubernetes默认并没有启用审计,需要启用审计功能。指定审计策略文件的路径:--audit-policy-file=/etc/kubernetes/audit/audit-policy.yaml

root@k8scludes1:~# vim /etc/kubernetes/manifests/kube-apiserver.yaml 

root@k8scludes1:~# grep audit-policy-file /etc/kubernetes/manifests/kube-apiserver.yaml
    - --audit-policy-file=/etc/kubernetes/audit/audit-policy.yaml

创建审计策略文件,文件内容可以先为空。

root@k8scludes1:~# mkdir /etc/kubernetes/audit/

root@k8scludes1:~# cd /etc/kubernetes/audit/

root@k8scludes1:/etc/kubernetes/audit# touch audit-policy.yaml

root@k8scludes1:/etc/kubernetes/audit# ls /etc/kubernetes/audit/audit-policy.yaml
/etc/kubernetes/audit/audit-policy.yaml

指定审计日志存放的路径:--audit-log-path=/var/log/kubernetes/audit/audit.log 。审计日志格式为
JSONlines 格式

root@k8scludes1:/etc/kubernetes/audit# vim /etc/kubernetes/manifests/kube-apiserver.yaml

root@k8scludes1:/etc/kubernetes/audit# grep audit-log-path /etc/kubernetes/manifests/kube-apiserver.yaml
    - --audit-log-path=/var/log/kubernetes/audit/audit.log

创建审计日志文件。

root@k8scludes1:/etc/kubernetes/audit# mkdir -p /var/log/kubernetes/audit/

root@k8scludes1:/etc/kubernetes/audit# touch /var/log/kubernetes/audit/audit.log

root@k8scludes1:/etc/kubernetes/audit# ls /var/log/kubernetes/audit/audit.log
/var/log/kubernetes/audit/audit.log

审计日志参数如下:

  • --audit-log-path 指定用来写入审计事件的日志文件路径。不指定此标志会禁用日志后端。- 意味着标准化
  • --audit-log-maxage 定义保留旧审计日志文件的最大天数
  • --audit-log-maxbackup 定义要保留的审计日志文件的最大数量
  • --audit-log-maxsize 定义审计日志文件轮转之前的最大大小(兆字节)

配置审计日志的参数。

root@k8scludes1:/etc/kubernetes/audit# vim /etc/kubernetes/manifests/kube-apiserver.yaml

root@k8scludes1:/etc/kubernetes/audit# grep audit /etc/kubernetes/manifests/kube-apiserver.yaml
    - --audit-policy-file=/etc/kubernetes/audit/audit-policy.yaml
    - --audit-log-path=/var/log/kubernetes/audit/audit.log
    - --audit-log-maxage=365
    - --audit-log-maxbackup=730
    - --audit-log-maxsize=250

如果你的集群控制面以 Pod 的形式运行 kube-apiserver,需要通过 hostPath 数据卷来访问策略文件和日志文件所在的目录,这样审计记录才会持久保存下来。

现在/etc/kubernetes/audit/audit-policy.yaml和/var/log/kubernetes/audit/audit.log在宿主机里是存在的,但是kube-apiserver是以pod的方式运行的,pod里并不存在这两个文件,需要通过hostPath 数据卷进行映射。

/etc/kubernetes/manifests/kube-apiserver.yaml所有的修改如下截图:

image-20240621110718241

注意:/var/log/kubernetes/audit/的readOnly为false,否则不能写入日志!

image-20240621110746808

自此,成功启用审计了,下面开始配置审计策略。

六.审计策略

6.1 记录审计阶段为:ResponseStarted,审计级别为Metadata,apiVersion为group: "" 的日志

先不用重启kubelet,因为审计策略文件还没写好。

现在编写审计策略文件,该审计策略表示什么都不记录。

root@k8scludes1:~# vim /etc/kubernetes/audit/audit-policy.yaml

root@k8scludes1:~# cat /etc/kubernetes/audit/audit-policy.yaml
apiVersion: audit.k8s.io/v1 # This is required.
kind: Policy
# Don't generate audit events for all requests in RequestReceived stage.
omitStages:
  - "ResponseStarted"
rules:
  - level: None
    resources:
    - group: ""

重启kubelet使配置生效。

root@k8scludes1:~# systemctl restart kubelet

root@k8scludes1:~# kubectl get node
NAME         STATUS   ROLES                  AGE   VERSION
k8scludes1   Ready    control-plane,master   66d   v1.22.2
k8scludes2   Ready    <none>                 66d   v1.22.2
k8scludes3   Ready    <none>                 66d   v1.22.2

在客户端etcd2机器执行命令:kubectl get ev --kubeconfig=kctom -n admissioncontr -o wide。

[root@etcd2 ~]# kubectl get ev --kubeconfig=kctom -n admissioncontr -o wide
No resources found in admissioncontr namespace.

查看审计日志,没有任何日志记录。

root@k8scludes1:~# cat /var/log/kubernetes/audit/audit.log 

审计级别修改为Metadata,表示记录请求的元数据(请求的用户、时间戳、资源、动词等等), 但是不记录请求或者响应的消息体。

root@k8scludes1:~# vim /etc/kubernetes/audit/audit-policy.yaml

root@k8scludes1:~# cat /etc/kubernetes/audit/audit-policy.yaml
apiVersion: audit.k8s.io/v1 # This is required.
kind: Policy
# Don't generate audit events for all requests in RequestReceived stage.
omitStages:
  - "ResponseStarted"
rules:
  - level: Metadata
    resources:
    - group: ""

重启kubelet使配置生效。

root@k8scludes1:~# systemctl daemon-reload ; systemctl restart kubelet

在客户端etcd2机器执行命令:kubectl get ev --kubeconfig=kctom -n admissioncontr -o wide 。

[root@etcd2 ~]# kubectl get ev --kubeconfig=kctom -n admissioncontr -o wide
No resources found in admissioncontr namespace.

查看审计日志,发现还是没有日志产生,看来重启kubelet还不行,现在重启机器。

root@k8scludes1:~# cat /var/log/kubernetes/audit/audit.log

现在直接重启k8scludes1机器,k8s worker节点不用重启。

root@k8scludes1:~# reboot

重启k8scludes1机器之后,现在/var/log/kubernetes/audit/audit.log里有很多数据,先删除数据,然后客户端执行命令,查看审计日志。

root@k8scludes1:~# cat /var/log/kubernetes/audit/audit.log | wc -l
3317

清空日志。

root@k8scludes1:~# >/var/log/kubernetes/audit/audit.log

客户端执行kubectl get ev --kubeconfig=kctom -n admissioncontr -o wide命令。

[root@etcd2 ~]# kubectl get ev --kubeconfig=kctom -n admissioncontr -o wide
No resources found in admissioncontr namespace.

现在不停的产生日志。

root@k8scludes1:~# cat /var/log/kubernetes/audit/audit.log | wc -l
32

这是最新的日志。

root@k8scludes1:~# tail -1 /var/log/kubernetes/audit/audit.log
{"kind":"Event","apiVersion":"audit.k8s.io/v1","level":"Metadata","auditID":"72736d33-508f-4279-89a5-d95aea7b98c5","stage":"ResponseComplete","requestURI":"/api/v1/namespaces/ingress-nginx/configmaps/ingress-controller-leader","verb":"update","user":{"username":"system:serviceaccount:ingress-nginx:ingress-nginx","uid":"70a36f2c-225f-450d-849e-2432db224f40","groups":["system:serviceaccounts","system:serviceaccounts:ingress-nginx","system:authenticated"],"extra":{"authentication.kubernetes.io/pod-name":["ingress-nginx-controller-684bbc4b45-jmpxk"],"authentication.kubernetes.io/pod-uid":["308fdf30-854b-49f7-b041-4e5d770c0667"]}},"sourceIPs":["192.168.110.130"],"userAgent":"nginx-ingress-controller/v1.0.0 (linux/amd64) ingress-nginx/041eb167c7bfccb1d1653f194924b0c5fd885e10","objectRef":{"resource":"configmaps","namespace":"ingress-nginx","name":"ingress-controller-leader","uid":"db625974-8e33-4bd6-a2e2-1816301af942","apiVersion":"v1","resourceVersion":"2739119"},"responseStatus":{"metadata":{},"code":200},"requestReceivedTimestamp":"2022-06-22T13:03:36.751232Z","stageTimestamp":"2022-06-22T13:03:36.759442Z","annotations":{"authorization.k8s.io/decision":"allow","authorization.k8s.io/reason":"RBAC: allowed by RoleBinding \"ingress-nginx/ingress-nginx\" of Role \"ingress-nginx\" to ServiceAccount \"ingress-nginx/ingress-nginx\"","mutation.webhook.admission.k8s.io/round_0_index_1":"{\"configuration\":\"gatekeeper-mutating-webhook-configuration\",\"webhook\":\"mutation.gatekeeper.sh\",\"mutated\":false}"}}

客户端不执行命令就产生了太多审计日志了,需要修改审计策略。

root@k8scludes1:~# cat /var/log/kubernetes/audit/audit.log | wc -l
1174

6.2 只记录audit命名空间里的日志

修改审计策略,现在配置只记录某个命名空间里的审计日志,namespaces: ["audit"]表示只记录audit命名空间里的日志。

root@k8scludes1:~# vim /etc/kubernetes/audit/audit-policy.yaml 

root@k8scludes1:~# cat /etc/kubernetes/audit/audit-policy.yaml
apiVersion: audit.k8s.io/v1 # This is required.
kind: Policy
# Don't generate audit events for all requests in RequestReceived stage.
omitStages:
  - "ResponseStarted"
rules:
  - level: Metadata
    resources:
    - group: ""
    namespaces: ["audit"]

重启kubelet使其生效,但是审计策略没有生效,重启机器。

root@k8scludes1:~# systemctl restart kubelet

重启k8s master节点。

root@k8scludes1:~# reboot

root@k8scludes1:~# kubectl get node
NAME         STATUS   ROLES                  AGE   VERSION
k8scludes1   Ready    control-plane,master   66d   v1.22.2
k8scludes2   Ready    <none>                 66d   v1.22.2
k8scludes3   Ready    <none>                 66d   v1.22.2

重启k8s master节点之后,清空日志。

root@k8scludes1:~# >/var/log/kubernetes/audit/audit.log

客户端执行kubectl get pod --kubeconfig=kctom -n default -o wide。

[root@etcd2 ~]# kubectl get pod --kubeconfig=kctom  -n default -o wide
No resources found in default namespace.

客户端执行 kubectl get pod --kubeconfig=kctom -n audit -o wide。

[root@etcd2 ~]# kubectl get pod --kubeconfig=kctom  -n audit -o wide
No resources found in audit namespace.

查看审计日志,只记录了audit命名空间的操作,default命名空间的操作没有记录。

root@k8scludes1:~# tail -2 /var/log/kubernetes/audit/audit.log
{"kind":"Event","apiVersion":"audit.k8s.io/v1","level":"Metadata","auditID":"08b8fca7-8390-4f59-8a21-6ff3541fd2a2","stage":"RequestReceived","requestURI":"/api/v1/namespaces/audit/pods?limit=500","verb":"list","user":{"username":"tom","groups":["normalusertom","system:authenticated"]},"sourceIPs":["192.168.110.131"],"userAgent":"kubectl/v1.23.1 (linux/amd64) kubernetes/86ec240","objectRef":{"resource":"pods","namespace":"audit","apiVersion":"v1"},"requestReceivedTimestamp":"2022-06-22T13:26:08.732751Z","stageTimestamp":"2022-06-22T13:26:08.732751Z"}
{"kind":"Event","apiVersion":"audit.k8s.io/v1","level":"Metadata","auditID":"08b8fca7-8390-4f59-8a21-6ff3541fd2a2","stage":"ResponseComplete","requestURI":"/api/v1/namespaces/audit/pods?limit=500","verb":"list","user":{"username":"tom","groups":["normalusertom","system:authenticated"]},"sourceIPs":["192.168.110.131"],"userAgent":"kubectl/v1.23.1 (linux/amd64) kubernetes/86ec240","objectRef":{"resource":"pods","namespace":"audit","apiVersion":"v1"},"responseStatus":{"metadata":{},"code":200},"requestReceivedTimestamp":"2022-06-22T13:26:08.732751Z","stageTimestamp":"2022-06-22T13:26:08.741665Z","annotations":{"authorization.k8s.io/decision":"allow","authorization.k8s.io/reason":"RBAC: allowed by ClusterRoleBinding \"crbindtom\" of ClusterRole \"cluster-admin\" to User \"tom\""}}

6.3 只记录audit命名空间的pods操作日志

修改审计策略,该审计策略表示只记录audit命名空间的pods操作。

root@k8scludes1:~# vim /etc/kubernetes/audit/audit-policy.yaml 

root@k8scludes1:~# cat /etc/kubernetes/audit/audit-policy.yaml
apiVersion: audit.k8s.io/v1 # This is required.
kind: Policy
# Don't generate audit events for all requests in RequestReceived stage.
omitStages:
  - "ResponseStarted"
rules:
  - level: Metadata
    resources:
    - group: ""
      resources: ["pods"]
    namespaces: ["audit"]

重启机器使审计策略生效。

root@k8scludes1:~# reboot

重启之后,清空日志。

root@k8scludes1:~# >/var/log/kubernetes/audit/audit.log 

在客户端执行kubectl get pod --kubeconfig=kctom -n audit -o wide。

[root@etcd2 ~]# kubectl get pod --kubeconfig=kctom  -n audit -o wide
No resources found in audit namespace.

在客户端执行 kubectl get svc --kubeconfig=kctom -n audit -o wide 。

[root@etcd2 ~]# kubectl get svc --kubeconfig=kctom  -n audit -o wide
No resources found in audit namespace.

查看日志,发现只记录了audit命名空间的pod操作,svc操作没有记录。

root@k8scludes1:~# cat /var/log/kubernetes/audit/audit.log
{"kind":"Event","apiVersion":"audit.k8s.io/v1","level":"Metadata","auditID":"4748e76d-e9cb-46de-af28-d467171ca105","stage":"RequestReceived","requestURI":"/api/v1/namespaces/audit/pods?limit=500","verb":"list","user":{"username":"tom","groups":["normalusertom","system:authenticated"]},"sourceIPs":["192.168.110.131"],"userAgent":"kubectl/v1.23.1 (linux/amd64) kubernetes/86ec240","objectRef":{"resource":"pods","namespace":"audit","apiVersion":"v1"},"requestReceivedTimestamp":"2022-06-23T00:24:45.351036Z","stageTimestamp":"2022-06-23T00:24:45.351036Z"}
{"kind":"Event","apiVersion":"audit.k8s.io/v1","level":"Metadata","auditID":"4748e76d-e9cb-46de-af28-d467171ca105","stage":"ResponseComplete","requestURI":"/api/v1/namespaces/audit/pods?limit=500","verb":"list","user":{"username":"tom","groups":["normalusertom","system:authenticated"]},"sourceIPs":["192.168.110.131"],"userAgent":"kubectl/v1.23.1 (linux/amd64) kubernetes/86ec240","objectRef":{"resource":"pods","namespace":"audit","apiVersion":"v1"},"responseStatus":{"metadata":{},"code":200},"requestReceivedTimestamp":"2022-06-23T00:24:45.351036Z","stageTimestamp":"2022-06-23T00:24:45.434147Z","annotations":{"authorization.k8s.io/decision":"allow","authorization.k8s.io/reason":"RBAC: allowed by ClusterRoleBinding \"clusterrolebinding1\" of ClusterRole \"clusterole1\" to User \"tom\""}}

6.4 只记录audit命名空间的pods,services,deployments操作日志

编辑审计策略文件,表示只记录audit命名空间的pods,services,deployments操作,因为deployments的apiVersion的父级为apps,所以需要group: "apps" 。

root@k8scludes1:~# vim /etc/kubernetes/audit/audit-policy.yaml 

root@k8scludes1:~# cat /etc/kubernetes/audit/audit-policy.yaml
apiVersion: audit.k8s.io/v1 # This is required.
kind: Policy
# Don't generate audit events for all requests in RequestReceived stage.
omitStages:
  - "ResponseStarted"
rules:
  - level: Metadata
    resources:
    - group: ""
      resources: ["pods","services"]
    - group: "apps"
      resources: ["deployments"]
    namespaces: ["audit"]

6.5 只记录audit命名空间的pods操作,审计级别为RequestResponse

编辑审计策略文件,表示只记录audit命名空间的pods操作,审计级别为RequestResponse,记录事件的元数据,请求和响应的消息体。

root@k8scludes1:~# vim /etc/kubernetes/audit/audit-policy.yaml

root@k8scludes1:~# cat /etc/kubernetes/audit/audit-policy.yaml
apiVersion: audit.k8s.io/v1 # This is required.
kind: Policy
# Don't generate audit events for all requests in RequestReceived stage.
omitStages:
  - "ResponseStarted"
rules:
  - level: RequestResponse
    resources:
    - group: ""
      resources: ["pods"]
    namespaces: ["audit"]

重启机器使审计策略生效。

root@k8scludes1:~# reboot

创建一个pod。

root@k8scludes1:~# kubectl apply -f pod.yaml 
pod/podtest created

root@k8scludes1:~# kubectl get pod
NAME      READY   STATUS    RESTARTS   AGE
podtest   1/1     Running   0          5s

清空审计日志。

root@k8scludes1:~# >/var/log/kubernetes/audit/audit.log 

客户端执行kubectl get pod --kubeconfig=kctom -n audit -o wide 。

[root@etcd2 ~]# kubectl get pod --kubeconfig=kctom  -n audit -o wide
NAME      READY   STATUS    RESTARTS   AGE   IP            NODE         NOMINATED NODE   READINESS GATES
podtest   1/1     Running   0          48s   10.244.1.97   k8scludes3   <none>           <none>

查看审计日志,记录的内容很丰富,元数据,请求和响应的消息体都显示出来了。

root@k8scludes1:~# cat /var/log/kubernetes/audit/audit.log
{"kind":"Event","apiVersion":"audit.k8s.io/v1","level":"RequestResponse","auditID":"d6836f9b-4918-49f2-80dc-ad02e246949b","stage":"RequestReceived","requestURI":"/api/v1/namespaces/audit/pods?limit=500","verb":"list","user":{"username":"tom","groups":["normalusertom","system:authenticated"]},"sourceIPs":["192.168.110.131"],"userAgent":"kubectl/v1.23.1 (linux/amd64) kubernetes/86ec240","objectRef":{"resource":"pods","namespace":"audit","apiVersion":"v1"},"requestReceivedTimestamp":"2022-06-23T00:43:30.782241Z","stageTimestamp":"2022-06-23T00:43:30.782241Z"}
......
23T00:42:46Z","fieldsType":"FieldsV1","fieldsV1":{"f:status":{"f:conditions":{".":{},"k:{\"type\":\"ContainersReady\"}":{".":{},"f:lastProbeTime":{},"f:lastTransitionTime":{},"f:status":{},"f:type":{}},"k:{\"type\":\"Initialized\"}":{".":{},"f:lastProbeTime":{},"f:lastTransitionTime":{},"f:status":{},"f:type":{}},"k:{\"type\":\"PodScheduled\"}":{".":{},"f:lastProbeTime":{},"f:lastTransitionTime":{},"f:status":{},"f:type":{}},"k:{\"type\":\"Ready\"}":{".":{},"f:lastProbeTime":{},"f:lastTransitionTime":{},"f:status":{},"f:type":{}}},"f:containerStatuses":{},"f:hostIP":{},"f:phase":{},"f:podIP":{},"f:podIPs":{".":{},"k:{\"ip\":\"10.244.1.97\"}":{".":{},"f:ip":{}}},"f:startTime":{}}},"subresource":"status"}]}}}]},"requestReceivedTimestamp":"2022-06-23T00:43:30.782241Z","stageTimestamp":"2022-06-23T00:43:30.785883Z","annotations":{"authorization.k8s.io/decision":"allow","authorization.k8s.io/reason":"RBAC: allowed by ClusterRoleBinding \"clusterrolebinding1\" of ClusterRole \"clusterole1\" to User \"tom\""}}

6.6 只记录audit命名空间下的tom用户的pods操作,其他用户操作不记录

编辑审计策略文件,设置审计策略:只记录audit命名空间下的tom用户的pods操作,其他用户的操作不记录。

root@k8scludes1:~# vim /etc/kubernetes/audit/audit-policy.yaml 

root@k8scludes1:~# cat /etc/kubernetes/audit/audit-policy.yaml
apiVersion: audit.k8s.io/v1 # This is required.
kind: Policy
# Don't generate audit events for all requests in RequestReceived stage.
omitStages:
  - "ResponseStarted"
rules:
  - level: Metadata
    users: ["tom"]
    resources:
    - group: ""
      resources: ["pods"]
    namespaces: ["audit"]

重启机器使审计策略生效。

root@k8scludes1:~# reboot 

清空日志。

root@k8scludes1:~# >/var/log/kubernetes/audit/audit.log 

使用管理员用户查看pod。

root@k8scludes1:~# kubectl get pod -n audit
NAME      READY   STATUS    RESTARTS   AGE
podtest   1/1     Running   0          29m

使用tom用户在客户端查看pod 。

[root@etcd2 ~]# kubectl get pod --kubeconfig=kctom  -n audit -o wide
NAME      READY   STATUS    RESTARTS   AGE   IP            NODE         NOMINATED NODE   READINESS GATES
podtest   1/1     Running   0          29m   10.244.1.97   k8scludes3   <none>           <none>

查看日志,可以发现只记录了tom用户的pod查询操作。

root@k8scludes1:~# cat /var/log/kubernetes/audit/audit.log
{"kind":"Event","apiVersion":"audit.k8s.io/v1","level":"Metadata","auditID":"a70f1b39-3c54-4ed8-ab45-9cbb7b4688ef","stage":"RequestReceived","requestURI":"/api/v1/namespaces/audit/pods?limit=500","verb":"list","user":{"username":"tom","groups":["normalusertom","system:authenticated"]},"sourceIPs":["192.168.110.131"],"userAgent":"kubectl/v1.23.1 (linux/amd64) kubernetes/86ec240","objectRef":{"resource":"pods","namespace":"audit","apiVersion":"v1"},"requestReceivedTimestamp":"2022-06-23T01:12:24.103036Z","stageTimestamp":"2022-06-23T01:12:24.103036Z"}
{"kind":"Event","apiVersion":"audit.k8s.io/v1","level":"Metadata","auditID":"a70f1b39-3c54-4ed8-ab45-9cbb7b4688ef","stage":"ResponseComplete","requestURI":"/api/v1/namespaces/audit/pods?limit=500","verb":"list","user":{"username":"tom","groups":["normalusertom","system:authenticated"]},"sourceIPs":["192.168.110.131"],"userAgent":"kubectl/v1.23.1 (linux/amd64) kubernetes/86ec240","objectRef":{"resource":"pods","namespace":"audit","apiVersion":"v1"},"responseStatus":{"metadata":{},"code":200},"requestReceivedTimestamp":"2022-06-23T01:12:24.103036Z","stageTimestamp":"2022-06-23T01:12:24.107872Z","annotations":{"authorization.k8s.io/decision":"allow","authorization.k8s.io/reason":"RBAC: allowed by ClusterRoleBinding \"crbindtom\" of ClusterRole \"cluster-admin\" to User \"tom\""}}

6.7 rules规则是从上往下匹配的,第一条规则已经匹配了,第二条就不匹配了

编辑审计策略文件,有两条策略,一条是:apiVersion为group: ""的操作不记录日志,另外一条是:只记录audit命名空间下tom用户的pod操作。

root@k8scludes1:~# vim /etc/kubernetes/audit/audit-policy.yaml

root@k8scludes1:~# cat /etc/kubernetes/audit/audit-policy.yaml
apiVersion: audit.k8s.io/v1 # This is required.
kind: Policy
# Don't generate audit events for all requests in RequestReceived stage.
omitStages:
  - "ResponseStarted"
rules:
  #apiVersion为group: ""的操作不记录日志
  - level: None
    resources:
    - group: ""
  #只记录audit命名空间下tom用户的pod操作
  - level: Metadata
    users: ["tom"]
    resources:
    - group: ""
      resources: ["pods"]
    namespaces: ["audit"]

重启机器使配置生效。

root@k8scludes1:~# reboot 

清空日志。

root@k8scludes1:~# >/var/log/kubernetes/audit/audit.log 

使用管理员用户查询audit命名空间的pod。

root@k8scludes1:~# kubectl get pod -n audit
NAME      READY   STATUS    RESTARTS   AGE
podtest   1/1     Running   0          37m

使用tom用户在客户端机器etcd2上查询audit命名空间的pod。

[root@etcd2 ~]# kubectl get pod --kubeconfig=kctom  -n audit -o wide
NAME      READY   STATUS    RESTARTS   AGE   IP            NODE         NOMINATED NODE   READINESS GATES
podtest   1/1     Running   0          37m   10.244.1.97   k8scludes3   <none>           <none>

查看审计日志,可以发现在客户端使用tom用户查询pod和使用管理员用户查看pod都没有生成审计日志,rules规则是从上往下匹配的,第一条规则已经匹配了,第二条就不匹配了。

root@k8scludes1:~# cat /var/log/kubernetes/audit/audit.log

6.8 在 Metadata 级别为所有请求生成日志

编辑审计策略文件,在 Metadata 级别为所有请求生成日志。

root@k8scludes1:~# vim /etc/kubernetes/audit/audit-policy.yaml 

root@k8scludes1:~# cat /etc/kubernetes/audit/audit-policy.yaml
apiVersion: audit.k8s.io/v1beta1
kind: Policy
rules:
- level: Metadata

七.总结

在 Kubernetes 1.22.2 环境中实施审计策略可以帮助管理员监控和记录集群中的资源操作,确保集群的安全性和符合性。通过启用审计 Admission Controller 和配置相应的审计策略,我们可以灵活地控制审计记录的格式和范围。