IT TL;DR – Telegram
IT TL;DR
343 subscribers
3 photos
6 links
Обзоры на статьи, книги, доклады по IT-тематике, а иногда мои собственные мысли
Download Telegram
Channel created
Channel name was changed to «IT TL;DR»
Channel photo updated
#ddia #book #summary

По этим хэштегам можно будет найти заметки, мысли, комментарии по материалу книги Designing Data-Intensive Applications. The Big Ideas Behind Reliable, Scalable, and Maintainable Systems от Мартина Клеппмана (Martin Kleppmann).

Читаю книгу на английском, но заметки будут на русском, иногда обращаю внимание на нюансы, возникающие при переводе.
#ddia #book #summary

Введение

Во введении автор обозначает фокус книги: data-intensive applications, противопоставляя их compute-intensive applications. К первой категории относят приложения, работающие с большим количеством данных, а ко второй - требующие большого объема вычислений. Часто можно встретить похожее противопоставление IO-bound vs CPU-bound системы/операции.

Лингвистика. Интересно, что в русскоязычном издании книги в заглавии data-intensive applications переведены как высоконагруженные приложения, при этом не подчеркивается, что речь пойдет об IO-bound системах, в которых основным вызовом является масштаб данных. А высоконагруженными могут быть и CPU-bound приложения, то есть часть смысла при переводе утеряна уже в заглавии книги.

В последние годы большое развитие получили различные базы данных и распределенные системы. Это произошло в том числе благодаря набору факторов:

- Бигтех компании (Google, Amazon, etc.) растят бизнес, объемы данных и сетевого трафика; нужны технологии, которые могут справиться с подобным ростом.

- Технологический прогресс идет не в сторону роста мощности одного элемента системы (например, тактовой частоты одного процессора), а в сторону параллелизма во всех его проявлениях: многоядерные процессоры, распределенные системы (работающие на разных серверах, или даже в географически удаленных датацентрах); последние требуют надежной сети с хорошей пропускной способностью.

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

В связи с популяризацией распределенных систем постоянно возникают новые технологии и баззворды, уследить за которыми сложно. Хорошая новость в том, что базовые принципы, лежащие в основе всех этих инструментов, не меняются, и если в этих принципах разобраться, то вы без проблем сможете выбирать правильный инструмент под конкретную задачу. В книге в том числе будет разбираться внутреннее устройство подобных инструментов, в том числе алгоритмы их работы и компромиссы, которые заложены в их устройстве.
👍1
#ddia #book #summary

Часть 1. Foundations of Data Systems

Лингвистика. Во введении покритиковал вариант перевода высоконагруженные приложения для data-intensive applications, но не предложил альтернативу. Если обратиться к словарю, то увидим, что постфикс -intensive часто переводится как -ёмкий, например, labour-intensive – трудоёмкий, resource-intensive – ресурсоёмкий (затратный по ресурсам). Поэтому далее буду использовать вариант датаемкие приложения для передачи английского data-intensive applications.

Глава 1. Reliable, Scalable, and Maintainable Applications

Датаемкие приложения, как правило, собирают из типовых "кирпичиков", способных решать типовые задачи: баз данных, кэшей, поисковых индексов, систем потоковой и пакетной (батч) обработки. При этом важно выбирать подходящие кирпичики под конкретную задачу, а также следить за тем, чтобы эти кирпичики хорошо стыковались между собой. Кроме того, создатели приложений стремятся к тому, чтобы построенная система была надежной (reliable), масштабируемой (scalable) и удобно поддерживаемой (maintainable).

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

Масштабируемость - система должна быть готова к росту нагрузки (включая рост объема данных и рост потока/скорости изменений данных), а также к росту сложности (по мере развития системы). Имеется ввиду, что должны существовать разумные способы решения проблем, возникающих при росте нагрузки или сложности, не включающие в себя переделывание системы с нуля.

Удобство поддержки - команды разработки и эксплуатации способны эффективно заниматься обслуживанием системы: поддержанием ее текущего поведения, а также ее развитием для новых сценариев использования.
👍21👏1
#ddia #book #summary

