liuhao163.github.io

杂七杂八


  • Home

  • Categories

  • Tags

  • Archives

  • Sitemap

系统优化-内存-Linux内存工作原理

Posted on 2020-04-27 | Edited on 2022-09-21 | In 性能优化 , 内存

日常生活中我们常提起的内存主要指物理内存,又称为主存。一般指DRAM。linux的进程出于系统保护的目的不能直接访问物理能存,而是为每一个进程开辟了一块虚拟内存,它的地址是连续的,进程可以很方便的访问内存。

虚拟内存又分为用户空间和内核空间。

  • 32位系统中虚拟内存最大支持4gb,系统空间大小是1gb占虚拟内存的高位,用户空间大小是0~3gb占虚拟内存的低位
  • 64位操作系统则是内核空间大小是128T占虚拟内存的高位,中间是未定义,用户空间大小是128T占虚拟内存的低位

    如图:
    avator

    进程处于用户态只能访问虚拟内存的用户空间,比如程序的赋值等简单操作,当进程处于内核态时才能操作虚拟内存的内核空间。

内存映射

这样如果所有的进程的虚拟内存加起来可能会远远大于物理内存,而linux实际上并不是给所有的虚拟内存分配物理内存,只会在进程需要时候才会分配物理内存。并且分配后的物理内存是通过内存映射来管理的。

由于进程不能直接操作物理内存,所以每一个进程就会持有一个内存页,用来维护虚拟内存和物理内存的映射关系,可以很方便的操作物理内存。这些内存页被保存在CPU的MMU模块中,CPU可以通过MMU模块来访问进程需要的内存。如图:

avator

注意:由于分配虚拟内存的进程并不会直接去分配物理内存,而是MMU发现没有找到内存的物理地址时候出现缺页异常,这时候进程会切换到内核态分配内存,再刷新MMU和内存页返回到用户态继续执行用户程序。

MMU的单位是4KB成为PTE(page table entity),每一次内存映射,都需要关联4KB或者4KB整数倍的内存空间。这样有个问题,会需要大量的页表项比如一个4gb的内存就要100万个PTE。一般用多级表和大表来解决。多级表实际上就是索引的概念,linux采用4级表来解决。

avator

内存分配

内核空间我们暂不讨论,用户空间分为

avator

  1. 只读段,包括代码和常量等。
  2. 数据段,包括全局变量等。
  3. 堆,包括动态分配的内存,从低地址开始向上增长。
  4. 文件映射段,包括动态库、共享内存等,从高地址开始向下增长。
  5. 栈,包括局部变量和函数调用的上下文等。栈的大小是固定的,一般是 8 MB

malloc() 是 C 标准库提供的内存分配函数,对应到系统调用上,有两种实现方式,即 brk() 和 mmap()。

  1. 对于小于(128k)的内存采用brk()来分配,通过移动堆顶的位置来分配内存。这些内存释放后并不会立刻归还系统,而是被缓存起来,这样就可以重复使用。优点:不会立刻归还系统可以重复利用,缺点:频繁申请、释放会造成内存碎片
  2. 对于大于(128k)的内存采用mmap()来分配,则直接使用内存映射 mmap() 来分配,也就是在文件映射段找一块空闲内存分配出去。优点:减少磁盘碎片。缺点:释放归还系统所以频繁出现缺页异常增大系统负担

对内存来说,如果只分配而不释放,就会造成内存泄漏,甚至会耗尽系统内存。所以,在应用程序用完内存后,还需要调用 free() 或 unmap() ,来释放这些不用的内存。当然,系统也不会任由某个进程用完所有内存。在发现内存紧张时,系统就会通过一系列机制来回收内存,比如下面这三种方式:

  1. 回收缓存,比如使用 LRU(Least Recently Used)算法,回收最近使用最少的内存页面;
  2. 回收不常访问的内存,把不常用的内存通过交换分区直接写到磁盘中;
  3. 杀死进程,内存紧张时系统还会通过 OOM(Out of Memory),直接杀掉占用大量内存的进程。

    可以通过以下文件调整oom的权重,越大越容易杀死

    1
    echo -16 > /proc/$(pidof sshd)/oom_adj

如何查看内存使用

Swap,资源紧张时候Linux会将内存数据映射到磁盘上,这种情况会严重的影响系统性能,要尽量避免。

top or ps:主要查看VIRT-虚拟内存 RES–常驻内存 SHR–共享内存 SWAP–文件交换区 %MEM–使用率

系统优化-CPU-阶段性总结

Posted on 2020-04-26 | Edited on 2022-09-21 | In 性能优化 , CPU

排查思路

首先我们判断CPU的问题应该先看CPU使用率【CPU的idel也行】,和其类似的还有CPU的平均负载,使用率表示的是瞬时状态,平均负载表示的是趋势。
查看的工具我们可以用top、uptime、vmstat等

常见的几个指标:

  • user+ni过高一般我们去优化程序
  • sy过高有可能是cpu内核线程调度出问题
  • iowait高有可能是硬件除了问题。
  • 软中断和硬中断高,通常说明系统发生了大量的中断。

具体哪些指标见下图:

avator

常用工具

avator

avator

排查问题路径

avator

性能优化的经典

avator

性能之巅:https://book.douban.com/subject/26586598/

系统优化-CPU-软中断以及软中断过多带来的危害

Posted on 2020-04-25 | Edited on 2022-09-21 | In 性能优化 , CPU

什么是软中断

因为响应硬件的事件优先级高于我们正在运行的进程,所以CPU为了响应硬件,需要中断正在运行的进程。这个中断就是我们今天要讨论的中断。

