进程性能分析工具 pidstat 和用 python 的 matplotlib 库输出分析图表

前情提要

这段时间在忙服务器压测的工作,虽然我们程序里面有统计 cpu 消耗的日志,但是有两点不足。一是该统计信息只统计了整个进程的总的 cpu 消耗,没有细致到每个线程的 cpu 消耗。二是该统计信息只包含了 cpu 的开销,没有其他诸如 io 消耗等数据。在 github 上以性能统计为关键字找到了 sysstat 库,这是一个 linux 系统性能分析的工具集。里面大多数工具是以整个系统为对象进行数据采样和统计的。而我主要是想统计指定进程的信息,我在这个工具集中找到一个符合的工具 pidstat,“pidstat reports statistics for Linux tasks (processes) : I/O, CPU, memory, etc.”。本文介绍 pidstat 的基础使用,以及将 pidstat 采样的数据以图表的方式输出出来,再依靠 matplotlib 的 backend 机制创建一个临时的 web 服务器,我们可以通过访问该临时站点查看和下载图表的 png 文件。

效果展示

这是执行脚本分析 pidstat 采样到的数据,将生成的图表展示到一个临时的 web 服务器上
在这里插入图片描述

访问上面提示的站点,有如下效果
在这里插入图片描述

在网页的左下角有下载按钮
在这里插入图片描述

pidstat 简介

pidstat 是 sysstat 工具集的一员。专门用于统计进程的各项指标。要在服务器上使用 pidstat,需要先安装 sysstat 工具集,用 yum 管理软件包可以使用以下命令安装:

yum install -y sysstat  # 安装
systemctl enable --now sysstat # 启用

下面介绍一下 pidstat 的常用命令:

  • -C name 表示采样进程的 Command 包含 name 字段(name 支持正则表达式),Command 为进程名字:
    在这里插入图片描述
  • -p pid 表示指定采样进程的 pid。使用该选项可以更精准的指定采样目标。
  • -u 采样 cpu 使用率,包含了 cpu 百分比,分为内核占用,用户占用,总占用。例如:
    在这里插入图片描述
  • -d 采样 IO 消耗,该选项只支持内核版本 2.6.20 以后的机器,可以用 uname -r 查看内核版本。主要关注每秒读写信息:
    在这里插入图片描述
  • -r 采样页中断次数和内存消耗,页中断次数可以反应程序申请和释放内存的频率,一些不好的代码逻辑也可能造成页中断次数增高。主要关注 rss(resident size),驻留物理内存。
    在这里插入图片描述
  • -t 同时展示归属该进程的线程信息,他们的关系会用树的形式展现,使用该选项的前提是用 -p 指定pid,我用 -C name 试了下没有捕获到具体的线程信息。如下:
    在这里插入图片描述
  • -h 该选项在你使用了上诉的多个采样选项之后可以加上,它可以让所有采样信息在水平一行一起输出,否则就是一个或者某几个天然可以搭配的采样内容一起输出,另外的单独输出,这样不方便将数据汇总起来二次处理。
  • 另外,在所有选项最后接上一个数字表示采样间隔时间(单位秒),如果你还想控制更精确点,后面还可以跟上一个数字表示在前一个数字指定的间隔时间内的采样次数。(采样间隔时间是可选项,默认值为 10 分钟,采样次数也是可选项,默认值为 1 次,我一般是设置为采样间隔 1 秒,采样次数就用默认值,即 1 秒 1 次)

这是我使用的采样命令:

nohup pidstat -p $pid -udrth 1 > $stat_cpu_log 2>/dev/null &
  • pidstat -p $pid -udrth 1 这部分是表示统计指定 pid 的进程的所有线程的 cpu 消耗,io 消耗,内存消耗,并且将每次的采样信息输出到一排,采样频率为 1 秒 1 次。
  • nohup 使得我关掉终端并不影响 pidstat 的运行。
  • > $stat_cpu_log 将 pidstat 输出的内容重定向到文件,方便我后续做二次处理。
  • 2>/dev/null 将 pidstat 产生的错误信息重定向到空文件,就是丢弃。
  • & 后台运行。

matplotlib 简介

