Год с Erlang, продолжение

1 сентября 2013

Прошло 2 месяца после первой статьи, где я рассказывал про опыт использования Erlang в игровом проекте. И за эти 2 месяца много воды утекло, и кое-что изменилось. И поэтому нужно продолжение :)

Еще раз про тестирование

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

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

Нет, я не стал выдумывать хитрые моки, чтобы покрыть все юнит-тестами. И не стал сочинять сложный тестовый стенд, прогоняющий внутри себя игры и эмулирующий реальных пользователей. Сделал проще: Консольное Mac OS приложение запускает N десятков (сотен) тестовых клиентов, каждый из которых дергает все возможные методы серверного АПИ. Клиенты сгруппированы по функциям. Часть из них дергает регистрацию и авторизацию, часть АПИ игры, часть АПИ покупок и т.д. Ответы сервера собираются, но не анализируются автоматически. Еще ведется учет дисконнектов (они, обычно, вызваны ошибками на сервере).

Далее этих клиентов можно запускать в двух режимах. Можно запустить небольшое количество и не на долго, на 3-5 минут. Потом вручную просмотреть логи на клиентах и на сервере, нет ли в них ошибок или необычных данных. А можно запустить несколько сотен клиентов на 10-15 минут. Получится режим стресс-теста с нагрузкой на сервер порядка 5-8 тысяч запросов в секунду. Тут уж все логи вручную просмотреть не получится, но ошибки на сервере, если они будут, незамеченными не останутся.

С помощью этих тестов были найдены узкие места, код был оптимизирован по производительности и отрефакторен. Кое-где переделана архитектура. После рефакторинга появились ошибки типа race condition, когда из разных потоков одновременно модифицировалось состояние игры. Опять переделана архитектура, чтобы устранить такие ошибки. И, в итоге, получился стабильный сервер без багов. С тех пор я больше не вижу ошибок в логах :)

Обновление сервера по-взрослому

С легкомысленными обновлениями сервера, когда каждая новая фича тут же выкатавалась в продакшен с помощью горячего обновления, тоже решено было покончить. Подняли тестовый сервер с внешним IP (чтобы можно было коннектится из дома). Я разработал и задокументировал процедуру обновления, типа, сперва прогоняются тесты, потом обновляется тестовый сервер, запускаются тесты на нем, играем с разных девайсов, и т.д.

Частые обновления и нестабильность принял на себя тестовый сервер. А продакшн сервер стал обновляться после каждого спринта -- раз в 2 недели. Однако горячие обновления стали невозможны, требовалась перегрузка. Но в любой момент времени на сервере идут игры. Прежний легкомысленный подход -- оборвать все игры и вернуть ставки, больше не подходил. Пришлось решать проблему восстановления состояний игр.

Впрочем, решилось это относительно просто. Каждая игра -- это gen_server процесс, и его состояние укладывается в некий довольно сложный record. Каким бы сложным ни был этот рекорд, он легко сериализуется в бинарник с помощью term_to_binary и десериализуется обратной функцией. Бинарник же можно положить в БД перед остановкой сервера. Так что процесс получился такой:

Со стороны клиента это выглядит так, что игра замирает на 5-10-15 секунд, потом продолжается дальше.

Еще раз про горячее обновление кода

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

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

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

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

comments powered by Disqus