cgroup 内存管理之 mlock 内存

我们在使用容器的过程中,可能会遇到一个问题,容器利用率很低,但是经常发生 oom,这是什么情况?

很可能是业务使用了一些不可回收的 page cache,其中最主要的应该就是 mlock

1. mlock 的背景

mlock 的作用就是防止页面被换出到 swap 分区,或者 page cache 被内核回收

由于线上服务器,基本默认都会关闭 swap 分区,所以这种情况暂不讨论,防止 page cache 被内核回首是主要作用

什么样的 page 需要 mlock?

1)通过 mmap 方式映射到内存中的 page cache,比如一些关键的索引数据,需要经常访问,那就得 mlock 住,否则 pgmajfault 带来的性能抖动是很大的

2)程序自身依赖的一些 so 动态链接库,由于内核的 page cache 回收算法,并不感知 page 具体是普通的文件 cache,还是 so 动态链接库,所以当容器内存不足时,内核通过一些粗略的回收算法回收 page cache,一旦把 so 的缓存页回收掉了,程序在调用相关函数时,会出现严重的性能抖动

因此,通过 mlock,显式的把一些关键的不希望被回收的 page cache 锁定起来,达到保证业务性能的目的

2. mlock 的内核实现

mlock 相关的系统调用可以参考:https://linux.die.net/man/2/mlock

2.1. unevictable 内存

cgroup 的 memory.stat 接口,包含一个容器内存使用的详细信息

cat /cgroups/v2/matrix/memory.stat 
anon 200723943424
file 158665015296
kernel_stack 0
slab 0
sock 229249024
shmem 4258582528
file_mapped 48234749952
file_dirty 8605696
file_writeback 0
inactive_anon 4258234368
active_anon 200570675200
inactive_file 73935654912
active_file 40680521728
unevictable 39942680576

其中 unevictable 表示不剔除的 page cache,mlock 属于 unevictable 的一部分,除此之外,unevictable 还有一些其他内存


cgroup 内存管理之 tmpfs

本文内核代码版本:https://elixir.bootlin.com/linux/v4.14.281/source/mm/shmem.c

1. tmpfs 内存简介

tmpfs 文件系统是 pod 中常见的一种“存储”介质,也叫 ram disk,都是一个东西

tmpfs 的特殊的地方在于:

  1. 首先它是个文件系统
  2. 但是它的文件数据是完全存放在内存里面的,不在磁盘上

所以要讲 tmpfs 的话,就得把这两部分都讲清楚,一个是文件系统的实现,一个是底层“持久化”层内存的管理

通常应用程序之间会通过 tmpfs 文件系统来实现高效的数据共享

/dev/shm 就是一个最典型的 tmpfs 文件系统,是操作系统为了解决大多数程序数据共享而默认挂在的一个 tmpfs

2. tmpfs 文件系统的实现

我们知道 file 是linux内核最重要的设计,一切皆文件

除了普通的文件,平时我们接触到的,unix管道、socket、proc、cgroup 等等,都是基于文件的实现

为了实现灵活可扩展的文件系统架构,Linux设计了 virtual file system 抽象层,简称 vfs,对用户台程序屏蔽了所有具体的底层文件系统的实现细节,提供统一的文件系统接口

2.1. virtual file system 接口定义

https://www.kernel.org/doc/html/latest/filesystems/vfs.html

vfs 属于一个专题,我们这里不讲那么复杂,有时间可以专门展开讲

vfs 定义了文件系统实现最关键的2个接口:

  1. 一个是 struct file_operations:文件读写的接口
  2. 一个是 struct inode_operations:inode操作接口

inode_opertions 定义如下:

struct inode_operations {
    int (*create)(struct user_namespace*, struct inode*, struct dentry*, umode_t, bool);
    int (*symlink)(struct user_namespace*, struct inode*, struct dentry*, const char*);
    int (*mkdir)(struct user_namespace*, struct inode*, struct dentry*, umode_t);
    int (*rmdir)(struct inode*, struct dentry*); /* 省略一万字 */
};

是不是很熟悉?

file_operations 的定义如下:

struct file_operations {
    int (*open)(struct inode*, struct file*);
    loff_t (*llseek)(struct file*, loff_t, int);
    ssize_t (*read)(struct file*, char __user*, size_t, loff_t*);
    ssize_t (*write)(struct file*, const char __user*, size_t, loff_t*); /* 省略一万字 */
};

是不是也很熟悉?

