DevilKing's blog

冷灯看剑,剑上几分功名?炉香无需计苍生,纵一穿烟逝,万丈云埋,孤阳还照古陵

0%

首先是hadoop部分涉及到的虚存问题

关于虚存限制,Hadoop 有两个参数控制,第一个参数决定是否打开虚存检查,默认为 true,第二个参数指出虚存的 Limit 的计算规则是申请实存的 2.1 倍,这解释了为什么实存设置的小了之后虚存也小了。

yarn.nodemanager.vmem-check-enabled true Whether virtual memory limits will be enforced for containers.
yarn.nodemanager.vmem-pmem-ratio 2.1 Ratio between virtual memory to physical memory when setting memory limits for containers. Container allocations are expressed in terms of physical memory, and virtual memory usage is allowed to exceed this allocation by this ratio.

什么是虚存

虚存本质上是 OS 对内存资源的超售,技术上来说从 OS 角度有 Demand Paging,从进程角度有 Overcommit。而 Overcommit 是 OOM 的根源,内核对进程对内存的使用做了较为乐观的估计和假设,所以像 JVM 这种一起来就申请 (malloc) 巨大内存的情况虽然很多,但由于 Overcommit 机制所以能很好的运行。 又因为这种假设并不能放之四海而皆准,所以当遇到进程真的挤兑资源的时候(与 Overcommit 假设不符),操作系统会通过 OOM - Killer 机制来挽救。挽救的方式是使用一些特殊的策略来随机的杀死进程,在进程的角度看到的就是跑的好好地被 OOM Killed 了。

当然,日常使用中还存在另一种 OOM,不同于 Overcommit 策略被挤兑时才出现,在 CGroup 生效的时候进程通过 cgroup 与内核协商了最大的资源限制,一旦 RSS(实存) 超过此限制后同样会被 OOM Killed。

理解linux的memory overcommit

理解memory overcommit的关键:commit(或overcommit)针对的是内存申请,内存申请不等于内存分配,内存只在实际用到的时候才分配。

内核参数 vm.overcommit_memory 接受三种取值:

  • 0 – Heuristic overcommit handling. 这是缺省值,它允许overcommit,但过于明目张胆的overcommit会被拒绝,比如malloc一次性申请的内存大小就超过了系统总内存。Heuristic的意思是“试探式的”,内核利用某种算法(对该算法的详细解释请看文末)猜测你的内存申请是否合理,它认为不合理就会拒绝overcommit。
  • 1 – Always overcommit. 允许overcommit,对内存申请来者不拒。
  • 2 – Don’t overcommit. 禁止overcommit。

“sar -r”是查看内存使用状况的常用工具,它的输出结果中有两个与overcommit有关,kbcommit 和 %commit:
kbcommit对应/proc/meminfo中的 Committed_AS;
%commit的计算公式并没有采用 CommitLimit作分母,而是Committed_AS/(MemTotal+SwapTotal),意思是_内存申请_占_物理内存与交换区之和_的百分比。

1
2
3
4
$ sar -r 

05:00:01 PM kbmemfree kbmemused %memused kbbuffers kbcached kbcommit %commit kbactive kbinact kbdirty
05:10:01 PM 160576 3648460 95.78 0 1846212 4939368 62.74 1390292 1854880 4

解决的问题

要求从N个元素中随机的抽取k个元素,其中N无法确定。
这种应用的场景一般是数据流的情况下,由于数据只能被读取一次,而且数据量很大,并不能全部保存,因此数据量N是无法在抽样开始时确定的;但又要保持随机性

证明过程

假设数据序列的规模为 nn,需要采样的数量的为 kk。

首先构建一个可容纳 kk 个元素的数组,将序列的前 kk 个元素放入数组中。

然后从第 k+1 个元素开始,以 $\frac {k}{n}$ 的概率来决定该元素是否被替换到数组中(数组中的元素被替换的概率是相同的)。 当遍历完所有元素之后,数组中剩下的元素即为所需采取的样本。

