понедельник, 26 сентября 2011 г.

[prog] Вспомнился случай, когда хотелось иметь горячую замену кода

Во время чтения вот этого обсуждения, в котором зашел разговор о применимости горячей замены кода (одна из важнейших фич языка Erlang), вспомнил ситуацию, когда такая замена не помешала бы мне в C++.

Есть в нашем SMS-шлюзе специальный компонент под названием send.bufferizator. Его задачей является сглаживание резких пиков в количестве исходящих сообщений. Например, происходит какое-то событие и генерируется пачка сообщений, объем которой многократно превышает разрешенную пропускную способность. Скажем, клиенту разрешено 100 sms/sec, а он однократно сгенерировал 500, а затем в течении 5-10 секунд больше ничего не отсылает. В такой ситуации send.bufferizator вбирает в себя всю пачку и отдает ее содержимое не превышая разрешенной скорости – т.е. эти 500 сообщений выйдут из send.bufferizator-а в течении 5 секунд по 100 в секунду.

Данные внутри send.bufferizator хранятся в ОП, не в БД. Хранение в долговременной памяти не нужно, т.к. наш протокол обмена сообщениями устроен так, что операция отсылки является безопасной (идемпотентной, если употребить умный термин) – до тех пор пока шлюз не пришлет подтверждение клиент может (и должен) повторять попытки отсылки. Поэтому, если send.bufferizator по какой-то причине перестает работать, то теряется содержимое его очередей. Но это не страшно, т.к. никаких подтверждений мы клиенту не отсылали и клиент вновь повторит их отправку. Тем не менее, при больших скоростях send.bufferizator может содержать у себя по несколько тысяч сообщений, поэтому прерывать его работу не очень хорошо.

Но это все преамбула. Теперь сама история. Однажды среди ночи меня разбудил телефонный звонок из нашей службы техподдержки с просьбой разобраться с проблемой, с которой раньше сталкиваться никому не приходилось. Было видно, что один из send.bufferizator-ов реагирует на внешние раздражители, но сообщения через себя не пропускает.

Проблема оказалась любопытная – при обработке одной из ситуаций я лишний раз декрементировал текущий показатель нагрузки и, поскольку это был unsigned int, значение переходило через ноль и получалось 0xffffffff. Из-за чего send.bufferizator считал, что нагрузка сейчас многократно превышена и новые сообщения должны приостанавливаться пока она не снизится. А снизится она, понятное дело, не могла.

Лекарство, как и во многих других случаях – “выйти и зайти”, т.е. рестартовать компонент. Однако, ошибка в коде оставалась, я ее быстро исправил. Но для обновления всех компонентов пришлось делать их рестарты, естественно, с потерей текущего содержимого ОП. И вот это был тот самый случай, когда горячая замена только кода работающего модуля, без потери его текущих данных, пришлась бы очень кстати. Т.к. тогда этот bugfix прошел бы вообще никем не замеченный.

Мораль сей басни такова. Если кто-то считает, что некая фича является бесполезной потому, что он не имеет перед глазами примеров ее применения, то это не значит, что фича действительно бесполезная. Скорее это просто отсутствие примеров здесь и сейчас. Но, в свою очередь, наличие примеров не делает фичу очень полезной и очень востребованной ;)

PS. Кстати, в той истории еще раз проявились законы Мерфи. Ошибка могла возникнуть в очень экзотической ситуации. А ситуация это сложилась поздно ночью.

PPS. Когда-то на глаза попадалась статья Practical Dynamic Software Updating for C в которой описывалась технология накатов бинарных обновлений на работающий C-шный код. Правда, не могу судить о том, насколько все это жизнеспособно и применимо. Так же не знаю и о дальнейшей судьбе данного исследования.

PPPS. Не могу отказать себе в удовольствии с сарказмом пройтись по цитате, которую thesz вырвал из доклада “Running a startup on Haskell”: Deploying our code is a simple matter of redirecting a symlink, then bouncing the server. Downtime during a deploy is a fraction of a second. Ну прям бином Ньютона ребята выдумали, никто кроме хаскелистов до такого додуматься не смог бы, поэтому этим нужно непременно хвастаться! Хотя такому приему уже сто лет в обед и его переизобретает, имхо, любой разработчик, которому приходится плотно сталкиваться с проблемой развертывания бинарников.

20 комментариев:

TheIvan комментирует...

Привет, Женя.

"PPS. Когда-то на глаза попадалась статья Practical Dynamic Software Updating for C в которой описывалась технология накатов бинарных обновлений на работающий C-шный код. Правда, не могу судить о том, насколько все это жизнеспособно и применимо. Так же не знаю и о дальнейшей судьбе данного исследования."