Reliability

Надежность - интуитивно понятное и ожидаемое свойство системы. Простыми словами это способность системы продолжать корректно работать, даже если что-то идет не так. Более строго, моменты, когда что-то идет не так, называют сбоями, а систему, которая способна предвидеть и/или нивелировать эффект от сбоев, называют устойчивой к сбоям (англ. fault-tolerant или resilient).

Лингвистика. Автор призывает разделять сбои (англ. faults) и отказы (англ. failures). Первое - отклонение поведения какого-то компонента системы от рабочих характеристик, например, деградация времени чтения с диска, второе - состояние системы, в котором она перестает выполнять свои функции. В словаре можно увидеть вариант перевода resilient как "отказоустойчивый", хотя на самом деле система должна быть устойчивой к сбоям, а именно, чтобы сбои не приводили к отказам.

На первый взгляд может показаться парадоксальным, но нередко в систему искусственно добавляют различные сбои, чтобы проверить, устойчива ли к ним система. На практике именно логика обработки ошибок содержит больше ошибок, чем happy path сценарии, т.к. ошибки возникают довольно редко (иногда их называют "исключительные ситуации"), поэтому и логика их обработки "тестируется" в продакшене тоже редко. Чтобы компенсировать такой дисбаланс, некоторые компании берут на вооружение подход chaos-engineering (можно встретить перевод хаос-тестирование). Подход популяризирован компанией Netflix, которая выложила в open-source свой инструмент для хаос-тестирования - Chaos Monkey.

Аппаратные сбои

Каждый с легкостью назовет примеры аппаратных сбоев (англ. hardware faults): выход из строя дисков, оперативной памяти, отключение питания, человеческий фактор (пресловутый инженер датацентра, который выдернул провод не из той серверной стойки).

Важно не ввести себя в заблуждение, что железо ломается редко, а, значит, можно не тратить время на попытки устранить этот риск. Дело в том, что риск отказа железа проявляет себя на большом масштабе: если принять среднее время службы диска за 30 лет, то в датацентре с 10 000 дисками в среднем каждый день выходит из строя один диск (уже не звучит, как что-то невероятное?).

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

Однако с появлением датаемких систем, которые работают на большом числе серверов, как и в примере с дисками, начинает играть эффект масштаба: с ростом числа серверов/узлов пропорционально растет частота возникновения аппаратных сбоев. Таким образом, возникает необходимость создавать системы, которые устойчивы к частым отказам целых узлов. Плюсом таких систем является возможность накатывать обновления, требующие полной перезагрузки (например, обновления безопасности): с помощью плавного обновления (англ. rolling upgrade) можно избежать даунтайма всей системы.
👏1
#ddia #book #summary

Reliability (продолжение)

Программные ошибки

В отличие от аппаратных сбоев, которые происходят в случайные моменты времени и, как правило, независимо друг от друга, программные ошибки могут возникать на многих или даже на всех узлах, относящихся к одной и той же системе. В силу своей скоррелированности подобные систематические ошибки могут оказывать намного более серьезный эффект на систему, нежели случайный одиночный выход диска из строя. Примеры: баг в коде, приводящий к падению всего процесса HTTP-сервера на определенных запросах, утечка системных ресурсов (память, файловые дескрипторы), каскадные падения (поломка одного компонента приводит к поломке других по цепочке).

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

Человеческий фактор

Люди проектируют, строят и, наконец, эксплуатируют системы. При этом люди могут ошибаться (даже если увести за скобки злой умысел). В контексте надежности люди очень ненадежны: при внесении изменений в конфигурацию системы можно допустить ошибку, которая приведет к поломке системы. К счастью, есть ряд подходов, позволяющих нивелировать человеческий фактор:
- проектировать интерфейсы, админки, API таким образом, чтобы минимизировать возможность ошибиться: проверять различные инварианты, которые не позволят сделать конфигурацию ошибочной
- создавать изолированные окружения, в которых можно получить навык управления системой без риска поломки продакшена (тестовое окружение, песочница)
- предусматривать возможность быстрого отката изменений на случай, если в конфигурации будет допущена ошибка
- давать возможность раскатывать изменения плавно, чтобы не влиять сразу на 100% пользователей системы

