2024年1月

介绍

dotnet-dump 是 .NET Core 官方工具之一,用于生成和分析 .NET Core 进程的转储文件(dump file)。它可以帮助开发人员在应用程序发生故障或性能问题时进行故障排查和诊断。

Linux 系统上的软件包的工具

  • RedHat系列使用(Centos)
    yum
  • Debian系列使用
    apt

dotnet-dump安装

先安装dotnet-sdk

  1. 将 Microsoft 的官方包存储库配置文件安装到 CentOS 7 系统中
    sudo rpm -Uvh https://packages.microsoft.com/config/centos/7/packages-microsoft-prod.rpm
  2. 安装运行时
    sudo yum install dotnet-sdk-7.0
  3. 查看当前版本-是否安装成功
    dotnet --version
    image

安装dotnet-dump

安装最新的即可,我的代码是asp.core3.1 也可以高版本排查
dotnet tool install --global dotnet-dump
查看当前版本-是否安装成功
dotnet-dump --version
image

生成转储文件(内存文件)

dotnet-dump collect -p 10232

分析转储文件

dotnet-dump analyze core_20231222_201626
image

分析SOS命令

  1. 找到内存比较大的类型,通过查看内存占用大小和对象数量
    dumpheap -stat
    默认从小到大,直接拉到最下面,看最大的对象
    image
  2. 然后分析类型具体对象
    umpheap -mt 命令,您可以快速查找指定类型的对象,了解其在堆上的分布情况和内存占用情况。这对于定位内存泄漏、查找内存使用问题等非常有用。
    dumpheap 7f9d28ec8b68
    image
  3. 然后找出的应用根(目的是找出在哪里被引用了)
    gcroot 7f9a14da0448
    image

分析生产环境dump机器配置要高一点,4G大小的文件跑崩了

image

前言


公众号每月定期推广和分享的C#/.NET/.NET Core优秀项目和框架(公众号每周至少推荐两个优秀的项目和框架当然节假日除外),公众号推文有项目和框架的介绍、功能特点以及部分功能截图等(打不开或者打开GitHub很慢的同学可以优先查看公众号推文,文末一定会附带项目和框架源码地址)。注意:排名不分先后,都是十分优秀的开源项目和框架,每周定期更新分享(欢迎关注公众号:
追逐时光者
,第一时间获取每周精选分享资讯

KNN
(K-近邻),全称
K-Nearest Neighbors
,是一种常用的
分类算法

KNN
算法的历史可以追溯到
1957年
,当时Cover和Hart提出了“最近邻分类”的概念。
但是,这个算法真正得到广泛认知和应用是在
1992年
,由Altman发表的一篇名为“
K-Nearest Neighbors
”的文章。

近年来,随着大数据和机器学习的快速发展,
KNN
算法因其简单且表现优秀,被广泛应用于各种数据分类问题中。

1. 算法概述

KNN
算法的基本原理是:在特征空间中,如果一个样本的最接近的
k个邻居
中大多数属于某一个类别,则该样本也属于这个类别。
换句话说,
KNN
算法假设类别是由其邻居决定的。

那么,
KNN
算法判断数据是否相似是关键,也就是数据之间的距离是如何计算的呢?
最常用的距离计算公式有:

  1. 曼哈顿距离

    \(L_1(x_i,x_j)= \sum_{l=1}^{n} |x_i^{(l)}-x_j^{(l)}|\)
  2. 欧氏距离

    \(L_2(x_i,x_j) = (\sum_{l=1}^{n} \; |x_i^{(l)}-x_j^{(l)}|^{2})^{\frac{1}{2}}\)
  3. 闵可夫斯基距离

    \(L_p(x_i,x_j) = (\sum_{l=1}^{n} \; |x_i^{(l)}-x_j^{(l)}|^{2})^{\frac{1}{p}}\)
  4. 等等

使用不同的距离,就会得到不同的分类效果。

2. 创建样本数据

这次用
scikit-learn
中的样本生成器
make_classification
来生成分类用的样本数据。

import matplotlib.pyplot as plt
from sklearn.datasets import make_classification

# 分类数据的样本生成器
X, y = make_classification(n_samples=1000, n_classes=4, n_clusters_per_class=1)
plt.scatter(X[:, 0], X[:, 1], marker="o", c=y, s=25)

plt.show()

image.png
关于
样本生成器
的详细内容,请参考:
TODO

3. 模型训练

首先,分割
训练集

测试集

from sklearn.model_selection import train_test_split

# 分割训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)

这次按照
8:2的比例
来划分训练集和测试集。

然后用
scikit-learn
中的
KNeighborsClassifier
模型来训练:

from sklearn.neighbors import KNeighborsClassifier

# 定义KNN模型(设置4个分类,因为样本数据是4个分类)
reg = KNeighborsClassifier(n_neighbors=4)

# 训练模型
reg.fit(X_train, y_train)

# 在测试集上进行预测
y_pred = reg.predict(X_test)

