前两节,学了文件系统和磁盘的 I/O 原理,复习一下。
文件系统,是对存储设备上的文件进行组织管理的一种机制。为了支持各类不同的文件系统,Linux 在各种文件系统上,抽象了一层虚拟文件系统 VFS。
它定义了一组所有文件系统都支持的数据结构和标准接口。这样,应用程序和内核中的其他子系统,就只需要跟 VFS 提供的统一接口进行交互。
在文件系统的下层,为了支持各种不同类型的存储设备,Linux 又在各种存储设备的基础上,抽象了一个通用块层。
通用块层,为文件系统和应用程序提供了访问块设备的标准接口;同时,为各种块设备的驱动程序提供了统一的框架。此外,通用块层还会对文件系统和应用程序发送过来的 I/O 请求进行排队,并通过重新排序、请求合并等方式,提高磁盘读写的效率。
通用块层的下一层,自然就是设备层了,包括各种块设备的驱动程序以及物理存储设备。
文件系统、通用块层以及设备层,就构成了 Linux 的存储 I/O 栈。存储系统的 I/O ,通常是整个系统中最慢的一环。所以,Linux 采用多种缓存机制,来优化 I/O 的效率,比方说,
  • 为了优化文件访问的性能,采用页缓存、索引节点缓存、目录项缓存等多种缓存机制,减少对下层块设备的直接调用。
  • 同样的,为了优化块设备的访问效率,使用缓冲区来缓存块设备的数据。
不过,在碰到文件系统和磁盘的 I/O 问题时,具体应该怎么定位和分析呢?今天,我就以一个最常见的应用程序记录大量日志的案例,带你来分析这种情况。

案例准备

本次案例还是基于 Ubuntu 18.04,同样适用于其他的 Linux 系统。我使用的案例环境如下所示:
  • 机器配置:2 CPU,8GB 内存
  • 预先安装 docker、sysstat 等工具,如 apt install docker.io sysstat
这里要感谢唯品会资深运维工程师阳祥义帮忙,分担了今天的案例。这个案例,是一个用 Python 开发的小应用,为了方便运行,我把它打包成了一个 Docker 镜像。这样,你只要运行 Docker 命令,就可以启动它。
接下来,打开一个终端,SSH 登录到案例所用的机器中,并安装上述工具。跟以前一样,案例中所有命令,都默认以 root 用户运行。如果你是用普通用户身份登陆系统,请运行 sudo su root 命令,切换到 root 用户。
到这里,准备工作就完成了。接下来,我们正式进入操作环节。
温馨提示:案例中 Python 应用的核心逻辑比较简单,你可能一眼就能看出问题,但实际生产环境中的源码就复杂多了。所以,我依旧建议,操作之前别看源码,避免先入为主,要把它当成一个黑盒来分析。这样,你可以更好把握住,怎么从系统的资源使用问题出发,分析出瓶颈所在的应用,以及瓶颈在应用中大概的位置。

案例分析

首先,我们在终端中执行下面的命令,运行今天的目标应用:
docker run -v /tmp:/tmp --name=app -itd feisky/logapp

然后,在终端中运行 ps 命令,确认案例应用正常启动。如果操作无误,你应该可以在 ps 的输出中,看到一个 app.py 的进程:
ps -ef | grep /app.py
root     18940 18921 73 14:41 pts/0    00:00:02 python /app.py

接着,我们来看看系统有没有性能问题。要观察哪些性能指标呢?前面文章中,我们知道 CPU、内存和磁盘 I/O 等系统资源,很容易出现资源瓶颈,这就是我们观察的方向了。我们来观察一下这些资源的使用情况。
当然,动手之前你应该想清楚,要用哪些工具来做,以及工具的使用顺序又是怎样的。你可以先回忆下前面的案例和思路,自己想一想,然后再继续下面的步骤。
我的想法是,我们可以先用 top ,来观察 CPU 和内存的使用情况;然后再用 iostat ,来观察磁盘的 I/O 情况。
所以,接下来,你可以在终端中运行 top 命令,观察 CPU 和内存的使用情况:
# 按 1 切换到每个 CPU 的使用情况 top
top - 14:43:43 up 1 day,  1:39,  2 users,  load average: 2.48, 1.09, 0.63
Tasks: 130 total,   2 running,  74 sleeping,   0 stopped,   0 zombie
%Cpu0  :  0.7 us,  6.0 sy,  0.0 ni,  0.7 id, 92.7 wa,  0.0 hi,  0.0 si,  0.0 st
%Cpu1  :  0.0 us,  0.3 sy,  0.0 ni, 92.3 id,  7.3 wa,  0.0 hi,  0.0 si,  0.0 st
KiB Mem :  8169308 total,   747684 free,   741336 used,  6680288 buff/cache
KiB Swap:        0 total,        0 free,        0 used.  7113124 avail Mem PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
18940 root      20   0  656108 355740   5236 R   6.3  4.4   0:12.56 python
1312 root      20   0  236532  24116   9648 S   0.3  0.3   9:29.80 python3

