從資料庫故障探究Linux nproc演算法

你好我是李白發表於2021-01-30

    某日,朋友跟我討論他巡檢oracle資料庫時遇到的一個情況,在使用root使用者切換grid使用者時報錯

    -bash: fork: retry: Resource temporarily unavailable,一般這個報錯都是因為

    /etc/security/limits.conf或/etc/security/limits.d/下相關使用者nproc設定過小導致,

    但是定位一波三折,最終了解清楚了nproc引數生成、限制,將案例詳細分享,供大家參考。

# 故障背景

巡檢su – grid無法完成切換,報錯

```language

-bash: fork: retry: Resource temporarily unavailable。

```

## 環境介紹

作業系統為Redhat 6.8,資料庫版本為Oracle 11.2.0.4 RAC。

# 初步分析,獲取已存在程式limits環境設定

根據經驗,上述報錯一般為下面三個原因:

1. 使用者的nproc達到限制,無法建立新的程式

2. 系統沒有可分配的的pid,即程式號已經達到核心引數kernel.pid_max的限制

3. 系統可用記憶體低,新的程式無法申請到記憶體導致不能啟動


**下面我們一步一步排查:**

檢查使用者已經存在程式limits設定與使用者ulimit設定,檢查如下:

如果已經有會話登陸grid使用者,可以透過下面命令得到當前限制

