cgroup 内存管理之 page cache 回收

page cache 的管理,是内核内存管理里最复杂的一块,也是容器混部场景下,问题最多的地方
我们这里只关注读 cache 的处理,脏页的控制单独讲。所以这篇文章里,无特殊说明 page cache 默认不包括脏页部分
当我们谈到 page cache 时, 我们会关注什么?
有以下几个关键的点
  1. 什么时机会触发 page cache 回收?
  2. 回收过程是什么样的
  3. 不可回收的页面有哪些?
  4. 不容易回收的页面有哪些?
  5. 回收力度如何控制
接下来,我们就这几点,来讲一讲 page cache 的一些内核实现内幕。以及混部场景下,可能会遇到的一些坑
实际上,不同的回收方式,其时机、回收的页面范围、力度、算法都稍有不同,所以下面我们将按照不同的回收方式来详细讲

1. 整机 drop_caches 回收

内核接口 /proc/sys/vm/drop_caches
内核的代码实现入口在 fs/drop_caches.c 里面
这个接口支持3种方式:
  1. echo 1,清理 page cache
  2. echo 2,清理 slab,比如 dentry cache 通常也很消耗内存
  3. echo 3,两种都清理
我们这里只讨论方式1

1.1. 回收时机、力度、算法

只有人为的 echo xx > /proc/sys/vm/drop_caches 时,才会触发 page cache 回收
每次触发 drop_caches,基本上都会把系统能回收的 clean page 一次性全部回收回来,注意,是全部能回收的
所以,这里其实也没有什么的特殊的回收算法了,简单全遍历就完了

1.2. 回收过程

内核代码 fs/drop_caches.c
简单来说,就是
  1. 遍历所有的超级块,super_block
  2. 遍历每个超级块上的所有 inode 对象
  3. 根据 inode->i_mapping 找到每个 inode 的 address_space 空间
  4. 遍历 address_space 下的所有 page
    1. 将 page 从 radix tree 上删除
    2. 调用文件系统的 releasepage 函数释放文件系统资源。这个可以忽略,我看 fs/* 几乎所有文件系统都不实现这个函数了
  5. 释放所有能释放的 page 内存(引用计数为0)
核心逻辑的调用栈如下:
  • drop_caches_sysctl_handler
    • iterate_supers(drop_pagecache_sb, NULL)
      • drop_pagecache_sb, list_for_each_entry(inode, &sb->s_inodes, i_sb_list)
        • invalidate_mapping_pages(inode->i_mapping, 0, -1) // 这个函数的实现在 mm/truncate.c 文件里,Invalidate all the unlocked pages of one inode
          • invalidate_inode_page(page) for page in pagevec_lookup_entries(&pvec)
            • invalidate_complete_page() 删除page的mapping,并从 page cache 的radix-tree 里面剔除,因为下一步就直接 free 内存了
        • pagevec_release(&pvec) // 释放所有的 page 内存空间

1.3. 回收范围

drop_caches 是一个非常轻量级的回收过程,只回收能够立即释放的 page
从 invalidate_inode_page() 我们可以看到,有3种页面,是不会被回收的:
  1. 脏页
  2. 正在回写的页
  3. mmap + MAP_SHARED 方式映射到 page table 的页
  4. PG_SyncReadahead 需要多次drop才能回收
int invalidate_inode_page(struct page *page)
{
        struct address_space *mapping = page_mapping(page);
        if (!mapping)
                return 0;
        if (PageDirty(page) || PageWriteback(page))
                return 0;
        if (page_mapped(page))
                return 0;
        return invalidate_complete_page(mapping, page);
}
注意,page_mapping() 和 page_mapped() 不是一个东西。另外,!mapping 这段代码我没看懂是过滤的啥?
page_mapping() 返回 page 的 address_space,读的是 page->mapping 信息
(1)返回 NULL,说明该页要么是 slab cache,要么是 anon
(2)返回非空,可能是 swap_address_space(),或者就是正常页所在的一个 address_space
struct address_space *page_mapping(struct page *page)
{
        struct address_space *mapping;
        page = compound_head(page);
        /* This happens if someone calls flush_dcache_page on slab page */
        if (unlikely(PageSlab(page)))
                return NULL;
        if (unlikely(PageSwapCache(page))) {
                swp_entry_t entry;

                entry.val = page_private(page);
                return swap_address_space(entry);
        }
        mapping = page->mapping;
        if ((unsigned long)mapping & PAGE_MAPPING_ANON)
                return NULL;
        return (void *)((unsigned long)mapping & ~PAGE_MAPPING_FLAGS);
}

