DataFrame.to_csv函数写入文件速度缓慢的解决方案

Pandas里的DataFrame.to_csv()是一个非常常用的函数,用于将内存中的数据以csv格式写到磁盘上,但当要写入的内容较多时,往往会遇到耗时过长的问题。这个问题的原因是to_csv()内部优化不够,我们可以利用其他软件包来曲线的解决此问题。

验证环境

硬件:

  • Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz 2.81 GHz
  • 32.0 GB / 512GB SSD

软件:

  • Windows 10 22H2
  • Python 3.9.13
  • pandas 2.2.2
  • numpy 1.26.4
  • duckdb 1.0.0
  • pyarrow 16.1.0

验证过程

首先随机生成一个较大尺寸的dataframe,内部所有数值都是float64类型,直接调用to_csv()写到磁盘并记录耗时;然后将此dataframe分别转换为numpyduckdbpyarrow的对应数据结构(转换过程本身耗时很短可忽略),并统计每次写入csv的耗时(毫秒),整理为下表:

DataFrame大小 CSV文件大小 pandas耗时 numpy耗时 duckdb耗时 pyarrow耗时
(10000,100) 17MB 1503 948 529 218
(50000,100) 87MB 7598 4815 2683 1107
(10000,500) 87MB 7341 4381 2811 1085

与Pandas.to_csv()对比的性能倍数关系:

DataFrame大小 CSV文件大小 pandas倍数 numpy倍数 duckdb倍数 pyarrow倍数
(10000,100) 17MB 1 1.6 2.8 6.9
(50000,100) 87MB 1 1.6 2.8 6.9
(10000,500) 87MB 1 1.7 2.6 6.8

注:验证过dask多partition的写入方式,但所得到的不是单个文件因此没有放入表格,并且写入耗时与pandas原生相差不大。

验证结论

可以看到在不同尺寸的数据量下,各软件包的性能都有所提升并且提升幅度比较稳定,其中pyarrow性能最佳,是pandas原生的接近7倍。建议对性能有要求的情况下,将dataframe转换为pyarrow table后再写入。

验证代码:

import pandas as pd
import numpy as np
import time

# 造dataframe数据
np.random.seed(42)
data = np.random.uniform(0, 100, size=(10000, 100))
df = pd.DataFrame(data)

# 直接用to_csv写入文件
# 验证过chunksize, index=False等参数对写入时间影响不大
t0 = time.time() * 1000
df.to_csv('c:/temp/1.csv')
t1 = time.time() * 1000
print(f'pandas cost {t1-t0} ms')

# df转换到numpy后写入csv文件
import numpy as np
numpy_array = df.values
t0 = time.time() * 1000
np.savetxt('c:/temp/1.csv', numpy_array, delimiter=',', fmt='%s')
t1 = time.time() * 1000
print(f'numpy cost {t1-t0} ms')

# df转换到duckdb表后写入csv文件
import duckdb
con = duckdb.connect(database=':memory:', read_only=False)
con.register('my_table', df)
t0 = time.time() * 1000
con.execute("COPY (SELECT * FROM my_table) TO 'c:/temp/2.csv' WITH (FORMAT CSV)")
t1 = time.time() * 1000
print(f'duckdb cost {t1-t0} ms')

# df转换到arrow表后写入csv文件
import pyarrow as pa
import pyarrow.csv as csv
table = pa.Table.from_pandas(df)
t0 = time.time() * 1000
csv.write_csv(table, 'c:/temp/1.csv')
t1 = time.time() * 1000
print(f'pyarrow cost {t1-t0} ms')

参考链接

最新版python安装Apache Beam失败问题和解决

Beam安装:
https://beam.apache.org/get-started/quickstart-py/

为安装Apache Beam,首先下载安装了最新版python 3.12.4。

file

> python --version
Python 3.12.4

按文档安装beam时报错无法编译wheel:

> pip install apache-beam
...
      INFO:root:copying apache_beam\portability\api\org\apache\beam\model\pipeline\v1\schema_pb2.pyi -> build\lib.win-amd64-cpython-312\apache_beam\portability\api\org\apache\beam\model\pipeline\v1
      INFO:root:copying apache_beam\portability\api\org\apache\beam\model\pipeline\v1\standard_window_fns_pb2.pyi -> build\lib.win-amd64-cpython-312\apache_beam\portability\api\org\apache\beam\model\pipeline\v1
      INFO:root:copying apache_beam\portability\api\standard_coders.yaml -> build\lib.win-amd64-cpython-312\apache_beam\portability\api
      INFO:root:running build_ext
      INFO:root:building 'apache_beam.coders.coder_impl_row_encoders' extension
      error: Microsoft Visual C++ 14.0 or greater is required. Get it with "Microsoft C++ Build Tools": https://visualstudio.microsoft.com/visual-cpp-build-tools/
      [end of output]

  note: This error originates from a subprocess, and is likely not a problem with pip.
  ERROR: Failed building wheel for apache-beam
  Building wheel for pyarrow (pyproject.toml) ... error
  error: subprocess-exited-with-error

  × Building wheel for pyarrow (pyproject.toml) did not run successfully.
  │ exit code: 1
  ╰─> [299 lines of output]
      <string>:34: DeprecationWarning: pkg_resources is deprecated as an API. See https://setuptools.pypa.io/en/latest/pkg_resources.html
      WARNING setuptools_scm.pyproject_reading toml section missing 'pyproject.toml does not contain a tool.setuptools_scm section'
      Traceback (most recent call last):
        File "C:\Users\zhanghao\AppData\Local\Temp\pip-build-env-0wvk6red\overlay\Lib\site-packages\setuptools_scm\_integration\pyproject_reading.py", line 36, in read_pyproject
          section = defn.get("tool", {})[tool_name]
                    ~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^
      KeyError: 'setuptools_scm'
      running bdist_wheel
      running build
      running build_py

按报错信息里的提示,从 https://visualstudio.microsoft.com/zh-hans/visual-cpp-build-tools/ 下载了最新版的vc++ buildtools仍然报错。

file

在so上找到类似的问题,其中一个解决方法是安装整个vc++,但需要下载6GB+太重了。发现还有另一个方法,就是不要使用最新版的python,因为最新版python里没有包含一些已编译的包。

