Transcription
Этот полный учебник по проектированию систем охватывает масштабируемость, надежность, обработку данных и высокоуровневую архитектуру с четкими объяснениями, реальными примерами и практическими стратегиями. Этот курс научит вас основным концепциям, которые вам нужно знать для собеседования по проектированию систем. Это полный интенсивный курс по концепциям собеседования по проектированию систем, которые вам нужно знать, чтобы успешно пройти собеседование. Собеседование по проектированию систем не имеет много общего с программированием, и люди не хотят видеть, как вы пишете фактический код, а скорее, как вы связываете всю систему воедино, и именно это мы будем охватывать в этом учебнике. Мы пройдемся по всем концепциям, которые вам нужно знать, чтобы успешно пройти собеседование, прежде чем проектировать крупномасштабные распределенные системы. Важно понимать высокоуровневую архитектуру отдельного компьютера. Давайте посмотрим, как различные части компьютера работают вместе для выполнения нашего кода. Компьютеры функционируют через многоуровневую систему, каждая из которых оптимизирована для различных задач. На базовом уровне компьютеры понимают только двоичные нули и единицы. Они представлены в виде битов. Один бит — это наименьшая единица данных в вычислительной технике. Он может быть либо нулем, либо единицей. Один байт состоит из восьми битов и используется для представления одного символа, такого как 'a', или числа, такого как '1'. Расширяясь отсюда, у нас есть килобайты, мегабайты, гигабайты и терабайты. Для хранения этих данных у нас есть дисковое хранилище компьютера, которое содержит основные данные. Оно может быть либо типа HDD, либо SSD. Дисковое хранилище является энергонезависимым, оно сохраняет данные без питания, что означает, что если вы выключите или перезагрузите компьютер, данные останутся там. Оно содержит операционную систему, приложения и все пользовательские файлы. По размеру диски обычно варьируются от сотен гигабайт до нескольких терабайт. Хотя SSD дороже, они обеспечивают значительно более быстрое извлечение данных, чем HDD. Например, SSD может иметь скорость чтения от 500 МБ/с до 3500 МБ/с, в то время как HDD может предлагать от 80 до 160 МБ/с. Следующей непосредственной точкой доступа после диска является ОЗУ, или оперативная память (Random Access Memory). ОЗУ служит основным активным хранилищем данных и содержит структуры данных, переменные и данные приложений, которые в настоящее время используются или обрабатываются. Когда программа выполняется, ее переменные, промежуточные вычисления, стек времени выполнения и многое другое хранятся в ОЗУ, поскольку она обеспечивает быстрый доступ для чтения и записи. Это энергозависимая память, что означает, что ей требуется питание для сохранения содержимого, и после перезагрузки компьютера данные могут не сохраняться. По размеру ОЗУ варьируется от нескольких ГБ в потребительских устройствах до сотен ГБ на высокопроизводительных серверах. Их скорость чтения/записи часто превышает 5000 МБ/с, что быстрее, чем даже у самых быстрых SSD. Но иногда даже этой скорости недостаточно, что подводит нас к кэшу. Кэш меньше ОЗУ, обычно он измеряется в мегабайтах, но время доступа к кэш-памяти еще быстрее, чем у ОЗУ, предлагая всего несколько наносекунд для кэша L1. ЦП сначала проверяет кэш L1 на наличие данных. Если они не найдены, он проверяет кэши L2 и L3, а затем, наконец, проверяет ОЗУ. Цель кэша — сократить среднее время доступа к данным. Вот почему мы храним здесь часто используемые данные для оптимизации производительности ЦП. А как насчет ЦП? ЦП — это мозг компьютера. Он извлекает, декодирует и выполняет инструкции. Когда вы запускаете свой код, именно ЦП обрабатывает операции, определенные в этой программе. Но прежде чем он сможет выполнить наш код, написанный на языках высокого уровня, таких как Java, C++, Python или других языках, наш код сначала должен быть скомпилирован в машинный код. Компилятор выполняет эту трансляцию, и как только код скомпилирован в машинный код, ЦП может его выполнить. Он может читать и записывать данные из нашей ОЗУ, диска и кэша. И, наконец, у нас есть материнская плата или главная плата, которую вы можете считать компонентом, который соединяет все. Она предоставляет пути, которые позволяют данным течь между этими компонентами. Теперь давайте взглянем на высокоуровневую архитектуру готового к производству приложения. Наша первая ключевая область — это конвейер CI/CD (непрерывная интеграция и непрерывное развертывание). Это гарантирует, что наш код проходит из репозитория через ряд тестов и проверок конвейера и попадает на производственный сервер без какого-либо ручного вмешательства. Он настроен с использованием таких платформ, как Jenkins или GitHub Actions, для автоматизации наших процессов развертывания. И как только наше приложение находится в производстве, оно должно обрабатывать множество пользовательских запросов. Это управляется нашими балансировщиками нагрузки и обратными прокси, такими как Nginx. Они гарантируют, что пользовательские запросы равномерно распределяются между несколькими серверами, поддерживая плавный пользовательский опыт даже во время пиковых нагрузок. Нашему серверу также потребуется хранить данные. Для этого у нас также есть внешний сервер хранения, который не работает на том же производственном сервере. Вместо этого он подключен по сети. Наши серверы также могут обмениваться данными с другими серверами. И у нас может быть много таких служб, не только одна. Чтобы все работало гладко, у нас есть системы логирования и мониторинга, которые внимательно следят за каждым микро-взаимодействием. Хранение журналов и анализ данных — стандартная практика. Журналы обычно хранятся на внешних службах, часто за пределами нашего основного производственного сервера. Для серверных инструментов, таких как PM2, можно использовать для логирования и мониторинга. На стороне клиента, для фронтенда, платформы, такие как Sentry, могут использоваться для захвата и сообщения об ошибках в реальном времени. И когда что-то идет не так, как планировалось, то есть наши системы логирования обнаруживают сбои запросов или аномалии, сначала срабатывает наша служба оповещения. После этого отправляются push-уведомления, чтобы информировать пользователей, от общего «что-то пошло не так» до конкретного «платеж не прошел». Современная практика — интегрировать эти оповещения непосредственно в платформы, которые мы обычно используем, такие как Slack. Представьте себе выделенный канал Slack, где оповещения появляются в момент возникновения проблемы. Это позволяет разработчикам действовать почти мгновенно, устраняя первопричину до того, как она обострится. И после этого разработчики должны отладить проблему. Прежде всего, проблему необходимо выявить. Те журналы, о которых мы говорили ранее, являются нашим первым обращением. Разработчики просматривают их, ища закономерности или аномалии, которые могут указывать на источник проблемы. После этого ее необходимо воспроизвести в безопасной среде. Золотое правило — никогда не отлаживать непосредственно в производственной среде. Вместо этого разработчики воссоздают проблему в тестовой или промежуточной среде. Это гарантирует, что пользователи не пострадают от процесса отладки. Затем разработчики используют инструменты, чтобы заглянуть в работающее приложение и начать отладку. Как только ошибка исправлена, выпускается срочное исправление (hotfix). Это быстрое временное исправление, предназначенное для восстановления работоспособности. Это похоже на патч перед внедрением более постоянного решения. В этом разделе давайте разберемся в основах проектирования систем и в том, что на самом деле требуется для создания надежного и устойчивого приложения. Теперь, прежде чем перейти к техническим деталям, давайте поговорим о том, что на самом деле делает хороший дизайн. Когда мы говорим о хорошем дизайне в системной архитектуре, мы действительно фокусируемся на нескольких ключевых принципах: масштабируемость, которая заключается в росте нашей системы вместе с ее пользовательской базой; поддерживаемость, которая заключается в обеспечении того, чтобы будущие разработчики могли понимать и улучшать нашу систему; и эффективность, которая заключается в наилучшем использовании наших ресурсов. Но хороший дизайн также означает планирование на случай сбоев и создание системы, которая не только хорошо работает, когда все идет гладко, но и сохраняет спокойствие, когда что-то идет не так. В основе проектирования систем лежат три ключевых элемента: перемещение данных, хранение данных и преобразование данных. Перемещение данных заключается в обеспечении беспрепятственного потока данных из одной части нашей системы в другую, будь то пользовательские запросы, поступающие на наши серверы, или передача данных между базами данных. Нам нужно оптимизировать скорость и безопасность. Хранение данных — это не просто выбор между базами данных SQL или NoSQL. Это понимание шаблонов доступа, стратегий индексирования и решений для резервного копирования. Нам нужно убедиться, что наши данные не только безопасно хранятся, но и легко доступны, когда они нужны. А преобразование данных заключается в том, чтобы брать необработанные данные и превращать их в значимую информацию, будь то агрегирование журналов для анализа или преобразование пользовательского ввода в другой формат. Теперь давайте уделим минуту, чтобы понять решающую концепцию в проектировании систем — теорему CAP, также известную как теорема Брюера, названную в честь ученого-компьютерщика Эрика Брюера. Эта теорема представляет собой набор принципов, которые помогают нам принимать обоснованные компромиссы между тремя ключевыми компонентами распределенной системы: согласованностью (Consistency), доступностью (Availability) и устойчивостью к разделению (Partition Tolerance). Согласованность гарантирует, что все узлы в распределенной системе имеют одни и те же данные в одно и то же время. Если вы внесете изменение в один узел, это изменение должно также отразиться на всех узлах. Думайте об этом как об обновлении Google Docs: если один человек вносит правку, все остальные видят эту правку немедленно. Доступность означает, что система всегда работает и отвечает на запросы, независимо от того, что может происходить за кулисами, как надежный интернет-магазин: когда бы вы ни посетили его, он всегда открыт и готов принять ваш заказ. А устойчивость к разделению относится к способности системы продолжать функционировать, даже когда происходит разделение сети, то есть если возникает сбой связи между узлами, система все равно работает. Это похоже на групповой чат, где даже если один человек теряет соединение, остальные участники группы могут продолжать общаться. И согласно теореме CAP, распределенная система может достичь только двух из этих трех свойств одновременно. Если вы отдаете приоритет согласованности и устойчивости к разделению, вам, возможно, придется пожертвовать доступностью, и наоборот. Например, банковская система должна быть согласованной и устойчивой к разделению, чтобы обеспечить финансовую точность, даже если это означает, что некоторые транзакции обрабатываются дольше, временно жертвуя доступностью. Таким образом, каждое решение о проектировании сопряжено с компромиссами. Например, система, оптимизированная для операций чтения, может плохо работать при операциях записи, или, чтобы получить производительность, нам, возможно, придется пожертвовать некоторой сложностью. Так что дело не в поиске идеального решения, а в поиске лучшего решения для нашего конкретного случая использования, и это означает принятие обоснованных решений о том, где мы можем позволить себе пойти на компромисс. Итак, одно важное измерение системы — это доступность. Это мера эксплуатационной производительности и надежности системы. Когда мы говорим о доступности, мы, по сути, спрашиваем: работает ли наша система, когда она нужна нашим пользователям? Это часто измеряется в процентах, стремясь к той золотой доступности «5 девяток». Допустим, мы запускаем критически важную службу с доступностью 99,9%, что допускает около 8,76 часов простоя в год. Но если мы добавим к этому две девятки, мы говорим всего о 5 минутах простоя в год, и это огромная разница, особенно для служб, где каждая секунда на счету. Мы часто измеряем это с точки зрения времени безотказной работы и времени простоя. И здесь в игру вступают целевые показатели уровня обслуживания (SLO) и соглашения об уровне обслуживания (SLA). SLO похожи на постановку целей для производительности и доступности наших систем. Например, мы можем установить SLO, заявив, что наша веб-служба должна отвечать на запросы в течение 300 миллисекунд в 99,9% случаев. SLA, с другой стороны, похожи на официальные контракты с нашими пользователями или клиентами. Они определяют минимальный уровень обслуживания, который мы обязуемся предоставлять. Так что, если наш SLA гарантирует доступность 99,99%, а мы опускаемся ниже этого, нам, возможно, придется предоставить возмещение или другую компенсацию нашим клиентам. Встраивание отказоустойчивости в нашу систему означает ожидание неожиданного. Это может означать внедрение избыточных систем, обеспечение того, чтобы всегда был готов резервный вариант для замены в случае сбоя, или это может означать проектирование нашей системы для плавного снижения производительности, так что даже если определенные функции недоступны, основная функциональность остается нетронутой. Для измерения этого аспекта мы используем надежность, отказоустойчивость и избыточность. Надежность означает обеспечение того, что наша система работает правильно и последовательно. Отказоустойчивость — это подготовка к тому, когда что-то пойдет не так: как наша система справляется с неожиданными сбоями или атаками? А избыточность — это наличие резервных копий, гарантирующих, что если одна часть нашей системы выйдет из строя, другая будет готова ее заменить. Нам также нужно измерять скорость нашей системы, и для этого у нас есть пропускная способность и задержка. Пропускная способность измеряет, сколько данных может обрабатывать наша система за определенный период времени. У нас есть пропускная способность сервера, измеряемая в запросах в секунду (RPS). Этот показатель дает представление о том, сколько клиентских запросов может обработать сервер за данный период времени. Более высокое значение RPS обычно указывает на лучшую производительность и способность обрабатывать больше одновременных пользователей. У нас есть пропускная способность базы данных, измеряемая в запросах в секунду (QPS). Это количественно определяет количество запросов, которое база данных может обработать в секунду. Как и пропускная способность сервера, более высокое значение QPS обычно означает лучшую производительность. И у нас также есть пропускная способность данных, измеряемая в байтах в секунду. Это отражает объем данных, передаваемых по сети или обрабатываемых системой за данный период времени. С другой стороны, задержка измеряет, сколько времени требуется для обработки одного запроса. Это время, которое требуется запросу, чтобы получить ответ. И оптимизация одного часто может привести к компромиссам в другом. Например, пакетная обработка операций может увеличить пропускную способность, но также может увеличить задержку. И проектирование системы с ошибками может привести к множеству проблем в дальнейшем, от узких мест производительности до уязвимостей безопасности. И в отличие от кода, который можно легко рефакторить, перепроектирование системы может быть монументальной задачей. Вот почему крайне важно инвестировать время и ресурсы в правильное проектирование с самого начала и заложить прочный фундамент, который сможет выдержать вес будущих функций и рост пользователей. Теперь давайте поговорим об основах сетей. Когда мы говорим об основах сетей, мы, по сути, обсуждаем, как компьютеры общаются друг с другом. В основе этого общения лежит IP-адрес — уникальный идентификатор каждого устройства в сети. Адреса IPv4 имеют 32 бита, что позволяет получить примерно 4 миллиарда уникальных адресов. Однако с увеличением числа устройств мы переходим к IPv6, который использует 128-битные адреса, значительно увеличивая количество доступных уникальных адресов. Когда два компьютера общаются по сети, они отправляют и получают пакеты данных, и каждый пакет содержит IP-заголовок, который содержит важную информацию, такую как IP-адреса отправителя и получателя, гарантируя, что данные достигают правильного назначения. Этот процесс регулируется Интернет-протоколом, который представляет собой набор правил, определяющих, как данные отправляются и получаются. Помимо уровня IP, у нас также есть прикладной уровень, где хранятся данные, специфичные для протокола приложения. Данные в этих пакетах форматируются в соответствии с конкретными протоколами прикладного уровня, такими как HTTP для веб-браузинга, чтобы данные правильно интерпретировались принимающим устройством. Как только мы поймем основы IP-адресации и пакетов данных, мы можем перейти к транспортному уровню, где в игру вступают TCP и UDP. TCP работает на транспортном уровне и обеспечивает надежную связь. Это похоже на курьера, который гарантирует, что ваша посылка не только прибудет, но и проверяет, ничего ли не пропало. Таким образом, каждый пакет данных также включает TCP-заголовок, который содержит важную информацию, такую как номера портов и управляющие потоки, необходимые для управления соединением и потоком данных. TCP известен своей надежностью; он обеспечивает полную и правильную доставку пакетов данных. Он достигает этого с помощью таких функций, как порядковые номера, которые отслеживают порядок пакетов, и процесс, известный как трехстороннее рукопожатие, которое устанавливает стабильное соединение между двумя устройствами. В отличие от этого, UDP быстрее, но менее надежен, чем TCP. Он не устанавливает соединение перед отправкой данных и не гарантирует доставку или порядок пакетов. Но это делает UDP предпочтительным для связи, чувствительной ко времени, такой как видеозвонки или прямые трансляции, где скорость имеет решающее значение, а некоторая потеря данных допустима. Чтобы связать все эти концепции воедино, давайте поговорим о DNS (системе доменных имен). DNS действует как телефонная книга Интернета, преобразуя удобные для человека доменные имена в IP-адреса. Когда вы вводите URL-адрес в свой браузер, браузер отправляет DNS-запрос, чтобы найти соответствующий IP-адрес, что позволяет ему установить соединение с сервером и получить веб-страницу. Функционирование DNS контролируется ICANN, которая координирует глобальное адресное пространство IP и систему доменных имен. А регистраторы доменных имен, такие как Namecheap или GoDaddy, аккредитованы ICANN для продажи доменных имен общественности. DNS использует различные типы записей, такие как A-записи, которые сопоставляют домен с соответствующим IP-адресом, гарантируя, что ваш запрос достигает правильного сервера, или AAAA-записи, которые сопоставляют доменное имя с адресом IPv6. И, наконец, давайте поговорим о сетевой инфраструктуре, которая поддерживает все эти коммуникации. Устройства в сети имеют либо общедоступные, либо частные IP-адреса. Общедоступные IP-адреса уникальны в Интернете, в то время как частные IP-адреса уникальны в локальной сети. IP-адрес может быть постоянно назначен устройству или динамически меняться со временем. Динамические IP-адреса обычно используются для домашних интернет-соединений. И устройства, подключенные в локальной сети, могут общаться друг с другом напрямую. И для защиты этих сетей мы используем брандмауэры, которые отслеживают и контролируют входящий и исходящий сетевой трафик. И внутри устройства конкретные процессы или службы идентифицируются портами, которые в сочетании с IP-адресом создают уникальный идентификатор сетевой службы. Некоторые порты зарезервированы для конкретных протоколов, таких как 80 для HTTP или 22 для SSH. Теперь давайте рассмотрим все основные протоколы прикладного уровня. Самым распространенным протоколом из них является HTTP (Hypertext Transfer Protocol), который построен на TCP/IP. Это протокол запрос-ответ, но представьте его как разговор без памяти. Каждое взаимодействие отдельное, без воспоминаний о прошлом. Это означает, что серверу не нужно хранить какой-либо контекст между запросами. Вместо этого каждый запрос содержит всю необходимую информацию. И обратите внимание, как заголовки включают такие детали, как URL и метод, в то время как тело несет суть запроса или ответа. Каждый ответ также включает код состояния, который просто предоставляет обратную связь о результате запроса клиента на сервере. Например, 2xx — это коды успеха. Они указывают на то, что запрос был успешно получен и обработан. 3xx — это коды перенаправления. Они означают, что для выполнения запроса требуется дальнейшее действие со стороны пользовательского агента. 4xx — это коды ошибок клиента. Они используются, когда запрос содержит синтаксическую ошибку или не может быть выполнен. И 5xx — это коды ошибок сервера. Они указывают на то, что на сервере что-то пошло не так. У нас также есть методы для каждого запроса. Наиболее распространенные методы: GET, POST, PUT, PATCH и DELETE. GET используется для получения данных. POST обычно используется для создания данных на сервере. PUT и PATCH используются для обновления записи. А DELETE — для удаления записи из базы данных. HTTP — это одностороннее соединение, но для обновлений в реальном времени мы используем WebSockets, которые обеспечивают двусторонний канал связи по одному долгоживущему соединению, позволяя серверам отправлять обновления в реальном времени клиентам. Это очень важно для приложений, требующих постоянных обновлений данных без накладных расходов на повторяющиеся циклы запросов-ответов HTTP. Он обычно используется для чат-приложений, обновлений спортивных событий в реальном времени или котировок фондового рынка, где действие никогда не останавливается, как и разговор. Из протоколов, связанных с электронной почтой, SMTP является стандартом для передачи электронной почты через Интернет. Это протокол для отправки сообщений электронной почты между серверами. Большинство почтовых клиентов используют SMTP для отправки электронной почты и либо IMAP, либо POP3 для их получения. IMAP используется для получения электронной почты с сервера, позволяя клиенту получать доступ к сообщениям и манипулировать ими. Это идеально подходит для пользователей, которым нужен доступ к своей электронной почте с нескольких устройств. POP3 используется для загрузки электронной почты с сервера на локальный клиент, обычно используется, когда электронная почта управляется с одного устройства. Переходя к протоколам передачи и управления файлами, традиционным протоколом для передачи файлов через Интернет является FTP, который часто используется при обслуживании веб-сайтов и передаче больших объемов данных. Он используется для передачи файлов между клиентом и сервером, полезен для загрузки файлов на сервер или резервного копирования файлов. И у нас также есть SSH (Secure Shell), который предназначен для безопасной работы сетевых служб в незащищенной сети. Он обычно используется для входа в удаленную машину и выполнения команд или передачи файлов. Существуют также протоколы связи в реальном времени, такие как WebRTC, который позволяет приложениям браузер-браузер для голосовых вызовов, видеочатов и обмена файлами без внутренних или внешних плагинов. Это важно для таких приложений, как видеоконференции и прямые трансляции. Еще один — MQTT, который является легким протоколом обмена сообщениями, идеально подходящим для устройств с ограниченной вычислительной мощностью и в сценариях, требующих низкой пропускной способности, таких как устройства IoT. А AMQP — это протокол для ориентированных на сообщения промежуточных программ, обеспечивающий надежность и безопасность для корпоративного обмена сообщениями. Например, он используется в таких инструментах, как RabbitMQ. Давайте также поговорим об RPC (Remote Procedure Call), который представляет собой протокол, позволяющий программе на одном компьютере выполнять код на сервере или другом компьютере. Это метод, используемый для вызова функции так, как если бы это был локальный вызов, в то время как на самом деле функция выполняется на удаленной машине. Таким образом, он абстрагирует детали сетевого взаимодействия, позволяя разработчику беспрепятственно взаимодействовать с удаленными функциями, как если бы они были локальными для приложения. И многие протоколы прикладного уровня используют механизмы RPC для выполнения своих операций. Например, в веб-службах HTTP-запросы могут приводить к вызовам RPC на бэкенде для обработки данных или выполнения действий от имени клиента, или SMTP-серверы могут использовать RPC-вызовы внутри себя для обработки сообщений электронной почты или взаимодействия с базами данных. Конечно, существует множество других протоколов прикладного уровня, но здесь рассмотренные являются одними из наиболее часто используемых и необходимых для веб-разработки. В этом разделе давайте пройдемся по проектированию API, начиная с основ и переходя к лучшим практикам, которые определяют исключительные API. Давайте рассмотрим API для платформы электронной коммерции, такой как Shopify, которая, если вы не знакомы, является известной платформой электронной коммерции, позволяющей предприятиям создавать интернет-магазины. В проектировании API мы занимаемся определением входных данных, таких как детали продукта для нового продукта, которые предоставляются продавцом, и выходных данных, таких как информация, возвращаемая, когда кто-то запрашивает продукт через API. Таким образом, основное внимание уделяется определению того, как CRUD-операции предоставляются пользовательскому интерфейсу. CRUD означает Create (создать), Read (читать), Update (обновить) и Delete (удалить), которые являются базовыми операциями любого приложения, управляемого данными. Например, чтобы добавить новый продукт, нам нужно отправить POST-запрос на `/api/products`, где детали продукта отправляются в теле запроса. Чтобы получить эти продукты, нам нужно отправить GET-запрос на `/api/products`. Для обновления мы используем PUT или PATCH-запросы на `/product/{id_продукта}`. А удаление похоже на обновление: это снова `/product/{id_продукта}`, который нам нужно удалить. И аналогично, у нас может быть еще один GET-запрос на `/product/{id_продукта}`, который извлекает один продукт. Другая часть — это выбор протокола связи, который будет использоваться, например HTTP, WebSockets или другие протоколы, и механизма передачи данных, который может быть JSON, XML или Protocol Buffers. Это обычно относится к RESTful API, но у нас также есть парадигмы GraphQL и gRPC. Таким образом, API бывают разных парадигм, каждая со своим набором протоколов и стандартов. Самая распространенная — REST, которая означает Representational State Transfer. Она stateless, что означает, что каждый запрос от клиента к серверу должен содержать всю информацию, необходимую для понимания и выполнения запроса. Она использует стандартные HTTP-методы GET, POST, PUT и DELETE, и ее легко использовать различным клиентам, браузерам или мобильным приложениям. Недостатком RESTful API является то, что они могут приводить к чрезмерному или недостаточному получению данных, поскольку для доступа к конкретным данным может потребоваться больше конечных точек. И обычно RESTful API используют JSON для обмена данными. С другой стороны, GraphQL API позволяют клиентам запрашивать именно то, что им нужно, избегая чрезмерного или недостаточного получения данных. У них есть строго типизированные запросы, но сложные запросы могут повлиять на производительность сервера. И все запросы отправляются как POST-запросы, а GraphQL API обычно отвечает кодом состояния HTTP 200, даже в случае ошибок, с деталями ошибок в теле ответа. gRPC означает Google Remote Procedure Call, который построен на HTTP/2, который предоставляет расширенные функции, такие как мультиплексирование и push-сервер. Он использует Protocol Buffers, который является способом сериализации структурированных данных, и благодаря этому он достаточен с точки зрения пропускной способности и ресурсов, особенно подходит для микросервисов. Недостатком является то, что он менее читаем для человека по сравнению с JSON и требует поддержки HTTP/2 для работы. В контексте электронной коммерции у вас могут быть отношения, такие как пользователь к заказам или заказы к продуктам, и вам нужно спроектировать конечные точки, чтобы отразить эти отношения. Например, чтобы получить заказы для конкретного пользователя, вам нужно выполнить запрос GET `/users/{user_id}/orders`. Общие запросы также включают `limit` и `offset` для пагинации или `start_date` и `end_date` для фильтрации продуктов в определенном диапазоне дат. Это позволяет пользователям или клиенту получать конкретные наборы данных, не перегружая систему. Хорошо спроектированный GET-запрос должен быть идемпотентным, то есть многократный вызов не изменяет результат, и он всегда должен возвращать один и тот же результат. А GET-запросы никогда не должны изменять данные; они предназначены только для извлечения. Если вам нужно обновить или создать данные, вам нужно выполнить PUT или POST-запрос. При изменении конечных точек важно поддерживать обратную совместимость. Это означает, что нам нужно убедиться, что изменения не нарушают существующие клиенты. Распространенной практикой является введение новых версий, таких как `/v2/products`, чтобы API версии 1 мог по-прежнему обслуживать старых клиентов, а API версии 2 должен обслуживать текущих клиентов. Это в случае RESTful API. В случае GraphQL API добавление новых полей, таких как поля V2, без удаления старых, помогает развивать API без нарушения существующих клиентов. Еще одна лучшая практика — установить ограничения скорости (rate limiting). Это может предотвратить DDoS-атаки на API. Он используется для контроля количества запросов, которое пользователь может сделать за определенный период времени, и предотвращает отправку одним пользователем слишком большого количества запросов к вашему единственному API. Распространенной практикой также является настройка параметров CORS (Cross-Origin Resource Sharing). С настройками CORS вы можете контролировать, какие домены могут получить доступ к вашему API, предотвращая нежелательные межсайтовые взаимодействия. Теперь представьте, что компания размещает веб-сайт на сервере в дата-центрах Google Cloud в Финляндии. Загрузка может занять около 100 миллисекунд для пользователей в Европе, но 3-5 секунд для пользователей в Мексике. К счастью, существуют стратегии для минимизации этой задержки запросов для пользователей, которые находятся далеко. Эти стратегии называются кэшированием и сетями доставки контента (CDN), которые являются двумя важными концепциями в современной веб-разработке и проектировании систем. Кэширование — это метод, используемый для повышения производительности и эффективности системы. Он включает в себя хранение копии определенных данных во временном хранилище, чтобы будущие запросы на эти данные могли быть обслужены быстрее. Существует четыре распространенных места, где может храниться кэш. Первое — это кэширование браузера, где мы храним ресурсы веб-сайта на локальном компьютере пользователя. Поэтому, когда пользователь повторно посещает сайт, браузер может загрузить сайт из локального кэша, а не загружать все с сервера снова. Пользователи могут отключить кэширование, изменив настройки браузера. В большинстве браузеров разработчики могут отключить кэш из инструментов разработчика. Например, в Chrome у нас есть опция «Отключить кэш» на вкладке «Сеть» инструментов разработчика. Кэш хранится в каталоге на жестком диске клиента, управляемом браузером. И кэши браузера хранят файлы HTML, CSS и JS-пакетов на локальной машине пользователя, обычно в выделенном каталоге кэша, управляемом браузером. Мы используем заголовок `Cache-Control`, чтобы сообщить браузеру, как долго этот контент должен кэшироваться. Например, здесь `Cache-Control` установлен на 7200 секунд, что эквивалентно 2 часам. Когда запрошенные данные найдены в кэше, мы называем это «кэш-попадание» (cache hit). И наоборот, у нас есть «кэш-промах» (cache miss), который происходит, когда запрошенные данные отсутствуют в кэше, что требует получения из исходного источника. А коэффициент кэширования — это процент запросов, которые обслуживаются из кэша по сравнению со всеми запросами. И более высокий коэффициент указывает на более эффективный кэш. Вы можете проверить, произошел ли кэш-попадание или промах, по заголовку `X-Cache`. Например, в этом случае он говорит «Miss», так что кэш-промах произошел. А в случае, если кэш найден, у нас будет здесь «Hit». У нас также есть серверное кэширование, которое включает хранение часто используемых данных на стороне сервера, уменьшая необходимость выполнять дорогостоящие операции, такие как запросы к базе данных. Серверные кэши хранятся на сервере или на отдельном сервере кэширования, либо в памяти (например, Redis), либо на диске. Обычно сервер проверяет кэш на наличие данных перед запросом к базе данных. Если данные находятся в кэше, они возвращаются напрямую. В противном случае сервер запрашивает базу данных, и если данные не находятся в кэше, сервер извлекает их из базы данных, возвращает пользователю, а затем сохраняет их в кэше для будущих запросов. Это случай кэша «запись вокруг» (write-around cache), где данные записываются непосредственно в постоянное хранилище, минуя кэш. Он используется, когда производительность записи менее критична. У вас также есть кэш «запись через» (write-through cache), где данные одновременно записываются в кэш и в постоянное хранилище. Он обеспечивает согласованность данных, но может быть медленнее, чем кэш «запись вокруг». И у нас также есть кэш «обратной записи» (write-back cache), где данные сначала записываются в кэш, а затем в постоянное хранилище позже. Это улучшает производительность записи, но вы рискуете потерять эти данные в случае сбоя сервера. Но что происходит, если кэш заполнен, и нам нужно освободить место, чтобы снова использовать наш кэш? Для этого у нас есть политики вытеснения, которые представляют собой правила, определяющие, какие элементы следует удалить из кэша, когда он заполнен. Распространенные политики: удалять наименее недавно использованные (LRU), или «первым пришел — первым ушел» (FIFO), где мы удаляем те, которые были добавлены первыми, или удалять наименее часто используемые (LFU). Кэширование базы данных — еще один важный аспект, и оно относится к практике кэширования результатов запросов к базе данных для повышения производительности приложений, управляемых базами данных. Это часто делается либо в самой системе базы данных, либо через внешний слой кэширования, такой как Redis или Memcached. Когда делается запрос, мы сначала проверяем кэш, чтобы увидеть, был ли сохранен результат этого запроса. Если да, мы возвращаем кэшированное состояние, избегая необходимости выполнять запрос к базе данных. Но если данные не найдены в кэше, запрос выполняется к базе данных, и результат сохраняется в кэше для будущих запросов. Это полезно для приложений с интенсивным чтением, где некоторые запросы выполняются часто. И мы используем те же политики вытеснения, что и для серверного кэширования. Другой тип кэширования — CDN (сети доставки контента), которые представляют собой сеть серверов, распределенных географически. Они обычно используются для обслуживания статического контента, такого как файлы JavaScript, HTML, CSS или изображения и видео. Они кэшируют контент с исходного сервера и доставляют его пользователям с ближайшего сервера CDN. Когда пользователь запрашивает файл, такой как изображение или веб-сайт, запрос перенаправляется на ближайший сервер CDN. Если на сервере CDN есть кэшированный контент, он доставляется пользователю. Если нет, он извлекает контент с исходного сервера, кэширует его, а затем пересылает пользователю. Это «пул-основанный» (pull-based) тип CDN, где CDN автоматически извлекает контент с исходного сервера, когда он впервые запрашивается пользователем. Он идеально подходит для веб-сайтов с большим количеством статического контента, который часто обновляется. Он требует меньшего активного управления, поскольку CDN автоматически поддерживает контент в актуальном состоянии. Другой тип — CDN с «push-доставкой» (push-based). Это когда вы загружаете контент на исходный сервер, а затем он распространяет эти файлы на CDN. Это полезно, когда у вас есть большие файлы, которые редко обновляются, но должны быстро распространяться при обновлении. Это требует более активного управления тем, какой контент хранится на CDN. Мы снова используем заголовок `Cache-Control`, чтобы сообщить браузеру, как долго он должен кэшировать контент из CDN. CDN обычно используются для доставки статических ресурсов, таких как изображения, файлы CSS, пакеты JavaScript или видеоконтент. И это может быть полезно, если вам нужно обеспечить высокую доступность и производительность для пользователей. Это также может снизить нагрузку на исходный сервер. Но есть некоторые случаи, когда нам все еще нужно обращаться к нашему исходному серверу, например, при обслуживании динамического контента, который часто меняется, или при выполнении задач, требующих обработки в реальном времени. И в случаях, когда приложение требует сложной серверной логики, которую нельзя выполнить в CDN. Некоторые преимущества, которые мы получаем от CDN: снижение задержки. Обслуживая контент из мест, более близких к пользователю, CDN значительно снижают задержку. Они также обеспечивают высокую доступность и масштабируемость. CDN могут обрабатывать высокие нагрузки трафика и устойчивы к сбоям оборудования. Они также улучшают безопасность, поскольку многие CDN предлагают функции безопасности, такие как защита от DDoS и шифрование трафика. И преимущества кэширования также включают снижение задержки, поскольку у нас есть быстрый доступ к данным, так как данные извлекаются из ближайшего кэша, а не с удаленного сервера. Это снижает нагрузку на сервер, уменьшая количество запросов к основному источнику данных, снижая нагрузку на сервер и общее ускорение загрузки приводит к лучшему пользовательскому опыту. Теперь давайте поговорим о прокси-серверах, которые действуют как посредник между клиентом, запрашивающим ресурс, и сервером, предоставляющим этот ресурс. Они могут служить различным целям, таким как кэширование ресурсов для более быстрого доступа, анонимизация запросов и балансировка нагрузки между несколькими серверами. По сути, они получают запросы от клиентов, пересылают их соответствующим серверам, а затем возвращают ответы серверов клиенту. Существует несколько типов прокси-серверов, каждый из которых служит разным целям. Вот некоторые из основных типов. Первый — это прямой прокси (forward proxy), который находится перед клиентами и используется для отправки запросов на другие серверы в Интернете. Он часто используется в локальных сетях для контроля доступа в Интернет. Следующий — это обратный прокси (reverse proxy), который находится перед одним или несколькими веб-серверами, перехватывая запросы от Интернета. Он используется для балансировки нагрузки, ускорения работы веб-сайтов и в качестве уровня безопасности. Другой тип — открытый прокси (open proxy), который позволяет любому пользователю подключаться и использовать прокси-сервер, часто используется для анонимизации веб-браузинга и обхода ограничений контента. У нас также есть типы прозрачных прокси (transparent proxy), которые передают запросы и ресурсы без их изменения, но они видимы для клиента и часто используются для кэширования и фильтрации контента. Следующий тип — анонимный прокси (anonymous proxy), который идентифицируется как прокси-сервер, но не раскрывает исходный IP-адрес. Этот тип используется для анонимного браузинга. У нас также есть искажающие прокси (distorting proxy), которые предоставляют неверный исходный IP-адрес целевому серверу. Этот тип похож на анонимный прокси, но с намеренным искажением IP-адреса. И следующий популярный тип — прокси с высокой степенью анонимности (high anonymity proxy) или элитный прокси (elite proxy), которые делают обнаружение использования прокси очень трудным. Эти прокси не отправляют заголовки `X-Forwarded-For` или другие идентифицирующие заголовки и обеспечивают максимальную анонимность. Наиболее часто используемыми прокси-серверами являются прямые и обратные прокси. Прямой прокси действует как промежуточный слой между клиентом и сервером. Он находится между клиентом, который может быть компьютером в локальной сети, и внешними серверами, которые могут быть веб-сайтами в Интернете. Когда клиент делает запрос, он сначала отправляется на прямой прокси. Затем прокси оценивает запрос и решает на основе своей конфигурации и правил, разрешить ли запрос, изменить его или заблокировать. Одной из основных функций прямого прокси является скрытие IP-адреса клиента при пересылке запроса на целевой сервер. Создается впечатление, что запрос исходит от самого прокси-сервера. Давайте посмотрим на некоторые примеры использования прямых прокси. Один популярный пример — прокси для Instagram. Это специфический тип прямого прокси, используемый для управления несколькими учетными записями Instagram без срабатывания блокировок или ограничений. Маркетологи и менеджеры социальных сетей используют прокси для Instagram, чтобы создавать впечатление, будто они находятся в другом месте или как разные пользователи, что позволяет им управлять несколькими учетными записями, автоматизировать задачи или собирать данные без пометки как подозрительная активность. Следующий пример — контроль и мониторинг использования Интернета. Некоторые организации используют прямые прокси для мониторинга и контроля использования Интернета сотрудниками. Они могут блокировать доступ к не связанным сайтам и защищать от веб-угроз. Они также могут сканировать входящий контент на наличие вирусов и вредоносных программ. Следующий распространенный сценарий использования — кэширование часто используемого контента. Прямые прокси также могут кэшировать популярные веб-сайты или контент, сокращая использование полосы пропускания и ускоряя доступ для пользователей в сети. Это особенно полезно в сетях, где полоса пропускания дорога или ограничена. И это также может использоваться для анонимизации веб-доступа. Люди, обеспокоенные конфиденциальностью, могут использовать прямые прокси, чтобы скрыть свой IP-адрес и другую идентифицирующую информацию от веб-сайтов, которые они посещают, и затруднить отслеживание их действий в Интернете. С другой стороны, обратный прокси — это тип прокси-сервера, который находится перед одним или несколькими веб-серверами, перехватывая запросы от клиентов, прежде чем они достигнут серверов. В то время как прямой прокси скрывает личность клиента, обратный прокси, по сути, скрывает личность сервера или существование нескольких серверов за ним. Клиент взаимодействует только с обратным прокси и может не знать о серверах за ним. Он также распределяет клиентские запросы между несколькими серверами, балансируя нагрузку и гарантируя, что ни один сервер не будет перегружен. Обратные прокси также могут сжимать входящие и исходящие данные, кэшировать файлы и управлять SSL-шифрованием, тем самым ускоряя время загрузки и уменьшая нагрузку на сервер. Некоторые распространенные сценарии использования обратных прокси: балансировщики нагрузки. Они распределяют входящий сетевой трафик между несколькими серверами, гарантируя, что ни один сервер не получает слишком большую нагрузку. Распределяя трафик эффективно, мы предотвращаем превращение любого отдельного сервера в узкое место и поддерживаем оптимальную скорость и надежность обслуживания. CDN также являются типом обратных прокси. Это сеть серверов, которые доставляют кэшированный статический контент с веб-сайтов пользователям на основе географического положения пользователя. Они действуют как обратные прокси, извлекая контент с исходного сервера и кэшируя его, чтобы он находился ближе к пользователю для более быстрой доставки. Другой пример — межсетевые экраны веб-приложений (WAF), которые располагаются перед веб-приложениями. Они проверяют входящий трафик, чтобы блокировать попытки взлома и фильтровать нежелательный трафик. Межсетевые экраны также защищают приложение от распространенных веб-уязвимостей. Другой пример — SSL-разгрузка или ускорение. Некоторые обратные прокси обрабатывают шифрование и дешифрование SSL/TLS-трафика, разгружая эту задачу с веб-серверов для оптимизации их производительности. Балансировщики нагрузки, пожалуй, являются наиболее популярными сценариями использования прокси-серверов. Они распределяют входящий трафик между несколькими серверами, чтобы убедиться, что ни один сервер не несет слишком большой нагрузки. Эффективно распределяя запросы, они увеличивают мощность и надежность приложений. Вот некоторые распространенные стратегии и алгоритмы, используемые в балансировке нагрузки. Первый — Round Robin (циклический перебор). Это простейшая форма балансировки нагрузки, где каждый сервер в пуле получает запрос в последовательном вращающемся порядке. Когда достигается последний сервер, он возвращается к первому. Этот тип хорошо работает для серверов с аналогичными спецификациями и когда нагрузка равномерно распределяется. Далее у нас есть алгоритм наименьшего количества соединений (least connections), который направляет трафик на сервер с наименьшим количеством активных соединений. Он идеально подходит для более длительных задач или когда нагрузка на сервер распределена неравномерно. Далее у нас есть алгоритм наименьшего времени отклика (least response time), который выбирает сервер с наименьшим временем отклика и наименьшим количеством активных соединений. Это эффективно, и цель — обеспечить самый быстрый ответ на запросы. Следующий алгоритм — IP Hashing (хеширование по IP-адресу), который определяет, какой сервер получает запрос на основе хеша IP-адреса клиента. Это гарантирует, что клиент постоянно подключается к одному и тому же серверу, и полезно для сохранения сеанса в приложениях, где важно, чтобы клиент постоянно подключался к одному и тому же серверу. Варианты этих методов также могут быть взвешенными, что приводит нас к взвешенным алгоритмам. Например, во взвешенном циклическом переборе (weighted round robin) или взвешенном наименьшем количестве соединений (weighted least connections) серверам назначаются веса, обычно на основе их мощности или метрик производительности, и более мощные серверы обрабатывают больше запросов. Это эффективно, когда серверы в пуле имеют разные возможности, такие как разный ЦП или разная ОЗУ. У нас также есть географические алгоритмы, которые направляют запросы на сервер, географически ближайший к пользователю, или на основе конкретных региональных требований. Это полезно для глобальных служб, где снижение задержки является приоритетом. И следующий распространенный алгоритм — согласованное хеширование (consistent hashing), которое использует хеш-функцию для распределения данных по различным узлам. Представьте себе хеш-пространство, образующее круг, где конец возвращается к началу, часто называемое кольцом хеширования. И узлы, и данные, такие как ключи или хранимые значения, хешируются на это кольцо. Это гарантирует, что клиент постоянно подключается к одному и тому же серверу каждый раз. Важной функцией балансировщиков нагрузки является непрерывная проверка работоспособности серверов, чтобы гарантировать, что трафик направляется только на серверы, которые онлайн и отвечают. Если сервер выходит из строя, балансировщик нагрузки перестает отправлять на него трафик, пока он не вернется в сеть. И балансировщики нагрузки могут быть в различных формах, включая аппаратные устройства, программные решения и облачные сервисы. Некоторые популярные аппаратные балансировщики нагрузки: F5 BIG-IP, который является широко используемым аппаратным балансировщиком нагрузки, известным своей высокой производительностью и обширным набором функций. Он предлагает управление локальным трафиком, глобальную балансировку нагрузки серверов и безопасность приложений. Другой пример — Citrix (ранее NetScaler), который обеспечивает балансировку нагрузки, коммутацию контента и ускорение приложений. Некоторые популярные программные балансировщики нагрузки: HAProxy, который является популярным программным балансировщиком нагрузки с открытым исходным кодом и прокси-сервером для приложений на основе TCP и HTTP. И, конечно же, Nginx, который часто используется как веб-сервер, но также функционирует как балансировщик нагрузки и обратный прокси для HTTP и других сетевых протоколов. И некоторые популярные облачные балансировщики нагрузки: Elastic Load Balancing от AWS, Load Balancer от Microsoft Azure или Load Balancer от Google Cloud. Существуют даже некоторые виртуальные балансировщики нагрузки, такие как VMware Advanced Load Balancer, который предлагает программно-определяемый контроллер доставки приложений, который может быть развернут локально или в облаке. Теперь давайте посмотрим, что происходит, когда балансировщик нагрузки выходит из строя. Когда балансировщик нагрузки выходит из строя, это может повлиять на всю доступность и производительность приложения или служб, которыми он управляет. По сути, это единая точка отказа, и в случае его выхода из строя все серверы становятся недоступными для клиентов. Чтобы избежать или минимизировать влияние сбоя балансировщика нагрузки, существует несколько стратегий, которые могут быть применены. Первая — это реализация избыточной балансировки нагрузки путем использования более чем одного балансировщика нагрузки, часто парами, что является распространенным подходом. Если один из них выходит из строя, другой берет на себя управление, что является методом, известным как отказ (failover). Следующая стратегия — непрерывный мониторинг и проверка работоспособности самого балансировщика нагрузки. Это может гарантировать, что любые проблемы будут обнаружены на ранней стадии и могут быть устранены до того, как они вызовут значительные сбои. Мы также можем внедрить системы автоматического масштабирования и самовосстановления. Некоторые современные инфраструктуры спроектированы так, чтобы автоматически обнаруживать сбой балансировщика нагрузки и заменять его новым экземпляром без ручного вмешательства. И в некоторых конфигурациях отказ сети (network failover) может перенаправить трафик с IP-адреса, который больше не принимает соединения, например, с отказавшего балансировщика нагрузки, на предварительно настроенный резервный IP-адрес, который является нашим новым балансировщиком нагрузки. Собеседования по проектированию систем неполны без глубокого погружения в базы данных. В ближайшие несколько минут я познакомлю вас с основами баз данных, которые вам нужно понять, чтобы успешно пройти собеседование. Мы изучим роль баз данных в проектировании систем, методы шардинга и репликации, а также ключевые свойства ACID. Мы также обсудим различные типы баз данных, варианты вертикального и горизонтального масштабирования, а также методы повышения производительности баз данных. У нас есть разные типы баз данных, каждая из которых предназначена для конкретных задач и проблем. Давайте изучим их. Первый тип — реляционные базы данных. Думайте о реляционной базе данных как о хорошо организованном картотеке, где все файлы аккуратно отсортированы по разным ящикам и папкам. Некоторые популярные примеры SQL-баз данных: PostgreSQL, MySQL и SQLite. Все SQL-базы данных используют таблицы для хранения данных и SQL в качестве языка запросов. Они отлично подходят для транзакций, сложных запросов и целостности. Реляционные базы данных также соответствуют требованиям ACID, что означает, что они поддерживают свойства ACID. A означает атомарность, что означает, что транзакции — это все или ничего. C означает согласованность, что означает, что после транзакции ваша база данных должна находиться в согласованном состоянии. I означает изоляцию, что означает, что транзакции должны быть независимыми. А D означает долговечность, что означает, что после фиксации транзакции данные остаются там навсегда. У нас также есть NoSQL базы данных, которые отказываются от свойства согласованности из свойств ACID. Представьте себе NoSQL базу данных как доску для мозгового штурма с липкими заметками. Вы можете добавлять или удалять заметки в любой форме. Это гибко. Некоторые популярные примеры: MongoDB, Cassandra и Redis. Существуют различные типы NoSQL баз данных, такие как пары ключ-значение (например, Redis), документоориентированные базы данных (например, MongoDB) или графовые базы данных (например, Neo4j). NoSQL базы данных не имеют схемы, что означает, что у них нет внешних ключей между таблицами, которые связывают данные. Они хороши для неструктурированных данных, идеально подходят для масштабируемости, быстрой итерации и простых запросов. Существуют также базы данных в памяти. Это похоже на наличие доски для быстрых расчетов и временных набросков. Это быстро, потому что все находится в памяти. Некоторые примеры: Redis и Memcached. Они обеспечивают молниеносный доступ к данным и используются в основном для кэширования и хранения сеансов. Теперь давайте посмотрим, как мы можем масштабировать базы данных. Первый вариант — вертикальное масштабирование (scale up). При вертикальном масштабировании вы повышаете производительность вашей базы данных, улучшая возможности отдельного сервера, на котором работают данные. Это может включать увеличение мощности ЦП, добавление большего объема ОЗУ, добавление более быстрых или большего дискового хранилища или обновление сети. Но существует максимальный предел ресурсов, которые вы можете добавить к одной машине, и из-за этого это очень ограничено. Следующий вариант — горизонтальное масштабирование (scale out), которое включает добавление большего количества машин к существующему пулу ресурсов вместо модернизации одного устройства. Базы данных, поддерживающие горизонтальное масштабирование, распределяют данные по кластеру машин. Это может включать шардинг базы данных или репликацию данных. Первый вариант — шардинг базы данных, который заключается в распределении различных частей (шардов) набора данных по нескольким серверам. Это означает, что вы разделяете данные на более мелкие части и распределяете их по нескольким серверам. Некоторые стратегии шардинга включают: шардинг на основе диапазона (range-based sharding), где вы распределяете данные на основе диапазона заданного ключа; шардинг на основе каталога (directory-based sharding), который использует службу поиска для направления трафика на правильную базу данных; мы также имеем географический шардинг (geographical sharding), который разделяет базы данных на основе географического положения. И следующий вариант горизонтального масштабирования — репликация данных. Это сохранение копий данных на нескольких серверах для высокой доступности. У нас есть репликация Master-Slave, где у нас есть одна мастер-база данных и несколько подчиненных баз данных только для чтения, или у нас может быть репликация Master-Master, где несколько баз данных могут как читать, так и записывать. Масштабирование ваших данных — это одно, но вы также хотите получать к ним доступ быстрее. Так давайте поговорим о различных методах повышения производительности, которые могут помочь быстрее получать доступ к вашим данным. Самый очевидный — кэширование. Кэширование — это не только для веб-серверов; кэширование баз данных может выполняться через базы данных в памяти, такие как Redis. Вы можете использовать его для кэширования частых запросов и повышения производительности. Следующий метод — индексирование. Индексы — это еще один способ повысить производительность вашей базы данных. Создание индекса для часто используемых столбцов значительно ускорит время извлечения. И следующий метод — оптимизация запросов. Вы также можете рассмотреть возможность оптимизации запросов для быстрого доступа к данным. Это включает минимизацию соединений (joins) и использование таких инструментов, как анализатор SQL-запросов или план `EXPLAIN`, для понимания производительности вашего запроса. Во всех случаях вы должны помнить о теореме CAP, которая гласит, что вы можете иметь только два из трех: согласованность, доступность и устойчивость к разделению. При проектировании системы вы должны расставить приоритеты двум из них на основе требований, которые вам представлены на собеседовании. Если вам понравился этот интенсивный курс, рассмотрите возможность просмотра моих других видео о концепциях и собеседованиях по проектированию систем. До скорой встречи!