Быстрые старты
Когда вы пишете очень сильный высоконагруженный сервис (скажем, поиск), очень важно делать так, чтобы вы стартовали и отвечали уже в достаточно "разогретом" виде, иначе при выкладке нового бинаря первые несколько секунд будут очень болезненными, что все запросы раз в 5 медленнее.
Это происходит из-за того, что коду надо "прогреться", а именно когда вы заходите в новый сегмент кода в ассемблере, память может не находиться в резидентной, и вам надо сделать pagefault -- загрузить страницу в памяти. Это медленно, и именно из-за этого много проблем на старте. Иногда даже бывает, что редкие запросы заходят в сложные и редкие куски кода, и страницы памяти Linux, ответственные за этот кусок кода, могут вымываться.
Одно из решений, которое часто можно встретить в интернете, но всё равно непопулярное среди перформанс трюков, это системный вызов
Часто это делают прям первой строкой в main:
Часто секции подгружаются частями, если вы запустите бинарный файл и посмотрите на все mmap'ed секции, то скорее всего увидите что-то такое:
Две, наверное, понятно — мы хотим разграничить память по чтению и записи, скажем, .text (код), .rodata (read only data, константы и тд) не должны быть записываемы, а .data, .bss должны. Так делает линкер GOLD. Cм вывод.
Линкер lld (LLVM) делает 3 LOAD секции. Из-за проблем безопасности разграничивает .rodata и .text. ld (GNU) делает 4 секции, разделяя .data от .got (Global Offset Table).
Если коротко, то сделано для безопасности, но в угоду немного перформансу. В реальности вряд ли этот перформанс можно заметить или найти.
Если всё сопоставить, то получится, что .text (код) находится у ClickHouse в
Когда вы пишете очень сильный высоконагруженный сервис (скажем, поиск), очень важно делать так, чтобы вы стартовали и отвечали уже в достаточно "разогретом" виде, иначе при выкладке нового бинаря первые несколько секунд будут очень болезненными, что все запросы раз в 5 медленнее.
Это происходит из-за того, что коду надо "прогреться", а именно когда вы заходите в новый сегмент кода в ассемблере, память может не находиться в резидентной, и вам надо сделать pagefault -- загрузить страницу в памяти. Это медленно, и именно из-за этого много проблем на старте. Иногда даже бывает, что редкие запросы заходят в сложные и редкие куски кода, и страницы памяти Linux, ответственные за этот кусок кода, могут вымываться.
Одно из решений, которое часто можно встретить в интернете, но всё равно непопулярное среди перформанс трюков, это системный вызов
mlockall
. Он "блокирует" все страницы, отображаемые в адресное пространство вызывающего процесса. Сюда входят страницы кода (секция .text), сегмент данных (секция .data), стека, все открытые файлы, всё, до чего процесс может дотянуться. Блокировка в данном случае означает, что страницы памяти никогда не уйдут. Это решает выше приведённую проблему, и если не хочется сильно думать, это отличное решение для вашего сервиса.Часто это делают прям первой строкой в main:
int main(int argc, char** argv) { mlockall(); run(argc, argv); }
Можно делать умнее, в бинарном файле как правило секция с дебаг символами (названия функций, просто какая-то дебаг информация) занимает до 80% бинарного файла и при mlockall
они тоже все пойдут в память, хотя они практически никогда не нужны при рантайме, только при дебаге. Чтобы решить эту проблему, можно искать секцию .text в бинарном файле, и подгружать только её. Благо весь стек компилятора и особенности Linux в какой-то степени делают это за нас. Рассмотрим пример ClickHouse:Часто секции подгружаются частями, если вы запустите бинарный файл и посмотрите на все mmap'ed секции, то скорее всего увидите что-то такое:
$ cat /proc/$PID/maps
address perms offset dev inode pathname
00200000-0acdf000 r--p 00000000 fe:01 1837801 clickhouse
0acdf000-1b200000 r-xp 0aade000 fe:01 1837801 clickhouse
1b200000-1b205000 r--p 1affe000 fe:01 1837801 clickhouse
1b205000-1b3b3000 rw-p 1b002000 fe:01 1837801 clickhouse
...
При старте бинарный файл грузит аж 4 отдельные секции. Это так же можно видеть в выводе readelf
$ readelf -l clickhouse
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
LOAD 0x0000000000000000 0x0000000000200000 0x0000000000200000
0x000000000aade1f4 0x000000000aade1f4 R 0x1000
LOAD 0x000000000aade200 0x000000000acdf200 0x000000000acdf200
0x00000000105201b0 0x00000000105201b0 R E 0x1000
LOAD 0x000000001affe3c0 0x000000001b2003c0 0x000000001b2003c0
0x0000000000004790 0x00000000000047d0 RW 0x1000
LOAD 0x000000001b002bc0 0x000000001b205bc0 0x000000001b205bc0
0x00000000001acaa8 0x00000000005e3f48 RW 0x1000
Почему их четыре? Well...Две, наверное, понятно — мы хотим разграничить память по чтению и записи, скажем, .text (код), .rodata (read only data, константы и тд) не должны быть записываемы, а .data, .bss должны. Так делает линкер GOLD. Cм вывод.
Линкер lld (LLVM) делает 3 LOAD секции. Из-за проблем безопасности разграничивает .rodata и .text. ld (GNU) делает 4 секции, разделяя .data от .got (Global Offset Table).
Если коротко, то сделано для безопасности, но в угоду немного перформансу. В реальности вряд ли этот перформанс можно заметить или найти.
Если всё сопоставить, то получится, что .text (код) находится у ClickHouse в
0acdf000-1b200000 r-xp 0aade000 .text .init .fini malloc_hook .plt
В итоге при старте можно грузить (0x1b200000-0x0acdf000)/2**20=261 мегабайт, когда как сам ClickHouse весит 1926 мегабайт. Быстрые старты и никаких проблем с pagefaults у кода. В целом это ClickHouse и делает через обычный mlock (два).