https://stackoverflow.com/questions/64261546/how-to-solve-error-microsoft-visual-c-14-0-or-greater-is-required-when-inst

This error can happen when using the latest version of Python, e.g. 3.12, because the package wheels were only built for earlier versions of Python

卸载python 3.12.4,安装python 3.9.13,再重新安装beam成功。

> python --version
Python 3.9.13

> pip install apache-beam
...
Using legacy 'setup.py install' for crcmod, since package 'wheel' is not installed.
Using legacy 'setup.py install' for dill, since package 'wheel' is not installed.
Using legacy 'setup.py install' for hdfs, since package 'wheel' is not installed.
Using legacy 'setup.py install' for pyjsparser, since package 'wheel' is not installed.
Using legacy 'setup.py install' for docopt, since package 'wheel' is not installed.
Installing collected packages: pytz, pyjsparser, docopt, crcmod, zstandard, urllib3, tzdata, typing-extensions, six, rpds-py, regex, pyparsing, pyarrow-hotfix, protobuf, packaging, orjson, objsize, numpy, jsonpickle, idna, grpcio, fasteners, fastavro, dnspython, dill, cloudpickle, charset-normalizer, certifi, attrs, async-timeout, tzlocal, requests, referencing, redis, python-dateutil, pymongo, pydot, pyarrow, proto-plus, httplib2, jsonschema-specifications, js2py, hdfs, jsonschema, apache-beam
  Running setup.py install for pyjsparser ... done
  Running setup.py install for docopt ... done
  Running setup.py install for crcmod ... done
  Running setup.py install for dill ... done
  Running setup.py install for hdfs ... done
Successfully installed apache-beam-2.56.0 async-timeout-4.0.3 attrs-23.2.0 ...

Linux服务器基准性能测试常用工具

对Linux服务器做基准测试可以帮助确定服务器在特定任务或负载下的性能。这不仅有助于我们了解服务器的处理能力、响应时间、吞吐量等关键性能指标,也有助于我们进行服务器容量规划,确保服务器在未来的工作负载下仍能保持良好的性能。

本文总结Linux下常用的基准测试工具的使用场景和方法,按硬件组成分为CPU、内存、磁盘和网络四个部分。其中有些工具不是Linux自带的,此时需要先安装工具。以fio为例,在Ubuntu里使用sudo apt-get install fio,在CentOS里则使用sudo yum install fio即可完成安装。

一、CPU基准测试

lscpu - 查看CPU硬件信息

命令lscpu是显示关于 CPU 体系结构信息的一个工具,它从/proc/cpuinfo和其他系统文件中收集数据,汇总并以易于阅读的格式显示,不需要root权限。我们通常用它了解CPU的架构、核数和主频等基本信息:

> lscpu
Architecture:                    aarch64   <-- CPU架构,这里是ARM架构的
CPU op-mode(s):                  64-bit
Byte Order:                      Little Endian
CPU(s):                          128   <-- CPU核心数量128个
On-line CPU(s) list:             0-127
Thread(s) per core:              1  <-- 每核心线程数
Core(s) per socket:              64
Socket(s):                       2  <-- 两个物理CPU插槽
NUMA node(s):                    4  <-- NUMA节点数
Vendor ID:                       0x48
Model:                           0
Stepping:                        0x1
CPU max MHz:                     2600.0000   <-- CPU最大频率
CPU min MHz:                     200.0000
BogoMIPS:                        200.00
L1d cache:                       8 MiB
L1i cache:                       8 MiB
L2 cache:                        64 MiB
L3 cache:                        128 MiB     <-- 三级缓存大小,此缓存是多核共享的
NUMA node0 CPU(s):               0-31
NUMA node1 CPU(s):               32-63
NUMA node2 CPU(s):               64-95
NUMA node3 CPU(s):               96-127
...

其中Core、Socket和NUMA node的概念可以参考下图(来源):

file

sysbench cpu - 测试CPU性能

使用sysbench工具可以测试CPU的实际性能,它内部是通过反复查找指定范围(默认10000)内所有素数实现的,每完整查找完成一次即为一个事件(event),默认10秒后输出平均每秒完成的事件次数(eps, events per second)作为衡量CPU性能的指标。

若测试时在命令行里添加--threads参数指定了多线程,则eps指标也会相应上升,简单公平起见我们都用默认单线程测试即可。

> sysbench cpu run
Prime numbers limit: 10000
Initializing worker threads...
Threads started!

CPU speed:
    events per second:  3329.35   <-- 主要关注这个eps指标

General statistics:
    total time:                          10.0002s
    total number of events:              33299

Latency (ms):
         min:                                    0.30
         avg:                                    0.30
         max:                                    0.62
         95th percentile:                        0.31
         sum:                                 9994.03

Threads fairness:
    events (avg/stddev):           33299.0000/0.00
    execution time (avg/stddev):   9.9940/0.00

二、内存基准测试

dmidecode - 查看内存条硬件信息

命令dmidecode从系统的 DMI(Desktop Management Interface)表中读取数据,提供关于系统硬件组件的详细信息,如 主板、BIOS、处理器、内存、缓存、芯片组和其他系统硬件的信息。我们通常用它查看主板上各个内存条的型号、容量、频率等硬件信息。

查看已安装的物理内存条数及容量:

> sudo dmidecode -t memory | grep 'GB'
Size: 32 GB  <-- 单条内存容量32GB
Size: 32 GB
Size: 32 GB
Size: 32 GB

查看已安装的物理内存频率:

> sudo dmidecode -t memory | grep 'MT/s'
Speed: 3200 MT/s                    <-- 物理内存支持的最大频率
Configured Memory Speed: 2933 MT/s  <-- 实际运行频率, 通常是CPU无法支持到内存最高频率导致的
Speed: 3200 MT/s
Configured Memory Speed: 2933 MT/s
Speed: 3200 MT/s
Configured Memory Speed: 2933 MT/s
Speed: 3200 MT/s
Configured Memory Speed: 2933 MT/s

在测试内存性能时,需要注意缓存的影响,下图(来源)展示了典型的三级缓存架构。测试项目所处理的数据量应该大大超过缓存的大小,否则就变成了测试缓存的性能。