当cpu响应中断的时候回临时关闭中断,也就是说其他的中断都需要响应完现有的中断才行,这时候就会出现中断丢失或者中断延迟比较大的现象,那linux是如何解决的呢?

linux将中断变为上下俩部分

-上部分:主要处理响应和硬件相关的事件,它会关闭中断响应,特点是速度很快,处理完毕后会发送软中断信号,我们称他为硬中断;
-下部分:根据中断的类型处理具体的事件逻辑,它由内核线程负责,在Linux中,每个CPU都对应一个软中断内核线程,名字是ksoftirqd/【CPU编号】,处理硬中断中的数据然后发送给需要的用户线程,我们称他为软中断,特点是延迟执行。

软中断的类型

我们可以通过查看/proc/softirqs来看各cpu的中断情况,由于软中断是内核线程所以在top中我们看到的软中断进程是[ksoftirqd/0]这样的

1
2
3
4
5
6
7
8
9
10
11
12
$ cat /proc/softirqs
CPU0 CPU1
HI: 0 0
TIMER: 811613 1972736
NET_TX: 49 7
NET_RX: 1136736 1506885
BLOCK: 0 0
IRQ_POLL: 0 0
TASKLET: 304787 3691
SCHED: 689718 1897539
HRTIMER: 0 0
RCU: 1330771 1354737

一般来讲每种软中断应该在各cpu上运行的次数差不多,但是TASKLET例外,它是最常用的软中断实现机制,每次运行一次就会推出,且只在调用它的cpu上运行。

软中断带来的CPU使用率够高的性能问题

工具准备

  • sar 是一个系统活动报告工具,既可以实时查看系统的当前活动,又可以配置保存和报告历史统计数据。
  • hping3 是一个可以构造 TCP/IP 协议数据包的工具,可以对系统进行安全审计、防火墙测试等。
  • tcpdump 是一个常用的网络抓包工具,常用来分析各种网络问题。

    测试的方案

  1. 我们准备一个nginx的容器
  2. 我们通过hping3来模拟对nginx的SYNC_FLOOD攻击。

    现象:系统的吞吐会降低,响应变慢,用top查看结果发现cpu占用不高,但是都用在软中断上,我们可以去查看软中断。

1
2
3
4
5
6
7
8
9
10
11
12
13
#watch可以查看变化并且-d可以看到变化的数据
$ watch -d cat /proc/softirqs
CPU0 CPU1
HI: 0 0
TIMER: 1083906 2368646
NET_TX: 53 9
NET_RX: 1550643 1916776
BLOCK: 0 0
IRQ_POLL: 0 0
TASKLET: 333637 3930
SCHED: 963675 2293171
HRTIMER: 0 0
RCU: 1542111 1590625

发现NET_RX变化很快,初步判断是网络引发用sar来看下玩过的情况

1
2
3
4
5
6
7
8

# -n DEV 表示显示网络收发的报告,间隔1秒输出一组数据
$ sar -n DEV 1
15:03:46 IFACE rxpck/s txpck/s rxkB/s txkB/s rxcmp/s txcmp/s rxmcst/s %ifutil
15:03:47 eth0 12607.00 6304.00 664.86 358.11 0.00 0.00 0.00 0.01
15:03:47 docker0 6302.00 12604.00 270.79 664.66 0.00 0.00 0.00 0.00
15:03:47 lo 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
15:03:47 veth9f6bbcd 6302.00 12604.00 356.95 664.66 0.00 0.00 0.00 0.05
  • 第一列:表示报告的时间。
  • 第二列:IFACE 表示网卡。
  • 第三、四列:rxpck/s 和 txpck/s 分别表示每秒接收、发送的网络帧数,也就是 PPS。
  • 第五、六列:rxkB/s 和 txkB/s 分别表示每秒接收、发送的千字节数,也就是 BPS。后面的其他参数基本接近 0,显然跟今天的问题没有直接关系,你可以先忽略掉。

eth0这个网卡接收远大于发送,查看报文的大小,664*1024/12607=54字节,这是一个小包。

我们用tcpdump抓包

1
2
3
4
5
# -i eth0 只抓取eth0网卡,-n不解析协议名和主机名
# tcp port 80表示只抓取tcp协议并且端口号为80的网络帧
$ tcpdump -i eth0 -n tcp port 80
15:11:32.678966 IP 192.168.0.2.18238 > 192.168.0.30.80: Flags [S], seq 458303614, win 512, length 0
...
  • 从 tcpdump 的输出中,你可以发现192.168.0.2.18238 > 192.168.0.30.80 ,表示网络帧从 192.168.0.2 的 18238 端口发送到 192.168.0.30 的 80 端口,也就是从运行 hping3 机器的 18238 端口发送网络帧,目的为 Nginx 所在机器的 80 端口。
  • Flags [S] 则表示这是一个 SYN 包。再加上前面用 sar 发现的, PPS 超过 12000 的现象,现在我们可以确认,这就是从 192.168.0.2 这个地址发送过来的 SYN FLOOD 攻击。

系统优化-CPU-排查僵尸进程和io问题

Posted on 2020-04-24 | Edited on 2022-09-21 | In 性能优化 , CPU

我们的系统如果遇到io问题和过多僵尸进程的排查思路

io问题

