sdb报告-10 错误问题定位

# sdb报告-10 错误问题定位
在sdb 的集群环境中,如果面对的是一个高并发的操作场景,有时候会莫名其妙地报告 -10 错误。

在 sdb 的错误列表中,-10 错误代表:系统错误。

这是一个笼统的错误描述,也是一个让人迷惑的错误描述。"系统错误"?什么鬼的系统错误,究竟是什么地方导致了系统错误?

作者通过分析大量的 -10 错误案例,发现大部分情况下,都是由于线程创建失败。

一般抛出 -10 错误的都是数据节点,并且一般数据节点的 diaglog 日志关键的错误信息如下

```
Failed to create new agent: boost::thread_resource_error: Resource temporaily unavailable
Failed to create new agent, probe = 30
Failed to create subagent thread, rc = -10
Failed to start session EDU, rc = -10
```

# linux 线程创建失败,原因分析

一般操作linux 系统在创建线程时,会受限于哪些参数呢,主要有两个

* 文件句柄数限制
* 内存资源

## 系统句柄

我们先来介绍文件句柄数
在linux 操作系统中,号称一切皆为文件,无论是进程、线程、socket 还是其他,最终都会被操作系统归为文件操作。操作系统或者进程,每申请一个资源,例如线程、socket,都会打开一个文件,那么这个文件打开状态,就可以简单理解为文件句柄。

那么句柄数限制又是什么呢?其实就是操作系统,或者某个进程所能够打开的最多文件的数量的限制。

大家有了这个概念后,我们再来看操作系统是如何对文件句柄数进行限制的。

在操作系统中,有一个神奇的命令 -- ulimit ,专门设置这些奇奇怪怪的限制值,进程最大文件句柄数和用户最大进程数就是其中的设置。

### 进程最大文件句柄数

例如我们可以查看 root 用户的 ulimit 输出, -n open file = 1024 就是root 用户允许一个进程打开的最大文件句柄数。

这里有一个小细节需要大家注意的。

由于root 用户是linux 中的管理员用户,所以如果root 用户的 ulimit open file 设置成 1024, 那么其他的用户,例如:test、mysql 用户等,想将 ulimit opon file 设置成 大于 1024,是不允许的。

这个大家一定要知道,如果希望将普通用户的 ulimit 值修改得很大,一定要先修改 root 用户的值。

### 用户最大进程数
一个系统用户,也有它最大的进程数,参数为 ulimit -u 命令。

该参数的官方解释如下:

```
Linux itself has a Max Processes per user limit. This feature allows us to control the number of processes an existing user on the server may be authorized to have
```

虽然解释是一个用户的进程限制,但是从使用效果分析,对线程数也有明显限制效果,毕竟线程是从进程中派生出来的资源。

一个用户的进程数限制,修改的方式和 ulimit -n 的方式一样。

### 系统总句柄限制

另外,关于句柄数的限制,还不单单是进程中和一个用户的句柄数限制,还有整个操作系统的句柄数限制,因为一个操作系统,总不能无限地打开句柄的。所以又引入另外一个设置,操作系统最大打开的句柄数限制。

以 centos 7 系统为例,该参数是被保存在 /proc/sys/fs/file-max 文件中

如果操作系统总的句柄数已经达到上限,那么即使进程还没有启动几个线程,也会出现句柄不够的情况。

如果希望临时修改操作系统最大句柄数的设置,可以直接执行

```
echo 2000000 > /proc/sys/fs/file-max
```

如果希望永久修改操作系统最大句柄数的设置,可以编辑 /etc/sysctl.conf 文件,增加 fs.file-max = 2000000 内容,然后在root 用户中执行

```
sysctl -p
```

## 系统内存

我们接着来介绍内存资源
因为在创建线程时,在linux 中,是需要给它预先分配内存的 – 也叫栈大小,用来存储线程中数据的值。

这里再科普另外一个知识,一个程序中,内存主要分为两个大的部分,一个称为 “堆”,一个称为“栈”。“堆”是程序用来保存常量和变量名字的,“栈”则是程序来用保存具体的变量数字的。

好,背景知识介绍完毕后,开始进入正题。

### 线程栈大小影响

开始时说到,如果系统内存资源不足,也是无法创建线程的。这个原因就是在于创建线程时,操作系统需要分配一块内存给线程,这个内存是多大呢,就是 ulimit 中 -s stack size 的大小。如果操作系统连 stack size 大小的内存都无法 malloc 了,创建线程就会失败。

有一些读者可能会奇怪,为啥这么一点内存都没有了?

其实如果仔细查看操作系统,你就会发现,那么多进程,每个进程又是那么多线程在运行,每个线程都在申请内存(注意,这块的内存是虚拟内存),内存不足正常的很。这个也容易让人联想到JVM 的OOM ,但是他们真的不是一回事,大家千万不要误会。

