<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Mikhail</title>
    <description>The latest articles on DEV Community by Mikhail (@zabelin).</description>
    <link>https://dev.to/zabelin</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1188280%2F3d8086a0-9c06-4058-8b81-abdd51a97c89.png</url>
      <title>DEV Community: Mikhail</title>
      <link>https://dev.to/zabelin</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/zabelin"/>
    <language>en</language>
    <item>
      <title>Высоконагруженные системы. Глава 6. Секционирование.</title>
      <dc:creator>Mikhail</dc:creator>
      <pubDate>Tue, 06 Feb 2024 22:14:20 +0000</pubDate>
      <link>https://dev.to/zabelin/vysokonaghruzhiennyie-sistiemy-glava-5-siektsionirovaniie-1p91</link>
      <guid>https://dev.to/zabelin/vysokonaghruzhiennyie-sistiemy-glava-5-siektsionirovaniie-1p91</guid>
      <description>&lt;h2&gt;
  
  
  Основное
&lt;/h2&gt;

&lt;p&gt;Секционирование - это разбиение данных на секции, чтобы каждый элемент данных был в каждой секции (в отличиее от репликации, где каждый элемент данных хранится в каждом узле). Основная цель секционирования - масштабируемость, так как увеличивается пропускная способность на кол-во запросов, так как секции независимы. &lt;/p&gt;

&lt;p&gt;При этом секционирование и репликация используются чаще всего вместе, так как первое дает масштабирумость и второе - отказоустойчивость. На каждом узле хранится несколько секций (но не обязтельно все). И для одних секций один и тот же узел может быть ведущим, а для других секций - ведомым (что лишний раз показывает независимость секций).&lt;/p&gt;

&lt;h2&gt;
  
  
  Подходы к секционированию
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Секционирование по диапазону ключей
&lt;/h3&gt;

&lt;p&gt;Этот подход заключается в том, чтобы закрепить за каждой секций диапазон значению ключа. Например, за секцией 1 - значения от А до В, за секций 2 - от Г до Е и т.д. При этом внутри самой секции используются SS-таблицы (под капотом самобалансирующиеся структуры данных - AVL дерево, например), благодаря которым значения отсортированы и становися просто искать по диапазону.&lt;/p&gt;

&lt;p&gt;Однако такой подход имеет недостаток - вероятность "горячей точки". Это значение, по которому осуществлено секционирование, по которому слишком много данных (например, в декабре кол-во транзакций гораздо больше, чем в другие месяцы). В результате секция может быть перегружена данными.&lt;/p&gt;

&lt;h3&gt;
  
  
  Секциоинирование по хэшу ключа
&lt;/h3&gt;

&lt;p&gt;Этот подход решает проблему "горячих точек". Хорошая хэш-функция возвращает равномерно распределенные значения для разных ключей. Можно каждоый секции сопоставить диапазон значения хэш функции. Однако теряется возможность быстрого поиска по диапазону, так как близкие по значению ключи лежат теперь уже в разных секциях. Также стоит иметь в виду, что данный подход не защищает от крайнего случая "горячей точки", когда все операции происходит по одному и тому же ключю.&lt;/p&gt;

&lt;h2&gt;
  
  
  Секционирование и вторичные индексы
&lt;/h2&gt;

&lt;p&gt;Вторичные индексы в отличие от ключей не идентифицируют однозначно запись - а значит, нельзя им поставить в соответствие определенную секцию. Но есть 2 варианта, как использовать вторичный индекс.&lt;/p&gt;

&lt;h3&gt;
  
  
  Секционирование вторичных индексов по документам
&lt;/h3&gt;

&lt;p&gt;Этот подход заключается в том, что внутри каждой секции строится свой независимый индекс. Поэтому при добавлении/изменении данных в секции меняется только этот индекс. Поэтому его еще называют локальным.&lt;/p&gt;

&lt;p&gt;Минусом такого подхода является то, что при поиске приходится делать запросы во все секции и объединять полученные результаты, что весьма затратно по времени.&lt;/p&gt;

&lt;h3&gt;
  
  
  Секционирование вторичных индексов по термам
&lt;/h3&gt;

&lt;p&gt;Этот подход заключается в том, что индекс секционирован по нескольким узлам. Например, поле color - значениям цвета, начинаяющимся с буквы a до r, соответствует секция 1. А цветам от s до z - секция 2.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwqowdlmpeyuhoyhcsxex.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwqowdlmpeyuhoyhcsxex.png" alt="Image description" width="777" height="312"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Благодаря такому подходу скоросить поиска сильно возрастает по сравнению с индексом по документу. Однако скорость записи наоборот, падает, так как при этом подходе секции становятся связанными, ведь индекс по термам может содержать ссылку на данные из другой секции.&lt;/p&gt;

&lt;h2&gt;
  
  
  Маршрутизация запросов
&lt;/h2&gt;

&lt;p&gt;Когда клиент делает запрос, откуда он знает, к какому узлу обратиться (какие секции какому узлу соответствуют)? Есть несколько вариантов:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;сами узлы могут знать - тогда при обращении на узел, он может отправить на другой, если у него нет требуемой секции&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;использование gateway, у которого есть эта информация&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;хранить эту информацию на самом клиенте&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>высоконагруженныеприложения</category>
      <category>клеппман</category>
      <category>книги</category>
    </item>
    <item>
      <title>Высоконагруженные системы. Глава 5. Репликация.</title>
      <dc:creator>Mikhail</dc:creator>
      <pubDate>Fri, 02 Feb 2024 19:42:09 +0000</pubDate>
      <link>https://dev.to/zabelin/vysokonaghruzhiennyie-sistiemy-glava-5-rieplikatsiia-58p2</link>
      <guid>https://dev.to/zabelin/vysokonaghruzhiennyie-sistiemy-glava-5-rieplikatsiia-58p2</guid>
      <description>&lt;h2&gt;
  
  
  Основное
&lt;/h2&gt;

&lt;p&gt;Репликация - это хранение копий одних и тех же данных по нескольким серверам, соединенным по сети. Репликация служит для нескольких целей:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Доставка контента из географически близкого к пользователю сервера&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Чтобы система продолжала работать при отказе некоторых серверов БД&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Для горизонтально масштабирования по чтению&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Говоря о репликация, пользуются термином узел (синоним сервера). Узел при репликации может быть ведущим (куда может осуществляться и запись, и чтение) и ведомым (только для чтения).&lt;/p&gt;

