Pull to refresh

Высоконагруженное приложение на java

Reading time4 min
Views28K
Хотелось бы добавить по поводу высоконагруженных систем на java относительно этой статьи. В комментариях писать слишком много, поэтому напишу отдельно. Подход не совсем программиста, но может кому пригодится. Это только мое мнение, возможно, оно где-то неверно.

Платформа JEE


Конечно на стандартной java ваше приложение будет работать быстрее, потреблять меньше памяти, и вы можете сделать архитектуру именно такой, как вам нужно. Но это при том условии, что у вас неограниченное количество человеко-часов, и вы каждый день пишете такую ерунду как приложение на 100к клиентов. В противном случае для этого потребуется очень много времени и итераций, и в итоге все равно получиться чуточку хуже, чем то, как это реализовано в современных JEE серверах. Все промахи всплывут только под реальной нагрузкой, и архитектуру приложения придется переделывать не один раз.

Java.nio для сети


В java.nio появился не блокирующий ввод вывод, это значит не нужно запускать поток для каждого нового соединения. Один поток может обслуживать несколько сотен и больше соединений, не вызывая частых context switch и не загружая память. Но это не панацея, у такого подхода есть существенный недостаток: любая задержка в потоке сказывается на запросах в очереди. В некоторых случаях подход «одно соединение – один поток» может оказаться предпочтительнее.

Со стороны операционной системы на Linux для поддержки epoll нужно ядро 2.6+. Для Solaris для поддержки eventport нужна java 8 и параметр -Djava.nio.channels.spi.Selector=sun.nio.ch.EventPortSelectorProvider, иначе будет /dev/poll, который тоже не плох.

Пулы потоков


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

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

Для примера: Пул из 100 потоков со средним временем выполнения 5мс может обслуживать 20к запросов в секунду. А если им придется ждать ответа от внешнего сервиса 50мс, то количество обрабатываемых запросов сократится до 1.8к. Если вызов сервиса нужен не для всех запросов, целесообразнее придумать асинхронную обработку.

Пулы не нужно создавать сразу максимальными, лучше увеличивать их по мере роста нагрузки, а со снижением нагрузки уменьшать. Так как потоки, даже простаивающие, потребляют память и замедляют всю систему. Поток в java == потоку в ОС, слишком большое превышение размера пула над потоками цпу ведет к падению производительности.

Пулы соединений к БД


Смысл использования пулов заключается в экономии времени на установку соединения и контроль количества соединений. Если вы пишете пул сами, а не используете готовый в случае JEE, нужно внимательно расставить тайминги. Если соединение ожидать слишком долго, это ведет к задержке потока выполнения и росту размера соответствующего пула. При пересоздании пула при сбое нужно сделать тайминг на повтор попытки соединения, может быть стоит его сделать прогрессивным, иначе можно случайно совершить DDoS-атаку на сервер.

Это касается не только БД, но и любого другого внешнего ресурса, даже http.

Очереди


Они нужны для взаимодействия между потоками, в реализации это какой-нибудь LinkedList или LinkedBlockingQueue. Не стоит их делать слишком большими, так как они потребляют много памяти и может так случиться, что потоки не успевают обработать запросы из очереди за время tcp или http таймаута. В тоже время они должны быть достаточно большими, чтобы сглаживать кратковременные всплески нагрузки. Ориентировочная величина в 10 раз больше размера обрабатывающего пула.

JSP


Не стоит использовать JSP для высоко нагруженной системы, даже с кэшем он будет медленнее сервлетов. Чтобы можно было без проблем горизонтально масштабировать систему, нужно делать сервисы без хранения состояния на сервере.

Логирование


Понятно, что логировать нужно минимально необходимую информацию. Важный момент: логирование не должно приводить к отказу системы, если нет специальных требований этого. То есть если не удалось записать лог, пишем об этом в System.err и работаем дальше, а не завершаем поток с экзепшеном. Лог можно ротировать по размеру, по времени, но он не должен быть бесконечным. Благо все java библиотеки для логирования замечательно с этим справляются.

Подготовку и вывод дебаг лога рекомендуют оборачивать в нечто подобное, чтобы не тратить лишние такты:

if(log.isDebugEnabled()){
	StringBuilder sb = new StringBuilder();
	sb.append(arg0);
	sb.append(arg1);
	log.debug(sb.toString());
}

Мониторинг java


Для оценки нагрузки важно в реалтайме смотреть размеры хипа, пулов потоков, пулов соединений к БД и другим ресурсам, сессий. Нужно так же иметь исторические данные для оценки. Можно мониторить через jcontrol или подобный функционал, но это не очень удобно. Лучше собирать и писать в базу эти метрики отдельным софтом или самостоятельно. Количество метрик должно быть минимальным для оценки состояния, так как тоже требует системных ресурсов, при большом количестве соединений это может быть ощутимо.

Deadlock потоков


Для нормального JEE сервера не стоит использовать свои потоки и пулы потоков. Лучше использовать средства сервера, даже если они не так удобны. Дело в том, что сервер следит за своими потоками и отлавливает deadlock, а о ваших он ничего знать не будет.

Если приходится писать свои, то нужно отслеживать deadlock и просто блокировку и принудительно их завершать. В случае большого количества потоков deadlock возникает в самых неожиданных местах.
Tags:
Hubs:
Total votes 30: ↑18 and ↓12+6
Comments36

Articles