Всем привет!

В этой статье я хочу показать, что из себя представляет System Design. На примере простого и очень популярного насобеседованиях сервиса «Сокращатель ссылок», мы рассмотрим стадии System Design и в конце у нас будет готова схема системы со всеми расчетами.

В данной статье мы не будем обсуждать сложные определения и подводные камни каждого подхода. Мы лишь сфокусируемся на порядке обязательных этапов в System Design для получения результата. Материал будет простой и понятный для того чтобы точно убедиться, что в System Design нет ничего сложного, это лишь конечный набор правил и технологий, комбинацию которых нужно применять.

System Design состоит из следующих шагов:

  1. Требования к системе (рассмотрим в этой статье)

  2. Расчет нагрузки и стоимости вашей системы (рассмотрим в этой статье)

  3. Верхнеуровневый дизайн (рассмотрим в этой статье)

  4. Выбор баз данных (рассмотрим частично в этой статье)

  5. Модульный дизайн (не будем рассматривать)

  6. Оптимизация системы (рассмотрим в этой статье)

  7. Оснащение системы дополнительными подсистемами (не будем рассматривать)

Так каксистема «Сокращать ссылок» очень простая, шаги «Модульный дизайн» и «Оснащение системы дополнительными подсистемами» мы опустим.

Не буду долго тянуть, давайте начинать!

Сбор требований

Как мы знаем, есть 2 вида требований:

Функциональные требования — это четкие, конкретные описания, что должна делать система, какие функции она предоставляет пользователям и как реагирует на их действия. Они отвечают на вопрос: «Какую задачу решает система?» и описывают конкретные функции.

Давайте сразу зафиксируем функциональные требования для нашего сервиса «Сокращаетесь ссылок»:

  1. Получение короткой короткой ссылки из длинного URL.

  2. Перенаправление с короткой ссылки на исходный URL.

Нефункциональные требования — описывают как IT‑Система должна работать, а не что она делает. Они определяют характеристики производительности, надежности, безопасности и другие аспекты, которые влияют на качество системы и которые должны также быть связаны с бизнес‑целями.

  1. Высокая доступность — сервис должен работать всегда.

  2. Минимальная задержка — перенаправление должно быть быстрым.

  3. Масштабируемость — система должна выдерживать миллионы запросов.

Давайте также определим дополнительные требования которые возникают из бизнес-ограничений, анализа рынка и т.д.

  1. Дневное кол-во уникальных пользователей нашего сервиса (DAU) = 100 000

  2. В среднем 10% пользователей генерирует в день 1 короткую ссылку

  3. Остальные 90% пользователей в среднем переходят по коротким ссылкам из нашего сервиса по 10 раз в день.

Подробнее про сбор требований можете найти тут: System Design: Чек-лист по сбору и фиксации требований на все случае жизни.

Двигаемся дальше.

Расчет нагрузки и стоимости

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

  1. Начнем с расчета кол-ва запросов в секунду. Придется разделить запросы к системе на 2 части. 1ая часть говорит о том что 10000 пользователей обращаются в системе 1 раз, 2ая часть говорит о том, что 90000 обращаются к системе по 10 раз каждый, следовательно

    RPS = {DAU * Q \over 86400}  = {10000*1 + 90000*10\over86400}={910000\over86400} \approx11RPS,

  2. Далее считаем кол-во одновременных соединений в течении дня. Для этого зафиксируем, что в среднем сессия пользователя ограничивается запросом к системе и ответом. Таким образом предположим что s = 0.3с., p = 86400с., так как наша система работает 24 часа.

    CCU = {DAU * Q * s \over p} = {910000*0.3\over86400}\approx3\  соедниения/сек.

    С этим справится почти любой компьютер.

  3. Далее определяем каков средний размер одной единицы данных которая проходит через нашу систему. Предположим что длинный URL весит в среднем 200Б или 1600Бит. В таком случае размер маленькой ссылки в расчетах учитывать не будем и считаем через большее значение:

    R = RPS* S=11*1600\approx18000Бит=18Кбит/сек

    Так как 1 сетевой инстанс на 1Гбит/сек стоит 300$, то мы выберем его, так как даже при превышении нагрузки в 10 раз, его все равно хватит.

  4. Далее вычисляем сетевой трафик, где кол-во операции в день = 910000, а средний размер информации для запроса и ответа в среднем равно 200Б. Так как при запросе и при ответе каждый раз фигурирует ссылка, то читаем что на 1 запрос приходится 400Б.

    A = N *  S = 910000*400 = 364000000Б=364000МБ=364ГБ\ в\ сутки  ,

    Так как 1 ГБ трафик провайдеры просят 0.1$, следовательно мы потратим примерно 37$ в сутки или 1110$ в месяц.

  5. Далее считаем объем хранимых данных на горизонте времени (1 год). Считаем что хранени й ссылки с ее метаданными занимает не более 500Б. Ссылку загружают всего 10000 раз в день (согласно требованиям):

    V =N*  S*  T = 10000*500*365\approx2000000000Б=2ТБ

    Так как SSD стоит за 1ТБ 300$, то в год это 600$ на быструю память. Часть также можно заходить в RAM.