KNeighborsClassifier
的主要参数包括:

  1. n_neighbors
    :这是
    kNN
    算法中的
    k值
    ,即选择最近的
    k个点
    。默认值为5。
  2. weights
    :此参数默认为'
    uniform
    ',也可以设置为'
    distance
    ',或者用户自定义的函数。其中,'
    uniform
    '表示所有的邻近点的权重都是相等的,'
    distance
    '表示距离近的点比距离远的点的影响大。
  3. algorithm
    :此参数默认为'
    auto
    ',也可以设置为'
    auto
    ','
    ball_tree
    ','
    kd_tree
    ',或'
    brute
    '。这决定了在计算最近邻时使用的算法。
  4. leaf_size
    :此参数默认为30,也可以设置为一个整数,用于指定传递给构建叶子节点时使用的最小样本数。
  5. p
    :此参数默认为2,也可以设置为一个值<=1。这决定了在计算
    Minkowski
    距离度量时使用的p值。
  6. metric
    :此参数默认为'
    minkowski
    ',也可以设置为'
    euclidean
    ','
    manhattan
    '等。这决定了距离度量方法。
  7. metric_params
    :此参数默认为None,也可以是一个字典,包含了额外的关键字参数传递给距离度量函数。
  8. n_jobs
    :此参数默认为None,也可以是一个大于等于1的整数,表示可用于执行并行计算的CPU数量。

最后验证模型的训练效果:

# 比较测试集中有多少个分类预测正确
correct_pred = np.sum(y_pred == y_test)

print("预测正确率:{}%".format(correct_pred/len(y_pred)*100))

# 运行结果
预测正确率:68.5%

模型使用了默认的参数,可以看出,模型正确率不高。
感兴趣的同学可以试试调整
KNeighborsClassifier
的参数,看看是否可以提高模型的预测正确率。

4. 总结

KNN
算法被广泛应用于各种不同的应用场景,如图像识别、文本分类、垃圾邮件识别、客户流失预测等。
这些场景的一个共同特点是,需要对一个未知的样本进行快速的分类或预测。

KNN
算法主要优势在于:

  1. 简单直观

    KNN
    算法的概念简单直观,容易理解和实现。
  2. 适用于小样本数据

    KNN
    算法在
    小样本数据
    上的表现往往优于其他机器学习算法。
  3. 对数据预处理要求较低

    KNN
    算法
    不需要
    对数据进行复杂的
    预处理
    ,例如标准化、归一化等。

不过,
KNN
算法也有不足之处:

  1. 计算量大:
    对于大型数据集,
    KNN
    算法可能需要大量的存储空间和计算时间,因为需要计算每个样本与所有已知样本的距离。
  2. 选择合适的K值困难

    K值
    的选择对结果影响很大,选择不当可能会导致结果的不稳定。
  3. 对噪声数据敏感
    :如果数据集中存在噪声数据,
    KNN
    算法可能会受到较大影响。

平衡二叉搜索树

平衡二叉搜索树(Balanced Binary Search Tree)的每个节点的左右子树高度差不超过 1,它可以在 O(logn) 时间复杂度内完成插入、查找和删除操作,最早被提出的自平衡二叉搜索树是 AVL 树。

AVL 树在执行插入或删除操作后,会根据节点的平衡因子来判断是否平衡,若非平衡则执行旋转操作来维持树的平衡,本文主要是对红黑树相关的讲解,如果大家感兴趣可以去了解一下 AVL 树相关的知识,在这里不做赘述。

2-3 搜索树

标准二叉树中的节点称为 2-节点(含有一个键和两个指针),为了保证二叉搜索树的平衡性,需要增加一些灵活性,增加 3-节点(含有两个键和三个指针)。2-节点的指针和 3-节点的指针对应的区间大小关系如下:

  • 2-节点:left 指针指向的左子树中所有的键值均小于当前节点键值,right 指针指向的右子树中所有的键值均大于当前节点键值

  • 3-节点:left 指针指向的左子树中所有的键值均小于当前节点键值,mid 指针指向的中子树中所有的键值在当前节点的两个键值之间,right 指针指向的右子树中所有的键值均大于当前节点的键值

我们先来看一下一棵2-3搜索树的样例:

image.png

这里需要注意:2-3搜索树无论如何增删节点,它
始终都能维持完美的平衡性
,看下文时要牢记这一点。

3-节点的引入是如何保证树平衡的?

我们以插入键值 11 的节点为例,它会查找到该键对应的位置为 12 节点的左子树,如果我们未引入3-节点,那么树高会增加,如下图中(1)所示,而我们引入3-节点后,我们只需将2-节点替换为3-节点,而不会导致树高的增加,2-3搜索树依然完美平衡,如下图中(2)所示:

image.png

我们接着看,如果我们插入的键值为 26,它会查找到该键的位置为 19 和 24 这个3-节点,因为这个节点中已经没有多余的键的位置了,所以26键只能放在右子树的位置,使得树高加一。不过我们可以通过巧妙的方法,先将该节点转换为4-节点,一个4-节点又能转换成 3 个 2-节点, 我们将这 3 个2-节点的根节点(4-节点的中键值)插入到原来的父节点中,那么树高仍能保持不变(完美平衡),如下图所示:

image.png

