Highload — это тема одновременно модная и достаточно заезженная, тем более, что нет четкого определения, что же такое «Highload». Для ясности, давайте назовем «Highload» сетевое приложение, которое должно обрабатывать 1000 запросов в секунду. А приложение, обрабатывающее 1 запрос в секунду, соответственно, «не Highload». Мой опыт показывает, что между первым и вторым есть существенная разница в архитектуре, подходах к разработке и проблемах. В этой статье я попытаюсь изложить эти отличия, как я их понимаю.
Итак…
С этим придется просто смириться. Очень большое количество кода не способно работать под большой нагрузкой — из-за чрезмерных требований к памяти, процессору, неэффективной синхронизации, устаревших механизмов ввода-вывода или плохой обработки ошибок при нехватке ресурсов. Даже если ваша любимая библиотека написана очень умными людьми и не имеет изъянов — она может не подойти просто потому, что при её проектировании эффективность принесли в жертву удобству или простоте использования. Поэтому — будьте готовы жить с самописками или искать те немногие решения, которые пройдут нагрузочные тесты под ваше конкретное приложение. Верить в этом деле нельзя никому.
Редкая высоконагруженная система обходится без кешей — особенно над СУБД. Правильное кеширование в распределенной системе — это большая и сложная тема, поэтому имеет смысл задуматься сразу о данных: кто и как будет их обновлять и запрашивать, а также где и как можно пожертвовать целостностью или актуальностью данных.
В целом — чем проще система, тем быстрее она работает. Нужно стремиться к максимальной простоте, часто в ущерб понятности, концептуальности или красоте архитектуры.
При выборе формата сериализации позаботьтесь заранее умножить средний размер пакета на кол-во пакетов в секунду и сравнить с пропускной способностью вашей сети. В этом плане Json лучше, чем XML, Protobuf лучше, чем JSON, а иногда приходится изобретать свой формат, с лучшей степенью упаковки.
Некоторые объекты в Java требуют дополнительную память для работы, поэтому не нужно рассчитывать, что если -Xmx выставлен, то приложение точно влезет на сервер. Например, каждый поток в Java требует от 256K до 2 мегабайт off-heap памяти для своей работы. При умножении на 1000 получается уже достаточно много, так что следите за кол-вом потоков, используемых вашим приложением.
Даже если строгих требований к latency нет, о сборке мусора нужно помнить. Старайтесь ограничивать кол-во аллокаций и, особенно, общий объем выделяемой памяти на каждый запрос. Все необходимые метрики есть в профайлере Java Mission Control.
Утверждение «приличное приложение всегда должно логгировать входные и выходные данные» — верно, но в высоконагруженном приложении легко станет узким местом вашей системы. Contention на логгере, недостаточная скорость жестого диска, гигабайты логов в час — это всё суровая реальность. Поэтому выбирать данные для логгирования нужно очень аккуратно и помнить про стектрейсы — они занимают много места и при большом кол-ве ошибок способны положить приложение. Часто приходится вообще не писать логи на диск, а слать их по сети в специализированную систему (и помнить, что сеть тоже не резиновая, ага).
Что делать, если приложению приходит больше запросов, чем оно способно прожевать? Если база внезапно стала отвечать в два раза медленней? Если в сети начались потери пакетов? Хорошее приложение должно не виснуть наглухо, а отвечать «завтра приходи». Мораль — для всех взаимодействий с внешними системами должны быть таймауты, приложение должно ограничивать кол-во параллельно обрабатываемых запросов и клиент должен знать, что делать, если приложение занято или не отвечает.
Еще одна сложная и обширная тема, но вкратце — если приложение наметво повесило машину, то где-то должны остаться графики потребления памяти, свопа, диска, цпу, потоков, системных дескрипторов.
Приложение легко может умереть под высокой нагрузкой, вполне прилично себя ведя под низкой. Если оно работало неделями на 1 запросе в секунду, на 1000 оно может умереть из-за ничтожной утечки памяти или ресурсов, перегрузки сети, перегрузки или переполнения диска и еще по 1001 причине. Поэтому — всегда гоняйте билд на полной нагрузке перед релизом на прод.
На этом всё. Комментируйте, поправляйте, делитесь своим опытом.
Итак…
Многие фреймворки имеют ограничения на производительность
С этим придется просто смириться. Очень большое количество кода не способно работать под большой нагрузкой — из-за чрезмерных требований к памяти, процессору, неэффективной синхронизации, устаревших механизмов ввода-вывода или плохой обработки ошибок при нехватке ресурсов. Даже если ваша любимая библиотека написана очень умными людьми и не имеет изъянов — она может не подойти просто потому, что при её проектировании эффективность принесли в жертву удобству или простоте использования. Поэтому — будьте готовы жить с самописками или искать те немногие решения, которые пройдут нагрузочные тесты под ваше конкретное приложение. Верить в этом деле нельзя никому.
Кеширование, кеширование и еще раз кеширование
Редкая высоконагруженная система обходится без кешей — особенно над СУБД. Правильное кеширование в распределенной системе — это большая и сложная тема, поэтому имеет смысл задуматься сразу о данных: кто и как будет их обновлять и запрашивать, а также где и как можно пожертвовать целостностью или актуальностью данных.
Простота
В целом — чем проще система, тем быстрее она работает. Нужно стремиться к максимальной простоте, часто в ущерб понятности, концептуальности или красоте архитектуры.
Пропускная способность сети имеет пределы
При выборе формата сериализации позаботьтесь заранее умножить средний размер пакета на кол-во пакетов в секунду и сравнить с пропускной способностью вашей сети. В этом плане Json лучше, чем XML, Protobuf лучше, чем JSON, а иногда приходится изобретать свой формат, с лучшей степенью упаковки.
Не забывайте про off-heap
Некоторые объекты в Java требуют дополнительную память для работы, поэтому не нужно рассчитывать, что если -Xmx выставлен, то приложение точно влезет на сервер. Например, каждый поток в Java требует от 256K до 2 мегабайт off-heap памяти для своей работы. При умножении на 1000 получается уже достаточно много, так что следите за кол-вом потоков, используемых вашим приложением.
GC не резиновый
Даже если строгих требований к latency нет, о сборке мусора нужно помнить. Старайтесь ограничивать кол-во аллокаций и, особенно, общий объем выделяемой памяти на каждый запрос. Все необходимые метрики есть в профайлере Java Mission Control.
Аккуратнее с логгированием
Утверждение «приличное приложение всегда должно логгировать входные и выходные данные» — верно, но в высоконагруженном приложении легко станет узким местом вашей системы. Contention на логгере, недостаточная скорость жестого диска, гигабайты логов в час — это всё суровая реальность. Поэтому выбирать данные для логгирования нужно очень аккуратно и помнить про стектрейсы — они занимают много места и при большом кол-ве ошибок способны положить приложение. Часто приходится вообще не писать логи на диск, а слать их по сети в специализированную систему (и помнить, что сеть тоже не резиновая, ага).
Поведение при нехватке ресурсов
Что делать, если приложению приходит больше запросов, чем оно способно прожевать? Если база внезапно стала отвечать в два раза медленней? Если в сети начались потери пакетов? Хорошее приложение должно не виснуть наглухо, а отвечать «завтра приходи». Мораль — для всех взаимодействий с внешними системами должны быть таймауты, приложение должно ограничивать кол-во параллельно обрабатываемых запросов и клиент должен знать, что делать, если приложение занято или не отвечает.
Сделайте нормальный мониторинг
Еще одна сложная и обширная тема, но вкратце — если приложение наметво повесило машину, то где-то должны остаться графики потребления памяти, свопа, диска, цпу, потоков, системных дескрипторов.
И всегда тестируйте приложение под нагрузкой
Приложение легко может умереть под высокой нагрузкой, вполне прилично себя ведя под низкой. Если оно работало неделями на 1 запросе в секунду, на 1000 оно может умереть из-за ничтожной утечки памяти или ресурсов, перегрузки сети, перегрузки или переполнения диска и еще по 1001 причине. Поэтому — всегда гоняйте билд на полной нагрузке перед релизом на прод.
На этом всё. Комментируйте, поправляйте, делитесь своим опытом.
Комментарии (6)
gto
03.11.2015 18:07+6Простите, конечно, но смысл статьи: «Чтобы построить HL приложение, нужно задуматься». А так оно не понятно? Я бы даже больше сказал, при проектировке любого приложения надо заботится и о памяти и о gc и о кешах. Ведь даже одному запросу в секунду будет приятно получить ответ сразу же, без задержек. И соглашусь с первым комментатором, жизненых примеров-то, которые украшают любую статью, нет, а без них польза написанного сомнительна.
kidar2
Про Java ничего не нашёл в статье.
Scf
У разных языков программирования разный рантайм, разные наборы библиотек, разные подходы к разработке и разная культура. Изложенное в статье — это опыт программирования на Java и для JVM. Конечно, что-то верно для всех языков и платформ, но везде есть своя специфика и какие-то свои острые углы, неприменимые для Java/JVM разработки.
Я не пытался давать конкретные советы с примером кода — почти по каждому пункту можно написать очень много, но мне хотелось изложить самое важное с моей точки зрения и при этом не раздуть статью до состояния полной нечитабельности.
Suvitruf
kidar2 имел ввиду, что эти советы применительны к любой технологии практически. Поэтому логичнее было бы назвать статью просто «Highload: о чем нужно помнить».
Scf
Это я и пытаюсь объяснить — я не хочу давать советы для технологий, с которыми я не знаком. Моё мнение — эти советы не универсальны, так что удаление слова Java из названия статьи будет немного обманом.
madkite
ну как — наличие GC и то, что стэк потока не в heap-е, который под GC. Но негусто, конечно. Я ожидал услышать что-то типа — не юзайте поток для каждого долгоживущего соединения, юзайте неблокирующий ввод/вывод (Java NIO). Хотя если 1k уже считать highload…