老规矩用top查看系统的进程,如果发现cpu很高但是user,sys,nic等指标不高,但是wa很高,说明可能存在了io问题
我们可以采用新的工具dstat,他可以同时查看cpu和io问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 间隔1秒输出10组数据
$ dstat 1 10
You did not select any stats, using -cdngy by default.
--total-cpu-usage-- -dsk/total- -net/total- ---paging-- ---system--
usr sys idl wai stl| read writ| recv send| in out | int csw
0 0 96 4 0|1219k 408k| 0 0 | 0 0 | 42 885
0 0 2 98 0| 34M 0 | 198B 790B| 0 0 | 42 138
0 0 0 100 0| 34M 0 | 66B 342B| 0 0 | 42 135
0 0 84 16 0|5633k 0 | 66B 342B| 0 0 | 52 177
0 3 39 58 0| 22M 0 | 66B 342B| 0 0 | 43 144
0 0 0 100 0| 34M 0 | 200B 450B| 0 0 | 46 147
0 0 2 98 0| 34M 0 | 66B 342B| 0 0 | 45 134
0 0 0 100 0| 34M 0 | 66B 342B| 0 0 | 39 131
0 0 83 17 0|5633k 0 | 66B 342B| 0 0 | 46 168
0 3 39 59 0| 22M 0 | 66B 342B| 0 0 | 37 134

图中所示,idl掉的时候都是read升高。于此我们判断io出现问题,通过perf和pidstat工具进一步排查

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
# 间隔 1 秒输出多组数据 (这里是 20 组)
$ pidstat -d 1 20
...
06:48:46 UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command
06:48:47 0 4615 0.00 0.00 0.00 1 kworker/u4:1
06:48:47 0 6080 32768.00 0.00 0.00 170 app
06:48:47 0 6081 32768.00 0.00 0.00 184 app

06:48:47 UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command
06:48:48 0 6080 0.00 0.00 0.00 110 app

06:48:48 UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command
06:48:49 0 6081 0.00 0.00 0.00 191 app

06:48:49 UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command

06:48:50 UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command
06:48:51 0 6082 32768.00 0.00 0.00 0 app
06:48:51 0 6083 32768.00 0.00 0.00 0 app

06:48:51 UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command
06:48:52 0 6082 32768.00 0.00 0.00 184 app
06:48:52 0 6083 32768.00 0.00 0.00 175 app

06:48:52 UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command
06:48:53 0 6083 0.00 0.00 0.00 105 app
...

看到app没次有32MB的数据的读取

然后同过perf top去查看代码

僵尸进程

首先通过top看进程状态我们常见的几种状态

  • R:Running 或 Runnable 的缩写,表示进程在 CPU 的就绪队列中,正在运行或者正在等待运行。
  • D:Disk Sleep 的缩写,也就是不可中断状态睡眠(Uninterruptible Sleep),一般表示进程正在跟硬件交互,并且交互过程不允许被其他进程或中断打断。
  • Z:是Zombie 的缩写,表示进程已经结束但是父进程还没有回收它
  • S:是Interruptible Sleep 的缩写,也就是可中断状态睡眠,表示进程因为等待某个事件而被系统挂起。当进程等待的事件发生时,它会被唤醒并进入 R 状态。
  • I:是 Idle 的缩写,也就是空闲状态,用在不可中断睡眠的内核线程上,不会引起某些负载的升高。
  • T:也就是 Stopped 或 Traced 的缩写,表示进程处于暂停或者跟踪状态。
  • X:进程消亡,top不会出现。

    我们系统应该尽量避免Z出现,Zombie是如何产生的?如果系统出现了大量的Zombie我们该如何排查?

产生原因

正常情况下,当一个进程创建了子进程后,它应该通过系统调用 wait() 或者 waitpid() 等待子进程结束,回收子进程的资源;而子进程在结束时,会向它的父进程发送 SIGCHLD 信号,所以,父进程还可以注册 SIGCHLD 信号的处理函数,异步回收资源。如果一个子进程被销毁父进程还没来得及对子进程进行回收就会出现Zombie。短暂的Zombie是没问题的,但是大量的Zombie进程我们就需要关注了。

排查思路

通过pstree查看僵尸进程的父进程,比如下面的4009进程,然后去查看4009的程序代码

1
2
3
4
5
6
7
8
9
10
11

# -a 表示输出命令行选项
# p表PID
# s表示指定进程的父进程
$ pstree -aps 3084
systemd,1
└─dockerd,15006 -H fd://
└─docker-containe,15024 --config /var/run/docker/containerd/containerd.toml
└─docker-containe,3991 -namespace moby -workdir...
└─app,4009
└─(app,3084)

系统优化-CPU-排查短时进程的工具

Posted on 2020-04-23 | Edited on 2022-09-21 | In 性能优化 , CPU

场景

我们在排查问题时候并不一定能找到引起CPU升高的“罪魁祸首”,他们可能隐藏在一些进程的子进程中。这时候的排查思路就是

top看哪些是进场活跃的进程,然后看这些进程的pid是否一直存在,如果他是不停的在变化的说明可能又以下俩种情况

  1. 短时进程:频繁的创建、销毁
  2. 创建的过程中出现错误会被销毁,

查看问题

对于短时进程可以通过pstree

我们通过perf top分析找到问题所在,在通过grep ‘xxx’ -r ./app 查看源代码找到结果取修改。

execsnoop工具

  1. cd /usr/bin
  2. wget https://raw.githubusercontent.com/brendangregg/perf-tools/master/execsnoop
  3. chmod 755 execsnoop
1
2
3
4
5
6
7
8
9
10
11
12
13