虽然我们临时借助了4-节点,但是最终变换完毕后,整棵树还是依然是由2-节点和3-节点组成的。

我们再插入键值为4的节点会如何呢?它会查找到1和3这个3-节点的位置,还是采用相同的办法,将其转换成4-节点,但是转换完后准备将它插入到原来的父节点时,会发现父节点也是3-节点,这就导致我们不得不再次使用同样的方法,这样推广到一般情况,
需要不断地分解临时的4-节点并将其中键插入到更高的父节点中,直到遇到一个2-节点并将其转换成不需要分解的3-节点或者到达为2-节点的根节点
,如下图所示:

2-3树4.jpg

那如果处理到根节点时,根节点仍然为3-节点呢?其实原理是一样的,只不过根节点没有父节点,不需要再将4-节点的中键向上合并,转换成3个2-节点即可,相应地树高会加一。

在以上所述的情况中,2-3树的插入算法使树本身结构发生的改变是
局部
的,除了相关的节点和引用之外,不必修改和检查树的其他部分,这些局部变换不会影响到树的全局有序性和平衡性,在插入的过程中,
2-3树始终是完美平衡二叉树

左倾红黑树(LLRBT)

学会了 2-3 树我们先来学习一种简单的红黑树,
左倾红黑树
,它是经典红黑树的变体,相对更容易实现。它的基本思想是用标准的二叉搜索树和“颜色信息”来表示2-3树:其中的链接分为两种,红链接将两个2-节点连接起来构成一个3-节点,黑链接是2-3树中的普通链接,如下图所示:

LLRB.jpg

我们规定被红色链接指向的节点为红色节点,被黑色链接指向的节点为黑色节点,从上图中我们也能够发现:如果我们将红链接“横过来”,它和3-节点的表示是一样的,这就是在左倾红黑树中用颜色信息表示3-节点的方式,确切地说,3-节点是由两个2-节点组成,并且其中一个2-节点被另一个2-节点用
红链接左引用

可以说左倾红黑树就是一棵2-3搜索树,它满足如下条件:

  • 根节点是黑色的

  • 红链接均为左引用

  • 没有任何一个节点同时和两条红链接相连

  • 任意叶子节点到根节点路径上的黑色节点数量相同,即该树是黑色平衡的

2-3搜索树始终能保持完美平衡,那么任意叶子节点到根节点的距离是相等的。左倾红黑树又是一棵2-3搜索树,其中的黑链接是2-3搜索树中的普通链接,那么左倾红黑树中被黑色链接引用的黑色节点也必然是完美平衡的,所以任意叶子节点到根节点路径上的黑色节点数量必然相同。

性质我们已经理解了,接下来我们看看左倾红黑树的代码实现:

节点定义

上文我们已经规定,被红链接引用的节点为红色,被黑色链接引用的节点为黑色,我们在节点中添加
color
字段来标记颜色:
True
表示红色,
False
表示黑色,定义如下:

public class LeftLeaningRedBlackTree {

    private static final boolean RED = true;

    private static final boolean BLACK = false;

    static class Node {

        int key;

        int value;

        boolean color;

        Node left;

        Node right;

        public Node(int key, int value, boolean color) {
            this.key = key;
            this.value = value;
            this.color = color;
        }
    }

    /**
     * 判断节点是否为红色
     */
    private boolean isRed(Node node) {
        if (node == null) {
            return false;
        } else {
            return node.color == RED;
        }
    }
}

旋转

插入和删除操作可能会使左倾红黑树出现红链接右引用或者左右引用都是红链接的情况,造成黑色不平衡,为了始终满足左倾红黑树的性质,我们需要通过旋转操作进行修复。

左旋

如果有红链接右引用,我们可以通过
左旋
操作将其调整为红链接左引用,过程如下图所示:

LLRB2.jpg

代码如下:

    /**
     * 左旋
     */
    private Node rotateLeft(Node node) {
        Node newRoot = node.right;
        node.right = newRoot.left;
        newRoot.left = node;

        newRoot.color = node.color;
        node.color = RED;

        return newRoot;
    }

旋转完成后可以发现,左旋是将两个键中较小的作为根节点转变为较大的作为根节点。

右旋

右旋是和左旋完全相反的动作,它能将红链接左引用旋转成红链接右引用,过程如下图所示:

LLRB3.jpg

代码如下:

    /**
     * 右旋
     */
    private Node rotateRight(Node node) {
        Node newRoot = node.left;
        node.left = newRoot.right;
        newRoot.right = node;

        newRoot.color = node.color;
        node.color = RED;

        return newRoot;
    }

右旋是将两个键中较大的作为根节点转变为较小的作为根节点。

左旋和右旋的方法返回值都是
Node
,旋转完成后父节点对旋转节点的引用会发生改变,在插入节点时旋转操作可以保持左倾红黑树的
有序性

完美平衡性
,也就是说我们在红黑树中进行旋转时无需为它的有序性和完美平衡性担心。

插入节点

插入的每个节点默认红色,之后根据插入后节点颜色情况,使用旋转操作进行修复,我们分情况讨论:

向2-节点中插入

