Страницы

вторник, 1 апреля 2014 г.

Вакансия .NET разработчика в моей командe.

 

Наши продукты развиваются, что потихоньку приводит к необходимости расширения команды.

В ассортименте сложные и интересные задачи, и амбициозные планы по развитию продуктов.

Откликайтесь на вакансию. Ищем двух человек, добровольная акция “приведи друга” приветствуется. Smile

пятница, 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 останется незаконченной. Как всегда, времени на всё не хватает. Да и с тех пор успел изрядно подзабыть начатое.