# 按 Ctrl+C 结束
$ execsnoop
PCOMM PID PPID RET ARGS
sh 30394 30393 0
stress 30396 30394 0 /usr/local/bin/stress -t 1 -d 1
sh 30398 30393 0
stress 30399 30398 0 /usr/local/bin/stress -t 1 -d 1
sh 30402 30400 0
stress 30403 30402 0 /usr/local/bin/stress -t 1 -d 1
sh 30405 30393 0
stress 30407 30405 0 /usr/local/bin/stress -t 1 -d 1
...

一次堆外内存泄露的排查

Posted on 2020-04-22 | Edited on 2022-09-21 | In 经验积累 , 项目积累

背景

我们的一个后端java服务,采用的jetty+spring,堆内存1个g,但是一运行很快内存会占到4个g,启动参数以及top见下图:

1
nohup java -Djava.awt.headless=true -Djava.net.preferIPv4Stack=true -DappName=uapi -server -Xms1g -Xmx1g -XX:SurvivorRatio=8 -Xss256k -XX:ReservedCodeCacheSize=64m -XX:+CMSParallelRemarkEnabled -XX:+UseCMSCompactAtFullCollection -XX:+CMSScavengeBeforeRemark -XX:+UseFastAccessorMethods -XX:+DisableExplicitGC -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=80 -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC -XX:+PrintTenuringDistribution -Xloggc:./gc.logs -jar xxx.jar > nohup.txt 2>&1 &

avator

分析思路

jmap分析结果如下,堆和新生代没有异常,排除法判断是堆外内存导致。

1
jmap -heap $pid

avator

持续观察一段时间后,通过top发现RES没有继续升高整体稳定在4g左右,猜测不是内存泄露而是某些程序通过堆外内存生成了一些buffer。所以想看下内存都在干嘛。

排查问题步骤:观察内存

先用pmap查看内存情况,发现有很多奇怪的64MB的空间。结果和命令如下:

1
pmap -x $pid

avator

问题预估:进过查看资料以及和同事沟通,怀疑是glibc组件的ptmalloc的arena机制导致的:

  • 当一个线程调用malloc申请内存时候,会先查看自己是否有一个分配去,如果有加锁,加锁失败,去环形链表找一个未加锁的分配区,如果没找到,malloc会开辟一个新的分配区加入环形链表并加锁,用它来分配内存。
  • 这种机制在多线程竞争锁激烈的场景下会带来一个问题:非主分配区开辟越来越多,因为它一旦开辟了就不会释放,一个分配区就是64MB。这样也会导致进程占用的内存越来越多(可能实际使用的并不多)。如果系统配置的ulimit进程最大虚拟内存值不是unlimited,那么当进程占用的内存达到ulimit值,就会core掉。这个情况也可以在pmap -p pid中看到里面有大量的64MB大小的anon内存块。这个问题可以通过设置MALLOC_ARENA_MAX环境变量来限制Arena的最大数量规避。
  • 经过后面的gperf排查发现我们的代码中有GZipInputStream在创建对象时候底层的Java_java_util_zip_Inflater_init方法确实是调用了mallloc。

    所以,这其实不是内存泄露而是内存空洞的现象,在我们的系统资源捉襟见肘时候就比较致命。

排查问题步骤:dump内存

查看/proc/$pid/smaps 查看进程内存的使用情况发现进程大量申请了64MB的空间。【怀疑是glibc在2.10引入的arena引起的,需要后续深入研究】

通过gdb dump出内存来查看;【具体命令见下方】

1
2
3
4
5
6
gdb attach <pid>
dump memory outfile.txt startAddreess endAddress
# 其中startAddreess endAddress在上面的smaps中都有

#查看内存
strings outfile.txt

注意:生产环境慎用,gdb会阻塞进程。

这些64MB的很多内存是空的,总共加起来差不多有2.5g,算上我们分配的堆内存空间差不多正好4个G,大概原因找到了:由于频繁的调用系统malloc:os申请内存没有回收。查阅文章,可以试着用tcmalloc来解决遂开始安装tcmalloc,并且用gperf分析具体的调用

排查问题步骤:安装tcmalloct和gperf

1.安装环境

1
yum -y install gcc gcc-c++

2.安装libunwind

1
2
3
4
5
6
cd /usr/local/src
wget http://download.savannah.gnu.org/releases/libunwind/libunwind-0.99.tar.gz
tar -xzvf libunwind-0.99.tar.gz
cd libunwind-0.99
./configure  --prefix=/usr/local/google-perftools/libunwind
make && make install

3.安装gperftools

1
2
3
4
5
wget https://github.com/gperftools/gperftools/releases/download/gperftools-2.5/gperftools-2.5.tar.gz
tar -xzvf gperftools-2.5.tar.gz
cd gperftools-2.5
./configure --prefix=/usr/local/google-perftools/
make && make install

4.使配置生效

1
2
3
4
5
6
7
vim /etc/ld.so.conf.d/usr_local_lib.conf

#新增以下内容
/usr/local/google-perftools/libunwind/lib
# 按esc再:wq! #保存退出

/sbin/ldconfig  #执行此命令,使libunwind生效。 需要sudo权限

5.加入环境变量

1
2
3
export LD_PRELOAD=/usr/local/google-perftools/lib/libtcmalloc.so
export HEAPPROFILE=/usr/local/gperftools/tmp/gzip
##注意 mkdir -p /usr/local/gperftools/tmp

问题解决

通过top查看进程

之前的进程 top结果 RES 3.3g
avator

之后的进程 top结果 RES 1.8g
avator

我们这时候再用pmap去看进程,64MB的内存那块消失

avator

用pperf查看内存