观察 top 的输出,你会发现,CPU0 的使用率非常高,它的系统 CPU 使用率(sys%)为 6%,而 iowait 超过了 90%。这说明 CPU0 上,可能正在运行 I/O 密集型的进程。不过,究竟是什么原因呢?这个疑问先保留着,我们先继续看完。
接着我们来看,进程部分的 CPU 使用情况。你会发现, python 进程的 CPU 使用率已经达到了 6%,而其余进程的 CPU 使用率都比较低,不超过 0.3%。看起来 python 是个可疑进程。记下 python 进程的 PID 号 18940,我们稍后分析。
最后再看内存的使用情况,总内存 8G,剩余内存只有 730 MB,而 Buffer/Cache 占用内存高达 6GB 之多,这说明内存主要被缓存占用。虽然大部分缓存可回收,我们还是得了解下缓存的去处,确认缓存使用都是合理的。

iostat

到这一步,你基本可以判断出,CPU 使用率中的 iowait 是一个潜在瓶颈,而内存部分的缓存占比较大,那磁盘 I/O 又是怎么样的情况呢?
我们在终端中按 Ctrl+C ,停止 top 命令,再运行 iostat 命令,观察 I/O 的使用情况:
# -d 表示显示 I/O 性能指标,-x 表示显示扩展统计(即所有 I/O 指标) iostat -x -d 1
Device            r/s     w/s     rkB/s     wkB/s   rrqm/s   wrqm/s  %rrqm  %wrqm r_await w_await aqu-sz rareq-sz wareq-sz  svctm  %util
loop0            0.00    0.00      0.00      0.00     0.00     0.00   0.00   0.00    0.00    0.00   0.00     0.00     0.00   0.00   0.00
sdb              0.00    0.00      0.00      0.00     0.00     0.00   0.00   0.00    0.00    0.00   0.00     0.00     0.00   0.00   0.00
sda              0.00   64.00      0.00  32768.00     0.00     0.00   0.00   0.00    0.00 7270.44 1102.18     0.00   512.00  15.50  99.20

还记得这些性能指标的含义吗?先自己回忆一下,如果实在想不起来,查看上一节的内容,或者用 man iostat 查询。
观察 iostat 的最后一列,你会看到,磁盘 sda 的 I/O 使用率已经高达 99%,很可能已经接近 I/O 饱和。
再看前面的各个指标,每秒写磁盘请求数是 64 ,写大小是 32 MB,写请求的响应时间为 7 秒,而请求队列长度则达到了 1100。
超慢的响应时间和特长的请求队列长度,进一步验证了 I/O 已经饱和的猜想。此时,sda 磁盘已经遇到了严重的性能瓶颈。
到这里,也就可以理解,为什么前面看到的 iowait 高达 90% 了,这正是磁盘 sda 的 I/O 瓶颈导致的。接下来的重点就是分析 I/O 性能瓶颈的根源了。那要怎么知道,这些 I/O 请求相关的进程呢?
不知道你还记不记得,上一节我曾提到过,可以用 pidstat 或者 iotop ,观察进程的 I/O 情况。这里,我就用 pidstat 来看一下。
使用 pidstat 加上 -d 参数,就可以显示每个进程的 I/O 情况。所以,你可以在终端中运行如下命令来观察:
pidstat -d 1 15:08:35      UID       PID   kB_rd/s   kB_wr/s kB_ccwr/s iodelay  Command
15:08:36        0     18940      0.00  45816.00      0.00      96  python 15:08:36      UID       PID   kB_rd/s   kB_wr/s kB_ccwr/s iodelay  Command
15:08:37        0       354      0.00      0.00      0.00     350  jbd2/sda1-8
15:08:37        0     18940      0.00  46000.00      0.00      96  python
15:08:37        0     20065      0.00      0.00      0.00    1503  kworker/u4:2