内核 tmpfs 文件系统的源码:mm/shmem.c

tmpfs 其实并没有实现 vfs 中的所有接口,主要原因是因为由于绝大部分的 file system 底层实现其实都是一样的,没区别,所以内核为了简化文件系统的开发,把这些通用的实现都抽象出来,变成 generic_file_read/write 之类的通用实现

static const struct file_operations shmem_file_operations = { /* 普通文件的读、写、seek、fsync */ .mmap = shmem_mmap, .get_unmapped_area = shmem_get_unmapped_area, #ifdef CONFIG_TMPFS .llseek = shmem_file_llseek, .read_iter = shmem_file_read_iter, .write_iter = generic_file_write_iter, .fsync = noop_fsync, .splice_read = generic_file_splice_read, .splice_write = iter_file_splice_write, .fallocate = shmem_fallocate, #endif };
static const struct inode_operations shmem_inode_operations = { /* 这个是针对普通文件的 inode 操作 */ .getattr = shmem_getattr, .setattr = shmem_setattr, };
static const struct inode_operations shmem_dir_inode_operations = { /* 这个是针对目录的 inode 操作 */ #ifdef CONFIG_TMPFS .create = shmem_create, .lookup = simple_lookup, .link = shmem_link, .unlink = shmem_unlink, .symlink = shmem_symlink, .mkdir = shmem_mkdir, .rmdir = shmem_rmdir, .mknod = shmem_mknod, .rename = shmem_rename2, .tmpfile = shmem_tmpfile, #endif };


记一则 cfs 硬限引发的 cgroup_mutex 死锁

cgroup_mutex 是内核实现 cgroup 子系统而重度依赖的一把全局锁,这把全局锁在很多场景下会带来非常多的性能问题。具体这里不展开讲了,google 一下 cgroup_mutex deadlock,可以看到一堆 bugs report

最近线上恰好遇到一次机器死机的场景,后来分析发现和 cgroup_mutex 死锁有关(严格来说不叫死锁,叫夯死更合适一些),以此记录分析过程

1. 现象

机器存在大量的 D 进程,监控发现系统负载缓慢持续增高,应该是由于D进程持续不断地堆积,导致负载越来越高

所有与 cgroup_mutex 打交道的地方,都会卡死,比如简单来说 cat /proc/self/cgroup 会立即进入夯死状态,进程无法 kill -9 杀掉

我们通过一些艰难的内核栈跟踪,终于捕捉到了内核临死前的锁持有的状态(此处省略掉1万字跟踪过程)

2. 内核栈分析

读过内核源码或者了解内核原理的应该知道,vfs_write 是内核文件系统的抽象层。vfs_write 接着调用 cgroup_file_write,这个说明当前进程正在写 cgroup 文件系统,然后在写过程中,陷入 synchronize_sched,进程被换出,然后应该是一直卡在这里了。

但是真正导致系统死机的,并不是 synchronize_sched 这个地方,二是一个很复杂的链,触发场景:

    1. 进程持有 cgroup_mutex
  1. 进程尝试获取其他锁,或者进入睡眠态
  2. 进程 cfs 时间片被 throt,导致进程无法重新获得 cpu 的控制权,cgroup_mutex 无法释放

cgroup_mutex 是一把极大的锁,几乎任何 cgroup 操作都会涉及到这把锁的操作。在我们这个场景里,进程持有 cgroup_mutex 之后陷入 __wait_rcu,其他进程在尝试持有 cgroup_mutex 的时候几乎全部都夯住了


CTS: An operating system CPU scheduler to mitigate tail latency for latency-sensitive multi-threaded applications

论文原址:https://www.cs.jhu.edu/~erfan/files/cts.pdf

cts 提出了一种优化长尾延迟的方法,通过改进 linux 内核默认的 cfs 调度器,引入线程调度机制

论文的一些要点:

  • It has been proven that FCFS scheduling of threads leads to lower tail latency
  • Experiments show that CFS policies lead to LCFS scheduling, aggravating tail latency
  • CTS policies ensure FCFS thread scheduling, which yields lower tail latency.
  • CTS enforces our policies while maintaining the key features of the Linux scheduler
  • Experimental results show that CTS significantly outperforms the Linux scheduler

延迟敏感型的程序,通常有两种进程模型,一种是 thread-driven,另一种是 event-driven。前者很容易理解,起1个线程来服务一个请求,模型很简单。后者相对复杂一些,比如 libev,libevent 都是用来实现 event-driven 架构的基础库,通过 io 多路复用技术,将 io 线程和 worker 线程分离,io 线程专注数据转发,worker 线程专注业务逻辑处理

但是这个 io 线程很关键,如果数据 ready,但是得不到处理,会导致严重的长尾延迟。io 密集型的程序瓶颈不在 cpu,而是 io,CPU 上的时间片消耗是非常小的,远小于1ms,因而这种进程理论上应该优先获得响应


ImageNet Classification with Deep Convolutional Neural Networks

论文原址:https://proceedings.neurips.cc/paper/2012/file/c399862d3b9d6b76c8436e924a68c45b-Paper.pdf

无论是AI、深度学习,还是卷积神经网络,都不是这几年才有的概念,但确实是最近10年才流行起来。这里最主要的原因,还是要归功于AlexNet 2012年在大规模视觉识别挑战赛上取得的成功,所以才有了这篇论文

在这篇论文里面,AlexNet 提出了很多创新性的 idea,对卷积神经网络的发展影响深远,其中很多概念仍然是今天 CNN 的核心

论文的特点:

  1. AlexNet 包含许多心的不同寻常的特性,这些特性提高了神经网络的性能并减少了训练时间
  2. 提出 Dropout 解决过拟合的问题

下面我们再详细看下

不同寻常的网络特性

1. 非饱和神经元 ReLU

使用 ReLU 的四层卷积神经网络在 CIFAR-10 数据集上达到 25% 的训练误差比使用 tanh 神经元的等价网络(虚线)快 6 倍。为了使训练尽可能快,每个网络的学习率是单独选择的。没有采用任何类型的正则化。影响的大小随着网络结构的变化而变化,这一点已得到证实,但使用 ReLU 的网络都比等价的饱和神经元快几倍。

0

论文里有一片引用论文:https://www.cs.toronto.edu/~fritz/absps/reluICML.pdf

可以参考下


Visualizing and Understanding Convolutional Networks

论文原址:https://arxiv.org/pdf/1311.2901.pdf

1. 背景

我们都知道 CNN 卷积神经网络 效果好,但是一直以来都是一个黑盒,有2个关键问题:

  1. However there is no clear understanding of why they perform so wel
  2. or how they might be improved

其实包括我自己也是很困惑的,卷积神经网络有非常多。但是你看网上的资源来来回回都是在讲这些神经网络的结构。但是很少人知道为什么要把结构设计成这个样子。比如LeNet-5第一层卷积层为啥需要6个通道?为什么整个网络只需要2个卷积层,能不能更多或者更少?

为了解决这个黑盒问题,论文作者提出了一种可视化的方法,能够观测到卷积层的feature maps到底识别到了图片的什么特征,并以此反馈来改进神经网络的结构设计。最后通过这个方法,作者在ImageNet测试集上取得了非常突出的结果

2. 方法

论文的核心思想:map these activities back to the input pixel space, showing what input pattern originally caused a given activation in the feature maps

以LeNet-5这种经典的2D网络为例,正向的卷积过程一般包括几个步骤:

  1. filter
  2. relu
  3. max pooling
  4. 【optionally】local contrast operation

0


Mlsys & 深度学习相关方向的技术栈梳理

一些系统性的学习资料:

1. 深度网络设计

神经网络

  1. 经典2D/3D网络(LeNet-5,AlexNet,VCG)
  2. 残差网络:ResNet,DesNet,ResNeXt
  3. 时序网络:RNN,LSTM,GRU
  4. 生成对抗网络:DCGAN,LAPGAN,CycleGAN
  5. 其他:AutoML

2. 模型开发库

对神经网络模型的封装,更易用的API,集成度更高

  1. paddleNLP,paddleDetection,paddleOCR
  2. 其他公司好像没

3. 深度学习框架

面向 AI 的深度学习 toolkit 集合,主要研究的我看有两块,一块是深度学习框架本身,一块是深度学习可视化相关的

  1. 计算框架:
    1. tensorflow
    2. pytorch
    3. mxnet
    4. paddlepaddle
  2. 可视化:
    1. tensorboard
    2. VisualDL
    3. visdom

 


使用 LSTM 实现长期预测

pytorch LSTM:https://pytorch.org/docs/stable/generated/torch.nn.LSTM.html

最近看 google 的论文 autopilot,讲的是怎么通过一个app的历史的资源使用数据,来预测这个app到底需要多少资源,才能满足其服务的正常运行

论文讲了2种方法,就是:

  1. 一种是传统的,对历史数据加权
  2. 一种是基于增强学习的,这个有点复杂,没太看懂

不过我有个疑问,像资源使用这种时序的数据,用 lstm 这种循环神经网络不是最简单吗?为什么要用增强学习这么复杂的东西(状态空间复杂,状态的迁移也复杂,并且需要更大的计算量)

所以我打算试试用 lstm 来实现长期的资源预测

我的数据样本:

20220510000000,38.388889
20220510003000,33.722222
20220510010000,31.538889
...
20220510053000,31.183333
20220510060000,32.983333
20220510063000,36.694444

第一列是明文的时间,第二列是app的cpu使用

1. 思路

使用 lstm 有几个关键的地方:

  1. 定义好网络的输入输出,简单来说就是你希望神经网络帮你学习什么样的特征映射。
  2. 怎么实现长期预测

由于典型的 rnn 神经网络,是输入一串历史数据,输出下一个时刻的预测值。这是经典用法,可能也有一些方法,可以直接输出多个历史预测值的,不过我没研究过,这里不讨论

我的方法也比较简单,就是用预测出来的值作为神经网络的数据,继续滚动预测下一个值,通过这种方法,实现“长期”的预测

2. 网络(输入、输出)建模

首先要定义清楚网络的输入,输出是什么

由于经典的 rnn 只有一个输出,就是我们下一步要预测的值,所以这个没什么好讨论的

但是输入是可以多个的,你可以用当前最近的2个数据来预测下一个数据,也可以用当前10个数据预测下一个数据,也可以是N个。但是据我的实测观察来看,使用2个数据预测下一个数据效果好像更好一些

2个数据预测1个

0


VGG-16 神经网络结构详解

视频:https://www.bilibili.com/video/BV1FT4y1E74V?p=120

0

这节课要讲的第三个,也是最后一个范例是VGG,也叫作VGG-16网络。

值得注意的一点是,VGG-16网络没有那么多超参数,这是一种只需要专注于构建卷积层的简单网络。首先用3×3,步幅为1的过滤器构建卷积层,padding参数为same卷积中的参数。然后用一个2×2,步幅为2的过滤器构建最大池化层。因此VGG网络的一大优点是它确实简化了神经网络结构,下面我们具体讲讲这种网络结构。

0

假设要识别这个图像,在最开始的两层用64个3×3的过滤器对输入图像进行卷积,输出结果是224×224×64,因为使用了same卷积,通道数量也一样。VGG-16其实是一个很深的网络,这里我并没有把所有卷积层都画出来。

0

假设这个小图是我们的输入图像,尺寸是224×224×3,进行第一个卷积之后得到224×224×64的特征图,接着还有一层224×224×64,得到这样2个厚度为64的卷积层,意味着我们用64个过滤器进行了两次卷积。正如我在前面提到的,这里采用的都是大小为3×3,步幅为1的过滤器,并且都是采用same卷积,所以我就不再把所有的层都画出来了,只用一串数字代表这些网络。


AlexNet 神经网络结构详解

视频:https://www.bilibili.com/video/BV1FT4y1E74V?p=120 

我要举例说明的第二种神经网络是AlexNet,是以论文的第一作者Alex Krizhevsky的名字命名的,另外两位合著者是Ilya Sutskever和Geoffery Hinton。

0

AlexNet首先用一张227×227×3的图片作为输入,实际上原文中使用的图像是224×224×3,但是如果你尝试去推导一下,你会发现227×227这个尺寸更好一些。

第一层我们使用96个11×11的过滤器,步幅为4,由于步幅是4,因此尺寸缩小到55×55,缩小了4倍左右。

(以下请有兴趣的同学一定要推导一下,算一算才有长进~)

然后用一个3×3的过滤器构建最大池化层,f=3,步幅s为2,卷积层尺寸缩小为27×27×96。接着再执行一个5×5的卷积,padding之后,输出是27×27×276。然后再次进行最大池化,尺寸缩小到13×13。

再执行一次same卷积,相同的padding,得到的结果是13×13×384,384个过滤器。

再做一次same卷积,就像这样。

再做一次同样的操作,最后再进行一次最大池化,尺寸缩小到6×6×256。

6×6×256等于9216,将其展开为9216个单元,然后是一些全连接层。

最后使用softmax函数输出识别的结果,看它究竟是1000个可能的对象中的哪一个。