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

Выращиваем архитектуру
Начинаем выращивать наше решение с классической трехзвенки:
клиент, он же браузер
сервер, он же бизнес-логика, он же БЛ
база, она же... база и есть база

В самом простом начальном варианте логика работы и отображения никак не разделены, существуют в едином кодовом пространстве (видимо, ниндзя уже приложил руку) в, не в обиду самому языку, классическом PHP-стиле:

Разделим логику представления и логику работы. Это может быть как логическое выделение шаблонов отображения в коде, так и физический вынос таких статичных данных на независимый от БЛ ресурс:

Проблема: блокировка бизнес-логики
Теперь наш ниндзя готов нанести первый удар: написать настолько медленный код, чтобы вся бизнес-логика оказалась заблокированной целиком - например, содержащий бесконечный цикл или постоянное потребление памяти. Даже если такой запрос блокирует всего лишь один процесс на БЛ, то нет��рпеливый пользователь, не дождавшись ответа, "дернет" его снова и снова, пока не заблокируется все:

Посмотрите, как красиво "покраснели" все участки, где сбой "сыграл" - ни один клиент ничего не получает, ниндзя доволен, диверсия удалась!
Но мы добавим перед БЛ некоторый диспетчер (например, на базе nginx), который умеет "отстреливать" конкретный узел, если он перестал отзываться, а самих узлов сделаем несколько:

Проблема: монолитная база
Хитрый ниндзя заметил, что чем по более глубокому слою он нанесет удар, тем больше эффект. В текущей нашей схеме таким удобным местом является база данных. Опередим его, разделив данные по некоторым прикладным критериям - например, оперативные текущие данные и статистические отчеты:

Фактически, в этот момент мы получили уже сервисную архитектуру. Насколько она будет "микро" еще вопрос, но сервисы уже получают некоторую прикладную "цветовую дифференциацию штанов".
Проблема: жесткая взаимозависимость сервисов
Смотрим на схему выше - и видим, что в случае проблем на сервисе Y, сервис X тоже начнет страдать, хотя в большинстве случаев мог бы этого не делать. Например, если после оформления заказа нам надо отправить его клиенту на email, си��хронный вызов здесь вовсе не нужен.
Поэтому мы можем разорвать некоторые цепочки вызовов и "размазать" пиковую нагрузку во времени, превратив их в асинхронные с помощью межсервисной шины:

Проблема: DDoS на базу
Если в качестве базы данных для своего решения вы использовали PostgreSQL (больше возможностей, чем MySQL, почти такой же enterprise, как Oracle, надежно, да еще и бесплатно!), то рано или поздно начинаете понимать, что работать в web-системах с большим количеством коротко живущих подключений ему некомфортно. Ведь каждый коннект к PG - это отдельный процесс на сервере СУБД, выделяющий себе при старте не менее 8MB памяти.
Чтобы нивелировать эти негативные эффекты, умные люди придумали пулеры соединений - например, pgbouncer или Одиссей. На вход они принимают множество подключений, а на выходе кроссируют их в небольшое количество постоянно активных соединений с PostgreSQL.
В результате, наша архитектура превращается во что-то такое:

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

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

"Роняем" браузер
Чтобы заставить Chrome затупить, существует (минимум!) три простых метода:
Заставить его выкачивать много трафика.
Скормить ему "жирный" JS-файл. Поскольку трансляция JS пока идет в том же единственном потоке, что и его исполнение, то "пусть весь мир подождет".
Заставить его сгенерировать тучу HTTP-запросов. Даже если они у вас все закэшированы, то где-то в районе 700-го запроса Chrome "ломается" и начинает отдавать файлы с дискового кэша по несколько секунд.

TODO #01: Используйте минификацию.
TODO #02: Оптимизируйте количество кода.
TODO #03: Применяйте пакетирование. И следите, чтобы оно реально использовалось.
"Роняем" диспетчер
Чем можно "прижать" диспетчер? По сути, только большим количеством запросов и/или трафика за короткое время.
Чтобы добиться такого эффекта, уже понадобится напрячься - синхронизировать большое количество клиентских браузеров, потому что с од��ой машины значимо нагрузить не получится.
Для этого лучше всего...
разослать некоторое событие "на всех" пользователей
повесить на его приход "мгновенную" обработку
независимо из каждой вкладки
... и запросить с сервера что-то объемное и несжимаемое (фотки, например) или сжимаемое динамически