file

sysbench memory - 测试内存性能

sysbench工具可以测试内存的实际性能:

> sysbench memory run
Running memory speed test with the following options:
  block size: 1KiB
  total size: 102400MiB
  operation: write
  scope: global

Initializing worker threads...
Threads started!

Total operations: 38967914 (3896177.52 per second)
38054.60 MiB transferred (3804.86 MiB/sec)

General statistics:
    total time:                          10.0002s
    total number of events:              38967914

Latency (ms):
         min:                                    0.00
         avg:                                    0.00
         max:                                    0.26
         95th percentile:                        0.00
         sum:                                 4468.85

Threads fairness:
    events (avg/stddev):           38967914.0000/0.00
    execution time (avg/stddev):   4.4688/0.00

三、磁盘基准测试

lsblk - 查看磁盘信息

此命令可列出所有块设备及其属性,且不需要root权限。

> lsblk
NAME   MAJ:MIN RM   SIZE RO TYPE MOUNTPOINT
sda      8:0    0 446.6G  0 disk
├─sda1   8:1    0   512M  0 part /boot/efi
└─sda2   8:2    0 446.1G  0 part /

fdisk - 查看磁盘分区信息

命令fdisk是一个磁盘分区工具,也经常被用于显示磁盘信息,需要root权限。它比lsblk给出的信息更加详细一些,例如包含了每个磁盘分区的大小和文件系统信息。

> sudo fdisk -l
Disk /dev/sda: 446.64 GiB, 479559942144 bytes, 936640512 sectors
Disk model: SAS3908
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 4096 bytes
I/O size (minimum/optimal): 262144 bytes / 262144 bytes
Disklabel type: gpt
Disk identifier: 6819FBA6-5719-44F1-A2B6-F444F542E8BC

Device       Start       End   Sectors   Size Type
/dev/sda1     2048   1050623   1048576   512M EFI System
/dev/sda2  1050624 936638463 935587840 446.1G Linux filesystem

fio - 测试磁盘性能

fio是一个专业的磁盘性能测试工具。它可以配置多种测试场景,包括顺序读写和随机读写等,我们主要关注IOPS(每秒操作数)和BW(带宽)指标。

以测试顺序读写性能为例:

> fio --rw=rw  --directory=/tmp --size=256m --direct=1 --name=mytest
mytest: (g=0): rw=rw, bs=(R) 4096B-4096B, (W) 4096B-4096B, (T) 4096B-4096B, ioengine=psync, iodepth=1
fio-3.7
Starting 1 process
Jobs: 1 (f=1): [M(1)][100.0%][r=1749KiB/s,w=1789KiB/s][r=437,w=447 IOPS][eta 00m:00s]
mytest: (groupid=0, jobs=1): err= 0: pid=25055: Sun Jun 30 11:44:53 2024
...
Run status group 0 (all jobs):
   READ: bw=1712KiB/s (1753kB/s), 1712KiB/s-1712KiB/s (1753kB/s-1753kB/s), io=128MiB (134MB), run=76476-76476msec
  WRITE: bw=1716KiB/s (1757kB/s), 1716KiB/s-1716KiB/s (1757kB/s-1757kB/s), io=128MiB (134MB), run=76476-76476msec

命令行参数--direct表示是否使用操作系统级的磁盘缓存,默认值1。取01对结果影响很大(几十倍),0反映磁盘本身的性能,1更接近实际使用场景。

命令行参数--rw是指定要测试的项目,可选项是以下之一:

: read       Sequential read
: write      Sequential write
: trim       Sequential trim
: randread   Random read
: randwrite  Random write
: randtrim   Random trim
: rw         Sequential read and write mix
: readwrite  Sequential read and write mix
: randrw     Random read and write mix
: trimwrite  Trim and write mix, trims preceding writes

dd - 测试磁盘性能(仅顺序读写)

如果在服务器上无法安装fio等专业工具,可以用dd命令粗略测试磁盘的顺序读写性能,绝大部分linux发行版都自带dd命令,它本来的作用是快速复制和转换文件,不需要root权限。

凭空创建一个大小为 1GB 的文件 testfile,以此测量顺序写入的速度:

> dd if=/dev/zero of=/tmp/testfile bs=1G count=1 oflag=direct
1+0 records in
1+0 records out
1073741824 bytes (1.1 GB, 1.0 GiB) copied, 0.834568 s, 1.3 GB/s

从刚才创建的 testfile 中读取数据,以此测量顺序读取的速度:

> dd if=testfile of=/dev/null bs=1G count=1 iflag=direct
1+0 records in
1+0 records out
1073741824 bytes (1.1 GB, 1.0 GiB) copied, 0.665267 s, 1.6 GB/s

四、网络基准测试

lspci - 查看网卡硬件信息

用Linux自带的lspci命令可以查看所有pci设备的基本信息,我们用ethernet过滤一下就可以得到网卡信息,例如网卡型号。此命令不需要root权限。

> lspci | grep -i ethernet
7d:00.0 Ethernet controller: Huawei Technologies Co., Ltd. HNS GE/10GE/25GE RDMA Network Controller (rev 21)
7d:00.1 Ethernet controller: Huawei Technologies Co., Ltd. HNS GE/10GE/25GE Network Controller (rev 21)
7d:00.2 Ethernet controller: Huawei Technologies Co., Ltd. HNS GE/10GE/25GE RDMA Network Controller (rev 21)
7d:00.3 Ethernet controller: Huawei Technologies Co., Ltd. HNS GE/10GE/25GE Network Controller (rev 21)

更详细的信息可以加-v参数获得:

> lspci -v | grep -i ethernet -A 10
7d:00.0 Ethernet controller: Huawei Technologies Co., Ltd. HNS GE/10GE/25GE RDMA Network Controller (rev 21)
        Subsystem: Huawei Technologies Co., Ltd. HNS GE/10GE/25GE RDMA Network Controller
        Flags: bus master, fast devsel, latency 0, NUMA node 0
        Memory at 121000000 (64-bit, prefetchable) [size=64K]
        Memory at 120000000 (64-bit, prefetchable) [size=1M]
        Capabilities: <access denied>    <-- 要显示全部能力需要加sudo
        Kernel driver in use: hns3
        Kernel modules: hclge, hns3, hns_roce_hw_v2
