Как получить контроль над кучей 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 вы рекомендуете при использовании больших кучей с очень стабильными данными?
Ответы
Ответ 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...