Matplotlib是一个Python数据可视化库,提供了丰富的绘图工具,包括线图、散点图、条形图、等高线图、图像等。它的设计灵活,可以用于各种不同的绘图需求,从简单的图形到复杂的动态图形。Matplotlib的优点是它易于使用,具有广泛的文档和社区支持,可以与NumPy、SciPy等Python科学计算库无缝集成。同时,Matplotlib也是许多其他Python数据可视化库的基础,如Seaborn、ggplot等。

认识 figure 和 axes

figure 可以理解为一个图表的画布,这个画布上可以有多个 axes(轴,包含 x轴 和 y轴 组成的坐标轴),以下这张图是官网提供的 figure 介绍图,内容量很大。其中的蓝色圆圈表示圈中的内容可以由圈下面的函数调用绘制而成。
在这里插入图片描述

figure 和 axes 可以由 plt.subplots() 函数创建出来:

fig, ax = plt.subplots() # 创建包含了一个坐标轴的 figure
                         # figure 为一个 matplotlib.figure 对象
						 # ax 为一个 matplotlib.axes.Axes 对象
fig, axs = plt.subplots(rows, cols) # 创建包含了 rows * cols 个坐标轴的 figure
                                    # axs 为 matplotlib.axes.Axes 对象的二维数组
                                    # 形如 axs = matplotlib.axes.Axes[rows][cols]

我们的曲线或者其他图形都是基于 axes 来绘制的,figure 可以不要,例如用 plt.subplot 接口只创建坐标轴来绘制。但是基于 figure 来创建 axes ,可以用 figure 的一些属性来提供对图表的总体调控。

绘制曲线图

# -*- coding: utf-8 -*-
import matplotlib as mpl
import matplotlib.pyplot as plt
import numpy as np

# 0. 创建坐标轴,可以根据自己实际需要选择创建一个轴还是多个轴,只是要注意下多个轴是返回的数组
fig, ax = plt.subplots()
x_list = [1,2,3] # x 轴数据
y_list = [3,6,5]
# 1. 基本调用
ax.plot(x_list, y_list) # x 和 y 轴数据都由用户传入
# 2. 省略 x 数组
ax.plot(y_list)  # x 轴数据自动生成,系统会根据 y 轴数组长度自动填充 1~n 的 x 轴数据
# 3. 给曲线命名
ax.plot(y_list, label="name1") # label 属性可以指定该条曲线的名称
ax.legend() # 显示每条线的名字列表
# 4. 给坐标轴命名
ax.set_xlabel("我是x轴")
ax.set_ylabel("我是y轴")
# 5. 绘制
plt.show()

在这里插入图片描述

绘制柱形图

# -*- coding: utf-8 -*-
import matplotlib as mpl
import matplotlib.pyplot as plt
import numpy as np

# 0. 创建坐标轴,可以根据自己实际需要选择创建一个轴还是多个轴,只是要注意下多个轴是返回的数组
fig, ax = plt.subplots()
categories = ["name1", "name2", "name3"] # 柱形图 x 轴命名
values = [10, 35.5, 99] # 数据
# 1. 绘制到坐标轴上
rects = ax.bar(categories, values) # 返回值可以不关心,这里是用于设置柱形图顶上的数据显示需要用到下一个函数
# 2. 将数据标注到柱形图上方,方便观察
ax.bar_label(rects, padding=3, rotation=-55) # 
# 3. 将 x 轴的名字按 -55 度进行旋转,即是 顺时针旋转 55 度
ax.tick_params(axis='x', rotation=-55) # 这是为了避免名字过长,左右两个名字出现重叠
# 4. 绘制
plt.show()

在这里插入图片描述

创建两个轴,将上面两种图形放到一个 figure 中

# -*- coding: utf-8 -*-
import matplotlib as mpl
import matplotlib.pyplot as plt
import numpy as np

# 0. 创建两个坐标轴,用 figsize 设置图表的总大小
fig, axs = plt.subplots(1, 2, figsize=(12, 6))
ax = axs[0] # 曲线图用第一个
ax_bar = axs[1] # 柱形图用第二个

x_list = [1,2,3] # x 轴数据
y_list = [3,6,5]
# 1. 基本调用
ax.plot(x_list, y_list) # x 和 y 轴数据都由用户传入
# 2. 省略 x 数组
ax.plot(y_list)  # x 轴数据自动生成,系统会根据 y 轴数组长度自动填充 1~n 的 x 轴数据
# 3. 给曲线命名
ax.plot(y_list, label="name1") # label 属性可以指定该条曲线的名称
ax.set_xlabel("IamX")
ax.set_ylabel("IamY")
ax.legend() # 显示每条线的名字列表