上面的环境变量生效后运行java就会生成heap的快照地址在见上面HEAPPROFILE的设置

用下面的命令处理heap的快照

1
2
3
4
5
6
7
cd /usr/local/gperftools/tmp/

#查看heap信息
/usr/local/gperftools-2.5/bin/pprof --text $JAVA_HOME/bin/java /usr/local/gperftools/tmp/gzip_618.0057.heap

#导出pdf
/usr/local/gperftools-2.5/bin/pprof --pdf $JAVA_HOME/bin/java /usr/local/gperftools/tmp/gzip_618.0057.heap >gzip_618.0057.pdf

pdf的文件如下

avator

总结

  • 原因:glibc的ptmalloc的arana机制会在并发情况下额外申请很多64MB内存空间,这部分空间不会回收的会重复利用。因为服务的线程数一定所以一般来讲不会出现泄漏,而是内存空洞问题。系统资源不足进程就会被kill
  • 解决方案:
    • ptmalloc取代tcmalloc。
    • 减少堆大小。

附录:压测记录

方案

对服务进行压测,压倒系统出现瓶颈无法在继续给出压力为止,这时候观测系统的参数。*本次压测主要模拟在系统不通负载的运行中内存的使用情况,cpu的占用以及系统瓶颈仅作为参考

结果

下图是在aapi测试时候的监控图:

avator

  • 在100QPS下aapi系统出现瓶颈,已经无法继续增加压力,这时候,可以看出100QPS的场景下cpu的波动很大,但是内存增长在合理范围内。
  • 系统空转后java进程内存逐渐落回1.6G

docker内部

top图

avator

pidstat -w -p <$pid> 1,上下文切换始终正常

avator

pidstat -u -p <$pid> 1 cpu的使用率

avator

压力主要在user%和system%,代表程序和内核态的调度。

系统优化-CPU-如何排查CPU使用率到100%

Posted on 2020-04-22 | Edited on 2022-09-21 | In 性能优化 , CPU

我们如何去查看CPU的使用率呢?CPU使用率中的几个重要指标分别代表什么?

CPU使用率相关的概念介绍

我们多核的CPU为了能“并行”运行我们的程序,我们需要为每个进程分配执行时间,执行时间到了我们会进行一次中断,中断的频率我们定义为hz,其中内核态的中断我们可以设置,一般是100,250,1000hz,表示秒中断多少次,我们的用户态是没法直接控制hz的。用户态则规定为100HZ,也就是10ms。我们查看CPU的指标的单位就是10ms。

我们可以通过查看/proc/cpu/stat来看cpu的指标。

1
2
3
4
5
6
7
8
9
10
cat /proc/cpu/stat|grep ^cpu

cpu 280580 7407 286084 172900810 83602 0 583 0 0 0
cpu0 144745 4181 176701 86423902 52076 0 301 0 0 0
cpu1 135834 3226 109383 86476907 31525 0 282 0 0 0

man /proc/stat
#kernel/system statistics. Varies with architecture. Common entries include:
# The amount of time, measured in units of USER_HZ (1/100ths of a second on most architectures, use sysconf(_SC_CLK_TCK) to obtain the right value), that the system spent in various states:
# Number of processes in runnable state. (Linux 2.5.45 onward.)

其中几个重要指标

  • user(通常缩写为 us),代表用户态 CPU 时间。注意,它不包括下面的 nice 时间,但包括了 guest 时间。
  • nice(通常缩写为 ni),代表低优先级用户态 CPU 时间,也就是进程的 nice 值被调整为 1-19 之间时的 CPU 时间。这里注意,nice 可取值范围是 -20 到 19,数值越大,优先级反而越
  • system(通常缩写为 sys),代表内核态 CPU 时间。
  • idle(通常缩写为 id),代表空闲时间。注意,它不包括等待 I/O 的时间(iowait)。
  • iowait(通常缩写为 wa),代表等待 I/O 的 CPU 时间。
  • irq(通常缩写为 hi),代表处理硬中断的 CPU 时间。
  • softirq(通常缩写为 si),代表处理软中断的 CPU 时间。
  • steal(通常缩写为 st),代表当系统运行在虚拟机中的时候,被其他虚拟机占用的 CPU 时间。
  • guest(通常缩写为 guest),代表通过虚拟化运行其他操作系统的时间,也就是运行虚拟机的 CPU 时间。
  • guest_nice(通常缩写为 gnice),代表以低优先级运行虚拟机的时间。

    我们还可以通过/proc/$pid/stat看某个进程的cpu使用情况,但是这些都是开机到现在的历史时间。其实是没有参考价值的,我们应该看一段时间内的cpu使用情况,我们一般采用以下2个方法看

top

按1查看每个cpu

1
Cpu(s): 10.2%us,  5.8%sy,  0.0%ni, 83.2%id,  0.0%wa,  0.0%hi,  0.8%si,  0.0%st

pidstat

1
2
3
4
5
6
7
8

pid -u 5 1

Linux 3.10.0-514.16.1.es01.x86_64 (xxxxx.xxx) 04/22/2020 _x86_64_ (40 CPU)

01:58:07 PM PID %usr %system %guest %CPU CPU Command
01:58:12 PM 354 0.00 1.20 0.00 1.20 10 xxxxx
01:58:12 PM 946 0.20 0.40 0.00 0.60 14 java

CPU使用过高怎么办

我们前期可以采用perf,不用gdb的原因是gdb会阻塞进程影响线上应用

1
2
3
4
5
6
7
8
9