向2-节点中插入节点很简单:如果新键值比老键小,那么将该节点被老节点红链接左引用即可;如果新键值比老键大,那么该节点需要被老节点红链接右引用,因为左倾红黑树中红链接不能左引用,所以我们需要左旋进行调节,过程图示如下:

LLRB4.jpg

向3-节点中插入

向3-节点中插入分三种情况,我们分别进行讨论:

  • 新节点大于3-节点中的两个键值

这种情况该节点被3-节点右引用,前文中我们说过,在3-节点中插入新节点可以临时转化成4-节点进行处理,4-节点又能转换成3个2-节点,那么我们得到的就是一颗树高为 2 的平衡树,此时根节点的左右引用均为红链接,我们可以将它们全部转换成黑链接(反色处理),如下图所示:

LLRB5.jpg

  • 新节点小于3-节点中的两个键值

这种情况该节点被3-节点左引用,这样会出现两条红链接连续的情况,我们只需将上层的红链接右旋,这样便转换成了我们上述的第一种情况,过程如下图所示:

LLRB6.jpg

  • 新节点介于3-节点的两键值中间

这种情况仍然会出现两条红链接连续的情况,我们只需要将下层的红链接左旋,便转换成了上述的第二种情况,过程如下图所示:

LLRB7.jpg

颜色转换

我们需要为上述将某个节点的两条红链接转换成黑链接的动作定义
flipColors
方法,在图示中能够发现除了将左右节点转换成黑色外,
该节点被转换成了红色
,颜色转换在本质上和旋转操作同样都是局部的,并
不会影响整棵树的黑色平衡性
。该节点颜色转换成红色后,
相当于将其送入了它的父节点,意味着在父节点插入了一个新键
,我们需要按照上述插入节点的对应情况进行处理。

    /**
     * 颜色转换
     */
    private void flipColors(Node node) {
        node.color = RED;
        node.left.color = BLACK;
        node.right.color = BLACK;
    }

不过,这对于根节点来说,并不能完全按照这种规则进行颜色转换,因为如果根节点是红色的,那么说明根节点为3-节点中较小的节点,但实际上并非如此(根节点始终为其中较大的值)。所以我们
每次插入节点时需要保证根节点是黑色的
,而且
每当根节点由红色转变为黑色时,都意味着树高增加了 1

在3-节点中插入节点后,将临时转换成4-节点,并将4-节点的中键作为红色节点插入到它的父节点中(无论是左引用还是右引用),中键作为红色节点插入父节点无异于处理新插入一个红色节点,我们需要不断地重复这个过程,直到遇到根节点或2-节点,这样我们就能始终保持左倾红黑树和2-3搜索树的一一对应关系。

LLRB8.jpg

上图对应了节点插入后,执行旋转和颜色转换的所有情况。这些保持树平衡的操作需要在该插入节点到根节点路径上(回溯)的所有节点执行,规则如下:

  • 如果某节点的右节点是红色,左节点为空或为黑色,需要左旋

  • 如果某节点的左节点是红色,该左节点的左节点仍是红色,需要右旋

  • 如果某节点的左节点和右节点均为红色,需要执行颜色转换

现在我们已经讨论了插入节点时所有可能出现的情况,接下来我们要实现左倾红黑树的插入方法。它的实现是在普通二叉搜索树的插入方法上进行了扩展,添加了保持树平衡性的逻辑,而平衡性的保持又是
由下到上
的,所以我们需要将旋转和颜色转换的逻辑放在递归逻辑之后,也就是我们在回溯时修复树的平衡,代码如下:

    /**
     * 插入节点
     */
    public void put(Integer key, Integer value) {
        root = put(root, key, value);
        // 根节点永远都是黑色
        root.color = BLACK;
    }

    /**
     * 插入节点的执行逻辑
     */
    private Node put(Node node, Integer key, Integer value) {
        if (node == null) {
            return new Node(key, value, RED);
        }

        if (key .key) {
node.right = put(node.right, key, value);
} else if (key .key) {
node.left = put(node.left, key, value);
} else {
node.value = value;
}
// 将3-节点的红链接右引用左旋
if (isRed(node.right) !isRed(node.left)) {
node = rotateLeft(node);
}
// 将4-节点两条连续的红链接左引用左旋
if (isRed(node.left) isRed(node.left.left)) {
node = rotateRight(node);
}
// 进行颜色转换并将红链接在树中向上传递
if (isRed(node.left) isRed(node.right)) {
flipColor(node);
}

return node;
}

删除节点

删除节点方法是左倾红黑树中最复杂的实现,所以接下来的内容可能会让大家觉得有一些困难,但是千万不要灰心,我在学习的时候也是重复了很多遍才理解了这个过程,也请大家多给自己一些耐心。

左倾红黑树在执行删除的过程中,如果直接删除一个2-节点,会留下空链接,这样
会破坏树的完美平衡性
,如下图所示:
LLRB_DELETE.jpg

所以,在执行删除之前,需要将被删除节点转换成3-节点或临时的4-节点的一部分,这样才能保证删除节点前后左倾红黑树一直都是完美平衡的,相应地,如果被删除节点为3-节点或4-节点,那么直接移除它是不影响树的完美平衡的。

