web-dev-qa-db-ja.com

C ++アプリケーションが特定の制限を超える巨大ページの割り当てに失敗する

概要概要

大量のデータ(〜1T)を読み取るC++アプリケーションがあります。私はhugepages(2Mで614400ページ)を使用して実行しましたが、これは128Gに達するまで機能します。

テストのために、c ++で、2Mのチャンクを割り当てられなくなるまで割り当てる単純なアプリケーションを作成しました。

アプリケーションは以下を使用して実行されます。

LD_PRELOAD=/usr/lib64/libhugetlbfs.so HUGETLB_MORECORE=yes ./a.out

実行中、私は無料の巨大ページの数を監視します(/ proc/meminfoから)。予想される速度で巨大なページを消費していることがわかります。

ただし、アプリケーションは、割り当てられた128G(または65536ページ)でstd :: bad_alloc例外でクラッシュします。

2つ以上のインスタンスを同時に実行すると、すべて128Gでクラッシュします。

Cgroupの制限を16Gなどの小さな値に減らすと、その時点でアプリが正しくクラッシュし、「バスエラー」が発生します。

些細なことを見逃していますか?詳しくは下記をご覧ください。

アイデアが足りなくなってきました...

詳細

マシン、OS、およびソフトウェア:

CPU    :  Intel(R) Xeon(R) CPU E5-2650 v4 @ 2.20GHz
Memory :  1.5T
Kernel :  3.10.0-693.5.2.el7.x86_64 #1 SMP Fri Oct 20 20:32:50 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux
OS     :  CentOS Linux release 7.4.1708 (Core)

hugetlbfs : 2.16-12.el7
gcc       : 7.2.1 20170829

私が使用した単純なテストコード(空きhugepagesが制限を下回るまで2Mのチャンクを割り当てます)

#include <iostream>
#include <fstream>
#include <vector>
#include <array>
#include <string>


#define MEM512K 512*1024ul
#define MEM2M   4*MEM512K

// data block
template <size_t N>
struct DataBlock {
  char data[N];
};

// Hugepage info
struct HugePageInfo {
  size_t memfree;
  size_t total;
  size_t free;
  size_t size;
  size_t used;
  double used_size;
};

// dump hugepage info
void dumpHPI(const HugePageInfo & hpi) {
  std::cout << "HugePages total : " << hpi.total << std::endl;
  std::cout << "HugePages free  : " << hpi.free << std::endl;
  std::cout << "HugePages size  : " << hpi.size << std::endl;
}

// dump hugepage info in one line
void dumpHPIline(const size_t i, const HugePageInfo & hpi) {
  std::cout << i << " "
            << hpi.memfree << " "
            << hpi.total-hpi.free << " "
        << hpi.free << " "
            << hpi.used_size
            << std::endl;
}

// get hugepage info from /proc/meminfo
void getHugePageInfo( HugePageInfo & hpi ) {
  std::ifstream fmeminfo;

  fmeminfo.open("/proc/meminfo",std::ifstream::in);
  std::string line;
  size_t n=0;
  while (fmeminfo.good()) {
    std::getline(fmeminfo,line);
    const size_t sep = line.find_first_of(':');
    if (sep==std::string::npos) continue;

    const std::string lblstr = line.substr(0,sep);
    const size_t      endpos = line.find(" kB");
    const std::string trmstr = line.substr(sep+1,(endpos==std::string::npos ? line.size() : endpos-sep-1));
    const size_t      startpos = trmstr.find_first_not_of(' ');
    const std::string valstr = (startpos==std::string::npos ? trmstr : trmstr.substr(startpos) );
    if (lblstr=="HugePages_Total") {
      hpi.total = std::stoi(valstr);
    } else if (lblstr=="HugePages_Free") {
      hpi.free = std::stoi(valstr);
    } else if (lblstr=="Hugepagesize") {
      hpi.size = std::stoi(valstr);
    } else if (lblstr=="MemFree") {
      hpi.memfree = std::stoi(valstr);
    }
  }
  hpi.used = hpi.total - hpi.free;
  hpi.used_size = double(hpi.used*hpi.size)/1024.0/1024.0;
}
// allocate data
void test_rnd_data() {
  typedef DataBlock<MEM2M> elem_t;
  HugePageInfo hpi;
  getHugePageInfo(hpi);
  dumpHPIline(0,hpi);
  std::array<elem_t *,MEM512K> memmap;
  for (size_t i=0; i<memmap.size(); i++) memmap[i]=nullptr;

  for (size_t i=0; i<memmap.size(); i++) {
    // allocate a new 2M block
    memmap[i] = new elem_t();

    // output progress
    if (i%1000==0) {
      getHugePageInfo(hpi); 
      dumpHPIline(i,hpi);
      if (hpi.free<1000) break;
    }
  }
  std::cout << "Cleaning up...." << std::endl;
  for (size_t i=0; i<memmap.size(); i++) {
    if (memmap[i]==nullptr) continue;
    delete memmap[i];
  }
}

