Страницы

пятница, 12 апреля 2013 г.

NLog. Memory Leak.

Вчера вечером один из наших сервисов перешёл в странное состояние. Занял всю свободную память (около 5 Гб), но еще продолжал работать. При этом обычное потребление  составляло 200-300 Мб. В общем, проблему решили традиционным для утечек памяти способом – сделали дамп памяти через Process Explorer, и перезапустили сервис.

Анализ дампа через WinDebug показал огромное количество объектов типа LogEventInfo, и сопутствующих ему объектов. Если коротко – то во всём виноват NLog. Почему такое произошло было непонятно, но решили посмотреть как будет развиваться ситуация. Весь вечер ничего подозрительного не происходило. Однако утром сервис скушал уже около 1Гб памяти, и потихоньку продолжал набивать себя байтами. Надо было что-то срочно менять.

Для начала я просмотрел все файлы логов, и обнаружил - один из них отстаёт от жизни на 8 минут. Остальные шли строго по графику. Нагрузки на диск тоже не наблюдалось. Тогда исследовал конфигурацию проблемного логгера:

На таких конфигурациях работало 7 target'ов. Но сломался только один, нагрузка на который была особенно интенсивной (8-10 ГБ в сутки). Изучение документации на AsyncWrapper показало наличие интересного параметра batchSize:

batchSize - Number of log events that should be processed in a batch by the lazy writer thread. Integer Default: 100

Т.е. в нашем случае AsyncWrapper сбрасывал не более 100 событий за один раз. При этом параметр timeToSleepBetweenBatches равен 500 мс (значение по-умолчанию 50 мс приводило к очень серьёзной загрузке ЦП, поэтому мы брали больший интервал). Итого NLog записывал на диск не более 200 записей в секунду. Очевидно, надо было попробовать увеличить количество записей. Кроме того, проконсультировавшись с документаций на FileTarget, увеличил размер буфера записи. В итоге получилась такая конфигурация:

Через полминуты сервис вернулся к своему обычному состоянию. Мораль - внимательно изучайте возможности своих инструментов!

среда, 10 апреля 2013 г.

Codestellation DarkFlow. Неправильный путь.

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

image

В теории выглядело неплохо, однако на практике привело к ряду трудностей:

  • Интеграция с DI-контейнером требовала интенсивной чёрной магии, чтобы клиенту желаемый экземпляр IExecutor. Игры с точками расширения контейнера позволяли это сделать, но было выглядело туманно и ненадёжно.
  • Само по себе неявная зависимость способа исполнения задачи от того, кто поместил ее в очередь, а не от самой задачи выглядит противоестественно. Управлять этим процессом правильнее, отталкиваясь от задачи.
  • Нет контроля над общим уровнем многопоточности, т.к. каждый IExecutor не общается с другим, и количество одновременно запущенных потоков может достигать суммы ограничений потоков для всех исполнителей.
  • Конфигурировать приходилось каждый исполнитель в отдельности, и Dispose’ить тоже. Что добавляло излишней возни с кодом.

Под тяжестью проблем пришлось практически полностью переписать реализацию. Что из этого получилось расскажу в следующий раз.

воскресенье, 7 апреля 2013 г.

Codestellation DarkFlow. API.

Как я и говорил в прошлый раз, ключ к читаемому и поддерживаемому коду – отделение кода многопоточности от кода приложения. Об этом писал еще Фаулер (Martin Fowler)  в своей библии PoEAA. Вынести многопоточность на уровень инфраструктуры очень заманчиво, и… вполне возможно! Но с помощью голого .NET этот узел не разрубить.

Приступая к проектированию какой-либо библиотеке, надо аккуратно продумать продумать вопрос API и абстракций, которые она предоставляет. И API, и абстракции должны быть чертовски просты. В ином случае остаётся слишком много возможностей для клиентского кода, что увеличивает сложность.

Мне очень нравятся эти интерфейсы. Они просты, и разделяются две очень важные вещи: действие от момента его исполнения. Просим выполнить задачу, и всё. Когда она выполнится – решит реализация IExecutor. Стоит отметить, что для успешного применения код приложения должен быть написан в стиле, который исключает предположения о моменте и порядке выполнения задач. В противном случае получается транзитивная связность между клиентским и инфраструктурным кодом. Такой код тяжело понимать, отлаживать и тестировать.