接下来我们以删除最小节点为例,讲解下节点删除的过程。

删除最小节点

既然我们需要将被删除的2-节点转换成3-或4-节点的一部分,那么我们该如何转换呢?

我们先看第一种情况,要被删除的节点和它的兄弟节点都是2-节点。根据上文中插入节点的逻辑,
两个黑色的左右子节点是由4-节点转换颜色后得到的
,那么我们可以再对其进行颜色转换,使之再成为4-节点,被删除节点也就成了4-节点的一部分,此时将其删除就不会影响树的平衡性了。不过在删除之后,虽然树的平衡性不被影响,但是红链接右引用不符合左倾红黑树的性质,需要左旋操作来修复,过程图示如下:

LLRB_DELETE3.jpg

图示中由(1)到(2)将根节点转化成了红色,不知道大家还记不记得,在插入节点时4-节点转换成3个2-节点不只将左右节点转换成了黑色,还将该节点染成了红色,表示该节点会插入到父节点中,由于该节点为根节点,所以在(1)中表示为黑色,图(2)将根节点切换回了红色,对应了4-节点转换成3个2-节点的一般情况,图(3)中将这3个节点颜色均取反便得到了4-节点

我们接着看第二种情况,要被删除的节点为2-节点,它的兄弟节点为3-节点,《算法》中介绍了“借键方法”:左子节点将根节点中较小的节点借过来组成3-节点,根节点将右节点中较小的节点借过来保持自身原来节点样式不变,而右节点便成了一个2-节点,借键完毕后再将左子节点(3-节点)中较小的键(红色节点)移除,不会影响树的完美平衡性,图示如下:

LLRB_DELETE2.jpg

现在我们已经整理好了删除最小节点的过程,试着写一下代码:

    /**
     * 删除最小节点
     */
    public void deleteMin() {
        if (!isRed(root.right)) {
root.color = RED;
}
root = deleteMin(root);
// 根节点永远都是黑色
if (root != null) {
root.color = BLACK;
}
}

/**
* 删除最小节点
*/
private Node deleteMin(Node node) {
if (node.left == null) {
return null;
}

// 判断当前节点和左子节点是不是3-节点,不是的话执行借键逻辑
if (!isRed(node.left.left)) {
node = moveRedLeft(node);
}
node.left = deleteMin(node.left);

return balance(node);
}

/**
* 从右向左借键
*/
private Node moveRedLeft(Node node) {
flipColors(node);
// 兄弟节点为3-节点的情况
if (isRed(node.right.left)) {
node.right = rotateRight(node.right);
node = rotateLeft(node);
}

return node;
}

/**
* 从下到上再平衡
*/
private Node balance(Node node) {
if (isRed(node.right)) {
node = rotateLeft(node);
}
if (isRed(node.left) isRed(node.left.left)) {
node = rotateRight(node);
}
if (isRed(node.left) isRed(node.right)) {
flipColors(node);
}

return node;
}

需要注意的是,在插入节点定义的
flipColors
如下,本质上在插入节点时,该操作是将当前节点颜色取反:

    private void flipColors(Node node) {
        node.color = RED;
        node.left.color = BLACK;
        node.right.color = BLACK;
    }

现在将该方法改成了反色处理,为了在删除节点方法中复用,如下:

    private void flipColors(Node node) {
        node.color = !node.color;
        node.left.color = !node.left.color;
        node.right.color = !node.right.color;
    }

本质上,删除最小节点的过程是从上到下不断地
将当前节点染红,保证当前节点始终不为2-节点,避免移除节点后发生树的不平衡
,因为不知道什么时候能找到最小节点,所以该路径上的每个节点都需要执行该操作。

删除最大节点

删除最大节点我们只讨论被删除节点为2-节点,其兄弟节点为3-节点的情况,另一种被删除节点和它的兄弟节点均为2-节点的情况非常简单,不再赘述。

其兄弟节点为3-节点时,我们仍需要采用“借键”的方法,过程如下图所示:

LLRB_DELETE4.jpg

删除最大节点的代码逻辑如下所示:

    /**
     * 删除最大节点
     */
    public void deleteMax() {
        if (!isRed(root.right)) {
root.color = RED;
}
root = deleteMax(root);
// 根节点永远都是黑色
if (root != null) {
root.color = BLACK;
}
}

/**
* 删除最大节点
*/
private Node deleteMax(Node node) {
if (isRed(node.left)) {
node = rotateRight(node);
}
if (node.right == null) {
return null;
}

// 当前节点的右引用不是红链接且右节点不是3-节点
if (!isRed(node.right.left)) {
node = moveRedRight(node);
}
node.right = deleteMax(node.right);

return balance(node);
}

/**
* 从左向右借键
*/
private Node moveRedRight(Node node) {
flipColors(node);
if (isRed(node.left.left)) {
node = rotateRight(node);
}
return node;
}

其中有如下代码是在删除最小节点没有的:

    private Node deleteMax(Node node) {
        // 区别于删除最小节点的操作
        if (isRed(node.left)) {
            node = rotateRight(node);
        }
        // ...
    }

