33 lotop与lostat命令:聊聊命令背后的故事与工作原理

你好,我是LMOS。

前面的课程里,我们学习了IO Cache、IO调度和IO管理的相关知识,但怎样度量和检测一个应用使用IO的情况呢?我们今天就来聊聊这个问题。

这节课我想带你认识两大监控IO操作的神器——iostat与iotop,让你掌握安装、使用它们的方法以及它们的工作原理。在Linux系统上,iostat和iotop这两个IO数据工具非常常用。它们都是性能分析领域中不可缺少的工具性软件,也经常被Linux网络服务器运维人员,用于分析某些服务器的IO类性能与故障。

安装iostat与iotop

在带你安装这两个工具之前,我先简单介绍下这两个工具的功能。iostat可以用来分析Linux系统整体IO的使用情况;而iotop作为iostat增强版和功能升级版,可以分析Linux系统每一个进程使用IO的情况。

在我们日常使用的Linux发行版中,是不包含iostat与iotop两个IO工具软件包的,需要我们自行安装它们才可以使用。

各大Linux发行版软件包管理方法并不统一,导致安装应用软件的方式不尽相同。考虑到Ubuntu、Deepin都是基于Debain开发的,所以我们这里以Debain系的Linux发行版为例进行操作。

我们只要在终端中输入如下命令,就可以安装iostat与iotop了。

//安装iostat
sudo apt-get install sysstat
//安装iotop
sudo apt-get install iotop

不过,我们并不能直接安装iostat,这样会提示找不到iostat软件包,因为它是包含在sysstat软件包中,所以我们必须先安装sysstat;而iotop却是独立的软件包,直接安装它就好了。如果你的Linux系统软件源和网络没有问题,肯定能安装成功。

你可能对这两个命令的使用方法不熟悉,没事,我们不妨在终端里输入这两个命令试一试,看看会出现什么效果。后面截图展示的是我自己机器上的情况:

图片

上图中左边是iostat命令执行的结果,右边是iotop命令执行的结果。如果你现在还看不懂这些信息,也没有关系,我们后面再介绍,这里仅仅是为了给你一个参考,你输入命令后显示效果类似上图的话,就说明安装成功了。

iostat命令

在前面我们已经成功安装了iostat命令,接下来我们重点聊聊它的使用方法,还有输出的数据表示的是什么。

iostat命令,是用来展示系统中的IO设备和CPU使用情况的。它的最大优势在于能汇报所有块设备活动的统计情况,同时也能汇报出CPU使用情况。但是iostat命令有一个缺陷,就是它不能对每个应用程序进程进行深入分析,只能分析系统的整体情况。

我们先来看iostat如何使用,它的使用形式如下:

iostat [选项] [参数]

“[]”中的内容可以省略,我们直接在终端中输入iostat,就会输出相关的全部信息。但是我们如果要观察特定信息,就需要使用相关选项了,我给你列了一个表梳理了最常用的部分,如下所示:

图片

了解了选项,还有个参数我们可能用得上,这个参数可以指定为设备名,比如/dev/sda。如果不带参数,就会默认显示所有IO储存设备的情况。

好,我们就这来练练手,使用iostat命令看看我们自己机器上的IO设备,主要是硬盘的使用情况。这里我们使用iostat -d -m -p -x 这个命令,该命令可以显示所有硬盘及其分区的信息,在我的机器上情况如下所示:

图片

上图中展示了所有硬盘及其分区的IO情况,第一列就是设备名,后面几列的相关说明,我用表格方式给你做了梳理:

图片

有了这些量化数据,我们就能判断每个硬盘分区的IO使用情况,从而分析出哪个设备占用IO请求量高或者低、占用时间多少、读取或者写入的数据量有多少。这样,性能瓶颈出现在哪个设备上,我们心中就有数了。

接下来我们继续讲解iostat的工作原理。iostat命令只是一个应用软件,它的功能就是计算统计数据并且显示。IO设备操作的数据肯定来源于内核,那iostat怎么获取这些数据就成了关键。