Подробнее про расчет нагрузки и стоимости можно найти тут: System Design: Чек-лист для расчета нагрузки и стоимости системы на все случаи жизни и тут Магия чисел в System Design: эти формулы спасут вас от банкротства и помогут оптимизировать вашу систему.

Едем дальше.

Дизайн

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

У нас есть:

  • Client — браузер или мобильное приложение

  • Link service — сервис. который будет получать 2 вида запросов: один на генерацию короткой ссылки по длинной, а второй за перенаправление с короткой на длинную.

  • Shortener service — сервис, который отвечает за генерирование короткой ссылки из длинной

  • DB — База данных, где хранятся все наши короткие и длинные ссылки, а также дата добавления, id, кол‑во запросов короткой ссылки и т. д.

Верхеуровневый дизайн сервиса "Сокращатель ссылок"
Верхеуровневый дизайн сервиса «Сокращатель ссылок»

Давайте вернемся к функциональным требованиям:

  1. Получение короткой ссылки из длинного URL. Работает следующим образом: Наш Client в запросе передаtт длинную ссылку в сервис Link service. После чего Link service сперва проверяет в DB, не была ли для этого длинного URL сгенерированна короткая ссылка ранее. Если да, то Link service , сразу возвращает это корочку ссылку. Если нет, то он обращается к Shortener service, для генерации короткой ссылки. Shortener service генерирует короткую ссылку, сохраняет ее в DB и передает короткую ссылку в Link service, который в свою очередь отправляет эту ссылку Client. (Опустим момент, что Shortener service также через DB проверяет уникальность созданной короткой ссылки).

  2. Перенаправление с короткой ссылки на исходный URL. Работает следующим образом: Наш Client переходит по короткой ссылке и отправляется в наш сервис Link service, который по id короткой ссылки получает из DB и отдает Client информацию о редиректе на сайт по длинному URL.

Проектирование API

Давайте схематичного набросаем API.

API для создания короткой ссылки. Для сервиса Link service запрос на генерирование короткой ссылки из длинной будем использовать ручку POST /shorten. Метод POST используется так как почти каждый такой запрос будет сопровождаться созданием новой записи в базе данных.

Пример запроса:

POST http://shorturl.ru/shorten
{
  "long_url" : "https://systemdesign.ru/long-long-long-url"
}

Пример ответа:

HTTP/1.1 200 
Content-Length: 40
{
  "short_url":"http://shorturl.ru/qwe123"
}

Тут qwe123 — это короткий код ссылки. В последствии он может быть использован для поиска в DB.

API для редиректа по короткой ссылке будем использовать также метод сервиса Link service: GET /{short_url}. Метод GET используется потому, что мы не будем влиять на систему, мы лишь получим данные и разойдемся)

Пример запроса:

GET https://shorturl.ru/qwe123

Пример ответа:

HTTP/1.1 301 Moved Permanently  
Location: https://systemdesign.ru/long-long-long-url
Content-Length: 0

HTTP 301 Moved Permanently — код состояния, который указывает, что ссылка была перемещена навсегда. То есть мы тут сообщаем, что каждый раз при запросе по этой короткой ссылке мы всегда будем его перенаправлять на длинную. Кстати для временного редиректа используется не 301 Moved Permanently, а 302 Found, который сообщает что это временная акция и в скором времени длинная ссылка может измениться, но это не наш случай.

Location — заголовок, который указывает на длинную ссылку, на которую необходимо перенаправить пользователя.

Content-Length — заголовок, указывающий на то, что тело ответа пустое.

Как работает перенаправление?

Шаг 1: Вы кликаете на https://shorturl.ru/qwe123

Ваш браузер отправляет запрос к серверу http://shorturl.ru

Шаг 2: Сервер ищет код qwe123 в БД

Он быстро проверяет: Есть ли запись с таким кодом? Куда перенаправлять?

Шаг 3: Сервер отправляет ответ 301 Moved Permanently

Если код найден, сервер отвечает:

HTTP/1.1 301 Moved Permanently  
Location: https://systemdesign.ru/long-long-long-url

Шаг 4: Браузер перенаправляет вас на длинный URL