&lt;p&gt;Репликация бывает следующих видов с точки зрения узлов:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Репликация с одним ведущим узлов. Клиенты пишут в один узел. После этого записи отправляются в ведомые узлы. Есть риск чтения устаревшых данных с узлов, куда данные по какой-то причине не дошли.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Репликация с несколькими ведущими узлами. Клиенты могут писать в разные узлы. После этого записи отправляются с ведущего на другие ведущие и ведомые. Доступность выше чем в варианте с одним узлов, однако появляется вероятность конфликтов при записи, когда после нескольких записей в разные ведущие узлы на них хранятся разные версии одной и той же записи и непонятно, какая итоговая.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Репликация без ведущего узла. Название немного не отражает сути - ведь все узлы ведущие. Клиенты могуть писать в любые узлы. Данные в этот момент не отправляются в другие узлы. Только при чтении происходит проверка всех узлов в поиске самой последней версии. Отдаются свежие данные и во время/после этого актуализируются остальные ноды.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Репликация бывает следующих видов в точки зрения момента отправки записей с одного узла на другой:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Синхронная. Гарантирует согласованность данных на всех узлах. Клиент всегда будет читать свежие данные. Однако обладает низкой доступностью, так как один недоступный узел влечет неуспех записи на остальных узлах, так как повлечет откат. И скорость записи снижается, так как клиент должен дождаться, пока данные скопируются на все узлы.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Асинхронная. Не гарантирует согласованности, поэтому есть вероятность чтения устаревшых данных. Обладает высокой доступностью, так как копирование с одного узла на другой происходит уже после того, как клиент получил "успех" при записи. Скорость записи высокая.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Смешанная. Не гарантирует абсолютной согласованности и обладает средней доступностью. На какие-то узлы репликация синхронна. После этого запись для клиента считается "успешной". Затем в фоне происходит копирование на остальные ведомые узлы.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Как раз смешанный вариант на практике показывает селя лучше всего.&lt;/p&gt;

&lt;h2&gt;
  
  
  Реализация репликации
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Операторная. Заключается в том, что с ведущего узла на ведомые передаются sql операторы. Однако в таком виде недерменированные функции (now(), rand()) возвращали бы на разных узлах разные результаты. Аналогично с счетчиками, которые должны для каждой записи выполняться строго в том же порядке, что и на ведущем узле. Поэтому на практике команды передаются немного в измененном виде - вызов недерменированных функция заменен на их результат на ведущем узле.&lt;br&gt;
Раньше такая репликация использовалась в MySQL.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Перенос WAL журнала&lt;br&gt;
Все записываемые данные заносятся в журнал упреждаюей записи. Этот журнал хранится в бинарном виде. Можно этот журнал отправлять на ведомые узлы. Недостаток в том, что есть привязка к внутренней структуре бинарного файла (к уровню физического представления), которая от версии к версии субд может меняться. Поэтому во первых, на всех узлах должны быть СУБД одинаковой версии, а обновление СУБД становится головной болью.&lt;br&gt;
Этот метод репликации используется в PostgreSQL.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Логическая репликация. Подразумевает использование логического журнала, в котором в утвержденном формате хранятся строки, однозначно описыващие операции с данными. Например, для обновления это идентификатор, список измененных полей и их новых значения. Для удаление - только идентификатор. С ведущего узла на ведомые передается записи из логического журнала. Формат этих записей не привязан к версии СУБД и может легко парсится внешними приложениями&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Проблемы задержки репликации
&lt;/h2&gt;

&lt;p&gt;Как уже написал, использование асинхронной репликации может приводить к несогласованности данных между узлами. Рассмотрим возможные проблемы и пути их решения.&lt;/p&gt;

&lt;h3&gt;
  
  
  Чтение своих же устаревшых записей
&lt;/h3&gt;

&lt;p&gt;Это очень частый сценарий - пользователь точно знает, какие данные вставлял или модифицировал. Поэтому, если отдадутся устаревшие данные, пользователь это сразу заметит. Вариантов решения несколько:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;"свои" данные читать только с ведущего узла (например, информация о своем профиле) - в большинстве случаев невозможен, так как таких данных может быть очень много. Да и вообще на корню рушит идею высокой доступности, ради которой затевалась идея с репликацией.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;отслеживать время последней модификации данных - на первое время читать с ведущего, а все остальное время с ведомого.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;развитие предыдущего пункта - передавать при чтении метку времени последней записи, и если напротив записи в БД метка меньше (т.е. изменения еще не дошли), то читать с ведущего&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;В идее с меткой времени также стоит помнить, что клиент может заходить с разных устройств, и информацию о времени последней модификации нужно хранить в централизованном месте (этим снижается доступность).&lt;/p&gt;

&lt;h3&gt;
  
  
  Монотонное чтение
&lt;/h3&gt;

&lt;p&gt;Есть вероятность, что при каждом чтении пользовател будет видеть данные в разном порядке.&lt;br&gt;
Возможное решение - дать пользователю возможность читать только с какой-то одной реплики (которая присваиватся пользователю по хэшу ид пользователя, например). Тогда есть гарантия, что чтения будут в неименающимся порядке порядке (но гарантии чтения самых актуальных данных нет, так как данные по присвоенного ведомого узла могли не дойти). Также нет гарантии, что пользователь будет видеть записи в том порядке, в котором они были при записи (для этого при чтении можно делать сортировку).&lt;/p&gt;

&lt;h3&gt;
  
  
  Согласованное префиксное чтение
&lt;/h3&gt;

&lt;p&gt;Есть вероятность потери причинно-следственных связей, если на реплику более новая запись попала раньше более старой (например, сначала ответ, затем - чтение). Гарантия согласованного префиксного чтения гарантирует, что данные будут прочитаны в том же порядке, в котором были записаны. Для этого можно использовать метки времени, читая в отсортированном виде.&lt;/p&gt;

&lt;h2&gt;
  
  
  Репликация с несколькими ведущими узлами
&lt;/h2&gt;

&lt;h3&gt;
  
  
  обработка конфликтов
&lt;/h3&gt;

&lt;p&gt;Предположим, что есть 2 ведущих узла - 1 и 2. И есть пользователь 1 и 2, и записывают они данные в таком порядке 1 -&amp;gt; 1, 2 -&amp;gt; 2. Первый пользователь меняет запись A на B на ведущем узле 1, второй - A на C на ведущем узле 2. Возникает конфликт при попытке реплицировать изменения с одного ведущего на другой. Возможное решение - изменение одной и той же строки делать через один и тот же узел. Если узел выходит из строя, менять на другой. Если такой возможности нет, то можно напротив строчки ставить метку времени и при конфликте выигрывает значение с самым большим значением этой метки.&lt;/p&gt;

