📱

Get Our Mobile App

Take your business learning on the go!

Download on the App StoreGet it on Google Play

The Complete System Design Course (CAP Theorem, Databases, Caching, Kafka, Redis and More)

The Coding Camp56:27

Transcription

Системное проектирование — один из самых ценных навыков, который может развить инженер-программист. Готовитесь ли вы к собеседованиям по системному проектированию, стремитесь к старшим инженерным должностям или просто интересуетесь тем, как работают крупномасштабные распределенные системы, этот курс даст вам необходимые знания и структуру. Мы начнем с фундаментальных концепций, которые должен понимать каждый инженер, включая масштабируемость, доступность, надежность и теорему CAP. Оттуда мы рассмотрим основы сетевых технологий, которые обеспечивают работу современных приложений, такие как HTTP, HTTPS и DNS. Далее мы углубимся в хранение данных, изучая как SQL, так и NoSQL базы данных. Вы узнаете различия между горизонтальным и вертикальным масштабированием и откроете для себя, как реальные системы используют балансировщики нагрузки, кэширование, очереди сообщений и ограничение скорости для эффективной обработки миллионов пользователей и запросов. Мы также представим некоторые из наиболее широко используемых технологий в современных системных архитектурах, включая Redis, MySQL, DynamoDB, Kafka и Apache Spark, и обсудим, где они вписываются в крупномасштабные системы. Построив прочный фундамент, мы объединим все с помощью практической структуры собеседования по системному проектированию. Вы научитесь уточнять требования, оценивать масштаб, проектировать архитектуры, оценивать компромиссы и эффективно доносить свои решения во время собеседований. Наконец, вы примените все, что узнали, на полных примерах системного проектирования, где мы с нуля спроектируем реальные системы, такие как Uber и Bit.ly. К концу этого курса у вас будет твердое понимание строительных блоков масштабируемых распределенных систем и проверенный подход к уверенному прохождению собеседований по системному проектированию. Итак, если вы готовы повысить свой уровень навыков системного проектирования, давайте начнем. Давайте начнем с понимания того, что подразумевается под системным проектированием. Определение системного проектирования — это процесс планирования того, как различные части программной системы работают вместе. Представьте себе это. Вы создали простой веб-сайт, возможно, сокращатель URL-адресов, такой как Bitly. Сначала он идеально работал для 10 пользователей, но затем внезапно появилось 10 000 пользователей, затем 1 миллион, и ваш веб-сайт рухнул. Это потому, что ваша система не была спроектирована для масштабирования. Вот почему системное проектирование важно. Как разработчики, мы не просто пишем код. Мы строим системы, от которых зависят люди. При проектировании систем мы должны учитывать проектирование надежной системы, которая может легко масштабироваться, обрабатывать большие объемы данных и легко восстанавливаться после сбоев. Основными строительными блоками любой системы являются клиент. Это может быть веб-, настольное или мобильное приложение, например. Сервер, здесь выполняется код приложения. База данных, здесь хранятся данные приложения. и сеть. Это определяет, как все эти компоненты взаимодействуют друг с другом. Ваша задача как системного проектировщика — эффективно соединить все эти компоненты. Вы должны подходить к любому собеседованию по системному проектированию, следуя этим пяти шагам. Уточнить требования, определить масштаб, предложить высокоуровневый дизайн, затем углубиться в некоторые компоненты, а затем обсудить компромиссы. Давайте поговорим о каждом шаге более подробно. Во-первых, уточнение требований. Существует две категории требований. Функциональные и нефункциональные требования. Функциональные требования — это то, что делает система. Например, для приложения чата отправка сообщений и уведомлений являются функциональными требованиями, в то время как нефункциональные требования определяют, как система должна вести себя. Например, производительность, масштабируемость и задержка. Следующий шаг — определение масштаба системы. Задавайте интервьюерам вопросы, такие как количество активных пользователей в день, количество запросов в секунду и объем данных. Это повлияет на решения, которые вы примете позже при проектировании системы. Затем предложите высокоуровневый дизайн. Сохраняйте его простым. Пока не углубляйтесь ни в какие компоненты. Просто убедитесь, что этот дизайн соответствует функциональным требованиям. Затем углубитесь в некоторые компоненты. Если интервьюер явно не попросил вас углубиться в определенную область, выберите любой компонент или тему, с которой у вас больше опыта, и углубитесь в нее. Например, выбор базы данных, стратегия кэширования или стратегия шардинга. Наконец, обсудите компромиссы. Это очень важный сигнал о вашем опыте для интервьюера. Объясните, почему вы приняли определенные решения, каковы альтернативы и каковы плюсы и минусы каждого выбора. Масштабируемость — это способность системы справляться с большей работой или ростом без сбоев или слишком сильного замедления. Проще говоря, если ваше приложение внезапно получит больше пользователей, данных или запросов, сможет ли оно по-прежнему работать хорошо? Масштабируемость очень важна при проектировании систем. Она обеспечивает устойчивость и адаптивность. Без нее системы страдают от простоев, высокой задержки или снижения производительности во время высокой активности. Существует в основном два типа масштабирования системы. Горизонтальное и вертикальное масштабирование. Вертикальное масштабирование означает повышение мощности одной машины. Например, обновление процессора, оперативной памяти или хранилища. Горизонтальное масштабирование означает добавление большего количества машин вместо обновления одной. Мы узнаем больше об обоих подходах и о том, как выбирать между ними, позже в курсе. Надежность в системном проектировании относится к способности системы последовательно выполнять свою предполагаемую функцию правильно и без сбоев в течение определенного периода времени, даже в изменяющихся условиях. Надежность важна в системном проектировании, поскольку она обеспечивает непрерывную работу. Надежные системы избегают неожиданных сбоев. Это критически важно для таких услуг, как банковское дело и здравоохранение, где простои могут нарушить работу миллионов пользователей. Это также повышает доверие и опыт пользователей, снижает затраты на сбои и поддерживает масштабируемость. Система, которая не является надежной, будет испытывать трудности по мере роста. Таким образом, надежность гарантирует, что производительность остается стабильной даже при увеличении спроса. Надежные системы часто проектируются с избыточностью, резервным копированием и механизмами отработки отказа, чтобы они могли продолжать работать даже при сбоях отдельных частей. Доступность в системном проектировании относится к способности системы быть оперативной и доступной, когда она нужна пользователям. Обычно она выражается в процентах времени безотказной работы за определенный период времени. Например, система с 99,9% доступностью, также известная как три девятки, простаивает лишь небольшое количество времени в год. И не недооценивайте десятичные числа. 99% доступность означает, что система простаивает 3,75 часа в год, что неприемлемо для большинства систем. Некоторые из методов достижения доступности — это репликация, балансировка нагрузки, отказ и проверки работоспособности. Так что мы узнаем об этих методах подробно позже в курсе. Представьте, что вы используете банковское приложение. Вы переводите 100 долларов со своего сберегательного счета на свой текущий счет. Сразу после проверки баланса на телефоне отображается обновленный баланс. В то время как на вашем ноутбуке все еще отображается старый баланс. Теперь вы не уверены, произошла ли транзакция на самом деле. Именно такую проблему пытается решить согласованность в распределенных системах. В крупномасштабных системах данные хранятся на нескольких машинах. Сохранять все копии идеально синхронизированными не так просто, как кажется. При проектировании систем вы постоянно сталкиваетесь с компромиссами. Должны ли пользователи всегда видеть последние данные, или допустимо, чтобы данные были немного устаревшими для лучшей производительности? Понимание согласованности в теореме CAP помогает вам принимать эти решения разумно. Согласованность означает, что каждое чтение получает последнее обновление или ошибку. Проще говоря, все пользователи видят одни и те же данные в одно и то же время. Существует три типа согласованности. Строгая согласованность, которая означает, что каждое чтение получает последнее значение, и никогда нет устаревших данных. Некоторые примеры — банковские и платежные системы. Недостатком строгой согласованности является то, что она медленная и имеет более высокую задержку. Затем конечная согласованность, которая означает, что данные могут быть временно несогласованными, но в конечном итоге все синхронизируется. Некоторые примеры — социальные сети и DNS-резолверы. Недостатком является то, что пользователи будут видеть устаревшие данные кратко. затем слабая согласованность, которая не гарантирует, когда обновления будут распространяться. Это редко используется в критически важных системах. Давайте разберемся, что подразумевается под доступностью в распределенных системах. Доступность означает, что каждый запрос получает ответ, даже если ответ не полностью актуален. Система никогда не говорит «Я не могу ответить». Она может вернуть устаревшие или кэшированные данные, но всегда отвечает. Системы достигают доступности за счет репликации, которая означает хранение нескольких копий одних и тех же данных в нескольких базах данных. Если одна выходит из строя, другая отвечает. Недостатком этого подхода является то, что при обновлении данных требуется время для обновления данных во всех экземплярах. Поэтому иногда пользователи будут читать устаревшие данные. Кэширование также повышает доступность, извлекая данные из памяти, а не из базы данных. Современные приложения работают на нескольких серверах в разных местах. Но сети не идеальны. Иногда соединения выходят из строя или регионы отключаются. Если ваша система не может справиться с этим, она выходит из строя или ведет себя непредсказуемо. Отказоустойчивость означает, что система продолжает работать, даже когда между частями системы возникает сбой связи. Естественный раздел происходит, когда некоторые серверы не могут общаться с другими. Таким образом, система разделяется на изолированные группы. Естественные разделы неизбежны. Поэтому реальный вопрос не в том, произойдут ли разделы, а в том, как ваша система будет вести себя, когда они произойдут. Теорема CAP гласит, что в распределенной системе вы можете гарантировать только два из этих трех свойств одновременно. Согласованность, доступность и отказоустойчивость. Отсюда и название теорема CAP. Первая буква каждого из этих слов. Мы уже узнали, что означает каждое из них. Но давайте еще раз посмотрим на их определения. Согласованность означает, что каждое чтение получает последнее обновление или ошибку. Проще говоря, все пользователи видят одни и те же данные в одно и то же время. Доступность означает, что каждый запрос получает ответ без гарантии, что это последние данные. Проще говоря, система всегда отвечает, даже если данные немного устарели. Отказоустойчивость означает, что система продолжает работать, даже если части ее не могут общаться. В реальном мире сбои сети будут происходить. Никуда не деться. Поэтому отказоустойчивость не подлежит обсуждению. Вам придется выбирать между согласованностью и доступностью. Система не может одновременно обладать согласованностью и доступностью при отказоустойчивости, потому что во время сетевого раздела требования согласованности и доступности напрямую конфликтуют. Представьте, что данные системы разделены на два сервера. Затем произошел сетевой раздел. Теперь серверы не могут общаться. Затем пользователь обновил значение, скажем, x с 50 до 100 на сервере A. Теперь другой пользователь читает это значение с сервера B. Если система согласована, это означает, что сервер B должен вернуть последнее значение, которое составляет 100. А если система доступна, это означает, что она возвращает любое значение, независимо от того, является ли оно последним или нет. Поскольку существует сетевой раздел, сервер B может либо вернуть 50, что делает систему доступной, но не согласованной, либо вернуть ошибку, что делает систему согласованной, но не доступной. Каждый раз, когда вы проектируете систему, вам приходится думать о компромиссах между согласованностью и доступностью и выбирать соответствующим образом. Давайте разберемся с основным протоколом связи HTTP и разницей между HTTP и HTTPS. HTTP, что является аббревиатурой от hypertext transfer protocol (протокол передачи гипертекста), является основным протоколом, используемым для связи между клиентом и сервером. Он определяет, как структурируются запросы и ответы. Проблема с HTTP заключается в том, что он не безопасен. Данные отправляются в виде обычного текста. И именно это решает HTTPS. HTTPS является аббревиатурой от hypertext transfer protocol secure (безопасный протокол передачи гипертекста). Так что это, по сути, HTTP плюс шифрование. Протокол шифрования — SSL/TLS. Данные шифруются, что предотвращает чтение или изменение данных злоумышленниками. Давайте поговорим о разрешении DNS, которое является чем-то, что обеспечивает почти все, что вы делаете в Интернете, но часто невидимо. Каждый раз, когда вы вводите URL-адрес в свой браузер, вашей системе нужно выяснить, какой IP-адрес у этого домена. Этот процесс называется разрешением DNS. Начнем с простого вопроса. Почему бы нам просто не использовать IP-адреса вместо доменных имен? Потому что доменные имена легче запомнить, чем числа. Кроме того, IP-адреса могут меняться, а доменные имена — нет. DNS расшифровывается как domain name system (система доменных имен), и это распределенная база данных, которая сопоставляет доменные имена с их IP-адресами. Давайте пройдемся по тому, что происходит, когда вы вводите URL-адрес в свой браузер. Сначала браузер проверяет локальный кэш. Если сопоставление не найдено, операционная система проверяет кэш DNS. Если кэширования нет, запрос отправляется DNS-резолверу, и DNS-резолвер возвращает IP-адрес браузеру. Затем веб-сайт загружается. SQL является аббревиатурой от structured query language (язык структурированных запросов), и это язык для общения с реляционными базами данных. Давайте сначала разберемся, что такое реляционные базы данных. Реляционные базы данных организуют данные в таблицы. Таблицы состоят из строк. Каждая строка представляет собой запись. Например, в таблице пользователей строка представляет одного пользователя. А столбцы представляют атрибуты. Например, электронная почта и имя. В реляционных базах данных несколько таблиц в одной базе данных могут иметь отношения, отсюда и название «реляционные». Например, представьте базу данных, состоящую из таблиц пользователей и постов. Каждый пост принадлежит пользователю. Так как же узнать, какой пост принадлежит какому пользователю? Добавив идентификатор пользователя к каждому посту. Теперь существует связь между таблицами постов и пользователей. В этом случае поле user_id в таблице постов называется внешним ключом. А поскольку это уникальный идентификатор для каждой строки в таблице пользователей, он называется первичным ключом для таблицы пользователей. SQL — правильный выбор, если система имеет много отношений. Недостатком SQL-баз данных является сложность горизонтального масштабирования и жесткая или негибкая схема. Некоторые популярные SQL-базы данных — MySQL и PostgreSQL. NoSQL является аббревиатурой от not only SQL (не только SQL), это базы данных, которые хранят и извлекают данные способами, оптимизированными для масштабируемости, скорости и гибкости, а не только структурированные таблицы. В отличие от SQL-баз данных, они не имеют фиксированной схемы данных и могут легко масштабироваться горизонтально. И в отличие от SQL-баз данных, между таблицами нет отношений. Недостатком этого является большое количество дублирующихся данных. Например, если есть таблица пользователей и таблица постов, вы не можете объединить таблицу постов с таблицей пользователей по user_id, как в SQL-базах данных. Вместо этого вам придется добавить всю запись пользователя к каждой записи поста, что приводит к дублированию данных и сложности обновления базы данных. Если вы хотите обновить конкретного пользователя, вам придется обновить его во всех записях, где он присутствует. NoSQL — лучший выбор, чем SQL, если вам нужно масштабироваться или если структура данных не фиксирована. Существуют различные типы NoSQL-баз данных. Хранилища ключ-значение, документоориентированные базы данных, ширококолоночные хранилища и графовые базы данных. Мы узнаем о каждом типе более подробно в следующих уроках. Если вы когда-либо работали с JSON API, вы уже на полпути к пониманию документоориентированных баз данных. Они хранят данные в очень похожем формате. Документоориентированная база данных — это тип NoSQL-базы данных, которая хранит данные в виде документов, обычно в таких форматах, как JSON, BSON или XML. Каждый документ является самостоятельной единицей данных. По сравнению с SQL-базами данных, каждый документ похож на строку в таблице SQL-базы данных. Например, документ может представлять пользователя в базе данных, а группа документов называется коллекцией. В мире SQL это похоже на таблицу. Документоориентированные базы данных имеют гибкую схему. Каждый документ может иметь разную структуру. Документоориентированные базы данных обычно используются, если схема данных гибкая и требуется высокая горизонтальная масштабируемость. Недостатком документоориентированных баз данных является дублирование данных и сложность обновления документов. Некоторые популярные документоориентированные базы данных — MongoDB, CouchDB и AWS DocumentDB. По мере роста систем они неизбежно сталкиваются с фундаментальной проблемой. Как нам обрабатывать больше пользователей, больше данных и больше трафика? Масштабирование — это ответ. Но то, как вы масштабируетесь, кардинально влияет на производительность, стоимость и сложность. Давайте начнем с понимания того, что такое масштабирование. Масштабирование — это процесс увеличения пропускной способности системы для обработки нагрузки. Существует два способа масштабирования систем: вертикальное и горизонтальное масштабирование. Вертикальное масштабирование означает увеличение мощности одной машины. Например, обновление процессора, оперативной памяти или жесткого диска. Преимущество вертикального масштабирования в том, что оно простое и легко достигается. Недостатком является ограничение оборудования. Нет возможности масштабироваться бесконечно, и система по-прежнему является единой точкой отказа. Горизонтальное масштабирование означает добавление большего количества машин для распределения нагрузки. Запросы распределяются между серверами с помощью балансировщика нагрузки. Мы узнаем больше о балансировщиках нагрузки и о том, как они решают, какой запрос куда отправлять, позже в курсе. Преимущества горизонтального масштабирования — практически неограниченная масштабируемость, отказоустойчивость, поскольку если одна машина выходит из строя, другая машина может обрабатывать запросы, и высокая доступность. Недостатки — сложность проектирования и проблемы согласованности данных. Представьте, что вы создали веб-приложение, которое внезапно стало популярным. Сначала один сервер справляется со всеми входящими запросами без проблем. Но по мере роста трафика время отклика замедляется, и сервер перегружается. Вот где вступает в игру балансировка нагрузки. Балансировка нагрузки — это процесс распределения входящего сетевого трафика между несколькими серверами, чтобы гарантировать, что нет одного перегруженного сервера и что система высокодоступна. Балансировщик нагрузки находится между клиентами и серверами и решает, куда должен идти каждый запрос. Когда поступает запрос, балансировщик нагрузки получает его, затем выбирает сервер назначения с помощью стратегии, затем перенаправляет запрос, затем сервер обрабатывает его и возвращает ответ. Теперь вопрос в том, как балансировщик нагрузки узнает, на какой сервер должен идти запрос? Существует несколько стратегий для этого, и именно этому мы научимся на следующем уроке. Представьте, что вы создаете популярное приложение, скажем, платформу социальных сетей. Каждый раз, когда пользователь обновляет свою ленту, ваша система запрашивает базу данных, обрабатывает логику и возвращает результаты. Теперь умножьте это на миллионы пользователей. Без оптимизации ваша система становится медленной, дорогой и перегруженной. Вот где вступает в игру кэширование. Кэширование — это процесс хранения часто используемых данных в быстром временном хранилище. Таким образом, будущие запросы могут быть обслужены быстрее. Основной поток: клиент запрашивает данные. Приложение проверяет, найдены ли они в кэше, что называется попаданием в кэш. Затем оно извлекает их и возвращает клиенту. В противном случае, если их нет в кэше, что называется промахом кэша, приложение извлекает их из базы данных, сохраняет в кэше и возвращает клиенту. Преимущества кэширования — более быстрое время отклика. Поскольку кэш является хранилищем в памяти, он намного быстрее баз данных. Снижение нагрузки на базу данных, поскольку меньше запросов поступает в базы данных. Улучшенная масштабируемость, поскольку система может обрабатывать больше пользователей без масштабирования базы данных, и экономическая эффективность, поскольку требуется меньше вычислительных ресурсов и использования базы данных. Существуют различные типы кэширования, включая кэширование на стороне клиента, которое хранится в браузере или мобильном приложении, CDN или сеть доставки контента, которая кэширует статический контент географически, и кэширование на стороне сервера, которое хранится на уровне сервера и является наиболее распространенным типом в системном проектировании. Мы узнаем больше о каждом типе далее. Очереди сообщений являются одним из наиболее важных строительных блоков в масштабируемых распределенных системах. Они помогают системам справляться с всплесками трафика, разделять службы и масштабироваться независимо. Очередь сообщений — это механизм связи, где одна служба отправляет сообщение, а другая служба получает и обрабатывает его позже. Думайте об этом как о почтовом ящике. Вы кладете письмо в почтовый ящик. Почтальон забирает его позже. Получатель читает его позже. Вам не нужно, чтобы отправитель и получатель были активны одновременно. Именно это и нужно асинхронным системам. Это просто то, как работает очередь сообщений. Производитель отправляет сообщения в очередь. Очередь хранит сообщения, а потребители обрабатывают эти сообщения из очереди. Сообщение потребляется один раз. Это означает, что после того, как потребитель обработал его, оно удаляется из очереди. Так зачем нам нужны очереди сообщений? Представьте себе интернет-магазин. Клиент размещает заказ. Сразу после размещения заказа должно произойти множество задач. Например, отправка подтверждающего письма, уменьшение запасов, уведомление склада и многое другое. Если веб-сервер выполняет все задачи синхронно, пользователь ждет несколько секунд, что приводит к плохому пользовательскому опыту. Вместо этого веб-сервер может мгновенно сохранить заказ, поместить оставшиеся задачи в очередь сообщений, вернуть ответ пользователю, а затем фоновые рабочие процессы могут обрабатывать задачи из очереди. Это быстрее и масштабируемее. И это основная идея очередей сообщений. Представьте, что вы открыли кофейню. Бизнес идет хорошо. Люди заходят, заказывают кофе, и вы и ваша команда гладко обслуживаете каждого клиента. Но однажды утром подъезжает автобус с туристами, и внезапно 300 человек одновременно входят в вашу дверь. Ваши эспрессо-машины перегружены. Ваш персонал не успевает. Заказы начинают накапливаться. Качество падает. Некоторые клиенты вообще не обслуживаются. Несколько постоянных клиентов, которые просто хотели свой обычный утренний кофе, уходят, потому что очередь слишком длинная. Теперь вот в чем дело. Ваша кофейня не изменилась. Пространство, пространство то же самое. Машины те же. Персонал тот же. Проблема в том, что входящий спрос превысил пропускную способность системы за очень короткий промежуток времени. Именно такую проблему решает ограничение скорости в программных системах. Так что же такое ограничение скорости? Ограничение скорости — это метод, используемый для контроля того, сколько запросов пользователь, клиент или система могут сделать к службе в течение определенного периода времени. Это основное определение. Если пользователю разрешено делать 100 запросов к API в минуту, и он делает 101-й запрос в течение той же минуты, система отклонит этот запрос. Она не будет его обрабатывать. Обычно она возвращает ответ об ошибке, сообщая вызывающему абоненту замедлиться и попробовать позже. Теперь, почему это важно? Есть четыре основные причины. Первая — защита ресурсов. Серверы имеют ограниченные ресурсы ЦП, памяти и соединения с базами данных. Неограниченный трафик может полностью исчерпать эти ресурсы и вывести ваш сервис из строя для всех. Ограничение скорости удерживает спрос в пределах того, что может выдержать ваша инфраструктура. Вторая — предотвращение злоупотреблений. Автоматизированные скрипты могут тысячи раз в секунду обращаться к вашему API для сбора данных, подбора паролей или проведения атак типа «отказ в обслуживании». Ограничение скорости значительно увеличивает стоимость такого злоупотребления. Третье — справедливость. В любой общей системе один тяжелый пользователь может потреблять большую часть пропускной способности и ухудшать работу для всех остальных. Ограничение скорости распределяет ресурсы более справедливо. Четвертое — контроль затрат. Ошибка, вызывающая бесконечный цикл, или недобросовестный клиент могут генерировать огромные счета, если вы платите за использование. Ограничение скорости ограничивает это воздействие. В современном системном проектировании скорость имеет значение. Приложения часто становятся медленными, потому что базы данных основаны на дисках и требуют дорогостоящих операций чтения и записи. Redis решает эту проблему. Redis — это база данных ключ-значение с открытым исходным кодом в памяти. Он используется для кэширования, управления сеансами, аналитики в реальном времени, очередей сообщений, таблиц лидеров и распределенных блокировок. Он чрезвычайно быстр, потому что данные хранятся в основном в ОЗУ. Он поддерживает строки, списки, хэши, наборы, отсортированные наборы, потоки, битовые карты и гиперлогарифмы. Все в Redis хранится в виде пар ключей и значений. Некоторые из распространенных типов данных Redis — строки. Это пример хранения и извлечения строкового значения. Обычно используется для кэширования счетчиков и токенов. Затем списки, хэши, которые в основном используются для хранения объектов, наборы и отсортированные наборы, которые обычно используются для хранения таблиц лидеров. Redis основан на памяти, но данные все равно могут быть сохранены путем периодического сохранения снимков на диск. Кэширование — очень распространенный сценарий использования Redis. Вместо постоянного чтения из базы данных часто запрашиваемые данные обслуживаются мгновенно. Одной из наиболее широко используемых реляционных баз данных является MySQL. Популярные компании и платформы широко использовали MySQL, потому что она проста, масштабируема и проверена в бою. MySQL является открытым исходным кодом, реляционной, основанной на SQL, быстрой и надежной. MySQL хранит данные в базах данных. Каждая база данных состоит из таблиц, а каждая таблица состоит из строк и столбцов. Каждая строка — это запись, а каждый столбец представляет атрибут. MySQL является удобным выбором, если система требует согласованности данных, надежности, транзакций и если существует много отношений между данными. Некоторые распространенные сценарии использования — учетные записи пользователей, платежи, системы инвентаризации, управление заказами, банковские системы и корпоративные приложения. В современном системном проектировании приложения нуждаются в базах данных, которые автоматически масштабируются, обрабатывают огромный трафик и остаются высокодоступными в разных регионах. Традиционные реляционные базы данных часто испытывают трудности с этими требованиями в масштабе Интернета. Именно здесь становится важной NoSQL база данных, такая как AWS DynamoDB. DynamoDB — это полностью управляемая база данных ключ-значение, созданная для высокой масштабируемости и низкой задержки, и она полностью управляется AWS. Это означает, что управление инфраструктурой не требуется. Она горизонтально масштабируема и распределена по умолчанию. В отличие от SQL-баз данных, DynamoDB не имеет соединений и сложных реляционных схем. Вместо этого она ориентирована на масштабируемость и скорость. Вместо таблиц с отношениями, DynamoDB использует таблицы, элементы и атрибуты. DynamoDB автоматически распределяет данные по разделам. Это обеспечивает горизонтальное масштабирование, высокую пропускную способность и параллельное чтение и запись. Ключ раздела определяет, где хранятся данные и какой узел обрабатывает запросы. AWS масштабируется автоматически. Таким образом, это удобный выбор, если трафик непредсказуем или требуется быстрое масштабирование. В современных распределенных системах службы постоянно обмениваются данными. Если службы общаются напрямую друг с другом, системы становятся тесно связанными и трудными для масштабирования. Именно здесь помогает Kafka. Apache Kafka — это распределенная платформа потоковой передачи событий, используемая для обмена сообщениями между службами, конвейеров данных в реальном времени, событийно-ориентированных систем, агрегации журналов и потоковой обработки. Без Kafka службы общались бы напрямую, что делает службы тесно связанными и делает систему очень трудной для масштабирования. С Kafka производители отправляют сообщения, также известные как события. Kafka их хранит, а потребители их читают. Это делает систему более масштабируемой и слабо связанной. Производители и потребители могут масштабироваться независимо. Kafka организует сообщения по категориям, называемым темами. Думайте о теме как о таблице базы данных для событий, а темы разделены на разделы. Причина этого — ввести параллелизм, упростить масштабирование и увеличить пропускную способность. Каждое сообщение внутри раздела получает уникальный номер, называемый смещением, и потребители отслеживают смещения, чтобы знать, что они уже обработали. Каждый сервер Kafka называется брокером, а каждый кластер Kafka содержит несколько брокеров. Spark — это распределенный вычислительный движок, созданный для обработки огромных наборов данных на кластерах машин. Основная идея Spark проста. Вы пишете программу, которая описывает, что вы хотите сделать с вашими данными. Spark выясняет, как эффективно распределить эту работу между множеством машин. Таким образом, вместо того, чтобы думать: «Как мне распараллелить это?», «Как мне координировать потоки?», «Как мне разделить работу между серверами?» или «Как мне восстановиться после сбоев машин?», вы просто определяете преобразования данных, а Spark обрабатывает параллелизм и распределенную обработку данных. До Spark крупномасштабная распределенная обработка в основном выполнялась Apache Hadoop: HDFS для хранения и MapReduce для вычислений. MapReduce работал, но имел серьезные ограничения. Каждый этап обработки требовал чтения с диска, обработки и записи обратно на диск. Это повторяющееся дисковое ввод-вывод стало чрезвычайно дорогим. Spark изменил правила игры, представив обработку в памяти, что привело к ускорению примерно в 10 раз даже при работе с дисковыми нагрузками. Давайте поговорим об архитектуре Spark. На высоком уровне Spark состоит из программы драйвера, менеджера кластера, рабочих узлов, исполнителей и задач. Программа драйвера — это приложение, которое вы пишете, обычно на Scala, Python, Java или SQL. Она отвечает за преобразование данных и координацию исполнителей. Вы можете думать о драйвере как о мозге приложения Spark. Драйвер взаимодействует с менеджером кластера, который отвечает за выделение ЦП и памяти, а также за планирование задач. Spark поддерживает сторонние менеджеры кластера или собственный автономный менеджер кластера Spark. Рабочие узлы — это фактические машины в кластере. Это могут быть физические серверы или экземпляры EC2. Например, каждый рабочий узел запускает один или несколько исполнителей. И, наконец, задачи — это наименьшая единица работы в Spark. Если ваш набор данных имеет 100 разделов, Spark может создать 100 задач. Каждая задача обрабатывает один раздел независимо. Этот параллелизм — то, что обеспечивает скорость Spark. Когда кандидаты испытывают трудности на собеседовании по системному проектированию, это часто не потому, что им не хватает технических знаний. Это потому, что они начинают проектировать слишком рано. Сильный системный проектировщик не сразу переходит к базам данных, API или микросервисам. Вместо этого они сначала уточняют, что должна делать система. Этот шаг называется уточнением функциональных требований. И это то, что вы должны сделать в первую очередь в начале собеседования. Если вы неправильно поймете требования к продукту, даже технически впечатляющая архитектура может стать совершенно неактуальной. Функциональные требования описывают, что должна делать система. Это все примеры функциональных требований. Как вы видите, они определяют основные поведения и возможности продукта. Они фокусируются на функциях продукта и действиях пользователя. Давайте посмотрим на пример. Представьте, что интервьюер говорит: «Спроектируйте YouTube». Тогда функциональными требованиями могут быть: пользователи могут загружать видео, смотреть видео, искать видео и подписываться на каналы. Начало собеседования с уточнения функциональных требований помогает вам сузить область, продемонстрировать продуктовое мышление, поскольку интервьюеры оценивают, думаете ли вы как инженер, решающий бизнес-задачи, а не просто как человек, рисующий диаграммы, и построить правильную архитектуру. На предыдущей лекции мы узнали, как уточнять функциональные требования. Следующий шаг — уточнить нефункциональные требования системы. Это одна из самых важных частей собеседования по системному проектированию. Две системы могут поддерживать абсолютно одинаковые функции. Тем не менее, одна прекрасно работает при огромном масштабе, а другая постоянно выходит из строя. Почему? Из-за нефункциональных требований. Функциональные требования отвечают на вопрос, что должна делать система, а нефункциональные требования отвечают на вопрос, насколько хорошо она должна это делать. К ним относятся такие вещи, как масштабируемость, надежность, задержка, доступность, согласованность, безопасность, долговечность и экономическая эффективность. Представьте себе этот вопрос на собеседовании: «Спроектируйте YouTube без уточнения нефункциональных требований». Вы можете спроектировать небольшой внутренний учебный портал или глобальную видеоплатформу, обслуживающую миллиарды пользователей. Совершенно разные архитектуры. Интервьюер ожидает, что вы зададите вопросы, такие как: «Сколько пользователей?», «Каков ожидаемый трафик?», «Система больше ориентирована на чтение или на запись?», «Требования реального времени?», «Каковы ожидания по доступности?». Эти ответы формируют весь дизайн. Основные категории нефункциональных требований: масштабируемость. Это отвечает на вопрос: может ли система справиться с ростом, ростом числа пользователей, трафика и данных? Некоторые примеры вопросов: «Сколько активных пользователей в день?», «Сколько запросов в секунду?», «Каково соотношение чтения/записи?», «Сколько данных хранится?». Высокий трафик означает больше кэширования, огромные наборы данных означают шардинг. Глобальные пользователи означают использование CDN, а массовые записи означают распределенные базы данных. Следующая категория — доступность. Это означает, как часто система работает, и обычно выражается в процентах времени безотказной работы. Вот несколько примеров. Высокая доступность обычно подразумевает репликацию, избыточность, балансировщики нагрузки, несколько центров обработки данных, проверки работоспособности и системы отработки отказа. Следующая категория — надежность. Это означает, ведет ли система себя правильно с течением времени. Надежная система избегает потери и повреждения данных и всегда дает правильные результаты. Следующая категория — задержка и производительность, что означает, насколько быстро должна отвечать система. Например, система автодополнения может допускать задержку, в то время как система конвейера аналитики не может. Более низкая задержка часто требует кэширования и предварительных вычислений. Следующая категория — компромиссы. Вам не нужна глубокая теория немедленно, но интервьюеры ожидают осведомленности. Затем географическое распределение. Вы должны уточнить, доступна ли система глобально или локально. Это важно, поскольку глобальные системы могут нуждаться в CDN и георепликации. После уточнения функциональных и нефункциональных требований пришло время определить масштаб системы и провести некоторые приблизительные оценки. Масштаб имеет значение, потому что разные системы требуют разных архитектур, поскольку они работают в разных масштабах. Без понимания масштаба вы не сможете правильно выбрать базы данных, стратегии кэширования, балансировку нагрузки и методы репликации. Основные метрики масштаба в системном проектировании — это ежедневные активные пользователи, запросы в секунду, количество чтений, количество записей, общий объем хранимых данных и использование сети. Начните с оценки количества ежедневных активных пользователей. Затем оцените поведение пользователей. Например, каждый пользователь создает три поста в день и читает 100 постов в день. Затем оцените трафик чтения и записи. Например, общее количество записей в день равно количеству ежедневных активных пользователей, умноженному на количество постов, которые каждый пользователь создает в день. То же самое для чтений. Для пиковых оценок умножьте число на пять. Если чтений намного больше, чем записей, это указывает на необходимость интенсивного кэширования. Затем оцените хранилище. Например, размер каждого поста составляет 300 байт. Поскольку общее количество постов в день составляет 300 миллионов, общий объем хранилища в день составляет 90 ТБ, а годовое хранилище — это число, умноженное на 365. Это помогает определить масштабирование базы данных, стратегии шардинга и резервного копирования. Затем оцените пропускную способность. Например, размер ответа составляет 5 килобайт. Умножьте это на пиковое количество ежедневных чтений. Большие требования к пропускной способности могут потребовать CDN или сжатия. Например, после оценки масштаба и пропускной способности системы предложите первоначальную архитектуру. Убедитесь, что архитектура проста, полна и соответствует функциональным требованиям. Первоначальная архитектура должна отвечать на вопросы: каковы основные компоненты, как пользователи взаимодействуют с системой, где хранятся данные, как службы взаимодействуют, как система масштабируется на базовом уровне. Думайте об этом как о дизайне версии 1.0. Затем позже вы улучшите его, чтобы соответствовать нефункциональным требованиям. Основные компоненты — это клиент, балансировщик нагрузки, сервер приложений и база данных. Для выбора базы данных сохраняйте простоту. Вам не нужна идеальная база данных немедленно. Нулевые базы данных разумны, когда важна строгая согласованность, важны отношения или критически важны транзакции. NoSQL базы данных разумны, если масштаб огромен, требуется гибкость схемы или приоритет отдается доступности. Вам не нужна идеальная база данных немедленно. Например, вы можете сказать: «Сначала я буду использовать PostgreSQL для простоты и согласованности. Если масштаб станет огромным позже, мы можем изучить шардинг или альтернативы NoSQL». Далее идет объяснение потока запросов. Теперь пройдитесь по всему процессу. Это чрезвычайно важно. Интервьюеры хотят видеть, понимаете ли вы, как взаимодействуют компоненты. Например, в системе сокращения URL-адресов вы можете объяснить создание короткого URL-адреса. Пользователь отправляет длинный URL. Балансировщик нагрузки направляет запрос. Сервер приложений генерирует короткий идентификатор. Сохраняет сопоставление в базе данных. Возвращает короткий URL. Следующий шаг — выявление очевидных узких мест. После создания базовой архитектуры немедленно проанализируйте слабые стороны. Спросите себя: «Что ломается первым?», «Что становится медленным?», «Что не может масштабироваться?». Например, одна база данных, что приводит к ограниченной пропускной способности записи и единой точке отказа, или отсутствие кэша, что приводит к повторяющимся дорогостоящим чтениям из базы данных. Теперь переходим к самой важной фазе собеседования — глубокому погружению. Именно здесь сильные кандидаты отделяются от средних. На этом этапе вы должны убедиться, что архитектура системы также соответствует нефункциональным требованиям. Если интервьюер явно не попросил вас углубиться в что-то, вы можете углубиться в любую тему, с которой у вас больше всего опыта. На этом этапе интервьюер теперь хочет оценить вашу глубину понимания, системное мышление, рассуждения о масштабируемости и инженерную зрелость. Теперь переходим к одному из самых важных навыков на собеседованиях по системному проектированию — обсуждению компромиссов. Это разница между тем, кто запоминает архитектуры, и тем, кто действительно думает как старший инженер, потому что в реальной инженерии нет идеального дизайна. Каждое решение имеет преимущества и недостатки, и каждая оптимизация вносит свои затраты где-то еще. Ваша задача на собеседовании по системному проектированию — не создать правильную архитектуру. Ваша задача — обосновать решения, объяснить компромиссы и продемонстрировать инженерное суждение. Именно это оценивают интервьюеры. Например, новичок часто говорит: «Мы должны использовать кэширование». Опытный инженер говорит: «Кэширование улучшает задержку и снижает нагрузку на базу данных, но вносит сложности с инвалидацией кэша и возможные устаревшие данные». Второй ответ демонстрирует понимание. Собеседования по системному проектированию в основном посвящены балансировке конкурирующих целей. Вы постоянно выбираете между производительностью и согласованностью, простотой и масштабируемостью, стоимостью и надежностью, задержкой и долговечностью, доступностью и корректностью. Интервьюеры хотят услышать, что вы получаете, что теряете и почему решение приемлемо. >> Я хочу, чтобы вы спроектировали сервис сокращения URL-адресов, такой как bit.ly. >> Конечно. Сначала я уточню требования. Для функциональных требований я предполагаю, что пользователи могут отправлять длинный URL-адрес и получать короткий URL-адрес. Когда пользователи открывают короткий URL-адрес, они должны быть перенаправлены на исходный URL-адрес. URL-адреса могут истекать опционально. Пользователи могут захотеть пользовательские псевдонимы. Мы должны отслеживать аналитику, такую как количество кликов, временная метка, страна и устройство. >> Да, это так. >> Я сначала проигнорирую аутентификацию и сосредоточусь на основной системе. Теперь нефункциональные требования. Высокая доступность, поскольку перенаправления всегда должны работать. Низкая задержка, поскольку перенаправления должны происходить за миллисекунды. Масштабируемость, поскольку система должна поддерживать огромный трафик. Долговечность, поскольку мы никогда не должны терять сопоставления между короткими и длинными URL-адресами. Оптимизация для чтения, поскольку чтений намного больше, чем записей, потому что перенаправления происходят гораздо чаще, чем создание URL-адресов. >> Хорошо, продолжайте. >> Давайте оценим масштаб. Предположим, 100 миллионов новых URL-адресов в месяц и 10 миллиардов перенаправлений в месяц. Это дает примерно 40 записей в секунду, что очень управляемо, и 4000 перенаправлений в секунду. Пиковый трафик может быть в 10 раз больше. Это явно система, ориентированная на чтение. Теперь давайте оценим хранилище. Каждая запись хранит ключ короткого URL, исходный URL и метаданные. Предположим, 500 байт на запись. Таким образом, 50 ГБ в месяц и 600 ГБ в год. Очень управляемо с шардингом. >> Хорошо. Теперь спроектируйте высокоуровневую архитектуру. >> Я начну с простой архитектуры. Клиент взаимодействует с балансировщиком нагрузки для распределения трафика между горизонтально масштабируемыми серверами, что предотвращает единую точку отказа, а также, поскольку перенаправления очень частые, распределение трафика имеет решающее значение. Затем без состояния серверы приложений, они отвечают за создание коротких URL-адресов, проверку URL-адресов, перенаправление запросов и взаимодействие с базой данных. Для базы данных я бы изначально выбрал MySQL, потому что сопоставления — это простые записи в стиле ключ-значение. Строгая согласованность полезна, а транзакции надежны. Вот как будет выглядеть схема. >> Будет ли NoSQL работать? >> Да, при очень большом масштабе я могу перейти на Apache Cassandra, потому что она имеет чрезвычайно высокую пропускную способность записи, легкое горизонтальное масштабирование и распределенную архитектуру. Но изначально MySQL проще в эксплуатации. Я бы добавил кэш, такой как Redis, поскольку перенаправления ориентированы на чтение. В этом случае также горячие URL-адреса становятся чрезвычайно быстрыми. Для аналитики я бы использовал Kafka, поскольку аналитика не должна замедлять перенаправления, а асинхронная обработка лучше. Сервисы аналитики потребляют события из Kafka и сохраняют их в базе данных аналитики. Я бы выбрал Elasticsearch, поскольку он оптимизирован для быстрого поиска, агрегации и аналитики временных рядов. >> Как вы генерируете короткие URL-адреса? >> Существует несколько вариантов. Вариант 1: хэширование исходного URL с использованием MD5 или SHA-256. Затем взять первые семь символов. Проблема этого подхода в том, что возможны коллизии, и дублирующиеся URL-адреса могут непреднамеренно давать одинаковый результат, что требует обработки коллизий. Вариант 2: автоинкрементный ID + Base62. Это предпочтительный подход. Он детерминированный, легко генерируется и уникален. >> Почему Base62? чтобы создавать более короткие URL-адреса. Безопасные для URL символы и эффективное кодирование. 62 символа обеспечивают 3,5 триллиона URL-адресов, что очень достаточно. Это поток запросов на перенаправление. Пользователь открывает короткий URL. Балансировщик нагрузки направляет запрос. API проверяет адрес. Если попадание в кэш, немедленно перенаправляет. В противном случае запрашивает базу данных, обновляет кэш и перенаправляет пользователя. Затем отправляет аналитическое событие в Kafka. >> Как бы вы масштабировали базу данных? >> В конечном итоге одна база данных становится узким местом. Я бы разделил ее по хэшу короткого ключа. Это приведет к равномерному распределению трафика. Я бы также использовал репликацию первичный-реплика. Первичный обрабатывает записи, а реплики обрабатывают чтения. Это улучшает масштабируемость чтения и отказоустойчивость. >> Что, если один URL станет вирусным? Хороший вопрос. Вирусный URL может перегрузить один короткий Redis, который поглотит большую часть чтений, но мы также можем использовать кэширование CDN. Это значительно снижает нагрузку на источник. Теперь я обсужу некоторые компромиссы в выборе, которые я сделал для архитектуры. Я выбрал MySQL изначально вместо Apache Cassandra для простоты, но она не так легко масштабируется горизонтально. Cassandra добавляет распределенную сложность слишком рано, а конечная согласованность не нужна на ранней стадии. Я хотел сначала простоту и корректность, а масштабирование — позже. Я использовал Redis, поскольку система ориентирована на чтение. Это значительно снижает нагрузку на базу данных, но вносит сложности с инвалидацией кэша и возможные устаревшие данные. Я использовал Apache Kafka для отслеживания кликов. Это позволяет перенаправлениям оставаться быстрыми и обрабатывать большой объем событий асинхронно, но это вызывает конечную согласованность в аналитике и большую сложность инфраструктуры, поскольку аналитика не находится на критическом пути. Так что асинхронность того стоит. Хорошо. Мне нравится, как вы последовательно привязывали каждый компромисс к приоритетам системы, а не просто перечисляли плюсы и минусы. Вы явно сначала оптимизировали для простоты, а затем масштабировали сложность только там, где это было оправдано. Это все от меня. Спасибо. Это было сильное решение. Давайте спроектируем Uber. >> Звучит хорошо. Я начну с функциональных требований. Пассажиры могут заказать поездку. Водители могут выйти в онлайн и принять поездку. Сопоставлять пассажиров с ближайшими водителями и отслеживать местоположение водителя в реальном времени. Для нефункциональных требований: высокая доступность, поскольку пассажиры должны иметь возможность заказывать поездки в любое время. Низкая задержка, поскольку сопоставление водителей должно происходить в течение нескольких секунд. Масштабируемость, поскольку система должна поддерживать миллионы пользователей. Обновления местоположения в реальном времени и согласованность важны для назначения поездок, чтобы избежать назначения нескольких водителей одному пассажиру. Теперь я оценю масштаб. Допустим, 50 миллионов активных пользователей в день, 10 миллионов поездок в день, а средняя продолжительность поездки — 20 минут. Таким образом, имеется 1 миллион одновременных активных поездок. Водители отправляют обновления местоположения каждые 5 секунд. И предположим, имеется 5 миллионов активных водителей. Таким образом, имеется 1 миллион обновлений местоположения в секунду. Это самая требовательная часть системы. >> Хорошо. Теперь спроектируйте первоначальную архитектуру. >> Конечно. Клиенты будут взаимодействовать с API-шлюзом как единой точкой входа для централизации аутентификации, ограничения скорости и маршрутизации. Затем сервис поездок отвечает за переходы состояния поездки, сервис сопоставления, который находит ближайших водителей, сервис местоположения, который хранит местоположения водителей в реальном времени, и сервис уведомлений, который отправляет запросы на поездки и обновления. Я буду хранить метаданные поездок и маршрутов в базе данных MySQL, поскольку MySQL обеспечивает строгую согласованность для записей поездок, и Redis для хранения активных местоположений водителей, поскольку Redis чрезвычайно быстр для геопространственных запросов. >> Давайте углубимся в сопоставление водителей. >> Приложение клиента водителя отправляет GPS-местоположение в Kafka, и сервис местоположения потребляет их. Kafka позволяет системе поглощать всплески без перегрузки последующих служб. Водители непрерывно отправляют GPS-координаты. Сервис местоположения сохраняет их в Redis, используя геопространственные индексы. Когда пассажир запрашивает поездку, сервис поездок создает запрос на поездку. Сервис сопоставления запрашивает у Redis ближайших водителей. Кандидаты-водители ранжируются по расстоянию, доступности водителя и ETA. Сервис уведомлений отправляет запросы. Затем первый принявший водитель подтверждает, и назначение поездки записывается. >> Как вы предотвратите одновременное принятие поездки несколькими водителями? >> Redis может выполнять атомарную операцию. Например, скажем, водитель А принимает поездку. Если сервис сопоставления пытается обновить статус поездки, только одно обновление будет успешным, а остальные подтверждения будут отклонены. Это предотвращает дублирование назначений. >> Как бы вы масштабировали Redis? >> Я бы разделил Redis географически, чтобы сопоставление искало только в пределах региона водителя, уменьшая нагрузку на запросы и задержку. >> Хорошо, отлично. Это все от меня. Если вы нашли этот курс полезным и хотите углубиться, мой полный мастер-класс по собеседованию по системному проектированию включает дополнительные примеры, продвинутые темы распределенных систем, викторины и загружаемые ресурсы. Ссылку вы можете найти в описании. Спасибо и до встречи в следующем.