Нужно ли заниматься надежностью?

Надежность это не только про атомные электростанции и самолеты. В любом сервисе пользователи ожидают, что система будет доступна 24/7, данные не потеряются и т.д. Иногда надежность задвигается на второй план ради скорости разработки новых фич или достижения бизнес-результата, но долгосрочно терять надежность из фокуса точно не стоит.
👏1
#ddia #book #summary

Scalability

Масштабируемость - это заложенная в системе возможность продолжать [надежно] работать при росте нагрузки. Нагрузка может измеряться в числе пользователей, в частоте запросов, в объеме данных, которые система обрабатывает. Как правило, мы ожидаем, что при росте нагрузки достаточно добавить определенного вида серверных ресурсов, при этом не делая изменений в архитектуре системы.

Прежде всего, важно уметь описывать нагрузку на систему, как качественно (например, какие типы запросов могут приходить в систему), так и количественно (сколько всего запросов в секунду, какое соотношение читающей и пишущей нагрузки, сколько пользователей единовременно находится в системе). Здесь приводится небезызвестный пример Твиттера. В сервисе есть два типа основных действий: написать пост (пишущая нагрузка, в среднем 5К RPS, данные за 2012) и загрузить ленту (посты пользователей, на которых подписан, читающая нагрузка, 300K RPS).

Один из подходов к хранению данных в такой системе - складывать данные в нормализованном виде в классическую реляционную БД. Тогда запрос ленты это JOIN нескольких таблиц (посты, пользователи, подписки). Этот подход использовался в Твиттере изначально, но с ростом нагрузки (с ростом числа пользователей растет и объем данных, и нагрузка в RPS) такая схема плохо масштабировалась, было тяжело поддерживать работу тяжелого JOIN-запроса по вычислению ленты конкретного пользователя.

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

Подход с предрасчетом ленты работает хорошо для среднего пользователя, у которого десятки подписчиков. Однако есть аккаунты знаменитостей, на которых подписаны десятки миллионов пользователей. Для постов знаменитостей подход с предрасчетом ленты уже работает не так хорошо, потому что каждый твит вызывает огромное число записей, нагружая систему. В итоге Твиттер перешел на гибридную схему: для большинства пользователей их посты записываются в предрассчитанную ленту, а посты знаменитостей добавляются в ленту на лету, как в изначальном подходе с нормализованными данными в реляционной БД.
👍1
#ddia #book #summary

Scalability (продолжение)

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

Лингвистика. Автор обращает внимание, что хотя и термины response time (время ответа) и latency (задержка) часто используют как синонимы, они обозначают разные вещи. Время ответа замеряется на стороне клиента: помимо времени обработки запроса на сервере (англ. service time) оно включает в себя время передачи данных по сети, а также время нахождения запроса в различных очередях. Задержка это время, в течение которого запрос ожидает обработки, находится в latent-состоянии.

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

Арифметическое среднее плохо подходит для описания распределений, т.к. чувствительно к выбросам (очень большое единичное значение может сильно повлиять на среднее, хотя это единичный случай). Поэтому чаще всего используют персентили - p95, p99, p999. Например, если для некоторой системы p99 времени ответа 200мс, это значит, что на 99% запросов система отвечает не дольше, чем 200мс.

Может показаться нецелесообразным следить за высокими персентилями времени ответа, также известными как tail latencies, от англ. tail - хвост [распределения]. Казалось бы, оптимизировать персентиль p999 избыточно, т.к. это влияет только на 1 запрос (или пользователя) из 1000. Однако часто бывает что эти самые 1 из 1000 запросов приходят от пользователей с наибольшим количеством данных в сервисе (потому они и самые медленные), а, значит, это могут быть важные 0.1% пользователей с точки зрения бизнеса. При этом оптимизация p9999 времени ответа уже выглядит как невероятно дорогая оптимизация, не приносящая достаточно выгоды (данные от компании Amazon).