我们来解释一下这段逻辑:在左倾红黑树中,如果当前节点的左节点为红色,那么会有可能存在这种情况:

LLRB_DELETE5.jpg

直接将图示中最大节点移除的话,会导致它的左节点丢失,为了避免,会先执行右旋,将要被删除的节点旋转到右引用的位置。

删除节点

删除节点的方法在弄明白了删除最大和最小节点的方法后,理解起来是非常容易的。它其实是删除最大节点和删除最小节点方法的结合,同样也是需要保证经过的节点为3-节点或临时的4-节点,在找到对应节点时,将该节点值替换为右子树中的最小节点值,再执行删除右子树最小节点,删除完成后执行平衡修复,大家需要关注其中的注释信息,如下:

    /**
     * 删除节点
     */
    public void delete(Integer key) {
        if (!isRed(root.right)) {
root.color = RED;
}
root = delete(root, key);
if (root != null) {
root.color = BLACK;
}
}

/**
* 删除节点
*/
private Node delete(Node node, Integer key) {
if (key .key) {
// 要删除的节点在左子树,保证当前节点为红色
if (!isRed(node.left.left)) {
node = moveRedLeft(node);
}
node.left = delete(node.left, key);
} else {
if (isRed(node.left)) {
node = rotateRight(node);
}
// node.right == null 可知上面右旋没有发生,右子树为 null,左节点不是红色节点,能证明左子树必然为null(满足黑色平衡)
if (key.equals(node.key) .right == null) {
return null;
}
// 要删除的节点在右子树
if (!isRed(node.right.left)) {
node = moveRedRight(node);
}

if (key.equals(node.key)) {
// 找到右子树中最小的节点替换当前节点的键和值
Node min = min(node.right);
node.key = min.key;
node.value = min.value;
// 再将该右子树最小的节点移除
deleteMin(node.right);
} else {
node.right = delete(node.right, key);
}
}

return balance(node);
}

为方便大家学习参考,全量代码如下:

public class LeftLeaningRedBlackTree {

    private static final boolean RED = true;

    private static final boolean BLACK = false;

    static class Node {

        int key;

        int value;

        boolean color;

        Node left;

        Node right;

        public Node(int key, int value, boolean color) {
            this.key = key;
            this.value = value;
            this.color = color;
        }
    }

    private Node root;

    /**
     * 插入节点
     */
    public void put(Integer key, Integer value) {
        root = put(root, key, value);
        // 根节点永远都是黑色
        root.color = BLACK;
    }

    /**
     * 插入节点的执行逻辑
     */
    private Node put(Node node, Integer key, Integer value) {
        if (node == null) {
            return new Node(key, value, RED);
        }

        if (key .key) {
node.right = put(node.right, key, value);
} else if (key .key) {
node.left = put(node.left, key, value);
} else {
node.value = value;
}
// 将3-节点的红链接右引用左旋
if (isRed(node.right) !isRed(node.left)) {
node = rotateLeft(node);
}
// 将4-节点两条连续的红链接左引用左旋
if (isRed(node.left) isRed(node.left.left)) {
node = rotateRight(node);
}
// 进行颜色转换并将红链接在树中向上传递
if (isRed(node.left) isRed(node.right)) {
flipColors(node);
}

return node;
}

/**
* 删除最小节点
*/
public void deleteMin() {
if (!isRed(root.right)) {
root.color = RED;
}
root = deleteMin(root);
// 根节点永远都是黑色
if (root != null) {
root.color = BLACK;
}
}

/**
* 删除最小节点
*/
private Node deleteMin(Node node) {
if (node.left == null) {
return null;
}

// 判断当前节点和左子节点是不是3-节点,不是的话执行借键逻辑
if (!isRed(node.left.left)) {
node = moveRedLeft(node);
}
node.left = deleteMin(node.left);

return balance(node);
}

/**
* 从右向左借键
*/
private Node moveRedLeft(Node node) {
flipColors(node);
// 兄弟节点为3-节点的情况
if (isRed(node.right.left)) {
node.right = rotateRight(node.right);
node = rotateLeft(node);
}

return node;
}

/**
* 删除最大节点
*/
public void deleteMax() {
if (!isRed(root.right)) {
root.color = RED;
}
root = deleteMax(root);
// 根节点永远都是黑色
if (root != null) {
root.color = BLACK;
}
}

/**
* 删除最大节点
*/
private Node deleteMax(Node node) {
if (isRed(node.left)) {
node = rotateRight(node);
}
if (node.right == null) {
return null;
}

// 当前节点的右引用不是红链接且右节点不是3-节点
if (!isRed(node.right.left)) {
node = moveRedRight(node);
}
node.right = deleteMax(node.right);

return balance(node);
}

/**
* 从左向右借键
*/
private Node moveRedRight(Node node) {
flipColors(node);
if (isRed(node.left.left)) {
node = rotateRight(node);
}
return node;
}

/**
* 删除节点
*/
public void delete(Integer key) {
if (!isRed(root.right)) {
root.color = RED;
}
root = delete(root, key);
if (root != null) {
root.color = BLACK;
}
}

/**
* 删除节点
*/
private Node delete(Node node, Integer key) {
if (key .key) {
// 要删除的节点在左子树,保证当前节点为红色
if (!isRed(node.left.left)) {
node = moveRedLeft(node);
}
node.left = delete(node.left, key);
} else {
if (isRed(node.left)) {
node = rotateRight(node);
}
// node.right == null 可知上面右旋没有发生,右子树为 null,左节点不是红色节点,能证明左子树必然为null(满足黑色平衡)
if (key.equals(node.key) .right == null) {
return null;
}
// 要删除的节点在右子树
if (!isRed(node.right.left)) {
node = moveRedRight(node);
}

if (key.equals(node.key)) {
// 找到右子树中最小的节点替换当前节点的键和值
Node min = min(node.right);
node.key = min.key;
node.value = min.value;
// 再将该右子树最小的节点移除
deleteMin(node.right);
} else {
node.right = delete(node.right, key);
}
}

return balance(node);
}

/**
* 获取最小节点
*/
private Node min(Node node) {
if (node.left == null) {
return node;
}
return min(node.left);
}

/**
* 从下到上再平衡
*/
private Node balance(Node node) {
if (isRed(node.right)) {
node = rotateLeft(node);
}
if (isRed(node.left) isRed(node.left.left)) {
node = rotateRight(node);
}
if (isRed(node.left) isRed(node.right)) {
flipColors(node);
}

return node;
}

/**
* 左旋
*/
private Node rotateLeft(Node node) {
Node newNode = node.right;
node.right = newNode.left;
newNode.left = node;

newNode.color = node.color;
node.color = RED;

return newNode;
}

/**
* 右旋
*/
private Node rotateRight(Node node) {
Node newNode = node.left;
node.left = newNode.right;
newNode.right = node;

newNode.color = node.color;
node.color = RED;

return newNode;
}

/**
* 颜色转换
*/
private void flipColors(Node node) {
node.color = !node.color;
node.left.color = !node.left.color;
node.right.color = !node.right.color;
}

/**
* 判断节点是否为红色
*/
private boolean isRed(Node node) {
if (node == null) {
return false;
} else {
return node.color == RED;
}
}
}


