System Design 101
О сложных системах простыми словами.
В шпаргалке на высоком уровне рассматриваются такие вещи, как протоколы коммуникации, DevOps, CI/CD, архитектурные паттерны, базы данных, кэширование, микросервисы (и монолиты), платежные системы, Git, облачные сервисы etc. Особую ценность представляют диаграммы - рекомендую уделить им пристальное внимание.
Выражаю благодарность Анне Неустроевой за помощь в редактировании материала.
System Design (сборник на английском языке).
Протоколы
Архитектура (дизайн) определяет, как разные компоненты приложения взаимодействуют друг с другом. Она обеспечивает эффективность, надежность и легкость интеграции с другими системами путем предоставления стандартного подхода к проектированию и разработке API. Наиболее популярными архитектурами являются следующие:
- SOAP:
- зрелый, всесторонний, основанный на XML
- хорошо подходит для корпоративных приложений
- REST:
- популярный, легкий в реализации, основанный на методах HTTP
- хорошо подходит для веб-сервисов
- GraphQL:
- язык запросов (query language), позволяющий запрашивать данные избирательно (частично)
- меньше нагрузка на сеть, более быстрые ответы от сервера
- gRPC:
- современный, высокопроизводительный, основанный на буферах протоколов (protocol buffers)
- хорошо подходит для микросервисов
- WebSocket:
- двусторонний, позволяет обмениваться данными в реальном времени, соединение остается открытым
- хорошо подходит для обмена данными небольшого размера
- Webhook:
- основанный на событиях и коллбэках HTTP, асинхронный
- хорошо подходит для систем уведомлений
REST и GraphQL
Сравнение REST и GraphQL:
- GraphQL – это язык запросов для API, разработанный Meta. Он предоставляет полное описание данных в API и позволяет клиенту запрашивать только то, что ему нужно
- сервер GraphQL является посредником между клиентом и сервером. GraphQL может агрегировать несколько запросов REST в один запрос (и request, и query в переводе на русский означают "запрос"). Сервер GraphQL организует ресурсы в граф (отсюда и название)
- GraphQL поддерживает запросы, мутации (модификация ресурсов) и подписки (получение уведомлений о модификациях)
gRPC
RPC (Remote Procedure Call – удаленный вызов процедур) называется "удаленным", поскольку обеспечивает взаимодействие между удаленными сервисами, когда они находятся на разных серверах в микросервисной архитектуре. С точки зрения пользователя это выглядит, как вызов локальной функции.
На диаграмме представлен поток данных (flow) gRPC:
- Клиент отправляет REST-запрос. Тело запроса (request body) содержит данные в формате JSON (как правило). 2-4. Сервис заказов (order service) (клиент gRPC) получает запрос, преобразует его и отправляет запрос RPC сервису оплаты (payment service) (сервер gRPC). gRPC кодирует данные клиента в двоичный формат и отправляет их в низкоуровневый транспортный слой (transport layer).
- gRPC отправляет пакеты данных (data packages) по сети с помощью HTTP2. Благодаря двоичной кодировке и сетевым оптимизациям gRPC может быть до 5 раз быстрее JSON. 6-8. Сервис оплаты получает пакеты данных по сети, декодирует их и вызывает серверное приложение. 9-11. Результат, полученный от серверного приложения, кодируется и отправляется в транспортный слой. 12-14. Сервис заказов получает пакеты данных, декодирует их и отправляет результат клиентскому приложению.
Webhook
Сравнение Polling (опроса) и Webhook:
Предположим, что у нас есть электронный магазин. Клиенты отправляют заказы в сервис заказов через шлюз API (API Gateway), а сервис заказов обращается к сервису оплаты для выполнения денежных транзакций. Сервис оплаты, в свою очередь, обращается к внешнему провайдеру сервиса оплаты (Payment Service Provider, PSP) для завершения транзакции.
Существует 2 способа взаимодействия с внешним PSP.
- Короткий опрос.
После отправки платежного запроса PSP, сервис оплаты продолжает опрашивать PSP о статусе платежа (путем периодической отправки запросов) до тех пор, пока PSP не сообщит о завершении операции.
Короткий опрос имеет следующие недостатки:
- постоянные запросы статуса расходуют ресурсы сервиса оплаты
- внешний сервис взаимодействует напрямую с сервисом оплаты, что создает уязвимости безопасности
- Веб-хуки
Веб-хук регистрируется во внешнем сервисе. Мы как бы просим внешний сервис сообщить нам об изменениях по запросу по указанному URL. После выполнения операции, PSP отправляет запрос HTTP для обновления статуса платежа.
Ресурсы сервиса оплаты больше не расходуются на опрос.
Веб-хуки часто называют реверсивными (reverse) API или push-API, поскольку сервер отправляет запрос HTTP клиенту, а не наоборот.
При использовании веб-хуков следует уделять пристальное внимание следующим вещам:
- API должно быть правильно спроектировано для взаимодействия с внешним сервисом
- в шлюзе API должны быть установлены определенные правила безопасности
- во внешнем сервисе следует регистрировать правильный URL
Производительность API
5 распространенных способов улучшения производительности API:
Пагинация
Эту оптимизацию применяют в случае большого объема данных. Результат отправляется клиенту по частям (чанкам - chunks) для улучшения отзывчивости сервиса.
Асинхронное логирование
Синхронное логирование работает с диском при каждом вызове и может замедлить систему. Асинхронное логирование отправляет логи в буфер без блокировки (lock-free buffer) и сразу возвращается. Логи записываются на диск с определенной периодичностью. Это существенно снижает нагрузку на ввод/вывод.
Кэширование
Мы можем сохранять часто запрашиваемые данные в кэше и возвращать их клиентам без повторного обращения к базе данных. Доступ к данным, хранящемся в памяти Redis, например, гораздо быстрее, чем доступ к БД.
Сжатие полезной нагрузки
Запросы и ответы могут сжиматься с помощью GZIP и других алгоритмов сжатия. Чем меньше размер данных, тем быстрее они передаются по сети. Таким образом, сжатие ускоряет загрузку и скачивание данных.
Пул подключений
При доступе к ресурсам нам часто приходится загружать данные из БД. Открытие нового подключения к БД – дорогая операция, с точки зрения производительности, поэтому для доступа к БД следует использовать пул открытых (набор существующих) подключений (connection pool). Пул подключений отвечает за управление жизненным циклом соединений.
HTTP 1.0 -> HTTP 1.1 -> HTTP 2.0 -> HTTP 3.0 (QUIC)
- HTTP 1.0 был завершен и полностью задокументирован в 1996. Каждый запрос к серверу требует отдельного соединения TCP
- HTTP 1.1 был опубликован в 1997. TCP-соединение может оставаться открытым для повторного использования (постоянное подключение), но проблема блокировки HOL (head-of-line) остается. Блокировка HOL означает, что когда исчерпан лимит параллельных запросов, новые запросы ждут завершения предыдущих
- HTTP 2.0 был опубликован в 2015. Он решает проблему HOL путем мультиплексирования запросов на уровне приложения (application layer), но HOL остается на транспортном уровне (transport layer, например, TCP). Как видно на диаграмме, HTTP 2.0 представил концепцию "потоков" (streams) HTTP - абстракция, позволяющая разным запросам HTTP использовать одно соединение TCP. Потоки могут отправляться в разном порядке
- первый черновик HTTP 3.0 был опубликован в 2020. В качестве нижележащего транспортного протокола вместо TCP в нем используется QUIC, что решает проблему HOL в транспортном слое
QUIC основан на UDP. Он обеспечивает первоклассную поддержку потоков в транспортном слое. Потоки QUIC используют одно соединение QUIC, поэтому не требуется затрат на рукопожатия (handshakes) и холодные запуски для создания новых соединений. Потоки QUIC доставляются независимо, поэтому в большинстве случаев потеря пакетов в одном потоке не влияет на пакеты в другом потоке.
SOAP, REST, GraphQL и RPC
Существуют разные архитектурные стили API, каждый со своими паттернами и стандартами обмена данными:
Сначала код и сначала API
Разница между подходами к разработке "Сначала код" и "Сначала API" (Code First, API First):
- Микросервисы повышают сложность системы. Разные функции системы обслуживаются отдельными сервисами. Хотя такая архитектура облегчает разделение обязанностей, реализация взаимодействия между сервисами является дополнительным вызовом.
При написании кода следует помнить о сложности системы и аккуратно определять границы (зоны ответственности) сервисов.
- Отдельные команды разработчиков должны говорить на одном языке. Каждая команда отвечает только з а свои компоненты и сервисы. Рекомендуется заранее проектировать дизайн API на уровне организации.
Для валидации дизайна API перед написанием кода можно использовать фиктивные запросы и ответы.
- В целом, микросервисная архитектура повышает качество ПО и продуктивность разработчиков. Грамотно спроектированное API позволяет быстрее запускать проект и делает процесс разработки более плавным.
Улучшается опыт разработки, поскольку разработчики могут сосредоточиться на реализации функционала вместо того, чтобы постоянно заниматься настройкой и интеграцией.
Снижается вероятность возникновения неприятных сюрпризов на последних этапах жизненного цикла проекта.
Наличие спроектированного API позволяет писать тесты, не дожидаясь разработки. Отсюда один шаг к разработке на основе тестов (Test Driven Design, TDD).
Коды статусов HTTP
Коды ответов(статусов) HTTP делятся на 5 категорий:
- Информационные (100-199).
- Коды успеха (200-299).
- Коды перенаправления (300-399).
- Коды ошибок на стороне клиента (400-499).
- Коды ошибок на стороне сервера (500-599).
Шлюз API
- Клиент отправляет запрос HTTP в шлюз API (API Gateway).
- Шлюз разбирает (парсит) и проверяет атрибуты запроса.
- Шлюз выполняет проверки по белому/черному списку ресурсов.
- Шлюз обращается к провайдеру идентификации (поставщику удостоверений – Identity Provider) для аутентификации и авторизации.
- К запросу применяются правила ограничения частоты запросов (rate limit). При превышении лимита запрос отклоняется. 6 и 7. После выполнения проверок шлюз определяет сервис, совпадающий с путем роута (route path).
- Шлюз преобразует запрос в подходящий протокол и передает его серверным микросервисам. 9-12. Шлюз отвечает за обработку ошибок, а также провалов, связанных с тем, что решение проблемы требует больше времени (разрыв цепочки - circuit break). Для логирования и мониторинга здесь также может использоваться стек ELK (Elastic-Logstash-Kibana). Здесь же можно реализовать кэширование данных.
Эффективное и безопасное API
Типичные проекты/схемы API на примере корзины товаров:
Обратите внимание, что дизайн API – это не только дизайн URL. Необходимо правильно выбирать названия ресурсов, идентификаторы и паттерны путей (path patterns). Также важно устанавливать правильные заголовки HTTP и эффективные правила ограничения частоты запросов (rate limit).
Инкапсуляция TCP/IP
Как данные передаются по сети? Почему в сетевой модели OSI (Open Systems Interconnection – взаимосвязь открытых систем) так много уровней?
На диаграмме по казано, как данные инкапсулируются и распаковываются при передаче по сети.
- Когда устройства А передает данные устройству Б по протоколу HTTP, сначала на уровне приложения (application layer) в запрос добавляется заголовок HTTP.
- Затем к данным добавляются заголовки TCP или UDP. Они инкапсулируются в сегменты TCP на транспортном уровне (transport layer). Заголовок содержит порт источника (source port), порт назначения (destination port) и номер последовательности (sequential number).
- Затем на сетевом уровне (network layer) добавляется заголовок IP. Он содержит адреса IP источника/назначения.
- На уровне связи данных (data link layer) в датаграмму (datagram) IP добавляется заголовок MAC с MAC-адресами источника и получателя.
- Инкапсулированные кадры (фреймы – frames) попадают на физический уровень (physical layer) и передаются по сети в двоичном виде. 6-10. При получении битов по сети устройство Б выполняет процесс распаковки, обратный процессу инкапсуляции. Заголовки удаляются слой за слоем, после чего устройство Б может читать данные.
У каждого уровня своя задача. Каждый уровень извлекает инструкции из соответствующего заголовка, что избавляет от необход имости владеть полной информацией о данных.
Почему NGINX называют "обратным" прокси?
Разница между прямым (forward) и обратным (reverse) прокси:
Прямой прокси – это сервер, находящийся между пользователем и Интернетом.
Прямой прокси обычно используется для:
- Защиты клиентов.
- Преодоления ограничений браузера.
- Блокировки доступа к определенным ресурсам.
Обратный прокси – это сервер, принимающий запросы от клиента, передающий их веб-серверу и возвращающий результат клиенту после обработки запроса сервером.
Обратный прокси хорошо подходит для:
- Защиты сервера.
- Балансировки нагрузки.
- Кэширования статических ресурсов.
- Кодирования и декодирования подключений SSL.