&lt;h3&gt;
  
  
  Топология репликации с несколькими ведущими узлами
&lt;/h3&gt;

&lt;p&gt;Сущестуют следущие топологии&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Колько&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Звезда&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Каждый с каждым&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr4pzt7pd9lqdzfpgb1fe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr4pzt7pd9lqdzfpgb1fe.png" alt="Image description" width="676" height="192"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Топология "каждый с каждым" самая общая и надежная. Топология "звезда" подразумевает использование центрального узла, через которой идет коммуникация, что снижает надежность. Топология "кольцо" самая ненадежная (и несмотря на это, используется в MySQL), так как узел может получить запись строго от конкретного узла. И если один узел вышел из строя, то репликация до узлов, следующих за ним, встанет.&lt;/p&gt;

&lt;h2&gt;
  
  
  Репликация без ведущих узлов
&lt;/h2&gt;

&lt;p&gt;При записи какие-то узлы могут быть недоступны. Пусть есть n узлов, на w узлов усуществилась запись и с r узлов читают. Тогда гарантией, что всегда при такой конфигурации будут читаться актульные данные, является условие w + r &amp;gt; n (это условие показывает, что узлы для записи и чтения пересекаются) - тогда операции записи и чтения называются операциями по кворуму. Тогда можно считать итоговый результат операции записи или чтения неуспешным, если не выполняется условие выше.&lt;/p&gt;

</description>
      <category>высоконагруженныеприложения</category>
      <category>клеппман</category>
      <category>книги</category>
    </item>
    <item>
      <title>Highload Junior. 1. HighLoad++ для начинающих</title>
      <dc:creator>Mikhail</dc:creator>
      <pubDate>Tue, 14 Nov 2023 17:55:39 +0000</pubDate>
      <link>https://dev.to/zabelin/highload-junior-1-highload-dlia-nachinaiushchikh-c6m</link>
      <guid>https://dev.to/zabelin/highload-junior-1-highload-dlia-nachinaiushchikh-c6m</guid>
      <description>&lt;h2&gt;
  
  
  Проблематика
&lt;/h2&gt;

&lt;p&gt;Высокая нагрузка - это нагрузка, с которой по какой-то причине не справляется железо (не хватает CPU или памяти). &lt;/p&gt;

&lt;p&gt;На данный момент с развитием железа проблемы его нехватки отпали, если приложение тормозит, то в 99% случаев это косяк в архитектуре.&lt;/p&gt;

&lt;p&gt;Расммотрим рядовую операцию на бэкенде &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Прием данных по сети&lt;/li&gt;
&lt;li&gt;Парсинг полученных данных&lt;/li&gt;
&lt;li&gt;Взаимодействие с БД&lt;/li&gt;
&lt;li&gt;Формирование ответа&lt;/li&gt;
&lt;li&gt;Отправка ответа клиенту&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Самый простой способ реализовать эту операцию - использовать один поток на один запрос. Но тогда быстро упремся в кол-во обрабатываемых запросов в секунду. &lt;br&gt;
Далее можно увеличить кол-во используемых котоков или процессов, распределить нагрузку между несколькими серверами. &lt;br&gt;
Однако все это не даст значительного прироста в RPS - он останется в пределах нескольких сотен.&lt;/p&gt;

&lt;p&gt;При этом если проанализировать, сколько времени выполняется каждый из пунктов в операции на бэкенде, то увидим, что:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Прием данных по сети - 15к в сек&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Парсинг полученных данных - 15к в сек&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Взаимодействие с БД - 60к в сек&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Формирование ответа - 100к в сек&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Отправка ответа клиенту - 15к в сек&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Если этого все сложить по формуле&lt;/p&gt;

&lt;p&gt;1/sum(1/freq(i))&lt;/p&gt;

&lt;p&gt;то получим 6к запросов в секунду, а не условно 500.&lt;/p&gt;

&lt;p&gt;В таких случаях нужно заняться профайлингом. Тогда увидим, что код выполняется процессором от силы 10% всего времени, а остальное времени идет ожидание от БД и сети.&lt;/p&gt;

&lt;h2&gt;
  
  
  Событийно-ориентированная архитектура
&lt;/h2&gt;

&lt;p&gt;Решить предыдущую проблему поможет изменение парадигмы программирования.&lt;/p&gt;

&lt;p&gt;Событийно-ориентированная архитектура подразумевает, что выполнение программы определяется событиями, на которые осуществилась "подписка". То есть мы говорим, что когда поступит событие успешного сохранения в БД, сделай то-то. &lt;br&gt;
Это то-то определяем в колбеке, который передаем отдельным аргументом. Из-за этого придется менять основную часть кодовой базы, которая крутится вокруг взаимодействия по сети.&lt;/p&gt;

&lt;p&gt;Также приходится решать проблемы, связанные со сменой парадигмы.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;сохранение контекста между колбеками&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;обработка исключений&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Чтобы было нагляднее, традиционную парадигму с потоками можно сравнить с обувным магазином, где покупатель часто находится в ожидании, когда ему принесут обувь.&lt;br&gt;
А событийно-ориентированную парадигму - с продуктовым магазином, где продавец всегда при деле находится и обслуживает очередь.&lt;/p&gt;

&lt;h2&gt;
  
  
  Green Threads
&lt;/h2&gt;

&lt;p&gt;Если брать обычные потоки, то их переключением занимается планировщик в составе операционной системы.&lt;br&gt;
Однако можно написать собственный планировщик, который бы управлял зелеными потоками (эмуляция многопоточной среды - подпрограмма), переключаясь между ними в момент ожидания операций ввода/вывода в одном.&lt;br&gt;
Соответственно, если операция сугубо процессорная, то профита не будет.&lt;/p&gt;

&lt;p&gt;Тогда если соединить вместе друг с другом свой планировщик и машину событий, то получим таком порядок выполнения операции:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Регистрация событий в машине состояний&lt;/li&gt;
&lt;li&gt;Передача управления планировщику&lt;/li&gt;
&lt;li&gt;При наступлении события планировщик будит зеленый поток&lt;/li&gt;
&lt;li&gt;Программа работает с данными от события, которое наступило в пред. пункте&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Т.е. зеленый поток усыпляется до тех пор, пока не наступает событие.&lt;/p&gt;

&lt;p&gt;Такой подход подразумевает использования библиотеки для использования зеленых потоков и написания кода-оберток над БД и сетевых вызовов.&lt;/p&gt;