巨人的肩膀

  • 《算法 第四版》:第 3.3 章 平衡查找树

  • 维基百科 - 平衡二元搜寻树

  • 维基百科 - AVL树

  • 知乎 - 关于AVL树和红黑树的一点看法

  • 维基百科 - 左倾红黑树

  • LeetCode - 红黑树从入门到看开

  • 知乎 - 有人能讲清楚《Algorithms》中左倾红黑树(LLRB)删除操作的每一行代码吗?

作者:京东物流 王奕龙

来源:京东云开发者社区 自猿其说 Tech 转载请注明来源

有时,我们不想手动建立pv和pvc,这时,我们可以通过strongClass存储类来帮我们实现,动态建立pvc,并动态为它分配pv存储空间,我们以nfs为例,说一下动态分配在nfs存储截至上建立pv的方式。

本文导读

  • StorageClass和PVC及PV
  • 集群权限与绑定rbac.yaml
  • 建立动态pvc的provisioner.yaml
  • 建立strongClass的strongclass.yaml
  • 在有状态服务StatefulSet中使用strongClass
  • 遇到的问题与解决

StorageClass和PVC及PV

当使用StorageClass创建PersistentVolumeClaim(PVC)时,它们之间的关系可以用以下文字图示表示:

           +------------------+
           |   StorageClass   |
           +------------------+
                     |
                     |  +------------------+
                     |  |       PVC        |
                     |  +------------------+
                     |         |
                     |         |
                     |  +------------------+
                     |  |        PV        |
                     |  +------------------+

在这个图示中:

  • StorageClass是用于定义动态卷分配的规则和配置的对象。
  • PVC是用来请求存储资源的声明,它指定了所需的存储容量、访问模式等。
  • PV是实际的持久化存储资源,它是由集群管理员预先创建并配置好的。

当一个PVC被创建时,它会根据所指定的StorageClass进行动态分配,并绑定到一个可用的PV上。这样,PVC就可以通过PV来获取所需的存储资源。PVC和PV之间的绑定关系是自动完成的,不需要用户手动干预。

集群权限与绑定rbac.yaml

首先,你要在k8s中添加pvc,pv这些资源,你需要有自己的sa(service account),然后把你的pod(建立pvc和pv)去分配这个有权限的sa,这个pod就可以像人一样,为你在k8s中创建资源了。

---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: nfs-provisioner
  namespace: elk
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
   name: nfs-provisioner-runner