...

不过lspci命令无法看到网卡的MAC地址,为解决这个问题我们可以使用ifconfig命令或ip命令。

ip -查看MAC地址和IP地址

以前我们经常使用ifconfig命令查看mac地址和ip地址,但这个工具已经几乎停止开发,逐渐被新的ip命令取代。因此这里我们仅介绍后者的使用方法。

注:有些Linux发行版已经不再默认提供ifconfig,若一定要使用,请先安装net-tools组件,例如sudo apt-get install net-tools

使用ip addr命令可以查看mac地址和ip地址。在下面的例子里,网卡1是LoopBack接口用于与本机通讯,网卡2~4为没有接入网络的物理网卡(DOWN),网卡5为已接入网络的网卡(UP)

> ip addr
ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: enp125s0f0: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
    link/ether c0:4e:f6:5d:24:06 brd ff:ff:ff:ff:ff:ff
3: enp125s0f1: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
    link/ether c0:4e:f6:5d:24:07 brd ff:ff:ff:ff:ff:ff
4: enp125s0f2: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
    link/ether c0:4e:f6:5d:24:08 brd ff:ff:ff:ff:ff:ff
5: enp125s0f3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether c0:4e:f6:5d:24:09 brd ff:ff:ff:ff:ff:ff                  <-- MAC地址
    inet 192.168.1.6/16 brd 192.168.255.255 scope global enp125s0f3     <-- IP地址、广播地址
       valid_lft forever preferred_lft forever
    inet6 fe80::b24f:a6ff:fe5c:1409/64 scope link
       valid_lft forever preferred_lft forever

ip命令还有很多功能,例如ip link set可以修改网卡属性,ip route可以操作静态路由表等,限于篇幅这里不作展开了。

netperf - 测试网络性能

使用 netperf 测试网络性能需要两台服务器,一台以netserver命令启动服务端(默认端口12865):

> sudo netserver -4 -L 0.0.0.0 -p 9292
Starting netserver with host '0.0.0.0' port '9292' and family AF_INET

另一台作为客户端以netperf命令向服务端反复发送数据,下面的例子中-t TCP_CRR表示为每次交易建立一个新的tcp连接(http的场景),-r 4k,1k指定请求和响应内容的大小,-l 30指定测试时长为30秒:

> netperf  -H 127.0.0.1 -P 9292 -l 30 -t TCP_CRR -- -r 4k,1k
MIGRATED TCP Connect/Request/Response TEST from 0.0.0.0 (0.0.0.0) port 0 AF_INET to 127.0.0.1 () port 0 AF_INET : demo
Local /Remote
Socket Size   Request  Resp.   Elapsed  Trans.
Send   Recv   Size     Size    Time     Rate
bytes  Bytes  bytes    bytes   secs.    per sec

16384  131072 4000     1000    30.00    10808.27    <-- 每秒完成10808个请求响应
16384  131072

华为鲲鹏服务器与Apple M2笔记本对比DuckDB查询性能

一、服务器配置

鲲鹏920(128核),512GB DDR4,SSD(原厂没有做RAID)

二、服务器环境测试结果

某项目的数据库使用DuckDB,其中有一个综合查询负载,此负载包含一个Select DISTINCT查询,50次Update操作,以及一个分页查询。

经测试发现此综合查询场景耗时比Apple M2笔记本长1 倍以上:

file

三、原因分析

1、CPU因素

华为服务器虽然有128核,测试过程中用top命令观察CPU使用情况,实际利用的核数在4~60之间浮动,说明数据库(duckdb)已经利用了多核。

m2比服务器CPU频率快38%。

华为服务器:鲲鹏920 @2.6GHz,128-bit SIMD
m2笔记本:Apple M2 @3.6GHz

file

2、磁盘因素

m2与服务器都是SSD,前者比后者快43%。

测试命令:

fio --rw=readwrite --ioengine=sync --fdatasync=1 --directory=test-data --size=4g --bs=4k --name=mytest

华为服务器:

read: IOPS=22.2k, BW=86.9MiB/s (91.1MB/s)(2049MiB/23590msec)
write: IOPS=22.2k, BW=86.8MiB/s (90.0MB/s)(2047MiB/23590msec)

m2笔记本(256G SSD):

read: IOPS=31.6k, BW=123MiB/s (129MB/s)(2049MiB/16621msec)
write: IOPS=31.5k, BW=123MiB/s (129MB/s)(2047MiB/16621msec)

3、内存因素

m2比服务器MEMCPY快4.8倍,MCBLOCK快1.6倍。

分析原因是服务器DDR4 3200MHz(鲲鹏920实际支持到2933MHz)频率低于m2笔记本的DDR5 6400MHz导致。

测试命令:

mbw -q -n 10 256

华为服务器:

AVG Method: MEMCPY Elapsed: 0.05330 MiB: 256.00000 Copy: 4802.921 MiB/s
AVG Method: DUMB Elapsed: 0.04562 MiB: 256.00000 Copy: 5611.106 MiB/s
AVG Method: MCBLOCK Elapsed: 0.02116 MiB: 256.00000 Copy: 12099.614 MiB/s

m2笔记本:

AVG Method: MEMCPY Elapsed: 0.00783 MiB: 256.00000 Copy: 32710.639 MiB/s
AVG Method: DUMB Elapsed: 0.01096 MiB: 256.00000 Copy: 23348.930 MiB/s
AVG Method: MCBLOCK Elapsed: 0.00800 MiB: 256.00000 Copy: 32016.809 MiB/s

提高有效内存带宽:

numactl -H命令看到鲲鹏920的128个核分布在4个numa node:

file

画成图就是这样的,CPU访问其他节点的内存时速度会受影响,影响程度取决于上图中的node distances数值:

file

numactl -C 0-31命令可以将进程限制在node 0上,这样避免了跨node的cpu到内存访问延迟。例如用 numactl -C 0-31 mbw -q -n 10 256 命令再次测试内存带宽,可以看到MCBLOCK提高了接近1倍(但仍然显著低于m2笔记本),MCBLOCK通常用于需要高效传输大块数据的场景因此对内存带宽比较敏感:

华为服务器(0-31核):