Браузер видит код 301 и автоматически переходит по указанному в Location адресу.

Выбор базы данных

У нас всего лишь одна база данных DB. Будем использовать реляционную СУБД PostgreSQL. Потому что наша таблица имеет строгую структуру и нам важна быстрота. Не будем уходить в подробности на данном этапе. Тема очень обширная и на это понадобится отдельная статья или курс.

Оптимизация

Первым делать нужно понимать, что кол-во пользователей будет расти, а нам ссылаясь на нефункицональные требования необходимо, чтобы наша система была масштабируемой, доступной и с минимальной задержкой. Давай те оптимизирует систему, чтобы она отвечала этим требованиям, при росте пользователей.

Если кол-во пользователей вырастит в 1000 раз это будет уже совсем другая нагрузка. 1 сервер может не справиться. Поэтому мы можем применить горизонтальное маштабирование - Добавляем несколько серверов (инстанс = экземпляр= нода) и распределяем между ними данные и нагрузку. Покажем это на схеме:

Горизонтальное масштабирование
Горизонтальное масштабирование

Но как же нам распределить нагрузку равномерно между всеми серверами? Для этого существуют балансировщики нагрузки (load balancer) — это как «умный распределитель» в мире серверов. Основная задача — равномерно раздавать запросы между разными машинами, чтобы ни один сервер не захлебнулся от перегрузки, пользователи получали ответы быстро (даже при наплыве трафика), система не падала, если одна из машин сломалась. Покажем работу балансировка нагрузки на схеме:

Балансировка нагрузки
Балансировка нагрузки

А что же с БД? Тут мы также можем осуществить горизонтальное масштабирование для чтения. Работает это так, 1 БД (мастер) будет использоваться для записи, после чего 1 БД синхронизирует данные со 2 БД (реплика) и в последствии также через балансировщик нагрузки будем читать данные из двух таблиц. (На самом деле вопрос репликация очень широкий и данной статье я привожу просто пример распространенного использования, который также покрыт различными подводными камнями). Таким образом наша система будет выглядеть следующим образом:

Репликация БД
Репликация БД

На рисунке явно видно, что сервис Shortener servive, генерирует короткие ссылки и складывает их в DB (Мастер БД на запись). Сервис Link service же наоборот только читает данные из DB (мастер) и DB_2 (реплика), которыесинхронизируются между собой (стрелочка от DB до DB_2).

Отлично, но как бы нам еще снизить нагрузку на БД, а также повысить время откика? Для этого есть Кэширование — это временное сохранение часто используемых данных в быстродоступном хранилище (кэше), чтобы ускорить их получение и снизить нагрузку на основную систему. В качестве кэширования выберем БД типа «Ключ‑значение» Redis. Данная БД отлично подходит для кэширования, так как поиск по ней очень быстрый и простой (Не будет вдаваться в подробности). Далее наша схема будет выглядеть уже вот так:

Кэщирование
Кэщирование

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

Что еще можно сделать?

Это систему всегда можно усложнить, например добавив такие сервисы как аутентификации и авторизации, добавив сервис оплаты и еще много сервисов, улучшающих нашу систему. Также можно добавить систему мониторинга и сервис уведомлений с брокером сообщений. А также, инвалиадцию кэша, партицирование, шардирование, избыточность и многое другое. Все лишь зависит от требований и нужды. Но мы на этой закончим)

В данной статье я постарался наглядно показать из каких шагов состоит System Design, без детального погружения. Секрет System Design в том, что даже одну и ту же систему мы можем построить по разному. System Design — это не про знание технологий, а про образ мышления. Если вы научитесь разбирать задачу по частям, взвешивать компромиссы и предвидеть проблемы, вы пройдёте не только собеседование, но и станете сильнее как инженер.

Нет «идеального» дизайна — есть оптимальный для конкретных требований и усоливий. 

Спасибо за внимание!

PS

Если же вас интересует еще больше примеров, детальное объяснение каждого шага, а также если вы хотите научиться принимать взвешенные архитектурные решения, которые выдержат миллионы пользователей и не сломаются при первой же проблеме. С гордостью представляю вам свой новый курс на Stepik, где представлены все этапы System Design с подробными лекциями, конспектами и практикой: C нуля до проектирования систем уровня senior-инженераСпециально для Habr до 31 августа действует промокод 20% HABR20.