</description>
      <category>highload</category>
      <category>article</category>
      <category>highloadjunior</category>
    </item>
    <item>
      <title>Глава 4. Кодирование и эволюция</title>
      <dc:creator>Mikhail</dc:creator>
      <pubDate>Mon, 13 Nov 2023 21:36:24 +0000</pubDate>
      <link>https://dev.to/zabelin/glava-4-kodirovaniie-i-evoliutsiia-26oh</link>
      <guid>https://dev.to/zabelin/glava-4-kodirovaniie-i-evoliutsiia-26oh</guid>
      <description>&lt;h2&gt;
  
  
  Введение
&lt;/h2&gt;

&lt;p&gt;С течением времени приложение меняется. Однако данные, с которыми работает новая версия приложение, могли быть созданы еще при старой версии приложения.&lt;br&gt;
Тем не менее приложение должно уметь работать как со старым форматом данных, так и с новым. Это называется &lt;em&gt;обратной совместимостью&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Также бывает нужно, чтобы старая версия приложения умела работать с данными, созданными более новой версией приложения.&lt;br&gt;
Например, при плавающем обновлении, когда на некоторых нодах развернута уже новая версия приложения, а где-то старая. Это называется &lt;em&gt;прямой соместимостью&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Форматы кодирования данных
&lt;/h2&gt;

&lt;p&gt;Для разных целей одни и те же данные могут представляться по-разному. Для хранения использоваться один формат, а для пересылки по сети - другой.&lt;br&gt;
Преобразование из формата для хранения в формат для пересылки называется кодированием, маршаллингом или сериализацией. Обратное преобразование, когда получатель получил данные - декодированием, демаршаллингом или десериализацией.&lt;/p&gt;

&lt;h3&gt;
  
  
  Форматы, ориентированные на ЯП
&lt;/h3&gt;

&lt;p&gt;В большинстве языков программирования есть "нативная" сериализация, достигаемая минимумом кода (например, java.io.Serializable в Java). Однако такие реализации имеют много минусов:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Она заточена под один язык программирования. А значит, программы, написанные на разных ЯП, не смогут "общаться".&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Как правило, низкая эффективность по скорости и размеру потребляемой памяти.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Проблемы с безопасностью - если подложить для декодирования вредоносные данные, то после декодирования будет в памяти объект, способный выполнять вредные вещи.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Плохо продуманы вопросы, касающиеся совместимости.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Текстовые форматы данных
&lt;/h3&gt;

&lt;h4&gt;
  
  
  XML
&lt;/h4&gt;

&lt;p&gt;Многословный текстовый формат, часто применяющийся к корпоративных приложениях, особенно, если они были написаны в 90-е и 2000-е. В XML нет типов данных как таковых. Все можно считать строкой.&lt;br&gt;
Но можно подключить схему валидации (своего рода костыль). Для XML использование схем валидации частое явление.&lt;/p&gt;

&lt;h4&gt;
  
  
  JSON
&lt;/h4&gt;

&lt;p&gt;Текстовый формат, часто применяющийся в современных приложениях. Менее многословный, чем XML. Также поддерживает схемы валидации, однако для JSON они на практике очень редко используются.&lt;br&gt;
Зато есть поддержка типов данных (boolean, string, int64). Однако есть проблема с большими числами - числа более 2^53 кодируются с неточностями из-за представления числа с плавающей точкой по стандарту IEEE-754.&lt;/p&gt;

&lt;p&gt;Также есть двочные кодирования для JSON - BSON, MessagePack и др. Но они редко использутся и дают не настолько сильный выигрыш в снижении объема данных, чтобы терять в удобочитаемости. Я встречал в БД его использование.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--llLSC0vV--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/b4butcc77hc72z72sght.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--llLSC0vV--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/b4butcc77hc72z72sght.png" alt="Image description" width="800" height="604"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Стоит отметить, что оба формат не поддерживают работу с бинарными строками. Поэтому использутся костыль в виде base64 кодирования, который примерно на 30% увеличивает объем данных.&lt;/p&gt;

&lt;h3&gt;
  
  
  Двоичные форматы данных
&lt;/h3&gt;

&lt;p&gt;Apache Thrift и Protocol Buffers - популярные библиотеки для кодирования данных. Оба требуют предварительно иметь на отправителе и получателе схему.&lt;br&gt;
Отчасти благодаря тому, что часть необходимой для кодирования/декодирования данных информации зашита в схему, достигается сильное снижение объема данных.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--a2oyAU49--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6h7duy91iclsbsb9raoc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--a2oyAU49--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6h7duy91iclsbsb9raoc.png" alt="Image description" width="505" height="144"&gt;&lt;/a&gt;&lt;br&gt;
Apache Thrift&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--QgF1R0_3--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7dw0ips7zub5ii8au48x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--QgF1R0_3--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7dw0ips7zub5ii8au48x.png" alt="Image description" width="492" height="124"&gt;&lt;/a&gt;&lt;br&gt;
Protobuf&lt;/p&gt;

&lt;p&gt;Так как protobuf популярнее, рассмотрим его устройство подробнее.&lt;/p&gt;

&lt;p&gt;Вместо названия полей в protobuf в закодированных данных хранятся тип значения (число), теги полей - числа (что-то вроде псевдонимов для полей), длина (для строки) и сами данные.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--yRcpIFaB--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sm81lcjd72dyb0r5p5i2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--yRcpIFaB--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sm81lcjd72dyb0r5p5i2.png" alt="Image description" width="800" height="413"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;В итоге данные, которые в JSON весили 84 байта, в protobuf весят 33. &lt;/p&gt;

&lt;p&gt;Однако что с совместимостью у бинарных форматов?&lt;/p&gt;

&lt;p&gt;Как видно, закодированная запись это просто конкатенация закодированных полей.&lt;/p&gt;

&lt;p&gt;Если поле добавляется, то чтобы новый код мог работать со старыми данными, новое поле должно быть необязательным или иметь дефолтное значение - тогда новая версия приложения может читать старые данные.&lt;br&gt;
Если поле удаляется, то чтобы старый код мог работать с новым форматом поле удаляемое поле должно быть необязательным изначально и тег для него нельзя будет использовать для новых полей.&lt;/p&gt;

&lt;h2&gt;
  
  
  Способы обмена данными
&lt;/h2&gt;

&lt;p&gt;Теперь рассмотрим способы отправки данных в форматах, описанных ранее.&lt;/p&gt;

&lt;p&gt;Существует 3 способа обмена данными между процессами на одной ноде или разных:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;через общую БД&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;через REST или RPC&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;асинхронный обмен через очередь сообщений&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Общая БД
&lt;/h3&gt;