AVG Method: MEMCPY  Elapsed: 0.04889    MiB: 256.00000  Copy: 5236.255 MiB/s
AVG Method: DUMB    Elapsed: 0.04096    MiB: 256.00000  Copy: 6249.985 MiB/s
AVG Method: MCBLOCK Elapsed: 0.01208    MiB: 256.00000  Copy: 21185.915 MiB/s

四、改进

硬件方面服务器CPU无法更改,磁盘暂时无法更改(未来组RAID可提高性能),目前可以使用numactl提高有效内存带宽,测试查询性能是否有提高。

1、查询操作

经测试Select操作在所有核上运行与在0-31核运行,所消耗时间几乎相同。

2、更新操作

经测试发现Update操作对内存带宽比较敏感。上图为全部0-127核,下图为0-31核,可以看到后者消耗时间约为前者的70%~80%,性能提升还是比较明显的:

file

file

3、综合查询

numactl -C 0-31方式运行综合查询测试,新的耗时大约是上次测试的 2/3,上图为全部0-127核,下图为0-31核:

file

file

若使用64核运行测试,虽然CPU多了一倍,但耗时反而会增加20%左右,说明内存带宽对此负载的影响大于CPU核数的影响。
若使用16核运行测试,耗时介于比32核与64核之间。

五、附录

鲲鹏920 spec(非官方):https://ucfconsortium.org/wp-content/uploads/2020/02/Alex_Margolin_Kunpeng_UCX_Hackathon_2019.pdf

后台频繁Full GC导致进程卡住问题一例

问题现象

Springboot后台服务在向MySQL数据库写入数据的过程中,出现数据写入耗时过长的现象。正常情况下,这批数据的写入操作不应超过20分钟,但实际耗时却达数小时之久。

问题排查

首先怀疑是mysql瓶颈,但在mysql命令行里用processlist命令检查,未发现长时间等待的SQL语句,表明MySQL本身并没有出现性能不足的问题。

mysql -uroot -p mydatabase
show processlist

然后查看java后台日志发现线程池里的线程执行速度非常缓慢,按线程号过滤日志发现负责写入此批数据的线程在7:53卡住,直到9:47才恢复:

file

进一步检查后,发现发现gc日志目录里有大小为182G的dump文件,这个文件是项目组之前在java启动命令里配置的jvm参数(-XX:+HeapDumpBeforeFullGC)自动导出的:

file

Heap dump到磁盘本身会带来一些开销,因此我们先去掉了这个jvm参数。但重跑发现数据插入速度依然缓慢。

继续查看gc.log日志文件,可以看到有不少full gc发生,每次full gc会导致jvm暂停10秒左右。而两次full gc之间的间隔从一开始的几十秒到最后的1秒越来越短(每次gc释放的内存越来越少),导致jvm绝大部分时间都在进行gc,效率非常低。

file

full gc多数是由于在内存里创建了大对象造成的,从日志里看到要更新的计划在内存里是用map对象表示的,这些map对象的key数量总和1000万~2000万之间,每个value占用内存估算超过1KB,总共需要的内存粗估约10GB~20GB。

file

写入这批数据时,服务器可用内存情况:

file

jmap统计的backend jvm内存占用情况,可以看到BucketValue和DateFlagDto这两个对象占用的内存很高,而它们就是准备插入数据库的数据:

file

改进方案

至此频繁full gc的原因基本搞清楚了,即要导入的数据占用内存过多,以至于jvm不得不通过full gc让出足够的空间,而实际让出的空间有限且很快被从磁盘加载的新数据占满,导致再次full gc发生,如此循环。

解决方案也很简单,将数据拆为更小的批写入,改进后每批数据占用的内存大约是之前的十分之一,保险起见每次提交数据库后显式释放内存。现场验证频繁Full GC问题得到解决。

Pandas向量化计算与for循环对比

对pandas DataFrame数据进行相同类型的计算时,对比使用向量化方式与for循环方式的时间开销差异。

TLDR

浮点数计算速度:apply比for循环快4.8倍,向量化比for循环快6900倍。

字符串计算速度:apply比for循环快5.0倍,向量化比for循环快580倍。

因此建议尽量避免使用for循环的方式处理dataframe数据。

一、测试环境:

Apple M2,16GB,SSD

python 3.11.7

pandas 2.1.4

二、求和两列浮点数

用不同的内存对象计算两列之和(new_col=A+B),比较所消耗的时间,列A和列B都是1~100之间的浮点数。

file

向量化速度提升倍数:

file

apply比for循环提升4.8倍左右,向量化比for循环提升3000~9000倍(平均6900倍)。

三、字符串连接

用不同的内存对象将两列字符串连接,比较所消耗的时间,列A和列B都是8字节的字符串。

file

向量化速度提升倍数:

file

apply比for循环提升5.0倍左右,向量化比for循环提升430~650倍(平均580倍)。

Python代码单元测试和性能测试

本文介绍如何使用pytest对python工程进行单元测试和统计测试覆盖率,同时介绍如何测试python程序的运行速度和内存消耗。

一、工程目录结构

测试用例统一放在tests目录下,tests内部目录结构与被测代码的目录结构相同,以便快速定位测试用例代码。

file

如果环境还没有安装过pytest,先执行pip install pytest进行安装。

二、测试用例内容

测试用例的原则是逻辑尽量简单直白,这样当用例运行失败时能够较快的通过调试定位到问题原因,建议在测试代码里添加必要的注释。另外注意下面几个规则:

  • 文件名必须以test_开头,这样pytest命令可以正确识别此文件为测试用例。
  • 文件里每个测试方法必须以test_开头,原因同上。
  • 如果测试用例引用外部文件,可将文件放在同目录下,然后用os.path.dirname(file)引用py文件所在目录,确保从各个目录执行此测试用例都能正确找到外部文件。
  • 如果测试用例生成新的文件,建议用pytest提供的tmp_path参数(文档)作为输出目录,此临时目录下的文件会被自动清理,以免影响测试用例的下次运行。

以下是一个测试用例的例子(test_adapter_csv.py):

from acme.datasets.adapter import *
import os
import pandas as pd