要解决这个问题也比较简单 –- 简单粗暴?就是将 ulimit 中 -s stack size 调小一点,每个线程不要申请那么多内存了,操作系统的内存资源就会更加的充裕。毕竟进程、线程这些,都是用完就完了,不可能都永久占用内存的。

在centos 7 的系统中,默认的 stack size 是8MB,一般使用默认值,都不会出现 "创建线程" 的错误,只有当极限情况才会对用户造成影响。

### 内存地址检查

在linux 系统中,内存管理是一个细致活。

其实在linux 的系统内存管理中,有两层

* 物理内存
* 虚拟内存

物理内存很好理解,就是用户为操作系统配置的RAM 大小,也是人们日常所说的服务器内存大小。

虚拟内存,是操作系统为了更好管理物理内存而设计出来的一层虚拟资源。因为操作系统在运行过程中,很多进程都在使用、释放内存,真实 free 的物理内存必然是不连续的。

所以操作系统为了让程序在使用内存时,更加方便的使用物理内存,构造了一层虚拟内存。

进程在向操作系统 malloc 一段连续的内存时,实际从虚拟内存中申请了一段连续的地址。在操作系统的内存映射表中,记录了虚拟内存地址指向真实的物理内存地址。

同时,即使程序 malloc 了内存,但实际上在 malloc 后,并不会真的立马占用了物理内存,只有当真正开始保存数据时,才会实际使用物理内存。

介绍到这里,读者们可能就对内存 malloc 有所了解了。

提问:如果操作系统真的是没有内存了而导致无法创建新的线程,那么如何定位这个问题呢?

在linux 操作系统中,实际上时刻都在记录内存的使用情况。

用户可以通过以下命令获得重要信息

```
cat /proc/meminfo | grep Commit
```

该命令将打印两个重要的数值
* CommitLimit
* Committed_AS

CommitLimit 参数,是操作系统最大可以 malloc 的内存的大小。

Committed_AS 参数,是操作系统当前已经被 malloc 了的内存的大小。

所以简单的总结,就是当 Committed_AS 参数的值无限接近 CommitLimit 参数的值时,证明当前的操作系统,真的没有太多可用内存可以继续给 malloc 了。

### 内核参数中的内存管理
针对linux 系统的内存控制,实际上真的是一个细致活。

它对内存的管理,各种参数有着交叉互相影响关系,作者在这里尝试使用毕生功力,尽量为读者解释明白。读者如果还有疑问,请咨询谷姐和度娘,作者本人可能会对大家热情的提问,累觉不爱🙈。

#### CommitLimit
首先从/proc/meminfo 中的 CommitLimit 参数开始介绍。
该参数的数值,和 3 个内核参数有关系,分别是
1. /proc/sys/vm/overcommit_memory
2. /proc/sys/vm/overcommit_ratio
3. Swap 空间大小

/proc/sys/vm/overcommit_memory 参数是linux 对内存管理中非常重要的参数,默认值为:0。
它可以取值 0,1,2,分别含义是
* 0,表示内核将检查是否有足够的可用内存供应用进程使用,如果有足够的可用内存,内存申请允许;否则,内存申请失败,并把错误返回给应用进程;
* 1,表示内核允许分配所有的物理内存,而不管当前的内存状态如何;
* 2,表示内核允许分配超过所有物理内存和交换空间总和的内存

/proc/sys/vm/overcommit_ratio 参数,默认值为:50,只有当 /proc/sys/vm/overcommit_memory = 2 时才会生效,表示可以被使用的最大物理内存比例

Swap 空间的大小,就是操作系统被设置的交换空间的大小。

当 /proc/sys/vm/overcommit_memory = 2 时,CommitLimit 参数值的计算公式为:
CommitLimit = Swap + 物理内存 * (/proc/sys/vm/overcommit_ratio / 100)

#### 什么时候使用Swap 交换空间
在linux 的vm 内核参数中,有一个关于控制 Swap 交换空间的使用参数 -- /proc/sys/vm/swappiness

这个参数很有意思,是告诉操作系统应该什么时候开始使用 Swap 交换空间,默认值为:60。

首先啰嗦介绍一下这个参数的设置范围:【0-100】。

当 /proc/sys/vm/swappiness 参数被设置为60 时,官方介绍是:当剩余物理内存低于40%(40=100-60)时,开始使用交换空间。

> 作者觉得这个解释很绕口,通俗点解释,就是:使用物理内存超过 60% 时,就开始使用交换空间。

在SequoiaDB 的官网介绍中,是建议将该参数设置为 0, 含义是:当剩余的物理内存少于 /proc/sys/vm/min_free_kbytes 参数所设置的值时,才开始真正使用 Swap 交换空间。

