📱

Get Our Mobile App

Take your business learning on the go!

Download on the App StoreGet it on Google Play

1000+ hours on System Design Explained | System Design Crash Course By Microsoft SDE

Mayank Joshi50:16

Transcription

Всем привет, добро пожаловать на мой YouTube канал. Меня зовут Маянк, и сегодня у нас будет экспресс-курс по проектированию систем. Итак, в этом экспресс-курсе мы не просто пройдемся по списку важных тем. Вместо этого мы построим нашу систему с нуля. То есть, от клиентского и серверного слоя. А затем каждую минуту мы будем ломать ее, добавлять больше компонентов, чтобы сделать ее масштабируемой, надежной. А затем мы снова сломаем ее и снова добавим больше компонентов, и будем делать это до тех пор, пока у нас не появится готовая к продакшену масштабируемая архитектура. Такова цель этого видео, и к концу этого видео у вас будет полное представление о том, как собираются крупномасштабные системы, почему существует каждый компонент, как каждый компонент связан друг с другом, и это действительно поможет вам в проектировании любой масштабируемой архитектуры на вашем следующем собеседовании по проектированию систем. Итак, давайте не будем терять время и начнем с абсолютного нуля, то есть с клиент-серверной архитектуры. Это самая базовая архитектура в мире. Есть клиент и есть сервер. Хорошо, есть клиент и есть сервер. Клиент хочет получить доступ к серверу, но у него есть URL. У него есть URL, например, example.com. Хорошо, у него есть URL example.com, и теперь, если он хочет получить доступ к серверу, что он сделает? Он обратится к DNS-резолверу и скажет: "О, у меня есть URL, и я хочу получить доступ к серверу или удаленной машине, как я могу это сделать?" DNS-резолвер скажет: "О, у вас есть URL, дайте мне URL". Он скажет: "О, это example.com". Он скажет: "Да, это IP-ответ. 192.15.16.28". Это может быть что угодно, хорошо? Или это может быть любой другой IP-адрес, хорошо? Теперь, когда он получил IP-адрес, что он сделает? Он установит TCP-соединение, то есть отправит HTTP-запрос. Он отправит запрос на сервер, и сервер ответит: "О, это ваш ответ". Хорошо? Теперь в реальном мире мы никогда не используем HTTP. HTTP не используется, потому что он небезопасен. Вместо этого мы будем использовать HTTPS. Хорошо. Это одна важная вещь. HTTP не используется ни в одном реальном приложении. Хорошо. Поэтому мы используем только HTTPS. И этот HTTP-запрос или HTTPS-запрос — это TCP-соединение. Теперь это хорошо. Все хорошо. Все работает нормально. Но давайте предположим, что ваше приложение стало популярным. Все, что вы построили, стало популярным, и теперь его посещают 10 000 пользователей. Раньше его посещали два пользователя, три пользователя в день, но теперь его посещают 10 000 пользователей в день. Теперь сервер, сервер недостаточно мощный, чтобы справиться с 10 000 пользователей. Итак, что мы сделаем? Мы добавим больше оперативной памяти. Больше хранилища. Хорошо. Мы сделаем все это. Итак, мы добавим больше оперативной памяти, добавим больше хранилища. И да, мы сможем успешно разместить 10 тысяч человек. Это хорошо. Но теперь оно стало чрезвычайно популярным. Эти 10 тысяч человек рассказали еще 10 тысячам человек и еще 20 тысячам человек, и теперь ваш сайт посещают 1 миллион пользователей. 1 миллион. Теперь, можете ли вы увеличить объем оперативной памяти? Или увеличить объем хранилища? Ну, есть предел тому, насколько вы можете расширить свою машину. Вы не можете бесконечно увеличивать мощность своей машины. Это невозможно. Итак, что вы сделаете? Увеличение оперативной памяти, увеличение хранилища — это хороший вариант, но только до определенного предела. Потому что всегда есть предел тому, сколько машина может выдержать. Вы не можете бесконечно увеличивать любой ресурс. Хорошо. И еще одна проблема в этом — это единая точка отказа. Если сервер выходит из строя, все выходит из строя. Если один сервер выходит из строя, вся служба прекращает работу. Тогда нашим решением является горизонтальное масштабирование. Сначала мы сделали вертикальное масштабирование. Это означает, что мы делаем текущий сервер больше, добавляем больше процессоров, больше оперативной памяти, и это единая точка отказа. Теперь мы сделали горизонтальное масштабирование. Вместо добавления одного большого сервера мы добавили больше обычных серверов. Хорошо, это все обычные серверы. Теперь, если один из серверов выходит из строя, скажем, сервер два вышел из строя. Он мертв. У нас все еще есть сервер один и сервер три для обслуживания наших запросов. И поскольку эти два сервера работают, мы все еще можем удовлетворить наши запросы или обслужить наших клиентов. Итак, наша система сломалась, потому что у нас был один сервер. Мы сначала сделали вертикальное масштабирование, но у вертикального масштабирования есть жесткий предел. И это единая точка отказа. Затем мы сделали горизонтальное масштабирование. Это хорошо, но теперь есть одна большая проблема. Клиент знает только один IP-адрес. Как он решит, с каким сервером говорить? Это большая проблема, не так ли? Чтобы решить эту проблему, у нас есть еще один компонент — балансировщик нагрузки. Чтобы решить проблему, которая возникла при добавлении большего количества серверов, то есть горизонтального масштабирования, мы добавляем балансировщик нагрузки. Итак, что делает балансировщик нагрузки? Он располагается перед вашим сервером, хорошо? Он располагается перед нашими серверами, сервер приложений один, сервер приложений два, сервер приложений три. Он располагается перед ними. И клиент теперь не общается напрямую с сервером. Этого соединения нет. Вместо этого он общается с балансировщиком нагрузки. Хорошо, он не общается напрямую с сервером приложений. Вместо этого он общается с балансировщиком нагрузки. Поэтому, когда клиент отправляет запрос, он идет к балансировщику нагрузки, и балансировщик нагрузки решает, какой сервер получит какой запрос. Но как он должен это делать? Ну, есть три способа сделать это: Round Robin, Least Connections и IP Hashing. Итак, что такое Round Robin? Round Robin означает распределение запросов каждому серверу равномерно, в порядке очереди. Итак, если есть запрос один, R1 идет сюда, R2 идет сюда, R3 идет сюда, R4 идет сюда, R5 идет сюда, R6 идет сюда. Таким образом, запросы распределяются равномерно, хорошо? Round Robin хорош, когда у нас равномерные запросы. Что это значит? Это значит, что мы предполагаем, что каждый запрос займет одинаковое количество времени. Итак, R1, R2, R3 займут одинаковое количество времени и будут распределены по очереди. Но мы знаем, что в реальном мире нет гарантии, что каждый запрос займет одинаковое количество времени. Поэтому существует другая концепция — Least Connections. Что означает Least Connections? Предположим, у нас есть запрос один, запрос два и запрос три. Хорошо, он занял 5 секунд, он занял 1 секунду, он занял 3 секунды. Хорошо. Теперь пришел еще один запрос, запрос четыре. Куда будет направлен запрос четыре? Запрос четыре пойдет на сервер приложений два. Почему? Потому что запрос, который обрабатывал сервер приложений два, завершен. Поэтому запрос четыре пойдет на сервер приложений два. Хорошо. Итак, алгоритм Least Connections отправляет запрос на тот сервер, у которого в данный момент меньше всего активных соединений. Вот что он делает. Это хорошо, когда у нас неравномерные запросы. Хорошо. Теперь давайте поговорим об IP Hashing. Тогда, если Least Connections хорош, зачем нам нужен IP Hashing? Ну, иногда мы хотим, чтобы один и тот же клиент всегда обращался к одному и тому же серверу. Итак, давайте предположим, у нас есть клиент один, он всегда должен обращаться к серверу приложений один. Клиент два всегда должен обращаться к серверу приложений один. Клиент три всегда должен обращаться к серверу приложений два. Если мы хотим что-то подобное, тогда мы используем IP Hashing. Теперь, зачем это используется? Это полезно, когда нам нужны "липкие сессии". Итак, давайте предположим, что каждый запрос не является независимым, и каждый запрос содержит часть данных. Часть данных находится на стороне сервера, и поступает новый запрос. Хорошо, и мы хотим, чтобы наш сервер имел контекст предыдущего запроса для обработки нашего нового запроса. Вот тогда мы используем IP Hashing. Хорошо, это хорошо. Теперь мы знаем, что есть балансировщик нагрузки, балансировщик нагрузки может использовать три алгоритма, чтобы решить, какой сервер должен получить какой запрос, но как балансировщик нагрузки должен смотреть на запрос? И это зависит от того, какой тип балансировщика нагрузки мы используем. Либо мы используем балансировщик нагрузки уровня 4, либо балансировщик нагрузки уровня 7. Итак, балансировщик нагрузки уровня 4 работает на транспортном уровне. Поэтому вы должны знать модель OSI, и мы знаем, что у нас есть транспортный уровень и прикладной уровень, хорошо? Итак, балансировщик нагрузки уровня 4 работает на транспортном уровне. Что это значит? Это значит, что они выполняют маршрутизацию исключительно на основе IP-адреса и TCP-порта. Поэтому они выполняют маршрутизацию на основе IP-адреса и TCP-порта. И поскольку они не перехватывают пакеты на прикладном уровне, они быстры, хорошо? Они быстрые и легкие, хорошо? Поэтому им не нужно заглядывать внутрь запроса. Поэтому они не знают, является ли это запросом изображения, является ли это вызовом API, они считают каждый запрос одинаковым и выполняют маршрутизацию на основе IP-адреса и TCP-порта. Но все реальные балансировщики нагрузки не работают на сетевом уровне. Они чрезвычайно умны. Поэтому они работают на прикладном уровне. И тогда появляются наши балансировщики нагрузки уровня 7. Итак, что делает балансировщик нагрузки уровня 7? Он может понимать ваш запрос. Он может маршрутизировать ваш пакет на основе пути URL, заголовка, cookie, хорошо? Итак, вы можете сделать что-то вроде этого: API * к вашему серверу приложений, а затем вы можете сказать: статический. Такой файл может перейти в CDN, хорошо? Или файловый сервер, хорошо? Поэтому они могут видеть, является ли это вызовом API, они могут передать его на сервер приложений. Если это что-то статическое, оно может перейти на файловый сервер. Таким образом, по сути, он выполняет маршрутизацию на основе пути URL, заголовка и cookie. И все современные балансировщики нагрузки, такие как AWS ALB, Nginx, работают на прикладном уровне. Все современные балансировщики нагрузки являются балансировщиками нагрузки уровня 7. Итак, есть еще одна вещь, которую делает балансировщик нагрузки, — это проверка работоспособности. Итак, что делает балансировщик нагрузки? Он постоянно пингует сервер, спрашивая: "Ты жив? О, ты жив?" Хорошо, он делает это для каждого сервера. И если сервер перестает отвечать, балансировщик нагрузки автоматически удаляет его из ротации и перестает отправлять на него трафик. Таким образом, новые запросы будут автоматически маршрутизироваться только к работоспособным серверам. Хорошо, вы можете увидеть пример здесь. Итак, вы видите здесь, проверка работоспособности сервера приложений три не удалась. Теперь запросы будут идти только к серверам приложений один и два. Таким образом, мы достигаем высокой доступности, без того, чтобы пользователь когда-либо замечал, что сервер вышел из строя. Хорошо, сервер три вышел из строя, но пользователь никогда не узнает, потому что балансировщик нагрузки автоматически обрабатывает все. Теперь вы, должно быть, думаете еще об одной вещи. Для серверов приложений у нас есть балансировщики нагрузки. Таким образом, мы избегаем единой точки отказа для серверов приложений. Но что насчет балансировщика нагрузки? Балансировщик нагрузки также может быть единой точкой отказа, верно? И это правильно. Да, балансировщик нагрузки также может быть единой точкой отказа, и поэтому вы запускаете несколько балансировщиков нагрузки в паре активный-активный или активный-пассивный. И на практике ваш облачный провайдер делает это. Вам не нужно об этом беспокоиться. Хорошо. Поэтому на практике вы добавили балансировщик нагрузки, вы не беспокоитесь о том, что он является единой точкой отказа. Ваш облачный провайдер позаботится об этом. Хорошо, это просто. Отлично. Теперь наша система горизонтально масштабируема и отказоустойчива. Но теперь у нас есть одна большая проблема. Пользователи в Сингапуре также получают ответы от пользователей в Вирджинии. Поэтому каждый запрос путешествует через половину мира. И именно поэтому возникает огромная задержка. Поскольку каждый запрос теперь занимает больше времени, у нас увеличивается задержка. Итак, как нам это исправить? Чтобы исправить это, у нас есть CDN, или сеть доставки контента. Итак, если ваш исходный сервер находится, например, на востоке США, а ваши пользователи находятся в Сингапуре, Мумбаи, у них будет огромная задержка. Но пользователь в Калифорнии получит результат быстрее. Итак, мы решаем эту проблему, добавляя CDN. Хорошо. Итак, вы можете видеть здесь, у нас огромная задержка. Здесь все быстро. Таким образом, CDN — это глобально распределенная сеть серверов, которую мы называем пограничными серверами или пограничными узлами, или точками присутствия (PoP). Итак, при наличии CDN, когда пользователь запрашивает статический файл, вместо обращения к нашему исходному серверу, мы получаем его из ближайшего пограничного узла или узла PoP. Хорошо? Итак, есть пограничный узел CDN в Азии, а затем пограничный узел CDN в Европе. Хорошо? Итак, пользователь в Сингапуре или Мумбаи получит статический файл контента, а пользователь в Лондоне получит данные из этого CDN, который находится в Европе. Теперь, что происходит с этим? С этим у нас очень низкая задержка, потому что мы доставляем статический контент из пограничного узла, который находится рядом с пользователем. У нас будет очень низкая задержка. И это преимущество CDN. Итак, как работает CDN? Итак, когда пользователь запрашивает что-то, статический файл, хорошо? Речь идет не о динамических данных, а о статических данных. Итак, пользователь запрашивает что-то из CDN. Итак, пользователь запрашивает статические данные. Пограничного узла у него еще нет. Хорошо? Пользователь в Сингапуре запросил какие-то данные, пограничного узла у него не было, поэтому это промах кэша. Итак, что мы сделаем? Мы обратимся к исходному серверу, получим данные и сохраним их локально, а затем вернем их пользователю. Хорошо? А затем мы вернем их пользователю. И в следующий раз, скажем, пользователь в Мумбаи или пользователь в Сингапуре запросит те же данные, вместо того, чтобы обращаться к исходному серверу, мы напрямую доставим их из пограничного узла или азиатского узла PoP. И то же самое произойдет и с европейским узлом. Теперь, если данные присутствуют в пограничном узле CDN, это называется попаданием в кэш. Мы обслуживаем его немедленно. Если данных там нет, мы называем это промахом кэша. Мы получаем данные с исходного сервера и заполняем CDN или пограничные узлы. Хорошо? Теперь, что относится к CDN? Ну, к CDN могут относиться изображения, видео, аудиофайлы, файлы JavaScript, CSS, шрифты и HTML для статических сайтов. Технически, все статические данные у нас есть в CDN. Мы не храним там динамические данные. Например, ответы вашего API, ваши пользовательские данные, все, что генерируется на запрос, не будет храниться в CDN. Теперь мы знаем, что такое CDN, и теперь мы знаем, что если в CDN нет данных, он получает их с исходного сервера, иначе он возвращает их только из CDN. Но есть одна важная вещь. Это инвалидация кэша. Давайте предположим, что что-то хранится в CDN и лежит там долгое время. Как вы его инвалидируете? Если вы его не инвалидируете, когда ваш CDN заполнится, вы не сможете добавить в него новые файлы. Итак, давайте предположим, что какой-то файл, например, HTML-файл, изменился. Но если вы не инвалидируете существующий файл, проблема в том, что вы никогда не получите последний файл в своем CDN. Итак, как мы будем инвалидировать наш кэш? Простая вещь: если мы развертываем новый ресурс, наш CDN должен знать, что он должен прекратить обслуживание старой версии. Это простой запрос. Итак, как мы можем это сделать? Есть два подхода. Первый — установить время жизни для каждого запроса. Хорошо. Итак, что делает время жизни? Каждый кэшированный файл автоматически истекает через n минут или n дней, хорошо? Потому что у него есть время жизни. Другой подход — иметь имя файла с версией. Итак, давайте предположим, ваш первый URL был main.a1b2c3.js. Это был ваш первый файл. Теперь вы его обновили, он стал main.a2b2c3.js. И давайте предположим, вы обновили его дальше, он стал main.a2b2c4.js. Хорошо, что-то вроде этого. Итак, что здесь произошло? У вас есть имя файла с версией. Хорошо, вот что вы используете в продакшене. Хорошо. Теперь это хорошо. Наш контент доставляется молниеносно. Но что насчет ответов API? Наша база данных по-прежнему запрашивается для каждого динамического запроса, и это становится для нас узким местом. Поэтому, когда наша база данных становится узким местом, у нас есть кэш. Хорошо, in-memory cache, то есть Redis. Хорошо, вот почему существует Redis. Итак, давайте предположим, что запрос к базе данных обычно занимает от 5 до 50 миллисекунд. И если одни и те же данные, например, профиль пользователя или страница продукта или таблица лидеров, запрашиваются тысячи раз в секунду, то, по сути, мы бомбардируем базу данных данными, которые даже не меняются так часто. Итак, каково решение? Введите кэш, введите сервер Redis между ними. Итак, что он делает? Он кэширует часто используемые данные в памяти. Итак, этот слой кэширования кэширует или хранит часто используемые данные в памяти. Поэтому доступ к памяти занимает наносекунды, а доступ к базе данных — миллисекунды. Поэтому, поскольку мы доставляем все за наносекунды, запрос становится чрезвычайно быстрым. И кроме того, Redis хранит все в памяти, то есть он хранит все в оперативной памяти. Поэтому доступ к оперативной памяти чрезвычайно быстр, мы все это знаем. Поэтому это in-memory хранилище данных. Поэтому этот Redis уже является отраслевым стандартом, и он хранит данные в памяти в виде сложных структур данных, таких как строки, хэши, отсортированные наборы, списки. И, вероятно, это то, что вы должны ответить на собеседовании. Теперь давайте обсудим, какой должна быть наша стратегия кэширования. Почему? Потому что то, как мы читаем и записываем данные, имеет большое значение. Итак, давайте поговорим о стратегиях кэширования. Первая — Write Through. Итак, что означает Write Through? Write Through означает, что каждая запись одновременно идет в базу данных и в кэш. Таким образом, кэш всегда синхронизирован. Но вот проблема с этим. Вы платите за стоимость записи даже за данные, которые, возможно, больше не будут прочитаны. Это проблема с Write Through. Вторая — Write Back. Итак, что означает Write Back? Что вы сначала записываете в кэш. Хорошо, все, что вы записываете, вы просто записываете в кэш, а не сразу в базу данных. Затем кэш асинхронно передаст или сбросит эти данные в базу данных в фоновом режиме. Хорошо. Теперь это хорошо, потому что это чрезвычайно быстрая запись, и проблема в этом заключается в том, что вы рискуете потерей данных, потому что вы не записываете их в постоянное хранилище, то есть в базу данных. Вместо этого вы просто держите их в кэше некоторое время и делаете это лениво. Так что это проблема с этим. Хорошо, и кэш может выйти из строя до записи в базу данных или постоянное хранилище. Теперь вторая — Write Back. Итак, что мы делаем в Write Back? Мы сначала записываем в кэш, а затем в фоновом режиме лениво записываем его в базу данных. Хорошо, это означает асинхронную передачу данных в базу данных. И это означает, что поскольку мы записываем его в кэш, наши записи будут чрезвычайно быстрыми. Хорошо, но одна большая проблема, поскольку мы записываем его в базу данных или в постоянное хранилище позже, мы рискуем потерей данных. Что, если наш кэш выйдет из строя до того, как мы даже запишем в базу данных? Хорошо, так что есть эта проблема с Write Back. И Write Through означает, что мы записываем его в кэш и базу данных одновременно. Но это может быть немного медленно. Поэтому всегда есть компромисс. Что вы хотите использовать? Хорошо. Теперь давайте поговорим о политиках вытеснения. Что, если наш кэш заполнится? Теперь наш кэш полон, нам нужно избавиться от некоторых данных, чтобы освободить место для новых данных. Итак, вот несколько популярных методов для этого. Первый — LRU. LRU — это наименее недавно использованный, что означает удаление элементов из списка, к которым обращались давным-давно. Хорошо. И это выбор по умолчанию. Второй — LFU, то есть наименее часто используемый. Наименее часто используемый означает удаление элементов, к которым обращались наименее всего в целом. Хорошо. И затем третий — время жизни. Хорошо, время жизни означает, что мы устанавливаем некоторое время для каждого кэшированного объекта, и данные автоматически истекают после установленной продолжительности. Хорошо. И это очень полезно, когда у нас есть данные, чувствительные ко времени, такие как цена акций или что-либо еще, что требует чувствительности ко времени, тогда мы используем TTL. Хорошо. Теперь это три политики вытеснения. Теперь есть одна большая проблема с этим кэшем, это "кэш-штамп" (cache stampede). Итак, кэш-штамп означает, что давайте предположим, что один из ключей чрезвычайно популярен. Хорошо. Давайте предположим, что одна из лент чрезвычайно популярна, чрезвычайно вирусна, и все пытаются посмотреть эту ленту, и все ищут данные. Вы поняли суть, верно? Итак, давайте предположим, что этот ключ истек. Теперь что произойдет? Все тысячи запросов одновременно промахнутся и пойдут в базу данных одновременно. Итак, давайте предположим, что 1 миллион запросов обращается к одному и тому же ключу, и этот ключ только что истек. Итак, что произойдет? Все эти 1 миллион запросов пойдут в базу данных. Все эти 1 миллион запросов идут в базу данных, потому что ключ Redis истек. Следовательно, мы снова оказываем большое давление на базу данных, или это называется кэш-штампом. Так что это одна большая проблема с наличием простого кэша. Итак, каково решение для этого? Ну, решение заключается в использовании мьютекс-блокировок. Итак, что делает мьютекс-блокировка? Давайте предположим, что 1 миллион запросов собирается обратиться к базе данных, но что мы собираемся сделать, это использовать мьютекс, чтобы только один запрос достиг базы данных, получил его, получил данные, а также заполнил Redis, а затем все последующие запросы напрямую получают данные только из Redis. Итак, вы понимаете мою мысль, верно? Если у вас нет мьютекса, и ключа нет в кэше, все запросы пойдут в базу данных, делая ее единой точкой отказа или перегружая ее. Вместо этого мы добавляем мьютекс-блокировку, так что если данных нет в кэше, один запрос прочитает их из базы данных, но все остальные не смогут прочитать, потому что есть мьютекс, и как только один запрос завершится, он автоматически обновит кэш, и последующие запросы смогут получать данные из кэша. Теперь еще одна важная вещь о кэше — это холодный старт. Хорошо. Теперь давайте поймем, что такое холодный старт. Итак, давайте предположим, вы добавили новый кэш в свою архитектуру, и он сейчас совершенно пуст. В нем ничего нет. Итак, как вы решите эту проблему? Потому что все первые, возможно, 1 миллион пользователей, которые посещают ваш сайт, ничего не найдут в кэше. Они пойдут напрямую в базу данных. Итак, решение для этого — предварительное заполнение кэша. Хорошо. Итак, решение для холодного старта кэша — предварительное заполнение кэша. Что это значит? Мы автоматически заполняем наш кэш популярными данными. Когда мы добавляем новый кэш. Итак, что это значит? Когда популярные данные находятся в кэше, наши популярные ключи в кэше, даже когда это новый кэш, он не пуст. Поэтому даже первая волна пользователей не обратится к базе данных. Они получат ответ только из кэша. Теперь это хорошо. Наша система медленно масштабируется. Наше чтение также быстрое. Но что, если наш продукт будет расти дальше? Давайте предположим, что у нас теперь 10 миллионов пользователей? Следовательно, база данных очень быстро заполняется большим количеством данных. И это больше, чем может обработать одна машина, потому что до сих пор у нас была только одна база данных. Хорошо. Итак, каково решение? Решение — репликация базы данных или масштабирование базы данных. Итак, что здесь произойдет? Мы сделаем репликацию базы данных. Хорошо. Итак, у нас будет одна основная база данных. Хорошо. У нас будет одна основная база данных, которая будет обрабатывать все записи, а затем у нас будут реплики для чтения, которые будут обслуживать запросы на чтение. Хорошо. Давайте предположим, что есть 10 миллионов запросов на чтение и 1 тысяча запросов на запись. Хорошо. Если есть 10 миллионов запросов на чтение и 1 тысяча запросов на запись, если вы обслуживаете все из одной базы данных, вы оказываете большое давление на нашу базу данных. Итак, что мы сделаем? Есть основная база данных, которая будет обрабатывать все записи, и она будет медленно реплицировать эти свежие данные на все реплики. И все чтения, все эти чтения будут обслуживаться с этих реплик. Реплика один, реплика два, реплика три, любая из этих реплик. Хорошо. Таким образом, чтение будет выполняться с реплик, а запись будет выполняться только в основную базу данных, и она позаботится о репликации данных на реплики. Хорошо. Теперь, что произойдет, если наша основная база данных выйдет из строя? Давайте предположим, она вышла из строя. Что произойдет? В этом случае любая из реплик, любая из этих реплик будет повышена до ведущей базы данных или основной базы данных, а остальные будут действовать как реплики только для чтения. Хорошо. Поэтому, если основная база данных по какой-то причине выходит из строя, мы проводим выборы лидера, и любая из реплик будет выбрана в качестве основной базы данных. Хорошо. И здесь в этом случае это становится новой основной базой данных. Хорошо. Таким образом, репликация базы данных помогает, потому что у нас больше запросов на чтение, чем запросов на запись на большинстве платформ, и именно поэтому мы делаем репликацию базы данных, чтобы можно было масштабировать чтение, а записи, их меньше, поэтому одна база данных может справиться с этим. И даже если есть миллионы записей, мы не можем делать репликацию для записей. Записи всегда должны происходить в основной базе данных. Для записей должен быть только один источник истины. Итак, каково преимущество этого? Преимущество в том, что трафик чтения распределяется между репликами. Хорошо. Теперь одна важная вещь — это задержка репликации. Теперь у нас есть задержка репликации. Вы записали в основную базу данных, и кто-то в этот момент читает с реплики. Они не получат свежий результат. Они получат устаревший результат. Почему? Из-за задержки репликации. Задержка репликации обычно занимает миллисекунды. Поэтому в течение нескольких миллисекунд основная база данных всегда опережает все эти реплики. Хорошо. Так что это еще одна важная вещь, которую следует учитывать. Итак, каким может быть решение для этого? Теперь для других пользователей это хорошо. Давайте предположим, другие пользователи читают какие-то данные. Они получают устаревшие данные, что все еще нормально, но что насчет пользователя, который их записал? Давайте предположим, вы что-то записали, вы обновили какое-то значение, и если вы по-прежнему видите устаревшие данные, вы можете быть сбиты с толку, о, это неправильно. Итак, каково решение для этого? Решение заключается в том, что когда кто-то записывает в основную базу данных в течение короткого периода времени, мы обслуживаем все чтения из основной базы данных для этого конкретного клиента. Хорошо. Это решение. И как только реплика догонит, этот клиент также начнет читать с реплик. Хорошо, так что, короче говоря, исправление заключается в том, чтобы направить чтение в основную базу данных на короткое окно сразу после того, как пользователь сделал запись. Хорошо? И как только все реплики догонят, вернитесь к репликам. Теперь это хорошо. Все хорошо. Теперь у нас есть одна основная база данных и несколько реплик. Но что, если данных слишком много? У нас 100 миллионов запросов или 100 миллионов данных. Тогда даже основная база данных должна хранить 100 миллионов данных, и даже реплики должны их хранить. И давайте предположим, что наша основная база данных достигает своего предела, потому что у нее есть емкость только 1 миллион данных или 1 миллион запросов. Это означает, что у наших реплик будет то же самое, и мы достигаем жесткого предела здесь. Итак, каково исправление? Исправление — это шардинг базы данных. Итак, да, это наше исправление, шардинг базы данных. В продакшене мы делаем шардинг базы данных плюс репликацию. Это исправление. Итак, когда один сервер базы данных содержит слишком много данных, мы делаем шардинг. И шардинг означает разделение данных между несколькими серверами, и каждый из них хранит подмножество данных. Хорошо? И каждый шард — это независимая база данных. Итак, как вы можете видеть здесь, раньше у нас была одна база данных, но теперь у нас есть два разных шарда, шард один, шард два. Хорошо? Теперь шард один также реплицируется, шард два также реплицируется. Итак, решение в реальной архитектуре — это шардинг базы данных плюс репликация. Они не работают по отдельности, они работают в комбинации. Хорошо, они работают в комбинации. Итак, что мы имели раньше, одна база данных, мы ее реплицировали. Теперь у нас есть различные шарды или меньшие отдельные базы данных, и мы реплицируем. Итак, теперь для каждого шарда у нас будет основная база данных. Итак, давайте предположим, что наш шард два — это основная база данных, а наш шард один — это основная база данных. Он хранит пользователей от А до М, а от N до Z хранятся в шарде два. Теперь для шарда один у нас будут отдельные реплики для чтения, а для шарда два у нас будут отдельные реплики для чтения. Итак, раньше мы делали репликацию для одной базы данных, но теперь мы разделили одну базу данных на несколько шардов, и теперь для каждого шарда мы делаем репликацию. И если один из основных шардов выходит из строя, то реплики проведут выборы лидера, и любая из реплик для этого шарда будет выбрана в качестве основной базы данных. Хорошо. Теперь одна распространенная проблема заключается в том, как решить, какая запись попадает в какой шард. Хорошо. Итак, вот некоторые распространенные стратегии. Одна — основанная на диапазоне. Итак, например, шард один хранит данные от А до М, а шард два будет хранить от N до Z. Итак, на основе диапазона идентификатора пользователя или первичного ключа, что бы это ни было, мы можем выбрать шард. Это один. Это основано на диапазоне. Вторая — основанная на хэше. Итак, что означает основанная на хэше? Мы хэшируем идентификатор пользователя или первичный ключ с количеством шардов, и к какому бы шарду мы ни получили запрос, он будет маршрутизирован к этому конкретному шарду. Хорошо. Давайте предположим, у нас есть пять шардов, и идентификатор пользователя — 57, идентификатор пользователя — 51. Итак, это пойдет в шард один, а это пойдет в шард два, потому что мы делаем остаток от деления на пять. Остаток от деления на пять. Хорошо. Это шардинг на основе хэша. Хорошо. Теперь, если вы используете базу данных, такую как MySQL, вам придется делать шардинг самостоятельно. Но если шардинг чрезвычайно сложен, и вы не имеете дело с свойствами активов, тогда вы можете использовать NoSQL базы данных. NoSQL базы данных, такие как Cassandra или DynamoDB, созданы для горизонтального масштабирования по умолчанию. Поэтому вам не нужно ничего управлять на уровне приложений. NoSQL база данных автоматически выполнит шардинг, репликацию, все. Хорошо. Они сделают это автоматически. Вам не придется ни о чем заботиться. Но с точки зрения интервьюера вам нужно это знать. Хорошо. Теперь здесь есть одна проблема с шардингом на основе хэша. Итак, проблема с шардингом на основе хэша заключается в том, что давайте предположим, вы добавляете n узлов, а затем один из узлов выходит из строя, затем вы добавляете больше узлов, затем один из узлов выходит из строя, затем вы добавляете еще один узел. Итак, что произойдет, если вы будете следовать этому процессу, что произойдет? Давайте предположим, одна из баз данных вышла из строя, вы перемещаете все данные из одной базы данных в другую. И если это происходит туда и обратно, вы делаете много перемещений, хорошо? Итак, решение — это согласованное хэширование. Итак, что делает согласованное хэширование? Оно исправляет, размещая как серверы, так и ключи в виртуальном кольце, а не в логическом кольце, а в виртуальном кольце. Каждый ключ назначается первому серверу, который он встречает при движении по часовой стрелке, хорошо? И серверы размещаются несколько раз, например, сервер один, сервер два, сервер три, затем снова сервер один, затем снова сервер два, и они могут быть размещены случайным образом, так что даже если один сервер выходит из строя, вам нужно переместить ключи только от этого одного конкретного сервера, а не от всех серверов, хорошо? И это также предотвращает неравномерное распределение. Итак, согласованное хэширование гарантирует, что нет неравномерного распределения. И как оно это делает? Ну, каждый сервер представлен множеством виртуальных узлов в кольце. И больше виртуальных узлов означает более равномерное распределение ключей. Хорошо, так что теперь у нас есть полная ментальная модель. Есть клиент, затем у нас есть балансировщик нагрузки, балансировщик нагрузки общается с сервером приложений, сервер приложений будет записывать в шард, потому что база данных теперь разделена на шарды, а чтение будет происходить с реплики для чтения шарда, а записи пойдут в основной шард. Если один из них выйдет из строя, любая из реплик будет повышена до основной базы данных. Теперь это выглядит хорошо, но в этом есть большой недостаток. Давайте предположим, пользователь хочет загрузить фотографию, хорошо? Пользователь хочет загрузить фотографию. Это хорошо. Теперь нам нужно будет изменить ее размер, обработать, сжать, создать миниатюру и обновить базу данных, отправить уведомление по электронной почте о том, что загрузка фотографии завершена. И если все это происходит синхронно, то мы обречены. Потому что если все происходит синхронно, пользователь должен ждать завершения всего этого, и он почувствует: "О, он застрял на экране". Или почему это занимает так много времени, почему ответ занимает так много времени. Хорошо, вы видите, да? Итак, решение? Решение — добавить очередь сообщений. Итак, вот проблема. Без очереди у нас синхронный дизайн. Поэтому пользователь ждет большую часть времени. Давайте предположим, пользователь загрузил фотографию. Она попала на сервер приложений. Сервер приложений выполнил изменение размера, сжатие, создание миниатюры, обновление базы данных. И давайте предположим, что все эти запросы занимают время. Давайте предположим, изменение размера занимает 800 мс, сжатие — 600 мс, создание миниатюры — 400 мс, обновление базы данных — 50 мс. Итак, пользователь ждет в основном 1850 мс или 3-4 секунды. Он в основном ждет и не знает, что здесь происходит. Он просто ждет, пока это закончится. И это на самом деле ужасный дизайн. Итак, каково решение для этого? Решение — разделить работу. И для разделения работы нам нужна событийно-ориентированная архитектура. И для событийно-ориентированной архитектуры основным требованием является очередь сообщений. Это основное требование, хорошо? Это основа для любой событийно-ориентированной архитектуры. Итак, что происходит? У нас есть очередь сообщений. Серверы приложений помещают сообщение в очередь или помещают задание в очередь и немедленно возвращаются к пользователю. И работник может обрабатывать его в фоновом режиме. Итак, давайте предположим, пользователь загрузил фотографию. Он отправил запрос на сервер приложений. Сервер приложений загрузил фотографию и вернулся через 30 мс, сказав: "О, ваш запрос выполнен". Пользователь подумает: "О, это чрезвычайно быстрый веб-сайт". Но что произойдет в фоновом режиме, это то, что он поместит задание в очередь сообщений, и все работники, такие как работник изображений, работник базы данных, работник электронной почты, возьмут эту фотографию и обработают ее в фоновом режиме и независимо. Не один за другим, а независимо. Поэтому работник изображений и работник базы данных делают одно и то же независимо и одновременно. Таким образом, это сделало нашу систему чрезвычайно быстрой, и поскольку задание помещается в очередь сообщений, оно долговечно, поэтому нам не нужно об этом беспокоиться. Мы можем выполнить эту работу позже в фоновом режиме, и пользователь увидит: "О, запрос выполнен немедленно". Теперь все работники работают параллельно, поэтому пользователю не нужно ждать, пока кто-либо из них закончит. Хорошо. Итак, мы все разделили. Во-вторых, больше нет буферизации, потому что, давайте предположим, произошел внезапный всплеск трафика, мы все равно помещаем задание в очередь, и работники могут выполнять его в своем темпе, и все пользователи получат ответ, ваша работа выполнена. Хорошо. Мы позаботились о внезапном всплеске трафика и нашей логике. Даже если один из работников выйдет из строя, скажем, работник изображений вышел из строя. Хорошо. Что он сделает? Он выйдет из строя, он снова вернется, он пойдет в очередь сообщений, он скажет: "О, что было последним незавершенным для меня?" Он получит задание и начнет обрабатывать его независимо. Поэтому, даже когда работник базы данных, работник электронной почты завершили работу, а работник изображений закончил, он может появиться в любое время и взять задание оттуда. Хорошо. Теперь, что произойдет, если какой-либо из работников выйдет из строя в процессе? Скажем, работник электронной почты вышел из строя. Что произойдет? Сообщение вернется в очередь. Это будет автоматически повторено, и пользователь никогда не узнает, а загрузка фотографии всегда будет выглядеть подтвержденной. Теперь еще одно преимущество этого в том, что, скажем, мы хотим добавить еще одного работника. Давайте предположим, мы хотим работника аналитики, который занимается в основном логированием. Давайте предположим, все работники работают независимо, он хочет регистрировать все функции. Давайте предположим, мы хотим дальше отлаживать, давайте предположим, почему что-то вышло из строя, как была наша обработка и многое другое, верно? Поэтому будет очень легко добавить. Вместо того, чтобы идти как одно, нам пришлось добавить еще один слой, но здесь мы просто добавляем еще один независимый слой, который напрямую общается с очередью. Поэтому здесь мы просто добавляем еще один независимый слой, который напрямую общается с очередями сообщений, получает данные и выполняет всю работу. Поэтому добавить нового работника также чрезвычайно легко, потому что это событийно-ориентированная архитектура. Все независимо. Теперь есть два популярных типа очередей сообщений. Один — Kafka, а второй — RabbitMQ. Хорошо. Это два популярных типа очередей сообщений. Теперь Kafka — это распределенный постоянный журнал событий. Что это значит? Сообщения записываются в журнал и хранятся в течение дней, недель или даже месяцев. Хорошо. Теперь каждый потребитель отслеживает свою собственную позицию в журнале и может даже воспроизводить события из любой точки. Хорошо. Это делает Kafka идеальным для таких событий, как потоковые события, события аналитики в реальном времени или журналы аудита. Хорошо? Поэтому в таких сценариях следует использовать Kafka. И именно поэтому вы, вероятно, много слышали о Kafka, потому что Kafka чрезвычайно популярен. И я не говорю, что RabbitMQ не популярен, он тоже популярен, но Kafka более популярен. Теперь, если вы внимательно посмотрите, в этом дизайне есть один большой недостаток. Итак, большой недостаток здесь в том, что мы храним наши медиафайлы или любые другие файлы, файлы типа blob, в нашей базе данных. И это совершенно неправильно. Мы никогда не должны хранить двоичные файлы, такие как изображения, медиа, PDF, аудио, в нашей базе данных. Потому что базы данных обычно оптимизированы для структурированных и запрашиваемых данных. Поэтому хранение больших файлов blob может сделать их чрезвычайно медленными, дорогими и трудными для масштабирования. Хорошо. Итак, каково решение для этого? Вместо хранения всего в базе данных используйте объектное хранилище для хранения этого. Потому что объектные хранилища, такие как Amazon S3, Google Cloud или Azure Blob Storage, специально разработаны для удовлетворения таких требований. Теперь, если вы можете видеть здесь, клиенты загружают файл на сервер приложений. Что делает сервер приложений? Он не хранит все в базе данных. Вместо этого он загружает файл непосредственно в объектное хранилище. И я использую S3, потому что это очень популярный вариант, и он иногда используется взаимозаменяемо с объектным хранилищем, но S3 — это тип объектного хранилища. Это не означает, что это единственное объектное хранилище, хорошо? Поэтому вы храните все двоичные файлы, такие как изображения, PDF, медиа, в объектном хранилище или в бакете S3. Теперь шаблон прост. Клиент загружает файл, сервер приложений загружает файл в объектное хранилище, используя предварительно подписанный URL, а затем объектное хранилище возвращает URL, URL загрузки, а затем этот URL загружается в очередь сообщений. Хорошо? И все процессы, которые должны быть выполнены работником, могут взять идентификатор задания из очереди сообщений. Там будет URL, они могут взять файл из объектного хранилища и выполнить всю обработку, которую они хотят. Поэтому база данных будет хранить только URL. Она не хранит файл. Фактический файл будет в объектном хранилище. Хорошо, при обслуживании этих файлов blob пользователю, мы направляем их через CDN. Хорошо. Эти файлы, те, которые мы хранили в объектном хранилище, обслуживаются пользователю через CDN. Теперь я ранее использовал термин "предварительно подписанный URL". Вы, должно быть, запутались, о, что такое предварительно подписанный URL? Вы, возможно, подумали: "Хорошо, мы проверим это позже, что такое предварительно подписанный URL?" Но давайте разберемся, что именно такое предварительно подписанный URL. И не откладывайте это дальше. Итак, что такое предварительно подписанный URL? Для частных файлов вам не нужно напрямую раскрывать часть S3. Хорошо? Ваш сервер генерирует предварительно подписанный URL, временную ссылку с встроенным сроком действия и криптографической подписью. Хорошо. Клиент использует этот URL для загрузки или скачивания напрямую из бакета S3 или объектного хранилища, даже не проходя через сервер. Это экономит пропускную способность вашего сервера. И именно так работают Dropbox, Google Drive и большинство платформ для обмена файлами. Итак, вы получаете предварительно подписанный URL, который имеет срок действия и криптографическую подпись, чтобы не каждый мог злоупотреблять им. Итак, теперь мы построили нашу архитектуру слой за слоем. Хорошо, мы добавили балансировщик нагрузки, мы добавили CDN, мы добавили репликацию базы данных, мы добавили шардинг базы данных, мы добавили очередь сообщений, мы превратили ее в событийно-ориентированную архитектуру, мы даже добавили объектное хранилище. Хорошо, так что теперь, прежде чем перейти к тому, как будет выглядеть окончательная масштабируемая архитектура, давайте разберемся с ключевыми шаблонами проектирования, потому что они будут чрезвычайно важны. Например, мы используем репликацию базы данных плюс шардинг, но одна важная концепция там — согласованное хэширование. У нас будет ограничение скорости, у нас будут веб-сокеты. Хорошо, так что все это важные шаблоны проектирования для проектирования систем, которые мы должны знать. Итак, сначала давайте разберемся с этим, а затем перейдем к окончательной архитектуре, окончательной масштабируемой архитектуре. Итак, сначала согласованное хэширование, как вы можете видеть здесь. Итак, что происходит в согласованном хэшировании? Ваши шарды или ваши кэши размещаются в виртуальном кольце, и каждому ключу назначается первый встреченный сервер, и он движется по часовой стрелке. Он движется по часовой стрелке. Хорошо. Теперь, если мы разместим сервер только один раз, то самая большая проблема заключается в неравномерном распределении. Теперь, что мы делаем? Мы повторяем серверы. Итак, виртуальные узлы, каждый сервер появляется несколько раз в кольце, и это обеспечивает распределение нагрузки. Итак, это исходный сервер, это исходный сервер, это исходный сервер, и это исходный сервер, но он появляется несколько раз в этом виртуальном кольце. С этим, имея виртуальные множественные узлы, появляющиеся несколько раз, мы обеспечиваем равномерное распределение нагрузки. И давайте предположим, что мы хотим Теперь давайте предположим, что мы хотим добавить новый сервер. Итак, только ключи между новым сервером и его предшественником в кольце должны переместиться. Итак, давайте предположим, что с обычным хэшированием мы перемещаем 75% ключей, с согласованным хэшированием мы перемещаем только примерно один из n ключей, где n — количество узлов. И это предотвращает неравномерное распределение, потому что каждый сервер представлен много раз в виртуальном кольце. Поэтому больше виртуальных узлов означает более равномерное распределение. Итак, вот что такое согласованное хэширование. Затем идет еще одна важная концепция — короткое опробование или простое опробование, затем веб-сокеты и серверные события. Итак, как выглядит стандартный запрос-ответ HTTP? Клиент задает серверу вопрос, сервер отвечает, и соединение закрывается. И для большинства теоретических вещей это идеально, но для функций реального времени, таких как живой чат, совместный редактор, живые уведомления, это неэффективно. Итак, у нас есть три метода. Первый — короткое опробование. Короткое опробование означает, что клиент беспокоит или пингует сервер несколько раз. Клиент говорит: "О, есть обновления?" Сервер говорит: "Нет". Есть обновления? Нет. Затем клиент говорит: "О, есть обновления?" Сервер говорит: "Да". И он отправляет ответ. Теперь это хорошо, но самая большая проблема в том, что большинство запросов возвращают ничего. Это означает, что у нас огромная потеря запросов. Хорошо, или огромная потеря ресурсов, потому что каждый запрос — это ресурс. Поэтому мы тратим много ресурсов. Теперь есть одна альтернатива этому, это длинное опробование. Клиент говорит: "Есть обновления?" Он держит соединение некоторое время, позволяя серверу ответить. Хорошо, вместо нескольких пингов, пингуйте один раз, держите соединение открытым, а затем закройте его. Это улучшение по сравнению с коротким опробованием, но это даже не практично. У нас все еще много переключений соединений. Итак, каковы два других решения? Веб-сокеты и серверные события. Теперь они популярно используются, и у них разные варианты использования. Например, веб-сокеты. Веб-сокеты — это одно постоянное соединение. Хорошо, и оно двунаправленное по своей природе. Это однонаправленное. Это однонаправленное. Это двунаправленное. Итак, что происходит в веб-сокетах? Клиент открывает соединение с сервером через рукопожатие. Хорошо, рукопожатие веб-сокета, а затем соединение остается открытым. Хорошо? Теперь, когда серверу есть что сказать, сервер отправляет это клиенту. Если у клиента есть что-то, клиент отправляет это сообщение серверу, и это соединение проходит насквозь и остается открытым. Итак, это двунаправленное, имеет низкие накладные расходы, и его реальные приложения — это чат, игры или живой совместный редактор. Теперь у нас есть серверные события. Итак, серверные события похожи на веб-сокеты, но они однонаправленные. Клиент выполняет рукопожатие с сервером. Соединение открыто, и тогда только сервер отправляет сообщения. Отправка сообщений клиентом невозможна. Только сервер будет отправлять сообщения. Итак, это однонаправленное, и это очень хорошо, когда мы хотим отправлять уведомления, ленты, или веб-сайт с акциями также является хорошим примером использования серверных событий. Теперь есть еще одна важная концепция проектирования в системном дизайне, которая называется идемпотентность. Итак, что происходит здесь без ключа идемпотентности? Давайте предположим, пользователь заплатил 100 долларов, и запрос был отправлен на сервер, сервер списал средства с карты, и в этот момент сеть умерла. Хорошо, поэтому ответ пользователю