TODO #04: Кэшируйте заголовками максимально, чтобы хотя бы часть запросов на сервер таки не долетела.
TODO #05: Убедитесь по заголовкам ответа, что диспетчер не пытается "сжимать несжимаемое" и тупить на этом - бинарники или заведомо мелкие ответы.
TODO #06: Делайте обработку события из единственной вкладки, раздавая остальным ответ через localStorage.
TODO #07: Делайте рандомизированную задержку в пределах нескольких секунд от момента прихода события на клиента до запроса на сервер.
"Роняем" бизнес-логику
Исчерпание ресурсов
Если ваша БЛ заточена под последовательную синхронную обработку каждого входящего запроса в отдельном процессе/потоке (ex: Apache, IIS), то достаточно занять их все, чтобы начала копиться очередь и расти время выполнения.
Если же БЛ у вас асинхронная (ex: NodeJS), то стоит усилить воздействие, вогнав event loop процесса в клинч чем-то вроде бесконечного цикла или множественных операций над строками.
Для этого нам отлично пригодится методика из предыдущего пункта, генерирующая тучу запросов.

TODO #08: Используйте различные варианты rate-limiter - лучше прямо на диспетчере.
Прикладной deadlock
Эффективным вариантом также будет организовать deadlock - отправить свои методы синхронно выполнять методы стороннего сервиса, а из них - снова в исходном сервисе!
TODO #09: Моделируйте возможность появления цепочек вызовов A -> B -> A. Нашли - избавляйтесь или обвешивайте "жесткими" таймаутами в пределах сотен миллисекунд.
"Роняем" межсервисную шину
Мультипликация данных
Сгенерировать "на вход" относительно немного сообщений с большим количеством получателей у каждого. Внутри они "помножатся", и если не "порвет" при синхронизации узлов, то "ляжет" выходной канал. Не лег - добавить получателей.

TODO #0A: А нужна ли вам тут именно общая шина обмена? Подумайте над поднятием специализированной БЛ для обмена такими событиями.
Mailbombing
Генерируем большое количество объемных сообщений в адрес другой БЛ. Вот тут точно на синхронизации узлов "порвет".
TODO #0B: Передавать не сам контент, а ссылку на него. Или смотри #0A.
"Роняем" пулер соединений
Жил-был pgbouncer в transaction mode... Почему именно в таком? Потому что именно этот режим позволяет, в большинстве случаев, наилучшим образом утилизировать соединение к БД.
А это значит, что для каждой отдельной транзакции идет новое пересопоставление "клиентского" соединения "серверному".
То есть если не "обернуть" в транзакцию, метод с тучей мелких запросов к БД, каждый из них будет восприниматься как отдельная транзакция, обрабатываться независимо на отдельном соединении, и издержки на эту обработку будут существенно выше времени самого выполнения каждого SQL-запроса.
TODO #0C: Оборачивайте свои методы в транзакции целиком, если это не противоречит прикладной задаче.
TODO #0D: Используйте session mode и prepared statements.
Автогенерируемые запросы/данные
Лучше всего заходит в комбинации с ORM, для которого в радость выдать тело запроса на пару мегабайт или IN (...) с сотней тысяч свежевычитанных из базы же идентификаторов. Для пулера это означает бесполезную трату ресурсов на перекладывание байтиков между сокетами.

TODO #0E: Не стоит использовать ORM, пока вы детально не понимаете, как он работает.
"Роняем" базу данных
Настоящее раздолье для ценителя.
DDoS
"А давайте в нашем методе делать X на базе одновременно в несколько потоков!" И в нескольких процессах БЛ, до кучи... и без разумного ограничителя количества процессов-потоков.
Точно-точно надо именно одновременно? В таком-то количестве? А #6 и #7 - не ваш ли вариант?..
"Ладно, у нас будет не больше Y одновременных запросов..." Ага, зато каждый читает гигабайты данных из кэша БД - тут-то пропускная способность памяти и кончается...
TODO #0F: Используйте кэширование ответов на стороне БЛ.
TODO #10: Оптимизируйте уже свои запросы! См. статью "Рецепты для хворающих SQL-запросов".
Блокировки
"Ну, я тогда сейчас заблокирую!.. advisory locks не просто так мне даны! ... и тысячи их!"

TODO #11: pg_try_advisory_xact_lock - при завершении транзакции снимаются автоматически. См. статью "Фантастические advisory locks, и где они обитают".
"Мне надо поменять поле/накатить индекс прямо сейчас!" и компания ALTER TABLE.
TODO #12: См. статью "DBA: когда почти закончился serial".
TODO #13: В целом, активность разных "ниндзя-разработчиков" на базе стоит мониторить и анализировать всегда. Как это делаем мы в "Тензоре" можно прочитать в статьях "Мониторим базу PostgreSQL — кто виноват, и что делать" и "Массовая оптимизация запросов PostgreSQL".
В общем, проверяйте почаще, что ваши ниндзя все еще работают именно на вас.