&lt;p&gt;Самый примитивный способ интеграции приложений, дающей сильное связывание между компонентами. Однако есть и плюс - актуальная схема данных хорошо видна.&lt;br&gt;
Для достижения прямой совместимости нельзя допускать, чтобы старая версия приложения целиком перезаписывала всю строку.&lt;/p&gt;

&lt;h3&gt;
  
  
  REST и RPC
&lt;/h3&gt;

&lt;p&gt;RPC - удаленный вызов процедур. В коде приложения это выглядит как вызов локальной функции. Популярный паттерн коммуникации в SOA (сервис ориентированной) архитектуре.&lt;br&gt;
Чаще всего RPC реализуется через SOAP протокол. В его основе лежит протокол HTTP. Однако SOAP не использует никакие  его возможности, такие как использование кодов ответов, заголовков.&lt;br&gt;
API сервиса, поддерживающего общение по SOAP, описывается на XML через WSDL схему. На основе этой схеме генерируются нужные методы на языках программирования. &lt;/p&gt;

&lt;p&gt;REST - подход к проектированю систем, общающихся по http и по-максимому использующих его возможности. В качестве формата данных могут использоваться разные форматы, однако чаще всего это JSON.&lt;/p&gt;

&lt;h3&gt;
  
  
  Очереди сообщения
&lt;/h3&gt;

&lt;p&gt;RPC - синхронный способ обмена сообщениями.&lt;/p&gt;

&lt;p&gt;БД - асинхронный, но изначально предполагающий использование в качестве хранилища данных.&lt;/p&gt;

&lt;p&gt;Очередь - тоже асинхронный способ, но изначально предполагающий использование для обемена сообщениями. Поэтому имеет такие сущности как топик, смещение, издатель и подписчик. Благодаря этим абстракциям обмен данными многими с многими упрощается.&lt;/p&gt;

&lt;p&gt;Преимущества ипользования очередей по сравнению с RPC:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;очередь является буфером, смягчающим скачки нагрузки на приложение&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;уменьшает связанность компонетов системы (отправителю не нужно знать адрес получателя)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;автоматически повтор отправки в случае сбоя&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Примеры часто использующихся брокеров сообщения: RabbitMQ, Apache Kafka, NATS.&lt;/p&gt;

</description>
      <category>клеппман</category>
      <category>высоконагруженныесистемы</category>
      <category>книги</category>
    </item>
    <item>
      <title>Глава 3. Подсистемы хранения и извлечения данных</title>
      <dc:creator>Mikhail</dc:creator>
      <pubDate>Sun, 12 Nov 2023 20:56:32 +0000</pubDate>
      <link>https://dev.to/zabelin/glava-3-podsistiemy-khranieniia-i-izvliechieniia-dannykh-5afb</link>
      <guid>https://dev.to/zabelin/glava-3-podsistiemy-khranieniia-i-izvliechieniia-dannykh-5afb</guid>
      <description>&lt;h2&gt;
  
  
  Вступление
&lt;/h2&gt;

&lt;p&gt;Журнал - самая простая структура для хранения данных в принципе. Она позвляет дописывать данные только в конец.&lt;br&gt;
Если рассмотреть данные вида ключ-значение, то при вставке записи с тем же ключом в журнале будут дублирующие данные.&lt;br&gt;
Сложность поиска по журналу - O(n). Это много.&lt;br&gt;
Для более эффективного поиска данных используются вспомогательные структуры, производные от основных данных - индексы.&lt;br&gt;
Поддержка индексов приводит к более быстрому поиску, но более медленной вставке и удалению, так как индексы тоже подлежат обновлению.&lt;br&gt;
Поэтому на усмотрение разработчика выбор индексов.&lt;/p&gt;

&lt;h2&gt;
  
  
  Индексы
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Журнал + хэш карта
&lt;/h3&gt;

&lt;p&gt;Самый простой вариант индекса для данных вида ключ-значение - хэш карта. Она будет хранить смещение в файле данных.&lt;br&gt;
Однако эта реализация с журналом имеет недостаток - файл с данными будет постоянно увеличиваться на диске.&lt;br&gt;
Поэтому можно придумать оптимизацию - разделить журнал на сегменты и фоном выполнять уплотнение сегментов, убирая "старые" значения для ключей. Этот процесс называется уплотнением журнала.&lt;br&gt;
У каждого сегмента будет свой индекс - своя хэш таблица.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--f3rvyHoY--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/791bc5lt1kotbd8wx9yl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--f3rvyHoY--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/791bc5lt1kotbd8wx9yl.png" alt="Image description" width="800" height="262"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Чтобы уплотнение не мешало основным операциям с данными, можно совершать уплотнение, оставляя старые сегменты и только после успешного уплотнения переключать запросы на "новые" сегменты.&lt;/p&gt;

&lt;p&gt;Плюсы:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;быстрое чтение O(1) поиск по индексу + перемещение на позицию в файле.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;реализация конкурентного доступа упрощается, так как данные идемпотентны, ведь любое изменение записи ведет к созданию новой записи&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;добавление в конец и слияние - очень быстрые операция на SSD и особенно на HDD дисках&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;благодаря слиянию достигается лишь небольшая фрагментация данных&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Минусы:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;запись может осуществляться в одном потоке, так как запись идет всегда в коней файла. Это замедляет параллельную запись.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;нет возможности поиска по интервалу&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Также нужно обдумать следующие вопросы:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;удаление данных - можно добавить каждой записи в сегменте метку признака удаления&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;восстановление после сбоя - так как индекс (хэш карта) хранится в оперативной памяти, то после сбоя придется ее восстанавливать, проходя через все сегменты, что займет много времени. Можно периодически делать снэпшоты хэш карты на диск&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Описываемый метод исользуется в подсистеме BitCask NoSQL БД RiakDB&lt;/p&gt;

&lt;h3&gt;
  
  
  Sorted String Table + хэш карта
&lt;/h3&gt;

&lt;p&gt;Журнал обладал тем минусом, что данные в нем не были отсортированы. Поэтому объединение сегментов там выполнялось за O(n^2), а для поиска по интервалу требовалось для каждого значения из интервала осуществлять отдельный поиск.&lt;/p&gt;