int main(int argc, const char** argv) {
  test_rnd_data();
}

Hugepagesは、起動時にそれぞれ2Mで614400ページを使用するように設定されています。

/ proc/meminfoから:

MemTotal:       1584978368 kB
MemFree:        311062332 kB
MemAvailable:   309934096 kB
Buffers:            3220 kB
Cached:           613396 kB
SwapCached:            0 kB
Active:           556884 kB
Inactive:         281648 kB
Active(anon):     224604 kB
Inactive(anon):    15660 kB
Active(file):     332280 kB
Inactive(file):   265988 kB
Unevictable:           0 kB
Mlocked:               0 kB
SwapTotal:       2097148 kB
SwapFree:        2097148 kB
Dirty:                 0 kB
Writeback:             0 kB
AnonPages:        222280 kB
Mapped:            89784 kB
Shmem:             18348 kB
Slab:             482556 kB
SReclaimable:     189720 kB
SUnreclaim:       292836 kB
KernelStack:       11248 kB
PageTables:        14628 kB
NFS_Unstable:          0 kB
Bounce:                0 kB
WritebackTmp:          0 kB
CommitLimit:    165440732 kB
Committed_AS:    1636296 kB
VmallocTotal:   34359738367 kB
VmallocUsed:     7789100 kB
VmallocChunk:   33546287092 kB
HardwareCorrupted:     0 kB
AnonHugePages:         0 kB
HugePages_Total:   614400
HugePages_Free:    614400
HugePages_Rsvd:        0
HugePages_Surp:        0
Hugepagesize:       2048 kB
DirectMap4k:      341900 kB
DirectMap2M:    59328512 kB
DirectMap1G:    1552941056 kB

Ulimitからの制限:

core file size          (blocks, -c) 0
data seg size           (kbytes, -d) unlimited
scheduling priority             (-e) 0
file size               (blocks, -f) unlimited
pending signals                 (-i) 6191203
max locked memory       (kbytes, -l) 1258291200
max memory size         (kbytes, -m) unlimited
open files                      (-n) 1024
pipe size            (512 bytes, -p) 8
POSIX message queues     (bytes, -q) 819200
real-time priority              (-r) 0
stack size              (kbytes, -s) 8192
cpu time               (seconds, -t) unlimited
max user processes              (-u) 4096
virtual memory          (kbytes, -v) unlimited
file locks                      (-x) unlimited

cgroupの制限:

> cat /sys/fs/cgroup/hugetlb/hugetlb.2MB.limit_in_bytes
9223372036854771712

テスト

HUGETLB_DEBUG = 1を使用してテストコードを実行した場合の出力:

...
libhugetlbfs [abc:185885]: INFO: Attempting to map 2097152 bytes
libhugetlbfs [abc:185885]: INFO: ... = 0x1ffb200000
libhugetlbfs [abc:185885]: INFO: hugetlbfs_morecore(2097152) = ...
libhugetlbfs [abc:185885]: INFO: heapbase = 0xa00000, heaptop = 0x1ffb400000, mapsize = 1ffaa00000, delta=2097152
libhugetlbfs [abc:185885]: INFO: Attempting to map 2097152 bytes
libhugetlbfs [abc:185885]: WARNING: New heap segment map at 0x1ffb400000 failed: Cannot allocate memory
terminate called after throwing an instance of 'std::bad_alloc'
  what():  std::bad_alloc
Aborted (core dumped)

Straceの使用:

...
mmap(0x1ffb400000, 2097152, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_HUGETLB, -1, 0x1ffa200000) = 0x1ffb400000
mmap(0x1ffb600000, 2097152, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_HUGETLB, -1, 0x1ffa400000) = 0x1ffb600000
mmap(0x1ffb800000, 2097152, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_HUGETLB, -1, 0x1ffa600000) = 0x1ffb800000
mmap(0x1ffba00000, 2097152, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_HUGETLB, -1, 0x1ffa800000) = 0x1ffba00000
mmap(0x1ffbc00000, 2097152, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_HUGETLB, -1, 0x1ffaa00000) = -1 ENOMEM (Cannot allocate memory)
write(2, "libhugetlbfs", 12)            = 12
write(2, ": WARNING: New heap segment map "..., 79) = 79
mmap(NULL, 3149824, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = -1 ENOMEM (Cannot allocate memory)
mmap(NULL, 134217728, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_NORESERVE, -1, 0) = -1 ENOMEM (Cannot allocate memory)
mmap(NULL, 67108864, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_NORESERVE, -1, 0) = -1 ENOMEM (Cannot allocate memory)
mmap(NULL, 134217728, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_NORESERVE, -1, 0) = -1 ENOMEM (Cannot allocate memory)
mmap(NULL, 67108864, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_NORESERVE, -1, 0) = -1 ENOMEM (Cannot allocate memory)
mmap(NULL, 2101248, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = -1 ENOMEM (Cannot allocate memory)
write(2, "terminate called after throwing "..., 48) = 48
write(2, "std::bad_alloc", 14)          = 14
write(2, "'\n", 2)                      = 2
write(2, "  what():  ", 11)             = 11
write(2, "std::bad_alloc", 14)          = 14
write(2, "\n", 1)                       = 1
rt_sigprocmask(SIG_UNBLOCK, [ABRT], NULL, 8) = 0
gettid()                                = 188617
tgkill(188617, 188617, SIGABRT)         = 0
--- SIGABRT {si_signo=SIGABRT, si_code=SI_TKILL, si_pid=188617, si_uid=1001} ---

最後に/ proc/pid/numa_maps:

...
1ffb000000 default file=/anon_hugepage\040(deleted) huge anon=1 dirty=1 N1=1 kernelpagesize_kB=2048
1ffb200000 default file=/anon_hugepage\040(deleted) huge anon=1 dirty=1 N1=1 kernelpagesize_kB=2048
1ffb400000 default file=/anon_hugepage\040(deleted) huge anon=1 dirty=1 N1=1 kernelpagesize_kB=2048
1ffb600000 default file=/anon_hugepage\040(deleted) huge anon=1 dirty=1 N1=1 kernelpagesize_kB=2048
1ffb800000 default file=/anon_hugepage\040(deleted) huge anon=1 dirty=1 N1=1 kernelpagesize_kB=2048
...
28

ただし、アプリケーションは、割り当てられた128G(または65536ページ)でstd :: bad_alloc例外でクラッシュします。

割り当てている小さなサイズのセグメントが多すぎるため、プロセスごとに取得できるマップセグメントの数に制限があります。

sysctl -n vm.max_map_count

配列に少なくとも1024 * 512 * 4 == 2097152 MAPを使用しようとしていますが、vm.max_map_countのデフォルト値は65536のみです。

次の方法で変更できます。

sysctl -w vm.max_map_count=3000000

または、コードでより大きなセグメントを割り当てることもできます。

27
Stargateur