categories = ["name1", "name2", "name3"] # 柱形图 x 轴命名
values = [10, 35.5, 99] # 数据
# 1. 绘制到坐标轴上
rects = ax_bar.bar(categories, values) # 返回值可以不关心,这里是用于设置柱形图顶上的数据显示需要用到下一个函数
# 2. 将数据标注到柱形图上方,方便观察
ax_bar.bar_label(rects, padding=3, rotation=-55)
# 3. 将 x 轴的名字按 -55 度进行旋转,即是 顺时针旋转 55 度
ax_bar.tick_params(axis='x', rotation=-55) # 这是为了避免名字过长,左右两个名字出现重叠

plt.show()

在这里插入图片描述

Backends of matplotlib

我在 matplotlib 的文档中发现一个 backends 机制,简单来说,matplotlib 可以让用户选择用什么后端技术来最终渲染输出绘制的内容,比如在 windows 平台,可以直接在控制台窗口执行脚本,弹出绘图窗口;或者将 matplotlib 嵌入到 PyQt 或 PyGObject 等图形用户界面中构建应用;还有一些人运行 web 应用程序服务器来动态展示图表。

由于我是在 linux 服务器上绘图,所以打开一个 web 服务器的 backend 特别合适,这样我每次有新的图表绘制后,只需要刷新一下网页即可,比生成图片,然后用 sz 发送到本地(sz 还和 tmux 冲突),再保存到文件夹,再打开来说方便太多了。

以下是 matplotlib 目前支持的 backend 模式,我要介绍的是 WebAgg 模式,其他模式大家可以按自己的需求来选择:
在这里插入图片描述

如何使用 WebAgg

  1. 由于 matplotlib 是根据环境变量 MPLBACKEND 的值来指定对应 backend 的, 所以要么预先设置服务器上的该环境变量为 MPLBACKEND=WebAgg,要么就是调用脚本时先指定环境变量,可以像这样调用:

    MPLBACKEND=WebAgg python3 test.py
    
  2. WebAgg 是通过调用 Tornado 这个库来生成 web 服务器的,所以需要用 pip 安装 Tornado 库。后面会说到版本的问题。

  3. 我自己测试发现,默认启动的 web 服务器的访问站点是 http://127.0.0.1:8898,如果你是在自己的电脑上执行脚本的话那么还好,你可以在网页中访问该站点。但是我是在内网的某个服务器上执行脚本,我从本地电脑是无法访问到服务器的这个站点的。官方有提供 WebAgg 的源码,通过源码发现可以像这样修改 web 服务器的 IP:

    import matplotlib as mpl
    mpl.rcParams['webagg.address'] = ip
    

    这里的 ip 你可以写死成你的服务器地址,但是这样肯定是不好的。这里提供一个获取 ip 的函数。

    # -*- coding: utf-8 -*-
    # filename: util.py
    
    import socket
    import matplotlib as mpl
    # 获取本机IP地址
    def get_local_ip():
        try:
            s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
            s.connect(('8.8.8.8', 80))
            ip = s.getsockname()[0]
        except:
            ip = '127.0.0.1'
        finally:
            s.close()
        return ip
    mpl.rcParams['webagg.address'] = get_local_ip()
    

示例

下面是封装了 matplotlib 的基础接口的一个示例代码:

# -*- coding: utf-8 -*-
# filename: graph.py
import matplotlib as mpl
import matplotlib.pyplot as plt
import numpy as np

import math

from util import *

