Горизонтальное масштабирование небольших Web-приложений на Java (вопросы собеседований) tutorial
Эта тема была поднята в ходе нескольких (3+) собеседований который я прошёл за последние полтора месяца — в разных вариациях но примерно об одном. Казалось бы, известные вещи — но собрав все ответы и объяснения какие я давал (и кое-что что нашёл позже в гугле), решил сохранить их не у себя в гугл-драйве, а написать краткий обзор.Речь шла о небольших и типовых приложениях Enterprise / Web на Java, каких пишется множество (ну такие, на 10-100 тысяч клиентов, миллион посещений и т.п.). Пусть это будет обобщённый диалог в виде вопросов и ответов.
В: Допустим, у вас есть приложение (самое обычное — JSP, Spring, Hibernate например) развернутое на томкате (Apache Tomcat) и вы однажды замечаете что сервер с томкатом загружен на 80% в среднем. Что делать?
О: Поставим несколько томкатов на отдельных серверах в паралель. Они будут по-прежнему использовать одну базу на одном сервере.
В: Но как же пользователь будет заходить на ваши несколько серверов?
О: Используем load-balancer, например перед томкатами может стоять апач (Apache httpd) с mod_proxy — он будет распределять (проксировать) запросы приходящие к нему между всеми нашими томкатами.
В: Но ведь может получиться что пользователь залогинится на одном томкате, а следующий запрос load-balancer пошлёт на другой, где пользователь не залогинен!
О: Это мы говорим о том как организовать сессию. Например, делаем sticky sessions (например когда load-balancer добавляет к запросу куку с указанием на какой томкат он этот запрос проксирует — а все последующие запросы с этой кукой направляет обязательно на тот же самый сервер. Таким образом каждый отдельный пользователь будет работать только с одним сервером.
В: А если этот конкретный сервер упадёт?
О: Сессия пользователя потеряется. Поэтому лучше использовать хранение сессий в кэше. Томкат «из коробки» умеет хранить их в memcached например. То есть мы дописываем строчку в конфиг и на отдельном сервере запускаем memcached — теперь все томкаты хранят сессии на нём и если пользователь попал с очередным запросом на другой сервер, он этого не заметит — сессия будет работать все равно.
В: Какие ещё преимущества у кэша для сессий?
О: Например, можно деплоить новую версию приложения только на один из нескольких томкатов, так что скажем 25% пользователей видят новую страницу входа и успеют высказать нам пожелания если им это не нравится, т.е. они невольно поработают бета-тестерами :)
В: Но если версии приложения по-разному используют базу?
О: Мы можем проектировать изменения базы так чтобы поддерживать обратную совместимость между двумя соседними версиями. Это нетрудно. Добавлять колонки, например, нужно вместе с новой версией, а вот удалять ненужные только при следующем релизе.
В: Хорошо, теперь у нас узким местом становится база. Что мы будем делать при возрастании нагрузки на неё?
О: В первую очередь между базой и томкатами полезно сделать кэш. Ещё раньше вероятно мы используем кэш на уровне ORM (например, второй уровень кеша в Hibernate). Общий смысл в том что в течение сессии пользователь использует ограниченный набор данных, поэтому их удобно кэшировать.
В: Ну а всё-таки, допустим даже кэш нас не спасает. Как можно уменьшить нагрузку на базу?
О: У нас есть несколько путей. Например можно часть базы (какую-нибудь особо насосную таблицу допустим) выделить в другую базу на отдельном сервере, может даже в NoSQL хранилище или какой-нибудь специальный кэш. Конечно, лучше это разделение сделать ещё при проектировании :)
В: А какие есть другие пути? Какие решения на уровне самой базы данных?
О: Можно использовать шардинг — при этом таблицы разбиваются на несколько серверов и обращение к нужному происходит, например, по части id-шника. В некоторых случаях можно разделить сразу, допустим, сделки, транзакции, электронные документы и т.п. касающиеся одного пользователя поскольку обычно пользователь не работает с чужими документами — а значит все его данные можно удобно хранить на одном сервере.
В: Какой недостаток этого подхода?
О: С такими таблицами впоследствии будет сложнее работать — join с таблицей лежащей на нескольких серверах очевидно будет менее эффективен — вообще усложняется индексирование, запросы по критериям и т.п. Вообще само проектирование ощутимо усложняется.
В: Хорошо, знаете ли вы ещё варианты?
О: Самый простой это настроить репликацию, например так что база содержит копии на нескольких серверах, из них один используется для записи, а остальные для чтения. Эти последние быстро синхронизируют своё содержимое при апдейтах. Получается что общее количетсво запросов к базе теперь распределяется по нескольким машинам. Конечно это полезно именно когда чтения больше чем записи.
В: Какие дальнейшие пути масштабирования вы могли бы предложить?
О: Например, очереди сообщений. Скажем, пользователь сохраняет новую транзакцию — но мы не пишем её в базу сами. Вместо этого отсылаем сообщение в очередь (скажем RabbitMQ) что такие-то данные должны быть сохранены. Это сообщение будет выдано одному из нескольких серверов осуществляющих обработку и сохранение в базу. Наращивать количество таких серверов (при использовании распределённой / реплицированной базы или кеша) вообще очень легко. Однако сама по себе архитектура на таком уровне уже требует больше внимания и размышлений — возможно даже это тот момент когда приложение стоит переписать целиком :)
В: Ладно, с этим ясно, давайте поговорим о другом… (и тут могут начать про гарбаж-коллекторы, или попросить написать двоичный поиск в массиве — проверка на вшивость — но это уже не важно)
Поделившись своими «наблюдениями» по собеседованиям я буду рад конечно дополнениям, исправлениям и т.п. которые могут оказаться полезными и мне и другим коллегам :)
Я не знаю Java & Tomcat, по этому мои советы могут показаться глупыми…
1) роль load_balanser хорошо выполняет nginx, он более шустрый, чем аппач и меньше занимает памяти
2) чтоб попадать на одну и ту же WEB морду, достаточно использовать модуль ip-hash
3) храниить сессии можно не только в мемкеше, но и в редисе например. Плюс редиса, например, в том, что если случайно упадет сервер, а ни кто от этого не застрахован, то сессии сохрранятся.
4) я бы на собеседовании задал еще такой вопрос — а что делать — если для сессий не хватает места в мемкеше?
5) про шардинг лучше рассказывать после репликации. В большинстве случаев репликации хватает.
6) рассказывая про шардинг, необходимо пояснить его недостатки.
7) рассказывая про масштабирование необходимо два слова сказать про архитектуру SOA. Тем более, что SOA родилась в недрах JAVA
8) ну, и если все же позиция промежуточноая между сеньёр-помидор и сустем-архитект, то тут как-то вообще куцо с ответами. Если требуется масштабирование, то исходя из поставленных условий сервер не справляется с нагрузкой. Что для этого нужно:
— сделать быстрый фронтэнд сервер с минимальной логикой
— если есть какие-то значительные задержки бэкенда — то можно для опроса использовать аякс, а в это время пользователя развлекать тулбаром (ваша задача решается… осталось 75%)
— часть задач фронтэнда (WEB-морда) должна передать внутренним сервисам (это ближе к SOA).
— как вариант, передачи задач можно использовать специализированные средства: сервера очередей (ActiveMQ, RabbitMR, redis etc) или задач ( zookeeper, Gearman )
— для опроса готовности можно использовать например memcached, не тягая тяжелый Tomcat, а брать информацию напрямую через nginx используя ngx_memcached, я чтоб защитьть прямой доступ к мемкешед от плохих парней — использовать ngx_acces_key
иногда, применяя парочку хорошо-зарекомендовавших себя трюков можно вообще избежать проблемы масштабирования.
Можно еще отделять крупные компоненты в отдельные приложения с простым API (например, REST) и проксировать запросы к ним через балансировщик. Тогда при увеличении нагрузки можно быстро добавить именно те компоненты, которые тормозят.
Комментариев нет:
Отправить комментарий