Главной задачей, которую мы ставили перед собой при работе над ONLYOFFICE Enterprise Edition, было повышение стабильности. Mono здорово выручил нас при разработке офиса под Linux (об этом мы уже писали немного), но в то же время причинил нам немало беспокойства. Именно с ним была связана такая проблема как падение http веб-серверов.
Ситуация, конечно, не самая приятная, поэтому мы решили подстраховаться и запускать не один сервер, а два. В обычном режиме они работают параллельно, а когда начинаются проблемы страхуют друг друга: один падает, другой, соответственно, берет на себя всю ответственность за происходящее. Но тут возникла проблема с синхронизацией кэшей серверов, для решения которой нам и понадобился Redis.
Далее мы немного расскажем о том, как мы начали работать с Redis'ом и что из этого вышло.
Начнем с начала: ONLYOFFICE работает под управлением http-сервера nginx и модуля fastcgi-mono-server4 для запуска .net приложений.
Для распределения нагрузки мы используем модуль nginx ngx_stream_upstream_module, который настраивается при помощи директив upstream и fastcgi_pass следующим образом:
То есть: одновременно запускаются два сервера fastcgi-mono-server4 на адресах 127.0.0.1:9000 и 127.0.0.1:9001 и nginx (спасибо ему!) начинает по кругу балансировать нагрузку.
В данном случае все входящие запросы распределяются поровну между двумя серверами: первый запрос отправляется на первый сервер, второй запрос на второй сервер, третий — снова на первый сервер и т.д.
Для ускорения работы используется кэширование. В кэше мы бережно храним наиболее часто используемые данные. К ним относятся, например, пользователи, группы, отношения принадлежности пользователей к группе, права доступа, списки порталов, подписки на оповещения, билинговая информация и квоты, настройки и еще много всего.
Как мы уже упоминали выше, nginx распределяет запросы по кругу на разные серверы. Это и тот факт, что в нашей бесплатной версии нет возможности приклеивать сессию к определенному ip-адресу (пользователю), приводило к многочисленными ошибкам из-за рассогласованности кэшей.
Например, пользователь решил начать всё с чистого листа и изменить своё имя на портале. Запрос на изменение пришёл на первый сервер, где новое имя нашего героя и сохранилось в базе данных. Информация о произошедшем событии также находится в кэше первого сервера. То есть, когда следующий запрос придёт на второй сервер, то выяснится, что он просто не в курсе произошедших перемен. Пользователь снова увидит свое старое имя и поймет, что новая жизнь так и не началась. А всё это из-за того, что информация в кэше второго сервера не была изменена. Обновление, конечно, произойдёт и второй сервер обо всем узнает, но только через несколько минут, после синхронизации кэша с данными из базы данных.
Собственно, для решения данной проблемы мы и решили воспользоваться проектом Redis, который выступил в роли общего кэша для наших серверов ONLYOFFICE. Тут, конечно, тоже возникли некоторые затруднения. Далее расскажем какие и как мы их преодолели.
Проблема с Nuget-пакет StackExchange.Redis
Что было не так: Выбранный нами для работы с Redis-ом Nuget-пакет StackExchange.Redis отказывался сотрудничать с Mono. Установив его, мы успешно запустили приложение для .net. Под Mono же возникала постоянная ошибка:
It was not possible to connect to the redis server(s); to create a disconnected multiplexer, disable AbortOnConnectFail. SocketFailure on PING
Как решили: тут всё просто. Собрали StackExchange.Redis.dll из исходников под Mono.
Увеличение времени отклика страницы
Что было не так: Страницы с большим числом обращений к кэшу (всё, что касается списков пользователей, групп и прав доступа), а также страницы, где кэшируемые данные занимают значительный объём (список всех порталов), стали менее оперативно откликаться на наши запросы. Всё-таки их данные хранятся в памяти другого процесса или вообще на другой физической машине, а доступ к ним осуществляется посредством сетевых сокетов, что значительно медленнее обращения к памяти своего процесса.
Как решили: На критичных к количеству обращений и размерам данных участках остались, как и раньше, локальные кэши, однако мы прикрутили туда небольшую систему оповещения.
Во все методы изменения кэшируемых по этому алгоритму данных мы добавили код, который посылал с помощью механизма publish/subscribe redis информацию об измененных данных. Далее, все сервера, которые получали оповещение, синхронизировали указанные объекты с базой данных.
Проблема с длительными операциями
Что было не так: Когда пользователь инициирует операцию с файлами, то создаётся задача, которая ставится в очередь. Затем любой освободившийся от предыдущей задачи поток начинает её выполнение. Количество потоков ограничено сверху для избежания чрезмерной загрузки сервера файловыми операциями (данный функционал был реализован на основе специализированной версии TaskScheduler).
Во время выполнения файловых задач у пользователя отображается прогресс выполнения, а по завершении задачи выводится информация об успехе или об ошибке. Это реализовано при помощи периодического опроса статуса файловых операций через API модуль документов, при этом вся информация о ходе операции хранилась в локальном кеше процесса. Но при запуске нескольких серверов ONLYOFFICE часть запросов о статусах операций попадали на сервер отличный от того, на котором была запущена текущая файловая операция. Это приводило к ошибкам отображения статусов операций у пользователя (задачи внезапно то появлялись, то исчезали).
Как решили: Мы создали менеджер распределенных задач. Он хранит состояния запущенных задач в Redis, ограничивает количество рабочих потоков, синхронизирует состояния задач между серверами и убирает совсем безнадежно зависшие.
Кстати, за основу мы взяли стандартные задачи System.Threading.Tasks из Microsoft.NET 4.0.
Проблема с провайдерами сессий
Что было не так: Мы также решили вынести сессии в Redis и выбрали для этого родной провайдер сессий для Redis от Microsoft RedisSessionStateProvider. Собственно, он используется и поддерживается Microsoft для платформы Azure, поэтому ставить его у себя на сервере можно, но исключительно на свой страх и риск. Как оказалось, он отлично работает под Windows. Но не с Mono: тут сразу возникли проблемы со стабильной работой, при небольшой нагрузке провайдер падал с NullReferenceException. Было решено попробовать сторонний провайдер. Но и здесь возникал тот же NullReferenceException.
Как решили: Мы начали разбираться во внутреннем устройстве провайдеров сессий и тем, как их обработка в ASP.NET под .NET отличается от Mono. Оказалось, что если запрос приходит без куков, то id сессии в версии для Mono будет равен null, а провайдеры не ожидают такого подвоха. Мы написали pull-request с добавлением проверки id сессии на null.
Из двух провайдеров сессий сторонний показался нам более быстрым и легковесным, поэтому мы и выбрали его. Для его подключения необходимо было заменить им стандартный провайдер в Web.config.
Кроме того, потребовалось добавить немного кода на старте приложения:
Здесь мы просто кратко скажем, что довольны проделанной работой и её результатами. Благодаря Redis'у нам удалось повысить и отказоустойчивость сервера, и его масштабируемость, что просто жизненно важно для корпоративной серверной версии, предполагающей большое (более того, постоянно растущее) количество пользователей.
В планах заменить Mono новой кроссплатформенной версией ASP.Net. Пока мы с ней присматриваемся друг к другу.
Ситуация, конечно, не самая приятная, поэтому мы решили подстраховаться и запускать не один сервер, а два. В обычном режиме они работают параллельно, а когда начинаются проблемы страхуют друг друга: один падает, другой, соответственно, берет на себя всю ответственность за происходящее. Но тут возникла проблема с синхронизацией кэшей серверов, для решения которой нам и понадобился Redis.
Далее мы немного расскажем о том, как мы начали работать с Redis'ом и что из этого вышло.
Как всё устроено
Начнем с начала: ONLYOFFICE работает под управлением http-сервера nginx и модуля fastcgi-mono-server4 для запуска .net приложений.
Для распределения нагрузки мы используем модуль nginx ngx_stream_upstream_module, который настраивается при помощи директив upstream и fastcgi_pass следующим образом:
upstream fastcgi_backend {
server 127.0.0.1:9000;
server 127.0.0.1:9001;
keepalive 64;
}
server {
listen 80;
location / {
...
fastcgi_pass fastcgi_backend;
...
То есть: одновременно запускаются два сервера fastcgi-mono-server4 на адресах 127.0.0.1:9000 и 127.0.0.1:9001 и nginx (спасибо ему!) начинает по кругу балансировать нагрузку.
В данном случае все входящие запросы распределяются поровну между двумя серверами: первый запрос отправляется на первый сервер, второй запрос на второй сервер, третий — снова на первый сервер и т.д.
Для ускорения работы используется кэширование. В кэше мы бережно храним наиболее часто используемые данные. К ним относятся, например, пользователи, группы, отношения принадлежности пользователей к группе, права доступа, списки порталов, подписки на оповещения, билинговая информация и квоты, настройки и еще много всего.
Теперь проблема
Как мы уже упоминали выше, nginx распределяет запросы по кругу на разные серверы. Это и тот факт, что в нашей бесплатной версии нет возможности приклеивать сессию к определенному ip-адресу (пользователю), приводило к многочисленными ошибкам из-за рассогласованности кэшей.
Например, пользователь решил начать всё с чистого листа и изменить своё имя на портале. Запрос на изменение пришёл на первый сервер, где новое имя нашего героя и сохранилось в базе данных. Информация о произошедшем событии также находится в кэше первого сервера. То есть, когда следующий запрос придёт на второй сервер, то выяснится, что он просто не в курсе произошедших перемен. Пользователь снова увидит свое старое имя и поймет, что новая жизнь так и не началась. А всё это из-за того, что информация в кэше второго сервера не была изменена. Обновление, конечно, произойдёт и второй сервер обо всем узнает, но только через несколько минут, после синхронизации кэша с данными из базы данных.
И вот он, Redis
Собственно, для решения данной проблемы мы и решили воспользоваться проектом Redis, который выступил в роли общего кэша для наших серверов ONLYOFFICE. Тут, конечно, тоже возникли некоторые затруднения. Далее расскажем какие и как мы их преодолели.
Основные трудности
Проблема с Nuget-пакет StackExchange.Redis
Что было не так: Выбранный нами для работы с Redis-ом Nuget-пакет StackExchange.Redis отказывался сотрудничать с Mono. Установив его, мы успешно запустили приложение для .net. Под Mono же возникала постоянная ошибка:
It was not possible to connect to the redis server(s); to create a disconnected multiplexer, disable AbortOnConnectFail. SocketFailure on PING
Как решили: тут всё просто. Собрали StackExchange.Redis.dll из исходников под Mono.
Увеличение времени отклика страницы
Что было не так: Страницы с большим числом обращений к кэшу (всё, что касается списков пользователей, групп и прав доступа), а также страницы, где кэшируемые данные занимают значительный объём (список всех порталов), стали менее оперативно откликаться на наши запросы. Всё-таки их данные хранятся в памяти другого процесса или вообще на другой физической машине, а доступ к ним осуществляется посредством сетевых сокетов, что значительно медленнее обращения к памяти своего процесса.
Как решили: На критичных к количеству обращений и размерам данных участках остались, как и раньше, локальные кэши, однако мы прикрутили туда небольшую систему оповещения.
Во все методы изменения кэшируемых по этому алгоритму данных мы добавили код, который посылал с помощью механизма publish/subscribe redis информацию об измененных данных. Далее, все сервера, которые получали оповещение, синхронизировали указанные объекты с базой данных.
Проблема с длительными операциями
Что было не так: Когда пользователь инициирует операцию с файлами, то создаётся задача, которая ставится в очередь. Затем любой освободившийся от предыдущей задачи поток начинает её выполнение. Количество потоков ограничено сверху для избежания чрезмерной загрузки сервера файловыми операциями (данный функционал был реализован на основе специализированной версии TaskScheduler).
Во время выполнения файловых задач у пользователя отображается прогресс выполнения, а по завершении задачи выводится информация об успехе или об ошибке. Это реализовано при помощи периодического опроса статуса файловых операций через API модуль документов, при этом вся информация о ходе операции хранилась в локальном кеше процесса. Но при запуске нескольких серверов ONLYOFFICE часть запросов о статусах операций попадали на сервер отличный от того, на котором была запущена текущая файловая операция. Это приводило к ошибкам отображения статусов операций у пользователя (задачи внезапно то появлялись, то исчезали).
Как решили: Мы создали менеджер распределенных задач. Он хранит состояния запущенных задач в Redis, ограничивает количество рабочих потоков, синхронизирует состояния задач между серверами и убирает совсем безнадежно зависшие.
Кстати, за основу мы взяли стандартные задачи System.Threading.Tasks из Microsoft.NET 4.0.
Проблема с провайдерами сессий
Что было не так: Мы также решили вынести сессии в Redis и выбрали для этого родной провайдер сессий для Redis от Microsoft RedisSessionStateProvider. Собственно, он используется и поддерживается Microsoft для платформы Azure, поэтому ставить его у себя на сервере можно, но исключительно на свой страх и риск. Как оказалось, он отлично работает под Windows. Но не с Mono: тут сразу возникли проблемы со стабильной работой, при небольшой нагрузке провайдер падал с NullReferenceException. Было решено попробовать сторонний провайдер. Но и здесь возникал тот же NullReferenceException.
Как решили: Мы начали разбираться во внутреннем устройстве провайдеров сессий и тем, как их обработка в ASP.NET под .NET отличается от Mono. Оказалось, что если запрос приходит без куков, то id сессии в версии для Mono будет равен null, а провайдеры не ожидают такого подвоха. Мы написали pull-request с добавлением проверки id сессии на null.
Из двух провайдеров сессий сторонний показался нам более быстрым и легковесным, поэтому мы и выбрали его. Для его подключения необходимо было заменить им стандартный провайдер в Web.config.
<sessionState mode="Custom" customProvider="RedisSessionStateProvider">
<providers>
<add name="RedisSessionStateProvider" type="RedisSessionProvider.RedisSessionStateStoreProvider, RedisSessionProvider" />
</providers>
</sessionState>
Кроме того, потребовалось добавить немного кода на старте приложения:
var configuration = RedisCachingSectionHandler.GetConfig();
RedisConnectionConfig.GetSERedisServerConfig = (HttpContextBase context) =>
{
if (configuration.RedisHosts != null && configuration.RedisHosts.Count > 0)
{
var host = configuration.RedisHosts[0];
return new KeyValuePair<string, ConfigurationOptions>("DefaultConnection",
ConfigurationOptions.Parse(String.Concat(host.Host, ":", host.CachePort)));
}
return new KeyValuePair<string, ConfigurationOptions>();
};
Итоги
Здесь мы просто кратко скажем, что довольны проделанной работой и её результатами. Благодаря Redis'у нам удалось повысить и отказоустойчивость сервера, и его масштабируемость, что просто жизненно важно для корпоративной серверной версии, предполагающей большое (более того, постоянно растущее) количество пользователей.
В планах заменить Mono новой кроссплатформенной версией ASP.Net. Пока мы с ней присматриваемся друг к другу.