Задача простоты тестирования для таких абстракций упрощается – необходимости тестировать многопоточный код нет. Он протестирован на уровне реализации IExecutor. А любую реализацию ITask легко тестировать, вызывая Execute в синхронном режиме.

Собственно, про API всё. В следующий раз расскажу, как я ошибся с первой реализаций, и почему это была ошибка.

суббота, 30 марта 2013 г.

Codestellation DarkFlow. Укрощение многопоточности.

Последние два года я глубоко вовлечён в процесс написания многопоточного серверного кода. Это были интересные сложные задачи, которые не всегда удавалось решить удачно.  Свои и чужие ошибки позволяют сделать выводы о проблемах, и способах их решения.

Проблемы

Доставшийся мне код страдал от двух главных проблем:

Поддержка кода. Любой плохо написанный код тяжело поддерживать. Зависимости между объектами, цикломатическая сложность, и т.д. бывает непростой для понимания задачей само по себе. Многопоточность добавляет  новое измерение, усложняя код многократно. Если попытаться систематизировать проблемы с поддержкой, получится примерно такой набор:

  • Неструктурированная синхронизация – возникающие в неожиданных местах примитивы синхронизации, пытающиеся связать множество претендующих на ресурс потоков. Часто это выражается в попытки сделать блокировки более глобальными. Что с чем синхронизируется при этом понять довольно сложно.
  • Асинхронные вызовы через APM/TPL. Модель APM перегружена инфраструктурными примитивами, что сильно усложняет его понимание (кто не верит – пусть попробует написать собственный IAsyncResult, и для окончательного счастья сделать вложенные вызовы APM). Смешение полезного кода с кодом APM делало код практически не читаемым. Использование TPL немного смягчало боль, но в целом не решало проблемы.
  • Сложности с тестированием. Написание тестов на многопоточный бизнес код совсем нетривиальная задача. Кроме сложности в понимании кода добавляются “радости” внезапных ошибок, пришедших из потока, запущенного совсем другим тестом (особый камень в огород таймеров).

Производительность. В большей части случаем по требуемой производительности сервисы работали неплохо, но иногда неприятности всё-таки случались. Краткий обзор проблем:

  • Каждый поток требует 1 Мб памяти для стека. Пока потоков мало – можно не беспокоится. Но когда в пиковых нагрузках количество потоков подскакивает до 1000 – это проблема.
  • Множество заблокированных потоков создают лишнюю нагрузку на планировщик потоков, увеличивают количество требуемых переключений контекста. Всё это создаёт бесполезную нагрузку на процессор.
  • Примитивы синхронизации Windows требуют перехода в режим ядра, даже если при этом не происходит ожидания и блокировки потока. Такие вызовы обходятся очень дорого. Кроме того, создание объектов ядра весьма дорого так же дорого. CLR например использует пулы объектов ядра для работы Monitor’а именно по этой причине.
  • Сборщик мусора останавливает (Suspend) все потоки, и сканирует их стеки в поисках более не используемых объектов. Надо ли говорить, что производительности это не добавляет? 

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

Решение

После долгих размышлений в голове прояснились контуры решения.

  • Избегать (в крайнем случае  минимизировать) вторжение асинхронного кода в код приложения. Для случая асинхронного выполнения синхронной операции этот пункт мог бы решить даже ThreadPool.QueueUserWorkItem, но при этом возникает проблемы обработки исключений и контроля уровня многопоточности. Для вызовов в стиле APM задача еще более усложняется, и окончательного решения у меня пока нет.
  • Избегать конкурентного доступа к ресурсам. Идею как этого достичь почерпнул из node.js. Чтобы избегать синхронизации – не надо использовать конкурентный доступ к ресурсам. Это не означает использовать только один поток вообще, но один поток одновременно.
  • Не тратить понапрасну машинные ресурсы -  не создавать лишние потоки (ThreadPool будет к месту, но использовать надо с хитростями), минимизировать использование примитивов синхронизации ядра, не блокировать потоки. Такая цель требует изменения подхода к проектированию, но вполне достигаема.

Эти идеи (частично) воплощены в проекте Codestellation DarkFlow. Пока он страдает отсутствием документации, но над этим недостатком я собираюсь в ближайшее время поработать. О его внутренностях и механизмах расскажу в следующей раз.

PS. Прощу прощения, но видимо предыдущая серия блогов про CardFlow останется незаконченной. Как всегда, времени на всё не хватает. Да и с тех пор успел изрядно подзабыть начатое.