参照 SequoiaDB 官网内核修改建议
```
vm.swappiness = 0
vm.min_free_kbytes = <物理内存大小的8%,单位KB,最大不超过1GB>
vm.overcommit_memory = 2
vm.overcommit_ratio = 85
```

可以理解为:当真正 free 的物理内存小于 vm.min_free_kbytes 时,才能够开始使用 Swap。但是由于 vm.overcommit_ratio 参数的限制,基本可以判断,真正 free 的物理内存不会小于 vm.min_free_kbytes 参数值。所以按照 SequoiaDB 官网的建议,Swap 空间是不可能被使用的。

读者们读到这里,会产生一个疑问:如果 CommitLimit 数值是包括了 Swap 空间大小,但是 Swap 空间又禁止使用。那么当物理内存真的 OOM 了,Linux 会发生什么呢?

#### Linux 进程冷酷 killer
Linux 系统也会出现 Out Of Memory (万恶的OOM),这个时候,Linux kernel 就会根据设置,派出“进程冷酷 killer” 来杀进程了。

首先,Linux 的 /proc/sys/vm/panic_on_oom 参数定义了,当出现OOM 时,是否要执行 kill 进程的动作。

/proc/sys/vm/panic_on_oom 参数默认为0,可选值为
* 0,表示OOM 时,派出 killer 杀进程
* 1,表示关闭killer 功能,当出现OOM时,Linux 直接进入kernel panic(通俗地讲:死给你看)

kill 进程的策略由 /proc/sys/vm/oom_kill_allocating_task 参数控制,默认值为:0,可选值为
* 0,哪个进程最坏,就kill 谁
* 非0,哪个进程触发了 OOM,就kill 谁

至于如何评价哪个进程更坏,评分规则过于复杂,已经超出考试大纲,不再累述。

#### 为啥明明还有 free 内存,为啥依然因为内存不足而导致创建线程失败
这个问题,在前面各种知识点中其实已经陆陆续续解释了。

读者们首先需要知道,当进程在创建线程时,需要首先申请一段固定的内存,这个初始内存,被stack size 参数所控制。

而操作系统在申请这段固定的内存时,会受到 CommitLimit 参数的限制。

程序在 malloc 内存后,操作系统只是分配了一段虚拟内存给程序,物理内存只有在真实存储数据时,才会被使用。

申请的虚拟内存的地址,会被累加到 Committed_AS 数值中。

所以,如果有程序不断地 malloc 内存,而又不保存数据,此时,就会出现 free 命令中,free 的内存还有富余,但是依然无法再 malloc 内存了。

所以create thread 失败,根本原因不是内存不足,而是无法 malloc 内存了。这两者实际含义完全不同,读者们需要区分清楚的。

# 内核参数调优
如果程序真的发生了无法创建线程错误,可以遵循以下原则进行优化

* 设置 ulimit -n 参数,尽可能调大一点
* 设置 ulimit -u 参数,尽可能调大一点
* 设置 ulmit -s 参数,可以考虑适当调小一点
* 设置 /proc/sys/vm/overcommit_memory 参数,将其设置为 2
* 设置 /proc/sys/vm/overcommit_ratio 参数,适当调大一点,但是建议不要设置超过 90
* 为系统配置适当大小的 Swap 交换空间

# 其他的知识点

因为有一些读者在解决这类问题时,执行 ulimit -a 命令,发现参数都设置正常,为啥还是不行?

这里就需要和读者说道说道。

你看见 ulimit -a 是好的,但是你怎么知道进程用的就是你设置的值呢?

所以眼见为实,读者解决问题时,应该要真正确认sdb 进程的ulimit 参数是啥。

方式有两个

在sdb的较新版本中,节点启动时的diaglog 日志,会打印它自己的 ulimit 参数,读者可以去翻翻日志
另外一种就更加直接,直接查看 linux 的系统记录。例如知道 11910 进程的 PID 是 123456,就直接打开 /proc/123456/limits 文件,查看里面的内容,这样想不知道,都难

关于句柄数和线程的命令

查看 某个进程总共开启了多少个 线程,可以

```
cat /proc/$PID/status | grep Threads
pstree -p $PID ,然后+1,因为还有主进程
top -Hp $PID,然后查看头部 “Threads”参数
ps hH p $PID | wc -l
```

查看linux 目前总打开的句柄数

```
lsof -n|awk '{print $2}'|sort|uniq -c|sort -nr|awk '{print $1}' | awk '{sum += $1};END {print sum}'
```

查看某个进程打开的总句柄数

```
lsof -n|awk '{print $2}'|sort|uniq -c|sort -nr | grep $PID
```

posted @ 2019-07-24 14:49  chenfool  阅读(575)  评论(0编辑  收藏  举报