![image.png](https://img-blog.csdnimg.cn/img_convert/6196c6f8d3fc2af77787edf76e034410.png)

 

如果已經無法正確切換grid使用者,可以使用如下步驟得到限制

## 第一步:獲取grid使用者任意程式pid

 ![image.png](https://img-blog.csdnimg.cn/img_convert/1f460cfd229c0dff6e5e9f595345d885.png)

## 第二步:使用cat檢視/proc/<pid>/limits獲取限制設定

 ![image.png](https://img-blog.csdnimg.cn/img_convert/d2043764a0b47c385bc1cbe62038f74b.png)

- 可以看到上述無論是已經登陸grid還是真實程式設定均為16384,設定均不算過低,RHEL預設/etc/sysctl.conf中核心引數kernel.pid_max為32768也足夠使用,grid使用者一般不會佔用這麼多的process,那到底是由於bug還是其他原因耗盡了設定呢還是記憶體或其他原因?

# 進一步分析,尋找limits.conf未生效原因

經過初步分析,初步判斷並非設定過小導致,16384設定並不算小,那接下來需要排查如下:

1. /etc/security/limits.conf與/etc/security/limits.d是否設定過低 ?

2. 是否/etc/profile、/etc/profile.d/、/etc/bashrc、家目錄下.bashrc、.bash_profile是否有相關ulimit配置指令碼設定nproc設定過低 ?

3. 是否grid使用者下真有超過16384 process佔用導致無法su – grid ?

4. 是否有程式real user不屬於grid但是effective user為grid消耗了grid所有程式數?

**接下來我們繼續一步一步排查**

經過排查,/etc/security/limits.conf設定如下:

![image.png](https://img-blog.csdnimg.cn/img_convert/25dd5f0a80764543b12163fa545e8efd.png)

可以看到limits.conf對grid使用者做了ulimit相關設定,nproc設定為204800,經過排查,由於/etc/profile在安裝RAC時按照文件設定瞭如下指令碼導致,所以limits.conf的nproc相關配置並未生效

```#Setting the appropriate ulimits for oracle and grid user

if [ $USER = "oracle" ]; then

    if [ $SHELL = "/bin/ksh" ]; then

       ulimit -u 16384

       ulimit -n 65536

    else

       ulimit -u 16384 -n 65536

    fi

fi


if [ $USER = "grid" ]; then

    if [ $SHELL = "/bin/ksh" ]; then

       ulimit -u 16384

       ulimit -n 65536

    else

       ulimit -u 16384 -n 65536

    fi

fi

```

# 獲取根因,到底是誰佔用了nproc

- 要獲取使用者下的真是佔用nproc,這裡需要講nproc到底是如何構成的

引用Redhat官網一段:

```language

RLIMIT_NPROC

              The maximum number of processes (or, more precisely on Linux, threads) that can be created for  the  real

              user ID of the calling process.  Upon encountering this limit, fork(2) fails with the error EAGAIN.


- nproc from /etc/security/limits.conf is to count the threads

- Threads differ from traditional multitasking operating system processes in that:

Raw

    * processes are typically independent, while threads exist as subsets of a process

    * processes carry considerably more state information than threads, whereas multiple threads within a process share process state as well as memory and other resources

    * processes have separate address spaces, whereas threads share their address space

    * processes interact only through system-provided inter-process communication mechanisms

    * context switching between threads in the same process is typically faster than context switching between processes.


At the kernel level, a process contains one or more kernel threads, which share the process's resources, such as memory and file handles – a process is a unit of resources, while a thread is a unit of scheduling and execution.


A process is a "heavyweight" unit of kernel scheduling, as creating, destroying, and switching processes is relatively expensive. Processes own resources allocated by the operating system. Resources include memory (for both code and data), file handles, sockets, device handles, windows, and a process control block.


A kernel thread is a "lightweight" unit of kernel scheduling. At least one kernel thread exists within each process. If multiple kernel threads can exist within a process, then they share the same memory and file resources.

```

- 可以看到nproc是指thread,並不是process,也就是我們需要使用**ps -L**選項獲取nproc佔用總數

命令如下:

```language

# ps -eLo ruser| sort |uniq -c|sort -rn

注:按real user分類,nproc限制real user執行緒數,程式建立分為real user與effective user,我們統計real user。

```

我們經過檢視當前伺服器執行緒數,如下:

```language

# ps -eLf|grep grid|wc -l

44609

注:當時未注意使用ruser統計真實執行緒數,所以上述44609實際要比真實grid使用者佔用的執行緒大很多,但是依然可以從中看出確實執行緒數佔用很多,基本可以判斷16384的nproc設定不足以支撐這麼多會話導致su – grid時報錯-bash: fork: retry: Resource temporarily unavailable

```

我們知道了確實是nproc不足導致無法su – grid,那到底是誰佔用了這麼多執行緒呢?

透過簡單ps -ef輸出,即可一眼判斷下面截圖程式異常暴漲導致:

 ![image.png](https://img-blog.csdnimg.cn/img_convert/5e730e04ba720b34cc0f9e8b0aa8ad6c.png)


根據經驗,一般這種情況均由於bug或者一些配置方面問題導致,透過搜尋Mos,發現對於ons -d程式異常暴漲有如下文章:

```language

ONS has Thousand Processes Threads and Still Increasing (Doc ID 1547703.1).pdf

```

- 文章意為,/etc/hosts在loopback地址行寫入了hostname,導致ons -d成千且每分鐘增長一個。

- 檢查/etc/hosts,發現確實將hostname寫到了lookback內容行。

- 至此,找到了原因,那解決就相對簡單了,修改/etc/hosts,想減少ons -d還需要重啟叢集,減少ons -d,使程式恢復正常。

# 追根溯源,nproc是怎麼計算的?

## 那麼nproc是如何計算的呢,我們如何更合理的設定該值呢?

### 1. nproc控制

- 檔案:/etc/profile 、/etc/profile.d/下指令碼、/etc/bashrc、家目錄.bashrc、.bash_profile

- 命令:ulimit設定。

### 2. 在不設定情況下預設值

可以從redhat官網文章:

```

What are the default ulimit values and where do they come from?

```

看到如下說明:

```

- On RHEL 6 and 7, nproc is unlimited for root by default (directly inherited from the kernel). For the other users, this value is equal to 4096, because it is limited at the PAM level by one of these files:

/etc/security/limits.d/90-nproc.conf on RHEL 6

/etc/security/limits.d/20-nproc.conf on RHEL 7


- On RHEL 8, these files have been removed, and the nproc default for the users is inherited from the kernel.

- 可以看到在不設定情況下,預設除root使用者外,在RHEL6、RHEL7預設為4096。


```

### 3. nproc上限如何計算呢?

可以從Redhat官網文章

```

How is the nproc hard limit calculated

```

可以找到如下計算公式說明:

```

For nproc, the limit is calculated in the kernel before the first process is forked in kernel/fork.c called by start_kernel:


RLIMIT_NPROC = max_threads/2


- The value of these variables are:


-> max_threads = mempages / (8 * THREAD_SIZE / PAGE_SIZE);

        mempages comes from the function argument : fork_init(totalram_pages);

-> #define THREAD_ORDER    2

-> #define THREAD_SIZE  (PAGE_SIZE << THREAD_ORDER)

-> PAGE_SIZE = 4096 (but useless)


```

#### 說明

公式為

```

RLIMIT_NPROC= max_threads/2

```

那麼max_threads如何計算呢?

```

max_threads = mempages / (8 * THREAD_SIZE / PAGE_SIZE)

```

mempages為實體記憶體頁數,可透過如下命令獲得:

```

# dmesg | awk '/Total.pages:/ {print $NF}' 

```

真實值也可透過下面命令獲得:

```

# dmesg |grep Mem

```

thrad_size為執行緒尺寸,可透過如下計算公式獲得:

```

THREAD_SIZE  (PAGE_SIZE << THREAD_ORDER)

```

其中page_size可透過命令

```

# getconf PAGE_SIZE       獲得,RHEL6 7均為4096

```

THREAD_ORDER在RHEL 6 7中均為2.

上述公式含義為:**將PAGE_SIZE轉化為2進位制,然後左移THREAD_ORDER位,即將4096轉化為2進位制,向左移動2位,2進位制向左移動2位,即2^2=*4。**

所以公式演變為:

```

THREAD_SIZE=PAGE_SIZE*4

```

則最終RHEL6/RHEL7 x86_64中公式演變為

```

RLIMIT_NPROC= max_threads/2

max_threads = mempages / (8 * THREAD_SIZE / PAGE_SIZE)

RLIMIT_NPROC=( mempages / (8 * THREAD_SIZE / PAGE_SIZE))/2

RLIMIT_NPROC=( mempages / (8 * PAGE_SIZE*4 / PAGE_SIZE))/2

RLIMIT_NPROC=( mempages / (8 * 4 ))/2

RLIMIT_NPROC= mempages / 64

```

計算示例:

```

# dmesg|grep Mem

[    0.000000] Memory: 4015580k/4718592k available (6764k kernel code, 524744k absent, 178268k reserved, 4433k data, 1680k init)

```

使用上述4015580代入公式得到如下值:

```

# echo "4015580/4/32/2"|bc

15685

# ulimit -Hu

15685

```

**可以看到確實計算一致,也就是當預設不設定limits.conf時,root使用者預設的hard nproc為上述公式計算值。**


**注:真實情況下,由於核心版本不同,相關引數可能發生變化,也由於啟動時,記憶體分配不同,會導致計算出來與實際值存在一些偏差,但是偏差不會很大。**

### 4. 那使用記憶體計算的nproc是硬限制嗎?

- 透過測試,雖然使用記憶體計算出來的nproc是有限的,且相對更為合理,但是並不是說該值為硬限制,依然可以透過檔案limits.conf、profile.d、limits.d、bashrc、ulimit等方式設定大於計算出來的值。

- 但是一般建議透過計算設定更為合理,防止記憶體耗盡。


參考:

What are the default ulimit values and where do they come from?

How is the nproc hard limit calculated

ONS has Thousand Processes Threads and Still Increasing (Doc ID 1547703.1).pdf


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31439444/viewspace-2754801/,如需轉載,請註明出處,否則將追究法律責任。

相關文章