而 page_mapped 是用来判断 page 是否在 page table 里面。这里用 page_mapped() 主要是用来判断当前 page 是否是一个 mmap + MAP_SHARED 产生的页(因为 MAP_PRIVATE 产生的页不会填充到 page table 里面,具体可以自己看下代码)


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 锁定起来,达到保证业务性能的目的


AllReduce 算法的前世今生

原文:https://zhuanlan.zhihu.com/p/79030485

从事分布式深度学习相关工作的同学,应该都频繁地用到了AllReduce(规约)操作。

图1 AllReduce的示意图

但是对于训练框架中集成的AllReduce相关操作,其背后实现的原理是什么?

除了最近几年名声大噪的Ring AllReduce是否还有其他的AllReduce算法?

他们各自的性能开销如何?如何取舍?

本文尝试从一个较为全面的角度来展现AllReduce算法的前世今生,既分析经典算法,也介绍发展中的新秀。

MPI中的AllReduce算法

其实说到AllReduce,很多人脑海里的第一反应都是MPI_AllReduce。作为集合通信中的元老,和高性能计算领域的通信标准,在MPI_AllReduce这个通信原语背后,MPI中实现了多种AllReduce算法。

以openMPI源码为例,里面实现了多种allreduce的算法。具体的算法选择在

ompi/mca/coll/tuned/coll_tuned_decision_fixed.c

图2 openMPI源码中选择allreduce算法的代码片段

Dynamic writeback throttling

https://lwn.net/Articles/405076/
Dynamic writeback throttling最主要的核心思想就是IO带宽估算。
the bandwidth estimation allows the kernel to scale dirty limits and I/O sizes to make the best use of all of the devices in the system, regardless of any specific device’s performance characteristics.
传统writeback机制的做法是,当进程脏页超过一定比例时,调用balance_dirty_pages()函数进入同步写dirty pages过程,直到dirty pages的比例下降到一定比例,之后才允许该进程返回。
该机制存在三个问题:
  1. 进程脏页比率多少才合适?
  2. 内存压力太大时,多个后台进程同时writeback,会产生大量的随机IO,设备吞吐量下降
  3. 如何更准确的估算设备的真实带宽?
Dynamic writeback throttling的基本做法是:启发式的计算设备的真实带宽,所有进程不再同步writeback改由sleep一段时间,等待后台线程统一writeback。


理解 LSTM 神经网络

LSTM 的论文原址:http://www.bioinf.jku.at/publications/older/2604.pdf

英文原文:http://colah.github.io/posts/2015-08-Understanding-LSTMs/

本文在 https://blog.csdn.net/Leo_Xu06/article/details/78141321 的翻译基础上做了一些修改

递归神经网络(Recurrent Neural Network, RNN)

人类每时每刻的思考都不是孤立的从头开始,就像你在阅读这篇文章时,你对每个词的理解都是基于对先前词的理解而产生的,因为你的想法是具有时序关联性的。

传统神经网络的一个主要缺点是——做不到信息的时序关联。举个例子,想象一下你想区分一个电影某个时间点所发生的事件,传统的神经网络就做不到根据之前的事件来推理得到下一个事件。

递归神经网络(RNN)可以解决这一问题,它的网络结构中存在回环,使得之前的信息得以保留。

0

上面的示意图中,模块 A 接受输入x_t,并输出 h_t,环形结构允许信息从一个网络状态转移到下一个网络状态。

这些循环让循环神经网络看起来有点神秘。但是,你再仔细想下,就会发现它们与常见的神经网络没有什么不一样的地方。循环神经网络可以被认为是同一网络的多个副本,每个副本向后继者传递一个消息。把它展开之后是这个样子的:

0

该链式结构揭示了RNN与序列(sequences)和列表(lists)紧密关联,用这种神经网络结构处理特定数据(文本,语言等)也是直观自然的。在过去的几年中,已经有一些难以置信的RNN成功应用案例来解决各种问题:语音识别、语言建模、翻译、图片字幕……这个名单还在不断扩展,相关讨论见Andrej Karpathy的博文:The Unreasonable Effectiveness of Recurrent Neural Networks

这些成功背后的本质是“长短期记忆模型(LSTMs)”的使用,这是一种特殊的递归神经网络,它对很多任务都适用,而且相较于标准的RNN模型,它的性能要高出许多,几乎所有基于RNN令人激动的成果都是由它取得。本文也将探索这些 LSTM 模型