从 pidstat 的输出,你可以发现,只有 python 进程的写比较大,而且每秒写的数据超过 45 MB,比上面 iostat 发现的 32MB 的结果还要大。很明显,正是 python 进程导致了 I/O 瓶颈。
再往下看 iodelay 项。虽然只有 python 在大量写数据,但你应该注意到了,有两个进程 (kworker 和 jbd2 )的延迟,居然比 python 进程还大很多。
这其中,kworker 是一个内核线程,而 jbd2 是 ext4 文件系统中,用来保证数据完整性的内核线程。他们都是保证文件系统基本功能的内核线程,所以具体细节暂时就不用管了,我们只需要明白,它们延迟的根源还是大量 I/O。
综合 pidstat 的输出来看,还是 python 进程的嫌疑最大。接下来,我们来分析 python 进程到底在写什么。
首先留意一下 python 进程的 PID 号, 18940。看到 18940 ,你有没有觉得熟悉?其实前面在使用 top 时,我们记录过的 CPU 使用率最高的进程,也正是它。不过,虽然在 top 中使用率最高,也不过是 6%,并不算高。所以,以 I/O 问题为分析方向还是正确的。
知道了进程的 PID 号,具体要怎么查看写的情况呢?

strace

其实,我在系统调用的案例中讲过,读写文件必须通过系统调用完成。观察系统调用情况,就可以知道进程正在写的文件。想起 strace 了吗,它正是我们分析系统调用时最常用的工具。
接下来,我们在终端中运行 strace 命令,并通过 -p 18940 指定 python 进程的 PID 号:
strace -p 18940
strace: Process 18940 attached
...
mmap(NULL, 314576896, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f0f7aee9000
mmap(NULL, 314576896, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f0f682e8000
write(3, "2018-12-05 15:23:01,709 - __main"..., 314572844
) = 314572844
munmap(0x7f0f682e8000, 314576896)       = 0
write(3, "\n", 1)                       = 1
munmap(0x7f0f7aee9000, 314576896)       = 0
close(3)                                = 0
stat("/tmp/logtest.txt.1", {st_mode=S_IFREG|0644, st_size=943718535, ...}) = 0

从 write() 系统调用上,我们可以看到,进程向文件描述符编号为 3 的文件中,写入了 300MB 的数据。看来,它应该是我们要找的文件。不过,write() 调用中只能看到文件的描述符编号,文件名和路径还是未知的。
再观察后面的 stat() 调用,你可以看到,它正在获取 /tmp/logtest.txt.1 的状态。 这种“点 + 数字格式”的文件,在日志回滚中非常常见。我们可以猜测,这是第一个日志回滚文件,而正在写的日志文件路径,则是 /tmp/logtest.txt。

lsof

当然,这只是我们的猜测,自然还需要验证。这里,我再给你介绍一个新的工具 lsof。它专门用来查看进程打开文件列表,不过,这里的“文件”不只有普通文件,还包括了目录、块设备、动态库、网络套接字等。
接下来,我们在终端中运行下面的 lsof 命令,看看进程 18940 都打开了哪些文件:
lsof -p 18940
COMMAND   PID USER   FD   TYPE DEVICE  SIZE/OFF    NODE NAME
python  18940 root  cwd    DIR   0,50      4096 1549389 /
python  18940 root  rtd    DIR   0,50      4096 1549389 /
…
python  18940 root    2u   CHR  136,0       0t0       3 /dev/pts/0
python  18940 root    3w   REG    8,1 117944320     303 /tmp/logtest.txt

这个输出界面中,有几列我简单介绍一下,FD 表示文件描述符号,TYPE 表示文件类型,NAME 表示文件路径。这也是我们需要关注的重点。
再看最后一行,这说明,这个进程打开了文件 /tmp/logtest.txt,并且它的文件描述符是 3 号,而 3 后面的 w ,表示以写的方式打开。
这跟刚才 strace 完我们猜测的结果一致,看来这就是问题的根源:进程 18940 以每次 300MB 的速度,在“疯狂”写日志,而日志文件的路径是 /tmp/logtest.txt。
既然找出了问题根源,接下来按照惯例,就该查看源代码,然后分析为什么这个进程会狂打日志了。
你可以运行 docker cp 命令,把案例应用的源代码拷贝出来,然后查看它的内容。(你也可以点击这里查看案例应用的源码):
# 拷贝案例应用源代码到当前目录docker cp app:/app.py . # 查看案例应用的源代码cat app.py logger = logging.getLogger(__name__)
logger.setLevel(level=logging.INFO)
rHandler = RotatingFileHandler("/tmp/logtest.txt", maxBytes=1024 * 1024 * 1024, backupCount=1)
rHandler.setLevel(logging.INFO) def write_log(size): '''Write logs to file''' message = get_message(size) while True: logger.info(message) time.sleep(0.1) if __name__ == '__main__': msg_size = 300 * 1024 * 1024 write_log(msg_size)

分析这个源码,我们发现,它的日志路径是 /tmp/logtest.txt,默认记录 INFO 级别以上的所有日志,而且每次写日志的大小是 300MB。这跟我们上面的分析结果是一致的。
一般来说,生产系统的应用程序,应该有动态调整日志级别的功能。继续查看源码,你会发现,这个程序也可以调整日志级别。如果你给它发送 SIGUSR1 信号,就可以把日志调整为 INFO 级;发送 SIGUSR2 信号,则会调整为 WARNING 级:
def set_logging_info(signal_num, frame): '''Set loging level to INFO when receives SIGUSR1''' logger.setLevel(logging.INFO) def set_logging_warning(signal_num, frame): '''Set loging level to WARNING when receives SIGUSR2''' logger.setLevel(logging.WARNING) signal.signal(signal.SIGUSR1, set_logging_info)
signal.signal(signal.SIGUSR2, set_logging_warning)

根据源码中的日志调用 logger. info(message) ,我们知道,它的日志是 INFO 级,这也正是它的默认级别。那么,只要把默认级别调高到 WARNING 级,日志问题应该就解决了。
接下来,我们就来检查一下,刚刚的分析对不对。在终端中运行下面的 kill 命令,给进程 18940 发送 SIGUSR2 信号:
kill -SIGUSR2 18940

然后,再执行 top 和 iostat 观察一下:
top
...
%Cpu(s):  0.3 us,  0.2 sy,  0.0 ni, 99.5 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st iostat -d -x 1
Device            r/s     w/s     rkB/s     wkB/s   rrqm/s   wrqm/s  %rrqm  %wrqm r_await w_await aqu-sz rareq-sz wareq-sz  svctm  %util
loop0            0.00    0.00      0.00      0.00     0.00     0.00   0.00   0.00    0.00    0.00   0.00     0.00     0.00   0.00   0.00
sdb              0.00    0.00      0.00      0.00     0.00     0.00   0.00   0.00    0.00    0.00   0.00     0.00     0.00   0.00   0.00
sda              0.00    0.00      0.00      0.00     0.00     0.00   0.00   0.00    0.00    0.00   0.00     0.00     0.00   0.00   0.00

观察 top 和 iostat 的输出,你会发现,稍等一段时间后,iowait 会变成 0,而 sda 磁盘的 I/O 使用率也会逐渐减少到 0。
到这里,我们不仅定位了狂打日志的应用程序,并通过调高日志级别的方法,完美解决了 I/O 的性能瓶颈。
案例最后,当然不要忘了运行下面的命令,停止案例应用:
docker rm -f app

小结

日志,是了解应用程序内部运行情况,最常用、也最有效的工具。无论是操作系统,还是应用程序,都会记录大量的运行日志,以便事后查看历史记录。这些日志一般按照不同级别来开启,比如,开发环境通常打开调试级别的日志,而线上环境则只记录警告和错误日志。
在排查应用程序问题时,我们可能需要,在线上环境临时开启应用程序的调试日志。有时候,事后一不小心就忘了调回去。没把线上的日志调高到警告级别,可能会导致 CPU 使用率、磁盘 I/O 等一系列的性能问题,严重时,甚至会影响到同一台服务器上运行的其他应用程序。
今后,在碰到这种“狂打日志”的场景时,你可以用 iostat、strace、lsof 等工具来定位狂打日志的进程,找出相应的日志文件,再通过应用程序的接口,调整日志级别来解决问题。
如果应用程序不能动态调整日志级别,你可能还需要修改应用的配置,并重启应用让配置生效。
思考
最后,给你留一个思考题。
在今天的案例开始时,我们用 top 和 iostat 查看了系统资源的使用情况。除了 CPU 和磁盘 I/O 外,剩余内存也比较少,而内存主要被 Buffer/Cache 占用。
那么,今天的问题就是,这些内存到底是被 Buffer 还是 Cache 占用了呢?有没有什么方法来确认你的分析结果呢?
欢迎在留言区和我讨论,也欢迎把这篇文章分享给你的同事、朋友。我们一起在实战中演练,在交流中进步。

26 | 案例篇:如何找出狂打日志的“内鬼”?相关推荐

  1. ArcGIS案例学习笔记-找出最近距离的垂线

    ArcGIS案例学习笔记-找出最近距离的垂线 联系方式:谢老师,135-4855-4328,xiexiaokui@qq.com 目的:对于任意矢量要素类,查找最近距离并做图 数据: 方法: 0. 计算 ...

  2. 查找txt中的中文字符_找出nginx请求日志中某个url请求总次数排名前十的ip地址...

    答案如下: # 利用linux自带命令:sort.awk.grep.head.uniq组合得出相应的答案 awk '{print $1}' | grep 'www.xxxx.com' access.l ...

  3. 一招教你玩转SQL:通过找出数据的共同属性实现SQL需求

    关注我们,下载更多资源 预告:12月22日,ACOUG 联合创始人及多位核心专家将欢聚一堂,为"关键词解析2018技术圈的那些事儿"为主题的分享讨论,欢迎大家踊跃报名参会. 点击& ...

  4. python将大于输出列表_python找出列表中大于某个阈值的数据段示例

    该算法实现对列表中大于某个阈值(比如level=5)的连续数据段的提取,具体效果如下: 找出list里面大于5的连续数据段: list = [1,2,3,4,2,3,4,5,6,7,4,6,7,8,5 ...

  5. 给一个长度为n的字符串,找出长度为m的最小字典子序列【单调栈】

    一个大大的分割线,如果这个傻逼题没有被作为某某复赛的签到题,可能我一会都一直傻逼下去了. [2019计蒜之道复赛--星云系统] 题目是,给出一个长度为n(1<n<5e6)的只有小写字母的字 ...

  6. 【linux性能优化】定位分析狂打日志的问题

    一.文件系统和磁盘I/O 回顾一下文件系统和磁盘I/O的原理 1.1 文件系统 对存储设备上的文件进行组织管理的一种机制 为了支持各类不同的文件系统,Linux在各种文件系统上抽象了一层虚拟文件系统V ...

  7. 一篇英文文档中找出频数最多的10个单词

    """一篇英文文档中找出频数最多的10个单词collections: Counter 提供计数器工具以支持方便和快速的计数 most_common(n) 返回n个最常见元 ...

  8. MUI组件:弹出层组件 - 案例篇

    MUI组件:弹出层组件 - 案例篇 官方组件 · 演示地址:http://dcloud.io/hellomui/examples/actionsheet.html 动画演示 · 效果图:(按" ...

  9. 有26个字母a~z,找出所有字母组合,a、b、c、ab、abc、a~z 都是一个组合(顺序无关)

    有26个字母a-z,找出所有字母组合,a.b.c.ab.abc.a~z 都是一个组合(顺序无关) import java.util.HashSet;public class Test05_3 {pub ...

最新文章

  1. MATLAB实现图像镜像变换的源代码
  2. Java 洛谷 P1008 三连击
  3. 《CSS蝉意花园读书精记》(基础篇---------上.资料篇1)
  4. 机器学习实战_09_树回归_源代码错误修正
  5. linux+守护进程+php,【转载】Linux 守护进程的编程方法
  6. Supervisor管理hhvm进程
  7. js 变量的声明能提升 初始化不会提升
  8. 大学计算机老师说未来不需要程序员,都是机器自动生成代码。老师说估计20年,程序员就会被取代,现实吗?
  9. Ubuntu 右键打开终端
  10. 鲸鱼优化算法_鲸鱼优化算法:一种群体智能最优化方法
  11. iOS http文件下载
  12. 为Ubuntu安装中文语言包
  13. android系统解压zip文件,Android解压缩zip文件
  14. 微信小游戏代码包侵权的一种解决方案
  15. 一个最全产品开发流程
  16. 人口红利提前消失?今年经济会如何?权威回应来了
  17. 英文网页批量翻译导出本地教程
  18. 微星显卡拷机测试软件,微星显卡拷机软件(MSI Kombustor)
  19. sarscape5.6.2 结果在arcgis中制图(2可以在envi里调好色带导入arcgis)
  20. 城市消费券,拒绝恶意爬取

热门文章

  1. echarts词云图形状_用Python 3.8绘制词云图就这么20行代码
  2. a commit git 参数是什么意思_深入理解Git - 一切皆commit
  3. 层和 native_React-Native与小程序的底层框架比较
  4. qt 加载 图片旋转_QT 实现图片旋转的两种方法
  5. python 模糊匹配文件名 glob_Python: glob匹配文件
  6. mysql下载备份数据库命令行,如何从MariaDB数据库备份和还原命令行
  7. 怎么设置电脑屏幕一直亮着_电脑屏幕出现条纹是什么原因 花屏怎么解决?
  8. Hive DML操作
  9. win10 笔记本猎豹WiFi无法打开
  10. 一行代码搞定Python逐行内存消耗分析