На высокие персентили времени ответа часто влияет долгое время нахождения запросов в очереди, связанное с эффектом блокировки начала очереди (англ. head-of-line blocking): один или несколько тяжелых запросов могут занять все ресурсы сервера (потоки, соединения к БД) на некоторое время, при этом остальные запросы, которые сами по себе можно обработать довольно быстро, вынуждены ждать в очереди, пока у сервера появятся свободные ресурсы. В микросервисной архитектуре высокие персентили времени ответа могут расти из-за одного медленного сервиса-зависимости: даже если делать запросы в N сервисов параллельно, время ответа будет не быстрее, чем время ответа самого медленного из N сервисов.

В контексте масштабирования часто всплывает дихотомия: вертикальное (увеличение мощности конкретного сервера) или горизонтальное (распределение нагрузки на бОльшее количество серверов) масштабирование. В реальности часто используется комбинация подходов, при этом для stateless систем характерно масштабироваться горизонтально, а для stateful - вертикально. При этом, конечно, у вертикального масштабирования есть вполне осязаемый предел, и в итоге нагруженные stateful системы сталкиваются с необходимостью становиться распределенными.
👍21
#ddia #book #summary

Maintainability

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

Чтобы упростить жизнь всем, кто занимается поддержкой, при проектировании системы важно придерживаться трех принципов: удобство эксплуатации (англ. operability), простота (англ. simplicity) и удобство развития (англ. evolvability). Последний принцип также известен как расширяемость (англ. extensibility), модифицируемость (англ. modifiability) или гибкость (англ. plasticity).

Удобство эксплуатации