class Graph:
    def __init__(self):
        ip = get_local_ip() # 上面有介绍怎么获取本地 ip
        print("show on web, ", ip)
        # export MPLBACKEND=WebAgg
        # 设置 web 地址,默认只能通过 127.0.0.1 访问,那么在服务器上启动,其他机器就不能访问
        mpl.rcParams['webagg.address'] = ip

    def showSingleAxes(self, x_list, y_list, ax):
        if not ax:
            fig, ax = plt.subplots()  # Create a figure containing a single axes.
        self.drawLine(ax, x_list, y_list)

    # @画单个坐标系,包含多条曲线
    # @x_list 横坐标数组
    # @dic <曲线名字, 曲线的纵坐标数组>
    # @title 坐标系名称
    # @ax 已存在的坐标系(可以先统一创建 n*2 的坐标系,然后依次填充内容)
    def showSingleAxesMultiLine(self, x_list, dic, title, ax):
        if not ax:
            fig, ax = plt.subplots(figsize=(8, 6))  # Create a figure containing a single axes.
        if title:
            ax.set_title(title)
        self.drawMutilLine(ax, x_list, dic)

    # 单 figure
    def showMultiAxes(self, x_list, dic):
        num_axes = len(dic)
        column = 2
        rows = math.ceil(num_axes / column)
        fig, axs = plt.subplots(rows, column, figsize=(18, 25))
        count = 0
        row = 0
        col = 0
        for thread_name, y_list in dic.items():
            axs[row][col].set_title(thread_name)
            self.drawLine(axs[row][col], x_list, y_list)
            col = col + 1
            if col == 2:
                row = row + 1
                col = 0

    # 获取轴数组
    def getAexsShare(self, row, col):
        fig, axs = plt.subplots(row, col, figsize=(9, 6 * row), sharex=True)
        fig.subplots_adjust(hspace=0)
        return axs

    # @创建指定 row 行 col 列的坐标系
    # @return 返回坐标系数组
    def getAexs(self, row, col):
        #fig, axs = plt.subplots(row, col, figsize=(18, 6 * row), gridspec_kw=dict(width_ratios=[10,8]))
        # layout{'constrained', 'compressed', 'tight', 'none', LayoutEngine, None}
        fig, axs = plt.subplots(row, col, figsize=(18, 8 * row),gridspec_kw=dict(width_ratios=[8,6]))
        fig.subplots_adjust(hspace=0.3)
        return axs

    # 多 figure
    def showMultiFigure(self, x_list, dic):
        for thread_name, y_list in dic.items():
            fig, ax = plt.subplots()
            ax.set_title(thread_name)
            self.drawLine(ax, x_list, y_list)

    # @平均值柱状图
    # @dic <柱形命名, 柱形纵坐标值>
    # @title 坐标系命名
    # @ax 已创建的坐标系
    # @rotation 柱形命名旋转角度(名字过长时适当倾斜)
    def showBar(self, dic, title, ax, rotation=0):
        if not ax:
            fig, ax = plt.subplots(figsize=(10, 6))
        if title:
            ax.set_title(title)
        categories = [name.replace("sub", "sub\n") for name in dic.keys()]
        rects = ax.bar(categories, [use for use in dic.values()])
        ax.bar_label(rects, padding=3, rotation=-55)
        ax.tick_params(axis='x', rotation=rotation)

    # @画单条曲线
    # @ax 已创建的坐标系
    # @x_list 横坐标数组
    # @y_list 纵坐标数组
    # @name 曲线名字
    def drawLine(self, ax, x_list, y_list, name):
        if len(x_list) == 0:
            ax.plot(y_list, label=name)
            ax.legend()
        else:
            ax.plot(x_list, y_list, label=name)
            ax.legend()


    # @画多条线
    # @ax 已创建的坐标系
    # @x_list 横坐标数组
    # @dic <曲线名字, 曲线的纵坐标数组>
    def drawMutilLine(self, ax, x_list, dic):
        for name, y_list in dic.items():
            self.drawLine(ax, x_list, y_list, name)

    # @启动网页服务器
    # 打开指定网址即可查看绘制内容
    def show(self):
        plt.show()

一般使用方式:

# -*- coding: utf-8 -*-
# filename: example.py

from graph import *

row = 10
col = 2
# ... 准备你要绘制的各种数据

gp = Graph()
axs = gp.getAexs(row, col) # 创建 row 行 col 列的坐标系

# 遍历坐标系,设 col 为 2, 每一行的左边坐标系绘制曲线,右边坐标系绘制柱形图
for _row in range(row):
# gp.showSingleAxesMultiLine(..., axs[_row][0]) 
# gp.showBar(..., axs[_row][1], ...)

gp.show() # 启动服务器 (MPLBACKEND=WebAgg 模式下)

注意事项

  • matplotlib 版本差异,python 版本差异,依赖库的差异都可能引发各种问题,下面列一下我使用的版本情况
    在这里插入图片描述
  • 如果有 python 安装模块不全的问题可以参考 python3.8 安装python3.10.3 安装
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

tobybo

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值