# 验证正常加载csv文件
def test_read_file_to_df():
    df = CSVHandler.read_file_to_df(os.path.dirname(__file__), file_type=CSVInputFile.BUFFER)
    assert len(df) == 16

# 验证数据正常写出到csv文件
def test_write_df_to_csv(tmp_path):
    df = pd.DataFrame()
    CSVHandler.write_df_to_csv(df, path=str(tmp_path), file_type=CSVOutputFile.LOT_FLOW)
    assert os.path.exists(os.path.join(str(tmp_path), 'RESULT.csv'))

为提高代码覆盖率,我们不能只验证输入正常的情况,还应该对处理异常的情况进行验证:

# 验证要加载的csv文件不存在的情况
def test_read_file_to_df_not_exist():
    with pytest.raises(FileNotFoundError):
        df = CSVHandler.read_file_to_df(os.path.dirname(__file__), file_type=CSVInputFile.DEMAND)

三、执行测试用例

方式1、在IDE里可直接执行test_xxx.py文件里的指定方法
file

方式2、在IDE里右键点击tests目录选择执行所有测试用例
file

方式3、在命令行里执行pytest命令可以执行所有测试用例
file

四、统计测试覆盖率

PyCharm专业版里内置了覆盖率工具,在运行时选择"Run with coverage“即可,这里介绍PyCharm社区版如何统计测试覆盖率。

首先确保已安装测试覆盖率组件:

pip install pytest-cov
pip install pytest-html

并且确保在tests目录内的每个目录下已包含init.py文件(否则统计时会忽略此目录下的测试用例)。
file

然后在命令行里执行:

pytest --cov=acme --cov-report=html

即可生成测试覆盖率结果文件,放在htmlcov目录下,打开其中的index.html可查看结果。

即使只有一个测试用例,工程的总体覆盖率也已经达到28%,这是因为有很多import、def代码在加载模块时被覆盖到,而实际的业务逻辑代码并没有被覆盖。
file

file

如果忽略 --cov-report=html 参数则会在控制台里输出每个py文件的覆盖率报告,但不会包含逐行的覆盖率结果。
file

五、性能测试(时间)

方案1:cProfile+Snakeviz

首先安装pytest-profiling组件和snakeviz查看工具:

pip install pytest-profiling
pip install snakeviz

在命令行里执行pytest时添加–profile参数即可统计运行时间:

pytest tests/datasets/test_adapter_csv.py --profile

file

同时会生成二进制格式的统计文件:
file

使用snakeviz工具查看指定prof文件的内容:

snakeviz prof/combined.prof

file

方案2、Viztracer(推荐)

viztracer查看结果的交互方式更加友好,有利于更快速的定位问题,因此推荐使用。

首先安装viztracer组件:

pip install viztracer

用viztracer命令运行指定的测试用例:

viztracer --min_duration 0.1ms tests/datasets/test_adapter_csv.py
vizviewer result.json

运行结束后会生成result.json文件:
file

用vizviewer命令查看result.json的内容(会自动启动浏览器):

vizviewer result.json

file

六、性能测试(内存)

a. 整体内存分析

首先安装必要的组件:

pip install memory_profiler
pip install matplotlib

使用mprof运行指定的测试用例:

mprof run tests/datasets/test_adapter_csv.py

每次运行结束后会生成一个.dat文件:
file

使用mprof plot命令查看最新一个.dat文件的内容,若不指定文件名则自动查看最新一个文件,图形展示了测试用例执行过程中消耗的内存变化情况:

mprof plot mprofile_20240410185255.dat

file

b. 逐行内存分析

有时我们希望分析某个函数或者某几行代码的内存占用,可以用@profile修饰要分析的函数,然后在PyCharm里点击测试函数旁边的运行按钮启动测试用例(命令行里启动不行)。当运行完成后,在控制台里会输出此函数的逐行内存变化。

file

逐行内存占用情况如下图所示,从图中可以看到,读取DataFrame的操作新增占用了0.6MB的内存。

file

我们也可以用@profile标记多个函数,在PyCharm里批量运行测试用例,这样在测试报告里可以分别查看这些函数的逐行内存占用情况:
file

内存向量化计算性能比较

本文比较pandas、polars、duckdb和arrow等几种类库,在内存中进行向量化计算的执行时间差异。

测试环境

硬件:
Apple M2,16GB,SSD

软件:
python 3.11.7
pandas 2.1.4
pyarrow 14.0.2
duckdb 0.10.1
polars 0.20.15

求和两列浮点数

用不同的内存对象计算两列之和(new_col=A+B),比较所消耗的时间,列A和列B都是1~100之间的浮点数。

file

内存压力大时耗时会显著增加,例如下面的pandas和pandas_on_arrow曲线:

file

计算指定列平均值

用不同的内存对象计算指定列的平均值,比较所消耗的时间,指定列是1~100之间的浮点数。

file

字符串连接

用不同的内存对象将两列字符串连接,比较所消耗的时间,列A和列B都是8字节的字符串。

file

GlusterFS复制卷迁移至分布式复制卷

GlusterFS是广泛使用的去中心化的分布式文件系统,本文主要介绍GlusterFS里创建不同类型卷的方法,以及如何对已有卷转换类型。

GlusterFS没有提供专门的命令行选项指定卷类型,而是根据根据副本(replica)数量与块(brick)数量关系自动进行判断。

环境信息

CentOS 7.9
GlusterFS 9.4

创建复制卷(Replicate)

GlusterFS的复制卷可以类比于RAID1。当一个volume的replica是brick的整数倍(包含1倍)时创建出来的卷就是复制模式:

> gluster volume create volume1 replica 2 server1:/exp1 server2:/exp2
volume create: volume1: success: please start the volume to access data

> gluster volume start volume1
volume start: volume1: success

> gluster volume info
Volume Name: volume1
Type: Replicate
...

文件在复制卷上的分布,图片来自GlusterFS官方文档:

file

创建分布式复制卷(Distributed-Replicate)

GlusterFS的分布式复制卷可以类比于RAID10当brick数量是replica的整数倍(不含1倍)时,创建出来的卷是分布式复制模式:

> gluster volume create volume1 replica 2 server1:/exp1 server2:/exp2 server3:/exp3 server4:/exp4
volume create: volume1: success: please start the volume to access data