PerfIso: Performance Isolation for Commercial Latency-Sensitive Services

论文原址:https://www.usenix.org/system/files/conference/atc18/atc18-iorgulescu.pdf

演讲PPT:https://www.usenix.org/sites/default/files/conference/protected-files/atc18_slides_iorgulescu.pdf

这是微软在18年 usenix 会议上公开的一篇混部论文。这也是微软在混部这个领域公开的为数不多的论文之一。论文中描述了bing业务(非公有云)如何通过在离线混部 + CPU Blind Isolation 这个技术,将 indexserver 集群的利用率从21%提升到66%的过程

其中最关键的,还是 CPU Blind Isolation 这个技术,很有意思。


约束最优化:lagrange 对偶函数

学SVM的时候你会经常听到拉格朗日对偶。拉格朗日对偶是凸优化问题中比较常见的一个手段,关于凸优化,强烈建议Boyd的《凸优化》这本书,总结的很好

lagrange 对偶的基本思想,是将原目标函数,通过对其约束条件的加权求和,变成一个广义的目标函数。从而将一个M个变量N个约束的目标函数转换成一个M+N个变量的目标函数,再得到这个函数的对偶函数

这个对偶函数非常有用,因为他有几个重要性质:

  1. 无论原始问题是不是凸的,对偶问题一定是凸的,可以证明
  2. 对偶问题给出了原始问题的一个下界,可以证明
  3. 当满足一定条件时,对偶问题的解和原问题的解是等价的,可以证明

SVM 支持向量机

参考 mit 课程:https://web.mit.edu/6.034/wwwbob/svm.pdf

SVM 是机器学习领域非常流行的一个分类算法。他的前身其实就是感知机,想了解感知机原理的可以简单看我之前写的文章

传统感知机是有很多局限性的问题的,比如解决不了线性不可分的问题,比如泛化能力不强因为你可以找到N个解,但是不知道哪一个是最好的。SVM 就能很好的解决这些问题。

1. 支持向量

搞清楚什么是支持向量是了解SVM的前提。但是这里只能抽象的讲解,了解后面的内容,会加深你对支持向量的理解

在一个分类问题里,我们可能会有多个解,比如:

但是取哪个是最好的呢?直觉告诉我们,选能让样本分割的最远的超平面,这个就是 SVM 的做法,最大间隔化(后面会讲形式化的定义和推导)。SVM使用两个间隔最大的超平面,将所有数据完美分割在两边。

所以数学上,只需要一个超平面 + 间隔,就能形成一个决策边界,那些依附在决策边界上的点,就叫支持向量。为什么要给这些点起一个专有的名词?因为定义这个超平面只需要用到这些点,而其他远距离的点都是多余的。所以通常在一个2维空间上,最少只需要3个点,就能定义这个超平面


理解感知机模型

感知机是一种很传统和经典的分类算法了,现在机器学习的书基本上也很少详细讲了,大多还会简单介绍一下。看 ng 的机器学习课程,在讲 SVM 支持向量机的时候,从逻辑回归的代价函数直接修改到SVM的代价函数,实在难以理解,逻辑回归和SVM之间的关系和区别,还得好好想想

感知机模型有自身的局限性,它只能做线性分类。如果数据不是线性可分的,那还真没办法。但是了解感知机模型,对后面学习SVM很有帮助的

1. 感知机模型

感知机只能用来做线性分类,所谓线性分类是说,假设我们有一堆样本数据,我们必须找到一条线或者一个超平面,能把这堆样本划分到线的两边

preview

用数学的语言来说,如果我们有m个样本,每个样本对应于n维特征和一个二元类别输出,如下:


用 ReLU 整流单元逼近非线性神经网络

刚学习深度学习的时候,在网上曾经看过很多资料,始终搞不懂,为什么大家都说神经网络为什么能够无限逼近任意连续函数。大家普遍认为,理论上只要神经元足够多,神经网络足够大,就可以实现

后来知乎有个人写了偏文章解释了一下,有兴趣的可以移步这里:https://zhuanlan.zhihu.com/p/25590725

我困惑的根源在于:

  1. 神经网络说白了就是一个矩阵,神经元之间的链接就是各种矩阵运算
  2. 有很多激活函数是“线性”的,比如ReLU及其各种变种

当然,有些人认为ReLU是非线性激活函数,其实也没有错。但是我个人理解,叫分段线性函数,更合适一些。

那么问题来了,如果一个神经网络中,全部使用线性单元,无论经过多少次矩阵运算,最终训练出来的网络,不还是一个线性函数吗?