Комментарии (11)


  1. pnmv
    11.07.2025 19:56

    Сокращатель ссылок - это хорошо.

    Плохо, что такие ссылки, да на незнакомый ресурс, могут восприеиматься как что-нибудь нехорошее.


  1. ReadOnlySadUser
    11.07.2025 19:56

    Если честно, я так и не понял, почему после добавления балансировщика, клиент вдруг стал ходить к Shortener Service напрямую)


  1. santjagocorkez
    11.07.2025 19:56

    Overkill на этапе выбора БД. Поскольку сервис заявлен, как крайне примитивный, а в метаданных очень сложно придумать отношения, то реляционная база данных здесь не нужна, и гораздо лучше подошла бы NoSQL база (например, GDB, MDB, dBase), которые работают вообще, как пулемет, имеют весьма лаконичный и строгий API и дадут огромный выигрыш уже тем, что этапы разбора запроса и планирование его выполнения отсутствуют как таковые.

    Вспомним, хотя бы, бенчмарки openldap (штатный сторадж у нее GDB, если не изменяет память), которая (даже с парсером LDAP запроса) показывала сотни тысяч RPS в начале 2000-х, естественно, на железе того времени.


  1. D_Dementy
    11.07.2025 19:56

    "Сервис опалы" звучит интригующе. "Сервис анафемы" еще предлагаю запилить.


  1. Gorthauer87
    11.07.2025 19:56

    Зачем тут распределённый монолит с общей базой под видом двух сервисов? Видно же, что это только усложняет логику и не даёт вообще никаких профитов.


    1. IvanZiv972003 Автор
      11.07.2025 19:56

      Хороший вопрос)

      Я решил не уходить к микросервисам напрямую, так как цель статьи показать шаги решения задач по System Design.

      Если мы говорим, про микросервисы, тогда Link service не должен ходить в БД, в которую пишет Shortener service. Варианты для Link service такие:
      1. Либо достает длинные ссылки по коротким из своего Кэша (Redis)
      2. Либо из реплики основной БД (куда пишет Shortener service)
      3. Либо комбинация

      Но тут возникает вопрос
      Что делать если Кэш сброшен или в реплике неактуальные данные?) Мы снова приходим к распределенному монолиту, так как приходится вызывать основную БД

      В таком случае для Кэша
      1. нам на помощь может прийти Kafka для асинхронного восстановления кэша: Кэш-воркер постоянно подписан на Kafka и восстанавливает Redis при сбоях (из истории Kafka). Так Kafka используется для асинхронного взаимодействия между Link service и Shortener service.
      2. Резервный кэш - всегда есть еще один Кэш (Redis), готовый прийти на помощь в случае сбоя.

      Если только реплика (без кэша) и она неактуальна, то мы тут как не крути либо возвращаем ошибку и ждем пока все синхронизируется, либо идем опять основную БД, тем самым порождая распределенный монолит

      Что касается реплики и кеша вместе - то тут нужно чтобы TTL в Redis, покрывал синхронизацию мастера и реплики (То есть удаляя запись из Кэша, мы должны быть уверены что она появилась в реплике)

      Либо Вообще сделать отдельный сервис на преобразование длинного в короткий ( сохранение в свое БД) и отдельный сервис для получения длинного из короткого (получаем из Кэша) - например как Bit.ly , но тогда все равно есть Кафка, Кэш-воркеры и периодические запросы основной БД.

      Как вы видите такие рассуждения выбиваются из общей тематики статьи, поэтому и не подсвечиваются.


      1. tkutru
        11.07.2025 19:56

        Если система изначально позиционируется как очень простая, то и монолит можно было не распределять. Притом бд общая. Микросервисы в данном случае тем более будут overkill. Имхо.


  1. dph
    11.07.2025 19:56

    А у автора есть реальный опыт проектирования и защиты сайзинга для сколь-нибудь сложных систем? А то тут что не пункт - то ошибка проектирования (


    1. trump-card
      11.07.2025 19:56

      Осуждаешь? Предлагай! Распиши ошибки и свой вариант решения!


      1. nin-jin
        11.07.2025 19:56

        Пишем простейший сервис, использующий децентрализованную конвергентную базу данных. Поднимаем его на нужном числе узлов в разных дата-центрах. Балансируем нагрузку через dns. Всё. Никаких Кафок, Редисов, Постгресов, балансировщиков, микросервисов и прочих звездолётов, скрепленных изолентой. При этом получаем синхронизацию узлов в реальном времени, устойчивость к разделению сети, неограниченное горизонтальное масштабирование, автоматическое восстановление при сбоях, локальную авторизацию, шаринг прав, и отработку каждого запроса за миллисекунды.


  1. Fafhrd
    11.07.2025 19:56

    Если да, то Link service , сразу возвращает это корочку ссылку. Если нет, то он обращается к Shortener service, для генерации короткой ссылки.

    Привет отслеживанию переходов и тестированию эффективности информационных площадок.