никогда не прибыл. Это означает, что пользователь снова пытается, снова сервер снял $100, и таким образом с пользователя было снято $200 вместо $100, потому что у сервера нет способа определить, была ли это повторная попытка или нет. И эта проблема может возникнуть не только в случае оплаты, но и в случае отправки заказа, доставки электронной почты или прав базы данных. Хорошо, так это одна из больших проблем. Итак, проблема в том, что сетевой запрос успешен, но сервер вышел из строя до того, как ответ достиг клиента, поэтому клиент повторил попытку. Сервер повторно обрабатывает тот же запрос, и с пользователя дважды снимаются деньги или происходит одно и то же действие дважды. Итак, каково решение для этого? Решение — наличие ключа идемпотентности. Итак, операция идемпотентности делает так, что она дает тот же результат, независимо от того, сколько раз вы ее выполняете. И, честно говоря, все GET-запросы по своей природе идемпотентны, а POST- и PUT-запросы вы можете сделать идемпотентными, имея этот ключ, или мы называем его ключом идемпотентности. Итак, по сути, клиент генерирует UUID перед отправкой любого запроса. Хорошо. А сервер проверяет Redis на наличие ключа. Если он найден, он возвращает кэшированный результат. Если он не найден, он обрабатывает и сохраняет его. И это используется Stripe, PayPal, Braintree и всеми другими платежными API. Они используют это. Это часть заголовка как ключ идемпотентности. Хорошо, этот ключ является частью заголовка запроса. Итак, вы, пользователь, платите $100, сервер снимает деньги, и вы также отправляете ключ, который является UUID платежа A3S9C2. Хорошо, что-то вроде этого вы отправляете UUID. Обычно UUID. Хорошо, вы отправляете что-то вроде этого. Теперь сеть умерла, ответ никогда не прибыл. Теперь пользователь повторно пытается оплатить. Пользователь повторно пытается заплатить $100. Сервер не снимает деньги снова. Почему? Потому что он вернул кэшированный результат. Так что второго списания нет, и с пользователя было снято ровно $100 один раз. Хорошо. Вот как это помогает. Теперь есть еще одна важная концепция — CAP-теорема. Хорошо? CAP-теорема. Очень популярна. В реальных приложениях CAP-теорема — самая популярная вещь. Итак, CAP-теорема означает согласованность (Consistency), доступность (Availability) и устойчивость к разделению (Partition Tolerance). Итак, согласованность говорит: «Дайте мне ответ только тогда, когда вы знаете, что ответ правильный. Не давайте мне устаревшие данные». Доступность говорит: «Будьте доступны. Даже если вы даете устаревшие данные, просто дайте их. Убедитесь, что система выглядит живой». Хорошо? А затем есть устойчивость к разделению. Устойчивость к разделению означает, что система продолжает работать, даже если сеть разрывается между узлами. Теперь важно то, что вы не можете гарантировать все три одновременно. Вы можете гарантировать либо CP, либо AP. А CA невозможно. Хорошо? CA невозможно. Почему? Потому что в реальной распределенной системе вы не можете игнорировать P. Устойчивость к разделению означает, что даже если какой-либо узел выходит из строя, система продолжает работать. Но вы не можете игнорировать устойчивость к разделению, потому что если вы ее игнорируете, это означает, что вы говорите: «О, если сервер вышел из строя, как будто мой сервис сломался». Вы не можете себе этого позволить. А также C и A не идут бок о бок. Согласованность говорит: «Дайте мне всегда правильные данные». Доступность говорит: «Дайте мне данные, даже если они устарели». Они не сталкиваются одновременно. Так что CA невозможно. Затем, реальный выбор, или при проектировании вашей системы, вам придется выбирать между CP или AP. Теперь, как вы это выберете? CP говорит: «Я предпочитаю правильные данные или отсутствие ответа». Например, финансовые учреждения. Так что, если вы имеете дело с чем-то, связанным с финансами, вы должны выбрать CP. AP похоже на аналитику YouTube. Допустим, я загрузил видео. Оно набрало 1 миллион просмотров. Что является настоящей мечтой для меня, кстати. Но для вас или для меня, я читаю из реплик чтения, и они еще не обновлены. Так что я вижу только 100 тысяч или 200 тысяч. Что нормально. YouTube говорит, что это нормально. Пользователь должен видеть, что сервис работает и функционирует, даже если он показывает устаревшие данные. Так что иногда я буду видеть: «О, у моих видео только 100 тысяч просмотров». А внезапно другие пользователи увидят: «О, у него 200 тысяч просмотров». И в конечном итоге оба они сойдутся к правильному, то есть к 1 миллиону просмотров. Вот что означает AP. Доступность и устойчивость к разделению. Система должна быть высокодоступной. Теперь, какой вариант выбрать, зависит от вас. Все сводится к тому, хотите ли вы устаревшие данные или нет. Игнорировать устойчивость к разделению нельзя. Так что, если вы хотите устаревшие данные, выбирайте AP. Если вы всегда хотите правильные данные, выбирайте CP. Теперь это полная картина того, что мы построили. Мы начали с простой клиент-серверной архитектуры. И у нее был DNS-резольвер для преобразования URL в IP-адрес. Затем мы увидели: «О, если у нас один сервер, то мы можем столкнуться с проблемой единой точки отказа, и его также трудно масштабировать». Затем мы добавили несколько серверов. Но с несколькими серверами, как нам добраться до этого сервера? Мы добавили балансировщик нагрузки. Теперь балансировщики нагрузки бывают двух типов: L4 и L7. Но мы всегда хотели быть последними, а последние — это L7. Который работает на уровне приложений, поэтому мы добавили балансировщик нагрузки L7. Но поскольку у нас были балансировщики нагрузки, у нас все еще была одна база данных, и все они обращались к одной базе данных. И это было большим ударом по нашей базе данных, особенно когда мы хотим вернуть тот же результат. Так что именно это мы добавили кэширование. Мы добавили Redis. Так что Redis начал кэшировать наши данные. Теперь наши данные росли, наша платформа росла. У нас была только одна база данных, и она становилась узким местом. Так что мы ее разделили. Шард один и шард два. Теперь есть шард один и шард два. Но есть одна проблема: эти шарды также являются единой точкой отказа. Так что мы добавили репликацию. Так что чтение может выполняться с любой реплики, а запись будет выполняться в основной шард или основную базу данных. И даже если один из шардов выйдет из строя, любая из реплик может стать основной. Так что наши базы данных также масштабированы. Теперь одной большой проблемой было то, что у нас не было очереди сообщений. Так что все эти операции были синхронными. Так что мы добавили очередь сообщений и преобразовали нашу архитектуру в событийно-ориентированную архитектуру. Теперь пользователь отправляет запрос. Мы принимаем его, немедленно отвечаем пользователю, говоря: «Ваш запрос выполнен». И очередь сообщений будет хранить задание, а затем рабочие могут независимо выбирать задания и обрабатывать их. Теперь другая проблема заключалась в хранении двоичных данных. Так что для хранения двоичных данных мы добавили блочное хранилище. Так что пользователь, когда загружает файл через предварительно подписанный URL, он загружается в блочное хранилище, а URL записывается в базу данных, а также в очередь заданий, а затем все остальные независимые рабочие, такие как рабочий для электронной почты, рабочий для миниатюр, рабочий для изменения размера изображений, любой такой рабочий, могут независимо получать задание из очереди и выполнять работу. И теперь эти данные из блочного хранилища обслуживаются пользователю через CDN. Хорошо. Так что статические ресурсы обслуживаются пользователю через CDN, чтобы задержка была как можно меньше. Вот как выглядит полная масштабируемая архитектура. Хорошо. Каждый компонент, который вы видите в большой системе, существует потому, что он решает конкретную проблему. Хорошо. Один сервер не мог справиться с нагрузкой, поэтому мы масштабировались горизонтально. Затем серверу понадобилась входная дверь, мы добавили балансировщик нагрузки. Доставка статического контента была медленной, мы добавили CDN. База данных была перегружена, мы добавили кэш. Затем операции блокировали пользователей, поэтому мы добавили очередь. И это ментальная модель для большой масштабируемой архитектуры. Хорошо. И вот как вы должны соединять блоки, и вот что вы должны ответить на собеседовании по проектированию систем. Итак, на этом все для экспресс-курса. Все, что вы видели сегодня, например, масштабирование, балансировка нагрузки, CDN, кэширование, репликация базы данных, шардинг, очередь сообщений, блочное хранилище, хэширование с постоянством, веб-сокеты, идемпотентность, CAP-теорема, это важные темы для любого собеседования по проектированию систем. Это базовые концепции, которые вы знаете, и вы должны знать, как их соединять и строить эту ментальную модель. Надеюсь, вам понравилось это видео. Если вам понравилось это видео, поставьте ему лайк, поделитесь им с друзьями. Если вы еще не подписались, пожалуйста, подпишитесь. Расскажите, что вы думаете об этом. И если вы хотите больше таких видео, пожалуйста, комментируйте. Если у вас есть какие-либо сомнения, пожалуйста, комментируйте. И если вам понравилось мое видео и у вас есть какие-либо мысли по этому поводу, пожалуйста, напишите их в комментариях. Я читаю все комментарии и постараюсь ответить на большинство из них. И большое вам спасибо. >> [музыка]