$ perf top
Samples: 833 of event 'cpu-clock', Event count (approx.): 97742399
Overhead Shared Object Symbol
7.28% perf [.] 0x00000000001f78a4
4.72% [kernel] [k] vsnprintf
4.32% [kernel] [k] module_get_kallsym
3.65% [kernel] [k] _raw_spin_unlock_irqrestore
...

也可以采用perf record && perf report来进行采样

其中 perf 添加-g可以查看具体的调用链

案例的总结

user+nice过高说明程序的进程可优化
sys过高可能是内核调度服务出问题
iowait过高可能是磁盘io出问题
si+hi过高可能是内核中断出问题

系统优化-CPU-理解CPU的上下文切换

Posted on 2020-04-19 | Edited on 2022-09-21 | In 性能优化 , CPU

什么是CPU的上下文切换,程序又是如何运行的?

程序在运行的时候,CPU会将一部分数据加载到CPU的寄存器,它是属于CPU专属的高速缓存,并且将程序运行的地址加载到程序计数器中,而CPU寄存器+程序计数器统称为CPU的上下文?
我们的系统一般可以并行运行多个程序,儿多个进程并不是真正的并行运行的,CPU会同时在多个程序中进行切换,造成这种并行运行的“错觉”,而切换之前就要现将CPU的上下文保存起来从而下次执行是偶继续执行,这个叫做CPU的上下文切换。

CPU上下文切换的几个场景

  1. 进程之间的上下文切换
  2. 线程之间的上下文切换
  3. 响应系统中断的上下文切换

    注: 还有一种场景就是系统调用的CPU上下文切换,我们的程序是运行在用户态的,而系统的资源是系统的内核态。一次操作往往需要多次系统调用,会发生多次CPU上下文切换比如:读取一个文件是以下3步。先open()打开一个文件,再read()读取磁盘数据,再write()写到标准数据流中。每一次系统调用会先保存用户态的数据,然后加载内核态数据,然后切换到内核态,操作完成后,在加载用户态数据,切换回用户态继续之前的操作。(俩次切换过程)不过我们一般称呼他们是特权切换,他们是不会发生进程切换的。

    如图:
    avator

    进程间切换:需要刷新系统的虚拟内存、栈等用户态信息,再保存CPU上下文。
    进程切换的主要原因有:分配的运行时间片已到;系统资源不够【比如内存不够】等待资源时候会被挂起;调用Sleep函数;有更高优先级的进程进来等

    线程间切换:线程是CPU调度的最小单位,进程是CPU的最小的资源单位,所谓的CPU上下文切换实际是线程的CPU上下文切换分一下俩种情况:

    1. 如果俩个线程属于同一个进程:不需要切换用户态信息,再切换CPU上下文
    2. 如果俩个线程不属于同一个进程:需要切换用户态信息,再切换CPU上下文。
      所以,现在程序大都用多线程取代多进程。

    响应系统中断:为了响应硬件事件,往往回中断当前正在运行的进程。响应中断往往比进程优先级高。

如何定位CPU切换的场景

工具介绍

vmstat使我们常用的分析io的工具他也能分析cpu的上下文切换,使用方法如下:其中

  • r 是runable或者runnaing的进程数
  • b 是block的进程数
  • system的in是中断数
  • cs是切换数
1
2
3
4
5
vmstat

procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
0 0 0 78018360 299560 30894864 0 0 0 7 0 0 1 0 99 0 0

pidstat,和上期不一样的是如果我们想看cpu上下文切换数需要加上-w,-t是查看线程

  • cswch/s是每秒自愿切换上下文的次数,比如:无法获取资源被挂起。
  • nvcswch/s是每秒非自愿切换上下文的次数,运行时间片到了被挂起。
1
2
3
4
pidstat -w -t  -p <pid>

08:30:23 PM UID PID cswch/s nvcswch/s Command
08:30:28 PM 669 27440 0.00 0.00 java

查看中断原因watch -d cat /proc/interrupts

1
2
3
4
5
6
7
$ watch -d cat /proc/interrupts
CPU0 CPU1
...
RES: 2450431 5279697 Rescheduling interrupts
...

res是指cpu调度中断,中断原因是cpu在频繁的切换线程

分析问题路径

  1. 自愿上下文切换变多了,说明进程都在等待资源,有可能发生了 I/O 等其他问题;
  2. 非自愿上下文切换变多了,说明进程都在被强制调度,也就是都在争抢 CPU,说明 CPU 的确成了瓶颈;
  3. 中断次数变多了,说明 CPU 被中断处理程序占用,还需要通过查看 /proc/interrupts 文件来分析具体的中断类型。

具体排查路径如下:

  1. 我们先运行vmstat,一般cs在1w左右都是正常的如果超过1w,我们就要去调用pidstat查看;
  2. pidstat -w -u查看cpu的占用和cswch,如果是Java或者golang等多线程的程序要加-t看线程之间的cswch/s值
  3. 之后通过watch -d cat /proc/interrupts查看cpu的中断原因【watch -d ‘cat /proc/interrupts | sort -nr -k 2 ‘】

SpringBoot中的MongoConfig

Posted on 2020-04-18 | Edited on 2022-09-21 | In 经验积累 , 工具

背景

我负责的一个项目采用了MongoDB作为数据源,同时使用了springboot生态顺利成章的使用了org.springframework.boot:spring-boot-starter-data-mongodb。开始配置采用下面的链接方式

1
spring.data.mongodb.uri=mongodb://127.0.0.1:27017/faceguard?retryWrites=true