&lt;p&gt;Поэтому эффективнее держать часть данных в оперативной памяти, а часть - на диске.&lt;br&gt;
В памяти удобно держать сбалансированную структуру данных (данные в ней отсортированы - при вставке происходит ребаланс),&lt;br&gt;
которую бы можно было при достижении определенного размера скидывать на диск в виде Sorted String Table (отсортированная строковая таблица), которая в фоне уплотняется, как в сортировке слиянием - O(n).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Wevt2Lqo--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x0l3nnxmvhnhql9g00hn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Wevt2Lqo--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x0l3nnxmvhnhql9g00hn.png" alt="Image description" width="800" height="510"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--rmUlOGcN--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pq4f40bqb225k8wczxur.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--rmUlOGcN--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pq4f40bqb225k8wczxur.png" alt="Image description" width="800" height="355"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Благодаря этому больше нет необходимости хранить в индексе смещение для данных по всем ключам, так как, например, если известно смещение для ключей a и c, то смещение для ключа b будет где-то между ними.&lt;/p&gt;

&lt;p&gt;Описываемый метод используется в БД LevelDB&lt;/p&gt;

&lt;h3&gt;
  
  
  В-деревья
&lt;/h3&gt;

&lt;p&gt;B-деревья - самый распространенный тип индекса.&lt;br&gt;
В отличие от журнала, основой которого является сегмент данных переменного размера, в B-деревьях данные разделяются на страницы фиксированного размера (обычно 4кб). Диски тоже разбиваются на блоки фикс. размера.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--nrhkOBxo--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v8c3vquqtho0ejsil2dp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--nrhkOBxo--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v8c3vquqtho0ejsil2dp.png" alt="Image description" width="800" height="360"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Все страницы, кроме страниц в листях, указывают на другие страницы B-дерева на диске. В листьях страницы содержат ссылку на страницу со значением в основной таблице с данными.&lt;br&gt;
Кол-во ссылок на дочерние страницы называется коэффициентом ветвления. На практике он равен нескольким сотням.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--X6kp6Toz--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6tia5wi06iyigr6pyam8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--X6kp6Toz--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6tia5wi06iyigr6pyam8.png" alt="Image description" width="800" height="423"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Если на странице нет места, то создается 2 новых полупустых страницы, куда копируются данные из старой таблицы и со стороны родителей перевешиваются указатели на новые страницы.&lt;/p&gt;

&lt;p&gt;B-дерево - сбалансированное дерево. Его высота и сложность поиска - O(log(n)).&lt;/p&gt;

&lt;p&gt;На практике дерево содержит высоту 3-4 уровня. &lt;/p&gt;

&lt;p&gt;Чтобы сделать бд с индексом на основе B-дерева отказоустойчивой, используются журнал упреждающей записи (WAL - write ahead log),&lt;br&gt;
в который записывается действие - только после этого происходит изменение самого B-дерева (а само изменение из-за ребаланса может быть значительным).&lt;/p&gt;

&lt;p&gt;Оптимизации:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;можно хранить на страницах не полные значение ключей, а часть (актуально, если ключ - строка), ведь во внутренних узлах при сравнении используется лишь часть ключа.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;вместо wal журнала можно делать копию узлов b дерева, которые связаны с измененной страницей. Это полезно при конкурентном доступе.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;По итогу SS таблицы быстрее при записи, но медленнее при чтении, чем B-деревья.&lt;/p&gt;

&lt;h3&gt;
  
  
  Хранение в памяти, а не на диске
&lt;/h3&gt;

&lt;p&gt;До этого все рассматриваемые структуры данных использовались для хранения данных на диске. Ранее стоимость места на диске была сильна ниже, чем в оперативной памяти, чей объем был к тому же ограничен.&lt;/p&gt;

&lt;p&gt;В данный момент идет тенденция, что в большинстве случаев объема оперативной памяти достаточно для хранения всех данных, а отказоустойчивость осуществляется с помощью репликации по сети и питания от аккумуляторов.&lt;/p&gt;

&lt;p&gt;Поэтому в последнее десятилетие активно развивается направление in-memory баз данных, например, Memcached.&lt;/p&gt;

&lt;p&gt;Однако не всегда можно заметить разницу между использованием БД, использующих диск и ОП, так как операционная система имеет свой кэш, куда сохраняются наиболее часто используемые страницы диска.&lt;/p&gt;

&lt;h2&gt;
  
  
  Паттерны использования БД
&lt;/h2&gt;

&lt;p&gt;Существует 2 сценария использования БД - обработка транзакций в реальном времени (OLTP) и аналитика данных (OLAP). Первый используется обычными пользователями, а второй - аналитиками данных.&lt;br&gt;
При обработке транзаций в реальном времени обычно ищется небольшое кол-во записей по ключу (а общий размер всех данных от гб до тб). При аналитике происходит агрегация больших объемов данных (от тб до пб).&lt;/p&gt;

&lt;h3&gt;
  
  
  Поступление данных в OLAP и OLTP.
&lt;/h3&gt;

&lt;p&gt;Поступление данных в OLTP хранилища происходит через какие-то бизнес процессы (работы клиента с системой обслуживания или работа с POS терминалом). Соответственно, требуется высокая доступность БД и низкая задержка в ответе.&lt;/p&gt;

&lt;p&gt;Поступление данных в OLAP хранилища осуществляется через групповой испорт (ETL - extract transform load) или потоковую загрузку.&lt;/p&gt;

&lt;h3&gt;
  
  
  Схемы для аналитики
&lt;/h3&gt;

&lt;p&gt;В аналитике популярна схема звезда и ее развитие - снежинка. &lt;/p&gt;

&lt;p&gt;В звезде имеется таблица с фактами - запись с внешники ключами на таблица с атрибутами (концы звезды)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--dgr0MtNk--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x5rml5t2yvfqzc40qj57.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--dgr0MtNk--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x5rml5t2yvfqzc40qj57.png" alt="Image description" width="800" height="806"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;В снежинке точно так же, только измерения разделяются на подизмерения.&lt;/p&gt;

&lt;h2&gt;
  
  
  Столбцовое хранение
&lt;/h2&gt;

&lt;p&gt;В таблице могут быть триллионы строк и петабайты данных. Значения соседних полей одной строки лежат рядом на диске. В аналитике обычно требуются не все поля, но все строки. Поэтому удобно хранить данные не по строкам, а по столбцам.&lt;br&gt;
Тогда можно на диске иметь свой файл под каждое поле.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--X602HUtj--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/oqfk6y4lop5j2dlemepc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--X602HUtj--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/oqfk6y4lop5j2dlemepc.png" alt="Image description" width="800" height="513"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Для оптимизации хранения полей, у которых фикисрованный набор значений, можно использовать битовые маски.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--A2Jw_jK0--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/blkmbb9blrcto56enfhj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--A2Jw_jK0--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/blkmbb9blrcto56enfhj.png" alt="Image description" width="800" height="484"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Высоконагруженные системы. Глава 2. Модели данных и языки запросов.</title>
      <dc:creator>Mikhail</dc:creator>
      <pubDate>Sat, 04 Nov 2023 19:37:59 +0000</pubDate>
      <link>https://dev.to/zabelin/vysokonaghruzhiennyie-sistiemy-glava-2-modieli-dannykh-i-iazyki-zaprosov-hgk</link>
      <guid>https://dev.to/zabelin/vysokonaghruzhiennyie-sistiemy-glava-2-modieli-dannykh-i-iazyki-zaprosov-hgk</guid>
      <description>&lt;h2&gt;
  
  
  Вступление
