Горячая замена кода

Относительно недавно почитывая RSS через доживающий свои дни Google Reader, о предстоящем закрытии которого не написал только самый ленивый IT-блоггер (к слову, любопытно насколько сильно просядет счетчик RSS-подписчиков Insight IT с текущих 16870, боюсь, что очень сильно...), я наткнулся на статью под заголовком "Горячее обновление кода не нужно?" и с выводом, что мол и правда особо не нужно, которая и подтолкнула меня поделиться своими мыслями на эту тему.

Для начала давайте разберемся в том, что же вообще такое "горячая замена кода" (hot code replacement или hot code swapping)? По сути это возможность обновить (или откатить) код работающей программы без её перезапуска и, как следствие, периода недоступности, потери состояния и повторной инициализации.

В каких ситуациях это может быть полезно? Ответ следует из моего импровизированного определения выше:

  • Когда простой (downtime) неприемлем
  • Когда есть какое-то состояние в памяти, которое не хочется терять
  • Когда инициализация процесса трудоемка и занимает много времени, что чаще всего связано с восстановлением  состояния с диска или других внешних источников

Ко многим клиент-серверным приложениям, в том числе и веб-сайтам, предъявляют очень высокие требования по отказоустойчивости, то есть простои как таковые не допустимы даже в экстренных случаях, не говоря уже о регулярном обновлении компонентов системы. Но чтобы обеспечить высокой уровень доступности, измеряемый количеством девяток после запятой в 99.(9)%, одной горячей замены кода не достаточно, нужно в любом случае обеспечить доступность всех данных и ключевых серверных компонентов системы даже в случае выхода из строя сервера, стойки, маршрутизатора и даже целого датацентра. Обычно это делается "на уровень выше" относительно самого кода приложения, путем добавления в систему как минимум резервных (активных или пассивных) копий всех компонентов и балансировщика нагрузки, способного обнаруживать неполадки и соответствующим образом перенаправлять поток запросов. Балансировщик нагрузки также нуждается в выделении под него как минимум двух серверов с переключением на уровне DNS. Возвращаясь к изначальной теме: если уж приложение способно пережить экстренный сбой любого компонента, то и без всякой горячей замены спокойно переживет его плановый перезапуск в связи с обновлением. Хотя на практике даже при резервировании всех компонентов небольшая доля запросов может быть потеряна или обработана за неприемлемо длинный срок в процессе перемаршрутизации их потока.

Казалось бы клиент-серверные приложения чаще всего не имеют состояния, в том плане, что все состояние находится в какой-то внешней сущности вроде СУБД, так что инициализировать особо нечего и состояние потерять не жалко. И на самом деле часто так и бывает, в том же мире PHP довольно популярна практика: положить новую версию кода в соседнюю папочку, поменять document root в конфиге nginx, попросить nginx перечитать свой конфиг - максимум сбросится кэш APC или xcache, что мало кого волнует, так как побочным эффектом будет просто несколько ответов на запросы медленнее обычного.

А как быть с самой СУБД? Например, Redis при запуске зачитывает в память все данные прежде чем начать принимать запросы, что может занимать сколько-то минут. Другие СУБД, которые могут отвечать на запросы и по данным на диске, стартуют относительно быстро, но провал в их производительности до того, как разогреется встроенный в них кэш, заметен невооруженным глазом. Очень похожа ситуация и с брокерами сообщений вроде RabbitMQ: если они и хранят данные на диске, то скорее как резервную копию. А memcached, Redis без персистентности и другие хранилища данных в памяти вовсе могут разогреваться после перезапуска неопределенно долго, так как наполняются по мере поступления запросов на запись.

В языках программирования, основанных на байт-коде и виртуальных машинах, зачастую есть возможность осуществлять горячее обновление кода без перезапуска самой виртуальной машины. Когда говорят про Erlang, очень часто упоминают эту возможность как одно из ключевых преимуществ платформы, так как она хорошо сочетается с её системой легковесных не зависящих от ОС процессов и встроенного в виртуальную машину эффективного хранилища данных в памяти (ETS). В мире Java это тоже возможно, но используется намного реже и позиционируется скорее как инструмент для отладки. В момент подмены кода в работающей виртуальной машине языка программирования с точки зрения ОС ничего не меняется: все открытые соединения, файловые дескрипторы и выделенная оперативная память остаются за ней, то есть она выступает своего рода посредником между одновременно загруженными двумя версиями кода, одна из которых будет выгружена из памяти, как только закончит свою работу и передаст все "дела" новой. Хотя это очень поверхностное описание, на практике всплывает масса нюансов вроде миграции состояний процессов в обе стороны, совместимости протоколов взаимодействия и пр. Плановый перезапуск в данной схеме нужен лишь для обновления самой виртуальной машины, что обычно требуется намного реже, чем обновление кода приложения.

Возможность горячей замены кода на уровне виртуальной машины, казалось бы, выглядит очень привлекательно для разработки обсуждавшихся выше инфраструктурных компонентов, от балансировщика нагрузки до различных хранилищ данных, что позволило бы не терять регулярно в производительности при обновлениях из-за сброса кэша и переустановки сетевых соединений. Но в реальности таких продуктов можно пересчитать по пальцам, почему? Все просто: удобство требует жертв - избегая периодических провалов в производительности из-за минимизации перезапусков процессов при обновлении, приходится проигрывать в производительности постоянно по сравнению с нативным кодом. Для инфраструктуры обычно оказывается приоритетнее постоянная производительность, что практически по-умолчанию означает разработку на одном из диалектов C/C++, где подобная схема с подменой кода на лету хоть и возможна, но сложна в реализации ввиду отсутствия популярных стабильных решений.

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

29 апреля 2013 |  Иван Блинков  |  Теория