对于第 i个数(i≤k)。在 k 步之前,被选中的概率为 1。当走到第 k+1 步时,被 k+1 个元素替换的概率 = k+1个元素被选中的概率 * i 被选中替换的概率,即为 $\frac{k}{k+1}×\frac{1}{k}=\frac{1}{k+1}$。则被保留的概率为 $1−\frac{1}{k+1}=\frac{k}{k+1}$。依次类推,不被 k+2个元素替换的概率为 $1−\frac{k}{k+2}×\frac{1}{k}=\frac{k+1}{k+2}$。则运行到第 n步时,被保留的概率 = 被选中的概率 * 不被替换的概率,即:

$$1×\frac{k}{k+1}×\frac{k+1}{k+2}×\frac{k+2}{k+3}×…×\frac{n−1}{n}=\frac{k}{n}$$

对于第 j 个数(j>k)。在第 j 步被选中的概率为 kjkj。不被 j+1j+1 个元素替换的概率为 1−kj+1×1k=jj+11−kj+1×1k=jj+1。则运行到第 nn 步时,被保留的概率 = 被选中的概率 * 不被替换的概率,即:

$$\frac{k}{j}×\frac{j}{j+1}×\frac{j+1}{j+2}×\frac{j+2}{j+3}×…×\frac{n−1}{n}=\frac{k}{n}$$

所以对于其中每个元素,被保留的概率都为 $\frac{k}{n}$.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class ReservoirSamplingTest {

private int[] pool; // 所有数据
private final int N = 100000; // 数据规模
private Random random = new Random();

@Before
public void setUp() throws Exception {
// 初始化
pool = new int[N];
for (int i = 0; i < N; i++) {
pool[i] = i;
}
}

private int[] sampling(int K) {
int[] result = new int[K];
for (int i = 0; i < K; i++) { // 前 K 个元素直接放入数组中
result[i] = pool[i];
}

for (int i = K; i < N; i++) { // K + 1 个元素开始进行概率采样
int r = random.nextInt(i + 1);
if (r < K) {
result[r] = pool[i];
}
}

return result;
}

@Test
public void test() throws Exception {
for (int i : sampling(100)) {
System.out.println(i);
}
}
}
  • 注意random部分
  • 以及r<K

保证$\frac{k}{n}$的概率

原文链接

利用arthas直接在线上定位问题的过程,主要使用 scgetstatic命令。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
$ sc io.netty.channel.nio.NioEventLoop
class-info io.netty.channel.nio.NioEventLoop
code-source file:/opt/app/plugins/tair-plugin/lib/netty-all-4.0.35.Final.jar!/
name io.netty.channel.nio.NioEventLoop
isInterface false
isAnnotation false
isEnum false
isAnonymousClass false
isArray false
isLocalClass false
isMemberClass false
isPrimitive false
isSynthetic false
simple-name NioEventLoop
modifier final,public
annotation
interfaces

super-class +-io.netty.channel.SingleThreadEventLoop
+-io.netty.util.concurrent.SingleThreadEventExecutor
+-io.netty.util.concurrent.AbstractScheduledEventExecutor
+-io.netty.util.concurrent.AbstractEventExecutor
+-java.util.concurrent.AbstractExecutorService
+-java.lang.Object

class-loader +-tair-plugin's ModuleClassLoader
classLoaderHash 73ad2d6

通过查看源码,了解相关的logger的实现

1
2
3
$ getstatic -c 73ad2d6 io.netty.channel.nio.NioEventLoop logger 'getClass().getName()'
field: logger
@String[io.netty.util.internal.logging.Slf4JLogger]

tair插件里的logback没有设置ROOT logger,所以它的默认level是DEBUG,并且默认的appender会输出到stdout里。

一些经验

  • logback默认的ROOT logger level是 DEBUG,输出是stdout
  • 利用arthas的 sc命令定位具体的类
  • 利用arthas的 getstatic获取static filed的值
  • 利用logger parent层联的特性,可以向上一层层获取到ROOT logger的配置