http://www.ksplice.com/

TheIvan комментирует...

Можешь позаимствовать что-нибудь. Код под GPL, хоть и сам сервис платный.

eao197 комментирует...

@buffovich:

Ваня, за ссылку спасибо. Будет время гляну на их статейку. Но судя по авторству, это совсем другие ребята.

имя комментирует...

это похоже существенно разные вещи

Ksplice's design was originally limited to patches that did not introduce semantic changes to data structures, тогда как Practical Dynamic Software Updating for C показывают создание паддинга в структурах, анализ на использование указателей к элементам структур; статья познавательная

ksplice по-моему более практичный подход, однако под виндой она вряд ли заработает -- она анализирует не исходник, а ELF

@Евгений Охотников

как насчет ограничиться патченьем именно кода?

еще один практический момент, который может обесценить ksplice в твоем случае: The system verifies that no processors were in the middle of executing functions that will be modified by the patch (хотя тут я уже не знаю, sleep подходит под это или нет?)

З.Ы. For patches that do introduce semantic changes to data structures, Ksplice requires a programmer to write a short amount of additional code to help apply the patch

eao197 комментирует...

@имя:

>как насчет ограничиться патченьем именно кода?

Так мне именно это и нужно было. Чтобы код поменялся, а все данные остались неизменными.

Анонимный комментирует...
Этот комментарий был удален автором.
Анонимный комментирует...

Женя, C++ мультипардигменный язык. На нем тоже так можно, заменять код. Например, известный веб сервер, написанный на ассемблере высокого уровня (который отличается от C++ тем, что там все нужно делать вручную, в плюсах хоть что-то поддерживается на уровне языка) умеет заменять выполнять "Обновление сервера на лету". Почитать можно тут http://sysoev.ru/nginx/docs/control.html

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

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

P.S. Эх, придумал бы кто-нибудь нормальный способ количественной оценки результата труда программиста. Тогда можно было бы организовать сдельную оплату. И сразу будет работа, результат и оплата отдельно, а бесполезные мечты и форумные разговоры с функциональщиками отдельно.
P.P.S. Самая большая трудность всех технарей не в инструментах разработки, а в неумении себя продать за нормальную цену. Сдельная оплата существенно упрощает это.

eao197 комментирует...

@Леша Сырников:

ngnix, как я понял, делает простую вещь -- стартует новые процессы, постепенно убивая старые. При этом не происходит замена старого кода на новый с сохранением старых данных.

Тогда как в Erlang-е именно это возможно. Например, там если в цикле вызывается функция f, то можно загрузить ее новую версию (тогда как старая будет продолжать работать) и при следующей итерации цикла будет вызвана уже новая f.

eao197 комментирует...

@Леша Сырников:

Сравнение языков между собой -- это вообще крайне спекулятивная тема. И не простая. Наверное, нужно какую-то заметочку написать на этот счет.

Анонимный комментирует...

> Сравнение языков между собой -- это вообще крайне спекулятивная тема.

Ой, не люблю я спекуляции. На мой взгляд, языки надо сравнивать исходя из потребностей и целей программиста, который на них пишет и задач проекта. Хочешь зарабатывать бабло - есть привязанные к одной платформе и произовдителю Java и C#, хочешь кроссплатформенное решение - C++. Нужен веб - есть php и RoR (первый более стабилен в API, второй позволяет проще писать крупные проекты) плюс полугиковский Django. Хочешь чего-то написать для себя - выбирай, что нравится.

Все спекуляции на тему сравнения языков в таком случае становятся "поводом поболтать". Нет смысла сравнивать D и C#, они в настоящее время для разных ниш, для разных целей, удовлетворяют разные потребности.

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

eao197 комментирует...

@Леша Сырников

>большинство программистов не отделяет личные предпочтения от возможностей языка (типа круче тот язык, который мне больше нравиться).

+100500

>Ну и вечная тема, у кого больше (а вот зачем собственные комплексы переводить на тему языков программирования - это я совсем не догоняю).

Тут-то все понятно -- что лучше знаешь, о том и говоришь.

имя комментирует...

> Так мне именно это и нужно было. Чтобы код поменялся, а все данные остались неизменными.

то тебе :-)

а вот лисперы емнип рассказывают о том, как все замечательно, если и данные можно менять (а я их в упор не понимаю), да и CLOS рассчитан на то, чтобы на ходу поменять класс (в терминологии с++) вместе живущими его экземплярами

