Страницы

пятница, 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 всё. В следующий раз расскажу, как я ошибся с первой реализаций, и почему это была ошибка.