这种方案我们遇到了俩个问题:

  1. 很难对连接池进行定制化的设计,连连接数都设置不了。
  2. 更为重要的我们对mongodb的server加了4层的负载,而这种链接方式不自持心跳导致我们的链接空闲一段时间内会自己断掉;

解决方案

自己注入MongoDbFactory,可以按需要注入进自己的参数了。我们需要实现下面几步

  1. 实现MongoClientOptionProperties,将配置文件中mongo.client.option签注的属性注入到MongoClientOptionProperties中
  2. 利用MongoClientOptionProperties生成MongoClientOptions对象,MongoClientOptions是典型的builder模式,通过build()生成对象在传递给MongoClient
  3. 注入MongoDbFactory方法时候将MongoClient传递给MongoDbFactory

talk is cheap上代码,MongoClientOptionProperties只提供了几个常用设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;

/**
* @Author: liuhaoeric
* Create time: 2020/04/18
* Description:
*/
@Data
@ConfigurationProperties(prefix = "mongo.client.option")
public class MongoClientOptionProperties {
private int connectionsPerHost = 20;
private int minConnectionsPerHost = 20;
private int connectTimeout = 6000;
private int maxWaitTime = 6000;
private int socketTimeout = 0;
private int heartbeatFrequency = 0;
}
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
package com.ericliu.mongodb.plus.config;

import com.mongodb.MongoClient;
import com.mongodb.MongoClientOptions;
import com.mongodb.MongoCredential;
import com.mongodb.ServerAddress;
import com.mongodb.client.MongoDatabase;
import com.mongodb.client.gridfs.GridFSBucket;
import com.mongodb.client.gridfs.GridFSBuckets;
import org.apache.logging.log4j.util.Strings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.convert.CustomConversions;
import org.springframework.data.mongodb.MongoDbFactory;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.SimpleMongoDbFactory;
import org.springframework.data.mongodb.core.convert.DbRefResolver;
import org.springframework.data.mongodb.core.convert.DefaultDbRefResolver;
import org.springframework.data.mongodb.core.convert.DefaultMongoTypeMapper;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;

import java.util.ArrayList;
import java.util.List;

/**
* @author pengkai
* @date 2019-02-21
*/
@Configuration
@EnableConfigurationProperties(MongoClientOptionProperties.class)//建议这样引入MongoClientOptionProperties
public class MongoConfig {
private static final Logger LOG = LoggerFactory.getLogger(MongoConfig.class);

@Value("${mongo.database}")
private String database;
@Value("${mongo.host}")
private String host;
@Value("${mongo.port}")
private int port;
@Value("${mongo.username:}")
private String userName;
@Value("${mongo.password:}")
private String password;

@Bean
public GridFSBucket getGridFSBuckets(MongoDbFactory mongoDbFactory) {
MongoDatabase db = mongoDbFactory.getDb();
return GridFSBuckets.create(db);
}

@Bean
public MappingMongoConverter mappingMongoConverter(MongoDbFactory factory, MongoMappingContext context, BeanFactory beanFactory) {
DbRefResolver dbRefResolver = new DefaultDbRefResolver(factory);

MappingMongoConverter mappingConverter = new MappingMongoConverter(dbRefResolver, context);
try {
mappingConverter.setCustomConversions(beanFactory.getBean(CustomConversions.class));
} catch (NoSuchBeanDefinitionException ignore) {
}

// Don't save _class to mongo
mappingConverter.setTypeMapper(new DefaultMongoTypeMapper(null));
return mappingConverter;
}


// 覆盖默认的MongoDbFactory
@Bean
MongoDbFactory mongoDbFactory(MongoClientOptionProperties mongoClientOptionProperties) {
MongoClientOptions.Builder builder = new MongoClientOptions.Builder();
builder.connectionsPerHost(mongoClientOptionProperties.getConnectionsPerHost());
builder.minConnectionsPerHost(mongoClientOptionProperties.getMinConnectionsPerHost());
builder.connectTimeout(mongoClientOptionProperties.getConnectTimeout());
builder.maxWaitTime(mongoClientOptionProperties.getMaxWaitTime());
builder.socketTimeout(mongoClientOptionProperties.getSocketTimeout());
if (mongoClientOptionProperties.getHeartbeatFrequency() > 0) {
builder.heartbeatFrequency(mongoClientOptionProperties.getHeartbeatFrequency());
}
MongoClientOptions mongoClientOptions = builder.build();

// MongoDB地址列表
List<ServerAddress> serverAddresses = new ArrayList<>();
ServerAddress serverAddress = new ServerAddress(host, port);
serverAddresses.add(serverAddress);

// 连接认证
MongoClient mongoClient = null;
if (!Strings.isEmpty(userName)) {
mongoClient = new MongoClient(serverAddresses, MongoCredential.createCredential(
userName,
database,
password.toCharArray()), mongoClientOptions);
} else {
mongoClient = new MongoClient(serverAddresses, mongoClientOptions);
}

//创建客户端和Factory
return new SimpleMongoDbFactory(mongoClient, database);
}

@Bean
MongoTemplate mongoTemplate(MongoDbFactory mongoDbFactory) {
return new MongoTemplate(mongoDbFactory);
}
}

代码

github地址:https://github.com/liuhao163/mongodb-plus

SpringMvc源码-DispatcherServlet(二)

Posted on 2020-04-14 | Edited on 2022-09-21 | In java , spring , mvc

上一篇SpringMvc源码-DispatcherServlet讲了DispatcherServlet的初始化过程,本篇着重讲DispatchServlet处理一个请求的流程,即面试SpringMvc的经典面试题,SpringMvc是如何处理reqeuest的。

准备工作