Бытует мнение, что сильная команда эксплуатации может нивелировать некачественную систему, при этом, обратное неверно: даже идеально написанная система не сможет хорошо работать при плохой экплуатации. В задачи эксплуатации среди прочего входят:
- мониторинг состояния системы и восстановление системы в случае сбоев
- поиск причин возникающих проблем, таких как системные сбои или деградация производительности
- регулярные обновления компонентов системы, в том числе патчи безопасности
- поддержание актуальных знаний об устройстве системы, что позволяет предвидеть влияние действий, производимых над системой, а также дает возможность не зависеть от экспертизы конкретных людей (уменьшение влияния bus factor'а)
- построение безопасных процессов изменений в продакшене, таких как выкатка нового кода или изменение различных конфигураций

Для того, чтобы команда эксплуатации могла эффективно справляться с перечисленным задачами, система должна обладать следующими свойствами:
- быть хорошо наблюдаемой: иметь удобные метрики работы системы и инструмент для их просмотра в реальном времени
- автоматизировать типовые операции вроде релизов, откатов, рестартов, профилировки, расширения серверных ресурсов
- не иметь критичных компонентов без резервирования, то есть не должно быть конкретных серверов/узлов, обслуживание которых приводит к даунтайму системы
- поддерживать актуальными ранбуки (англ. runbook) - подробные и понятные инструкции, что нужно делать в ситуации инцидента, в которых не нужно тратить время на обдумывание действий, а просто следовать инструкциям в стиле "если произошло X, сделай Y"
- иметь хорошее поведение по умолчанию (например, настройки таймаутов/ретраев сетевых запросов), но при этом давать гибкость в настройке для специфичных сценариев
- обладать возможностью автоматического восстановления при возникновении поломок, при этом представляя возможность ручкой починки команде эксплуатации
- иметь максимально предсказуемое поведение
#ddia #book #summary

Maintainability (продолжение)

Простота (управление сложностью)

Кодовая база многих небольших проектов простая и выразительная, но по мере того, как проект растет, код часто становится сложным для понимания. Это, в свою очередь, усложняет и замедляет поддержку подобных проектов, которые иногда называют "большой ком грязи" (англ. big ball of mud). В таких проектах внесение изменений нередко может занимать непредсказуемо долгое время (что рушит планы и бюджеты), а также существует высокая вероятность внести баг в существующую функциональность при добавлении новой фичи. Типичные симптомы подобных проектов: взрыв пространства состояний (вследствие экспоненциального роста), сильная связность модулей, запутанные зависимости, неконсистентные именования и терминология и многое другое.

Для упрощения поддержки хочется двигаться в обратном направлении и, наоборот, снижать сложность имеющейся системы, но возможно ли это делать без выкидывания части функционала? Оказывается, что во многих случаях возникающая сложность является непреднамеренной (англ. accidental complexity). Это значит, что сложность происходит не из предметной области задачи, которую решает построенная система, а является результатом конкретной реализации.

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

Удобство развития

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

Существует популярная методология Agile (англ. гибкий), которая дает ответы на вопросы, как адаптироваться к изменениям при разработке систем. В рамках этой методологии существуют разные подходы и фреймворки, например, рефакторинг и TDD (англ. Test-Driven Development). Однако Agile по большей части сосредоточен на изменениях в масштабах нескольких файлов с исходным кодом внутри одного приложения, при этом не давая ответа на вопрос, как похожие изменения проводить в большой распределенной системе, состоящей из разных сервисов/приложений с разными характеристиками (например, бэкенд и база данных). Автор обещает дать ответы на подобные вопросы в рамках данной книги.
👍2
#caching #talk

Lazy Defenses: Using Scaled TTLs to Keep Your Cache Correct
by Bonnie Eisenman

Ссылка на доклад: YouTube

Рассказ от инженера Twitter (название компании на момент выхода доклада в 2017 году), которая занимается core-сервисами, в том числе сервисами, отвечающими за информацию о пользователях и связях между ними. Доклад фокусируется на одной из возможных техник по масштабированию бэкенд-сервисов при росте нагрузки - кэшировании. На момент рассказа для кэширования в распределенных системах в Twitter использовали twitter/twemcache - форк memcached, который теперь заменен на twitter/pelikan.

В качестве первого подхода к кэшированию предлагается использовать look aside cache: сначала проверяем наличие данных в кэше, а если их там нет (либо они устарели) - делаем запрос в базу данных. Появление кэша сразу поднимает ряд вопросов: что делать, если весь набор данных не помещается в памяти (иными словами, какие данные удалять, если нужно добавить новые, а места уже нет), каким должно быть время жизни записи в кэше (англ. TTL - time to live), как достигать хорошей консистентности данных (если кто-то обновил данные в БД, их нужно обновить и в кэше).

Комментарий от меня: этот простой, но популярный подход несет в себе серьезные риски для надежности, т.к. при отказе кэша может приводить к увеличению нагрузки на БД в несколько раз и даже на порядки (при высоком hit-ratio). Для осознания проблемы крайне рекомендую статью Metastable Failures in Distributed Systems, там же есть отдельный параграф 2.2 как раз про look aside cache.

Простое решение для уменьшения количества cache misses - использовать два вида TTL - soft и hard TTL. При исчерпании soft TTL (который строго меньше, чем hard TTL) мы не считаем это за cache miss, т.е. обслуживаем запрос, используя данные из кэша. При этом в фоне мы делаем запрос в БД, чтобы актуализировать данные в кэше. Если же исчерпан hard TTL, то считаем это обычным cache miss с обязательным походом в БД за свежими данными.

Специфика User Service в Twitter: миллионы RPS, читающих запросов значительно больше, чем пишущих, всплески нагрузки из-за широко обсуждаемых событий в реальном мире (спорт, политика, искусство), неравномерное распределение запросов/данных между разными пользователями, обновления часто происходят пачками, данных очень много, не получится их все хранить в памяти. Слой кэширования позволяет обслуживать запросы быстрее (чем могли бы с использованием БД), делать нагрузку на БД более равномерной (благодаря срезанию всплесков запросов к БД за счет кэша), использовать меньше железа. Кстати, специфика данных и нагрузки Twitter использовалась как интересный пример в главе про масштабируемость в книге Designing Data-Intensive Applications, можно почитать детально в моем посте.
👍3
#caching #talk

Lazy Defenses: Using Scaled TTLs to Keep Your Cache Correct
by Bonnie Eisenman

(продолжение)

Даже при наличии кэширующего слоя проблема всплесков нагрузки все еще актуальна. Тут докладчик вспоминает случай, когда селфи с церемонии Оскар в 2014 году, которое запостили и ретвитнули сразу много знаменитостей, сломало Twitter. Запросы, вызванные этим вирусным постом, попадали в маленькое число узлов кэширующего слоя (узлы, в которых хранились данные аккаунтов кого-то из ретвитнутых знаменитостей) и в итоге перегрузили эти узлы. Для решения этой проблемы самые горячие (наиболее часто запрашиваемые) ключи и их данные стали сохранять в памяти основного процесса бэкенда. Чтение из памяти своего процесса сильно дешевле и быстрее похода в кэширующий узел, даже если он находится в том же датацентре.

Кэширующий слой также не решает проблему, известную как retry storm, часто вызываемую массовыми сбоями. Про сбои и устойчивость к ним можно почитать мое саммари из той же книги Designing Data-Intensive Applications. Здесь же порекомендую фундаментальную статью Дениса Исаева про опасность механизма ретраев. Про различные стратегии ретраев можно почитать в прекрасном блоге Марка Брукера: Fixing retries with token buckets and circuit breakers. Twitter (как и многие другие) для защиты от шквала ретраев использует circuit breaker'ы: при большой доле ошибок в ответах на некоторое время отключаются запросы в основной источник (как правило, БД или другой микросервис), а вместо данных источника можно использовать данные из кэша, даже если их TTL уже истек. Видно, что как и многие другие популярные сервисы Twitter тяготеет больше в сторону доступности (англ. availability), нежели консистентности (англ. consistency) в терминах CAP теоремы.

Приводится интересный пример, как в Twitter на продолжительное время пропал аккаунт популярной спортивной команды: оказалось, что в кэше на 12 часов был сохранен ответ 404 Not found, который и отдавался всем клиентам, пока не истек TTL этой записи в кэше. Также упоминается синхронизации кэшей в разных датацентрах сложной распределенной системы: eventually consistent means inconsistent (в каждой шутке есть доля шутки). При этом величина TTL записей в кэше по сути определяет баланс между консистетностью и доступностью.

Наконец, раскрывается подход из заглавия доклада - Scaled TTLs. Суть заключается в том, чтобы не выбирать TTL единым образом для всех записей, а использовать адаптивный алгоритм, который меняет TTL на основе следующей эвристики: если при повторных обращениях по одному и тому же ключу данные (при чтении из источника) не меняются, то мы увеличиваем TTL записи в кэше. Дальше строится шкала изменения TTL с N шагами, которые подбираются под конкретную систему, требования к консистентности и нагрузке. С результатами применения подхода, тонкостями внедрения и реализации в виде библиотеки можно ознакомиться в первоисточнике.
👍31
#MetastableFailureState #MFS #RandomThoughts

Про метастабильные состояния отказа (англ. metastable failure или metastable failure state, MFS) точно нужно будет написать отдельный пост, но периодически я натыкаюсь на примеры метастабильности в реальной жизни и про парочку таких захотелось написать.

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

По этой теме крайне рекомендую фундаментальную статью Metastable Failures in Distributed Systems. Я попросил Gemini сравнить ее с другими статьями про MFS и вот ключевая цитата про статью: "Это обязательное чтение для тех, кто хочет понять первопричины метастабильности на самом глубоком уровне. Она не столько говорит делай так, сколько объясняет "почему" системы ведут себя именно таким образом."

Итак, первый пример мне встретился в книге "Зачем мы спим. Новая наука о сне и сновидениях". Допустим, вы были вынуждены переработать, засиделись допоздна, вернулись домой позже обычного и поздно легли спать. Из-за того, что вы легли позже обычного, на следующий день вы будете невыспавшимся (если только вам не повезло и переработка не случилась перед выходными, когда можно отоспаться).

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

Второй пример возник у меня в голове, когда я читал студентам лекцию про надежность распределенных систем. В лекции много внимания уделялось механике возникновения MFS в системах с очередями. Подобной системой можно моделировать выполнение студентами домашних заданий в течение семестра. В некоторых курсах система домашки такая: после каждой лекции/семинара выдается домашка с дедлайном через 2-3 недели.

Многие склонны откладывать сдачу домашек на последний момент перед дедлайном и пытаются сдать задание, у которого дедлайн наступит раньше всего (что, казалось бы, логично). Но представим, что студент начал делать домашку за день до дедлайна, потратил весь день, но не успел ее сдать. Если дедлайн жесткий (сдача после дедлайна дает 0 баллов), то с точки зрения баллов за курс сдача такого задания бесполезна, а ресурсы на нее потрачены зря. Понятно, что студент приобрел какие-то знания/навыки, пока делал задание, но именно с точки зрения сдачи курса он получил 0 баллов. Дальше студент переключается на другие курсы, а когда он доберется до следующей домашки, может случиться похожая история.

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

Опять же понятно, что пример искусственный, в реальности студенты просят преподавателя сдвинуть дедлайны, дедлайны могут быть мягкими (сдача после дедлайна дает 50% баллов), предыдущие задания могут быть пререквизитом последующих и т.д. Но механика входа в метастабильное состояние, на мой взгляд, здесь неплохо иллюстрируется.
11❤‍🔥3👍2
#HyrumsLaw #HashMaps

Прочитал в книге Software Engineering at Google про интересное наблюдение, которое получило название закон Хайрама (англ. Hyrum's Law). Формулировка примерно следующая: "При достаточно большом числе пользователей некоторого API не важно, что именно прописано в контракте интерфейса API: для любого наблюдаемого поведения обязательно найдется пользователь, который завяжется на это поведение".

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

Например, в Python, начиная с версии 3.7, контейнер dict сохраняет порядок вставки ключей. Код, вывод:


0 1 2 3 4 5 6 7 8 9
0 1 2 3 4 5 6 7 8 9
0 1 2 3 4 5 6 7 8 9
0 1 2 3 4 5 6 7 8 9
0 1 2 3 4 5 6 7 8 9


В С++ никаких гарантий нет, но при повторных итерациях обхода контейнера (при условии, что между обходами не было модифицирующих операций) порядок сохраняется. Код, вывод:


9 8 7 6 5 4 3 2 1 0
9 8 7 6 5 4 3 2 1 0
9 8 7 6 5 4 3 2 1 0
9 8 7 6 5 4 3 2 1 0
9 8 7 6 5 4 3 2 1 0


При этом, зная детали реализации контейнера в C++ (коллизии разрешаются при помощи цепочек или связных списков), можно строить ожидания относительно того, какой будет порядок обхода элементов.

В Go порядок обхода может меняться даже при повторных итерациях обхода контейнера (без изменений элементов в нем). Код, вывод:


6 7 9 2 3 4 8 0 1 5
4 8 0 1 5 6 7 9 2 3
0 1 5 6 7 9 2 3 4 8
9 2 3 4 8 0 1 5 6 7
5 6 7 9 2 3 4 8 0 1


Объяснение от авторов языка можно почитать здесь, но TL;DR - это как раз способ борьбы с тем, что разработчики завязываются на наблюдаемое поведение в своих тестах, а при будущих обновлениях языка или изменении платформы (и реализации), тесты оказываются сломанными.

При этом, авторы книги Software Engineering at Google, в число которых входит тот самый Хайрам, говорят, что даже рандомизация бессильна против закона Хайрама, потому что найдется пользователь, который будет использовать порядок обхода ключей в качестве не очень эффективного генератора случайных чисел. И если в будущем порядок поменяется, то снова возникнут проблемы.

Каких-то хорошо работающих советов авторы не дают, приводят неутешительные аналогии со вторым началом термодинамики и неубыванием энтропии. Мне показался интересным подход с рандомизацией в Go, за счет того, что он позволяет сократить цикл обратной связи и не дать написать тест, проверяющий не контракт API, а его наблюдаемое поведение.
👍144🔥1
#Caching

Один коллега сказал, что название канала не соответствует размеру постов: обещают TL;DR, а букв много. Поднимем этот когнитивный диссонанс на новый уровень еще более длинным постом: https://telegra.ph/Kak-Uber-primenyaet-kehshi-dlya-uderzhaniya-nagruzki-v-40-millionov-RPS-09-08

Статья от Uber про то, как они держат большую нагрузку, используя разные интересные решения с кэшами. Много крутых подходов поверх простого look aside кэширования. По сути получился близкий к тексту пересказ, но выкинуть что-то рука не поднялась.
🔥12
#LittlesLaw #QueueingTheory #ConcurrencyLimits

Многие элементы IT-инфраструктуры по сути являются системами массового обслуживания: сетевые маршрутизаторы, балансировщики нагрузки, бэкенд-сервисы, базы данных. А в теории массового обслуживания (англ. queueing theory) известен закон Литтла, имеющий важное прикладное значение. Приведу его в формулировке с Википедии:

Долгосрочное среднее количество L требований в стационарной системе равно долгосрочной средней интенсивности λ входного потока, умноженной на среднее время W пребывания заявки в системе. Алгебраически,  L = λW.


Результат может показаться интуитивно понятным, но строгое доказательство занимает 5 страниц: A Proof for the Queuing Formula.

Если посмотреть на бэкенд-сервис, обслуживающий запросы, то в законе Литтла L - это число запросов, находящихся в обработке или ожидающих в очереди, λ - это число запросов в секунду, приходящих в сервис (англ. requests per seconds, RPS), W - среднее время обработки запроса (включая время нахождения в очереди). Например, если входящий поток запросов - 100 RPS, среднее время обработки запроса - 50 миллисекунд (0.05 секунды), то среднее число запросов в обработке (англ. in-flight requests) 100 x 0.05 = 5.

Полезно проверить этот результат графически, изобразив запросы в виде прямоугольников, левая сторона которых соответствует моменту прихода запроса в систему, а длина - времени обслуживания. Для простоты удобно считать, что запросы приходят с постоянной периодичностью. Тогда может получиться что-то вроде рисунка 1. Видно, что высота "кирпичной стены" равна 5, что соответствует числу запросов, одновременно находящихся в обработке, или уровню параллелизма.

Следующий интересный вывод можно получить, если проверить, что будет, если время обработки начнет увеличиваться, например, из-за какой-то деградации в системе. Пример того, что происходит со "стеной" можно увидеть на рисунке 2. При росте времени обработки в 6 раз с 30 миллисекунд до 180 число запросов в обработке также растет в 6 раз.

Чем это вообще может быть полезно?

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

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

Тут важно отметить, что модель "кирпичной стены" работает, только если обработчик запросов берет одновременно в работу только один запрос, т.е. не берет следующий, пока не обработает предыдущий. Такая модель обработки характерна для многих БД - в одном соединении в моменте обрабатывается один запрос. Некоторые бэкенд-фреймворки используют пул потоков (англ. thread pool), поток обрабатывает один запрос одновременно. В протоколе HTTP версии 1 в одном соединении происходит обработка одного запроса.

Более современный подход заключается в более экономном использовании ресурсов, поэтому, например, в HTTP версии 2 используется мультиплексирование - в одном соединении могут одновременно обслуживаться несколько запросов. Многие языки и фреймворки используют более легковесные корутины вместо тяжеловесных потоков, что также позволяет обрабатывать более одного запроса на один поток (если, конечно, это не CPU-bound нагрузка).

Здесь модель с формулой Литтла для расчета нужного числа потоков/соединений ломается из-за мультиплексирования, однако все еще можно использовать ее для других ресурсов. Например, запросы в обработке скорее всего располагаются в памяти, а утилизация памяти растет с ростом параллелизма. Если запросы большие, то при росте таймингов обработки можно быстро упереться в лимит RAM (основано на реальных событиях).
👍54