&lt;/h2&gt;

&lt;p&gt;Модель данных - это способ представления некоторых данных. Данные можно представить на разных уровнях, поэтому между собой модели данных образуют иерархию, когда одна модель данных выражается через другую на языке более низкого уровня. Например:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Выражение процесса в окружающем мире через некоторые структуры данных, а действие с ними через API. Это самый высокий уровень.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;При необходимости сохранять эти данные можно выбрать текстовый вид, например в виде json или xml.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Способ внутреннего представления json или xml в БД (на уровне последовательности байт), благодаря чему становятся возможными операции с этими данными, такие как поиск, вставка, обновление, удаление части данных.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Выражение байт в терминах эл. тока. Аппаратный уровень.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Таким образом, верхний слой инкапсулирует сложность нижестоящего слоя.&lt;/p&gt;

&lt;p&gt;Уровень 1 можно представить для хранения данных через 3 модели данных:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Документоориентированная модель (иерархическая)&lt;/li&gt;
&lt;li&gt;Реляционная&lt;/li&gt;
&lt;li&gt;Графовая&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Документоориентированная&lt;/strong&gt; хорошо подходит для хранения иерархических данных со связями 1 ко многим. Однако связь многие ко многим и многие к одному с ее помощью нельзя реализовать. В соответствие с этой моделью данные представляются в виде единого документа. Некоторые из NoSQL баз данных являются представителями этой модели. Получили широкое распространение в 2010-х годах. Данные не имеют четкой структуры. &lt;/p&gt;

&lt;p&gt;Плюсы - можно получить за раз все части документа, обеспечивают лучшую локальность данных, лучшую пропускную способность по записи, лучше подходят для выражения некоторых моделей данных (отсюда менее лаконичные запросы) и легко масштабируются&lt;/p&gt;

&lt;p&gt;Минусы - в память грузится весь документ. И меняется он тоже весь, что затратно по времени. Реализации этой модели чаще не дают гарантий, которые дает реляционная модель.&lt;/p&gt;

&lt;p&gt;Пример - хранения документов в виде json в MongoDB.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Реляционная модель&lt;/strong&gt; - самая универсальная из трех.&lt;/p&gt;

&lt;p&gt;Плюсы: поддерживает все виды соединений и могут представлять иерархические и графовые модели данных (через таблица с ребрами и вершинами и рекурсивные запросы через common table expressions). Также современные реляционные БД позволяют хранить неструктурированные документы json и xml и производить с ними манипуляции.&lt;/p&gt;

&lt;p&gt;Минусы: запросы получаются громоздкими, а сами данные организованы сложно для понимания. Пример - PostgreSQL c языком SQL.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Графовая модель&lt;/strong&gt; является самой узкоспециализированной. Подходит для случаев, когда потенциально любые данные могут быть взаимосвязаны. Для этого вершины еще могут хранить тип сущности. Например, есть пользователь и его место жительства. Пользователь связан с городом. Город связан с регионом. Регион со страной. И все этой разные типы сущностей.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--MdyOHz6d--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8yh6gxiczqe4tv5x3lpe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--MdyOHz6d--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8yh6gxiczqe4tv5x3lpe.png" alt="Image description" width="725" height="415"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Представляется в виде вершин (сущности) и ребер (связи).&lt;/p&gt;

&lt;p&gt;Вершины имеют:&lt;/p&gt;

&lt;p&gt;идентификатор&lt;br&gt;
множество ид входящих ребер&lt;br&gt;
множество ид исходящих ребер&lt;br&gt;
метаинформация в виде пар ключ-значение (json)&lt;/p&gt;

&lt;p&gt;Ребра имеют:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;ид вершины-начала&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;ид вершины-конца&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;идентификатор&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;метаинформация в виде пар ключ-значение (json)&lt;br&gt;
Данные не имеют четкой структуры.  Пример - Neo4j с языком запросов Cypher.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Особенность языков запросов к хранилищам
&lt;/h2&gt;

&lt;p&gt;Если рассмотреть языки запросов, которые используют реализации этих трем моделей, до обнаружим, что все они декларативные, когда мы указывает результат, к которому хотим прийти, а вся логика по достижению этого результата определяется оптимизатором.&lt;/p&gt;

</description>
      <category>highload</category>
      <category>книги</category>
      <category>клеппман</category>
      <category>высоконагруженныеприложения</category>
    </item>
    <item>
      <title>Высоконагруженные системы. Глава 1. Надежные, масштабируемые и удобные в сопровождении приложения</title>
      <dc:creator>Mikhail</dc:creator>
      <pubDate>Sat, 04 Nov 2023 19:25:42 +0000</pubDate>
      <link>https://dev.to/zabelin/vysokonaghruzhiennyie-sistiemy-glava-1-nadiezhnyie-masshtabiruiemyie-i-udobnyie-v-soprovozhdienii-prilozhieniia-1phm</link>
      <guid>https://dev.to/zabelin/vysokonaghruzhiennyie-sistiemy-glava-1-nadiezhnyie-masshtabiruiemyie-i-udobnyie-v-soprovozhdienii-prilozhieniia-1phm</guid>
      <description>&lt;p&gt;Приложения могут быть высоконагруженными данными и высоконагруженными вычислениями. &lt;/p&gt;

&lt;p&gt;Приложения, высоконагруженные данными, строятся из стандартных блоков:&lt;/p&gt;

&lt;p&gt;Долговременное хранение данных (базы данных)&lt;br&gt;
Запоминание на небольшой как правило промежуток времени ресурсоемкой операции (кэши)&lt;br&gt;
Поиск в хранилище данных, фильтрация (поисковые индексы)&lt;br&gt;
Обработка данных без задержек (потоковая обработка)&lt;br&gt;
Обработка накопленных данных (пакетная обработка)&lt;br&gt;
Однако существует много инструментов внутри каждого из блоков, отличающихся друг от друга. Поэтому не все так просто.&lt;/p&gt;