Linux的内核数据,都是以文件的形式提供的。换句话说,就是我们想要获取什么数据,就相应去读取什么文件。

下面我们手动读取一些文件,体验一下具体是什么情况,如下所示:

图片

对比iostat产生的数据,是不是感觉和上面读取的三个文件得到的数据很相似?是的,你猜的没有错,这些文件就是iostat命令的数据来源,主要的数据来源是/proc/diskstats文件,它记录着块设备IO操作的全部统计信息

下面我列了一个表,梳理了文件名和对应的统计信息,你可以看看:

图片

我们来看一看/proc/diskstats文件的数据结构,它的每一行代表一个块设备或者块设备的分区,总共20列数据,每一列的内容如下表:

图片

Linux块设备层在处理每个IO请求的时候,都会更新这些数据,具体的流程这里不展开了,iostat只使用了其中部分数据。由于这些数据是线性增加的,iostat只需要经过多次采集,进行简单的运算就可以了。

iostat只是负责的工作其实很简单,就是采集数据并计算显示。我们通过一段时间的IO请求数据、写入和读取的数据量、IO请求等待时间等等这些数据,就可以评估一个设备的IO性能了。好了关于iostat的工作原理我们讲到这里,我们接着探索iotop命令。

iotop命令

我们前面刚学过的iostat这个IO工具,只能收集量化到每个块设备的读写情况,但如果我们想知道每个进程是如何使用IO的就做不到,这就要用到iotop命令了。

iotop命令是一个top类工具,可以监视磁盘I/O使用状况,还可以作为iostat的升级工具使用。iotop命令具有与Linux系统自带的top相似的UI,只是top更关注进程,而iotop更关注IO。

iotop命令它是使用Python语言编写而成,需要用Python2.5以上的版本和Linux内核2.6以上的版本。iotop提供了源代码和二进制软件包,可以自己选择安装。在前面我们已经安装了iotop,如果你没有安装好,请回到前面看看怎么安装的。

像iostat一样,我们一起看看iotop如何使用,它的使用形式如下:

iotop [选项]

“[]”中的内容可以省略,直接在终端中输入iotop就会输出相关的全部信息,这一点与iostat相同,但是我们如果要观察特定信息,就需要使用相关选项了。我给你列了一个表梳理选项,如下所示:- 图片

我们马上来测试一下,使用sudo iotop 命令,注意该命令需要root权限才能运行,在前面要加上sudo。这条不带任何选项的命令,会显示所有用户的所有进程使用IO的情况,在我的机器上情况如下所示:

图片

上图中展示了所有进程读写硬盘的情况,头部的数据显示了每一秒钟,所有硬盘和当前硬盘的读写数据量。而下面的每一行代表一个进程,每一行的第一列就是进程id,也可以在运行过程中近“p”切换为线程id,那一行就表示一个线程。后面几列的相关说明,我给你列出了一个表格,如下所示:

图片

有了这些量化数据,我们就能判断哪些进程是IO型进程,哪些进程是计算型进程,每个进程的访问IO的数据一目了然。

根据这些数据,我们还能进一步分析出哪个进程使用IO的量是高或者低、占用时间多少、进程优先级多少。IO性能瓶颈出现在哪个进程上,需要优化哪个进程的IO模型,我们心中就有底了。

我们已经了解iotop的作用是观察所有进程的IO操作情况,那iotop的工作原理是什么呢?与iostat命令一样,iotop只是一个应用软件,用来统计所有进程的IO数据并显示。进程和IO操作数据必定来源于Linux内核,那iotop怎么获取这些数据呢?

在Linux上这些内核数据,都是以文件的形式提供的,即要获取什么数据,就读取什么文件。为了验证这个想法,下面我们试验一下,看看iotop是不是也是读取了一些/proc目录下的文件呢。

其实iotop是开源的,我们不妨下载它的代码来研究一下,命令如下:

//下载
wget http://guichaz.free.fr/iotop/files/iotop-0.4.4.tar.gz  
//解压
tar zxf iotop-0.4.4.tar.gz

