鲲鹏社区首页
中文
注册
一次nginx返回文件性能差异问题定位

一次nginx返回文件性能差异问题定位

案例分享

发表于 2025/07/05

0

作者 | 吴亦航

问题现象

某场景在CentOS 7.6和openEuler 22.03 LTS SP2上分别对nginx进行HTTP请求压测,测试观察到服务端nginx返回静态文件和不返回静态文件时的性能差距很大。
返回静态文件的nginx配置:

        location / {
            root         /usr/share/nginx/some_file;
        }

此时openEuler的QPS仅为CentOS一半。

但若不返回静态文件,只令nginx返回内存中一段等长且预设好的数据(测试时512字节的数据,此处仅示意):

        location = /index_64B.html {
            return 200 'some_512_bytes_data';
        }

此时两边QPS相近。


问题分析

上述现象说明nginx中“对文件的操作”影响了性能。由于读文件涉及多个系统调用,故使用strace对比openEuler和CentOS在整个测试期间的系统调用情况。使用以下命令抓取系统调用的统计。

strace -c -p {PID}

结果如下:



可以观察到,两边对文件相关的两个系统调用——打开文件和获取文件属性,用的是不同的实现。并且openEuler用的实现平均耗时显著高于CentOS。

用strace抓取“不返回静态文件”测试中的系统调用,发现系统调用只有recvfrom,writev,因此这个场景下openEuler和CentOS的性能相近。

事实上,在频繁访问静态文件的场景下,启用nginx的open_file_cache选项能极大地提升整体性能。这个选项会使nginx缓存文件描述符及相应的元数据,不用在每个请求中都执行打开、关闭文件的操作。此时,系统调用只有recvfrom,pread64,writev。这种情况下测得两边的性能也非常接近。

为避免内核差异影响,已手动在两个OS上安装相同的内核,那么系统调用的差异大概率是由glibc版本差异引入的。经CentOS环境的glibc版本为2.17,openEuler环境的glibc版本为2.33。在openEuler社区上能看到相关讨论,即openEuler 22.03 SP2里glibc的fstat函数使用了newfstatat系统调用作为实现,导致性能降低。SP3中重新用回fstat系统调用作为fstat函数的实现,不再存在这个性能问题。

方案验证

下载openEuler 22.03 LTS SP3版本里最新的glibc,glibc-common,glibc-devel,安装后进行测试,结果发现openEuler环境性能提升至与CentOS持平。

根因分析

对比相似场景下fstat和newfstatat的调用时间:

#include <linux/fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

const int NUM_ITERS = 1000000;
const char *FILE_NAME = "/usr/share/nginx/html/index_512B.html";

int main(int argc, char *argv[]) {
    struct timeval begin, end;
    long elapsedTime;
    int fd;

    struct stat sb;

    // fstat
    gettimeofday(&begin, NULL);
    fd = open(FILE_NAME, O_RDONLY | O_NONBLOCK);
    for (int i = 0; i < NUM_ITERS; ++i) {
#if !TEST_NEWFSTATAT
        fstat(fd, &sb);
#else
        fstatat(fd, "", &sb, AT_EMPTY_PATH);
#endif
    }
    close(fd);
    gettimeofday(&end, NULL);
    elapsedTime = end.tv_sec * 1000000 + end.tv_usec - begin.tv_sec * 1000000 - begin.tv_usec;

#if !TEST_NEWFSTATAT
    printf("Testing fstat: %d us\n", elapsedTime);
#else
    printf("Testing newfstatat: %d us\n", elapsedTime);
#endif
}

编译

gcc -o fstat test_fstat.c
gcc -DTEST_NEWFSTATAT -o newfstatat test_fstat.c

执行测试

time ./fstat
time ./newfstatat

结果如下,发现内核态耗时差距为一倍。



newfstatat性能差的原因

首先,newfstatat是一个比fstat更复杂的系统调用,会做一些额外的动作,本身会有一定耗时。

更为重要的是,newfstatat系统调用的传参形式为 (fd, "", &stat_buf, AT_EMPTY_PATH) ,其第一个参数是文件描述符,第二个参数是字符串格式的路径参数,即newfstatat既支持通过文件描述符调用,也支持传文件路径调用。相较之下,fstat系统调用的传参形式为 (fd, &statbuf) ,仅支持使用文件描述符调用。然而,newfstatat的灵活性导致在使用文件描述符调用时,也要传一个空的字符串过去。内核在处理该系统调用时,按规定,需要判断这个字符串参数是什么,因而需要将用户内存中的字符串拷贝到内核内存(strncpy_from_user),这里会产生开销。事实上,所有*at()形式是系统调用都会受到影响。但在现实负载下,通常只有newfstatat的性能下降会被诟病,因为只有它的使用频率可能会很高(如与open搭配使用),导致性能问题会被积累放大。

glibc上游社区于2023-09-11在master分支合入了修复此问题的commit,于行文之时,该commit只存在于master分支,没有合入2.38及更低版本的分支。至于为什么glibc社区当初选择用性能更差的newfstatat系统调用作为fstat函数的实现,Linus认为这只是社区演进中一个不经意的错误

src-openEuler/glibc仓于2023-10-20在openEuler-22.03-LTS-SP3分支中引入了上游社区的commit作为patch修复了fstat的性能问题,相应的glibc Release版本为2.34-138。因此在openEuler 22.03 SP3的镜像中,这个性能问题不复存在。

本页内容