rules:
   -  apiGroups: [""]
      resources: ["persistentvolumes"]
      verbs: ["get", "list", "watch", "create", "delete"]
   -  apiGroups: [""]
      resources: ["persistentvolumeclaims"]
      verbs: ["get", "list", "watch", "update","create"]
   -  apiGroups: ["storage.k8s.io"]
      resources: ["storageclasses"]
      verbs: ["get", "list", "watch"]
   -  apiGroups: [""]
      resources: ["events"]
      verbs: ["list", "watch", "create", "update", "patch"]
   -  apiGroups: [""]
      resources: ["services", "endpoints"]
      verbs: ["get","create","list", "watch","update"]
   -  apiGroups: ["extensions"]
      resources: ["podsecuritypolicies"]
      resourceNames: ["nfs-provisioner"]
      verbs: ["use"]
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: run-nfs-provisioner
subjects:
  - kind: ServiceAccount
    name: nfs-provisioner
roleRef:
  kind: ClusterRole
  name: nfs-provisioner-runner
  apiGroup: rbac.authorization.k8s.io

在Kubernetes中,ClusterRole和ClusterRoleBinding都是一种用于定义集群级别权限的资源,它与特定的命名空间无关。因此,在创建ClusterRole时,不需要为它指定namespace。
ClusterRole的权限范围覆盖整个集群,可以被任何命名空间中的对象引用和使用。这使得ClusterRole能够控制跨多个命名空间的资源和操作。

建立动态pvc的provisioner.yaml

---
kind: Deployment
apiVersion: apps/v1
metadata:
  name: nfs-client-provisioner
  namespace: elk
  labels:
    app: nfs-client-provisioner
spec:
  replicas: 1
  strategy:
    type: Recreate
  selector:
    matchLabels:
      app: nfs-client-provisioner
  template:
    metadata:
      labels:
        app: nfs-client-provisioner
    spec:
      serviceAccount: nfs-provisioner
      containers:
        - name: nfs-client-provisioner
          image: easzlab/nfs-subdir-external-provisioner:v4.0.1 #quay.io/external_storage/nfs-client-provisioner:latest
          volumeMounts:
            - name: nfs-client-root
              mountPath: /persistentvolumes
          env:
            - name: PROVISIONER_NAME
              value: nfs-provisioner # 指定分配器的名称,创建storageclass会用到
            - name: NFS_SERVER
              value: 192.168.1.x
            - name: NFS_PATH
              value: /mnt/disk/nfs_data #这个nfs服务器上的目录
      volumes:
        - name: nfs-client-root
          nfs:
            server: 192.168.1.x
            path: /mnt/disk/nfs_data #这个nfs服务器上的目录

建立strongClass的strongclass.yaml

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: elk-nfs
provisioner: nfs-provisioner # 必须与provisioner.yaml中PROVISIONER_NAME的值一致
parameters:
  archiveOnDelete: "true"  # 删除pv的时候,pv的内容是否要备份
allowVolumeExpansion: true  #如果对PVC扩容,则其对应的"storage class"中allowVolumeExpansion字段需要设置成true

在Kubernetes中,建立StorageClass时不需要指定命名空间(namespace)。StorageClass是一种用于定义持久卷的存储类别的资源,它是集群级别的。

在有状态服务StatefulSet中使用strongClass

在Kubernetes中,
StatefulSet
是一种用于管理有状态应用的控制器。它可以确保Pod按照指定的顺序和唯一标识符进行创建、更新和删除。
StatefulSet
通常与
PersistentVolumeClaim
(PVC)一起使用,以为每个Pod提供持久化存储。

要动态创建PVC并将其绑定到StatefulSet的Pod上,你可以使用
volumeClaimTemplates
字段。这个字段允许你定义一个模板,用于创建PVC。

下面是一个示例
StatefulSet
配置文件,其中包含了
volumeClaimTemplates
字段:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: example-statefulset
spec:
  serviceName: "example"
  replicas: 3
  selector:
    matchLabels:
      app: example
  template:
    metadata:
      labels:
        app: example
    spec:
      containers:
      - name: example-app
        image: nginx
        ports:
        - containerPort: 80
        volumeMounts:
        - name: data
          mountPath: /data
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes: [ "ReadWriteOnce" ]
      storageClassName: elk-nfs
      resources:
        requests:
          storage: 1Gi

在上述配置中,我们定义了一个
StatefulSet
,它由3个Pod组成。每个Pod都会自动创建一个名为"data"的PVC,并将其挂载到
/data
路径上。你需要将
elk-nfs
替换为实际的存储类名称。

遇到的问题与解决

最近把kubernetes集群从1.18升级到1.20以后,新建pvc一直处于pending状态,查看nfs-client-provisioner日志,提示:

unexpected error getting claim reference: selfLink was empty, can't  make reference

主要原因是kubernetes 1.20版本 禁用了 selfLink导致。

网上大部分文档的解决方法都是修改kube-apiserver.yaml,添加- --feature-gates=RemoveSelfLink=false,然后重新部署。

spec:
  containers:
  - command:
    - kube-apiserver
    - --feature-gates=RemoveSelfLink=false

但是根据github的issues,直接更改nfs-subdir-external-provisioner为v4.0.0以上的版本就可以了。

相关文档:
https://github.com/kubernetes-sigs/nfs-subdir-external-provisioner/issues/25

网上找了一个可以下载的镜像
easzlab/nfs-subdir-external-provisioner:v4.0.1
,pull以后测试,发现pvc申请正常了。