凌云的博客

行胜于言

捉虫日志:文件恢复速度慢

分类:debug| 发布时间:2016-09-08 14:44:00


场景

某客户报告文件恢复速度慢,环境如下:

备份集大小:500多G 文件个数:220W左右

环境以及数据收集

收集到的运行环境如下:

服务端为一体机,其 CPU,内存使用率,网络 I/O 以及 磁盘 I/O 都在合理范围内。

客户端为 VMWare 虚拟机,Windows 2008 R2 系统,文件系统为 NTFS, 分配了 24G 内存,4 核 CPU,CPU 占用率为 25% 左右, 网络 I/O 在 1Mbps 以内,磁盘 I/O 在 500 Kb/s 以内。

排除

依据恢复时数据的流向,出现问题的地方可能在 服务端处理本身,网络传输以及客户端本身。 首先在恢复慢的时候通过在客户端访问服务端的存储接口排除掉了网络传输以及服务端的可能。

将注意力具体放在客户端本身。

调试客户端

通过远程连接发现几个关键点:

  • 220W 的文件基本都在同一个目录下且大部分数据都是小于 100KB 的小文件。
  • 开始时恢复速度在 80 MB/s 左右,一个多小时后速度下降到 1 MB/s 以下,此时恢复了大概 130W 个文件。
  • 此时发现时间基本都花在了 CreateFileW 系统调用上,每秒只能创建不到 10 个文件。
  • 通过 process explorer.exe 查看发现进程占用的 CPU 时间基本都花在了 kernel time 上。

至此,基本可以排除速度慢是由我们软件本身引起的,而是受 NTFS 本身的瓶颈所限。 但是是否 NTFS 真的效率那么差?

环境模拟

前面说到关键点在于大量文件放在一个目录下会导致 Windows 创建新的文件时奇慢, 我们使用一个简单的程序来试试是否真的这样:

#include <ace/OS_NS_sys_time.h>
#include <ace/OS_NS_stdio.h>
#include <iostream>
#include <sstream>
#include <string>
#include <iomanip>
#include <stdio.h>

using namespace std;

void usage()
{
    cout << "Usage: " << " <count>" << endl;
}

void genfile(int i)
{
    time_t t = ACE_OS::time();
    stringstream ss;
    ss << i;
    cout << "genfile: " << ss.str() << " time: " << t << endl;
    FILE *fp = fopen(ss.str().c_str(), "a+");
    fclose(fp);
}

int main(int argc, char *argv[])
{
    if (argc < 2) {
        usage();
        return 0;
    }

    const int count = atoi(argv[1]);
    for (int i = 0; i < count; ++i) {
        genfile(content, i);
    }

    return 0;
}
// vim: set et ts=4 sts=4 sw=4:

尝试使用上述程序创建 300W 个文件,结果发现两个小时左右就创建完了, 没有出现上述所描述的单文件夹有 130W 个文件后每秒只能创建不到 10 个文件的情况。

重新查看了与客户的环境差异,发现除了文件名外没有其他差异,难道跟文件名有关? 修改测试程序,使得其生成的文件名与客户环境一致。

#include <ace/OS_NS_sys_time.h>
#include <ace/OS_NS_stdio.h>
#include <iostream>
#include <sstream>
#include <string>
#include <iomanip>
#include <stdio.h>

using namespace std;

void usage()
{
    cout << "Usage: " << " <count>" << endl;
}

void genfile(int i)
{
    time_t t = ACE_OS::time();
    stringstream ss;
    ss << std::setfill('0') << std::setw(16) << std::hex << i;
    cout << "genfile: " << ss.str() << " time: " << t << endl;
    FILE *fp = fopen(ss.str().c_str(), "a+");
    fclose(fp);
}

int main(int argc, char *argv[])
{
    if (argc < 2) {
        usage();
        return 0;
    }

    const int count = atoi(argv[1]);
    for (int i = 0; i < count; ++i) {
        genfile(content, i);
    }

    return 0;
}
// vim: set et ts=4 sts=4 sw=4:

尝试使用上述程序创建 300W 个文件,结果发现在生成了 130W 个文件后,速度急剧下降, 每秒只能生成不到 10 个文件,问题重现!!!

这是为什么呢?

8dot3name

这时同事给了一篇 NTFS 优化建议的文章过来,其中讲到禁用 8dot3name 会对性能有一定的提升。 抱着试试看的心态,禁用 8dot3name 重新生成 300W 个文件,结果发现两个小时左右就生成完了!!!

看来造成性能问题的罪魁祸首是 8dot3name 的生成。这是为什么呢? 搜索了下 8dot3name slow,发现有个家伙遇到跟我一样的问题: Debugging story: Slowness due to NTFS short file (8.3) name generation

其中还讲到了 8dot3name 的生成规则: How Windows Generates 8.3 File Names from Long File Names

简单来说就是去掉文件名所拥有的特殊字符后取文件名的前几个字符,再加上 ~1,~2 这类的后缀。 比如 alongfilename1 会生成: alongf~1,如果这个短文件名已经被占用则尝试使用 alongf~2,依次类推。

而这次客户的文件是具有相同前缀的(类似我们的测试程序生成的文件名),因此到一定程度后,生成 8dot3name 会变得极为缓慢。

禁用 8dot3name 的方法: How to disable 8.3 file name creation on NTFS partitions

fsutil.exe behavior set disable8dot3 1

Windows 7 以后的系统支持单独对特定卷进行此参数的设置:

fsutil.exe behavior set disable8dot3 2     // 设置全局选项为 Per-volume configurable (default)
fsutil.exe behavior set disable8dot3 e: 1  // 禁止在 e: 盘生成 8dot3

Windows 8 开始默认禁用了除 boot volume 外的其他 volumes 的 8dot3name。

总结

这个问题的重现有两个关键点:

  • 单个文件夹有大量文件(超过 150W)
  • 这些文件有相同的前缀(比如都是以 00000000 开头)

解决方法:在确认不会引发其他问题的前提下禁用 8dot3name