> gluster volume start volume1
volume start: volume1: success

> gluster volume info
Volume Name: volume1
Type: Distributed-Replicate
...

文件在分布式复制卷上的分布,图片来自GlusterFS官方文档:

file

将复制卷转为分布式复制卷

假设现在的复制卷是4节点4副本,要将其转换为4节点2副本。转换方法是先收缩位2节点2副本,再扩展为4节点2副本。

注意:收缩副本后,确保不用的旧目录(前面例子中/exp3和/exp4)一定要手工删除,不要直接使用这些目录进行扩容,否则会出现重复文件问题。

> gluster volume info
Volume Name: volume1
Type: Replicate
Volume ID: 38f39495-6678-40c1-9aa7-36399f6285cc
Status: Started
Snapshot Count: 0
Number of Bricks: 1 x 4 = 4
...

# 收缩(对replicate卷需要加force参数)
> gluster volume remove-brick volume1 replica 2 server3:/exp3 server4:/exp4 force

> gluster volume info
Volume Name: volume1
Type: Replicate
Volume ID: 38f39495-6678-40c1-9aa7-36399f6285cc
Status: Started
Snapshot Count: 0
Number of Bricks: 1 x 2 = 2
...

# 扩展(对replicate卷需要加force参数)
> gluster volume add-brick volume1 replica 2 server3:/exp3 server4:/exp4 force

> gluster volume info
Volume Name: volume1
Type: Distributed-Replicate
Volume ID: 38f39495-6678-40c1-9aa7-36399f6285cc
Status: Started
Snapshot Count: 0
Number of Bricks: 1 x 2 = 2
...

# 平衡
> gluster volume rebalance volume1 start
volume rebalance: volume1: success: Rebalance on volume1 has been started successfully. Use rebalance status command to check status of the rebalance process.
ID: 58799899-5f18-4cfb-98d9-96ab91ce6739

> gluster volume rebalance volume1 status
Node     ebalanced-files          size       scanned      failures       skipped               status  run time in h:m:s
---------      -----------   -----------   -----------   -----------   -----------         ------------     --------------
server2                2        0Bytes             5             0             0            completed        0:00:00
server3                0        0Bytes             5             0             0            completed        0:00:00
server4                0        0Bytes             5             0             0            completed        0:00:00
localhost              0        0Bytes             5             0             0            completed        0:00:00

当文件数量较多时,平衡所需的时间可能会很长,但这并不影响新文件写入glusterfs。下面是某个生产环境做平衡的过程,大约需要半个月:

         Node Rebalanced-files          size       scanned      failures       skipped               status  run time in h:m:s
    ---------      -----------   -----------   -----------   -----------   -----------         ------------     --------------
 10.102.9.127           496771         7.2TB       3340719             0             0          in progress       68:05:36
 10.102.9.128           403741         1.2TB       3433837             0             0          in progress       68:05:36
 10.102.9.132                1       28Bytes        675030            11         34918          in progress       68:05:31
 10.102.9.133                0        0Bytes        673981             0         12583          in progress       68:05:32
 10.102.9.134               12        93.2MB        681215             0         15036          in progress       68:05:34
    localhost           362702       677.4GB       3143169             0             0          in progress       68:05:36
Estimated time left for rebalance to complete :      322:37:43

遇到的问题

1、ls时发现有部分文件和目录名重复:

> ls -l /my/glusterfs
drwxr-xr-x 188 k2data k2data  4096 1月  22 10:14 ALaS
drwxr-xr-x 517 k2data k2data  4096 1月  24 15:19 AoF
drwxr-xr-x 517 k2data k2data  4096 1月  24 15:19 AoF
drwxr-xr-x 358 k2data k2data  4096 1月  22 09:23 BaiGL
drwxr-xr-x 328 k2data k2data  4096 1月  21 20:40 BaiS
drwxr-xr-x 328 k2data k2data  4096 1月  21 20:40 BaiS
...

此问题是由于有一个节点的brick目录没有清除,之前认为只要重命名即可(保留文件以便万一需要时恢复),但发现glusterfs似乎会识别到重命名后的文件,观察其下面的.glusterfs隐藏目录里的内容,修改时间一直在变。

后用gluster replace-brick命令从glusterfs里剔除此节点替换为另一个新服务器后问题消失。

2、有些目录权限丢失
例如下面的目录,一些目录权限变成了000,并且owner从k2data变成了root,导致相关程序报错Permission denied

> ls -l /my/glusterfs
drwxr-xr-x 273 k2data k2data  4096 1月  21 21:44 DiL
d--------- 315 root   root    4096 1月  21 21:46 FanC
drwxr-xr-x 338 k2data k2data  4096 1月  27 10:15 FengDS
...

出现原因不明,解决方法是手工将这些目录权限改过来(从000改为755):

ls -l | grep d--- | awk '{print $9}' | xargs chown k2data:k2data -R
ls -l | grep d--- | awk '{print $9}' | xargs chmod 755 -R

3、文件信息显示为问号

ls -l /my/glusterfs/data/172.17.23.230/20230629
ls: 无法访问/my/gluster/repo_FengDS_hfqfile/data/172.17.23.230/20230629/_zipped.zip: 没有那个文件或目录
总用量 0
-????????? ? ? ? ?            ? _zipped.zip

经排查是因为这个文件所在的物理目录不正确,例如文件本应在brick1、2、3,但实际被放在了brick4、5、6,如果手工移动到正确位置则问题消失。

产生这个问题的原因是gluster的元信息与文件实际所在物理节点不符,例如gluster认为文件a.txt存放在物理节点1和2上,但由于某种原因a.txt实际存放在节点3和4上。手工将文件移动到正确的位置后问题消失。

4、部分文件脑裂

> gluster volume heal volumn1 info
Brick server1:/my/glusterfs
<gfid:c22fa080-f917-4862-b9d9-a12aff40d392> - Is in split-brain
Status: Connected
Number of entries: 1