&lt;p&gt;Если вкратце, то есть 3 наиболее важных вопроса, которые необходимо рассмотреть при проектировании высоконагруженного приложения:&lt;/p&gt;

&lt;p&gt;Надежность (при программных/аппаратных сбоях система должна продолжать работать корректно для пользователя)&lt;br&gt;
Масштабируемость (при росте нагрузки должно быть легко модифицировать системы, чтобы она продолжала справляться с возрошей нагрузки)&lt;br&gt;
Удобство сопровождения (нужно предусмотреть, как системой будут пользоваться рядовые пользователи и ее сопровождения - у него должна быть возможность быстро расследования инцидентов)&lt;br&gt;
Надежность&lt;br&gt;
Надежность - это способность системы продолжать работать корректно при определенных сбоях. Сбои по причинам на 3 группы:&lt;/p&gt;

&lt;p&gt;Аппаратные (выход из строя плашки ОЗУ или диска). Выход - использование избыточного кол-ва аппаратного обеспечения, чтобы не наступил уже отказ всей системы.&lt;br&gt;
Программные (провайдер перестал отвечать, процесс исчерпал ресурс). Выход - изначальное продумывание возможных проблем и путей их недопущение, использование средств для ручного мониторинга и алертинга. Автоматический перезапуск системы после фатального сбоя. &lt;br&gt;
Человеческий фактор (неправильная настройка приложения или необработанная ошибка при каком-нибудь пользовательском сценарии). Выход - проектирование с учетом того, чтобы правильные действия было легко сделать, а неправильные - сложно. Грамотно спроектированное API. Зрелый CD (Continious Delivery), когда есть возможность быстрого отката изменения сервиса или конфига и канареечные релизы. Настроить телеметрию приложения для обнаружения проблем на ранней стадии до массового недовольства потребителей.&lt;br&gt;
Масштабируемость&lt;br&gt;
Масштабируемость - способность системы справлять с возрастающими нагрузками. Эта способность измеряется ответами на следующие вопросы:&lt;/p&gt;

&lt;p&gt;Насколько сильные изменения нужно произвести с системой, чтобы она стабильно выдерживала в 10 раз большую нагрузку&lt;br&gt;
Каким образом можно увеличить вычислительные ресурсы чтобы системы выдерживала в 10 раз большую нагрузку.&lt;br&gt;
У нагрузки есть параметры, тип которых разнится в зависимости от устройства системы. Это может быть RPS (кол-во запросов в секунду), кол-во одновременных пользователей, делающих что-то, отношение кол-ва операций чтения к кол-ву операций записи в БД.&lt;/p&gt;

&lt;p&gt;Пример с Twitter&lt;br&gt;
Например, рассмотрим архитектуру Twitter. Там есть 2 основных действия - публикация твита и просмотр ленты. В данном случае сложность в масштабировании представляет сильная степень разветвления (пользователь подписан на многих и сам имеет много подписчиков). Это и есть параметр масштабирования.&lt;/p&gt;

&lt;p&gt;Twitter сначала на каждую операцию обновления делал выборку постов через join авторов постов с теми, на кого подписан пользователь. Это самый примитивный вариант, который довольно скоро оказался нерабочим из-за возросшей нагрузки.&lt;/p&gt;

&lt;p&gt;Затем придумали вариант "кэша" для ленты пользователя, когда при публикации твита он попадал в ленты всех пользователей, которые подписаны на его автора. Однако из-за масштабов (особенно для авторов с миллионами подписчиков) проходило много времени между фактической публикации и тем, когда подписчик видел твит в своей ленте.&lt;/p&gt;

&lt;p&gt;Поэтому в итоге Twitter остановился на промежуточном варианте, когда твиты знаменитостей подгружаются именно при обновлении ленты и сливаются с "закэшированными" данными от менее популярных авторов.&lt;/p&gt;

&lt;h3&gt;
  
  
  Описание производительности системы
&lt;/h3&gt;

&lt;p&gt;После описания нагрузки нужно описать производительность. Для этого нужно ответить на 2 вопроса:&lt;/p&gt;

&lt;p&gt;Как изменится производительность системы, если параметры нагрузки увеличить в N раз&lt;br&gt;
Насколько нужно увеличить ресурсы, чтобы при увеличении параметров нагрузки в N раз производительность останется той же.&lt;br&gt;
Время обработки запроса - хорошая метрика производительности. При этом есть время отклика (общее время, которое ждет клиент, отправивший запрос) и время ожидания (время, пока запрос ожидает свой обработки сервером). Время отклика важно смотреть в контексте тысяч запросов, чтобы получить распределение времени запросов и иметь общую картину того, что будет происходить в проде. Медиана, 90-й, 95-й, 99-й процентили обычно берут. 99-й процентиль - это такое время отклика, что 99% запросов выполнялись меньше этого времени.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--JyRVHb_r--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/piqg11siu5sn7b2bdcnq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--JyRVHb_r--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/piqg11siu5sn7b2bdcnq.png" alt="Здесь по оси x - время обработки запроса, а по оси y - кол-во запросов, выполнившихся в определенном коридоре времени" width="788" height="856"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Здесь по оси x - время обработки запроса, а по оси y - кол-во запросов, выполнившихся в определенном коридоре времени.&lt;/p&gt;

&lt;p&gt;Такие значения обычно прописываются в требованиях к уровню предоставления сервис (SLA), что 99% запросов должны выполняться меньше 200мс, а 50% - менее 100мс.&lt;/p&gt;

&lt;h3&gt;
  
  
  Как справиться с нагрузкой
&lt;/h3&gt;

&lt;p&gt;Есть 2 варианта:&lt;/p&gt;

&lt;p&gt;Вертикальное масштабирование (увеличение мощности одной машины, на которой работает сервис)&lt;br&gt;
Горизонтальное масштабирование (увеличение кол-ва маломощных машин, на которых работают экземпляры сервиса)&lt;br&gt;
Там, где нужно хранить состояние (например, БД), горизонтальное масштабирование плохо подходит. Горизонтальное масштабирование идеально подходит под "числодробилки", которые состояния не имеют.&lt;/p&gt;

&lt;h3&gt;
  
  
  Удобство сопровождения
&lt;/h3&gt;

&lt;p&gt;Если коротко, то это грамотно написанный код системы без сильного зацепления модулей для их быстрой модификации и хороший мониторинг.&lt;/p&gt;

</description>
      <category>highload</category>
      <category>книги</category>
      <category>клеппман</category>
      <category>высоконагруженныеприложения</category>
    </item>
  </channel>
</rss>