我们的DispatchServlet继承于HttpServlet,HttpServlet处理请求的方法主要是httpServlet.service,该方法会根据request.method调用doXxx。FrameworkServlet自己去实现了doXxx。

整个的调用链是:接到reqeust请求后->FrameworkServlet.service()->FrameworkServlet.processRequest()->DispatchServlet.doService()(FrameworkServlet提供了抽象方法)
->DispatchServlet.doDispatch()处理请求

下面,我们根据这个调用链进行分析

FrameworkServlet

FrameworkServlet还重写了service方法,主要是支持HttpMethod.PATCH,它们和HttpMethod.PATCH一样都调用了processRequest(request, response)

所有的doXXX都是调用processRequest我们来看下processRequest方法做了什么

processRequest

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
39
40
41
42
43
44
45
46
47
48
49
protected final void processRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {

long startTime = System.currentTimeMillis();
Throwable failureCause = null;

//从ThreadLocal中获取上一个请求的LocaleContext
LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();//
//new当前请求的LocaleContext
LocaleContext localeContext = buildLocaleContext(request);//SimpleLocaleContext

//从ThreadLocal中获取上一个请求的RequestAttributes(ServletRequestAttributes)
RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
//new当前请求的ServletRequestAttributes
ServletRequestAttributes requestAttributes = buildRequestAttributes(request, response, previousAttributes);

WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), new RequestBindingInterceptor());

//保存进ThreadLocal中
initContextHolders(request, localeContext, requestAttributes);

try {
//抽象方法具体实现在DeipatchServlet,重点方法
doService(request, response);
}
catch (ServletException | IOException ex) {
failureCause = ex;
throw ex;
}
catch (Throwable ex) {
failureCause = ex;
throw new NestedServletException("Request processing failed", ex);
}

finally {
//恢复previousLocaleContext,previousAttributes
//这里是我没太看明白的地方,说下我的猜测:
// 1、previousLocaleContext,previousAttributes大概率为空这里就重置了ThreadLocal
// 2、previousLocaleContext,previousAttributes不为空,说明当前线程处理别的请求,所以
// 在doservice时候context用的是current的值,请求处理完了将值还原称之前的值。
resetContextHolders(request, previousLocaleContext, previousAttributes);
if (requestAttributes != null) {
requestAttributes.requestCompleted();
}
logResult(request, response, failureCause, asyncManager);
publishRequestHandledEvent(request, response, startTime, failureCause);
}
}

doService–>doDispatch方法负责处理具体的请求逻辑

doDispatch见下面

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;

WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

try {
ModelAndView mv = null;
Exception dispatchException = null;

try {
//判断reqeust的ContentType包含multipart/ 如果是multipart则返回StandardMultipartHttpServletRequest
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);

// Determine handler for the current request.
//根据请求获取HandlerExecutionChain,包含对应的handlerMethod和interceptor【MappedInterceptor 和 HandlerIntercpetor等等】
//具体介绍见下方
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}

// Determine handler adapter for the current request.
// 根据handler找到HandlerAdapter
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

// Process last-modified header, if supported by the handler.
String method = request.getMethod();
boolean isGet = "GET".equals(method);
if (isGet || "HEAD".equals(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
return;
}
}

//关键代码,执行拦截器的preHandle
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}

// Actually invoke the handler.
//关键代码,执行handlerMethod,ha的类型是RequestMappingHandlerAdapter,
// 调用路径是AbstractHandlerMethodAdapter.handle()-->AbstractHandlerMethodAdapter.handleInternal()-->ha的类型是RequestMappingHandlerAdapter.handleInternal
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

/**
* todo 待看的组件
* WebDataBinderFactory
* ModelFactory
*
* ServletInvocableHandlerMethod
*
* HandlerMethodArgumentResolverComposite
* HandlerMethodReturnValueHandlerComposite
*
* ModelAndViewContainer
* modelAndView
*/

//todo asyncManager 待看
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}

//todo
applyDefaultViewName(processedRequest, mv);
//关键代码,执行拦截器的postHandle
mappedHandler.applyPostHandle(processedRequest, response, mv);
} catch (Exception ex) {
dispatchException = ex;
} catch (Throwable err) {
// As of 4.3, we're processing Errors thrown from handler methods as well,
// making them available for @ExceptionHandler methods and other scenarios.
dispatchException = new NestedServletException("Handler dispatch failed", err);
}
//todo 书签 渲染modelAndView,同时触发mappedHandler.triggerAfterCompletion
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
} catch (Exception ex) {
//触发mappedHandler.triggerAfterCompletion
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
} catch (Throwable err) {
//触发mappedHandler.triggerAfterCompletion
triggerAfterCompletion(processedRequest, response, mappedHandler,
new NestedServletException("Handler processing failed", err));
} finally {
if (asyncManager.isConcurrentHandlingStarted()) {
// Instead of postHandle and afterCompletion
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
} else {
// Clean up any resources used by a multipart request.
//这里要注意,Multipar在这里被清理掉所以如果Multipart有异步场景,需要将copy到别的对象中。
if (multipartRequestParsed) {
cleanupMultipart(processedRequest);
}
}
}
}

关键文章索引:

  • SpringMvc源码-RequestMappingHandlerMapping
  • SpringMvc源码-RequestMappingHandlerAdapter
1…789…23

Liu hao

励志当好厨子的程序员

229 posts
54 categories
81 tags
RSS
GitHub E-Mail
© 2018 – 2023 Liu hao
Powered by Hexo v3.9.0
|
Theme – NexT.Pisces v7.0.0