Как получить контроль над кучей 5 ГБ в Haskell? - программирование
Подтвердить что ты не робот

Как получить контроль над кучей 5 ГБ в Haskell?

В настоящее время я экспериментирую с небольшим веб-сервером Haskell, написанным в Snap, который загружает и предоставляет клиенту множество данных. И мне очень трудно получить контроль над процессом сервера. В случайные моменты процесс использует много CPU в течение нескольких секунд до минут и становится невосприимчивым к клиентским запросам. Иногда использование памяти скапливается (а иногда и падает) сотнями мегабайт в течение нескольких секунд.

Надеюсь, у кого-то есть больше опыта с длительными процессами Haskell, которые используют много памяти и могут дать мне несколько указателей, чтобы сделать эту вещь более стабильной. Я отлаживал эту вещь в течение нескольких дней, и я начинаю немного отчаяться здесь.

Небольшой обзор моей установки:

  • При запуске сервера я прочитал около 5 гигабайт данных в большую (вложенную) структуру Data.Map в памяти. Вложенная карта - это значение строгое, и все значения внутри карты относятся к типам данных, причем все их поля также строги. Я потратил много времени на то, чтобы не избежать неоплаченных громов. Импорт (в зависимости от загрузки системы) занимает около 5-30 минут. Странная вещь - флуктуация последовательных прогонов намного больше, чем я ожидал бы, но это другая проблема.

  • Большая структура данных живет внутри "TVar", которая разделяется всеми потоками клиентов, порожденными сервером Snap. Клиенты могут запрашивать произвольные части данных с использованием небольшого языка запросов. Объем запроса данных обычно мал (до 300 кбайт или около того) и касается только небольшой части структуры данных. Все запросы только для чтения выполняются с использованием "readTVarIO", поэтому они не требуют каких-либо транзакций STM.

  • Сервер запускается со следующими флагами: + RTS -N -I0 -qg -qb. Это запустит сервер в многопоточном режиме, отключит время простоя и параллельный GC. Это, похоже, ускоряет процесс.

Сервер работает без проблем. Тем не менее, время от времени запрос клиента истекает, и процессор достигает 100% (или даже более 100%) и продолжает делать это в течение длительного времени. Тем временем сервер больше не отвечает на запрос.

Есть несколько причин, по которым я могу думать, что может привести к использованию ЦП:

  • Запрос требует много времени, потому что предстоит много работы. Это несколько маловероятно, потому что иногда это случается для запросов, которые оказались очень быстрыми в предыдущих запусках (с быстрым я имею в виду 20-80 мс или около того).

  • Есть все еще некоторые неоцененные thunks, которые необходимо вычислить до того, как данные могут быть обработаны и отправлены клиенту. Это также маловероятно по той же причине, что и предыдущая точка.

  • Как-то сборка мусора запускается и начинает сканирование всей моей кучи на 5 ГБ. Я могу себе представить, что это может занять много времени.

Проблема в том, что я не знаю, как правильно понять, что происходит и что с этим делать. Поскольку процесс импорта занимает столь долгое время, результаты профилирования не показывают мне ничего полезного. Кажется, что нет никакого способа условно включить и выключить профайлер из кода.

Я лично подозреваю, что проблема GC здесь. Я использую GHC7, который, похоже, имеет множество возможностей для настройки работы GC.

Какие настройки GC вы рекомендуете при использовании больших кучей с очень стабильными данными?

4b9b3361

Ответ 1

Большое использование памяти и случайные всплески CPU почти наверняка будут использоваться в GC. Вы можете убедиться, что это действительно так, используя параметры RTS, такие как -B, что приводит к сбою GHC всякий раз, когда имеется большая коллекция, -t, который скажет вам статистику после факта (в частности, посмотрите, действительно ли время GC очень длинное) или -Dg, который включает информацию об отладке для вызовов GC (хотя вам нужно скомпилировать с помощью -debug).

Есть несколько вещей, которые вы можете сделать, чтобы облегчить эту проблему:

  • При первоначальном импорте данных GHC тратит много времени на рост кучи. Вы можете сказать, чтобы он захватил всю необходимую вам память сразу, указав большой -H.

  • Большая куча со стабильными данными будет повышена до старого поколения. Если вы увеличите количество поколений с помощью -G, вы сможете получить стабильные данные в самом старом, очень редко генерации GC'd, тогда как у вас есть более традиционные молодые и старые кучи над ним.

  • В зависимости от использования памяти в остальной части приложения вы можете использовать -F, чтобы настроить, насколько GHC позволит старому поколению расти, прежде чем собирать его снова. Вы можете настроить этот параметр, чтобы сделать этот мусор ненужным.

  • Если нет записи, и у вас есть четко определенный интерфейс, может быть целесообразно сделать эту память не управляемой GHC (используйте C FFI), чтобы не было возможности получить супер-GC когда-либо.

Это все предположения, поэтому, пожалуйста, протестируйте свое приложение.

Ответ 2

У меня была очень похожая проблема с кучей 1,5 ГБ вложенных Карт. При включенном холостом GC по умолчанию я получал бы 3-4 секунды замораживания на каждом GC, а при выключенном холостом GC (+ RTS -I0) я бы получил 17 секунд замораживания после нескольких сотен запросов, в результате чего время клиента отъезда.

Мое "решение" было первым, кто увеличил время ожидания клиента и попросил, чтобы люди терпели это, в то время как 98% запросов составляли около 500 мс, около 2% запросов были бы медленными. Однако, желая лучшего решения, я закончил работу с двумя серверами с балансировкой нагрузки и отключил их от кластера для выполненияGC каждые 200 запросов, а затем вернулся в действие.

Добавляя оскорбление к травме, это была переработка оригинальной программы Python, у которой никогда не было таких проблем. Справедливости ради, мы добились повышения производительности на 40%, мертвой-простой распараллеливания и более стабильной кодовой базы. Но эта надоедливая проблема GC...