имя комментирует...

к практике:

1. в списке литературы твоей пдф-ки есть интересный заголовок G. Hjalmtysson and R. Gray. Dynamic C++ classes, a lightweight mechanism to update code in a running program. In Proc. USENIX
ATC, 1998.

2. в юниксах есть возможность dump core ("дампить корку"), а затем, емнип, загрузить его и начать исполнение -- так например работал М4 (чтобы долго не грузить макросы, он дампил свое состояни после загрузки, а потом быстро загружал)

2А. тут можно попробовать пропатчить код и затем загрузить дамп

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

филосовски это вынос всех данных, что хоть как-то персистентны, в отдельный процесс, умеющий сериализовывать данные -- в типовом случае это РБД (и мне это часто нравится), но РБД м.б. неудобна, и в твоем случае это может быть отдельна прога, которая принимает заказы на отправку смс и хранит их в памяти (чтобы затем отдавать второй проге в бинарном виде)

первую прогу можно не учить сериализовывать заказы, а просто дампить корку при необходимости и запускать снова с дампом

тут, естественно, должна быть критика: в частности, смена ABI компилятора таки заставит тормознуть первую прогу, и т.д.

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

и наконец, совсем практически -- а в твоем случае не достаточно ли было прицепиться дебаггером и поменять 0хFFFFFFFF на 0х00000001 или что-то в этом роде, затем дождаться многократного уменьшения очереди, и потом спокойно перезапустить прогу?

имя комментирует...

3. опять же, в юниксе можно сигналом стопорнуть прогу, и хотя кодовые сегменты обычно закрыты на запись, но, думаю, имея рутовые права можно все это исправить, и затем сигналом запустить снова

это я к чему -- ksplice занимается в общем-то акробатикой, а в юзерспейсе полно разных инструментов или хотя бы заготовок для них

имя комментирует...

вот кстати пример решения твоей задачи с помощью sytstemtap в linux:

http://www.linux.org.ru/jump-message.jsp?msgid=3855459&cid=3855849

eao197 комментирует...

@имя:

>а вот лисперы емнип рассказывают о том, как все замечательно, если и данные можно менять (а я их в упор не понимаю), да и CLOS рассчитан на то, чтобы на ходу поменять класс (в терминологии с++) вместе живущими его экземплярами

Это возможно, например, и в Ruby. За счет этого некоторые фреймворки (RubyOnRails, скажем) функциональность стандартных классов расширяют. Иногда бывает удобно.

eao197 комментирует...

@имя:

>и наконец, совсем практически -- а в твоем случае не достаточно ли было прицепиться дебаггером и поменять 0хFFFFFFFF на 0х00000001 или что-то в этом роде, затем дождаться многократного уменьшения очереди, и потом спокойно перезапустить прогу?

Нет. На боевом сервере нет дебагера. И отладочной информации в исполнимом коде так же нет.

eao197 комментирует...

@имя:

Есть и еще один вариант замены кода. Основные действия выносятся в DLL (у нас так и есть, кстати говоря), но DLL-ки пишутся так, чтобы данные хранились в долгоживущем хипе и оставались в ОП даже после выгрузки DLL (это не сложно, важно, чтобы это были plain-данные, без ссылок на код деструкторов). Тогда DLL-ку можно выгрузить, заменить ее и загрузить вновь. Уже с новым кодом, который начнет работать со старыми данными.

Но все это велосипеды. По сравнению с тем, что есть в Erlang или Ruby. Хотя понятно, что если в C++ очень захочется _изначально_ иметь динамическую замену кода, то найти приемлимое решение можно.

имя комментирует...

> На боевом сервере нет дебагера. И отладочной информации в исполнимом коде так же нет.

а если бы были? мне кажется, это самый простой вариант (под дебаггером я понимаю естественно командно-строчный дебаггер, или аналоги)

вообще как может быть горячая замена функций без отладочной инфы или чего-то аналогичного, где записан адрес заменяемой функции?

eao197 комментирует...

@имя:

>а если бы были? мне кажется, это самый простой вариант (под дебаггером я понимаю естественно командно-строчный дебаггер, или аналоги)

Я бы не рискнул. Поскольку дело было в 3 часа ночи, удаленным доступом через dial-up. Ну в баню такой экстрим :)

>вообще как может быть горячая замена функций без отладочной инфы или чего-то аналогичного, где записан адрес заменяемой функции?

ХЗ. В Erlang/Ruby за это скоростью исполнения приходится расплачиваться. Не хотелось бы в C++ платить такую же цену.