Brick server2:/my/glusterfs
<gfid:a60abad3-e42a-49ba-8b8e-463ea656c9cb>
<gfid:12c6e0ae-638c-4d59-96ff-c35a3bda3e07> - Is in split-brain
<gfid:f429f4ad-d935-48a0-bbfa-23407d433e8c> - Is in split-brain
<gfid:6e99bf54-07ab-4c55-92c1-0f5b297079a5> - Is in split-brain
/k2box/apps/com.my.import03/job - Is in split-brain
<gfid:5235e32e-4a7c-457a-ba08-3b7ef61a77e7> - Is in split-brain
<gfid:ba80b4bf-cff3-4dd2-b6a5-cba656431641> - Is in split-brain
/k2box/apps/com.my.import03/job/3471565
<gfid:493df16c-6c91-485a-b6f1-87208395b8f4>
<gfid:afd49274-d9e4-48b2-8814-53fd76fce327> - Is in split-brain
Status: Connected
Number of entries: 10
...

自动修复:

> gluster volume heal volumn1

5、有一个brick的统计信息异常
gluster volume top命令查询当前各brick打开文件数量,有一个brick的数量明显异常(18446744073709534192个):

> gluster volume top volume1 open | grep fds -B 2
Brick: 10.102.9.126:/my/glusterfs
Current open fds: 0, Max open fds: 1767, Max openfd time: 2023-08-16 04:08:46.556300 +0000
--
186883          /k2box/repos/repo_XuTuan_1sec/.repo/meta.settings
Brick: 10.102.9.134:/my/glusterfs
Current open fds: 18446744073709534192, Max open fds: 18446744073709551615, Max openfd time: 2024-02-01 20:44:34.245111 +0000
--
882             /k2box/apps/com.acme.import01/job/3527701/output/log/stats.log
Brick: 10.102.9.133:/my/glusterfs
Current open fds: 0, Max open fds: 400, Max openfd time: 2024-02-01 13:19:06.438338 +0000
--
2124            /k2box/repos/repo_ZhengXBQ_hive/.repo/batch.settings
Brick: 10.102.9.132:/my/glusterfs
Current open fds: 0, Max open fds: 400, Max openfd time: 2024-02-01 13:19:06.439036 +0000
...

重启glusterd服务或重新mount都无效。

参考资料

https://docs.gluster.org/en/latest/Administrator-Guide/Setting-Up-Volumes

NFS挂载意外断开导致java启动docker容器失败

一、环境信息

NFS客户端:
银河麒麟V10,ARM架构
Linux version 4.19.90-24.4.v2101.ky10.aarch64

NFS服务器:
Windows Server 2022 Standard

二、问题现象

通过java api启动docker容器时发现容器没有起来,查询后台日志信息如下:

[2024-01-14 15:10:01.141] [INFO] Starting docker container
[2024-01-14 15:10:02.117] [INFO] Recoverable I/O exception (java.net.SocketException) caught when processing request to {}->http://192.168.101.80:22375
[2024-01-14 15:10:02.152] [INFO] Docker client connected successfully
[2024-01-14 15:10:07.154] [INFO] Recoverable I/O exception (java.net.SocketException) caught when processing request to {}->http://192.168.101.80:22375
[2024-01-14 15:10:07.220] [INFO] Docker client connected successfully
[2024-01-14 15:10:12.221] [INFO] Recoverable I/O exception (java.net.SocketException) caught when processing request to {}->http://192.168.101.80:22375
[2024-01-14 15:10:12.282] [INFO] Docker client connected successfully
[2024-01-14 15:10:17.284] [INFO] Recoverable I/O exception (java.net.SocketException) caught when processing request to {}->http://192.168.101.80:22375
[2024-01-14 15:10:17.354] [INFO] Docker client connected successfully
[2024-01-14 15:10:22.356] [INFO] Recoverable I/O exception (java.net.SocketException) caught when processing request to {}->http://192.168.101.80:22375
[2024-01-14 15:10:22.391] [INFO] Docker client connected successfully

初步怀疑磁盘空间被占满导致,执行df命令时发现没有响应:

[root@192 storage]# df -h
(卡死在这里没有任何输出,只能按ctrl+c退出)

三、问题解决

由于前几天出现过df命令无响应的情况,所以马上意识到可能是远程nfs挂载的远程磁盘有问题,经过询问果然是有人改了远程nfs服务器的ip地址。

让运维恢复了nfs服务器的地址,然后重启一下rpcbind服务:

systemctl restart rpcbind

如果以上命令无效,可以尝试强制卸载nfs路径:

umount -f -l /mnt/hdfs/k2repos/CMSFTPServer

此时df命令恢复正常:

[root@192 storage]# df -h | grep -v docker
Filesystem                    Size  Used Avail Use% Mounted on
devtmpfs                       32G     0   32G   0% /dev
tmpfs                          32G     0   32G   0% /dev/shm
tmpfs                          32G  546M   31G   2% /run
tmpfs                          32G     0   32G   0% /sys/fs/cgroup
/dev/mapper/klas-root         4.0T  207G  3.8T   6% /
tmpfs                          32G     0   32G   0% /tmp
/dev/vda2                    1014M  218M  797M  22% /boot
/dev/vda1                     599M  6.5M  593M   2% /boot/efi
192.168.101.251:/home/data   1022G   15G 1007G   2% /mnt/hdfs/k2repos
10.144.136.169:/CMSFTPServer  7.3T  6.1T  1.3T  83% /mnt/hdfs/k2repos/CMSFTPServer
tmpfs                         6.3G     0  6.3G   0% /run/user/0

日志显示java启动docker容器也恢复正常:

[2024-01-14 16:00:01.149] [INFO] Starting docker container
[2024-01-14 16:00:13.955] [INFO] Container started, status code: 1

四、预防措施

为了预防类似情况发生时严重影响业务,可以考虑以soft模式挂载nfs服务(默认为hard模式),这样当nfs服务不可用时进程不会hang住,而是返回一个错误:

mount -o soft -t nfs 10.144.136.169:/CMSFTPServer /mnt/hdfs/k2repos/CMSFTPServer

实测以soft模式启动后,若nfs服务不可用,docker容器能够正常启动,但此时df命令仍然无响应,此时用df -x nfs4命令可以查看除nfs以外的磁盘。

但soft模式也有代价:

Since nfsv4 performs open/lock operations that have their ordering strictly enforced by the server, the options intr and soft cannot be safely used. hard nfsv4 mounts are strongly recommended.

五、参考链接