我已经帮你下载好了代码,放在了课程的工程目录中。我们进入工程目录,就可以发现iotop是用python写的,入口点是iotop.py文件。

在iotop/ui.pi里,这个文件中会调用主函数main,主函数进而会调用run_iotop_window函数执行主要功能。在run_iotop_window函数中会调用ProcessList对象,获取所有进程的相关信息。

我们不妨看一看它的代码片段,如下所示:

class ProcessList(DumpableObject):
    def __init__(self, taskstats_connection, options):
        # {pid: ProcessInfo}
        self.processes = {}
        self.taskstats_connection = taskstats_connection
        self.options = options
        self.timestamp = time.time()
        self.vmstat = vmstat.VmStat()

        # A first time as we are interested in the delta
        self.update_process_counts()

    def get_process(self, pid):
        """Either get the specified PID from self.processes or build a new
        ProcessInfo if we see this PID for the first time"""
        process = self.processes.get(pid, None)
        if not process:
            process = ProcessInfo(pid)
            self.processes[pid] = process

        if process.is_monitored(self.options):
            return process

    def list_tgids(self):
        if self.options.pids:
            return self.options.pids

        tgids = os.listdir('/proc')
        if self.options.processes:
            return [int(tgid) for tgid in tgids if '0' <= tgid[0] <= '9']

        tids = []
        for tgid in tgids:
            if '0' <= tgid[0] <= '9':
                try:
                    tids.extend(map(int, os.listdir('/proc/' + tgid + '/task')))
                except OSError:
                    # The PID went away
                    pass
        return tids

    def list_tids(self, tgid):
        if not self.options.processes:
            return [tgid]

        try:
            tids = map(int, os.listdir('/proc/%d/task' % tgid))
        except OSError:
            return []

        if self.options.pids:
            tids = list(set(self.options.pids).intersection(set(tids)))

        return tids

    def update_process_counts(self):
        new_timestamp = time.time()
        self.duration = new_timestamp - self.timestamp
        self.timestamp = new_timestamp

        for tgid in self.list_tgids():
            process = self.get_process(tgid)
            if not process:
                continue
            for tid in self.list_tids(tgid):
                thread = process.get_thread(tid, self.taskstats_connection)
                stats = self.taskstats_connection.get_single_task_stats(thread)
                if stats:
                    thread.update_stats(stats)
                    thread.mark = False

        return self.vmstat.delta()

我们来梳理一下上述代码都做了什么。在ProcessList类的构造方法init中,会调用update_process_counts方法,接着在其中调用list_tgids方法,该方法会打开/proc目录获取所有以数字命名的目录名称,那就是TGID。

TGID就是线程组ID,对于同一进程中的所有线程,TGID都是一致的,也就是该进程的进程ID。接着循环调用get_process方法,在该方法中会构造ProcessInfo对象以获取每个进程的数据。

ProcessInfo类的代码如下所示:

class ProcessInfo(DumpableObject):
    def __init__(self, pid):
        self.pid = pid
        self.uid = None
        self.user = None
        self.threads = {} # {tid: ThreadInfo}
        self.stats_delta = Stats.build_all_zero()
        self.stats_accum = Stats.build_all_zero()
        self.stats_accum_timestamp = time.time()

    def get_uid(self):
        if self.uid:
            return self.uid
        try:
            uid = os.stat('/proc/%d' % self.pid)[stat.ST_UID]
        except OSError:
            # The process disappeared
            uid = None
        if uid != self.uid:
            # Maybe the process called setuid()
            self.user = None
            self.uid = uid
        return uid

    def get_user(self):
        uid = self.get_uid()
        if uid is not None and not self.user:
            try:
                self.user = safe_utf8_decode(pwd.getpwuid(uid).pw_name)
            except KeyError:
                self.user = str(uid)
        return self.user or '{none}'

    def get_proc_status_name(self):
        try:
            first_line = open('/proc/%d/status' % self.pid).readline()
        except IOError:
            return '{no such process}'
        prefix = 'Name:\t'
        if first_line.startswith(prefix):
            name = first_line[6:].strip()
        else:
            name = ''
        if name:
            name = '[%s]' % name
        else:
            name = '{no name}'
        return name

    def get_cmdline(self):
        # A process may exec, so we must always reread its cmdline
        try:
            proc_cmdline = open('/proc/%d/cmdline' % self.pid)
            cmdline = proc_cmdline.read(4096)
        except IOError:
            return '{no such process}'
        #……
        return safe_utf8_decode(cmdline)

    def did_some_io(self, accumulated):
        if accumulated:
            return not self.stats_accum.is_all_zero()
        for t in self.threads.itervalues():
            if not t.stats_delta.is_all_zero():
                return True
        return False

    def get_ioprio(self):
        priorities = set(t.get_ioprio() for t in self.threads.itervalues())
        if len(priorities) == 1:
            return priorities.pop()
        return '?dif'

    def set_ioprio(self, ioprio_class, ioprio_data):
        for thread in self.threads.itervalues():
            thread.set_ioprio(ioprio_class, ioprio_data)

    def ioprio_sort_key(self):
        return ioprio.sort_key(self.get_ioprio())

    def get_thread(self, tid, taskstats_connection):
        thread = self.threads.get(tid, None)
        if not thread:
            thread = ThreadInfo(tid, taskstats_connection)
            self.threads[tid] = thread
        return thread

    def update_stats(self):
        stats_delta = Stats.build_all_zero()
        for tid, thread in self.threads.items():
            if thread.mark:
                del self.threads[tid]
            else:
                stats_delta.accumulate(thread.stats_delta, stats_delta)

        nr_threads = len(self.threads)
        if not nr_threads:
            return False

        stats_delta.blkio_delay_total /= nr_threads
        stats_delta.swapin_delay_total /= nr_threads

        self.stats_delta = stats_delta
        self.stats_accum.accumulate(self.stats_delta, self.stats_accum)

        return True

以上代码,无一例外都是从/proc目录下那些数字命名的子目录里获取数据。我们不妨打开proc目录观察一下,并且我们还要选择一个特定的、数字命名的子目录进入,如下所示:

图片

这是谷歌浏览器的进程,里面包含很多子目录,这些子目录中包括了进程的状态、属性、应用程序命令、打开的文件、IO、网络、虚拟内存空间、工作目录、权限、调度信息等大量信息数据。关于进程的所有信息,我们从这里都可以找到。而iotop也正是从这里获取数据,然后计算和显示的,这就是iotop的工作原理。

重点回顾

这节课我们一起学习了两大监控IO操作的神器,即iostat和iotop。它们俩在以后的性能调优路上,将是我们最忠诚的伙伴,一个观察系统全局IO情况,另一个用来查看单个进程的IO情况。有了它们,我们就能精确定位Linux服务器上IO性能瓶颈所在。

现在,让我们一起来回顾一下今天所学。首先我们安装了iostat和iotop。由于iostat包含在sysstat中,需要安装sysstat软件包,才能得到iostat。安装成功后,别忘了进行测试。

然后,我们学习了iostat怎么用,熟悉了它的选项和参数,以及iostat输出的数据表示什么。之后我们研究了iostat实现原理,它是通过读取/proc目录下的一些文件做到的。

iotop工具是一个用python语言编写的工具,它能监视全局硬盘的IO性能和每个进程的IO情况,是一个全面的IO监视工具。和iostat一样,它也是通过读取/proc目录下每个进程目录中的一些文件,获取其中的数据,再经过计算把数据展示给我们。

这节课的导图如下所示,供你参考:

图片

你是否想对/proc文件系统有更深的了解,写出更强大的监视工具呢?其实你需要的大部分数据源,都可以在/proc目录中找到,读取它们就能做出更符合自己业务需求的监视工具,赶快去大胆尝试吧。

思考题

请说一说 iostat与 iotop的不同之处?

欢迎你在留言区记录自己的收获或疑问,如果觉得这节课还不错,也别忘了推荐给自己身边的朋友。