<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>DataTalks.RU. Data Engineering / DWH / Data Pipeline</title>
	<atom:link href="https://datatalks.ru/feed/" rel="self" type="application/rss+xml" />
	<link>https://datatalks.ru/</link>
	<description>RoadMap для инженера данных. Дорожная карта по инструментам Data Engineer</description>
	<lastBuildDate>Sat, 14 Feb 2026 15:42:14 +0000</lastBuildDate>
	<language>ru-RU</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.0</generator>

<image>
	<url>https://datatalks.ru/wp-content/uploads/2024/12/cropped-logo_datatalks-32x32.png</url>
	<title>DataTalks.RU. Data Engineering / DWH / Data Pipeline</title>
	<link>https://datatalks.ru/</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>Context engineering = data governance + data engineering + data science</title>
		<link>https://datatalks.ru/context-engineering-data-teams/</link>
					<comments>https://datatalks.ru/context-engineering-data-teams/#respond</comments>
		
		<dc:creator><![CDATA[Data Engineer (Admin)]]></dc:creator>
		<pubDate>Thu, 12 Feb 2026 18:47:06 +0000</pubDate>
				<category><![CDATA[LLM / AI and Data Engineering]]></category>
		<category><![CDATA[Context engineering]]></category>
		<guid isPermaLink="false">https://datatalks.ru/?p=2850</guid>

					<description><![CDATA[<p>Источник статьи: Data teams should become context teams Data-команды должны стать командами контекста Context engineering = управление данными + инженерия данных + наука о данных. Помните, как ваша компания подключила BI-инструмент напрямую к продакшен-базе данных? Цифры постоянно были неверными. Никто не доверял дашбордам — поэтому мы построили data-стэки, чтобы это исправить. AI-агенты сегодня — это [&#8230;]</p>
<p>Сообщение <a href="https://datatalks.ru/context-engineering-data-teams/">Context engineering = data governance + data engineering + data science</a> появились сначала на <a href="https://datatalks.ru">DataTalks.RU. Data Engineering / DWH / Data Pipeline</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p><strong>Источник статьи:</strong> <a href="https://thenewaiorder.substack.com/p/data-teams-should-become-context" target="_blank" rel="noopener">Data teams should become context teams</a></p>
<h1>Data-команды должны стать командами контекста</h1>
<blockquote><p>Context engineering = управление данными + инженерия данных + наука о данных.</p></blockquote>
<hr />
<p>Помните, как ваша компания подключила BI-инструмент напрямую к продакшен-базе данных? Цифры постоянно были неверными. Никто не доверял дашбордам — поэтому мы построили data-стэки, чтобы это исправить.</p>
<p><strong>AI-агенты</strong> сегодня — это эквивалент BI-инструментов, подключённых к продакшен-БД. Теперь у каждой компании есть внутренние <strong>AI-агенты</strong>, подключённые к сырым источникам контекста: дискам, Notion, почте. Это вроде бы работает, но полностью доверять ответам нельзя.</p>
<p><strong>Context engineering</strong> — это создание источников истины для всех знаний компании надёжным и эффективным способом. И именно этим <strong>data-команды</strong> занимались с данными на протяжении многих лет.</p>
<p><strong>Context engineering</strong> требует ключевых навыков, которыми обладают data-команды:</p>
<ul>
<li><strong>Context Engineering</strong> = управление данными + инженерия данных + наука о данных</li>
<li>Context engineering требует управления для определения источников истины контекста</li>
<li>Context engineering требует инженерии для их загрузки и консолидации</li>
<li>Context engineering требует науки для измерения и повышения надёжности AI</li>
</ul>
<h2>Что такое context engineering?</h2>
<p><strong>Context engineering</strong> направлен на создание оптимального контекста для <strong>AI-агентов</strong>.</p>
<h3>Что такое оптимальный контекст для агента?</h3>
<ul>
<li><strong>Доля ответов:</strong> процент вопросов, на которые агент действительно может ответить</li>
<li><strong>Точность:</strong> процент ответов, которые являются корректными</li>
<li><strong>Стоимость:</strong> расходы на LLM, которые несёт агент</li>
<li><strong>Скорость:</strong> насколько быстро агент отвечает</li>
</ul>
<h3>Какие компромиссы нужно оптимизировать?</h3>
<p>Слишком мало контекста → неправильные ответы или их отсутствие.<br />
Агент знает недостаточно. Он галлюцинирует, упускает нюансы или полностью сдаётся.</p>
<p>Слишком много контекста → дорого и запутанно.</p>
<p>Входные токены могут очень быстро увеличить счёт за LLM (1 миллион токенов в Claude Opus 4.5 стоит $5). Вызов с большим объёмом контекста легко может отправлять 50–100 тыс. токенов на один запрос, что будет стоить ~50 центов. И помимо стоимости, нерелевантный контекст размывает сигнал — модель путается в шуме.</p>
<h3>Как можно спроектировать контекст?</h3>
<p>Выбирайте, какие источники включать, а какие исключать.</p>
<p>Определяйте, какой контент является источником истины по конкретной теме (правильное определение, самый свежий источник). Иногда вы можете обнаружить, что сами изначально не были в этом уверены.</p>
<h3>Создавайте новый контекст там, где его ещё нет.</h3>
<p>Форматируйте контекст так, чтобы модель могла эффективно его парсить: делайте его более модульным, хорошо структурированным.</p>
<p>Коротко говоря, <strong>context engineering</strong> следует тем же принципам, что и <strong>data engineering</strong>: измерять, итерировать, оптимизировать. Отслеживайте производительность вашего агента. Определяйте причины сбоев. Добавляйте недостающий контекст. Тестируйте улучшения. Повторяйте.</p>
<h1>Управление контекстом: источник истины контекста — это новый источник истины данных</h1>
<p>Нам нужно <strong>управление контекстом</strong> так же, как раньше нам было нужно управление данными.</p>
<p>Нам было нужно управление данными, потому что без него «выручка» означала три разных вещи в зависимости от того, кого вы спрашивали. Команда маркетинга считала валовые бронирования. Финансы считали чистый ARR. Продуктовая команда считала активные подписки. Нет метрического слоя, нет канонического определения — поэтому каждый дашборд рассказывал свою историю.</p>
<p>Сегодня нам нужно управление контекстом, потому что знания компании имеют ту же самую проблему. Спросите «какова наша политика возвратов?» — и ответ будет зависеть от того, какой документ агент найдёт первым: устаревший Notion, последний ответ в Zendesk или сообщение от юридического отдела в Slack за прошлый квартал. А иногда никто на самом деле и не задумывался, каким должен быть правильный ответ.</p>
<p>Многие специалисты по данным помнят тревожные времена, когда приходили в компанию, где BI был подключён напрямую к продакшен-базе данных. Все данные были на месте, но ни одна цифра не совпадала с другой, всё работало медленно и болезненно. Сегодня мы делаем ровно то же самое, подключая AI ко всем знаниям нашей компании.</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2026/02/Context_governance.jpg"><img fetchpriority="high" decoding="async" class="aligncenter size-full wp-image-2857" src="https://datatalks.ru/wp-content/uploads/2026/02/Context_governance.jpg" alt="" width="1140" height="752" srcset="https://datatalks.ru/wp-content/uploads/2026/02/Context_governance.jpg 1140w, https://datatalks.ru/wp-content/uploads/2026/02/Context_governance-300x198.jpg 300w, https://datatalks.ru/wp-content/uploads/2026/02/Context_governance-1024x675.jpg 1024w, https://datatalks.ru/wp-content/uploads/2026/02/Context_governance-768x507.jpg 768w, https://datatalks.ru/wp-content/uploads/2026/02/Context_governance-450x297.jpg 450w, https://datatalks.ru/wp-content/uploads/2026/02/Context_governance-780x515.jpg 780w" sizes="(max-width: 1140px) 100vw, 1140px" /></a></p>
<p>Мы все знаем, что знания компании полны неточностей, устаревших элементов и противоречий. Поэтому подключать агента напрямую к этому хаосу — не самая лучшая идея.</p>
<p>Нам нужен <strong>контекстный слой:</strong> единый, управляемый, версионируемый источник истины для знаний компании. Чёткий ответ на каждый вопрос, с которым может столкнуться агент. И нам нужна инфраструктура, чтобы его строить и поддерживать.</p>
<h1>Context engineering: контекстный стек — это data-стек</h1>
<ul>
<li>Чтобы создать источники истины данных, мы построили <strong>data-стек</strong>.</li>
<li>Чтобы создать источники истины контекста, нам нужен <strong>контекстный стек</strong>.</li>
</ul>
<p><a href="https://datatalks.ru/wp-content/uploads/2026/02/Context_engineering_tools.jpg"><img decoding="async" class="aligncenter size-full wp-image-2859" src="https://datatalks.ru/wp-content/uploads/2026/02/Context_engineering_tools.jpg" alt="" width="1484" height="950" srcset="https://datatalks.ru/wp-content/uploads/2026/02/Context_engineering_tools.jpg 1484w, https://datatalks.ru/wp-content/uploads/2026/02/Context_engineering_tools-300x192.jpg 300w, https://datatalks.ru/wp-content/uploads/2026/02/Context_engineering_tools-1024x656.jpg 1024w, https://datatalks.ru/wp-content/uploads/2026/02/Context_engineering_tools-768x492.jpg 768w, https://datatalks.ru/wp-content/uploads/2026/02/Context_engineering_tools-450x288.jpg 450w, https://datatalks.ru/wp-content/uploads/2026/02/Context_engineering_tools-780x499.jpg 780w" sizes="(max-width: 1484px) 100vw, 1484px" /></a></p>
<p>Ситуация сегодня такая же, как с данными 10 лет назад: у нас есть источники, у нас есть инструменты потребления. Но у нас нет промежуточного слоя — <strong>контекстного ETL-слоя</strong>.</p>
<p><strong>Нам нужны:</strong></p>
<ul>
<li>Инструменты ingestion для автоматического подтягивания источников контекста</li>
<li>Инструменты трансформации для выбора источника истины контекста</li>
<li>Контекстный слой как источник истины знаний компании</li>
<li>Оркестрация для поддержания актуальности контекста</li>
</ul>
<p><strong>Мониторинг AI</strong> для измерения и отслеживания производительности нашего контекста в AI-агентах</p>
<p>Некоторые data-команды уже начали собирать части этого самостоятельно. Я видел, как команды пишут скрипты для выгрузки метаданных схем и статистики профилирования из хранилища, синхронизируют документацию из data-каталога или отбирают проверенные запросы из BI-инструмента в markdown-файлы. Это работает — но это множество скриптов и постоянная поддержка.</p>
<p>С мониторингом всё ещё сложнее. Большинство инструментов для аналитических агентов пока не поддерживают фреймворки оценки, поэтому нет простого способа построить unit-тесты, которые проверяют, что ваш контекст по-прежнему выдаёт правильные ответы после изменений.</p>
<p>Когда у нас есть управление и стек, нам нужно использовать техники <strong>data science</strong>, чтобы итерироваться и улучшать контекст.</p>
<h1>Context sciences: тонкая настройка контекста как параметров ML-модели</h1>
<p>В ML вы определяете метрику успеха (<strong>accuracy</strong> и т.д.) и имеете <strong>train/test-набор размеченных данных</strong>. Затем вы настраиваете параметры, признаки, обучающие выборки. После каждого изменения измеряете производительность, пока не найдёте оптимум.</p>
<p>В <strong>context engineering</strong> должен быть тот же цикл. Вы определяете метрики успеха (надёжность, стоимость и т.д.). Ваши параметры — это источники истины контекста, форматирование контекста, инструменты. Вы можете создать набор <strong>unit-тестов</strong> из промптов и ожидаемых ответов. Вы меняете контекст, заново прогоняете тестовые промпты, измеряете влияние, оставляете то, что работает.</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2026/02/Context_sciences.png"><img decoding="async" class="aligncenter size-full wp-image-2860" src="https://datatalks.ru/wp-content/uploads/2026/02/Context_sciences.png" alt="" width="1686" height="890" srcset="https://datatalks.ru/wp-content/uploads/2026/02/Context_sciences.png 1686w, https://datatalks.ru/wp-content/uploads/2026/02/Context_sciences-300x158.png 300w, https://datatalks.ru/wp-content/uploads/2026/02/Context_sciences-1024x541.png 1024w, https://datatalks.ru/wp-content/uploads/2026/02/Context_sciences-768x405.png 768w, https://datatalks.ru/wp-content/uploads/2026/02/Context_sciences-1536x811.png 1536w, https://datatalks.ru/wp-content/uploads/2026/02/Context_sciences-450x238.png 450w, https://datatalks.ru/wp-content/uploads/2026/02/Context_sciences-780x412.png 780w, https://datatalks.ru/wp-content/uploads/2026/02/Context_sciences-1600x845.png 1600w" sizes="(max-width: 1686px) 100vw, 1686px" /></a></p>
<p><strong>Дополнительная сложность</strong> — как измерять метрики → стоимость и скорость измерить легко, но для оценки надёжности агента нужны более специализированные инструменты: проверять использованные файлы? точное совпадение? LLM как судья?</p>
<p>Чтобы это реализовать, нужно построить собственный <strong>evaluation framework</strong>. Определить KPI, которые вы будете отслеживать — что такое успех агента, как его измерять, какие ещё параметры важны (стоимость, скорость и т.д.). Затем создать набор <strong>unit-тестов</strong> и тонко настраивать контекст, измеряя производительность на разных наборах контекста.</p>
<h2>Как начать переход уже сейчас</h2>
<p>Как вы видите, контекстный стек пока ещё не сформирован. Нам всё ещё не хватает инструментов для открытого курирования и улучшения контекста.</p>
<p>Я думаю, что первым шагом для data-команд может быть демонстрация того, что они владеют <strong>context engineering</strong> в своей области: можете ли вы действительно заставить контекст для вашего аналитического агента работать?</p>
<p>Как я показывал в своих предыдущих статьях с бенчмарками аналитических агентов, готовые решения «из коробки» не работают и являются чёрными ящиками контекста. Если <strong>data-команды</strong> инвестируют в <strong>context engineering</strong> для собственных аналитических агентов, я уверен, они смогут показать, что это работает лучше, чем решения «из коробки».</p>
<p><strong>Два подхода уже сейчас позволяют войти в context engineering:</strong></p>
<ul>
<li>AI-агенты, работающие с файловой системой (<strong>Cursor</strong>, <strong>Claude Code</strong>, <strong>Cowork</strong>, <strong>Codex</strong>)</li>
<li>Эти инструменты читают контекст напрямую из файлов, которыми вы управляете. Вы точно видите, что знает агент, можете изменить это, отредактировав файл, и сразу измерить эффект.</li>
<li>Кроме того, можно построить <strong>evaluation framework</strong> поверх этого, поскольку всё доступно через код.</li>
</ul>
<h2>Собственные (in-house) агенты</h2>
<p><strong>Если вы построили собственного агента, вы контролируете весь конвейер контекста:</strong> какие элементы контекста добавлять и как оценивать агента. Создайте набор unit-тестов из промптов и начните прогонять их в разных сценариях контекста.</p>
<h1>Ссылки на дополнительные статьи / материалы</h1>
<ul>
<li>GitHub: <a href="https://github.com/humanlayer/12-factor-agents/" target="_blank" rel="noopener">12-Factor Agents &#8212; Principles for building reliable LLM applications</a></li>
<li><a href="https://www.datacamp.com/blog/context-engineering" target="_blank" rel="noopener">Context Engineering: A Guide With Examples</a></li>
<li><a href="https://github.com/different-ai/openwork" target="_blank" rel="noopener">GitHub: OpenWork</a></li>
<li><a href="https://github.com/microsoft/data-formulator" target="_blank" rel="noopener">GitHub: Data Formulator: AI-powered Data Visualization</a></li>
</ul>
<h2>Сайты со Skills для ai-agents</h2>
<ul>
<li><a href="https://skills.sh" target="_blank" rel="noopener">skills.sh</a></li>
<li><a href="https://skillhub.club/" target="_blank" rel="noopener">skillhub.club</a></li>
<li><a href="https://skillsmp.com" target="_blank" rel="noopener">skillsmp.com</a></li>
<li><a href="https://github.com/VoltAgent/awesome-agent-skills" target="_blank" rel="noopener">GitHub: Awesome Agent Skills</a></li>
</ul>
<h2>Обучающие материалы по ai, llm</h2>
<ul>
<li><a href="https://github.com/microsoft/ai-agents-for-beginners" target="_blank" rel="noopener">GitHub: Microsoft AI Agents for Beginners &#8212; A Course</a></li>
<li><a href="https://github.com/pguso/ai-agents-from-scratch" target="_blank" rel="noopener">GitHub: AI Agents From Scratch</a></li>
<li><a href="https://github.com/Marktechpost/AI-Tutorial-Codes-Included" target="_blank" rel="noopener">GitHub: Codes/Notebooks for AI Projects</a></li>
<li><a href="https://github.com/KalyanKS-NLP/rag-zero-to-hero-guide" target="_blank" rel="noopener">RAG Zero to Hero Guide</a></li>
<li><a href="https://github.com/NirDiamant/RAG_Techniques" target="_blank" rel="noopener">Advanced RAG Techniques: Elevating Your Retrieval-Augmented Generation Systems</a></li>
<li><a href="https://github.com/langchain-ai/rag-from-scratch" target="_blank" rel="noopener">RAG From Scratch</a></li>
<li><a href="https://github.com/microsoft/skills" target="_blank" rel="noopener">GitHub: Skills, MCP servers, Custom Agents, Agents.md for SDKs to ground Coding Agents</a></li>
<li><a href="https://github.com/microsoft/mcp-for-beginners" target="_blank" rel="noopener">Model Context Protocol (MCP) Curriculum for Beginners</a></li>
<li><a href="https://github.com/Devinterview-io/llms-interview-questions" target="_blank" rel="noopener">63 Must-Know LLMs Interview Questions in 2026</a></li>
</ul>
<h2>Data Engineering AI</h2>
<ul>
<li><a href="https://github.com/astronomer/agents" target="_blank" rel="noopener">GitHub Astronomer: AI agent tooling for data engineering workflows</a> &#8212; Includes an MCP server for Airflow, a CLI tool (af) for interacting with Airflow from your terminal, and skills that extend AI coding agents with specialized capabilities for working with Airflow and data warehouses.</li>
</ul>
<p>Сообщение <a href="https://datatalks.ru/context-engineering-data-teams/">Context engineering = data governance + data engineering + data science</a> появились сначала на <a href="https://datatalks.ru">DataTalks.RU. Data Engineering / DWH / Data Pipeline</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://datatalks.ru/context-engineering-data-teams/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Подготовка к собеседованию Python</title>
		<link>https://datatalks.ru/python-interview/</link>
					<comments>https://datatalks.ru/python-interview/#respond</comments>
		
		<dc:creator><![CDATA[Data Engineer (Admin)]]></dc:creator>
		<pubDate>Tue, 20 Jan 2026 19:07:45 +0000</pubDate>
				<category><![CDATA[Python]]></category>
		<category><![CDATA[Python Interview]]></category>
		<category><![CDATA[Python Интервью]]></category>
		<category><![CDATA[Python Собеседование]]></category>
		<guid isPermaLink="false">https://datatalks.ru/?p=2823</guid>

					<description><![CDATA[<p>Одним из основных hard skill дата инженера является знание Python, так как это достаточно удобный язык для реализации различного функционала. В этой статье будет подборка материалов для подготовки к интервью по Python. YouTube Ну НАСТОЯЩИЙ Senior! 10 лет опыта и экспертиза абсолютно во ВСЁМ? / Техсобес Senior Python Developer ТОП 70 ВОПРОСОВ НА СОБЕСЕДОВАНИИ SENIOR [&#8230;]</p>
<p>Сообщение <a href="https://datatalks.ru/python-interview/">Подготовка к собеседованию Python</a> появились сначала на <a href="https://datatalks.ru">DataTalks.RU. Data Engineering / DWH / Data Pipeline</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p>Одним из основных hard skill дата инженера является знание Python, так как это достаточно удобный язык для реализации различного функционала. В этой статье будет подборка материалов для подготовки к интервью по Python.</p>
<h1>YouTube</h1>
<ul>
<li><a href="https://www.youtube.com/watch?v=2J1Zxps_GIc" target="_blank" rel="noopener">Ну НАСТОЯЩИЙ Senior! 10 лет опыта и экспертиза абсолютно во ВСЁМ? / Техсобес Senior Python Developer</a></li>
<li><a href="https://www.youtube.com/watch?v=j2My4dt6mLk" target="_blank" rel="noopener">ТОП 70 ВОПРОСОВ НА СОБЕСЕДОВАНИИ SENIOR PYTHON DEVELOPER</a></li>
<li><a href="https://www.youtube.com/watch?v=QQD-l8Leqog" target="_blank" rel="noopener">#33 Собеседование Python 2025 в BIG TECH компанию | Разбор вопросов</a></li>
<li><a href="https://www.youtube.com/watch?v=bFcbnBGkBLY" target="_blank" rel="noopener">Python разработчик | Собеседование с задачей из Яндекса. Максим Никулин</a></li>
<li><a href="https://www.youtube.com/watch?v=6BioMYL56po" target="_blank" rel="noopener">«Техническое собеседование python-разработчика уровня мидл»</a></li>
</ul>
<h1>Рекомендации по подготовке к интервью в разных компаниях</h1>
<ul>
<li><a href="https://www.tbank.ru/career/it/interview/python/" target="_blank" rel="noopener">ТБанк: Как проходит интервью по Python</a></li>
<li><strong>Подготовка к алгоритмической секции (Яндекс):</strong>
<ul>
<li><a href="https://leetcode.com/explore/featured/card/top-interview-questions-easy/127/strings/885/" target="_blank" rel="noopener">https://leetcode.com/explore/featured/card/top-interview-questions-easy/127/strings/885/</a></li>
<li><a href="https://leetcode.com/problems/kth-smallest-element-in-a-bst/" target="_blank" rel="noopener">https://leetcode.com/problems/kth-smallest-element-in-a-bst/</a></li>
<li><a href="https://leetcode.com/problems/group-anagrams/" target="_blank" rel="noopener">https://leetcode.com/problems/group-anagrams/</a></li>
<li><a href="https://leetcode.com/problems/decode-ways/" target="_blank" rel="noopener">https://leetcode.com/problems/decode-ways/</a></li>
<li><a href="https://leetcode.com/problems/word-break/" target="_blank" rel="noopener">https://leetcode.com/problems/word-break/</a></li>
<li><a href="https://leetcode.com/problems/integer-to-roman/" target="_blank" rel="noopener">https://leetcode.com/problems/integer-to-roman/</a></li>
<li>Строки, массивы</li>
<li>Хеш-таблицы, словари</li>
<li>Обход двоичного дерева</li>
<li>Реализация различных примитивов: счётчиков, кешей и т. д.</li>
</ul>
</li>
</ul>
<h1>Подборки задач (Leetcode и другие)</h1>
<ul>
<li><a href="https://leetcode.com/discuss/post/460599/blind-75-leetcode-questions-by-krishnade-9xev/" target="_blank" rel="noopener">Blind 75 LeetCode Questions</a></li>
<li><a href="https://neetcode.io/roadmap" target="_blank" rel="noopener">neetcode.io</a></li>
<li><a href="https://leetcopilot.dev/blog/best-free-coding-interview-prep-resources-2026" target="_blank" rel="noopener">Best Free Coding Interview Prep Resources in 2026 (Complete List)</a></li>
<li><a href="https://github.com/umitkacar/awesome-interview" target="_blank" rel="noopener">Coding Interview Prep (awesome interview)</a></li>
<li><strong>Моя подборка задач Easy уровня на Leetcode:</strong>
<ul>
<li><a href="https://leetcode.com/problem-list/wpdwlphj/" target="_blank" rel="noopener">1. Arrays Easy</a></li>
<li><a href="https://leetcode.com/problem-list/wpd040lr/" target="_blank" rel="noopener">2. String Easy</a></li>
<li><a href="https://leetcode.com/problem-list/wpd0crwh/" target="_blank" rel="noopener">3. Sorting Easy</a></li>
<li><a href="https://leetcode.com/problem-list/wpd7hzr5/" target="_blank" rel="noopener">4. Counting Easy</a></li>
</ul>
</li>
<li>Решение задач по алгоритмам <a href="https://github.com/ivanshamaev/python-algorithms-data-engineer/tree/main/%D0%9F%D0%BE%D0%B4%D0%B3%D0%BE%D1%82%D0%BE%D0%B2%D0%BA%D0%B0%20%D0%BA%20%D1%81%D0%BE%D0%B1%D0%B5%D1%81%D0%B5%D0%B4%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D1%8F%D0%BC%20(%D0%B7%D0%B0%D0%B4%D0%B0%D1%87%D0%B8)" target="_blank" rel="noopener">&#171;Подготовка к собеседованиям (задачи)&#187;</a></li>
</ul>
<p>Сообщение <a href="https://datatalks.ru/python-interview/">Подготовка к собеседованию Python</a> появились сначала на <a href="https://datatalks.ru">DataTalks.RU. Data Engineering / DWH / Data Pipeline</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://datatalks.ru/python-interview/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Pytest Tutorial: тесты, fixture, mark, parametrize</title>
		<link>https://datatalks.ru/pytest-fixture-parametrization-data-engineer/</link>
					<comments>https://datatalks.ru/pytest-fixture-parametrization-data-engineer/#respond</comments>
		
		<dc:creator><![CDATA[Data Engineer (Admin)]]></dc:creator>
		<pubDate>Sun, 18 Jan 2026 19:47:03 +0000</pubDate>
				<category><![CDATA[Python]]></category>
		<category><![CDATA[pytest]]></category>
		<guid isPermaLink="false">https://datatalks.ru/?p=2808</guid>

					<description><![CDATA[<p>Основные термины и понятия Pytest Test / Test Case (тест / тест-кейс) &#8212; Одиночная проверка поведения кода: функция, имя которой начинается с test_. Pytest автоматически обнаруживает и запускает такие тесты. Assertion (ассерция / утверждение) &#8212; Проверка, выраженная через стандартный Python-оператор assert. Pytest делает assert-интроспекцию — показывает подробный разбор выражения при ошибке, что упрощает диагностику. Assertion Introspection (интроспекция [&#8230;]</p>
<p>Сообщение <a href="https://datatalks.ru/pytest-fixture-parametrization-data-engineer/">Pytest Tutorial: тесты, fixture, mark, parametrize</a> появились сначала на <a href="https://datatalks.ru">DataTalks.RU. Data Engineering / DWH / Data Pipeline</a>.</p>
]]></description>
										<content:encoded><![CDATA[<h1>Основные термины и понятия Pytest</h1>
<p><strong>Test / Test Case (тест / тест-кейс)</strong> &#8212; Одиночная проверка поведения кода: функция, имя которой начинается с <code>test_</code>. Pytest автоматически обнаруживает и запускает такие тесты.</p>
<p><strong>Assertion (ассерция / утверждение)</strong> &#8212; Проверка, выраженная через <strong>стандартный Python-оператор</strong> <code>assert</code>. Pytest делает <code>assert</code>-интроспекцию — показывает подробный разбор выражения при ошибке, что упрощает диагностику.</p>
<p><strong>Assertion Introspection (интроспекция ассерций)</strong> &#8212; Особенность Pytest: при провале <code>assert</code> показываются подробности выражения (значения левой/правой части, вложенные выражения и т.д.).</p>
<p><strong>Fixture (фикстура)</strong> &#8212; Функция, отмеченная декоратором <code>@pytest.fixture</code>, которая подготавливает окружение (данные, подключения и т. д.) для тестов и/или выполняет очистку после. Тесты “запрашивают” фикстуру через аргументы функций.</p>
<p><strong>Scope (область действия) фикстуры</strong> может быть:</p>
<ul>
<li>function — на каждый тест,</li>
<li>class — на класс,</li>
<li>module — на модуль,</li>
<li>package — на пакет,</li>
<li>session — на весь прогон.</li>
</ul>
<p><strong>Marker (маркер)</strong> &#8212; Метаданные, которыми помечаются тесты для отнесения их к категориям/группам (например, <code>@pytest.mark.slow</code>, <code>@pytest.mark.integration</code>), а также изменения их поведения. Маркеры упрощают выборку тестов при запуске с помощью опции <code>-m</code>.</p>
<p><strong>Mock</strong> &#8212; объект-заглушка, который заменяет реальный объект в тестах и позволяет проверять, как он использовался. <strong>Monkeypatch</strong> &#8212; фикстура <code>pytest</code> для временного изменения атрибутов, словарей или <code>environment variables</code>. Используется для изоляции тестируемого кода от его зависимостей (например, подмена запроса к <code>API</code> возвратом готовых данных).</p>
<p><strong>Parametrization (параметризация)</strong> &#8212; Механизм запуска одного теста с разными наборами аргументов, обычно через декоратор <code>@pytest.mark.parametrize</code>. Это позволяет покрывать больше сценариев без копирования кода.</p>
<p><strong>Skip / Skipif (пропуск теста)</strong> &#8212; Маркер или вызов (<code>pytest.mark.skip</code>, <code>pytest.mark.skipif</code>) для пропуска теста в определённых условиях (например, на неподходящей платформе).</p>
<p><strong>XFail / Expected Failure (ожидаемо падающий тест)</strong> &#8212; Маркер <code>pytest.mark.xfail</code> для тестов, которые ожидаемо должны упасть (например, из-за известной ошибки). Их падение не считается ошибкой прогонки.</p>
<p><strong>Plugins (плагины)</strong> &#8212; Расширения, которые добавляют Pytest-функциональность (например, отчеты, параллельный запуск, интеграции). Pytest имеет богатую экосистему плагинов.<br />
Популярные плагины:</p>
<ul>
<li><code>pytest-cov</code>: интеграция с <code>coverage.py</code> для анализа покрытия кода</li>
<li><code>pytest-xdist</code>: для параллельного запуска тестов</li>
<li><code>pytest-django</code>: для тестирования <strong>Django-приложений</strong></li>
<li><code>pytest-asyncio</code>: для тестирования <strong>asyncio-кода</strong></li>
</ul>
<p><strong>Conftest.py</strong> &#8212; Специальный файл Python, который используется для хранения общих фикстур, плагинов и хуков для всего проекта. Фикстуры и плагины, объявленные в <code>conftest.py</code>, автоматически обнаруживаются и становятся доступными во всех тестах того же <code>package</code> и его подпакетах. Это центральное место для общей конфигурации.</p>
<p><strong>Hook (хук)</strong> &#8212; Функция, которая вызывается в определенные моменты выполнения тестов (например, во время сбора или запуска тестов) и позволяет кастомизировать поведение pytest. Используется для написания плагинов или изменения стандартного поведения pytest (например, добавление своих действий перед запуском всех тестов).</p>
<p><strong>Test Suite (набор тестов)</strong> &#8212; Группа тестов (обычно множество функций/файлов), которые выполняются вместе.</p>
<p><strong>Test Collection (сбор тестов)</strong> &#8212; Процесс, когда Pytest сканирует каталоги и файлы, находит тесты и готовит их к выполнению.</p>
<p><strong>Test Report (отчет о тестах)</strong> &#8212; Итоговый вывод после выполнения тестов, включающий количество прошедших, проваленных, пропущенных и <code>xfail</code>-тестов.</p>
<h1>Тестовый проект PyTest на GitHub</h1>
<p><a href="https://github.com/ivanshamaev/pytest-tutorial" target="_blank" rel="noopener">https://github.com/ivanshamaev/pytest-tutorial</a></p>
<h1>Курсы pytest и материалы</h1>
<h2>YouTube</h2>
<ul>
<li><a href="https://www.youtube.com/playlist?list=PLeLN0qH0-mCVdHgdjlnKTl4jKuJgCK-4b" target="_blank" rel="noopener">Pytest Курс</a></li>
<li><a href="https://www.youtube.com/watch?v=J_Dfbr6tIKU" target="_blank" rel="noopener">Pytest. Необходимый минимум</a></li>
</ul>
<p>Сообщение <a href="https://datatalks.ru/pytest-fixture-parametrization-data-engineer/">Pytest Tutorial: тесты, fixture, mark, parametrize</a> появились сначала на <a href="https://datatalks.ru">DataTalks.RU. Data Engineering / DWH / Data Pipeline</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://datatalks.ru/pytest-fixture-parametrization-data-engineer/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Best Practices &#8212; Airflow 3 Документация</title>
		<link>https://datatalks.ru/best-practices-airflow-3-documentation/</link>
					<comments>https://datatalks.ru/best-practices-airflow-3-documentation/#respond</comments>
		
		<dc:creator><![CDATA[Data Engineer (Admin)]]></dc:creator>
		<pubDate>Sat, 17 Jan 2026 15:48:03 +0000</pubDate>
				<category><![CDATA[Apache Airflow Best Practices]]></category>
		<category><![CDATA[Apache Airflow]]></category>
		<category><![CDATA[Apache Airflow 3]]></category>
		<guid isPermaLink="false">https://datatalks.ru/?p=2751</guid>

					<description><![CDATA[<p>Перевод документации Apache Airflow 3 &#8212; Best Practices Лучшие практики по работе с Apache Airflow 3 Создание нового Dag — это процесс из трёх шагов: написание Python-кода для создания объекта Dag, проверка того, что код соответствует вашим ожиданиям, настройка зависимостей окружения для запуска вашего Dag В этом руководстве представлены лучшие практики для этих трёх шагов. [&#8230;]</p>
<p>Сообщение <a href="https://datatalks.ru/best-practices-airflow-3-documentation/">Best Practices &#8212; Airflow 3 Документация</a> появились сначала на <a href="https://datatalks.ru">DataTalks.RU. Data Engineering / DWH / Data Pipeline</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p>Перевод <a href="https://airflow.apache.org/docs/apache-airflow/stable/best-practices.html" target="_blank" rel="noopener">документации Apache Airflow 3 &#8212; Best Practices</a></p>
<h1>Лучшие практики по работе с Apache Airflow 3</h1>
<p>Создание нового <strong>Dag</strong> — это процесс из трёх шагов:</p>
<ul>
<li>написание Python-кода для создания объекта Dag,</li>
<li>проверка того, что код соответствует вашим ожиданиям,</li>
<li>настройка зависимостей окружения для запуска вашего Dag</li>
</ul>
<p>В этом руководстве представлены лучшие практики для этих трёх шагов.</p>
<h2>Написание Dag</h2>
<p>Создание нового Dag в Airflow довольно простое. Однако существует множество вещей, о которых необходимо позаботиться, чтобы запуск <strong>Dag</strong> или его сбой не приводили к неожиданным результатам.</p>
<h3><strong>Создание пользовательского Operator/Hook</strong></h3>
<p>Пожалуйста, следуйте нашему <a href="https://airflow.apache.org/docs/apache-airflow/stable/howto/custom-operator.html#custom-operator" target="_blank" rel="noopener">руководству по пользовательским (custom) <strong>Operator</strong>’ам</a>.</p>
<h3><strong>Создание задачи</strong></h3>
<p>Вы должны рассматривать задачи в <strong>Airflow</strong> как эквивалент транзакций в базе данных. Это означает, что ваши задачи никогда не должны производить неполные результаты. Например, нельзя оставлять неполные данные в <strong>HDFS</strong> или <strong>S3</strong> по завершении задачи.</p>
<p><strong>Airflow</strong> может повторно запускать задачу в случае её сбоя. Следовательно, задачи должны выдавать одинаковый результат при каждом повторном запуске. Некоторые способы избежать получения различного результата:</p>
<ul>
<li>Не используйте <code>INSERT</code> при повторном запуске задачи — оператор <code>INSERT</code> может привести к появлению дублирующихся строк в базе данных. Замените его на <code>UPSERT</code>.</li>
<li>Читайте и записывайте данные в конкретный партицию. Никогда не читайте самые последние доступные данные в задаче. Кто-то может обновить входные данные между повторными запусками, что приведёт к разным результатам. Лучший подход — читать входные данные из конкретного партициона. В качестве партициона можно использовать <code>data_interval_start</code>. Этот же метод партиционирования следует применять и при записи данных в S3/HDFS.</li>
<li>Функция Python datetime <code>now()</code> возвращает текущий объект <code>datetime</code>. Эту функцию никогда не следует использовать внутри задачи, особенно для выполнения критических вычислений, так как это приводит к разным результатам при каждом запуске. Допустимо использовать её, например, для генерации временного лога.</li>
</ul>
<p><strong>Совет</strong></p>
<p>Следует определять повторяющиеся параметры, такие как <code>connection_id</code> или пути <strong>S3</strong>, в <code>default_args</code>, а не объявлять их для каждой задачи. <code>default_args</code> помогают избежать ошибок, таких как опечатки. Кроме того, большинство типов соединений имеют уникальные имена параметров в задачах, поэтому вы можете объявить соединение только один раз в <code>default_args</code> (например, <code>gcp_conn_id</code>), и оно будет автоматически использоваться всеми операторами, которые работают с данным типом соединения.</p>
<h3><strong>Удаление задачи</strong></h3>
<p>Будьте осторожны при удалении задачи из Dag. После удаления вы не сможете увидеть эту задачу в <strong>Graph View</strong>, <strong>Grid View</strong> и других представлениях, что усложнит проверку логов данной задачи через <strong>Webserver</strong>. Если такое поведение нежелательно, пожалуйста, создайте новый <strong>Dag</strong>.</p>
<h3><strong>Коммуникация</strong></h3>
<p><strong>Airflow</strong> выполняет задачи Dag на разных серверах в случае использования <strong><a href="https://airflow.apache.org/docs/apache-airflow-providers-cncf-kubernetes/stable/kubernetes_executor.html" target="_blank" rel="noopener">Kubernetes Executor</a></strong> или <strong><a href="https://airflow.apache.org/docs/apache-airflow-providers-celery/stable/celery_executor.html" target="_blank" rel="noopener">Celery Executor</a></strong>. Поэтому не следует хранить какие-либо файлы или конфигурации в локальной файловой системе, так как следующая задача с большой вероятностью будет выполняться на другом сервере без доступа к ним — например, задача, которая загружает файл с данными, который затем обрабатывается следующей задачей. В случае использования <code>Local Executor</code> хранение файлов на диске также может усложнить повторные запуски, например если вашей задаче требуется конфигурационный файл, который удаляется другой задачей в Dag.</p>
<p>По возможности используйте <code>XCom</code> для передачи небольших сообщений между задачами, а для передачи больших объёмов данных используйте удалённое хранилище, такое как <strong>S3</strong> или <strong>HDFS</strong>. Например, если у вас есть задача, которая сохраняет обработанные данные в <strong>S3</strong>, эта задача может положить путь к выходным данным в <strong>S3</strong> в <strong>XCom</strong>, а <strong>downstream</strong>-задачи смогут получить этот путь из <strong>XCom</strong> и использовать его для чтения данных.</p>
<p>Задачи также не должны хранить внутри себя какие-либо параметры аутентификации, такие как пароли или токены. По возможности используйте <a href="https://airflow.apache.org/docs/apache-airflow/stable/authoring-and-scheduling/connections.html" target="_blank" rel="noopener"><strong>Connections</strong></a> для безопасного хранения данных в <strong>backend</strong>’е <strong>Airflow</strong> и получайте их с помощью уникального <strong>connection id</strong>.</p>
<h3><strong>Код верхнего уровня Python</strong></h3>
<p>Следует избегать написания кода верхнего уровня, который не требуется для создания <strong>Operator</strong>’ов и построения связей <strong>Dag</strong> между ними. Это связано с архитектурным решением планировщика <strong>Airflow</strong> и влиянием скорости парсинга кода верхнего уровня на производительность и масштабируемость Airflow.</p>
<p><strong>Планировщик Airflow</strong> выполняет код вне методов execute операторов с минимальным интервалом <a href="https://airflow.apache.org/docs/apache-airflow/stable/configurations-ref.html#config-dag-processor-min-file-process-interval" target="_blank" rel="noopener"><code>min_file_process_interval</code></a> секунд. Это делается для того, чтобы обеспечить динамическое планирование Dag’ов — когда расписание и зависимости могут со временем изменяться и влиять на следующий запуск Dag. <strong>Планировщик Airflow</strong> постоянно старается убедиться, что то, что описано в Dag’ах, корректно отражено в запланированных задачах.</p>
<p>В частности, не следует выполнять доступ к базам данных, тяжёлые вычисления и сетевые операции.</p>
<p>Одним из важных факторов, влияющих на время загрузки Dag, который часто упускают из виду Python-разработчики, является то, что импорты на верхнем уровне могут занимать неожиданно много времени и создавать значительные накладные расходы. Этого легко избежать, переместив такие импорты в локальные импорты внутри <strong>Python-callable</strong>, например.</p>
<p>Рассмотрим два примера ниже. В первом примере <strong>Dag</strong> будет парситься на дополнительные 1000 секунд дольше, чем функционально эквивалентный <strong>Dag</strong> во втором примере, где <code>expensive_api_call</code> выполняется в контексте своей задачи.</p>
<p>Неизбежание кода верхнего уровня Dag:</p><pre class="urvanov-syntax-highlighter-plain-tag">import pendulum

from airflow.sdk import DAG
from airflow.sdk import task


def expensive_api_call():
    print("Hello from Airflow!")
    sleep(1000)


my_expensive_response = expensive_api_call()

with DAG(
    dag_id="example_python_operator",
    schedule=None,
    start_date=pendulum.datetime(2021, 1, 1, tz="UTC"),
    catchup=False,
    tags=["example"],
) as dag:

    @task()
    def print_expensive_api_call():
        print(my_expensive_response)</pre><p>Избегание кода верхнего уровня <strong>Dag</strong>:</p><pre class="urvanov-syntax-highlighter-plain-tag">import pendulum

from airflow.sdk import DAG
from airflow.sdk import task


def expensive_api_call():
    sleep(1000)
    return "Hello from Airflow!"


with DAG(
    dag_id="example_python_operator",
    schedule=None,
    start_date=pendulum.datetime(2021, 1, 1, tz="UTC"),
    catchup=False,
    tags=["example"],
) as dag:

    @task()
    def print_expensive_api_call():
        my_expensive_response = expensive_api_call()
        print(my_expensive_response)</pre><p>В первом примере <code>expensive_api_call</code> выполняется каждый раз при парсинге файла <strong>Dag</strong>, что приводит к неоптимальной производительности при обработке <strong>Dag</strong>-файла. Во втором примере <code>expensive_api_call</code> вызывается только во время выполнения задачи и, таким образом, Dag может быть распарсен без потери производительности. Чтобы проверить это самостоятельно, реализуйте первый Dag и посмотрите, как строка <strong>«Hello from Airflow!»</strong> выводится в логах планировщика.</p>
<p>Обратите внимание, что операторы <code>import</code> также считаются кодом верхнего уровня. Поэтому, если у вас есть <code>import</code>, который выполняется долго, или импортируемый модуль сам выполняет код на верхнем уровне, это также может негативно сказаться на производительности планировщика. Следующий пример показывает, как работать с дорогостоящими импортами.</p><pre class="urvanov-syntax-highlighter-plain-tag"># It's ok to import modules that are not expensive to load at top-level of a Dag file
import random
import pendulum

# Expensive imports should be avoided as top level imports, because Dag files are parsed frequently, resulting in top-level code being executed.
#
# import pandas
# import torch
# import tensorflow
#

...


@task()
def do_stuff_with_pandas_and_torch():
    import pandas
    import torch

    # do some operations using pandas and torch


@task()
def do_stuff_with_tensorflow():
    import tensorflow

    # do some operations using tensorflow</pre><p></p>
<h3><strong>Как проверить, является ли мой код «кодом верхнего уровня»</strong></h3>
<p>Чтобы понять, является ли ваш код <strong>«кодом верхнего уровня»</strong> или нет, необходимо разбираться во многих тонкостях того, как работает <strong>парсинг</strong> Python. В общем случае, когда <strong>Python</strong> парсит файл, он выполняет весь код, который видит, за исключением (как правило) внутреннего кода методов, который он не выполняет.</p>
<p>Существует ряд неочевидных специальных случаев — например, к коду верхнего уровня также относится любой код, используемый для определения значений по умолчанию у методов.</p>
<p>Однако есть простой способ проверить, является ли ваш код <strong>«кодом верхнего уровня»</strong> или нет. Достаточно распарсить ваш код и посмотреть, выполняется ли данный фрагмент кода.</p>
<p>Представьте следующий код:</p><pre class="urvanov-syntax-highlighter-plain-tag">from airflow.sdk import DAG
from airflow.providers.standard.operators.python import PythonOperator
import pendulum


def get_task_id():
    return "print_array_task"  # &lt;- is that code going to be executed?


def get_array():
    return [1, 2, 3]  # &lt;- is that code going to be executed?


with DAG(
    dag_id="example_python_operator",
    schedule=None,
    start_date=pendulum.datetime(2021, 1, 1, tz="UTC"),
    catchup=False,
    tags=["example"],
) as dag:
    operator = PythonOperator(
        task_id=get_task_id(),
        python_callable=get_array,
        dag=dag,
    )</pre><p>Чтобы это проверить, вы можете добавить несколько операторов <code>print</code> в код, который хотите проверить, а затем выполнить команду <code>python &lt;my_dag_file&gt;.py</code>.</p><pre class="urvanov-syntax-highlighter-plain-tag">from airflow.sdk import DAG
from airflow.providers.standard.operators.python import PythonOperator
import pendulum


def get_task_id():
    print("Executing 1")
    return "print_array_task"  # &lt;- is that code going to be executed? YES


def get_array():
    print("Executing 2")
    return [1, 2, 3]  # &lt;- is that code going to be executed? NO


with DAG(
    dag_id="example_python_operator",
    schedule=None,
    start_date=pendulum.datetime(2021, 1, 1, tz="UTC"),
    catchup=False,
    tags=["example"],
) as dag:
    operator = PythonOperator(
        task_id=get_task_id(),
        python_callable=get_array,
        dag=dag,
    )</pre><p>При выполнении этого кода вы увидите:</p><pre class="urvanov-syntax-highlighter-plain-tag">[Breeze:3.10.19] root@cf85ab34571e:/opt/airflow# python /files/test_python.py
Executing 1</pre><p>Это означает, что <code>get_array</code> не выполняется как код верхнего уровня, а <code>get_task_id</code> — выполняется.</p>
<h3><strong>Качество кода и линтинг</strong></h3>
<p>Поддержание высокого качества кода имеет ключевое значение для надёжности и сопровождаемости ваших <strong>workflow</strong> в <strong>Airflow</strong>. Использование инструментов <strong>линтинга</strong> помогает выявлять потенциальные проблемы и обеспечивать соблюдение стандартов кодирования. Одним из таких инструментов является <strong>ruff</strong> — быстрый линтер для Python, который теперь включает специальные правила для Airflow.</p>
<p><strong>ruff</strong> помогает выявлять устаревшие возможности и паттерны, которые могут повлиять на миграцию на <strong>Airflow 3.0</strong>. Например, он включает правила с префиксом AIR, предназначенные для обнаружения потенциальных проблем.</p>
<p><em>Полный список этих правил описан в <a href="https://docs.astral.sh/ruff/rules/#airflow-air" target="_blank" rel="noopener">разделе Airflow (AIR)</a>.</em></p>
<h3><strong>Установка и использование ruff</strong></h3>
<blockquote><p><strong>ruff</strong> — это очень быстрый линтер и автоформаттер для Python, написанный на Rust (в десятки раз быстрее <code>flake8</code>, <code>isort</code>, <code>pylint</code>).</p></blockquote>
<hr />
<p>Установка: установите <code>ruff</code> с помощью <code>pip</code>:</p><pre class="urvanov-syntax-highlighter-plain-tag">pip install "ruff&gt;=0.14.10"</pre><p><strong>Запуск ruff:</strong> выполните <code>ruff</code> для проверки ваших Dag’ов на наличие потенциальных проблем:</p><pre class="urvanov-syntax-highlighter-plain-tag">ruff check dags/ --select AIR3</pre><p>Эта команда проанализирует ваши <strong>Dag</strong>’и, расположенные в директории <code>dags/</code>, и сообщит о проблемах, связанных с указанными правилами.</p>
<p><strong>Пример</strong></p>
<p>Рассмотрим <strong>legacy</strong> <strong>Dag</strong>, определённый следующим образом:</p><pre class="urvanov-syntax-highlighter-plain-tag">from airflow import dag
from airflow.datasets import Dataset
from airflow.sensors.filesystem import FileSensor


@dag()
def legacy_dag():
    FileSensor(task_id="wait_for_file", filepath="/tmp/test_file")</pre><p>Запуск <code>ruff</code> приведёт к следующему выводу:</p><pre class="urvanov-syntax-highlighter-plain-tag">dags/legacy_dag.py:7:2: AIR301 Dag should have an explicit schedule argument
dags/legacy_dag.py:12:6: AIR302 schedule_interval is removed in Airflow 3.0
dags/legacy_dag.py:17:15: AIR302 airflow.datasets.Dataset is removed in Airflow 3.0
dags/legacy_dag.py:19:5: AIR303 airflow.sensors.filesystem.FileSensor is moved into ``standard`` provider in Airflow 3.0</pre><p>Интегрируя <code>ruff</code> в ваш процесс разработки, вы можете заблаговременно устранять устаревшие элементы и поддерживать высокое качество кода, что облегчает переход между версиями <strong>Airflow</strong>.</p>
<h3><strong>Динамическая генерация Dag</strong></h3>
<p>Иногда написание <strong>Dag</strong>’ов вручную нецелесообразно. Возможно, у вас есть большое количество <strong>Dag</strong>’ов, которые делают одно и то же, отличаясь лишь параметрами. Или вам нужен набор Dag’ов для загрузки таблиц, но вы не хотите вручную обновлять Dag’и каждый раз при изменении этих таблиц. В этих и других случаях может быть полезно динамически генерировать <strong>Dag</strong>’и.</p>
<p>Избегание избыточной обработки в коде верхнего уровня, описанное в предыдущей главе, особенно важно в случае динамической конфигурации Dag’ов, которая, по сути, может быть реализована одним из следующих способов:</p>
<ul>
<li>через переменные окружения (не путать с <strong>Airflow Variables</strong>)</li>
<li>через внешне предоставляемый, сгенерированный <strong>Python</strong>-код, содержащий метаданные в папке <strong>Dag</strong>’ов</li>
<li>через внешний, сгенерированный файл конфигурационных метаданных в папке <strong>Dag</strong>’ов</li>
</ul>
<p>Некоторые случаи динамической генерации Dag’ов описаны в разделе <a href="https://airflow.apache.org/docs/apache-airflow/stable/howto/dynamic-dag-generation.html" target="_blank" rel="noopener"><strong>Dynamic Dag Generation</strong></a>.</p>
<h3><strong>Переменные Airflow</strong></h3>
<p>Использование переменных <strong>Airflow</strong> приводит к сетевым вызовам и обращениям к базе данных, поэтому их применение в коде Python верхнего уровня для DAG-ов следует по возможности избегать, как упоминалось в предыдущей главе Python-код верхнего уровня. Если переменные Airflow всё же необходимо использовать в коде DAG верхнего уровня, их влияние на парсинг DAG можно снизить, включив экспериментальный кэш, настроенный с разумным значением <code>ttl</code>.</p>
<p>Вы можете свободно использовать переменные Airflow внутри методов <code>execute()</code> операторов, а также передавать переменные Airflow в существующие операторы через <strong>Jinja-шаблоны</strong>, что откладывает чтение значения до момента выполнения задачи. Синтаксис шаблона для этого следующий:</p><pre class="urvanov-syntax-highlighter-plain-tag">{{ var.value.&lt;variable_name&gt; }}</pre><p>или, если требуется десериализовать JSON-объект из переменной:</p><pre class="urvanov-syntax-highlighter-plain-tag">{{ var.json.&lt;variable_name&gt; }}</pre><p>В коде верхнего уровня переменные, использующие <strong>Jinja-шаблоны</strong>, не выполняют запрос до момента запуска задачи, тогда как <code>Variable.get()</code> выполняет запрос каждый раз, когда файл DAG парсится планировщиком, если кэширование не включено. Использование <code>Variable.get()</code> без включённого кэширования приводит к неоптимальной производительности при обработке файлов DAG.</p>
<p>В некоторых случаях это может привести к тому, что файл <strong>DAG</strong> не успеет полностью распарситься и произойдёт тайм-аут.</p>
<p><strong>Плохой пример:</strong></p><pre class="urvanov-syntax-highlighter-plain-tag">from airflow.sdk import Variable

foo_var = Variable.get("foo")  # AVOID THAT
bash_use_variable_bad_1 = BashOperator(
    task_id="bash_use_variable_bad_1", bash_command="echo variable foo=${foo_env}", env={"foo_env": foo_var}
)

bash_use_variable_bad_2 = BashOperator(
    task_id="bash_use_variable_bad_2",
    bash_command=f"echo variable foo=${Variable.get('foo')}",  # AVOID THAT
)

bash_use_variable_bad_3 = BashOperator(
    task_id="bash_use_variable_bad_3",
    bash_command="echo variable foo=${foo_env}",
    env={"foo_env": Variable.get("foo")},  # AVOID THAT
)</pre><p><strong>Хороший пример:</strong></p><pre class="urvanov-syntax-highlighter-plain-tag">bash_use_variable_good = BashOperator(
    task_id="bash_use_variable_good",
    bash_command="echo variable foo=${foo_env}",
    env={"foo_env": "{{ var.value.get('foo') }}"},
)

@task
def my_task():
    var = Variable.get("foo")  # This is ok since my_task is called only during task run, not during Dag scan.
    print(var)</pre><p>В целях безопасности рекомендуется использовать <strong>Secrets Backend</strong> для любых переменных, содержащих чувствительные данные.</p>
<h3><strong>Расписания (Timetables)</strong></h3>
<p>Избегайте использования <strong>переменных/подключений Airflow</strong> или обращения к базе данных <strong>Airflow</strong> на верхнем уровне кода расписаний. Доступ к базе данных должен быть отложен до момента выполнения DAG. Это означает, что не следует получать переменные/подключения в качестве аргументов при инициализации класса расписания, а также использовать <strong>Variable/Connection</strong> на верхнем уровне вашего пользовательского модуля расписания.</p>
<p><strong>Плохой пример:</strong></p><pre class="urvanov-syntax-highlighter-plain-tag">from airflow.sdk import Variable
from airflow.timetables.interval import CronDataIntervalTimetable


class CustomTimetable(CronDataIntervalTimetable):
    def __init__(self, *args, something=Variable.get("something"), **kwargs):
        self._something = something
        super().__init__(*args, **kwargs)</pre><p><strong>Хороший пример:</strong></p><pre class="urvanov-syntax-highlighter-plain-tag">from airflow.sdk import Variable
from airflow.timetables.interval import CronDataIntervalTimetable


class CustomTimetable(CronDataIntervalTimetable):
    def __init__(self, *args, something="something", **kwargs):
        self._something = Variable.get(something)
        super().__init__(*args, **kwargs)</pre><p></p>
<h3><strong>Запуск DAG-ов после изменений</strong></h3>
<p>Избегайте запуска <strong>DAG</strong>-ов сразу после их изменения или изменения любых сопутствующих файлов в папке DAG-ов.</p>
<p>Необходимо дать системе достаточно времени для обработки изменённых файлов. Этот процесс включает несколько этапов. Сначала файлы должны быть доставлены планировщику — обычно через распределённую файловую систему или <strong>Git-Sync</strong>, затем планировщик должен распарсить Python-файлы и сохранить их в базе данных. В зависимости от вашей конфигурации, скорости распределённой файловой системы, количества файлов, количества <strong>DAG</strong>-ов, числа изменений в файлах, размеров файлов, количества планировщиков, скорости <strong>CPU</strong>, этот процесс может занимать от нескольких секунд до нескольких минут, а в крайних случаях — многие минуты. Вам следует дождаться появления <strong>DAG</strong>-а в <strong>UI</strong>, прежде чем пытаться его запустить.</p>
<p>Если вы наблюдаете большие задержки между обновлением <strong>DAG</strong>-а и моментом, когда он становится доступен для запуска, вы можете обратить внимание на следующие параметры конфигурации и настроить их в соответствии с вашими потребностями (подробности по каждому параметру см. по ссылкам):</p>
<ul>
<li><code>scheduler_idle_sleep_time</code> &#8212; Управляет временем ожидания планировщика между циклами, но если в цикле ничего не нужно делать, то есть если что-то запланировано, то следующая итерация цикла начнется немедленно.</li>
<li><code>min_file_process_interval</code> &#8212; Количество секунд, по истечении которых происходит разбор DAG-файла. Разбор DAG-файла происходит каждые несколько секунд. Обновления DAG-файлов отражаются после этого интервала. Низкое значение этого параметра приведет к увеличению загрузки ЦП.</li>
<li><code>refresh_interval</code> &#8212; Как часто (в секундах) следует обновлять или искать новые файлы в пакете DAG.</li>
<li><code>parsing_processes</code> &#8212; Процессор DAG может запускать несколько процессов параллельно для анализа DAG. Это определяет, сколько процессов будет запущено.</li>
<li><code>file_parsing_sort_mode</code> &#8212; Один из вариантов <strong>modified_time</strong>, <strong>random_seeded_by_host</strong> и <strong>alphabetical</strong>. Процессор DAG перечислит и отсортирует файлы DAG, чтобы определить порядок их анализа.
<ul>
<li><code>modified_time</code> &#8212; Сортировка файлов по времени изменения. Это полезно в больших масштабах для предварительной обработки недавно измененных DAG-графов.</li>
<li><code>random_seeded_by_host</code> &#8212; Произвольная сортировка файлов несколькими процессорами DAG, но в одном и том же порядке на одном и том же хосте, что позволяет каждому процессору обрабатывать файлы в разном порядке.</li>
<li><code>alphabetical</code> &#8212; Сортировка по имени файла</li>
</ul>
</li>
</ul>
<h3><strong>Пример паттерна watcher с правилами триггеров</strong></h3>
<p>Паттерн <strong>watcher</strong> — это способ организации DAG-а с задачей, которая «наблюдает» за состояниями других задач. Его основное назначение — пометить запуск DAG-а как <strong>failed</strong>, если любая другая задача завершилась с ошибкой. Необходимость в этом возникла в системных тестах <strong>Airflow</strong>, которые представляют собой <strong>DAG</strong>-и с разными задачами (аналогично тесту, состоящему из шагов).</p>
<p>Обычно, когда любая задача завершается с ошибкой, все остальные задачи не выполняются, и весь запуск <strong>DAG</strong>-а также получает статус <strong>failed</strong>. Однако при использовании правил триггеров можно нарушить стандартный поток выполнения задач, и весь DAG может получить статус, отличный от ожидаемого. Например, можно иметь задачу очистки ресурсов (<strong>teardown task</strong>) с правилом триггера <code>TriggerRule.ALL_DONE</code>, которая будет выполняться независимо от состояния других задач (например, для освобождения ресурсов). В такой ситуации <strong>DAG</strong> всегда выполнит эту задачу, и запуск <strong>DAG</strong>-а получит статус именно этой задачи, в результате чего можно потерять информацию о задачах, завершившихся с ошибкой. Если требуется гарантировать, что <strong>DAG</strong> с задачей очистки завершится с ошибкой при падении любой задачи, необходимо использовать паттерн <strong>watcher</strong>.</p>
<p>Задача <strong>watcher</strong> — это задача, которая всегда завершается с ошибкой при выполнении, но она должна запускаться только в том случае, если любая другая задача завершилась с ошибкой. Для неё необходимо установить правило триггера <code>TriggerRule.ONE_FAILED</code>, а также сделать её <strong>downstream-задачей</strong> для всех остальных задач в <strong>DAG</strong>-е. Благодаря этому, если все остальные задачи завершатся <strong>успешно</strong>, watcher будет пропущена, а если произойдёт ошибка, задача <strong>watcher</strong> выполнится и завершится с ошибкой, что приведёт к статусу <strong>failed</strong> у всего запуска <strong>DAG</strong>-а.</p>
<p><strong>Примечание</strong></p>
<p>Следует учитывать, что правила триггеров опираются только на непосредственные upstream-задачи (родительские). Например, <code>TriggerRule.ONE_FAILED</code> будет игнорировать любые задачи со статусом <strong>failed</strong> (или <strong>upstream_failed</strong>), которые не являются прямыми родителями параметризуемой задачи.</p>
<p>Проще понять концепцию на примере. Предположим, у нас есть следующий DAG:</p><pre class="urvanov-syntax-highlighter-plain-tag">from datetime import datetime

from airflow.sdk import DAG
from airflow.sdk import task
from airflow.exceptions import AirflowException
from airflow.providers.standard.operators.bash import BashOperator
from airflow.utils.trigger_rule import TriggerRule


@task(trigger_rule=TriggerRule.ONE_FAILED, retries=0)
def watcher():
    raise AirflowException("Failing task because one or more upstream tasks failed.")


with DAG(
    dag_id="watcher_example",
    schedule="@once",
    start_date=datetime(2021, 1, 1),
    catchup=False,
) as dag:
    failing_task = BashOperator(task_id="failing_task", bash_command="exit 1", retries=0)
    passing_task = BashOperator(task_id="passing_task", bash_command="echo passing_task")
    teardown = BashOperator(
        task_id="teardown",
        bash_command="echo teardown",
        trigger_rule=TriggerRule.ALL_DONE,
    )

    failing_task &gt;&gt; passing_task &gt;&gt; teardown
    list(dag.tasks) &gt;&gt; watcher()</pre><p>Визуальное представление этого <strong>DAG</strong>-а после выполнения выглядит следующим образом:</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2026/01/watcher_airflow_dag.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2767" src="https://datatalks.ru/wp-content/uploads/2026/01/watcher_airflow_dag.png" alt="" width="893" height="119" srcset="https://datatalks.ru/wp-content/uploads/2026/01/watcher_airflow_dag.png 893w, https://datatalks.ru/wp-content/uploads/2026/01/watcher_airflow_dag-300x40.png 300w, https://datatalks.ru/wp-content/uploads/2026/01/watcher_airflow_dag-768x102.png 768w, https://datatalks.ru/wp-content/uploads/2026/01/watcher_airflow_dag-450x60.png 450w, https://datatalks.ru/wp-content/uploads/2026/01/watcher_airflow_dag-780x104.png 780w" sizes="(max-width: 893px) 100vw, 893px" /></a></p>
<p>В нём есть несколько задач, выполняющих разные роли:</p>
<ul>
<li><code>failing_task</code> — всегда завершается с ошибкой;</li>
<li><code>passing_task</code> — всегда завершается успешно (если выполняется);</li>
<li><code>teardown</code> — всегда запускается (независимо от состояний других задач) и должна всегда завершаться успешно;</li>
<li><code>watcher</code> — является <strong>downstream-задачей</strong> для всех остальных задач, то есть запускается, когда любая задача завершается с ошибкой, и тем самым переводит весь запуск <strong>DAG</strong>-а в состояние <strong>failed</strong>, так как является листовой задачей.</li>
</ul>
<p>Важно отметить, что без задачи <strong>watcher</strong> весь запуск <strong>DAG</strong>-а получит состояние <strong>success</strong>, поскольку единственная задача, завершающаяся с ошибкой, не является листовой, а задача <strong>teardown</strong> завершится успешно. Если мы хотим, чтобы <strong>watcher</strong> отслеживала состояние всех задач, необходимо сделать её зависимой от каждой из них по отдельности. Благодаря этому мы можем перевести запуск <strong>DAG</strong>-а в состояние <strong>failed</strong>, если любая из задач завершится с ошибкой. Обратите внимание, что для задачи watcher установлено правило триггера <strong>&#171;one_failed&#187;</strong>.</p>
<p>С другой стороны, без задачи <strong>teardown</strong> задача <strong>watcher</strong> не понадобилась бы, поскольку <strong>failing_task</strong> передала бы свой статус <strong>failed</strong> <strong>downstream-задаче</strong> <strong>passing_task</strong>, и весь запуск <strong>DAG</strong>-а также получил бы статус <strong>failed</strong>.</p>
<h3><strong>Использование исключения AirflowClusterPolicySkipDag в кластерных политиках для пропуска определённых DAG-ов</strong></h3>
<p><em>Добавлено в версии 2.7.</em></p>
<p><strong>DAG</strong>-и <strong>Airflow</strong> обычно разворачиваются и обновляются из конкретной ветки Git-репозитория с помощью <code>git-sync</code>. Однако, когда по операционным причинам требуется запускать несколько кластеров <strong>Airflow</strong>, поддержка нескольких <strong>Git-веток</strong> становится крайне неудобной. Особенно это усложняется, когда необходимо периодически синхронизировать две отдельные ветки (например, <strong>prod</strong> и <strong>beta</strong>) с использованием корректной стратегии ветвления.</p>
<ul>
<li><code>cherry-pick</code> слишком трудоёмок для сопровождения <strong>Git</strong>-репозитория;</li>
<li><code>hard-reset</code> не является рекомендуемым подходом в <strong>GitOps</strong>.</li>
</ul>
<p>Вместо этого можно рассмотреть вариант подключения нескольких кластеров Airflow к одной и той же ветке <strong>Git</strong> (например, main) и управления ими с помощью разных переменных окружения и различных конфигураций подключений с одинаковым connection_id. При необходимости также можно выбрасывать исключение <code>AirflowClusterPolicySkipDag</code> в кластерной политике, чтобы загружать определённые <strong>DAG</strong>-и в <strong>DagBag</strong> только в конкретном развертывании Airflow.</p><pre class="urvanov-syntax-highlighter-plain-tag">def dag_policy(dag: DAG):
    """Пропуск DAG-а с тегом `only_for_beta`."""

    if "only_for_beta" in dag.tags:
        raise AirflowClusterPolicySkipDag(
            f"Dag {dag.dag_id} is not loaded on the production cluster, due to `only_for_beta` tag."
        )</pre><p>Приведённый выше пример показывает фрагмент кода <strong>dag_policy</strong>, который пропускает DAG в зависимости от тегов, указанных у него.</p>
<h2><strong>Снижение сложности DAG-ов</strong></h2>
<p>Хотя Airflow хорошо справляется с обработкой большого количества DAG-ов с множеством задач и зависимостей между ними, при наличии большого числа сложных DAG-ов их сложность может негативно сказаться на производительности планирования. Одним из способов поддерживать высокую производительность и эффективное использование экземпляра Airflow является стремление к упрощению и оптимизации DAG-ов везде, где это возможно. Следует помнить, что процесс парсинга и создания DAG-а — это всего лишь выполнение Python-кода, и именно от вас зависит, насколько производительным он будет. Не существует «волшебных рецептов» для того, чтобы сделать DAG «менее сложным» — поскольку это Python-код, именно автор DAG-а контролирует сложность своего кода.</p>
<p>Не существует метрик «сложности DAG-а», и в частности нет метрик, которые могли бы однозначно сказать, является ли DAG «достаточно простым». Однако, как и в случае с любым Python-кодом, можно определить, что код DAG-а стал «проще» или «быстрее», если он оптимизирован. Если вы хотите оптимизировать свои DAG-и, можно предпринять следующие действия:</p>
<ul>
<li><strong>Сделайте загрузку DAG-а быстрее:</strong><br />
Это единственная рекомендация по улучшению, которая может быть реализована разными способами, но именно она оказывает наибольшее влияние на производительность планировщика. Если у вас есть возможность ускорить загрузку <strong>DAG</strong>-а — делайте это, если ваша цель — повышение производительности. Обратитесь к разделу <strong>Python</strong>-код верхнего уровня для получения советов, а также к <strong>Dag Loader Test</strong>, чтобы оценить время загрузки <strong>DAG</strong>-а.</li>
<li><strong>Генерируйте более простую структуру DAG-а:</strong><br />
Каждая зависимость между задачами добавляет дополнительную нагрузку на планирование и выполнение. <strong>DAG</strong> с простой линейной структурой <code>A → B → C</code> будет испытывать меньшие задержки при планировании задач, чем <strong>DAG</strong> с глубоко вложенной древовидной структурой, например с экспоненциально растущим числом зависимых задач. Если вы можете сделать свои DAG-и более линейными — так, чтобы в каждый момент времени было как можно меньше потенциальных задач-кандидатов на запуск, — это, как правило, улучшит общую производительность планирования.</li>
<li><strong>Уменьшите количество DAG-ов в одном файле:</strong><br />
Хотя <strong>Airflow 2</strong> оптимизирован для сценария, при котором в одном файле описано несколько DAG-ов, в системе есть компоненты, из-за которых такой подход иногда менее производителен или приводит к большим задержкам по сравнению с разбиением DAG-ов по нескольким файлам. Уже сам факт того, что один файл может быть обработан только одним <code>FileProcessor</code>, делает этот подход менее масштабируемым. Если у вас много DAG-ов, генерируемых из одного файла, рассмотрите возможность их разделения, особенно если вы замечаете, что изменения в файлах DAG-ов долго отражаются в <strong>UI Airflow</strong>.</li>
<li><strong>Пишите эффективный Python-код:</strong><br />
Необходимо соблюдать баланс между меньшим количеством DAG-ов в файле (как указано выше) и общим объёмом кода. Файлы <strong>Python</strong>, описывающие DAG-и, должны следовать лучшим практикам программирования и не должны рассматриваться как конфигурационные файлы. Если ваши DAG-и используют схожий код, не следует копировать его снова и снова в большое количество почти идентичных исходных файлов, так как это приведёт к ненужным повторным импортам одних и тех же ресурсов. Вместо этого следует стремиться к минимизации повторяющегося кода во всех DAG-ах, чтобы приложение работало эффективно и было проще в отладке.<br />
<em>См. раздел Dynamic Dag Generation о том, как создавать несколько DAG-ов с похожей логикой.</em></li>
</ul>
<h2>Тестирование DAG-а</h2>
<p>Пользователям Airflow следует относиться к DAG-ам как к коду промышленного уровня, и у DAG-ов должны быть различные связанные тесты, чтобы гарантировать получение ожидаемых результатов. Для DAG-а можно написать широкий спектр тестов. Рассмотрим некоторые из них.</p>
<h3><strong>Тест загрузки DAG-а (Dag Loader Test)</strong></h3>
<p>Этот тест должен гарантировать, что ваш DAG не содержит кода, который вызывает ошибку во время загрузки. Для запуска этого теста пользователю не требуется писать дополнительный код.</p><pre class="urvanov-syntax-highlighter-plain-tag">python your-dag-file.py</pre><p>Выполнение приведённой выше команды без ошибок гарантирует, что в DAG-е нет неустановленных зависимостей, синтаксических ошибок и т. д. Убедитесь, что вы загружаете DAG в окружении, соответствующем окружению планировщика — с теми же зависимостями, переменными окружения и общим кодом, на который ссылается DAG.</p>
<p>Это также отличный способ проверить, загружается ли DAG быстрее после оптимизации, если вы хотите попробовать оптимизировать время загрузки DAG-а. Просто запустите DAG и измерьте время его выполнения, но, опять же, необходимо убедиться, что DAG выполняется с теми же зависимостями, переменными окружения и общим кодом.</p>
<p>Существует множество способов измерить время выполнения, один из них в <strong>Linux</strong> — использование встроенной команды <code>time</code>. Обязательно запускайте её несколько раз подряд, чтобы учесть эффекты кэширования. Сравнивайте результаты до и после оптимизации (в одинаковых условиях — на той же машине, в том же окружении и т. д.), чтобы оценить влияние оптимизации.</p><pre class="urvanov-syntax-highlighter-plain-tag">time python airflow/example_dags/example_python_operator.py</pre><p><strong>Результат:</strong></p><pre class="urvanov-syntax-highlighter-plain-tag">real    0m0.699s
user    0m0.590s
sys     0m0.108s</pre><p>Важной метрикой является <strong>«real time»</strong>, которая показывает, сколько времени заняла обработка DAG-а. Обратите внимание, что при таком способе загрузки файла каждый раз запускается новый интерпретатор, поэтому присутствует начальное время инициализации, которого нет при парсинге DAG-а самим <strong>Airflow</strong>. Оценить время инициализации можно, выполнив:</p><pre class="urvanov-syntax-highlighter-plain-tag">time python -c ''</pre><p><strong>Результат:</strong></p><pre class="urvanov-syntax-highlighter-plain-tag">real    0m0.073s
user    0m0.037s
sys     0m0.039s</pre><p>В данном случае начальное время запуска интерпретатора составляет примерно ~0,07 с, что составляет около 10% времени, необходимого для парсинга <code>example_python_operator.py</code> выше, поэтому фактическое время парсинга для примера <strong>DAG</strong>-а составляет примерно ~0,62 с.</p>
<p><em>Подробности о том, как тестировать отдельные операторы, см. в разделе Testing a Dag.</em></p>
<h3>Юнит-тесты</h3>
<p><strong>Юнит-тесты</strong> гарантируют отсутствие некорректного кода в вашем DAG-е. Вы можете писать юнит-тесты как для отдельных задач, так и для самого DAG-а.</p>
<p>Юнит-тест загрузки DAG-а:</p><pre class="urvanov-syntax-highlighter-plain-tag">import pytest

from airflow.models import DagBag

@pytest.fixture()
def dagbag():
    return DagBag()

def test_dag_loaded(dagbag):
    dag = dagbag.get_dag(dag_id="hello_world")
    assert dagbag.import_errors == {}
    assert dag is not None
    assert len(dag.tasks) == 1</pre><p><strong>Юнит-тест структуры DAG-а:</strong></p>
<p>Это пример теста, предназначенного для проверки структуры DAG-а, сгенерированного кодом, путём сравнения с объектом типа <code>dict</code>.</p><pre class="urvanov-syntax-highlighter-plain-tag">def assert_dag_dict_equal(source, dag):
    assert dag.task_dict.keys() == source.keys()
    for task_id, downstream_list in source.items():
        assert dag.has_task(task_id)
        task = dag.get_task(task_id)
        assert task.downstream_task_ids == set(downstream_list)

def test_dag():
    assert_dag_dict_equal(
        {
            "DummyInstruction_0": ["DummyInstruction_1"],
            "DummyInstruction_1": ["DummyInstruction_2"],
            "DummyInstruction_2": ["DummyInstruction_3"],
            "DummyInstruction_3": [],
        },
        dag,
    )</pre><p><strong>Юнит-тест</strong> для пользовательского оператора:</p><pre class="urvanov-syntax-highlighter-plain-tag">import pendulum

from airflow.sdk import DAG, TaskInstanceState

def test_my_custom_operator_execute_no_trigger(dag):
    TEST_TASK_ID = "my_custom_operator_task"
    with DAG(
        dag_id="my_custom_operator_dag",
        schedule="@daily",
        start_date=pendulum.datetime(2021, 9, 13, tz="UTC"),
    ) as dag:
        MyCustomOperator(
            task_id=TEST_TASK_ID,
            prefix="s3://bucket/some/prefix",
        )

    dagrun = dag.test()
    ti = dagrun.get_task_instance(task_id=TEST_TASK_ID)
    assert ti.state == TaskInstanceState.SUCCESS
    # Assert something related to tasks results: ti.xcom_pull()</pre><p></p>
<h3><strong>Самопроверки (Self-Checks)</strong></h3>
<p>Вы также можете реализовать проверки непосредственно в DAG-е, чтобы убедиться, что задачи производят ожидаемые результаты. Например, если у вас есть задача, которая выгружает данные в <strong>S3</strong>, вы можете реализовать проверку в следующей задаче. Такая проверка, к примеру, может удостовериться, что партиция создана в <strong>S3</strong>, и выполнить простые проверки, чтобы определить корректность данных.</p>
<p>Аналогично, если у вас есть задача, которая запускает микросервис в <strong>Kubernetes</strong> или <strong>Mesos</strong>, следует проверить, был ли сервис успешно запущен, используя <code>airflow.providers.http.sensors.http.HttpSensor</code>.</p><pre class="urvanov-syntax-highlighter-plain-tag">task = PushToS3(...)
check = S3KeySensor(
    task_id="check_parquet_exists",
    bucket_key="s3://bucket/key/foo.parquet",
    poke_interval=0,
    timeout=0,
)
task &gt;&gt; check</pre><p></p>
<h3><strong>Staging-окружение</strong></h3>
<p>По возможности поддерживайте <strong>staging-окружение</strong> для тестирования полного выполнения DAG-а перед деплоем в <strong>production</strong>. Убедитесь, что ваш DAG параметризован и позволяет изменять переменные, например путь вывода при работе с <strong>S3</strong> или базу данных, используемую для чтения конфигурации. Не хардкодьте значения внутри DAG-а и не изменяйте их вручную в зависимости от окружения.</p>
<p>Для параметризации <strong>DAG</strong>-а вы можете использовать переменные окружения.</p><pre class="urvanov-syntax-highlighter-plain-tag">import os

dest = os.environ.get("MY_DAG_DEST_PATH", "s3://default-target/path/")</pre><p></p>
<h2>Мокирование переменных и подключений</h2>
<p>При написании тестов для кода, использующего <strong>переменные</strong> или <strong>подключения</strong>, необходимо убедиться, что они существуют во время выполнения тестов. Очевидное решение — сохранить эти объекты в базе данных, чтобы их можно было прочитать во время выполнения кода. Однако чтение и запись объектов в базу данных сопровождаются дополнительными временными затратами. Чтобы ускорить выполнение тестов, имеет смысл имитировать наличие этих объектов без сохранения их в базе данных. Для этого можно создать переменные окружения, замокировав <code>os.environ</code> с помощью <code>unittest.mock.patch.dict()</code>.</p>
<p>Для переменных используйте <code>AIRFLOW_VAR_{KEY}</code>.</p><pre class="urvanov-syntax-highlighter-plain-tag">with mock.patch.dict("os.environ", AIRFLOW_VAR_KEY="env-value"):
    assert "env-value" == Variable.get("key")</pre><p>Для подключений используйте <code>AIRFLOW_CONN_{CONN_ID}</code>.</p><pre class="urvanov-syntax-highlighter-plain-tag">conn = Connection(
    conn_type="gcpssh",
    login="cat",
    host="conn-host",
)
conn_uri = conn.get_uri()
with mock.patch.dict("os.environ", AIRFLOW_CONN_MY_CONN=conn_uri):
    assert "cat" == Connection.get_connection_from_secrets("my_conn").login</pre><p></p>
<h2>Обслуживание metadata DB</h2>
<p>Со временем база метаданных будет увеличивать занимаемое дисковое пространство по мере накопления запусков <strong>DAG</strong>-ов и задач, а также логов событий.</p>
<p>Для очистки старых данных можно использовать <strong>Airflow CLI</strong> с командой <code>airflow db clean</code>.</p>
<p>Подробности см. в разделе использования <strong>db clean</strong>.</p>
<h2>Обновления и откаты версий</h2>
<h3><strong>Резервное копирование базы данных</strong></h3>
<p>Всегда разумно делать резервную копию базы метаданных перед выполнением любых операций, изменяющих базу данных.</p>
<h3><strong>Отключение планировщика</strong></h3>
<p>Во время проведения такого обслуживания можно рассмотреть отключение <strong>кластера Airflow</strong>.</p>
<p>Один из способов — установить параметр <code>[scheduler] &gt; use_job_schedule</code> в значение <code>False</code> и дождаться завершения всех выполняющихся <strong>DAG</strong>-ов; после этого новые запуски DAG-ов не будут создаваться, если только они не будут запущены извне.</p>
<p>Лучший способ (хотя и более ручной) — использовать команду <code>dags pause</code>. Вам потребуется заранее зафиксировать список DAG-ов, которые не находятся в состоянии паузы, чтобы затем знать, какие из них нужно вернуть в активное состояние после завершения обслуживания. Сначала выполните <code>airflow dags list</code> и сохраните список не приостановленных <strong>DAG</strong>-ов. Затем используйте этот же список для выполнения <code>dags pause</code> для каждого DAG-а перед обслуживанием и <code>dags unpause</code> после его завершения. Преимущество такого подхода в том, что после обновления можно попробовать снять с паузы только один или два <strong>DAG</strong>-а (например, специальные тестовые <strong>DAG</strong>-и), чтобы убедиться, что всё работает корректно, прежде чем включать все <strong>DAG</strong>-и обратно.</p>
<h3><strong>Добавление DAG-ов для интеграционного тестирования</strong></h3>
<p>Полезно добавить несколько <strong>DAG</strong>-ов для <strong>«интеграционного тестирования»</strong>, которые используют все основные сервисы вашей экосистемы (например, <strong>S3</strong>, <strong>Snowflake</strong>, <strong>Vault</strong>), но с тестовыми ресурсами или «<strong>dev»-аккаунтами</strong>. Эти тестовые <strong>DAG</strong>-и можно запускать первыми после обновления, поскольку в случае их сбоя это не приведёт к негативным последствиям, и вы сможете откатиться к резервной копии. Если же они выполняются успешно, это подтвердит, что кластер способен выполнять задачи с использованием необходимых библиотек и сервисов.</p>
<p>Например, если вы используете внешний <strong>secrets backend</strong>, убедитесь, что у вас есть задача, которая извлекает подключение. Если вы используете <code>KubernetesPodOperator</code>, добавьте задачу, выполняющую <code>sleep 30; echo "hello"</code>. Если требуется запись в <strong>S3</strong> — реализуйте это в тестовой задаче. А если нужен доступ к базе данных, добавьте задачу, выполняющую <code>select 1</code> на сервере.</p>
<h3><strong>Очистка данных перед обновлением (Prune data)</strong></h3>
<p>Некоторые миграции базы данных могут занимать значительное время. Если база метаданных имеет очень большой размер, перед выполнением обновления стоит рассмотреть возможность очистки части старых данных с помощью команды <code>db clean</code>. Используйте с осторожностью.</p>
<h2>Работа с конфликтующими и сложными Python-зависимостями</h2>
<p><strong>Airflow</strong> имеет множество Python-зависимостей, и иногда зависимости <strong>Airflow</strong> конфликтуют с зависимостями, которые ожидает код ваших задач. Поскольку по умолчанию окружение <strong>Airflow</strong> представляет собой единый набор <strong>Python-зависимостей</strong> и одно <strong>Python-окружение</strong>, нередко возникают ситуации, когда разные задачи требуют различных зависимостей, которые при этом конфликтуют между собой.</p>
<p>Если вы используете предопределённые <strong>Operator</strong>’ы <strong>Airflow</strong> для взаимодействия с внешними сервисами, выбор обычно невелик, однако такие операторы, как правило, имеют зависимости, не конфликтующие с базовыми зависимостями <strong>Airflow</strong>. Airflow использует механизм <strong>constraints</strong>, что означает наличие «зафиксированного» набора зависимостей, с которым сообщество гарантирует корректную установку <strong>Airflow</strong> (включая все <strong>community-провайдеры</strong>) без возникновения конфликтов. При этом вы можете обновлять провайдеры независимо, и их <strong>constraints</strong> вас не ограничивают, поэтому вероятность конфликтов зависимостей ниже (хотя такие зависимости всё равно необходимо тестировать). Таким образом, при использовании предопределённых операторов вероятность столкнуться с конфликтующими зависимостями минимальна или отсутствует вовсе.</p>
<p>Однако при более «современном» подходе к использованию <strong>Airflow</strong> — когда вы применяете <strong>TaskFlow API</strong> и большинство операторов реализуете с помощью собственного Python-кода, либо когда вы пишете собственные <strong>Custom Operator</strong>’ы — вы можете столкнуться с ситуацией, когда зависимости, требуемые вашим кастомным кодом, конфликтуют с зависимостями Airflow, или даже когда зависимости нескольких ваших <strong>Custom Operator</strong>’ов конфликтуют между собой.</p>
<p>Существует несколько стратегий, которые можно использовать для смягчения этой проблемы. И хотя работа с конфликтами зависимостей в кастомных операторах может быть сложной, она значительно упрощается при использовании <code>airflow.providers.standard.operators.python.PythonVirtualenvOperator</code> или <code>airflow.providers.standard.operators.python.ExternalPythonOperator</code> — как при прямом использовании классического подхода с <strong>Operator</strong>’ами, так и при использовании задач, декорированных <code>@task.virtualenv</code> или <code>@task.external_python</code>, если вы применяете <strong>TaskFlow</strong>.</p>
<p>Начнём со стратегий, которые проще всего реализовать (хотя они имеют определённые ограничения и накладные расходы), и постепенно перейдём к стратегиям, требующим изменений в развертывании Airflow.</p>
<h3><strong>Использование PythonVirtualenvOperator</strong></h3>
<p>Это самая простая в использовании и одновременно наиболее ограниченная стратегия. <code>PythonVirtualenvOperator</code> позволяет динамически создавать <code>virtualenv</code>, в котором будет выполняться ваш <strong>Python-callable</strong>. В современном подходе <strong>TaskFlow</strong>, описанном в разделе Pythonic Dags with the <strong>TaskFlow API</strong>, это также можно сделать, задекорировав callable декоратором <code>@task.virtualenv</code> (рекомендуемый способ использования оператора). Каждая задача <code>airflow.providers.standard.operators.python.PythonVirtualenvOperator</code> может иметь собственный независимый <strong>Python</strong> <code>virtualenv</code> (динамически создаваемый при каждом запуске задачи) и задавать детальный набор зависимостей, которые необходимо установить для выполнения этой задачи.</p>
<p><strong>Оператор берёт на себя:</strong></p>
<ul>
<li>создание <code>virtualenv</code> на основе вашего окружения,</li>
<li>сериализацию вашего Python-callable и передачу его на выполнение Python-интерпретатору внутри <code>virtualenv</code>,</li>
<li>выполнение <code>callable</code>, получение результата и передачу его через <code>XCom</code>, если это указано.</li>
</ul>
<p><strong>Преимущества оператора:</strong></p>
<ul>
<li>Нет необходимости заранее подготавливать <code>virtualenv</code>. Он динамически создаётся перед запуском задачи и удаляется после её завершения, поэтому для использования нескольких виртуальных окружений не требуется ничего особенного (кроме наличия пакета <code>virtualenv</code> в зависимостях <strong>Airflow</strong>).</li>
<li>Вы можете запускать задачи с разными наборами зависимостей на одних и тех же воркерах — таким образом, ресурсы памяти переиспользуются (хотя см. ниже про накладные расходы на <strong>CPU</strong> при создании <code>virtualenv</code>).</li>
<li>В крупных инсталляциях авторам Dag’ов не нужно просить кого-то создавать <code>virtualenv</code> за них. Как автор <strong>Dag’а</strong>, вам достаточно иметь установленную зависимость <code>virtualenv</code>, и вы можете задавать и изменять окружения по своему усмотрению.</li>
<li>Не требуется изменений в требованиях к деплою — независимо от того, используете ли вы локальный <code>virtualenv</code>, <strong>Docker</strong> или <strong>Kubernetes</strong>, задачи будут работать без добавления чего-либо в окружение развертывания.</li>
<li>Автору Dag’ов не нужно изучать контейнеры или <strong>Kubernetes</strong>. Для такого подхода к написанию <strong>Dag’ов</strong> достаточно знания <strong>Python-зависимостей</strong>.</li>
</ul>
<p>У данного оператора есть определённые ограничения и накладные расходы:</p>
<ul>
<li>Ваш <strong>Python-callable</strong> должен быть сериализуемым. Существует множество Python-объектов, которые не сериализуются стандартной библиотекой <code>pickle</code>. Часть этих ограничений можно обойти с помощью библиотеки <code>dill</code>, однако и она не решает всех проблем сериализации.</li>
<li>Все зависимости, отсутствующие в окружении <strong>Airflow</strong>, должны импортироваться локально внутри используемого <code>callable</code>, а код верхнего уровня <strong>Dag</strong> не должен импортировать или использовать эти библиотеки.</li>
<li><code>Virtualenv</code> запускаются в рамках одной и той же операционной системы, поэтому они не могут иметь конфликтующие системные зависимости (устанавливаемые через <code>apt</code> или <code>yum</code>). Независимо могут устанавливаться только <strong>Python-зависимости</strong>.</li>
<li>Оператор добавляет накладные расходы на <strong>CPU</strong>, <strong>сеть</strong> и общее время выполнения каждой задачи — Airflow вынужден пересоздавать <code>virtualenv</code> с нуля для каждого запуска задачи.</li>
<li>Воркеры должны иметь доступ к <strong>PyPI</strong> или приватным репозиториям для установки зависимостей.</li>
<li>Динамическое создание <code>virtualenv</code> подвержено временным сбоям (например, если репозиторий недоступен или возникают сетевые проблемы при подключении к нему).</li>
<li>Легко попасть в ситуацию «слишком» динамичного окружения — устанавливаемые зависимости могут обновляться, а их транзитивные зависимости могут получать независимые обновления, в результате чего задача может перестать работать из-за выхода новой версии зависимости или вы можете стать жертвой атаки на цепочку поставок, когда новая версия зависимости оказывается вредоносной.</li>
<li>Задачи изолированы друг от друга только за счёт выполнения в разных окружениях. Это означает, что выполняющиеся задачи всё ещё могут влиять друг на друга — например, последующие задачи, выполняемые на том же воркере, могут быть затронуты предыдущими задачами, которые создавали или изменяли файлы и т. п.</li>
</ul>
<p>Подробные примеры использования <code>airflow.providers.standard.operators.python.PythonVirtualenvOperator</code> приведены в соответствующем разделе руководства по <strong>TaskFlow API</strong>.</p>
<h3><strong>Использование ExternalPythonOperator</strong></h3>
<p><em>Добавлено в версии 2.4.</em></p>
<p>Более сложным в использовании, но при этом значительно менее накладным с точки зрения ресурсов, безопасности и стабильности вариантом является использование <code>airflow.providers.standard.operators.python.ExternalPythonOperator</code>. В современном подходе TaskFlow, описанном в разделе <strong>Pythonic Dags with the TaskFlow API</strong>, этого также можно добиться, задекорировав ваш callable декоратором @task.external_python (рекомендуемый способ использования оператора). Однако для этого требуется заранее подготовленное, неизменяемое Python-окружение. В отличие от <code>airflow.providers.standard.operators.python.PythonVirtualenvOperator</code>, вы не можете добавлять новые зависимости в такое предсуществующее окружение. Все необходимые зависимости должны быть добавлены заранее и быть доступны на всех воркерах, если Airflow работает в распределённом окружении.</p>
<p>Таким образом, вы избегаете накладных расходов и проблем, связанных с пересозданием <code>virtualenv</code>, однако такие окружения необходимо подготовить и задеплоить вместе с установкой Airflow. Обычно в этот процесс вовлечены специалисты, отвечающие за установку Airflow, и в крупных инсталляциях это, как правило, другие люди, нежели авторы Dag’ов (DevOps/System Admins).</p>
<p>Такие <code>virtualenv</code> могут быть подготовлены разными способами: при использовании <code>LocalExecutor</code> их достаточно установить на машине, где запускается планировщик; при использовании распределённой установки <code>Celery</code> должна существовать пайплайн, который устанавливает эти <code>virtualenv</code> на нескольких машинах; наконец, если вы используете <strong>Docker-образы</strong> (например, в <strong>Kubernetes</strong>), создание <code>virtualenv</code> должно быть добавлено в пайплайн сборки вашего кастомного образа.</p>
<p><strong>Преимущества оператора:</strong></p>
<ul>
<li>Отсутствие накладных расходов при запуске задачи. <code>Virtualenv</code> уже готов в момент начала выполнения задачи.</li>
<li>Вы можете запускать задачи с разными наборами зависимостей на одних и тех же воркерах — таким образом, все ресурсы переиспользуются.</li>
<li>Воркерам не требуется доступ к <strong>PyPI</strong> или приватным репозиториям. Меньше вероятность временных сбоев, связанных с сетью.</li>
<li>Зависимости могут быть заранее проверены администраторами и командой безопасности, и никакой новый, неожиданный код не будет динамически добавляться. Это полезно как с точки зрения безопасности, так и стабильности.</li>
<li>Минимальное влияние на деплой — вам не нужно переходить на Docker-контейнеры или <strong>Kubernetes</strong>, чтобы эффективно использовать оператор.</li>
<li>Автору <strong>Dag</strong>’ов не нужно изучать контейнеры или <strong>Kubernetes</strong>. Для написания Dag’ов таким способом достаточно знания <strong>Python</strong> и работы с <strong>requirements</strong>.</li>
</ul>
<p><strong>Недостатки:</strong></p>
<ul>
<li>Окружения должны быть подготовлены заранее. Обычно это означает, что вы не можете менять их «на лету»: добавление новых зависимостей или изменение существующих требует как минимум повторного деплоя <strong>Airflow</strong>, а время итераций при разработке новых версий может увеличиться.</li>
<li>Ваш <strong>Python-callable</strong> должен быть сериализуемым. Существует множество <strong>Python-объектов</strong>, которые не сериализуются стандартной библиотекой <code>pickle</code>. Часть этих ограничений можно смягчить с помощью библиотеки <code>dill</code>, однако она также не решает всех проблем сериализации.</li>
<li>Все зависимости, отсутствующие в окружении Airflow, должны импортироваться локально внутри используемого callable, а код верхнего уровня Dag не должен импортировать или использовать эти библиотеки.</li>
<li>Virtualenv запускаются в рамках одной и той же операционной системы, поэтому они не могут иметь конфликтующие системные зависимости (устанавливаемые через apt или yum). Независимо могут устанавливаться только Python-зависимости.</li>
<li>Задачи изолированы друг от друга только за счёт выполнения в разных окружениях. Это означает, что выполняющиеся задачи всё ещё могут влиять друг на друга — например, последующие задачи, выполняемые на том же воркере, могут быть затронуты предыдущими задачами, которые создавали или изменяли файлы и т. п.</li>
</ul>
<p><code>PythonVirtualenvOperator</code> и <code>ExternalPythonOperator</code> можно рассматривать как взаимодополняющие инструменты, которые упрощают переход от этапа разработки к продакшену. Как автор Dag’ов, вы обычно будете итерироваться с зависимостями и разрабатывать Dag, используя <code>PythonVirtualenvOperator</code> (декорируя задачи <code>@task.virtualenv</code>), а после завершения итераций и внесения изменений, для продакшена, скорее всего, переключитесь на <code>ExternalPythonOperator</code> (и <code>@task.external_python</code>) после того, как команды <strong>DevOps/System Admin</strong> развернут новые зависимости в предсуществующих <code>virtualenv</code> в продакшене. Преимущество такого подхода в том, что вы в любой момент можете вернуть декоратор обратно и продолжить «динамическую» разработку с <code>PythonVirtualenvOperator</code>.</p>
<p>Подробные примеры использования <code>airflow.providers.standard.operators.python.ExternalPythonOperator</code> приведены в разделе TaskFlow External Python example.</p>
<h3><strong>Использование DockerOperator или KubernetesPodOperator</strong></h3>
<p>Ещё одной стратегией является использование <code>airflow.providers.docker.operators.docker.DockerOperator</code> и <code>airflow.providers.cncf.kubernetes.operators.pod.KubernetesPodOperator</code>. Для этого требуется, чтобы <strong>Airflow</strong> имел доступ к <strong>Docker Engine</strong> или кластеру <strong>Kubernetes</strong>.</p>
<p>Аналогично Python-операторам, декораторы <strong>TaskFlow</strong> удобны в случае, если вы хотите использовать эти операторы для выполнения вашего <strong>Python-callable</strong>.</p>
<p>Однако этот подход значительно сложнее — вам необходимо понимать, как работают<strong> Docker-контейнеры</strong> и <strong>Kubernetes Pod</strong>’ы, если вы хотите его использовать. Зато задачи полностью изолированы друг от друга, и вы даже не ограничены выполнением только Python-кода. Вы можете писать задачи на любом языке программирования. Кроме того, ваши зависимости полностью независимы от зависимостей <strong>Airflow</strong> (включая системные зависимости), поэтому если вашей задаче требуется принципиально иное окружение, это подходящий вариант.</p>
<p><em>Добавлено в версии 2.2:</em><br />
Начиная с версии <strong>Airflow 2.2</strong>, вы можете использовать декоратор <code>@task.docker</code> для запуска функций с помощью <strong>DockerOperator</strong>.</p>
<p><em>Добавлено в версии 2.4:</em><br />
Начиная с версии <strong>Airflow 2.2</strong>, вы можете использовать декоратор <code>@task.kubernetes</code> для запуска функций с помощью <code>KubernetesPodOperator</code>.</p>
<p>Преимущества использования этих операторов:</p>
<ul>
<li>Вы можете запускать задачи с разными наборами как <strong>Python-</strong>, так и системных зависимостей, а также задачи, написанные на совершенно другом языке программирования или даже под другую архитектуру процессора (<code>x86</code> vs. <code>arm</code>).</li>
<li>Окружение, в котором выполняются задачи, использует оптимизации и неизменяемость контейнеров. Похожие наборы зависимостей эффективно переиспользуют закешированные слои образов, поэтому окружение хорошо оптимизировано для случаев, когда у вас есть несколько похожих, но разных окружений.</li>
<li>Зависимости могут быть заранее проверены администраторами и командой безопасности, и никакой новый, неожиданный код не будет динамически добавляться. Это полезно как с точки зрения безопасности, так и стабильности.</li>
<li>Полная изоляция между задачами. Они не могут влиять друг на друга иначе, чем через стандартные механизмы <strong>Airflow XCom</strong>.</li>
</ul>
<p><strong>Недостатки</strong>:</p>
<ul>
<li>Существует накладной расход на запуск задач. Обычно он меньше, чем при динамическом создании <code>virtualenv</code>, но всё равно заметен (особенно для <code>KubernetesPodOperator</code>).</li>
<li>В случае использования декораторов <strong>TaskFlow</strong> весь вызываемый метод должен быть сериализован и передан в <strong>Docker-контейнер</strong> или <strong>Kubernetes Pod</strong>, при этом существуют системные ограничения на размер метода. Сериализация, передача и последующая десериализация на удалённой стороне также добавляют накладные расходы.</li>
<li>Присутствуют накладные расходы по ресурсам, связанные с необходимостью нескольких процессов. При использовании этих операторов для выполнения задач требуется как минимум два процесса: один процесс (в <strong>Docker-контейнере</strong> или <strong>Kubernetes Pod</strong>), выполняющий задачу, и процесс-наблюдатель в воркере <strong>Airflow</strong>, который отправляет задание в <strong>Docker/Kubernetes</strong> и отслеживает его выполнение.</li>
<li><strong>Контейнерные образы</strong> должны быть подготовлены заранее. Обычно это означает, что вы не можете изменять их «на лету». Добавление системных зависимостей, изменение или обновление <strong>Python-зависимостей</strong> требует пересборки и публикации образа (как правило, в приватном реестре). Время итераций при работе с новыми зависимостями обычно больше и требует от разработчика сборки и использования собственных образов во время разработки. Наличие корректного пайплайна деплоя здесь критически важно для надёжного сопровождения системы.</li>
<li>Если вы хотите запускать <strong>Python-callable</strong> через <strong>декораторы</strong>, он должен быть сериализуемым. Также в этом случае все зависимости, отсутствующие в окружении <strong>Airflow</strong>, должны импортироваться локально внутри используемого <strong>callable</strong>, а код верхнего уровня <strong>Dag</strong> не должен импортировать или использовать эти библиотеки.</li>
<li>Вам необходимо глубже понимать, как работают <strong>Docker-контейнеры</strong> или <strong>Kubernetes</strong>. Абстракции, предоставляемые этими технологиями, являются «протекающими», поэтому для написания Dag’ов с использованием этих операторов нужно разбираться в ресурсах, сетях, контейнерах и других аспектах.</li>
</ul>
<p>Подробные примеры использования <code>airflow.providers.docker.operators.docker.DockerOperator</code> приведены в разделе <strong>TaskFlow Docker example</strong>, а <code>airflow.providers.cncf.kubernetes.operators.pod.KubernetesPodOperator</code> — в разделе <strong>TaskFlow Kubernetes example</strong>.</p>
<h3><strong>Использование нескольких Docker-образов и очередей Celery</strong></h3>
<p>Существует возможность (хотя она требует глубокого понимания деплоя <strong>Airflow</strong>) запускать задачи Airflow с использованием нескольких независимых <strong>Docker-образов</strong>. Это можно реализовать путём назначения разных задач разным <strong>очередям (Queues)</strong> и настройки <strong>Celery-воркеров</strong> на использование разных образов для разных очередей. Однако такой подход (по крайней мере на данный момент) требует большого объёма ручной конфигурации деплоя и глубоких знаний того, как работают <strong>Airflow, Celery</strong> и <strong>Kubernetes</strong>. Кроме того, он вносит существенные накладные расходы при выполнении задач — снижается возможность переиспользования ресурсов, а также становится значительно сложнее точно настраивать стоимость потребляемых ресурсов без негативного влияния на производительность и стабильность.</p>
<p>Одним из возможных способов сделать этот подход более полезным является реализация <strong>AIP-46 (Runtime isolation for Airflow tasks and Dag parsing)</strong> и завершение <strong>AIP-43 (Dag Processor Separation)</strong>. До реализации этих инициатив преимуществ у данного подхода крайне мало, и он не рекомендуется к использованию.</p>
<p>Однако после реализации этих <strong>AIP</strong> откроется возможность более <strong>мультиарендного (multi-tenant) подхода</strong>, при котором несколько команд смогут иметь полностью изолированные наборы зависимостей, используемые на протяжении всего <strong>жизненного цикла Dag</strong> — от парсинга до выполнения.</p>
<h1>Создание пользовательского оператора (custom Operator)</h1>
<p>Airflow позволяет создавать новые операторы в соответствии с требованиями вас или вашей команды. Такая расширяемость — одна из ключевых возможностей, делающих Apache Airflow мощным инструментом.</p>
<p>Вы можете создать любой оператор, унаследовавшись от публичного базового класса SDK — BaseOperator.</p>
<p><strong>В производном классе необходимо переопределить два метода:</strong></p>
<ul>
<li><strong>Конструктор (__init__)</strong> — определить параметры, необходимые для оператора. Нужно указывать только аргументы, специфичные для вашего оператора. <code>default_args</code> можно задать в файле <code>Dag</code>.</li>
<li><strong>Execute</strong> — код, который будет выполнен при вызове оператора раннером. Метод принимает контекст <code>Airflow</code> в качестве параметра, который можно использовать для чтения конфигурационных значений.</li>
</ul>
<p><strong>Примечание</strong></p>
<p>При реализации пользовательских операторов не выполняйте ресурсоёмкие операции в методе <code>init</code>. Операторы создаются один раз за цикл планировщика для каждой задачи, которая их использует, и выполнение, например, запросов к базе данных может существенно замедлить планирование и привести к неэффективному использованию ресурсов.</p>
<p>Реализуем пример <code>HelloOperator</code> в новом файле <code>hello_operator.py</code>:</p><pre class="urvanov-syntax-highlighter-plain-tag">from airflow.sdk import BaseOperator


class HelloOperator(BaseOperator):
    def __init__(self, name: str, **kwargs) -&gt; None:
        super().__init__(**kwargs)
        self.name = name

    def execute(self, context):
        message = f"Hello {self.name}"
        print(message)
        return message</pre><p><strong>Примечание</strong></p>
<p>Чтобы импорты работали корректно, файл должен находиться в директории, присутствующей в переменной окружения <code>PYTHONPATH</code>. <strong>Airflow</strong> по умолчанию добавляет директории <code>dags/</code>, <code>plugins/</code> и <code>config/</code> из домашнего каталога <strong>Airflow</strong> в <code>PYTHONPATH</code>. В нашем примере файл размещён в директории <code>custom_operator/</code>.</p>
<p>Теперь вы можете использовать созданный пользовательский оператор следующим образом:</p><pre class="urvanov-syntax-highlighter-plain-tag">from custom_operator.hello_operator import HelloOperator

with dag:
    hello_task = HelloOperator(task_id="sample-task", name="foo_bar")</pre><p>Вы также можете продолжать использовать папку <code>plugins</code> для хранения пользовательских операторов. Если файл <code>hello_operator.py</code> находится в директории <strong>plugins</strong>, оператор можно импортировать следующим образом:</p><pre class="urvanov-syntax-highlighter-plain-tag">from hello_operator import HelloOperator</pre><p>Если оператор взаимодействует с внешним сервисом (API, база данных и т. п.), рекомендуется реализовать слой взаимодействия через <strong>Hooks</strong>. Это позволит повторно использовать реализованную логику в других операторах. Такой подход обеспечивает лучшее разделение ответственности и более эффективное использование интеграции по сравнению с созданием <code>CustomServiceBaseOperator</code> для каждого внешнего сервиса.</p>
<p>Ещё один аспект — временное состояние. Если операция требует хранения состояния в памяти (например, job id, который должен использоваться в методе on_kill для отмены запроса), это состояние должно храниться в операторе, а не в hook. Таким образом, hook сервиса остаётся полностью stateless, а вся логика операции сосредоточена в одном месте — в операторе.</p>
<h2>Hooks</h2>
<p><strong>Hooks</strong> выступают интерфейсом для взаимодействия с внешними общими ресурсами в Dag. Например, нескольким задачам в <strong>Dag</strong> может потребоваться доступ к базе данных <strong>MySQL</strong>. Вместо создания отдельного подключения для каждой задачи можно получить подключение через <strong>hook</strong> и использовать его повторно.</p>
<p><strong>Hook</strong> также помогает избежать хранения параметров аутентификации подключения непосредственно в <strong>Dag</strong>.</p>
<p>Расширим предыдущий пример и получим имя из MySQL:</p><pre class="urvanov-syntax-highlighter-plain-tag">class HelloDBOperator(BaseOperator):
    def __init__(self, name: str, mysql_conn_id: str, database: str, **kwargs) -&gt; None:
        super().__init__(**kwargs)
        self.name = name
        self.mysql_conn_id = mysql_conn_id
        self.database = database

    def execute(self, context):
        hook = MySqlHook(mysql_conn_id=self.mysql_conn_id, schema=self.database)
        sql = "select name from user"
        result = hook.get_first(sql)
        message = f"Hello {result['name']}"
        print(message)
        return message</pre><p>Когда оператор выполняет запрос через объект <strong>hook</strong>, создаётся новое подключение, если оно ещё не существует. <strong>Hook</strong> получает параметры аутентификации (например, имя пользователя и пароль) из <strong>backend</strong> Airflow и передаёт их в <code>airflow.hooks.base.BaseHook.get_connection()</code>.</p>
<p>Создавать <strong>hook</strong> следует только в методе <strong>execute</strong> или в методах, вызываемых из <strong>execute</strong>. Конструктор вызывается каждый раз при парсинге <strong>Dag</strong> (а это происходит часто), и создание <strong>hook</strong> в нём приведёт к множеству ненужных подключений к базе данных. Метод <strong>execute</strong> вызывается только во время запуска <strong>Dag</strong>.</p>
<h3>Пользовательский интерфейс</h3>
<p>Airflow позволяет разработчику управлять отображением оператора в интерфейсе Dag.</p>
<ul>
<li>Переопределите <code>ui_color</code>, чтобы изменить цвет фона оператора в <strong>UI</strong>.</li>
<li>Переопределите <code>ui_fgcolor</code>, чтобы изменить цвет текста.</li>
</ul>
<p>Переопределите <code>custom_operator_name</code>, чтобы изменить отображаемое имя (отличное от имени класса).</p><pre class="urvanov-syntax-highlighter-plain-tag">class HelloOperator(BaseOperator):
    ui_color = "#ff0000"
    ui_fgcolor = "#000000"
    custom_operator_name = "Howdy"
    # ...</pre><p></p>
<h3>Шаблонизация (Templating)</h3>
<p>Вы можете использовать шаблоны <strong>Jinja</strong> для параметризации оператора. <strong>Airflow</strong> применяет шаблонизацию к полям, указанным в <code>template_fields</code>, во время рендеринга оператора.</p><pre class="urvanov-syntax-highlighter-plain-tag">class HelloOperator(BaseOperator):
    template_fields: Sequence[str] = ("name",)

    def __init__(self, name: str, world: str, **kwargs) -&gt; None:
        super().__init__(**kwargs)
        self.name = name
        self.world = world

    def execute(self, context):
        message = f"Hello {self.world} it's {self.name}!"
        print(message)
        return message</pre><p>Использование шаблона:</p><pre class="urvanov-syntax-highlighter-plain-tag">with dag:
    hello_task = HelloOperator(
        task_id="task_id_1",
        name="{{ task_instance.task_id }}",
        world="Earth",
    )</pre><p>В этом примере <code>Jinja</code> найдёт параметр <code>name</code> и заменит <code>{{ task_instance.task_id }}</code> на <code>task_id_1</code>.</p>
<p>Параметр также может содержать имя файла, например <strong>bash-скрипта</strong> или <strong>SQL-файла</strong>. В этом случае нужно указать расширение файла в <strong>template_ext</strong>. Если поле из <strong>template_fields</strong> содержит строку, заканчивающуюся расширением из <strong>template_ext</strong>, <strong>Jinja</strong> прочитает содержимое файла и заменит шаблоны на реальные значения.</p>
<p><strong>Обратите внимание:</strong> <code>Jinja</code> подставляет значения в атрибуты оператора, а не в аргументы функции.</p><pre class="urvanov-syntax-highlighter-plain-tag">class HelloOperator(BaseOperator):
    template_fields: Sequence[str] = ("guest_name",)
    template_ext = ".sql"

    def __init__(self, name: str, **kwargs) -&gt; None:
        super().__init__(**kwargs)
        self.guest_name = name</pre><p>В этом примере <code>template_fields</code> должен быть <code>['guest_name']</code>, а не <code>['name']</code>.</p>
<p>Дополнительно вы можете указать <code>template_fields_renderers</code> — словарь, определяющий, в каком формате значение шаблонного поля будет отображаться в веб-интерфейсе. Например:</p><pre class="urvanov-syntax-highlighter-plain-tag">class MyRequestOperator(BaseOperator):
    template_fields: Sequence[str] = ("request_body",)
    template_fields_renderers = {"request_body": "json"}

    def __init__(self, request_body: str, **kwargs) -&gt; None:
        super().__init__(**kwargs)
        self.request_body = request_body</pre><p>В ситуации, когда <code>template_field</code> сам по себе является словарём, также можно указать путь к ключу через точку, чтобы извлекать и корректно отображать отдельные элементы. Например:</p><pre class="urvanov-syntax-highlighter-plain-tag">class MyConfigOperator(BaseOperator):
    template_fields: Sequence[str] = ("configuration",)
    template_fields_renderers = {
        "configuration": "json",
        "configuration.query.sql": "sql",
    }

    def __init__(self, configuration: dict, **kwargs) -&gt; None:
        super().__init__(**kwargs)
        self.configuration = configuration</pre><p>Использование этого шаблона:</p><pre class="urvanov-syntax-highlighter-plain-tag">with dag:
    config_task = MyConfigOperator(
        task_id="task_id_1",
        configuration={"query": {"job_id": "123", "sql": "select * from my_table"}},
    )</pre><p>В результате в <strong>UI</strong> поле <code>configuration</code> будет отображаться в формате <strong>JSON</strong>, а значение, находящееся по пути <code>configuration.query.sql</code>, будет подсвечено с использованием <strong>SQL-лексера</strong>.</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2026/01/template_field_renderer_path.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2868" src="https://datatalks.ru/wp-content/uploads/2026/01/template_field_renderer_path.png" alt="" width="1274" height="574" srcset="https://datatalks.ru/wp-content/uploads/2026/01/template_field_renderer_path.png 1274w, https://datatalks.ru/wp-content/uploads/2026/01/template_field_renderer_path-300x135.png 300w, https://datatalks.ru/wp-content/uploads/2026/01/template_field_renderer_path-1024x461.png 1024w, https://datatalks.ru/wp-content/uploads/2026/01/template_field_renderer_path-768x346.png 768w, https://datatalks.ru/wp-content/uploads/2026/01/template_field_renderer_path-450x203.png 450w, https://datatalks.ru/wp-content/uploads/2026/01/template_field_renderer_path-780x351.png 780w" sizes="(max-width: 1274px) 100vw, 1274px" /></a></p>
<p>В настоящее время доступны следующие <strong>лексеры</strong>:</p>
<ul>
<li>bash</li>
<li>bash_command</li>
<li>doc</li>
<li>doc_json</li>
<li>doc_md</li>
<li>doc_rst</li>
<li>doc_yaml</li>
<li>doc_md</li>
<li>hql</li>
<li>html</li>
<li>jinja</li>
<li>json</li>
<li>md</li>
<li>mysql</li>
<li>postgresql</li>
<li>powershell</li>
<li>py</li>
<li>python_callable</li>
<li>rst</li>
<li>sql</li>
<li>tsql</li>
<li>yaml</li>
</ul>
<p>Если вы укажете несуществующий лексер, значение шаблонного поля будет отображено как красиво отформатированный (<strong>pretty-printed</strong>) объект.</p>
<h2>Ограничения</h2>
<p>Чтобы предотвратить неправильное использование, при определении и назначении шаблонизируемых полей в конструкторе оператора (если он определён, иначе — см. ниже) необходимо соблюдать следующие ограничения:</p>
<p><strong>1. Параметры конструктора, соответствующие шаблонным полям, должны называться точно так же, как и сами поля.</strong></p>
<p>Следующий пример некорректен, так как имя параметра конструктора не совпадает с именем шаблонного поля:</p><pre class="urvanov-syntax-highlighter-plain-tag">class HelloOperator(BaseOperator):
    template_fields = "foo"

    def __init__(self, foo_id) -&gt; None:  # должно быть def __init__(self, foo) -&gt; None
        self.foo = foo_id  # должно быть self.foo = foo</pre><p><strong>2. Атрибуты экземпляра, соответствующие шаблонным полям, должны быть явно присвоены из соответствующих параметров конструктора — либо напрямую, либо через вызов конструктора родительского класса (где эти поля определены как template_fields) с явной передачей параметров.</strong></p>
<p>Следующий пример некорректен, так как атрибут <code>self.foo</code> вообще не присваивается, несмотря на то, что он объявлен как шаблонное поле:</p><pre class="urvanov-syntax-highlighter-plain-tag">class HelloOperator(BaseOperator):
    template_fields = ("foo", "bar")

    def __init__(self, foo, bar) -&gt; None:
        self.bar = bar</pre><p>Следующий пример также некорректен, так как <code>self.foo</code> в <code>MyHelloOperator</code> инициализируется неявно через <code>kwargs</code>, переданные в конструктор родителя:</p><pre class="urvanov-syntax-highlighter-plain-tag">class HelloOperator(BaseOperator):
    template_fields = "foo"

    def __init__(self, foo) -&gt; None:
        self.foo = foo


class MyHelloOperator(HelloOperator):
    template_fields = ("foo", "bar")

    def __init__(self, bar, **kwargs) -&gt; None:  # должно быть def __init__(self, foo, bar, **kwargs)
        super().__init__(**kwargs)  # должно быть super().__init__(foo=foo, **kwargs)
        self.bar = bar</pre><p><strong>3. Нельзя применять преобразования к параметру при его присваивании в конструкторе.</strong></p>
<p>Любые действия над значением должны выполняться в методе <code>execute()</code>.</p>
<p>Следующий пример некорректен:</p><pre class="urvanov-syntax-highlighter-plain-tag">class HelloOperator(BaseOperator):
    template_fields = "foo"

    def __init__(self, foo) -&gt; None:
        self.foo = foo.lower()  # должно быть только self.foo = foo</pre><p>Если оператор наследуется от базового оператора и не определяет собственный конструктор, указанные ограничения не применяются. Однако шаблонные поля должны быть корректно определены в родительском классе с соблюдением этих правил.</p>
<p>Следующий пример корректен:</p><pre class="urvanov-syntax-highlighter-plain-tag">class HelloOperator(BaseOperator):
    template_fields = "foo"

    def __init__(self, foo) -&gt; None:
        self.foo = foo


class MyHelloOperator(HelloOperator):
    template_fields = "foo"</pre><p>Эти ограничения проверяются <code>pre-commit</code> <strong>hook’ом</strong> с именем <code>validate-operators-init</code>.</p>
<p>Добавление шаблонных полей через наследование</p>
<p>Распространённый сценарий создания пользовательского оператора — расширение уже существующих <code>template_fields</code>. Может возникнуть ситуация, когда нужный вам оператор не объявляет определённые параметры как шаблонные, но вы хотите передавать их динамически через <strong>Jinja-выражения</strong>. Это легко реализуется через простое наследование.</p>
<p>Предположим, у вас есть ранее определённый <code>HelloOperator</code>:</p><pre class="urvanov-syntax-highlighter-plain-tag">class HelloOperator(BaseOperator):
    template_fields: Sequence[str] = ("name",)

    def __init__(self, name: str, world: str, **kwargs) -&gt; None:
        super().__init__(**kwargs)
        self.name = name
        self.world = world

    def execute(self, context):
        message = f"Hello {self.world} it's {self.name}!"
        print(message)
        return message</pre><p>Допустим, вы хотите динамически параметризовать аргумент <strong>world</strong>.</p>
<p>Поскольку <code>template_fields</code> гарантированно является <code>Sequence[str]</code> (списком или кортежем строк), можно легко создать подкласс и расширить список шаблонных полей:</p><pre class="urvanov-syntax-highlighter-plain-tag">class MyHelloOperator(HelloOperator):
    template_fields: Sequence[str] = (*HelloOperator.template_fields, "world")</pre><p>Теперь можно использовать <code>MyHelloOperator</code> следующим образом:</p><pre class="urvanov-syntax-highlighter-plain-tag">with dag:
    hello_task = MyHelloOperator(
        task_id="task_id_1",
        name="{{ task_instance.task_id }}",
        world="{{ var.value.my_world }}",
    )</pre><p>В этом примере аргумент <strong>world</strong> будет динамически установлен в значение переменной <strong>Airflow</strong> с именем <strong>my_world</strong> через <strong>Jinja-выражение</strong>.</p>
<h2>Определение дополнительной ссылки (Extra Link) для оператора</h2>
<p>Для своего оператора вы можете определить <strong>дополнительную ссылку (extra link)</strong>, которая будет перенаправлять пользователей во внешние системы. Например, можно добавить ссылку, ведущую на документацию или руководство по использованию оператора.</p>
<h2>Sensors</h2>
<p><strong>Airflow</strong> предоставляет специальный тип оператора — <strong>Sensor</strong>, предназначенный для регулярной проверки (<strong>polling</strong>) некоторого состояния (например, наличия файла) до тех пор, пока не будет выполнено условие успешного завершения.</p>
<p>Вы можете создать собственный сенсор, унаследовавшись от <code>airflow.sensors.base.BaseSensorOperator</code> и реализовав метод <code>poke</code>, который будет опрашивать внешнее состояние и проверять критерий успешности.</p>
<h3>Режим reschedule</h3>
<p>У сенсоров есть мощная возможность — режим <code>reschedule</code>, который позволяет задаче сенсора быть перепланированной, вместо того чтобы занимать слот воркера между проверками.</p>
<p>Это полезно, если:</p>
<ul>
<li>вы можете позволить себе более длинный интервал опроса,</li>
<li>ожидается длительное ожидание выполнения условия.</li>
</ul>
<h3>Ограничение режима reschedule</h3>
<p>Режим <code>reschedule</code> имеет важное ограничение: сенсор не может сохранять внутреннее состояние между перепланированными запусками.</p>
<p>Если ваш сенсор хранит внутреннее состояние, его следует декорировать с помощью <code>airflow.sensors.base.poke_mode_only()</code>. Это даст пользователям понять, что сенсор не подходит для использования в режиме <code>reschedule</code>.</p>
<h3>Пример сенсора с внутренним состоянием</h3>
<p>Примером сенсора, который хранит внутреннее состояние и не может использоваться в режиме reschedule, является:</p>
<p><code>airflow.providers.google.cloud.sensors.gcs.GCSUploadSessionCompleteSensor</code></p>
<p>Этот сенсор:</p>
<ul>
<li>опрашивает количество объектов по заданному префиксу (это количество является его внутренним состоянием),</li>
<li>считается успешно завершённым, если в течение определённого времени количество объектов не меняется.</li>
</ul>
<p>Сообщение <a href="https://datatalks.ru/best-practices-airflow-3-documentation/">Best Practices &#8212; Airflow 3 Документация</a> появились сначала на <a href="https://datatalks.ru">DataTalks.RU. Data Engineering / DWH / Data Pipeline</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://datatalks.ru/best-practices-airflow-3-documentation/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Python &#8212; Многозадачность, конкурентность и асинхронность</title>
		<link>https://datatalks.ru/python-threading-multiprocessing-asyncio/</link>
					<comments>https://datatalks.ru/python-threading-multiprocessing-asyncio/#respond</comments>
		
		<dc:creator><![CDATA[Data Engineer (Admin)]]></dc:creator>
		<pubDate>Fri, 26 Dec 2025 19:05:32 +0000</pubDate>
				<category><![CDATA[Python]]></category>
		<category><![CDATA[asyncio]]></category>
		<category><![CDATA[concurrent.futures]]></category>
		<category><![CDATA[multiprocessing]]></category>
		<category><![CDATA[threading]]></category>
		<guid isPermaLink="false">https://datatalks.ru/?p=2596</guid>

					<description><![CDATA[<p>Подборка материалов для освоения темы многозадачности в Python YouTube ролики Как работает GIL в Python. Многопоточность. Многопроцессность. IO/CPU-Bound Yandex for Developers &#8212; 01. Устройство CPython – Егор Овчаренко [ZProger] Многопоточность и Многопроцессорность Python. Threading &#38; Multiprocessing Python Асинхронность, многопоточность, многопроцессность в python &#124; Библиотека asyncio и асинхронный код Threading. Кратко про Python Плейлист Асинхронность в [&#8230;]</p>
<p>Сообщение <a href="https://datatalks.ru/python-threading-multiprocessing-asyncio/">Python &#8212; Многозадачность, конкурентность и асинхронность</a> появились сначала на <a href="https://datatalks.ru">DataTalks.RU. Data Engineering / DWH / Data Pipeline</a>.</p>
]]></description>
										<content:encoded><![CDATA[<h1>Подборка материалов для освоения темы многозадачности в Python</h1>
<h2>YouTube ролики</h2>
<ul>
<li><a href="https://www.youtube.com/watch?v=hkWmKQqLT4k" target="_blank" rel="noopener">Как работает GIL в Python. Многопоточность. Многопроцессность. IO/CPU-Bound</a></li>
<li><a href="https://www.youtube.com/watch?v=PxIqLgjtQ5Y" target="_blank" rel="noopener">Yandex for Developers &#8212; 01. Устройство CPython – Егор Овчаренко</a></li>
<li><a href="https://www.youtube.com/playlist?list=PL6plRXMq5RAAb9gwGqmgAoA-KIr-7CMuz" target="_blank" rel="noopener">[ZProger] Многопоточность и Многопроцессорность Python. Threading &amp; Multiprocessing Python</a></li>
<li><a href="https://www.youtube.com/watch?v=_4QY1nGFRY8" target="_blank" rel="noopener">Асинхронность, многопоточность, многопроцессность в python | Библиотека asyncio и асинхронный код</a></li>
<li><a href="https://www.youtube.com/watch?v=_JCV3deaFvE" target="_blank" rel="noopener">Threading. Кратко про Python</a></li>
<li><a href="https://www.youtube.com/playlist?list=PLlWXhlUMyooawilqK4lPXRvxtbYiw34S8" target="_blank" rel="noopener">Плейлист Асинхронность в Python</a></li>
<li><a href="https://www.youtube.com/live/sCPMwXPggis" target="_blank" rel="noopener">Young&amp;&amp;Yandex ШБР 2023 — Асинхронное программирование (Python)</a> &#8212; полный плейлист <a href="https://www.youtube.com/playlist?list=PLZvfMc-lVSSPZ_VYTK8XEkZ_S_bCfyu8C" target="_blank" rel="noopener">Python ШБР 2023</a></li>
<li><a href="https://www.youtube.com/watch?v=DvVhG8-HMSQ" target="_blank" rel="noopener">Особенности asyncio.wait_for() в асинхронном Python. Как работает таймаут для корутины</a></li>
<li><a href="https://www.youtube.com/playlist?list=PLz8SX0iNPyAIHH3xtwrcxI5UWleLB5el_" target="_blank" rel="noopener">Плейлист Асинхронность в Python</a></li>
<li><a href="https://www.youtube.com/watch?v=o_COfPdWAPw" target="_blank" rel="noopener">Асинхронное программирование на примере Python / asyncio</a></li>
<li><a href="https://www.youtube.com/watch?v=BoazgBZ4D7k" target="_blank" rel="noopener">Собеседование Python. Разбор вопросов</a></li>
<li><a href="https://www.youtube.com/watch?v=dvfnYkEHmdA" target="_blank" rel="noopener">Денис Аникин. Вновь ускоряем cpu-bound задачи</a></li>
<li><a href="https://www.youtube.com/watch?v=G2EG-eCHOiI" target="_blank" rel="noopener">Python: Threads, GIL, asyncio</a></li>
<li><a href="https://www.youtube.com/watch?v=QitEF7Qvi4w" target="_blank" rel="noopener">Лекция Тимофей Хирьянов &#8212; Параллельное программирование на Python</a></li>
<li>Yandex Developer (плейлист <a href="https://www.youtube.com/playlist?list=PLQC2_0cDcSKBHamFYA6ncnc_fYuEQUy0s" target="_blank" rel="noopener">Школа бэкенд-разработки 2019</a>) &#8212; Асинхронное программирование &#8212; <a href="https://www.youtube.com/watch?v=AXkOli6BsBY" target="_blank" rel="noopener">Лекция 1</a>, <a href="https://www.youtube.com/watch?v=IB4bJqmfjI0" target="_blank" rel="noopener">Лекция 2</a>, <a href="https://www.youtube.com/watch?v=FFUYf8FHDlY" target="_blank" rel="noopener">Лекция 3</a>
<ul>
<li><a href="https://www.youtube.com/playlist?list=PLQC2_0cDcSKCMKnywAS8eI_EgCcE3yx0r" target="_blank" rel="noopener">Плейлист Школа бэкенд-разработки 2021</a></li>
</ul>
</li>
<li><a href="https://www.youtube.com/playlist?list=PLlKID9PnOE5ibKy6U7XaCA2Nqk_R1d5CJ" target="_blank" rel="noopener">Плейлист &#171;Конкурентность в Python&#187;</a></li>
<li><a href="https://www.youtube.com/watch?v=AWX4JnAnjBE" target="_blank" rel="noopener">GIL в Python: зачем он нужен и как с этим жить</a></li>
<li><a href="https://www.youtube.com/watch?v=z7WIm0iZcOU" target="_blank" rel="noopener">Асинхронный Python-код медленнее обычного кода! Ааа!!1один. Aiohttp VS синхронные фреймворки</a></li>
</ul>
<p><strong>YouTube English:</strong></p>
<ul>
<li><a href="https://www.youtube.com/playlist?list=PLhNSoGM2ik6SIkVGXWBwerucXjgP1rHmB" target="_blank" rel="noopener">PlayList: Воспроизвести все import asyncio: Learn Python&#8217;s AsyncIO</a></li>
<li><a href="https://www.youtube.com/watch?v=Wsv07g4ml8I" target="_blank" rel="noopener">CPU Bound vs. I/O Bound | Computer Basics</a></li>
<li><a href="https://www.youtube.com/watch?v=AZnGRKFUU0c" target="_blank" rel="noopener">threading vs multiprocessing in python</a></li>
<li><a href="https://www.youtube.com/watch?v=XbBFKco43aw" target="_blank" rel="noopener">I/OBound vs CPU Bound Code</a></li>
</ul>
<h2>Статьи</h2>
<ul>
<li><a href="https://habr.com/ru/companies/otus/articles/960206/" target="_blank" rel="noopener">CPython простыми словами: всё, что нужно знать начинающему</a></li>
<li><a href="https://habr.com/ru/companies/otus/articles/769448/" target="_blank" rel="noopener">Как устроен GIL (Global Interpreter Lock) в Python: влияние на многозадачность и производительность</a></li>
<li><a href="https://habr.com/ru/articles/84629/" target="_blank" rel="noopener">Как устроен GIL в Python</a></li>
<li><a href="https://habr.com/ru/companies/wunderfund/articles/586360/" target="_blank" rel="noopener">Глобальная блокировка интерпретатора (GIL) и её воздействие на многопоточность в Python</a></li>
<li><a href="https://habr.com/ru/articles/417215/" target="_blank" rel="noopener">Всё, что нужно знать о сборщике мусора в Python</a></li>
<li><a href="https://habr.com/ru/companies/ntechlab/articles/946098/" target="_blank" rel="noopener">Визуализация управления памятью в Python: что творится внутри?</a></li>
</ul>
<h1>Введение в Python</h1>
<h2>Исходный глоссарий</h2>
<h3>Виртуальное адресное пространство</h3>
<p><strong>Виртуальное адресное пространство</strong> — это абстракция, предоставляемая ОС, в рамках которой каждый процесс видит собственную непрерывную адресную память, не зная о реальном физическом расположении данных.</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/12/Virtual_address_space.jpg"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2627" src="https://datatalks.ru/wp-content/uploads/2025/12/Virtual_address_space.jpg" alt="" width="660" height="452" srcset="https://datatalks.ru/wp-content/uploads/2025/12/Virtual_address_space.jpg 660w, https://datatalks.ru/wp-content/uploads/2025/12/Virtual_address_space-300x205.jpg 300w, https://datatalks.ru/wp-content/uploads/2025/12/Virtual_address_space-450x308.jpg 450w" sizes="(max-width: 660px) 100vw, 660px" /></a></p>
<p><strong>Структура виртуального адресного пространства</strong></p><pre class="urvanov-syntax-highlighter-plain-tag">Высокие адреса
┌─────────────────────────┐
│ Kernel space (отображ.) │  ← недоступен напрямую
├─────────────────────────┤
│ Stack                   │  ← стек потоков
├─────────────────────────┤
│ Heap                    │  ← объекты Python
├─────────────────────────┤
│ Data / BSS              │  ← глобальные переменные
├─────────────────────────┤
│ Code (text segment)     │  ← байткод + C-расширения
└─────────────────────────┘
Низкие адреса</pre><p>Python не управляет адресным пространством напрямую — он запрашивает память у ОС через <code>malloc</code>, <code>mmap</code>, <code>brk</code>.</p>
<h3>Heap &amp; Stack</h3>
<p><strong>Стек(stack)</strong> и <strong>куча(heap)</strong> – области в оперативной памяти (ОЗУ, RAM), в которых хранятся данные приложения во время его выполнения. Управление оперативной памятью для приложения Python осуществляется с помощью <strong>Python memory manager</strong>.</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/12/python_memory_management.jpeg"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2628" src="https://datatalks.ru/wp-content/uploads/2025/12/python_memory_management.jpeg" alt="" width="764" height="529" srcset="https://datatalks.ru/wp-content/uploads/2025/12/python_memory_management.jpeg 764w, https://datatalks.ru/wp-content/uploads/2025/12/python_memory_management-300x208.jpeg 300w, https://datatalks.ru/wp-content/uploads/2025/12/python_memory_management-450x312.jpeg 450w" sizes="(max-width: 764px) 100vw, 764px" /></a></p>
<p><strong>В управлении памятью (Python memory management)</strong> существует <strong>механизм учёта ссылок (reference counting)</strong>, который ведет внутренний журнал того, как много ссылок ссылается на объект в куче. Когда на объект не ссылается ни одна ссылка <strong>сборщик мусора (Garbage collector)</strong> автоматически освобождает память выделенную ранее для этого объекта.</p>
<p><strong>Heap</strong> — область памяти процесса, предназначенная для динамического выделения памяти во время выполнения.</p>
<p><strong>В Python:</strong></p>
<ul>
<li>все объекты Python живут в heap</li>
<li><code>int</code>, <code>list</code>, <code>dict</code>, <code>class</code>, <code>function</code> — всё heap</li>
</ul>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/12/python_heap_stack.jpeg"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2629" src="https://datatalks.ru/wp-content/uploads/2025/12/python_heap_stack.jpeg" alt="" width="971" height="515" srcset="https://datatalks.ru/wp-content/uploads/2025/12/python_heap_stack.jpeg 971w, https://datatalks.ru/wp-content/uploads/2025/12/python_heap_stack-300x159.jpeg 300w, https://datatalks.ru/wp-content/uploads/2025/12/python_heap_stack-768x407.jpeg 768w, https://datatalks.ru/wp-content/uploads/2025/12/python_heap_stack-450x239.jpeg 450w, https://datatalks.ru/wp-content/uploads/2025/12/python_heap_stack-780x414.jpeg 780w" sizes="(max-width: 971px) 100vw, 971px" /></a></p>
<p><strong>Stack</strong> — область памяти, используемая для хранения локальных переменных, адресов возврата, аргументов функций. Каждый поток имеет собственный стек.</p>
<p><strong>Python stack</strong> — это логическая абстракция, а не «настоящий» stack ОС.</p>
<h3>Дескрипторы ресурсов (File Descriptors)</h3>
<p><strong>File descriptor</strong> — это целое число, которое операционная система даёт твоей программе, когда она открывает файл или другое устройство ввода-вывода (например, сокет, pipe). Это как минимальный идентификатор ресурса: Python использует его для низкоуровневых операций с файлами.</p>
<ul>
<li>Это не объект Python, это число, под которым ОС видит открытый файл/ресурс.</li>
<li>С помощью FD можно делать низкоуровневые операции (чтение, запись, дупликация, перемещение позиции и т.п.).</li>
<li>Отличие от обычного open() в том, что FD используют функции модуля os, а не методы объекта файла.</li>
</ul>
<p>Каждый <strong>FD</strong> — ограниченный ресурс. Если ты открыл много файлов или сокетов и не закрыл их, система закончится и новые операции упадут с ошибками вроде Too many open files. Это особенно критично для серверов, которые держат много соединений одновременно.</p>
<p>В Unix-системах всё представляется как файл. Стандартные дескрипторы:</p>
<ul>
<li><code>0</code> — stdin</li>
<li><code>1</code> — stdout</li>
<li><code>2</code> — stderr</li>
</ul>
<p>Ты можешь перенаправлять их (например, в скриптах bash или в приложениях), и это тоже работает через FD.</p>
<p><strong>Примеры ресурсов:</strong> файлы, сокеты, pipe, eventfd, epoll/kqueue</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/12/file_descriptor_linux.jpeg"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2630" src="https://datatalks.ru/wp-content/uploads/2025/12/file_descriptor_linux.jpeg" alt="" width="862" height="591" srcset="https://datatalks.ru/wp-content/uploads/2025/12/file_descriptor_linux.jpeg 862w, https://datatalks.ru/wp-content/uploads/2025/12/file_descriptor_linux-300x206.jpeg 300w, https://datatalks.ru/wp-content/uploads/2025/12/file_descriptor_linux-768x527.jpeg 768w, https://datatalks.ru/wp-content/uploads/2025/12/file_descriptor_linux-450x309.jpeg 450w, https://datatalks.ru/wp-content/uploads/2025/12/file_descriptor_linux-780x535.jpeg 780w" sizes="(max-width: 862px) 100vw, 862px" /></a></p>
<h3>Глобальные переменные в Python</h3>
<p>Глобальные переменные — это имена, привязанные в namespace модуля.</p>
<p>В реальности:</p>
<ul>
<li>имя <code>x</code> → указатель</li>
<li>объект <code>10</code> → heap</li>
<li>namespace модуля → dict в heap</li>
</ul>
<h3>Регистры CPU</h3>
<p><strong>Регистры CPU</strong> — сверхбыстрая память внутри процессора.</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/12/cpu_regestry.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2631" src="https://datatalks.ru/wp-content/uploads/2025/12/cpu_regestry.png" alt="" width="627" height="368" srcset="https://datatalks.ru/wp-content/uploads/2025/12/cpu_regestry.png 627w, https://datatalks.ru/wp-content/uploads/2025/12/cpu_regestry-300x176.png 300w, https://datatalks.ru/wp-content/uploads/2025/12/cpu_regestry-450x264.png 450w" sizes="(max-width: 627px) 100vw, 627px" /></a></p>
<p><strong>Хранят: </strong>указатель инструкции (<strong>IP</strong>), указатель стека (<strong>SP</strong>), флаги, временные значения.</p>
<p>Python не управляет регистрами напрямую. Но при <strong>context switch ОС</strong> сохраняет регистры, при переключении потоков Python → регистры меняются. Это основная стоимость <strong>context switch</strong>.</p>
<h3>User Space</h3>
<p><strong>User space</strong> — режим выполнения с ограниченными правами.</p>
<p>Python-код выполняется исключительно в user space.</p>
<p><strong>Запрещено:</strong></p>
<ul>
<li>прямой доступ к устройствам</li>
<li>управление памятью</li>
<li>прерывания</li>
</ul>
<h3>Kernel Space</h3>
<p><strong>Kernel space</strong> — привилегированный режим выполнения.</p>
<p><strong>Ядро:</strong></p>
<ul>
<li>управляет памятью</li>
<li>планирует процессы</li>
<li>обрабатывает I/O</li>
<li>управляет сетевым стеком</li>
</ul>
<h2>Что такое процесс, поток, системный вызов и context switch?</h2>
<p><strong>Процесс</strong> — это изолированное выполняемое окружение, предоставляемое ОС. Каждый процесс имеет собственное виртуальное адресное пространство, heap, stack, дескрипторы ресурсов (файлы, сокеты).</p>
<p><strong>Поток (Thread)</strong> &#8212; это единица выполнения внутри процесса. Потоки разделяют одно адресное пространство процесса, каждый поток имеет собственный stack, выполняются псевдопараллельно внутри 1 процесса. Общее у потоков heap, глобальные переменные, объекты Python. Раздельное &#8212; stack, регистры CPU.</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/12/multiprocessing_threading.jpeg"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2635" src="https://datatalks.ru/wp-content/uploads/2025/12/multiprocessing_threading.jpeg" alt="" width="772" height="959" srcset="https://datatalks.ru/wp-content/uploads/2025/12/multiprocessing_threading.jpeg 772w, https://datatalks.ru/wp-content/uploads/2025/12/multiprocessing_threading-242x300.jpeg 242w, https://datatalks.ru/wp-content/uploads/2025/12/multiprocessing_threading-768x954.jpeg 768w, https://datatalks.ru/wp-content/uploads/2025/12/multiprocessing_threading-450x559.jpeg 450w" sizes="(max-width: 772px) 100vw, 772px" /></a></p>
<p><strong>Системный вызов</strong> — это контролируемый переход из <strong>user space</strong> в <strong>kernel space</strong>.</p>
<p>Python-код не может напрямую:</p>
<ul>
<li>читать диск</li>
<li>писать в сокет</li>
<li>создавать процесс</li>
<li>спать</li>
</ul>
<p><strong>Что происходит при системном вызове:</strong></p>
<ul>
<li>Python вызывает <strong>C-функцию</strong></li>
<li><strong>C-функция</strong> делает <code>syscall</code></li>
<li>ОС выполняет операцию</li>
<li>Поток блокируется, пока ОС не закончит</li>
</ul>
<p><strong>В этот момент:</strong></p>
<ul>
<li><strong>GIL</strong> может быть освобождён</li>
<li>другой поток может выполняться</li>
</ul>
<p><strong>Context switch</strong> — это переключение CPU с одной задачи на другую.</p>
<p><strong>Бывает:</strong></p>
<ul>
<li>между потоками</li>
<li>между процессами</li>
</ul>
<p>Что сохраняется:</p>
<ul>
<li>регистры CPU</li>
<li>указатель стека</li>
<li>состояние планировщика</li>
</ul>
<h2>Архитектура CPython</h2>
<p><strong>CPython</strong> — это эталонная реализация языка программирования Python. Это версия Python по умолчанию, наиболее широко используемая и оригинальная реализация, написанная преимущественно на языке C.</p>
<p>Иными словами <strong>CPython</strong> &#8212; это программа, которая принимает ваш <strong>код на Python</strong> и выполняет его, преобразуя в понятные машине действия.</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/12/cpython.jpeg"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2659" src="https://datatalks.ru/wp-content/uploads/2025/12/cpython.jpeg" alt="" width="590" height="307" srcset="https://datatalks.ru/wp-content/uploads/2025/12/cpython.jpeg 590w, https://datatalks.ru/wp-content/uploads/2025/12/cpython-300x156.jpeg 300w, https://datatalks.ru/wp-content/uploads/2025/12/cpython-450x234.jpeg 450w" sizes="(max-width: 590px) 100vw, 590px" /></a></p>
<ol>
<li><strong>Исходный код (Source code)</strong> – mymodule.py преобразуется в <strong>байт-код</strong> с помощью компилятора (compiler) Python</li>
<li><strong>Байт-код (Byte code)</strong> сохраняется в определенном формате (.pyc, .pyo, .pyd) – mymodule.pyc</li>
<li><strong>Виртуальная машина Python (или PVM)</strong> получает байт-код и с помощью интерпретатора преобразует его в бинарный код.</li>
<li>Бинарный или машинный код (Binary code)</li>
<li>Компьютер читает бинарный код и выполняет программу</li>
</ol>
<p><strong>Важно понимать разницу между языком Python и интерпретатором CPython.</strong> Язык Python — это набор правил и синтаксиса (описанных в документации), а CPython конкретная программа, исполняющая код на этом языке.</p>
<hr />
<blockquote><p><strong>Python</strong> &#8212; язык, а <strong>CPython</strong> &#8212; его основной движок.</p></blockquote>
<hr />
<p><strong>Что такое PVM?</strong></p>
<p>Мы знаем, что компьютеры понимают только машинный код, состоящий из нулей и единиц. Поскольку компьютер понимает исключительно машинный код, любой программный код перед выполнением должен быть преобразован в машинный код. Для этого используется компилятор. Обычно компилятор преобразует исходный код программы непосредственно в машинный код.</p>
<p>Компилятор Python выполняет ту же задачу, но несколько иным образом. Он преобразует исходный код программы в другой вид кода, называемый байт-кодом. Каждая инструкция программы на Python преобразуется в набор инструкций байт-кода.</p>
<p><strong>Виртуальная машина Python (Python Virtual Machine, PVM)</strong> принимает этот байт-код и преобразует его в машинный код, чтобы компьютер мог выполнить соответствующие инструкции и вывести итоговый результат. Для выполнения этого преобразования PVM оснащена интерпретатором. Интерпретатор преобразует байт-код в машинный код и передаёт этот машинный код процессору компьютера для выполнения. Поскольку именно интерпретатор играет ключевую роль, виртуальную машину Python часто также называют интерпретатором.</p>
<h3>Альтернативные реализации</h3>
<p>Хотя <strong>CPython</strong> является стандартной реализацией, существуют и другие реализации Python, созданные для конкретных задач, таких как повышение производительности или интеграция с другими платформами:</p>
<ul>
<li><strong>PyPy</strong> — использует компиляцию <strong>Just-In-Time (JIT)</strong>, что позволяет во многих случаях выполнять Python-код значительно быстрее, чем в CPython.</li>
<li><strong>Jython</strong> — написан на <strong>Java</strong> и компилирует Python-код в байткод Java, что позволяет запускать Python на виртуальной машине Java (<strong>JVM</strong>) и взаимодействовать с библиотеками Java.</li>
<li><strong>IronPython</strong> — реализован для <strong>Common Language Infrastructure (CLI)</strong>, благодаря чему может работать на платформе <code>.NET</code>.</li>
<li><strong>MicroPython / CircuitPython</strong> — оптимизированные реализации, предназначенные для микроконтроллеров и встраиваемых систем.</li>
</ul>
<h3>Производительность</h3>
<p>Те, кто имеют опыт работы с компилирующими языками программирования, такими как C и C++, могут заметить несколько отличий в модели выполнения Python.</p>
<ul>
<li><strong>Первое</strong>, что бросается в глаза, – это отсутствие этапа сборки, или вызова утилиты «make»: программный код может запускаться сразу же, как только будет написан.</li>
<li><strong>Второе</strong> отличие: байт код не является двоичным машинным кодом (например, инструкциями для микропроцессора Intel). Байт код – это внутреннее представление программ на языке Python.</li>
</ul>
<p>По этой причине программный код на языке Python не может выполняться так же быстро, как программный код на языке C или C++. <strong>Обход инструкций выполняет виртуальная машина</strong>, а не микропроцессор, и <strong>чтобы выполнить байт код, необходима дополнительная интерпретация</strong>, инструкции которого требуют на выполнение больше времени, чем машинные инструкции микропроцессора. С другой стороны, в отличие от классических интерпретаторов, здесь присутствует дополнительный этап компиляции – интерпретатору не требуется всякий раз снова и снова анализировать инструкции исходного текста. В результате Python способен обеспечить скорость выполнения где то между традиционными компилирующими и традиционными интерпретирующими языками программирования.</p>
<h2>GIL (Global Interpreter Lock)</h2>
<p><strong>GIL (Global Interpreter Lock)</strong> &#8212; интерпретатор Python однопоточный в том смысле, что в каждый момент времени может выполняться только <strong>один участок байт-кода</strong>, даже если в процессе работает несколько потоков. <strong>Глобальная блокировка интерпретатора не позволяет выполнять несколько потоков одновременно.</strong></p>
<p>Python может освободить GIL на время выполнения <strong>операций ввода-вывода (I/O Bound)</strong>, потому что для выполнения ввода-вывода вызывается низкоуровневая функция операционной системы. Эти функции работают за пределами интерпретатора, т. е. никак не могут повредить его внутренние структуры, от чего и призвана защитить GIL.</p>
<p><strong>GIL</strong> был введён для упрощения управления памятью в Python, поскольку многие внутренние операции, такие как создание объектов, по умолчанию не являются потокобезопасными. Без <strong>GIL</strong> нескольким потокам, одновременно обращающимся к общим ресурсам, потребовались бы сложные механизмы блокировок или синхронизации для предотвращения гонок данных и повреждения состояния.</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/12/python_gil.jpeg"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2647" src="https://datatalks.ru/wp-content/uploads/2025/12/python_gil.jpeg" alt="" width="910" height="348" srcset="https://datatalks.ru/wp-content/uploads/2025/12/python_gil.jpeg 910w, https://datatalks.ru/wp-content/uploads/2025/12/python_gil-300x115.jpeg 300w, https://datatalks.ru/wp-content/uploads/2025/12/python_gil-768x294.jpeg 768w, https://datatalks.ru/wp-content/uploads/2025/12/python_gil-450x172.jpeg 450w, https://datatalks.ru/wp-content/uploads/2025/12/python_gil-780x298.jpeg 780w" sizes="(max-width: 910px) 100vw, 910px" /></a></p>
<p><strong>Когда GIL становится узким местом?</strong></p>
<ul>
<li>В однопоточных программах <strong>GIL</strong> не имеет значения, так как поток обладает эксклюзивным доступом к интерпретатору Python.</li>
<li>В многопоточных <strong>I/O-bound</strong> программах влияние <strong>GIL</strong> менее заметно, поскольку потоки освобождают GIL во время ожидания операций ввода-вывода.</li>
<li>В многопоточных <strong>CPU-bound</strong> задачах <strong>GIL</strong> становится серьёзным узким местом. Несколько потоков, конкурируя за GIL, вынуждены по очереди выполнять байткод Python.</li>
</ul>
<p>Интересный случай, на который стоит обратить внимание, — использование time.sleep. Python фактически рассматривает <code>time.sleep</code> как <strong>I/O-операцию</strong>. Функция <code>time.sleep</code> не является <strong>CPU-bound</strong>, поскольку во время сна не происходит активных вычислений или выполнения байткода Python. Вместо этого ответственность за отслеживание прошедшего времени передаётся операционной системе. В течение этого времени поток освобождает GIL, позволяя другим потокам выполняться и использовать интерпретатор.</p>
<h3><strong>Когда GIL может освобождать поток?</strong></h3>
<table>
<thead>
<tr>
<th>Ситуация</th>
<th>GIL</th>
</tr>
</thead>
<tbody>
<tr>
<td><code inline="">time.sleep()</code></td>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> отпущен</td>
</tr>
<tr>
<td>I/O</td>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> отпущен</td>
</tr>
<tr>
<td><code inline="">lock.acquire()</code> (ожидание)</td>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> отпущен</td>
</tr>
<tr>
<td>C-расширение без Python API</td>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> отпущен</td>
</tr>
<tr>
<td>Чистый Python CPU-код</td>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/274c.png" alt="❌" class="wp-smiley" style="height: 1em; max-height: 1em;" /></td>
</tr>
<tr>
<td>Работа с Python-объектами</td>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/274c.png" alt="❌" class="wp-smiley" style="height: 1em; max-height: 1em;" /></td>
</tr>
<tr>
<td>Переключение по таймеру</td>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> временно</td>
</tr>
</tbody>
</table>
<p>Рассмотрим подробно каждую ситуацию.</p>
<p><strong>1 кейс &#8212; блокирующие операции (I/O, sleep, lock wait):</strong> Когда поток заходит в операцию, которая может надолго заблокироваться, CPython отпускает GIL.</p>
<p>time.sleep()</p>
<p><strong>I/O:</strong></p><pre class="urvanov-syntax-highlighter-plain-tag">sock.recv()
sock.send()
open().read()
requests.get()</pre><p>Ожидание примитивов синхронизации</p><pre class="urvanov-syntax-highlighter-plain-tag">lock.acquire()      # если lock уже занят
event.wait()
condition.wait()
queue.get()         # если очередь пуста</pre><p><strong>Кейс 2 &#8212; выполнение C-кода, который отпускает GIL:</strong> Если поток заходит в C-расширение, где внутри есть:</p><pre class="urvanov-syntax-highlighter-plain-tag">Py_BEGIN_ALLOW_THREADS
// тяжёлая работа без Python-объектов
Py_END_ALLOW_THREADS</pre><p>текущий поток временно теряет GIL.</p>
<p>Примеры библиотек:</p>
<ul>
<li>numpy</li>
<li>hashlib</li>
<li>zlib</li>
<li>Pillow</li>
</ul>
<p><strong>C-расширение без Python API:</strong> C-код, который во время выполнения не создаёт, не читает и не изменяет Python-объекты (PyObject*).</p>
<p><strong>Пример Python API в C:</strong></p><pre class="urvanov-syntax-highlighter-plain-tag">PyLong_FromLong(10);      // создаёт Python-объект
PyList_Append(list, x);  // меняет Python-объект
Py_INCREF(obj);          // меняет refcount
PyObject_CallObject(f);  // вызывает Python-функцию
PyErr_SetString(...);    // трогает исключения</pre><p></p>
<h2>CPU-bound vs I/O-bound задачи</h2>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/12/i_o_inbound_cpu_inbound.jpeg"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2639" src="https://datatalks.ru/wp-content/uploads/2025/12/i_o_inbound_cpu_inbound.jpeg" alt="" width="706" height="353" srcset="https://datatalks.ru/wp-content/uploads/2025/12/i_o_inbound_cpu_inbound.jpeg 706w, https://datatalks.ru/wp-content/uploads/2025/12/i_o_inbound_cpu_inbound-300x150.jpeg 300w, https://datatalks.ru/wp-content/uploads/2025/12/i_o_inbound_cpu_inbound-450x225.jpeg 450w" sizes="(max-width: 706px) 100vw, 706px" /></a></p>
<h3>I/O-bound</h3>
<p><strong>I/O-bound задача</strong> — это задача, выполнение которой блокируется ожиданием операций ввода-вывода (I/O), например сетевых запросов, чтения/записи на диск или работы с внешними устройствами, и поэтому большая часть времени тратится не на вычисления, а на ожидание завершения этих операций.</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/12/i_o_inbound.jpeg"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2642" src="https://datatalks.ru/wp-content/uploads/2025/12/i_o_inbound.jpeg" alt="" width="1069" height="329" srcset="https://datatalks.ru/wp-content/uploads/2025/12/i_o_inbound.jpeg 1069w, https://datatalks.ru/wp-content/uploads/2025/12/i_o_inbound-300x92.jpeg 300w, https://datatalks.ru/wp-content/uploads/2025/12/i_o_inbound-1024x315.jpeg 1024w, https://datatalks.ru/wp-content/uploads/2025/12/i_o_inbound-768x236.jpeg 768w, https://datatalks.ru/wp-content/uploads/2025/12/i_o_inbound-450x138.jpeg 450w, https://datatalks.ru/wp-content/uploads/2025/12/i_o_inbound-780x240.jpeg 780w" sizes="(max-width: 1069px) 100vw, 1069px" /></a></p>
<h3>CPU-bound</h3>
<p><strong>CPU-bound задача</strong> — это задача, выполнение которой ограничено мощностью центрального процессора (CPU), а не ожиданием ввода-вывода. Время её выполнения определяется главным образом количеством вычислительных операций, которые нужно выполнить CPU, а не тем, сколько времени тратится на ожидание данных из внешних источников.</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/12/cpu_inbound.jpeg"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2643" src="https://datatalks.ru/wp-content/uploads/2025/12/cpu_inbound.jpeg" alt="" width="1068" height="297" srcset="https://datatalks.ru/wp-content/uploads/2025/12/cpu_inbound.jpeg 1068w, https://datatalks.ru/wp-content/uploads/2025/12/cpu_inbound-300x83.jpeg 300w, https://datatalks.ru/wp-content/uploads/2025/12/cpu_inbound-1024x285.jpeg 1024w, https://datatalks.ru/wp-content/uploads/2025/12/cpu_inbound-768x214.jpeg 768w, https://datatalks.ru/wp-content/uploads/2025/12/cpu_inbound-450x125.jpeg 450w, https://datatalks.ru/wp-content/uploads/2025/12/cpu_inbound-780x217.jpeg 780w" sizes="(max-width: 1068px) 100vw, 1068px" /></a></p>
<h1>Многозадачность в Python</h1>
<h2>Concurrency vs Parallelism</h2>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/12/concarency_parallelism_python.jpeg"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2654" src="https://datatalks.ru/wp-content/uploads/2025/12/concarency_parallelism_python.jpeg" alt="" width="1251" height="504" srcset="https://datatalks.ru/wp-content/uploads/2025/12/concarency_parallelism_python.jpeg 1251w, https://datatalks.ru/wp-content/uploads/2025/12/concarency_parallelism_python-300x121.jpeg 300w, https://datatalks.ru/wp-content/uploads/2025/12/concarency_parallelism_python-1024x413.jpeg 1024w, https://datatalks.ru/wp-content/uploads/2025/12/concarency_parallelism_python-768x309.jpeg 768w, https://datatalks.ru/wp-content/uploads/2025/12/concarency_parallelism_python-450x181.jpeg 450w, https://datatalks.ru/wp-content/uploads/2025/12/concarency_parallelism_python-780x314.jpeg 780w" sizes="(max-width: 1251px) 100vw, 1251px" /></a></p>
<ul>
<li><strong>Concurrency</strong> — это управление несколькими задачами в одно и то же время, но не обязательно их одновременное выполнение. Задачи могут выполняться по очереди, создавая иллюзию многозадачности.</li>
<li><strong>Parallelism</strong> — это одновременное выполнение нескольких задач, как правило за счёт использования нескольких ядер CPU.</li>
</ul>
<h2>Критерии выбора подхода &#8212; Multithreading, Multiprocessing или Asyncio</h2>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/12/python_threading_multiprocessing_asyncio.jpeg"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2651" src="https://datatalks.ru/wp-content/uploads/2025/12/python_threading_multiprocessing_asyncio.jpeg" alt="" width="861" height="489" srcset="https://datatalks.ru/wp-content/uploads/2025/12/python_threading_multiprocessing_asyncio.jpeg 861w, https://datatalks.ru/wp-content/uploads/2025/12/python_threading_multiprocessing_asyncio-300x170.jpeg 300w, https://datatalks.ru/wp-content/uploads/2025/12/python_threading_multiprocessing_asyncio-768x436.jpeg 768w, https://datatalks.ru/wp-content/uploads/2025/12/python_threading_multiprocessing_asyncio-450x256.jpeg 450w, https://datatalks.ru/wp-content/uploads/2025/12/python_threading_multiprocessing_asyncio-780x443.jpeg 780w" sizes="(max-width: 861px) 100vw, 861px" /></a></p>
<p><strong>Multiprocessing (многопроцессность)</strong></p>
<ul>
<li>Лучше всего подходит для CPU-bound задач, требующих интенсивных вычислений.</li>
<li>Используется, когда необходимо обойти GIL — каждый процесс имеет собственный интерпретатор Python, что позволяет достичь настоящего параллелизма.</li>
</ul>
<p><strong>Multithreading (многопоточность)</strong></p>
<ul>
<li>Лучше всего подходит для быстрых I/O-bound задач, так как уменьшается частота переключений контекста, и интерпретатор Python дольше остаётся в одном потоке.</li>
<li>Не подходит для CPU-bound задач из-за ограничений GIL.</li>
</ul>
<p><strong>Asyncio (асинхронность)</strong></p>
<ul>
<li>Идеально подходит для медленных I/O-bound задач, таких как длительные сетевые запросы или обращения к базе данных, поскольку эффективно обрабатывает ожидание и хорошо масштабируется.</li>
<li>Не подходит для CPU-bound задач, если вычисления не выносятся в другие процессы.</li>
</ul>
<h1>threading</h1>
<blockquote><p><span style="color: #ff6600;"><strong>threading</strong></span> в CPython — это инструмент для <span style="color: #ff6600;">I/O</span>-параллелизма.</p></blockquote>
<hr />
<h2>Начальный пример Threading</h2>
<p><strong>Модуль threading</strong> предоставляет способ запуска нескольких потоков (меньших единиц процесса) конкурентно внутри одного процесса. Он позволяет создавать и управлять потоками, делая возможным параллельное выполнение задач с разделяемым адресным пространством памяти. Потоки особенно полезны, когда задачи являются <strong>I/O-bound</strong>, например при работе с файлами или выполнении сетевых запросов, где значительная часть времени тратится на ожидание внешних ресурсов.</p>
<p>Типичный сценарий использования <strong>threading</strong> — управление пулом рабочих потоков, которые могут конкурентно обрабатывать несколько задач. Ниже приведён базовый пример создания и запуска потоков с использованием <strong>Thread</strong>:</p><pre class="urvanov-syntax-highlighter-plain-tag">import threading
import time
import random
from datetime import datetime

def crawl(link):
    print(f"crawl запустился для ссылки {link}. Время вызова: {datetime.now()}")
    time.sleep(random.randint(1, 11))  # Блокирующий I/O (имитация сетевого запроса)
    print(f"crawl завершен для {link}. Время вызова: {datetime.now()}")

links = [
    "https://python.org",
    "https://docs.python.org",
    "https://peps.python.org",
]

# Создаём потоки для каждой ссылки
threads = []
for i, link in enumerate(links):
    # Используем `args` для позиционных аргументов и `kwargs` для именованных
    t = threading.Thread(target=crawl, args=(link,), name=f"Thread-{i+1}")
    threads.append(t)

# Запускаем каждый поток
for t in threads:
    t.start()
    print(f'Поток {t} запущен в {datetime.now()}')

# Ожидаем завершения всех потоков
for t in threads:
    t.join()
    print(f'{t} завершен в {datetime.now()}')</pre><p><strong>Результат:</strong></p><pre class="urvanov-syntax-highlighter-plain-tag">crawl запустился для ссылки https://python.org. Время вызова: 2025-12-27 13:38:45.574122
Поток &lt;Thread(Thread-1, started 138519994431168)&gt; запущен в 2025-12-27 13:38:45.574331
crawl запустился для ссылки https://docs.python.org. Время вызова: 2025-12-27 13:38:45.574500
Поток &lt;Thread(Thread-2, started 138519986038464)&gt; запущен в 2025-12-27 13:38:45.574558
crawl запустился для ссылки https://peps.python.org. Время вызова: 2025-12-27 13:38:45.574701
Поток &lt;Thread(Thread-3, started 138519977645760)&gt; запущен в 2025-12-27 13:38:45.574758
crawl завершен для https://python.org. Время вызова: 2025-12-27 13:38:46.574254
&lt;Thread(Thread-1, stopped 138519994431168)&gt; завершен в 2025-12-27 13:38:46.574417
crawl завершен для https://docs.python.org. Время вызова: 2025-12-27 13:38:47.574610
&lt;Thread(Thread-2, stopped 138519986038464)&gt; завершен в 2025-12-27 13:38:47.574773
crawl завершен для https://peps.python.org. Время вызова: 2025-12-27 13:38:47.574813
&lt;Thread(Thread-3, stopped 138519977645760)&gt; завершен в 2025-12-27 13:38:47.574895</pre><p><strong>Результат второго запуска:</strong></p><pre class="urvanov-syntax-highlighter-plain-tag">crawl запустился для ссылки https://python.org. Время вызова: 2025-12-27 13:44:50.159111
Поток &lt;Thread(Thread-1, started 126693911033536)&gt; запущен в 2025-12-27 13:44:50.159203
crawl запустился для ссылки https://docs.python.org. Время вызова: 2025-12-27 13:44:50.159412
Поток &lt;Thread(Thread-2, started 126693902640832)&gt; запущен в 2025-12-27 13:44:50.159460
crawl запустился для ссылки https://peps.python.org. Время вызова: 2025-12-27 13:44:50.159612
Поток &lt;Thread(Thread-3, started 126693894248128)&gt; запущен в 2025-12-27 13:44:50.159679
crawl завершен для https://docs.python.org. Время вызова: 2025-12-27 13:44:57.159525
crawl завершен для https://peps.python.org. Время вызова: 2025-12-27 13:44:58.159735
crawl завершен для https://python.org. Время вызова: 2025-12-27 13:44:59.159292
&lt;Thread(Thread-1, stopped 126693911033536)&gt; завершен в 2025-12-27 13:44:59.159502
&lt;Thread(Thread-2, stopped 126693902640832)&gt; завершен в 2025-12-27 13:44:59.159545
&lt;Thread(Thread-3, stopped 126693894248128)&gt; завершен в 2025-12-27 13:44:59.159565</pre><p><strong>Общая схема start и join в threading:</strong></p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/12/threading_start_join.jpeg"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2690" src="https://datatalks.ru/wp-content/uploads/2025/12/threading_start_join.jpeg" alt="" width="745" height="626" srcset="https://datatalks.ru/wp-content/uploads/2025/12/threading_start_join.jpeg 745w, https://datatalks.ru/wp-content/uploads/2025/12/threading_start_join-300x252.jpeg 300w, https://datatalks.ru/wp-content/uploads/2025/12/threading_start_join-450x378.jpeg 450w" sizes="(max-width: 745px) 100vw, 745px" /></a></p>
<h3>Деталь реализации CPython</h3>
<p>В CPython из-за <strong>глобальной блокировки интерпретатора (GIL)</strong> только один поток может выполнять Python-код в каждый момент времени (хотя некоторые ориентированные на производительность библиотеки могут обходить это ограничение). Если требуется более эффективно использовать вычислительные ресурсы многоядерных машин, рекомендуется использовать <code>multiprocessing</code> или <code>concurrent.futures.ProcessPoolExecutor</code>. Тем не менее, <strong>threading</strong> остаётся подходящей моделью, если нужно одновременно выполнять несколько <strong>I/O-bound задач</strong>.</p>
<h3>GIL и вопросы производительности</h3>
<p>В отличие от модуля <strong>multiprocessing</strong>, который использует отдельные процессы для обхода GIL, модуль <strong>threading</strong> работает внутри одного процесса, а значит все потоки разделяют одно и то же адресное пространство памяти. Однако GIL ограничивает прирост производительности при работе с CPU-bound задачами, поскольку только один поток может выполнять байткод Python одновременно. Несмотря на это, потоки остаются полезным инструментом для достижения конкурентности во многих сценариях.</p>
<p><strong>Начиная с Python 3.13, существуют <code>free-threaded</code> сборки</strong>, в которых GIL может быть отключён, что позволяет добиться настоящего параллельного выполнения потоков. Однако по умолчанию эта возможность недоступна (см. <strong>PEP 703</strong>).</p>
<h2>Жизненный цикл потока</h2>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/12/thread_lifecycle_python.jpeg"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2679" src="https://datatalks.ru/wp-content/uploads/2025/12/thread_lifecycle_python.jpeg" alt="" width="779" height="379" srcset="https://datatalks.ru/wp-content/uploads/2025/12/thread_lifecycle_python.jpeg 779w, https://datatalks.ru/wp-content/uploads/2025/12/thread_lifecycle_python-300x146.jpeg 300w, https://datatalks.ru/wp-content/uploads/2025/12/thread_lifecycle_python-768x374.jpeg 768w, https://datatalks.ru/wp-content/uploads/2025/12/thread_lifecycle_python-450x219.jpeg 450w" sizes="(max-width: 779px) 100vw, 779px" /></a></p>
<p><strong>Жизненным циклом потоков можно управлять с помощью следующих методов:</strong></p>
<ul>
<li><code>start()</code> &#8212; Дает потоку жизнь.</li>
<li><code>run()</code> &#8212; Этот метод представляет действия, которые должны быть выполнены в<br />
потоке.</li>
<li><code>join([timeout])</code> &#8212; Поток, который вызывает этот метод, приостанавливается, ожидая завершения потока, чей метод вызван. Параметр <code>timeout</code> (число с плавающей точкой) позволяет указать время ожидания (в секундах), по истечении которого приостановленный поток продолжает свою работу независимо от завершения потока, чей метод <code>join</code> был вызван. Вызывать <code>join()</code> некоторого потока можно много раз. Поток не может вызвать метод <code>join()</code> самого себя. Также нельзя ожидать завершения еще не запущенного потока. Слово &#171;join&#187; в переводе с английского означает &#171;присоединить&#187;, то есть, метод, вызвавший <code>join()</code>, желает, чтобы поток по завершении присоединился к вызывающему <strong>метод потоку</strong>.</li>
<li><code>getName() </code>&#8212; Возвращает имя потока. Для главного потока это &#171;<code>MainThread</code>&#171;.</li>
<li><code>setName(name)</code> &#8212; Присваивает потоку имя <strong>name</strong>.</li>
<li><code>isAlive()</code> &#8212; Возвращает истину, если поток работает (метод <code>run()</code> уже вызван, но еще не завершился).</li>
<li><code>isDaemon()</code> &#8212; Возвращает истину, если поток имеет <strong>признак демона</strong>. Программа на Python завершается по завершении всех потоков, не являющихся демонами. Главный поток демоном не является.</li>
<li><code>setDaemon(daemonic)</code> &#8212; Устанавливает признак <strong>daemonic</strong> того, что поток является демоном. Начальное значение этого признака заимствуется у потока, запустившего данный. Признак можно изменять только для потоков, которые еще не запущены.</li>
</ul>
<p><strong>Атрибуты потока:</strong></p>
<ul>
<li><code>t.name</code> &#8212; имя потока</li>
<li><code>t.ident</code> &#8212; Уникальный идентификатор потока (ID) &#8212; None, если поток ещё не запущен</li>
<li><code>threading.current_thread()</code> &#8212; Возвращает объект текущего потока</li>
<li><code>t.daemon = True</code> &#8212; Демон-потоки убиваются при завершении главного потока. Используются для фоновых задач, логирования, heartbeat-потоков. daemon нужно задавать до start()</li>
<li><code>threading.active_count()</code> &#8212; Количество активных потоков</li>
<li><code>threading.enumerate()</code> &#8212; Список всех живых потоков</li>
</ul>
<h2>Реализация потокобезопасной записи результатов с Lock, чтобы избежать race condition</h2>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">import threading
import time
import random
from datetime import datetime

def crawl(link, results, lock):
    print(f"Поток {threading.current_thread().name} запущен. Время вызова: {datetime.now()}")

    # Имитация сетевого запроса
    delay = random.randint(1, 10)
    time.sleep(delay)

    # Имитация полученного JSON
    response = {
        "url": link,
        "status": 200,
        "data": {
            "title": f"Данные с {link}",
            "value": random.randint(1, 100),
        },
        "fetched_at": datetime.now().isoformat(),
        "thread": threading.current_thread().name,
    }

    # Потокобезопасная запись результата
    with lock:
        results[link] = response

    print(f"Поток {threading.current_thread().name} завершен. Запрос длился {delay} секунд. Время завершения: {datetime.now()}")


links = [
    "https://python.org",
    "https://docs.python.org",
    "https://peps.python.org",
]

# Общее хранилище результатов
results = {}

# Lock для синхронизации доступа к results
lock = threading.Lock()

# Создаём потоки
threads = []
for i, link in enumerate(links):
    t = threading.Thread(
        target=crawl,
        args=(link, results, lock),
        name=f"Thread-{i + 1}",
    )
    threads.append(t)

# Запускаем потоки
for t in threads:
    t.start()

# Ждём завершения
for t in threads:
    t.join()

# Итоговый объединённый результат
print("\nИТОГОВЫЙ РЕЗУЛЬТАТ:")
for url, data in results.items():
    print(f"{url} → {data}")</pre><p><strong>Что выполняется в коде:</strong></p>
<ul>
<li><code>t.start()</code> &#8212; Создаёт реальный системный поток. Вызывает <strong>crawl(&#8230;)</strong> в новом потоке. Нельзя вызывать <strong>start()</strong> дважды для одного и того же объекта.</li>
<li><code>t.join()</code> &#8212; Блокирует главный поток и ждёт, пока поток t завершится. Гарантирует, что все данные собраны.</li>
<li><code>threading.current_thread().name</code> &#8212; Позволяет узнать, какой поток сейчас выполняется. Используется для логирования и отладки</li>
</ul>
<table>
<thead>
<tr>
<th>Команда</th>
<th>Где используется</th>
<th>Назначение</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>Thread(...)</code></td>
<td>создание потоков</td>
<td>описание задачи</td>
</tr>
<tr>
<td><code>start()</code></td>
<td>запуск</td>
<td>старт выполнения</td>
</tr>
<tr>
<td><code>join()</code></td>
<td>ожидание</td>
<td>синхронизация</td>
</tr>
<tr>
<td><code>current_thread()</code></td>
<td>внутри <code>crawl</code></td>
<td>диагностика</td>
</tr>
<tr>
<td><code>Lock()</code></td>
<td>защита <code>results</code></td>
<td>потокобезопасность</td>
</tr>
</tbody>
</table>
<p><strong>Результат выполнения скрипта:</strong></p><pre class="urvanov-syntax-highlighter-plain-tag">Поток Thread-1 запущен. Время вызова: 2025-12-27 20:45:11.807784
Поток Thread-2 запущен. Время вызова: 2025-12-27 20:45:11.807957
Поток Thread-3 запущен. Время вызова: 2025-12-27 20:45:11.808193
Поток Thread-3 завершен. Запрос длился 1 секунд. Время завершения: 2025-12-27 20:45:12.808385
Поток Thread-1 завершен. Запрос длился 4 секунд. Время завершения: 2025-12-27 20:45:15.808069
Поток Thread-2 завершен. Запрос длился 10 секунд. Время завершения: 2025-12-27 20:45:21.808165

ИТОГОВЫЙ РЕЗУЛЬТАТ:
https://peps.python.org → {'url': 'https://peps.python.org', 'status': 200, 'data': {'title': 'Данные с https://peps.python.org', 'value': 15}, 'fetched_at': '2025-12-27T20:45:12.808319', 'thread': 'Thread-3'}
https://python.org → {'url': 'https://python.org', 'status': 200, 'data': {'title': 'Данные с https://python.org', 'value': 84}, 'fetched_at': '2025-12-27T20:45:15.808019', 'thread': 'Thread-1'}
https://docs.python.org → {'url': 'https://docs.python.org', 'status': 200, 'data': {'title': 'Данные с https://docs.python.org', 'value': 17}, 'fetched_at': '2025-12-27T20:45:21.808113', 'thread': 'Thread-2'}</pre><p></p>
<h2>Основы threading</h2>
<h3>Создание потока через конструктор <code>threading.Thread</code></h3>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">threading.Thread(
    target=None,
    args=(),
    kwargs={},
    name=None,
    daemon=None
)</pre><p>Основные параметры:</p>
<table>
<thead>
<tr>
<th>Параметр</th>
<th>Описание</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>target</code></td>
<td>Функция, которая будет выполнена в потоке</td>
</tr>
<tr>
<td><code>args</code></td>
<td>Кортеж позиционных аргументов</td>
</tr>
<tr>
<td><code>kwargs</code></td>
<td>Именованные аргументы</td>
</tr>
<tr>
<td><code>name</code></td>
<td>Имя потока</td>
</tr>
<tr>
<td><code>daemon</code></td>
<td>Демон-поток (<code>True/False</code>)</td>
</tr>
</tbody>
</table>
<h3>Запуск и управление потоками <code>t.start()</code></h3>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">t.start()</pre><p></p>
<ul>
<li>Запускает поток</li>
<li>Внутри вызывает run()</li>
<li>Нельзя вызвать повторно</li>
</ul>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">t.run()</pre><p></p>
<ul>
<li>Содержит код потока</li>
<li>Не запускает новый поток, если вызвать напрямую</li>
<li>Обычно не вызывается вручную</li>
</ul>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">join(timeout=None)</pre><p></p>
<ul>
<li>Ждёт завершения потока</li>
<li><code>timeout</code> — максимальное время ожидания (в секундах)</li>
</ul>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">t.is_alive()</pre><p></p>
<ul>
<li>Возвращает <code>True</code>, если поток ещё работает</li>
</ul>
<h2>Примитивы синхронизации: Lock, RLock, Semaphore, Event, Condition</h2>
<p><strong>Подробнее:</strong> <a href="https://devpractice.ru/python-lesson-23-concurrency-part-2/" target="_blank" rel="noopener">Python. Урок 23. Потоки и процессы в Python. Часть 2. Синхронизация потоков</a></p>
<hr />
<p>В Python примитивы синхронизации из модуля <code>threading</code> решают одну ключевую задачу: они позволяют нескольким потокам безопасно и предсказуемо взаимодействовать с общим состоянием. Несмотря на наличие <strong>GIL</strong>, эти примитивы остаются необходимыми, потому что <strong>GIL</strong> защищает интерпретатор, но не бизнес-логику и не целостность данных.</p>
<p>Диспетчеры контекста предусмотрены для всех объектов модуля <code>threading</code>, таких как <strong>Lock</strong>, <strong>RLock</strong>, <strong>Condition</strong>, <strong>Semaphore</strong> и <strong>BoundedSemaphore</strong>, то есть для работы с этими объектами может применяться инструкция <code>with</code>.</p>
<p>Начнём с <strong>Lock</strong> и <strong>RLock</strong>, так как они лежат в основе почти всех сценариев синхронизации.</p>
<h3><strong>Lock</strong></h3>
<p><strong>Lock</strong> — это обычный мьютекс, который может быть захвачен только одним потоком в конкретный момент времени. Когда поток вызывает <code>acquire()</code>, он либо сразу получает доступ к критической секции, либо блокируется до тех пор, пока другой поток не освободит <code>lock</code>. После выполнения защищённого участка кода поток обязан вызвать <code>release()</code>. В реальном коде <code>Lock</code> почти всегда используется через контекстный менеджер <code>with</code>, потому что это гарантирует освобождение блокировки даже при исключении. <code>Lock</code> подходит для защиты простых структур данных, таких как <strong>словари</strong>, <strong>списки</strong> или <strong>счётчики</strong>, и <strong>для коротких критических секций</strong>. Важно понимать, что <strong>один и тот же поток не может захватить Lock повторно</strong>: попытка сделать это приведёт к <code>deadlock</code>, когда поток будет ждать самого себя.</p>
<p><strong>Проблема (без Lock)</strong></p><pre class="urvanov-syntax-highlighter-plain-tag">import threading

counter = 0

def increment():
    global counter
    for _ in range(100_000):
        counter += 1

threads = [threading.Thread(target=increment) for _ in range(2)]

for t in threads:
    t.start()
for t in threads:
    t.join()

print(counter)  # <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2757.png" alt="❗" class="wp-smiley" style="height: 1em; max-height: 1em;" /> НЕ гарантировано 200000</pre><p><strong>Решение с Lock</strong></p><pre class="urvanov-syntax-highlighter-plain-tag">import threading

counter = 0
lock = threading.Lock()

def increment():
    global counter
    for _ in range(100_000):
        with lock:          # <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f512.png" alt="🔒" class="wp-smiley" style="height: 1em; max-height: 1em;" /> только один поток внутри
            counter += 1

threads = [threading.Thread(target=increment) for _ in range(2)]

for t in threads:
    t.start()
for t in threads:
    t.join()

print(counter)  # <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> всегда 200000</pre><p><strong>Что гарантирует Lock</strong></p>
<ul>
<li>Только один поток изменяет общее состояние</li>
<li>Нет гонок данных</li>
</ul>
<h3><strong>RLock</strong></h3>
<p>Эта проблема решается с помощью <strong>RLock</strong>, или <strong>reentrant lock</strong>. По сути это мьютекс с учётом владельца. Поток, который уже владеет <code>RLock</code>, может захватить его ещё раз, и <strong>Python</strong> просто увеличит внутренний счётчик захватов. Освобождать такой <code>lock</code> нужно столько же раз, сколько он был захвачен. <code>RLock</code> необходим в более сложных архитектурах, когда функции с защитой <code>lock</code> вызывают друг друга, либо когда публичный метод и внутренний метод используют одну и ту же блокировку. Без <code>RLock</code> такой код почти неизбежно приводит к взаимной блокировке.</p>
<p><strong>Проблема с Lock</strong></p><pre class="urvanov-syntax-highlighter-plain-tag">lock = threading.Lock()

def outer():
    with lock:
        inner()

def inner():
    with lock:   # <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/274c.png" alt="❌" class="wp-smiley" style="height: 1em; max-height: 1em;" /> deadlock: поток уже держит lock
        print("inner")</pre><p><strong>Решение с RLock</strong></p><pre class="urvanov-syntax-highlighter-plain-tag">import threading

lock = threading.RLock()

def outer():
    with lock:
        print("outer")
        inner()

def inner():
    with lock:
        print("inner")

threading.Thread(target=outer).start()</pre><p><strong>Что даёт RLock</strong></p>
<ul>
<li>Один поток может захватывать блокировку несколько раз</li>
<li>Важно для рекурсии и вложенных вызовов</li>
</ul>
<h3><strong>Semaphore</strong></h3>
<p>Следующий важный примитив — <strong>Semaphore</strong>. В отличие от <strong>Lock</strong>, который допускает ровно одного владельца, <code>semaphore</code> разрешает одновременно находиться в критической секции ограниченному числу потоков. При создании семафора задаётся счётчик, который уменьшается при <code>acquire()</code> и увеличивается при <code>release()</code>. Пока счётчик положительный, потоки могут входить без ожидания, а когда он становится равным нулю, все последующие вызовы <code>acquire()</code> блокируются. Семантически <strong>семафор</strong> описывает не владение ресурсом, а <strong>количество доступных слотов</strong>. Это делает его удобным для ограничения параллельного доступа к внешним системам, таким как <strong>база данных</strong>, <strong>пул соединений</strong> или <strong>сторонний API</strong>. В отличие от <strong>Lock</strong>, <strong>семафор</strong> не привязан к конкретному потоку, поэтому важно строго соблюдать баланс <code>acquire()</code> и <code>release()</code>, иначе система либо «утечёт» в блокировку, либо начнёт пускать больше потоков, чем предполагалось.</p>
<p>Пример: максимум 2 потока одновременно</p><pre class="urvanov-syntax-highlighter-plain-tag">import threading
import time

semaphore = threading.Semaphore(2)

def worker(name):
    print(f"{name} ждёт доступ")
    with semaphore:
        print(f"{name} вошёл")
        time.sleep(2)
        print(f"{name} вышел")

threads = [
    threading.Thread(target=worker, args=(f"Thread-{i}",))
    for i in range(5)
]

for t in threads:
    t.start()</pre><p><strong>Что гарантирует Semaphore</strong></p>
<ul>
<li>Не более N потоков внутри секции</li>
<li>Остальные ждут освобождения ресурса</li>
</ul>
<h4><strong>BoundedSemaphore</strong></h4>
<p><strong>BoundedSemaphore</strong> — это вариант семафора из модуля <code>threading</code>, который предназначен для строгого контроля количества одновременных доступов к ресурсу и дополнительно защищает от логических ошибок в коде.</p>
<p>По своей сути <strong>BoundedSemaphore</strong> работает так же, как обычный <strong>Semaphore</strong>: он хранит внутренний счётчик, и поток может войти в критическую секцию, только если счётчик больше нуля. При входе счётчик уменьшается, при выходе увеличивается. Это позволяет ограничить количество потоков, которые одновременно используют общий ресурс.</p>
<p>Ключевое отличие <strong>BoundedSemaphore</strong> от <strong>Semaphore</strong> заключается в том, что он не позволяет превысить начальное значение счётчика. Если вызвать <code>release()</code> больше раз, чем было успешных <code>acquire()</code>, <strong>BoundedSemaphore</strong> выбросит исключение <strong>ValueError</strong>. Обычный <strong>Semaphore</strong> такого не делает и молча увеличивает счётчик, что может привести к незаметным ошибкам и нарушению инвариантов программы.</p>
<p>Таким образом, <strong>BoundedSemaphore</strong> полезен в ситуациях, где важно гарантировать, что количество «освобождений» ресурса строго соответствует количеству его захватов, например при реализации пулов соединений или управлении ограниченными системными ресурсами. Он помогает выявлять ошибки проектирования на раннем этапе, вместо того чтобы позволять программе продолжать работу в некорректном состоянии.</p>
<p>Пример использования <strong>BoundedSemaphore</strong> для ограничения числа одновременных работников:</p><pre class="urvanov-syntax-highlighter-plain-tag">import threading
import time

pool = threading.BoundedSemaphore(2)

def worker(name):
    print(f"{name} пытается войти")
    pool.acquire()
    try:
        print(f"{name} работает")
        time.sleep(1)
    finally:
        pool.release()
        print(f"{name} вышел")

threads = [
    threading.Thread(target=worker, args=(f"Thread-{i}",))
    for i in range(4)
]

for t in threads:
    t.start()
for t in threads:
    t.join()</pre><p>Если в этом примере по ошибке вызвать <code>pool.release()</code> дважды в одном потоке, программа сразу упадёт с <code>ValueError</code>, что явно укажет на ошибку управления ресурсом. Именно это поведение и является главным практическим отличием <strong>BoundedSemaphore</strong> от обычного <strong>Semaphore</strong>.</p>
<h3><strong>Event</strong></h3>
<p><strong>Event</strong> решает другую задачу и не предназначен для защиты критических секций. Это <strong>потокобезопасный флаг</strong>, который может быть установлен или сброшен, и который другие потоки могут проверять или ожидать. Внутренне <code>Event</code> хранит состояние <strong>«установлен»</strong> или <strong>«не установлен»</strong>. Когда поток вызывает <code>wait()</code>, он блокируется до тех пор, пока другой поток не вызовет <code>set()</code>. Если событие уже установлено, <code>wait()</code> возвращается сразу. В отличие от <strong>lock</strong>-ов, событие не «потребляется» при ожидании, и все потоки, ожидающие одного и того же события, будут разбужены одновременно. На практике <strong>Event</strong> чаще всего используется для управления жизненным циклом потоков, например для корректной остановки воркеров или для сигнализации о готовности системы к работе. Это более выразительная и безопасная альтернатива общим флагам и бесконечным циклам с <code>sleep</code>.</p>
<p><strong>Пример: ожидание старта</strong></p><pre class="urvanov-syntax-highlighter-plain-tag">import threading
import time

event = threading.Event()

def worker():
    print("Рабочий поток ждёт сигнал...")
    event.wait()          # <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/23f3.png" alt="⏳" class="wp-smiley" style="height: 1em; max-height: 1em;" /> блокируется
    print("Рабочий поток получил сигнал!")

def starter():
    time.sleep(3)
    print("Сигнал отправлен!")
    event.set()           # <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f6a6.png" alt="🚦" class="wp-smiley" style="height: 1em; max-height: 1em;" /> разблокирует всех

threading.Thread(target=worker).start()
threading.Thread(target=starter).start()</pre><p><strong>Event идеально подходит для</strong></p>
<ul>
<li>старт / стоп сигналов</li>
<li><strong>graceful shutdown</strong> &#8212; (плавное или корректное завершение работы) &#8212; это процесс остановки компьютерной системы (приложения, сервера, контейнера), при котором она успевает выполнить необходимые задачи по очистке и сохранению данных перед полным выключением.</li>
<li>ожидания готовности ресурса</li>
</ul>
<h3><strong>Condition</strong></h3>
<p><strong>Condition</strong> является самым сложным и одновременно самым гибким примитивом синхронизации. Он объединяет в себе <strong>мьютекс</strong> и <strong>механизм ожидания уведомлений</strong>. Идея <strong>Condition</strong> заключается в том, что поток может ждать не просто сигнала, а выполнения определённого логического условия, связанного с состоянием программы. Поток захватывает условие, проверяет состояние, и если оно не удовлетворяет требованиям, вызывает <code>wait()</code>. При этом <strong>lock</strong> временно освобождается, чтобы другие потоки могли изменить состояние. Когда другой поток вызывает <code>notify()</code> или <code>notify_all()</code>, ожидающие потоки пробуждаются и снова проверяют условие. Именно повторная проверка условия является ключевым моментом, так как пробуждение не гарантирует, что состояние действительно изменилось нужным образом. <strong>Condition</strong> активно используется в классических <strong>паттернах producer–consumer</strong>, <strong>очередях задач</strong> и системах, где потоки должны реагировать на изменение общего состояния, а не просто на факт события.</p>
<p>Если смотреть на эти примитивы как на систему, то <strong>Lock</strong> и <strong>RLock</strong> отвечают за эксклюзивный доступ, <strong>Semaphore</strong> ограничивает параллелизм, <strong>Event</strong> передаёт сигналы между потоками, а <strong>Condition</strong> позволяет потокам координироваться на основе сложных условий. В продакшене выбор примитива почти всегда диктуется смыслом задачи, а не техническими деталями. Хорошая синхронизация делает код не только корректным, но и читаемым, потому что по выбранному примитиву сразу понятно, как именно потоки должны взаимодействовать друг с другом.</p>
<p><strong>Producer / Consumer с Condition</strong></p><pre class="urvanov-syntax-highlighter-plain-tag">import threading
import time
import random

condition = threading.Condition()
queue = []
MAX_ITEMS = 5

def producer():
    for i in range(10):
        time.sleep(random.uniform(0.1, 0.5))
        with condition:
            while len(queue) &gt;= MAX_ITEMS:
                condition.wait()   # <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/23f3.png" alt="⏳" class="wp-smiley" style="height: 1em; max-height: 1em;" /> ждём, пока потребитель заберёт
            queue.append(i)
            print(f"Producer добавил {i}")
            condition.notify()     # <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f514.png" alt="🔔" class="wp-smiley" style="height: 1em; max-height: 1em;" /> сигнал потребителю

def consumer():
    for _ in range(10):
        with condition:
            while not queue:
                condition.wait()   # <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/23f3.png" alt="⏳" class="wp-smiley" style="height: 1em; max-height: 1em;" /> ждём данные
            item = queue.pop(0)
            print(f"Consumer забрал {item}")
            condition.notify()     # <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f514.png" alt="🔔" class="wp-smiley" style="height: 1em; max-height: 1em;" /> сигнал производителю
        time.sleep(random.uniform(0.2, 0.6))

threading.Thread(target=producer).start()
threading.Thread(target=consumer).start()</pre><p><strong>Condition = Lock + Event</strong></p>
<ul>
<li><code>wait()</code> → отпускает lock и ждёт</li>
<li><code>notify()</code> → будит ожидающий поток</li>
<li>Позволяет ждать логических условий, а не просто блокировки</li>
</ul>
<h3><strong>Barrier</strong></h3>
<p><code>threading.Barrier</code> — это примитив синхронизации в Python, который позволяет группе потоков одновременно ожидать друг друга в определенной точке выполнения (контрольной точке) перед тем, как продолжить работу.</p>
<p>todo</p>
<h3><strong>Итоговая таблица</strong></h3>
<table>
<thead>
<tr>
<th>Примитив</th>
<th>Для чего</th>
</tr>
</thead>
<tbody>
<tr>
<td><code inline="">Lock</code></td>
<td>Простая защита общего состояния</td>
</tr>
<tr>
<td><code inline="">RLock</code></td>
<td>Вложенные / рекурсивные блокировки</td>
</tr>
<tr>
<td><code inline="">Semaphore</code></td>
<td>Ограничение количества потоков</td>
</tr>
<tr>
<td><code inline="">Event</code></td>
<td>Сигналы между потоками</td>
</tr>
<tr>
<td><code inline="">Condition</code></td>
<td>Сложная координация и ожидание условий</td>
</tr>
</tbody>
</table>
<h1>concurrent.futures</h1>
<p>todo</p>
<h1>multiprocessing</h1>
<p>Теория<br />
Разница между:<br />
fork / spawn / forkserver</p>
<p>IPC (межпроцессное взаимодействие):<br />
Queue<br />
Pipe<br />
Manager<br />
Стоимость сериализации (pickle)<br />
Copy-on-write (Linux)</p>
<p>Практика<br />
Параллельная обработка данных<br />
Использование multiprocessing.Pool</p>
<p>Бенчмарк:<br />
threading vs multiprocessing<br />
Поймать баг с pickling’ом</p>
<p><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4cc.png" alt="📌" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Продакшн insight: multiprocessing часто убивает latency, если использовать бездумно.</p>
<h1>asyncio</h1>
<p>Ключевая тема для highload backend.</p>
<p>Теория<br />
Event loop<br />
Coroutine<br />
Awaitable<br />
Task vs Future<br />
Cooperative multitasking<br />
Почему async ≠ threading</p>
<p>Практика<br />
Переписать синхронный код в async<br />
Одновременные HTTP-запросы (aiohttp)<br />
Ограничение параллелизма (Semaphore)</p>
<p>Ошибки:<br />
blocking call внутри async<br />
забытый await</p>
<p>Критически важно: понимание, почему один blocking вызов убивает весь сервис.</p>
<p>Сообщение <a href="https://datatalks.ru/python-threading-multiprocessing-asyncio/">Python &#8212; Многозадачность, конкурентность и асинхронность</a> появились сначала на <a href="https://datatalks.ru">DataTalks.RU. Data Engineering / DWH / Data Pipeline</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://datatalks.ru/python-threading-multiprocessing-asyncio/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>PySpark Interview &#8212; Вопросы и ответы</title>
		<link>https://datatalks.ru/pyspark-interview-questions-and-answers/</link>
					<comments>https://datatalks.ru/pyspark-interview-questions-and-answers/#respond</comments>
		
		<dc:creator><![CDATA[Data Engineer (Admin)]]></dc:creator>
		<pubDate>Thu, 25 Dec 2025 17:48:14 +0000</pubDate>
				<category><![CDATA[Apache Spark]]></category>
		<category><![CDATA[Apache PySpark]]></category>
		<category><![CDATA[pyspark]]></category>
		<guid isPermaLink="false">https://datatalks.ru/?p=2580</guid>

					<description><![CDATA[<p>Basic PySpark Interview Questions Каковы основные преимущества использования PySpark по сравнению с традиционным Python для обработки больших данных? PySpark, Python API для Apache Spark, предлагает несколько преимуществ по сравнению с традиционным Python для обработки больших данных. К ним относятся масштабируемость для работы с массивными наборами данных, высокая производительность за счёт параллельной обработки, отказоустойчивость для обеспечения [&#8230;]</p>
<p>Сообщение <a href="https://datatalks.ru/pyspark-interview-questions-and-answers/">PySpark Interview &#8212; Вопросы и ответы</a> появились сначала на <a href="https://datatalks.ru">DataTalks.RU. Data Engineering / DWH / Data Pipeline</a>.</p>
]]></description>
										<content:encoded><![CDATA[<h1>Basic PySpark Interview Questions</h1>
<h2>Каковы основные преимущества использования PySpark по сравнению с традиционным Python для обработки больших данных?</h2>
<p><strong>PySpark</strong>, <strong>Python API</strong> для <strong>Apache Spark</strong>, предлагает несколько преимуществ по сравнению с традиционным Python для обработки больших данных. К ним относятся масштабируемость для работы с массивными наборами данных, высокая производительность за счёт параллельной обработки, отказоустойчивость для обеспечения надёжности данных, а также интеграция с другими инструментами для работы с большими данными внутри экосистемы Apache.</p>
<h2>Как создать SparkSession в PySpark? Каковы его основные назначения?</h2>
<p>В PySpark <strong>SparkSession</strong> является точкой входа для использования функциональности Spark и создаётся с помощью API <code>SparkSession.builder</code>. Его основные назначения включают взаимодействие с <strong>Spark SQL</strong> для обработки структурированных данных, создание <strong>DataFrame</strong>, конфигурирование свойств Spark, а также управление жизненным циклом <strong>SparkContext</strong> и <strong>SparkSession</strong>. Ниже приведён пример того, как может быть создан <strong>SparkSession</strong>:</p><pre class="urvanov-syntax-highlighter-plain-tag">from pyspark.sql import SparkSession
     
spark = SparkSession.builder \
         .appName("MySparkApp") \
         .master("local[*]") \
         .getOrCreate()</pre><p></p>
<h2>Опиши различные способы чтения данных в PySpark.</h2>
<p><strong>PySpark</strong> поддерживает чтение данных из различных источников, таких как <strong>CSV</strong>, <strong>Parquet</strong> и <strong>JSON</strong>, среди прочих. Для этой цели он предоставляет разные методы, включая <code>spark.read.csv()</code>, <code>spark.read.parquet()</code>, <code>spark.read.json()</code>, <code>spark.read.format()</code> и <code>spark.read.load()</code>. Ниже приведён пример того, как данные могут быть прочитаны в PySpark:</p><pre class="urvanov-syntax-highlighter-plain-tag">df_from_csv = spark.read.csv("my_file.csv", header=True)
df_from_parquet = spark.read.parquet("my_file.parquet")
df_from_json = spark.read.json("my_file.json")</pre><p></p>
<h2>Как обрабатывать пропущенные данные в PySpark?</h2>
<p>В PySpark пропущенные данные можно обрабатывать с использованием нескольких методов. Можно удалять строки или столбцы, содержащие пропущенные значения, с помощью метода <code>.dropna()</code>. Также можно заполнять пропущенные данные конкретным значением или использовать методы интерполяции с помощью <code>.fillna()</code>. Кроме того, можно выполнять импутацию пропущенных значений с использованием статистических методов, таких как среднее значение или медиана, применяя <code>Imputer</code>. Ниже приведён пример обработки пропущенных данных в PySpark:</p><pre class="urvanov-syntax-highlighter-plain-tag"># Как удалить строки
df_from_csv.dropna(how="any")

# Как заполнить пропущенные значения константой
df_from_parquet.fillna(value=2)

# Как выполнить импутацию значений медианой
from pyspark.ml.feature import Imputer
imputer = Imputer(strategy="median", inputCols=["price","rooms"], outputCols=["price_imputed","rooms_imputed"])
model = imputer.fit(df_from_json)
df_imputed = model.transform(df_from_json)</pre><p></p>
<h2>Как можно кэшировать данные в PySpark для повышения производительности?</h2>
<p>Одним из преимуществ PySpark является возможность использовать методы <code>.cache()</code> или <code>.persist()</code> для хранения данных в памяти или на заданном уровне хранения. Это улучшает производительность за счёт предотвращения повторных вычислений и снижения необходимости сериализации и десериализации данных. Ниже приведён пример того, как кэшировать данные в PySpark:</p><pre class="urvanov-syntax-highlighter-plain-tag"># Как кэшировать данные в памяти
df_from_csv.cache()

# Как сохранить данные на локальном диске
df_from_csv.persist(storageLevel=StorageLevel.DISK_ONLY)</pre><p>При использовании <code>cache()</code> применяется только уровень хранения по умолчанию:</p>
<ul>
<li><code>MEMORY_ONLY</code> для <strong>RDD</strong></li>
<li><code>MEMORY_AND_DISK</code> для <strong>Dataset</strong></li>
</ul>
<p>При использовании <code>persist()</code> вы можете указать нужный уровень хранения как для <strong>RDD</strong>, так и для <strong>Dataset</strong>.</p>
<p><strong>Из официальной документации:</strong></p>
<ul>
<li>Вы можете пометить <strong>RDD</strong> для сохранения с помощью методов <code>persist()</code> или <code>cache()</code>.</li>
<li>Каждый сохранённый <strong>RDD</strong> может храниться с использованием разного уровня хранения.</li>
<li>Метод <code>cache()</code> — это сокращённая форма использования уровня хранения по умолчанию, а именно <code>StorageLevel.MEMORY_ONLY</code> (хранение десериализованных объектов в памяти).</li>
</ul>
<p>Используйте <code>persist()</code>, если вы хотите назначить уровень хранения, отличный от:</p>
<ul>
<li><code>MEMORY_ONLY</code> для <strong>RDD</strong></li>
<li>или <code>MEMORY_AND_DISK</code> для <strong>Dataset</strong></li>
</ul>
<p><strong>Подробнее почитать:</strong></p>
<ul>
<li><a href="https://sparkbyexamples.com/spark/spark-difference-between-cache-and-persist/" target="_blank" rel="noopener">Spark Difference between Cache and Persist?</a></li>
</ul>
<h2>Опиши выполнение соединений в PySpark</h2>
<p><strong>PySpark</strong> позволяет выполнять несколько типов соединений, таких как <strong>inner</strong>, <strong>outer</strong>, <strong>left</strong> и <strong>right</strong>. Используя метод <code>.join()</code>, можно задать условие соединения через параметр on и тип соединения через параметр how, как показано в примере:</p><pre class="urvanov-syntax-highlighter-plain-tag"># Как выполнить inner join двух наборов данных
df_from_csv.join(df_from_json, on="id", how="inner")

# Как выполнить outer join наборов данных
df_from_json.join(df_from_parquet, on="product_id", how="outer")</pre><p></p>
<h2>В чём заключаются ключевые различия между RDD, DataFrame и Dataset в PySpark?</h2>
<p><strong>Spark Resilient Distributed Dataset (RDD)</strong>, <strong>DataFrame</strong> и <strong>Dataset</strong> являются ключевыми абстракциями в Spark, которые позволяют работать со структурированными данными в распределённой вычислительной среде. Несмотря на то что все они представляют данные, между ними существуют важные различия.</p>
<p><strong>RDD</strong> являются низкоуровневыми API, не имеющими схемы и предоставляющими полный контроль над данными; они представляют собой неизменяемые коллекции объектов.</p>
<p><strong>DataFrame</strong> являются высокоуровневыми API, построенными поверх <strong>RDD</strong> и оптимизированными для производительности, но не обладающими типобезопасностью; они организуют структурированные и полуструктурированные данные в именованные столбцы.</p>
<p><strong>Dataset</strong> объединяют преимущества <strong>RDD</strong> и <strong>DataFrame</strong>, являясь высокоуровневыми API, которые предоставляют типобезопасную абстракцию; они поддерживают <strong>Python</strong> и <strong>Scala</strong>, обеспечивают проверку типов во время компиляции и при этом работают быстрее, чем DataFrame.</p>
<h2>Объясни концепцию ленивых вычислений в PySpark. Как она влияет на производительность?</h2>
<p><strong>PySpark</strong> реализует стратегию, называемую <strong>ленивыми вычислениями</strong>, при которой преобразования, применяемые к распределённым наборам данных, таким как <strong>RDD</strong>, <strong>DataFrame</strong> или <strong>Dataset</strong>, не выполняются немедленно. Вместо этого Spark строит последовательность операций или преобразований, которые должны быть выполнены над данными, называемую ориентированным ациклическим графом, или DAG. Такой подход улучшает производительность и оптимизирует выполнение, поскольку вычисления откладываются до момента, когда вызывается действие и их выполнение становится действительно необходимым.</p>
<h2>Какова роль партиционирования в PySpark и каким образом оно может улучшить производительность?</h2>
<p>В <strong>PySpark</strong> партиционирование данных является ключевой возможностью, которая помогает равномерно распределять нагрузку между узлами кластера. Партиционирование означает разделение данных на более мелкие части, называемые партициями, которые обрабатываются независимо и параллельно на разных узлах кластера.</p>
<p>Это повышает производительность за счёт параллельной обработки, уменьшения перемещения данных и более эффективного использования ресурсов. Управлять партиционированием можно с помощью таких методов, как <code>.repartition()</code> и <code>.coalesce()</code>.</p>
<h2>Объясни концепцию широковещательных переменных в PySpark и приведи пример использования</h2>
<p><strong>Широковещательные переменные</strong> являются важной возможностью распределённых вычислительных фреймворков Spark.</p>
<p>В PySpark это разделяемые переменные только для чтения, которые кэшируются и распространяются по узлам кластера для того, чтобы избежать операций <strong>shuffle</strong>. Они могут быть особенно полезны в распределённых приложениях машинного обучения, которым необходимо использовать и загружать предварительно обученную модель. В этом случае модель передаётся как широковещательная переменная, что помогает сократить накладные расходы на передачу данных и повысить производительность.</p>
<h2>В чём различия между PySpark и pandas?</h2>
<p><strong>PySpark</strong> и <strong>pandas</strong> оба широко используются для обработки данных, однако между ними существуют ключевые различия. <strong>PySpark</strong> ориентирован на масштабируемость и предназначен для работы с большими данными и распределённой обработки, тогда как <strong>pandas</strong> подходит для относительно небольших наборов данных, которые помещаются в память.</p>
<p>С точки зрения производительности PySpark выполняет параллельные вычисления на уровне кластера, что делает его значительно быстрее при работе с большими объёмами данных по сравнению с pandas, который работает на одной машине. С точки зрения удобства использования pandas проще для разведочного анализа данных, тогда как PySpark более сложен, но при этом сильно оптимизирован для распределённых вычислений.</p>
<h2>Как можно преобразовать DataFrame из pandas в PySpark DataFrame и обратно?</h2>
<p><strong>DataFrame pandas</strong> можно преобразовать в <strong>PySpark DataFrame</strong> с помощью метода <code>spark.createDataFrame()</code>, а <strong>PySpark</strong> <strong>DataFrame</strong> обратно в <strong>pandas DataFrame</strong> с помощью метода <code>.toPandas()</code>.</p><pre class="urvanov-syntax-highlighter-plain-tag">import pandas as pd
from pyspark.sql import SparkSession

# Инициализация SparkSession
spark = SparkSession.builder.appName("Example").getOrCreate()

# Создание Pandas DataFrame
pdf = pd.DataFrame({'id': [1, 2, 3], 'value': [10, 20, 30]})

# Преобразование в PySpark DataFrame
df_spark = spark.createDataFrame(pdf)

# Обратное преобразование в Pandas DataFrame
pdf_new = df_spark.toPandas()</pre><p></p>
<h1>Intermediate PySpark Interview Questions</h1>
<p>Рассмотрев основы, перейдём к вопросам для собеседования по PySpark среднего уровня, которые глубже затрагивают архитектуру и модель выполнения приложений Spark.</p>
<h2>Что такое Spark Driver и каковы его обязанности?</h2>
<p><strong>Spark Driver</strong> — это основной процесс, который координирует выполнение приложений Spark, распределяя задачи по кластеру. Он взаимодействует с менеджером кластера для выделения ресурсов, планирования задач и мониторинга выполнения Spark-задач (Tasks).</p>
<h2>Что такое Spark DAG?</h2>
<p><strong>Ориентированный ациклический граф (DAG)</strong> в Spark является ключевым понятием, поскольку он представляет <strong>логическую модель выполнения Spark</strong>. Он называется ориентированным, потому что каждая вершина представляет преобразование, выполняемое в определённом порядке, заданном рёбрами. Он является ациклическим, так как в плане выполнения отсутствуют циклы или петли. Этот план оптимизируется с использованием конвейерных преобразований, объединения задач и проталкивания предикатов.</p>
<h2>Какие типы менеджеров кластеров доступны в Spark?</h2>
<p>В настоящее время Spark поддерживает несколько менеджеров кластеров для управления ресурсами и планирования заданий.</p>
<ul>
<li>К ним относится <strong>Standalone</strong> — простой менеджер кластера, встроенный в Spark.</li>
<li><strong>Hadoop YARN</strong> — универсальный менеджер в экосистеме Hadoop, используемый для планирования заданий и управления ресурсами.</li>
<li><strong>Kubernetes</strong> применяется для автоматизации, развёртывания, масштабирования и управления контейнеризованными приложениями.</li>
<li><strong>Apache Mesos</strong> — распределённая система, используемая для управления ресурсами на уровне приложений.</li>
</ul>
<h2>Опиши, как реализовать пользовательское преобразование в PySpark</h2>
<p>Для реализации пользовательского преобразования в <strong>PySpark</strong> можно определить Python-функцию, которая работает с PySpark <strong>DataFrame</strong>, а затем использовать метод <code>.transform()</code> для вызова этого преобразования. Ниже приведён пример реализации пользовательского преобразования в PySpark:</p><pre class="urvanov-syntax-highlighter-plain-tag"># Определение Python-функции, работающей с PySpark DataFrame
def get_discounted_price(df):
    return df.withColumn("discounted_price", \
                          df.price - (df.price * df.discount) / 100) 

# Вызов преобразования
df_discounted = df_from_csv.transfrom(get_discounted_price)</pre><p></p>
<h2>Объясни концепцию оконных функций в PySpark и приведи пример</h2>
<p>Оконные функции в <strong>PySpark</strong> позволяют применять операции к окну строк, возвращая одно значение для каждой входной строки. С их помощью можно выполнять ранжирование, аналитические операции и агрегатные функции. Ниже приведён пример применения оконной функции в <strong>PySpark</strong>:</p><pre class="urvanov-syntax-highlighter-plain-tag">from pyspark.sql.window import Window
from pyspark.sql.functions import row_number

# Определение оконной функции
window = Window.orderBy("discounted_price")

# Применение оконной функции
df = df_from_csv.withColumn("row_number", row_number().over(window))</pre><p></p>
<h2>Как обрабатывать ошибки и исключения в PySpark?</h2>
<p>Одним из наиболее полезных способов обработки ошибок и исключений в преобразованиях и действиях PySpark является оборачивание кода в блоки try-except для их перехвата. В RDD можно использовать операцию <code>foreach</code> для итерации по элементам и обработки исключений.</p>
<h2>Каково назначение чекпойнтов в PySpark?</h2>
<p>В <strong>PySpark</strong> чекпойнтинг означает сохранение <strong>RDD</strong> на диск, чтобы к этой промежуточной точке можно было обратиться в будущем вместо повторного вычисления RDD из исходного источника. Чекпойнты обеспечивают возможность восстановления после сбоев, поскольку драйвер может быть перезапущен с использованием ранее вычисленного состояния.</p>
<h2>Как PySpark выполняет вывод схемы и как можно задать схему явно?</h2>
<p><strong>PySpark</strong> автоматически выводит схему при загрузке структурированных данных, однако для лучшего контроля и повышения эффективности можно явно определить схему с помощью <strong>StructType</strong> и <strong>StructField</strong>.</p><pre class="urvanov-syntax-highlighter-plain-tag">from pyspark.sql.types import StructType, StructField, IntegerType, StringType

schema = StructType([
    StructField("id", IntegerType(), True),
    StructField("name", StringType(), True)
])

df = spark.read.csv("data.csv", schema=schema, header=True)</pre><p></p>
<h1>Advanced PySpark Interview Questions</h1>
<p>Для тех, кто претендует на более старшие позиции или стремится продемонстрировать более глубокое понимание <strong>PySpark</strong>, далее рассматриваются продвинутые вопросы для собеседования, которые углубляются в тонкости преобразований и оптимизаций внутри экосистемы <strong>PySpark</strong>.</p>
<h2>Объясни различия между узкими и широкими преобразованиями в PySpark</h2>
<p>В <strong>PySpark</strong> узкие преобразования выполняются тогда, когда каждый входной партиции соответствует не более одной выходной партиции и при этом не требуется выполнение <strong>shuffle</strong>. К таким преобразованиям относятся, например, <code>map()</code>, <code>filter()</code> и <code>union()</code>. В противоположность этому, широкие преобразования необходимы для операций, при которых каждая входная партиция может вносить вклад в несколько выходных партиций, и при этом требуется перераспределение данных, выполнение соединений или агрегаций. Примерами таких преобразований являются <code>groupBy()</code>, <code>join()</code> и <code>sortBy()</code>.</p>
<h2>Что такое оптимизатор Catalyst в Spark и как он работает?</h2>
<p>В Spark оптимизатор <strong>Catalyst</strong> является компонентом <strong>Spark SQL</strong>, основанным на правилах и предназначенным для оптимизации производительности запросов. Его основная задача заключается в преобразовании и улучшении SQL-запросов или операций <strong>DataFrame</strong>, заданных пользователем, с целью генерации эффективного физического плана выполнения, адаптированного под конкретный запрос и характеристики набора данных.</p>
<h2>Опиши, как реализовать пользовательские агрегации в PySpark</h2>
<p>Для реализации пользовательских агрегаций в <strong>PySpark</strong> можно совместно использовать методы <code>groupBy()</code> и <code>agg()</code>. Внутри вызова <code>agg()</code> можно передавать различные функции из модуля <code>pyspark.sql.functions</code>. Кроме того, можно применять пользовательские агрегации <strong>Pandas</strong> к группам внутри <strong>PySpark</strong> <strong>DataFrame</strong> с помощью метода <code>.applyInPandas()</code>. Ниже приведён пример реализации пользовательских агрегаций в <strong>PySpark</strong>:</p><pre class="urvanov-syntax-highlighter-plain-tag"># Использование groupBy и agg с функциями
from pyspark.sql import functions as F
df_from_csv.groupBy("house_id").agg(F.mean("price_discounted"))

# Использование applyInPandas
def normalize_price(df):
    disc_price = df["discounted_price"]
    df["normalized_price"] = disc_price.mean() / disc_price.std()

df_from_csv.groupBy("house_id").applyInPandas(normalize_price)</pre><p></p>
<h2>С какими трудностями ты сталкивался при работе с большими наборами данных в PySpark и как ты их преодолевал?</h2>
<p>С помощью этого вопроса можно обратиться к собственному опыту и рассказать о конкретном случае, в котором возникали сложности при работе с <strong>PySpark</strong> и большими наборами данных, которые могут включать следующее:</p>
<ul>
<li>Управление памятью и использование ресурсов.</li>
<li>Перекос данных и неравномерное распределение нагрузки.</li>
<li>Оптимизация производительности, особенно для широких преобразований и операций shuffle.</li>
<li>Отладка и устранение неисправностей сложных сбоев заданий.</li>
<li>Эффективное партиционирование и хранение данных.</li>
</ul>
<p>Для преодоления этих проблем <strong>PySpark</strong> предоставляет возможности партиционирования наборов данных, кэширования промежуточных результатов, использования встроенных техник оптимизации, надёжного управления кластером и применения механизмов отказоустойчивости.</p>
<h2>Как интегрировать PySpark с другими инструментами и технологиями в экосистеме больших данных?</h2>
<p>PySpark обладает тесной интеграцией с различными инструментами для работы с большими данными, включая <strong>Hadoop</strong>, <strong>Hive</strong>, <strong>Kafka</strong> и <strong>HBase</strong>, а также с облачными хранилищами, такими как <strong>AWS S3</strong> и <strong>Google Cloud Storage</strong>.</p>
<p>Такая интеграция осуществляется с использованием встроенных коннекторов, библиотек и API, предоставляемых PySpark.</p>
<h2>Какие лучшие практики существуют для тестирования и отладки приложений PySpark?</h2>
<p>К числу рекомендуемых лучших практик для тестирования и отладки приложений <strong>PySpark</strong> относятся написание модульных тестов с использованием <code>pyspark.sql.test.SQLTestUtils</code> совместно с <strong>Python-библиотеками</strong>, такими как <code>pytest</code>, отладка приложений и логирование сообщений с помощью библиотеки <code>logging</code>, а также <strong>Spark UI</strong>, и оптимизация производительности с использованием API Spark <code>org.apache.spark.metrics</code> и инструментов мониторинга производительности.</p>
<h2>Как бы ты решал вопросы безопасности и конфиденциальности данных в среде PySpark?</h2>
<p>В настоящее время обмен данными стал значительно проще, поэтому защита чувствительной и конфиденциальной информации является важным способом предотвращения утечек данных. Одной из лучших практик является применение шифрования данных во время обработки и хранения. В <strong>PySpark</strong> этого можно добиться, используя функции <code>aes_encrypt()</code> и <code>aes_decrypt()</code> для столбцов <strong>DataFrame</strong>. Также для достижения этой цели можно использовать сторонние библиотеки, такие как библиотека <code>cryptography</code>.</p>
<h2>Опиши, как использовать PySpark для построения и развёртывания модели машинного обучения</h2>
<p><strong>PySpark</strong> предоставляет библиотеку <strong>MLlib</strong> — масштабируемую библиотеку машинного обучения для построения и развёртывания моделей машинного обучения на больших наборах данных. API этой библиотеки может использоваться на различных этапах ML-процесса, таких как предварительная обработка данных, инженерия признаков, обучение модели, оценка качества и развёртывание. Используя кластеры <strong>Spark</strong>, можно развёртывать модели машинного обучения на базе <strong>PySpark</strong> в промышленной среде, применяя <strong>пакетный</strong> или <strong>потоковый инференс</strong>.</p>
<h2>Как можно оптимизировать операции shuffle в PySpark?</h2>
<p><strong>Операции <code>shuffle</code></strong> возникают, когда данные перераспределяются между партициями, и они могут быть затратными с точки зрения производительности. Для оптимизации <code>shuffle</code> можно применять следующие подходы:</p>
<ul>
<li>Стратегически использовать <code>repartition()</code> для балансировки партиций перед затратными операциями, такими как <code>join</code>.</li>
<li>Отдавать предпочтение <code>coalesce()</code> вместо <code>repartition()</code> при уменьшении количества партиций, поскольку это минимизирует перемещение данных.</li>
<li>Выполнять широковещательные соединения небольших таблиц с помощью <code>broadcast()</code> перед соединением с большими таблицами, чтобы избежать операций, интенсивно использующих <code>shuffle</code>.</li>
<li>Настраивать конфигурации <strong>Spark</strong>, такие как <code>spark.sql.shuffle.partitions</code>, для оптимизации количества партиций при <strong>shuffle-операциях</strong>.</li>
</ul>
<h1>Вопросы для собеседования по PySpark для Data Engineer</h1>
<p>Если вы проходите собеседование на позицию инженера данных, ожидайте вопросы, которые оценивают вашу способность проектировать, оптимизировать и устранять проблемы в приложениях <strong>PySpark</strong> в промышленной среде.</p>
<p>Ниже приведены типичные вопросы, с которыми можно столкнуться.</p>
<h2>Опиши, как бы ты оптимизировал задание PySpark, которое работает медленно. На какие ключевые факторы ты бы обратил внимание?</h2>
<p>Если задание PySpark работает медленно, существует несколько аспектов, которые можно улучшить для оптимизации его производительности:</p>
<ul>
<li>Обеспечение корректного размера и количества партиций данных для минимизации перераспределения данных во время преобразований.</li>
<li>Использование <strong>DataFrame</strong> вместо <strong>RDD</strong>, поскольку они уже используют несколько модулей оптимизации для повышения производительности рабочих нагрузок <strong>Spark</strong>.</li>
<li>Использование <strong>широковещательных соединений</strong> и <strong>широковещательных переменных</strong> при соединении небольшого набора данных с большим набором данных.</li>
<li>Кэширование и сохранение промежуточных <strong>DataFrame</strong>, которые используются повторно.</li>
<li>Настройка количества партиций, ядер исполнителей и числа экземпляров для эффективного использования ресурсов кластера.</li>
</ul>
<p>Выбор подходящих форматов файлов для уменьшения объёма данных.</p>
<h2>Как обеспечить отказоустойчивость в приложениях PySpark?</h2>
<p>Для обеспечения отказоустойчивости в приложениях PySpark можно использовать несколько стратегий:</p>
<ul>
<li>Использование чекпойнтинга для сохранения данных в определённых точках.</li>
<li>Репликация данных путём их сохранения на разных машинах.</li>
<li>Ведение журнала изменений, выполняемых над данными до их применения.</li>
<li>Выполнение проверок валидации данных для выявления ошибок.</li>
<li>Выбор корректного уровня сохранения данных.</li>
</ul>
<p>Использование встроенных механизмов отказоустойчивости Spark для автоматического повторного выполнения задач, которые завершились с ошибкой.</p>
<h2>Какие существуют способы развертывания и управления приложениями PySpark?</h2>
<p>Мы можем развертывать и управлять приложениями PySpark с помощью следующих инструментов:</p>
<ul>
<li><strong>YARN:</strong> менеджер ресурсов, который помогает развертывать и управлять приложениями в Hadoop-кластерах.</li>
<li><strong>Kubernetes:</strong> Spark предоставляет поддержку для развертывания приложений в кластерах Kubernetes.</li>
<li><strong>Databricks:</strong> предоставляет полностью управляемую платформу для приложений PySpark, абстрагируя сложность управления кластерами.</li>
</ul>
<h2>Как вы бы мониторили и устраняли проблемы в заданиях PySpark, работающих в production-среде?</h2>
<p><strong>PySpark</strong> предоставляет следующие инструменты для мониторинга и устранения проблем заданий, работающих в <strong>production-среде</strong>:</p>
<ul>
<li><strong>Spark UI:</strong> веб-интерфейс, который помогает отслеживать прогресс выполнения заданий, использование ресурсов и выполнение задач.</li>
<li><strong>Логирование:</strong> мы можем настроить логирование для сбора детальной информации об ошибках и предупреждениях.</li>
<li><strong>Метрики:</strong> мы можем использовать системы мониторинга для сбора и анализа метрик, связанных с состоянием кластера и производительностью заданий.</li>
</ul>
<h2>Объясните разницу между динамическим и статическим распределением ресурсов в Spark и в каких случаях вы бы выбрали каждый из них</h2>
<p>В <strong>Spark</strong> статическое распределение ресурсов означает предварительное и постоянное выделение фиксированных ресурсов, таких как память и количество ядер <strong>executors</strong>, на всё время выполнения приложения. В отличие от этого, динамическое распределение ресурсов позволяет Spark динамически изменять количество <strong>executors</strong> в зависимости от нагрузки. Ресурсы могут добавляться или удаляться по мере необходимости, что улучшает использование ресурсов и снижает затраты.</p>
<h2>Как вы принимаете решение между использованием DataFrames и RDD в PySpark?</h2>
<p>Выбор между <strong>DataFrames</strong> и <strong>RDD</strong> зависит от структуры данных и типа операций, которые необходимо выполнять.</p>
<p>Используйте <strong>DataFrames</strong>, когда:</p>
<ul>
<li>требуется обработка структурированных данных со схемой;</li>
<li>нужна оптимизированная обработка с использованием Catalyst и Tungsten;</li>
<li>используются SQL-запросы и встроенные трансформации.</li>
</ul>
<p>Используйте <strong>RDD</strong>, когда:</p>
<ul>
<li>нужны низкоуровневые трансформации и более детальный контроль над вычислениями;</li>
<li>вы работаете с неструктурированными или полуструктурированными данными;</li>
<li>требуется большая гибкость в определении трансформаций.</li>
</ul>
<h2>Как бы вы реализовали инкрементальную обработку данных в PySpark?</h2>
<p><strong>Инкрементальная обработка</strong> необходима для эффективной работы с постоянно растущими наборами данных. Она может быть реализована с помощью:</p>
<ul>
<li><strong>Использования Delta Lake:</strong> хранение обновлений в формате Delta позволяет эффективно обрабатывать инкрементальные изменения.</li>
<li><strong>Использования watermarking в structured streaming:</strong> помогает отбрасывать устаревшие данные, сохраняя при этом агрегаты с состоянием.</li>
<li><strong>Партиционирования и фильтрации:</strong> загрузка только новых или изменённых данных вместо переработки всего объёма.</li>
<li><strong>Использования checkpointing:</strong> сохранение промежуточных результатов для предотвращения переработки с нуля в случае сбоя.</li>
</ul>
<p>Сообщение <a href="https://datatalks.ru/pyspark-interview-questions-and-answers/">PySpark Interview &#8212; Вопросы и ответы</a> появились сначала на <a href="https://datatalks.ru">DataTalks.RU. Data Engineering / DWH / Data Pipeline</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://datatalks.ru/pyspark-interview-questions-and-answers/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Системный дизайн. Интервью по System Design</title>
		<link>https://datatalks.ru/system-design-interview/</link>
					<comments>https://datatalks.ru/system-design-interview/#respond</comments>
		
		<dc:creator><![CDATA[Data Engineer (Admin)]]></dc:creator>
		<pubDate>Tue, 09 Dec 2025 20:46:45 +0000</pubDate>
				<category><![CDATA[System Design]]></category>
		<guid isPermaLink="false">https://datatalks.ru/?p=2451</guid>

					<description><![CDATA[<p>Полезные материалы GitHub: The System Design Primer (329k stars) &#8212; Learn how to design large-scale systems. Prep for the system design interview. Includes Anki flashcards ByteByteGoHq: System Design 101 (79.5k stars) &#8212; Explain complex systems using visuals and simple terms. Help you prepare for system design interviews. https://github.com/DovAmir/awesome-design-patterns (45.2k stars) &#8212; A curated list of [&#8230;]</p>
<p>Сообщение <a href="https://datatalks.ru/system-design-interview/">Системный дизайн. Интервью по System Design</a> появились сначала на <a href="https://datatalks.ru">DataTalks.RU. Data Engineering / DWH / Data Pipeline</a>.</p>
]]></description>
										<content:encoded><![CDATA[<h1>Полезные материалы</h1>
<p><strong>GitHub:</strong></p>
<ul>
<li><a href="https://github.com/donnemartin/system-design-primer" target="_blank" rel="noopener">The System Design Primer</a> (329k stars) &#8212; Learn how to design large-scale systems. Prep for the system design interview. Includes Anki flashcards</li>
<li><a href="https://github.com/ByteByteGoHq/system-design-101" target="_blank" rel="noopener">ByteByteGoHq: System Design 101</a> (79.5k stars) &#8212; Explain complex systems using visuals and simple terms. Help you prepare for system design interviews.</li>
<li><a href="https://github.com/DovAmir/awesome-design-patterns" target="_blank" rel="noopener">https://github.com/DovAmir/awesome-design-patterns</a> (45.2k stars) &#8212; A curated list of software and architecture related design patterns.</li>
<li><a href="https://github.com/ashishps1/awesome-system-design-resources" target="_blank" rel="noopener">https://github.com/ashishps1/awesome-system-design-resources</a> (28.2k stars) &#8212; Learn System Design concepts and prepare for interviews using free resources.</li>
<li><a href="https://github.com/madd86/awesome-system-design" target="_blank" rel="noopener">https://github.com/madd86/awesome-system-design</a> (11.3k stars) &#8212; About<br />
A curated list of awesome System Design (A.K.A. Distributed Systems) resources.</li>
<li><a href="https://github.com/ashishps1/awesome-low-level-design" target="_blank" rel="noopener">https://github.com/ashishps1/awesome-low-level-design</a> (20k stars) &#8212; Learn Low Level Design (LLD) and prepare for interviews using free resources.</li>
<li><a href="https://github.com/mehdihadeli/awesome-software-architecture" target="_blank" rel="noopener">https://github.com/mehdihadeli/awesome-software-architecture</a> (10.1k stars) &#8212; A curated list of awesome articles, videos, and other resources to learn and practice software architecture, patterns, and principles.</li>
<li><a href="https://github.com/heynickc/awesome-ddd" target="_blank" rel="noopener">https://github.com/heynickc/awesome-ddd</a> (12k stars) &#8212; A curated list of Domain-Driven Design (DDD), Command Query Responsibility Segregation (CQRS), Event Sourcing, and Event Storming resources</li>
</ul>
<p><strong>Статьи:</strong></p>
<ul>
<li><a href="https://habr.com/ru/articles/873388/" target="_blank" rel="noopener">System Design для начинающих: всё, что вам нужно. Часть 1</a></li>
<li><a href="https://habr.com/ru/articles/877312/" target="_blank" rel="noopener">System Design для начинающих: всё, что вам нужно. Часть 2</a></li>
<li><a href="https://habr.com/ru/articles/885054/" target="_blank" rel="noopener">System Design для начинающих: всё, что вам нужно. Часть 3</a></li>
<li><a href="https://habr.com/ru/articles/893548/" target="_blank" rel="noopener">System Design для начинающих: всё, что вам нужно. Часть 4</a></li>
<li><a href="https://habr.com/ru/articles/900396/" target="_blank" rel="noopener">System Design для начинающих: всё, что вам нужно. Часть 5</a></li>
<li><a href="https://apolomodov.medium.com/how-to-prepare-for-and-pass-the-system-design-interview-78b820589e8" target="_blank" rel="noopener">Статья &#8212; Как построить подготовку к System Design Interview (Александр Поломодов)</a></li>
</ul>
<p><strong>YouTube:</strong></p>
<ul>
<li><a href="https://www.youtube.com/watch?v=Wh5Ya6UFG1k" target="_blank" rel="noopener">Интервью по System Design. Александр Поломодов (Тинькофф)</a></li>
<li><a href="https://www.youtube.com/watch?v=L9Lt0KEPcDc" target="_blank" rel="noopener">System Design Youtube / TeamLead Avito</a></li>
<li><a href="https://www.youtube.com/watch?v=HRPcPNJ5zTg" target="_blank" rel="noopener">Почему все проваливают собеседование по System Design?</a></li>
<li><a href="https://www.youtube.com/watch?v=KhfeYD0VBOY" target="_blank" rel="noopener">Как пройти System Design Интервью? 4 этапа, 2 важных качества кандидата</a></li>
<li><a href="https://www.youtube.com/watch?v=9_8ShTF6aQA" target="_blank" rel="noopener">Mock-собеседование по System Design от Team Lead из Яндекса</a></li>
<li><a href="https://www.youtube.com/watch?v=hCg4N-r_kF0" target="_blank" rel="noopener">Mock-собеседование по System Design от Team Lead из Ozon</a></li>
<li><a href="https://www.youtube.com/watch?v=jUbOm0B-eKQ" target="_blank" rel="noopener">Как подготовиться и пройти System Design Interview. Александр Поломодов</a></li>
<li><a href="https://www.youtube.com/watch?v=RAMWRgt-3G0" target="_blank" rel="noopener">Дмитрий Волыхин — System Design-интервью для практиков</a></li>
<li><a href="https://www.youtube.com/watch?v=popkBBjbAv8" target="_blank" rel="noopener">Владимир Маслов — System Design. Как построить распределенную систему и пройти собеседование</a></li>
<li><a href="https://www.youtube.com/watch?v=jAi0hZTmOag" target="_blank" rel="noopener">Основы системного дизайна за 30 минут</a></li>
<li><a href="https://www.youtube.com/watch?v=vjjXTZOleQI" target="_blank" rel="noopener">Григорий Вахмистров, Владимир Иванов: публичное собеседование по System Design</a></li>
<li><a href="https://www.youtube.com/watch?v=MTCEZpWXIBM" target="_blank" rel="noopener">Владимир Иванов, Антон Сорокин: публичное собеседование по System Design</a></li>
<li><a href="https://www.youtube.com/watch?v=qsEvKryZ5YA" target="_blank" rel="noopener">Mock-собеседование по System Design | Ex-Team Lead Яндекс</a></li>
<li><a href="https://www.youtube.com/watch?v=yUf0jsZuy5k" target="_blank" rel="noopener">Публичное собеседование по System design</a></li>
<li><a href="https://www.youtube.com/watch?v=xfH2QMdCvWA" target="_blank" rel="noopener">System Design с Валерием Бабушкиным | Выпуск 4 | Собеседование</a></li>
<li><a href="https://www.youtube.com/watch?v=tnv3TJ2gkOk" target="_blank" rel="noopener">ЗАПЛАКАЛ на SYSTEM DESIGN в T-БАНК. FRONTEND СОБЕСЕДОВАНИЕ на 380К</a></li>
<li><a href="https://www.youtube.com/watch?v=1ooQg8q332E" target="_blank" rel="noopener">iOS Мок-собеседование по систем дизайну | Проектируем инстаграм с разработчиком из крупного банка</a></li>
<li><a href="https://www.youtube.com/watch?v=6mr_QkCnyuE" target="_blank" rel="noopener">#29 Интервью Data Scientist в МосБиржу | Секция System Design | Machine Learning</a></li>
<li><a href="https://www.youtube.com/watch?v=ItRLoTuyDuE" target="_blank" rel="noopener">#FaangTalk 69 &#8212; Вокруг и около System Design интервью</a></li>
<li><a href="https://www.youtube.com/watch?v=jsYohT5HBi4" target="_blank" rel="noopener">System Design Interview: 60 минут до оффера / Владимир Невзоров</a></li>
<li><a href="https://www.youtube.com/watch?v=fvm7xEfSbs4" target="_blank" rel="noopener">СОБЕСЕДОВАНИЕ в ЯНДЕКС на frontend-разработчика. Этап 3: System Design</a></li>
<li><a href="https://www.youtube.com/watch?v=jtYSNaXHV90" target="_blank" rel="noopener"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f6a8.png" alt="🚨" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Пытаюсь пройти System Design интервью / Такое спрашивают в MAANG? / Mock собеседование</a></li>
<li><a href="https://www.youtube.com/watch?v=lVlJRzyFR3c" target="_blank" rel="noopener">Собес в Яндекс на Java-разработчика \\ System Design интервью</a></li>
<li><a href="https://www.youtube.com/watch?v=WDEiIz3WtgA" target="_blank" rel="noopener">Интервью по System Design. youtube</a></li>
</ul>
<p><strong>YouTube English:</strong></p>
<ul>
<li><a href="https://www.youtube.com/watch?v=i7twT3x5yv8" target="_blank" rel="noopener">System Design Interview: A Step-By-Step Guide</a></li>
<li><a href="https://www.youtube.com/watch?v=o5n85GRKuzk" target="_blank" rel="noopener">Design Twitter &#8212; System Design Interview</a></li>
<li><a href="https://www.youtube.com/playlist?list=PLot-Xpze53le35rQuIbRET3YwEtrcJfdt" target="_blank" rel="noopener">System Design Playlist</a></li>
</ul>
<p><strong>YouTube System Design по темам:</strong></p>
<ul>
<li><a href="https://www.youtube.com/watch?v=iLMlYgQoTIE" target="_blank" rel="noopener">Как кэшировать данные | Теория кэширования &#8212; System Design</a></li>
<li><a href="https://www.youtube.com/watch?v=P5_cl_19zcQ" target="_blank" rel="noopener">Балансировка в System Design. Алгоритмы на примере NGINX</a></li>
<li><a href="https://www.youtube.com/watch?v=KWIUkbcDhHc" target="_blank" rel="noopener">Replication in System Design</a></li>
<li><a href="https://www.youtube.com/watch?v=przeNlYB8Lw" target="_blank" rel="noopener">System Design. Requirements, HTTP, TCP, Interview</a></li>
<li><a href="https://www.youtube.com/watch?v=GgRzHru9Hag" target="_blank" rel="noopener">REDIS для Начинающих за 1 Час | Redis с Нуля Уроки</a></li>
<li><a href="https://www.youtube.com/watch?v=6JzPr_8bMbk" target="_blank" rel="noopener">Лучший Гайд по HTTP для Начинающих за 25 Мин с Нуля</a></li>
<li><a href="https://www.youtube.com/watch?v=WPCz_U7D8PI" target="_blank" rel="noopener">Вебинар: Stateful vs. Stateless [Хекслет]</a></li>
<li><a href="https://www.youtube.com/watch?v=DZCHvxlxwmI" target="_blank" rel="noopener">Stateful vs Stateless Дизайн Веб-Приложений</a></li>
<li><a href="https://www.youtube.com/watch?v=mkpJIZWQlHY" target="_blank" rel="noopener">Что такое REST API? HTTP, Клиент-Сервер, Проектирование, Разработка, Документация, Swagger и OpenApi</a></li>
<li><a href="https://www.youtube.com/watch?v=tnzjswnQ7So" target="_blank" rel="noopener">State management от А до Я. Управление состоянием. Server vs client state. Global vs module</a></li>
<li><a href="https://www.youtube.com/watch?v=OmlkEhRHRTA" target="_blank" rel="noopener">SSL и TLS &#8212; в чем разница?</a></li>
<li><a href="https://www.youtube.com/watch?v=yMSJKBQINAc" target="_blank" rel="noopener">TCP и UDP | Что это такое и в чем разница?</a></li>
<li><a href="https://www.youtube.com/watch?v=uHBmx7HcmJQ" target="_blank" rel="noopener">Визуализация архитектуры C4 model / Максим Пальчиков</a></li>
<li><a href="https://www.youtube.com/watch?v=e3LjCD8kW1g" target="_blank" rel="noopener">Денис Цветцих — C4 model на практике</a></li>
<li><a href="https://www.youtube.com/watch?v=cTM6M6BbuMs" target="_blank" rel="noopener">Архитектурный репозиторий на базе GitLab и C4 Model для большой компании. Кирилл Ветчинкин</a></li>
<li><a href="https://www.youtube.com/watch?v=3IoZgDuel9c" target="_blank" rel="noopener">Нотация C4 и чем она полезна для проектирования Корниенко Андрей</a></li>
</ul>
<p><strong>YouTube System Design по темам English:</strong></p>
<ul>
<li><a href="https://www.youtube.com/watch?v=pBASqUbZgkY" target="_blank" rel="noopener">Every Type Of API You Must Know Explained!</a></li>
<li></li>
</ul>
<h1>Общий план по <span style="font-size: revert;">решению задач системного дизайна</span></h1>
<p>Основные этапы интервью (фреймворк) из статьи <a href="https://apolomodov.medium.com/how-to-prepare-for-and-pass-the-system-design-interview-78b820589e8" target="_blank" rel="noopener">https://apolomodov.medium.com/how-to-prepare-for-and-pass-the-system-design-interview-78b820589e8</a></p>
<ol>
<li><strong>Получение задания и контекста</strong> — интервьюер даёт название системы и краткое описание.</li>
<li><strong>Формализация требований</strong> — кандидат задаёт вопросы, уточняет функциональные и нефункциональные требования (например, масштабируемость, доступность, согласованность).</li>
<li><strong>Определение границ системы и API</strong> — фиксация внешних точек входа, интерфейсов и контрактов.</li>
<li><strong>Итеративное проектирование архитектуры</strong> — проработка основных потоков (happy path) и исключительных сценариев (exceptional flows).</li>
<li><strong>Концептуальная схема</strong> — сведение компонентов в общую архитектуру.</li>
<li><strong>Выбор технологий и оценка размеров системы (sizing)</strong> — обсуждение конкретных инструментов и сколько ресурсов потребуется под нагрузку.</li>
<li><strong>Дополнительные вопросы и расширения</strong> — optional этап, где обсуждаются дополнительные требования и возможные улучшения.</li>
</ol>
<p><strong>1) Понимание / уточнение задачи</strong></p>
<ul>
<li>Что делать:
<ul>
<li>Спокойно прочитайте задачу.</li>
<li>Задавайте вопросы интервьюеру о масштабе, ролях пользователей, сценариях и ограничениях.</li>
</ul>
</li>
<li>Цель:
<ul>
<li>Выяснить функциональные (что нужно системе делать) и нефункциональные требования (производительность, масштабируемость, отказоустойчивость).</li>
</ul>
</li>
</ul>
<p><strong>2) Определение ключевых характеристик</strong></p>
<ul>
<li>Что требуется выбрать:
<ul>
<li>SCALABILITY (масштабируемость)</li>
<li>AVAILABILITY (доступность)</li>
<li>CONSISTENCY (согласованность)</li>
<li>LATENCY (задержка отклика)</li>
</ul>
</li>
<li>Как спросить:
<ul>
<li>“Сколько запросов в секунду ожидается?”,</li>
<li>“Насколько важна точность данных?”,</li>
<li>“Нужно ли работать офлайн?”</li>
</ul>
</li>
</ul>
<p><strong>3) High-Level Design (Верхнеуровневая архитектура)</strong></p>
<ul>
<li>Что делать:
<ul>
<li>Нарисовать базовую схему: Clients → Load Balancer → API Gates → Services → Storage.</li>
<li>Определить основные компоненты системы.</li>
</ul>
</li>
<li>Важно:
<ul>
<li>Покажите, как запросы проходят через систему, какие модули есть и как они связаны между собой.</li>
</ul>
</li>
</ul>
<p><strong>4) Детализация компонентов</strong></p>
<ul>
<li>Проработайте:
<ul>
<li>Какие базы данных (SQL/NoSQL) и почему.</li>
<li>Где ставить кеши (Redis/Memcached).</li>
<li>Как будут работать очереди сообщений (Kafka, RabbitMQ) для асинхронных задач.</li>
<li>Как обеспечить отказоустойчивость (репликация, резервирование).</li>
</ul>
</li>
<li>Почему это важно:
<ul>
<li>Показывает понимание trade-offs (например, скорость чтения vs. согласованность).</li>
</ul>
</li>
</ul>
<p><strong>5) Обсуждение сценариев нагрузки</strong></p>
<ul>
<li>Примеры вопросов:
<ul>
<li>Что будет при пиковых нагрузках?</li>
<li>Как система масштабируется?</li>
<li>Чем заменить узкое место?</li>
</ul>
</li>
<li>Покажите:
<ul>
<li>Как добавить шардирование, их репликацию, горизонтальное масштабирование и оптимизации запросов.</li>
</ul>
</li>
</ul>
<p><strong>6) Trade-offs и ограничения</strong></p>
<ul>
<li>Обсудите:
<ul>
<li>Преимущества и недостатки выбранной архитектуры.</li>
<li>Почему выбран тип базы, взаимодействие компонентов и протоколы.</li>
</ul>
</li>
<li>Цель:
<ul>
<li>Демонстрировать рассуждения — интервьюер смотрит не на правильный ответ, а на логику и аргументацию.</li>
</ul>
</li>
</ul>
<p><strong>7) Заключение и возможные улучшения</strong></p>
<ul>
<li>Что делать:
<ul>
<li>Уточнить, что можно улучшить.</li>
<li>Обсудить мониторинг, логирование, безопасность, расширяемость.</li>
</ul>
</li>
<li>Важно:
<ul>
<li>Показать стратегическое понимание системы за рамками минимального решения.</li>
</ul>
</li>
</ul>
<p><strong>Короткая формула для интервью</strong></p>
<p><code>Clarify → High-Level → Deep Dive → Bottlenecks → Trade-offs → Wrap-up</code></p>
<h2>Вертикальное vs горизонтальное масштабирование</h2>
<p>Вертикальное и горизонтальное масштабирование — это два принципиально разных подхода к увеличению производительности системы, и каждый из них отражает определённую философию роста.</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/12/scalling.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2474" src="https://datatalks.ru/wp-content/uploads/2025/12/scalling.png" alt="" width="1123" height="344" srcset="https://datatalks.ru/wp-content/uploads/2025/12/scalling.png 1123w, https://datatalks.ru/wp-content/uploads/2025/12/scalling-300x92.png 300w, https://datatalks.ru/wp-content/uploads/2025/12/scalling-1024x314.png 1024w, https://datatalks.ru/wp-content/uploads/2025/12/scalling-768x235.png 768w, https://datatalks.ru/wp-content/uploads/2025/12/scalling-450x138.png 450w, https://datatalks.ru/wp-content/uploads/2025/12/scalling-780x239.png 780w" sizes="(max-width: 1123px) 100vw, 1123px" /></a></p>
<p><strong>Вертикальное масштабирование</strong> представляет собой усиление одной конкретной машины: ей добавляют больше оперативной памяти, увеличивают количество процессорных ядер, улучшают дисковую подсистему. Система по сути остаётся той же, меняется лишь «железо», на котором она работает. Такой подход привлекателен своей простотой: нет распределённых компонент, нет необходимости координировать состояние между несколькими серверами, нет дополнительных слоёв инфраструктуры. Приложению не требуется перестраиваться — оно просто получает больше ресурсов и продолжает функционировать.</p>
<p>Тем не менее в вертикальном масштабировании заложены фундаментальные ограничения. Во-первых, у любой машины есть предел: невозможно бесконечно увеличивать оперативную память или количество процессорных ядер, и чем мощнее становится сервер, тем быстрее растёт цена каждой следующей единицы мощности. Во-вторых, несмотря на своё удобство, вертикальное масштабирование плохо помогает обеспечить высокую доступность. Если такой единственный мощный сервер выходит из строя, система оказывается уязвимой, и её восстановление занимает время.</p>
<p><strong>Горизонтальное масштабирование</strong> решает проблему иначе: вместо усиления одного узла в архитектуру добавляются новые машины, и нагрузка распределяется между ними. Такой подход требует совершенно другой организации приложения. Оно должно уметь работать в распределённой среде, где его копии запускаются параллельно, часто в разных зонах доступности или даже регионах. Данные должны быть реплицированы или разделены на части между узлами, а доступ к сервисам — сбалансирован через специальные компоненты.</p>
<p>Горизонтальное масштабирование намного сложнее в реализации: возникают вопросы согласованности данных, появляются риски сетевых задержек, нужен продуманный механизм обнаружения и изоляции неисправных узлов, а также система автоматического масштабирования, которая будет добавлять и удалять ресурсы в соответствии с текущей нагрузкой. Но вместе с этой сложностью приходит то, чего невозможно достичь вертикальным путём: потенциально неограниченная масштабируемость. Система, построенная на горизонтальном увеличении ресурсов, может расти практически без верхней границы, добавляя всё больше узлов в ответ на увеличение нагрузки. Кроме того, высокая доступность становится естественной частью архитектуры — если один сервер выходит из строя, его роль автоматически берут на себя другие.</p>
<h2>Stateful vs stateless сервисы</h2>
<p>В мире распределённых систем различие между <strong>stateful</strong> и <strong>stateless</strong> сервисами оказывает огромное влияние на архитектуру, масштабируемость и отказоустойчивость.</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/12/Stateful_vs_stateless.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2481" src="https://datatalks.ru/wp-content/uploads/2025/12/Stateful_vs_stateless.png" alt="" width="918" height="656" srcset="https://datatalks.ru/wp-content/uploads/2025/12/Stateful_vs_stateless.png 918w, https://datatalks.ru/wp-content/uploads/2025/12/Stateful_vs_stateless-300x214.png 300w, https://datatalks.ru/wp-content/uploads/2025/12/Stateful_vs_stateless-768x549.png 768w, https://datatalks.ru/wp-content/uploads/2025/12/Stateful_vs_stateless-450x322.png 450w, https://datatalks.ru/wp-content/uploads/2025/12/Stateful_vs_stateless-780x557.png 780w" sizes="(max-width: 918px) 100vw, 918px" /></a></p>
<p><strong>Stateless-сервисы</strong> — это наиболее простой и предсказуемый тип сервисов. Они не хранят никакого состояния между запросами, и каждый запрос обрабатывается так, будто он первый. Сервис не полагается на память о предыдущих взаимодействиях и не требует сохранения пользовательских данных локально. Он получает входные данные, выполняет вычисление и возвращает результат — всё. Именно эта независимость от истории делает такие сервисы чрезвычайно удобными для масштабирования: любую копию сервиса можно добавить или удалить безо всяких последствий, а балансировщик запросов свободно распределяет нагрузку между экземплярами. Фактически stateless-архитектура превращает сервисы в вычислительные «ячейки», которые можно размножать, заменять, обновлять и уничтожать без риска потерять данные или нарушить целостность работы системы.</p>
<p><strong>Stateful-сервисы</strong> живут совсем по другим законам. <strong>Они хранят состояние</strong> — пользовательские сессии, данные, транзакции, контекст взаимодействия, внутренние буферы или кэш, необходимый для корректной работы. Это означает, что один и тот же клиентский запрос должен прийти на тот же узел, который держит состояние, либо состояние должно быть доступно всем узлам через внешний механизм — например, через распределённое хранилище. Система становится более связанной и менее гибкой: невозможно просто взять и создать ещё одну копию сервиса без обеспечения согласованности состояния, так же как невозможно безболезненно выключить существующий узел, пока данные не будут корректно перенесены. <strong>Stateful-архитектуры</strong> требуют репликации, механизмов консенсуса, мониторинга здоровья узлов и сложных алгоритмов распределения нагрузки. Именно состояние делает систему более “живой”, но одновременно и более хрупкой, более чувствительной к сбоям и сетевым задержкам.</p>
<p>На практике граница между этими двумя типами сервисов редко бывает абсолютно чёткой. Даже самые чистые stateless-сервисы почти всегда используют внешние stateful-хранилища — базы данных, кеши, очереди. Многие stateful-узлы пытаются минимизировать объём состояния, вынеся всё возможное наружу, чтобы упростить масштабирование. Современные архитектуры стремятся к тому, чтобы сами сервисы оставались максимально stateless, а состояние хранилось в специализированных, хорошо масштабируемых стореджах, разработанных для управления консистентностью и отказоустойчивостью.</p>
<p>И всё же фундаментальный принцип остаётся неизменным: <strong>stateless-сервисы</strong> дают гибкость, простоту и практически линейную масштабируемость, в то время как <strong>stateful-сервисы</strong> дают возможность работать с реальными данными, но требуют гораздо более сложной инфраструктуры и становятся естественной точкой роста сложности всей системы. Понимание различий между ними лежит в основе любого серьёзного проектирования — от API-сервисов до распределённых баз данных — и определяет, каким образом система будет расти, выдерживать нагрузку и восстанавливаться после сбоев.</p>
<h3>Где необходим Stateful подход</h3>
<p><strong>Во-первых, это базы данных и хранилища.</strong> Любая СУБД по своей сути stateful: она хранит данные, индексы, кэши, журналы транзакций и метаданные. Попытка сделать базу stateless лишена смысла, потому что её ценность — именно в сохранении и управлении состоянием. Здесь statefulness компенсируется репликацией, шардингом и механизмами восстановления.</p>
<p><strong>Во-вторых, in-memory системы с состоянием,</strong> такие как Redis, Memcached, Kafka, стриминговые движки и очереди сообщений. Они держат состояние либо в памяти, либо в логах, чтобы обеспечить низкую задержку и высокую пропускную способность. Например, Kafka хранит offset’ы, порядок событий и данные топиков; Flink хранит состояние операторов; Redis — кэш и структуры данных. Вынесение этого состояния «куда-то ещё» разрушило бы их основную функцию.</p>
<p><strong>Третья важная категория — долгоживущие соединения и real-time системы.</strong> WebSocket-серверы, игровые серверы, чаты, видеостриминг и push-уведомления требуют постоянного контекста соединения. Здесь состояние связано не только с данными, но и с самим фактом открытого канала связи. Каждый клиент «привязан» к конкретному соединению, и это невозможно сделать полностью stateless без потери функциональности или резкого усложнения архитектуры.</p>
<p><strong>Четвёртая область — стриминговая и event-driven обработка данных.</strong> В системах вроде Flink или Spark Streaming состояние необходимо для оконных агрегаций, подсчётов, дедупликации и работы с event time. Без хранения промежуточного состояния невозможно корректно обрабатывать потоки событий. Здесь stateful-подход — не компромисс, а обязательное условие корректности.</p>
<p><strong>Пятая категория — сессии и пользовательский контекст,</strong> когда объём состояния велик или часто изменяется. Например, сложные бизнес-процессы, оркестрация саг, workflow-движки, системы бронирования или финансовые процессы. Передавать весь контекст в каждом запросе было бы дорого и небезопасно, поэтому состояние хранится централизованно и обновляется постепенно.</p>
<p><strong>Наконец, stateful-подход оправдан в алгоритмически сложных системах,</strong> таких как антифрод, рекомендательные системы, графовые движки и системы машинного обучения онлайн-типа. Они опираются на накопленный контекст, историю и взаимосвязи, которые невозможно эффективно пересчитывать «с нуля» на каждый запрос.</p>
<h2>Шардинг vs Репликация</h2>
<p>В распределённых хранилищах <strong>шардинг</strong> и <strong>репликация</strong> выполняют две разные роли, хотя на практике почти всегда работают вместе.</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/12/sharding_vs_replication.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2463" src="https://datatalks.ru/wp-content/uploads/2025/12/sharding_vs_replication.png" alt="" width="1601" height="930" srcset="https://datatalks.ru/wp-content/uploads/2025/12/sharding_vs_replication.png 1601w, https://datatalks.ru/wp-content/uploads/2025/12/sharding_vs_replication-300x174.png 300w, https://datatalks.ru/wp-content/uploads/2025/12/sharding_vs_replication-1024x595.png 1024w, https://datatalks.ru/wp-content/uploads/2025/12/sharding_vs_replication-768x446.png 768w, https://datatalks.ru/wp-content/uploads/2025/12/sharding_vs_replication-1536x892.png 1536w, https://datatalks.ru/wp-content/uploads/2025/12/sharding_vs_replication-450x261.png 450w, https://datatalks.ru/wp-content/uploads/2025/12/sharding_vs_replication-780x453.png 780w" sizes="(max-width: 1601px) 100vw, 1601px" /></a></p>
<p><strong>Репликация</strong> — это способ обеспечить надёжность и доступность данных, копируя одну и ту же информацию на несколько узлов. Каждая реплика содержит полный набор данных или хотя бы их логически завершённую часть. Если один сервер выходит из строя, другой может мгновенно его заменить, а система продолжает работать без потерь. <strong>Репликация</strong> служит своего рода страховкой: она защищает от аппаратных отказов, позволяет распределить нагрузку на чтение между репликами и уменьшает вероятность простоев. Но при всей своей полезности репликация не увеличивает фактическую ёмкость хранилища. Сколько бы копий ни было, каждая хранит всё то же самое. Это делает систему более надёжной, но не более масштабируемой по данным.</p>
<p><strong>Шардинг</strong> решает совершенно другую задачу. Если объём информации растёт, а одна база данных перестаёт помещаться на один сервер, приходится дробить набор данных на отдельные сегменты — шарды. Каждый шард хранит только свою часть информации, и вместе эти части образуют целостное хранилище. Это увеличивает вместимость системы практически без предела: можно добавлять новые узлы и переносить на них отдельные части данных, обеспечивая горизонтальную масштабируемость. Но вместе с этим приходит сложность маршрутизации: система должна знать, на какой узел отправить запрос; данные становятся распределёнными, и объединить их в рамках одного запроса может быть гораздо сложнее. Шардинг усложняет операции поиска, агрегирования и транзакций — то, что на одном сервере выполнялось просто, в распределённой среде требует дополнительной логики и координации.</p>
<p>Если репликация защищает данные, но не делает систему “больше”, то шардинг делает систему “больше”, но не защищает её сам по себе. В реальной инфраструктуре редко выбирают между ними — чаще строят комбинацию: каждый шард реплицируется на несколько узлов, чтобы обеспечить и масштабируемость, и отказоустойчивость. Таким образом, хранилище растёт по горизонтали, но при этом любой сервер внутри шарда может выйти из строя без потери данных.</p>
<p>Репликация обеспечивает выживаемость и высокую доступность, но не повышает общую вместимость. Шардинг обеспечивает масштабируемость и распределение нагрузки, но требует более сложной архитектуры и не гарантирует защиту от отказов. Вместе они образуют фундамент современных распределённых хранилищ, позволяя одновременно хранить огромные объёмы данных, выдерживать высокие нагрузки и оставаться устойчивыми к сбоям.</p>
<h2>Синхронные vs Асинхронные коммуникации</h2>
<p>Взаимодействие сервисов в распределённой системе может строиться на синхронных или асинхронных коммуникациях, и выбор между ними определяет характер всей архитектуры.</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/12/synchronous_vs_acynchronous.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2465" src="https://datatalks.ru/wp-content/uploads/2025/12/synchronous_vs_acynchronous.png" alt="" width="1445" height="420" srcset="https://datatalks.ru/wp-content/uploads/2025/12/synchronous_vs_acynchronous.png 1445w, https://datatalks.ru/wp-content/uploads/2025/12/synchronous_vs_acynchronous-300x87.png 300w, https://datatalks.ru/wp-content/uploads/2025/12/synchronous_vs_acynchronous-1024x298.png 1024w, https://datatalks.ru/wp-content/uploads/2025/12/synchronous_vs_acynchronous-768x223.png 768w, https://datatalks.ru/wp-content/uploads/2025/12/synchronous_vs_acynchronous-450x131.png 450w, https://datatalks.ru/wp-content/uploads/2025/12/synchronous_vs_acynchronous-780x227.png 780w" sizes="(max-width: 1445px) 100vw, 1445px" /></a></p>
<p><strong>Синхронная модель</strong> предполагает, что один сервис отправляет запрос другому и ждёт ответа, прежде чем продолжить работу. Она проста и естественна: запрос связан с ответом, логика понятна, а поток выполнения легко проследить от начала до конца. Такой подход даёт ощущение прямого диалога между системами — пока один говорит, другой слушает и отвечает. Но эта же простота становится источником ограничений. Один медленный сервис способен заблокировать цепочку зависящих от него вызовов. Если где-то возникает задержка или временная недоступность, всё дерево запросов начинает сбоить, а нагрузка растёт каскадом. Система с активным использованием синхронных вызовов становится тесно связанной: здоровье каждого узла напрямую влияет на остальных, и архитектура в целом становится менее устойчивой к непредсказуемости реального мира.</p>
<p><strong>Асинхронные коммуникации</strong> предлагают совершенно иной способ взаимодействия. Вместо прямого запроса и ожидания ответа сервис только отправляет сообщение — чаще всего в очередь, шину событий или брокер сообщений — и продолжает работу, не дожидаясь реакции. Это создаёт естественную буферизацию между компонентами: отправитель не знает и не обязан знать, кто обработает сообщение, как скоро это произойдёт и какой объём нагрузки сейчас испытывает получатель. Каждый сервис работает в собственном темпе, а система становится более устойчивой к всплескам нагрузки и временной недоступности отдельных элементов. Потеря скорости одним узлом не приводит к немедленному эффекту домино: сообщения аккуратно накапливаются в очереди, а потребители обрабатывают их по мере готовности.</p>
<p>Однако асинхронность не является универсальным лекарством. Она усложняет разработку: связь между действиями теряется, цепочка событий больше не очевидна, а отладка требует инструментов трассировки. Возникают вопросы гарантии доставки, порядка обработки сообщений и идемпотентности — способности сервиса безопасно принимать одно и то же сообщение несколько раз. Кроме того, асинхронная модель неизбежно добавляет задержку между причиной и следствием, что неприемлемо для систем, требующих мгновенного ответа.</p>
<p>Синхронные коммуникации хороши там, где важна непосредственная взаимосвязь и актуальность данных: запрос к платежному шлюзу, получение результата вычисления, верификация пользователя. Асинхронные коммуникации незаменимы в системах, которые должны выдерживать пики нагрузки, сохранять устойчивость при сбоях и развязывать зависимости между компонентами: отправка email, обработка событий, построение аналитических пайплайнов.</p>
<p>Выбор между синхронностью и асинхронностью — это баланс между простотой и устойчивостью, между скоростью отклика и гибкостью, между тесным связующим контуром и свободным, событийным взаимодействием. В зрелых системах они почти всегда сосуществуют: критически важные операции выполняются синхронно, а всё остальное передаётся по асинхронным каналам, создавая архитектуру, которая одновременно понятна разработчику и остаётся надёжной под непостоянными нагрузками реального мира.</p>
<h1>HTTP, сети и протоколы</h1>
<h2>HTTP</h2>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/12/http1_http2_http3-scaled.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2466" src="https://datatalks.ru/wp-content/uploads/2025/12/http1_http2_http3-scaled.png" alt="" width="2027" height="2560" srcset="https://datatalks.ru/wp-content/uploads/2025/12/http1_http2_http3-scaled.png 2027w, https://datatalks.ru/wp-content/uploads/2025/12/http1_http2_http3-238x300.png 238w, https://datatalks.ru/wp-content/uploads/2025/12/http1_http2_http3-811x1024.png 811w, https://datatalks.ru/wp-content/uploads/2025/12/http1_http2_http3-768x970.png 768w, https://datatalks.ru/wp-content/uploads/2025/12/http1_http2_http3-1216x1536.png 1216w, https://datatalks.ru/wp-content/uploads/2025/12/http1_http2_http3-1622x2048.png 1622w, https://datatalks.ru/wp-content/uploads/2025/12/http1_http2_http3-450x568.png 450w, https://datatalks.ru/wp-content/uploads/2025/12/http1_http2_http3-780x985.png 780w, https://datatalks.ru/wp-content/uploads/2025/12/http1_http2_http3-1600x2021.png 1600w" sizes="(max-width: 2027px) 100vw, 2027px" /></a></p>
<p><strong>HTTP</strong> — это фундамент современных интернет-сервисов, и его эволюция показывает, как интернет постепенно менялся, подстраиваясь под рост потребностей.</p>
<p><strong>HTTP/1.1</strong> был простым и понятным, но страдал от ограничений: каждый запрос открывал новый TCP-коннект или пытался переиспользовать существующий, а браузер мог отправлять лишь несколько параллельных запросов к одному домену. Это приводило к классическим «водопадам» загрузки страниц, где каждый ресурс ждал своей очереди.</p>
<p><strong>HTTP/2</strong> решает эту проблему принципиально иначе: один <strong>TCP</strong>-коннект превращается в многопоточный канал, где десятки запросов и ответов идут параллельно, перемешиваясь между собой. Появляется бинарный формат, сжатие заголовков и серверные push-запросы. Но HTTP/2 всё равно опирается на TCP, а значит страдает от «головной блокировки строки»: потеря одного пакета может заморозить весь поток.</p>
<p><strong>HTTP/3</strong> делает шаг ещё дальше и строится поверх <strong>QUIC</strong> — протокола на базе <strong>UDP</strong>, который обеспечивает поточность на уровне приложения. Потеря пакета больше не замораживает весь трафик, только конкретный поток, и система становится заметно быстрее в реальных сетях с непостоянным качеством соединения, например в мобильных сетях.</p>
<h2>WebSockets и long polling</h2>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/12/WebSockets_SSE_long_polling.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2467" src="https://datatalks.ru/wp-content/uploads/2025/12/WebSockets_SSE_long_polling.png" alt="" width="1767" height="844" srcset="https://datatalks.ru/wp-content/uploads/2025/12/WebSockets_SSE_long_polling.png 1767w, https://datatalks.ru/wp-content/uploads/2025/12/WebSockets_SSE_long_polling-300x143.png 300w, https://datatalks.ru/wp-content/uploads/2025/12/WebSockets_SSE_long_polling-1024x489.png 1024w, https://datatalks.ru/wp-content/uploads/2025/12/WebSockets_SSE_long_polling-768x367.png 768w, https://datatalks.ru/wp-content/uploads/2025/12/WebSockets_SSE_long_polling-1536x734.png 1536w, https://datatalks.ru/wp-content/uploads/2025/12/WebSockets_SSE_long_polling-450x215.png 450w, https://datatalks.ru/wp-content/uploads/2025/12/WebSockets_SSE_long_polling-780x373.png 780w, https://datatalks.ru/wp-content/uploads/2025/12/WebSockets_SSE_long_polling-1600x764.png 1600w" sizes="(max-width: 1767px) 100vw, 1767px" /></a></p>
<p>Рядом с HTTP стоит другая важная модель взаимодействия: <strong>WebSockets</strong> и <strong>long polling</strong>. Обычный HTTP предполагает короткий запрос и короткий ответ, но многие приложения — чат, игры, торговые терминалы — требуют постоянного двустороннего соединения.</p>
<p><strong>Long polling</strong> пытается имитировать реальную подписку: клиент делает запрос, сервер держит его открытым и отвечает только тогда, когда есть новое событие. Это работает, но создаёт дополнительную нагрузку.</p>
<p><strong>WebSocket</strong> же устанавливает постоянный канал поверх HTTP-upgrade и превращает клиент и сервер в равноправных участников, способных посылать сообщения в любой момент. Такой канал позволяет строить системы реального времени без постоянных открытий и закрытий соединений.</p>
<h2>TCP и UDP</h2>
<p>На ещё более низком уровне находится различие между <strong>TCP</strong> и <strong>UDP</strong> — двумя фундаментальными транспортными протоколами.</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/12/TCP_UDP.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2469" src="https://datatalks.ru/wp-content/uploads/2025/12/TCP_UDP.png" alt="" width="983" height="399" srcset="https://datatalks.ru/wp-content/uploads/2025/12/TCP_UDP.png 983w, https://datatalks.ru/wp-content/uploads/2025/12/TCP_UDP-300x122.png 300w, https://datatalks.ru/wp-content/uploads/2025/12/TCP_UDP-768x312.png 768w, https://datatalks.ru/wp-content/uploads/2025/12/TCP_UDP-450x183.png 450w, https://datatalks.ru/wp-content/uploads/2025/12/TCP_UDP-780x317.png 780w" sizes="(max-width: 983px) 100vw, 983px" /></a></p>
<p><strong>TCP</strong> создаёт надёжный поток данных: пакеты приходят в правильном порядке, пропавшие переотправляются, а канал гарантирует доставку. Это идеально подходит для веб-страниц, API, финансовых транзакций.</p>
<p><strong>UDP</strong> работает иначе: он не пытается контролировать порядок доставки или гарантировать успех. Но именно благодаря этому UDP быстрее — и становится основой для мультимедиа, игр, стриминга и современных протоколов вроде <strong>QUIC</strong>. В системном дизайне важно понимать эту разницу: когда важнее скорость, а когда — надёжность.</p>
<table>
<thead>
<tr>
<th>Свойство</th>
<th>TCP</th>
<th>UDP</th>
</tr>
</thead>
<tbody>
<tr>
<td>Гарантия доставки</td>
<td>Да</td>
<td>Нет</td>
</tr>
<tr>
<td>Порядок пакетов</td>
<td>Гарантирован</td>
<td>Не гарантирован</td>
</tr>
<tr>
<td>Скорость</td>
<td>Медленнее</td>
<td>Быстрее</td>
</tr>
<tr>
<td>Установление соединения</td>
<td>Да</td>
<td>Нет</td>
</tr>
<tr>
<td>Используется для</td>
<td>HTTP, API, файлы</td>
<td>Игры, звонки, стриминг</td>
</tr>
<tr>
<td>Потеря пакетов</td>
<td>Исправляется</td>
<td>Игнорируется</td>
</tr>
</tbody>
</table>
<h2>CDN</h2>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/12/cdn.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2471" src="https://datatalks.ru/wp-content/uploads/2025/12/cdn.png" alt="" width="1159" height="932" srcset="https://datatalks.ru/wp-content/uploads/2025/12/cdn.png 1159w, https://datatalks.ru/wp-content/uploads/2025/12/cdn-300x241.png 300w, https://datatalks.ru/wp-content/uploads/2025/12/cdn-1024x823.png 1024w, https://datatalks.ru/wp-content/uploads/2025/12/cdn-768x618.png 768w, https://datatalks.ru/wp-content/uploads/2025/12/cdn-450x362.png 450w, https://datatalks.ru/wp-content/uploads/2025/12/cdn-780x627.png 780w" sizes="(max-width: 1159px) 100vw, 1159px" /></a></p>
<p>И наконец — <strong>CDN</strong>, тихий герой высокопроизводительного интернета. <strong>Контент-дистрибуционные сети</strong> выносят копии статических ресурсов ближе к пользователям, сокращая путь, который проходит запрос. Это не просто «кэширование на периферии», это целая географически распределённая сеть узлов, способная взять на себя огромную часть нагрузки и сделать систему более устойчивой к всплескам трафика. CDN особенно важна в глобальных сервисах, где пользователи находятся на разных континентах, а даже небольшое уменьшение задержки может сильно улучшить пользовательский опыт.</p>
<h1>Данные и хранение</h1>
<h2>SQL vs NoSQL</h2>
<p>Одним из первых выборов, который приходится делать при проектировании системы, становится выбор между SQL и NoSQL.</p>
<p><strong>Реляционные базы данных</strong> — это строгая схема, предсказуемые запросы, гарантии транзакционной целостности и мощная декларативная модель языка SQL. Они блестяще подходят для систем, где данные имеют чёткую структуру: финансы, транзакции, инвентаризация, биллинг. Там, где важна консистентность и возможность выполнять сложные запросы, SQL остаётся безальтернативным решением.</p>
<p><strong>NoSQL появился как ответ на другие требования:</strong> гибкость схемы, огромные объёмы данных и горизонтальная масштабируемость. <strong>Документные базы</strong> и <strong>key-value</strong> системы не навязывают структуру, легко растут по горизонтали и прекрасно работают там, где данные имеют иерархическую природу или где система должна выдерживать колоссальную нагрузку на чтение. Выбор между SQL и NoSQL никогда не сводится к моде — он всегда определяется моделями доступа, структурой процессов и требованиями к согласованности.</p>
<h2>Индексы</h2>
<p>Работа с данными неизбежно приводит к вопросу индексов — того механизма, который делает операции поиска не просто возможными, но эффективными.</p>
<p>В основе большинства индексов в реляционных базах лежат <strong>B-tree структуры</strong>, оптимизированные под дисковые операции. Они позволяют находить строки по ключу или по диапазону за логарифмическое время, что делает их универсальным инструментом для большинства запросов.</p>
<p><strong>Hash-индексы</strong> работают иначе: они идеальны для точечных lookup-запросов, но не позволяют выполнять операции по диапазонам. Понимание того, как устроен индекс, помогает избежать ошибок — например, почему сортировка по неиндексированному полю превращается в дорогостоящую операцию, или почему сложно оптимизировать запросы, которые фильтруют по нескольким колонкам с разной селективностью.</p>
<h2>Транзакционная изоляция</h2>
<p>Даже в относительно простых системах важно понимать основы транзакционной изоляции — те уровни, которые определяют, какие аномалии допускаются при одновременных операциях. <strong>Read Uncommitted</strong> почти не используется, <strong>Read Committed</strong> обеспечивает базовую защиту от грязных чтений, <strong>Repeatable Read</strong> предотвращает непредсказуемые изменения данных в пределах одной транзакции, а <strong>Serializable</strong> создаёт иллюзию последовательного выполнения операций. На практике полный <strong>Serializable</strong> слишком дорог, и большинство систем выбирают компромисс — изоляцию, которая устраняет самые опасные аномалии, но не мешает масштабированию.</p>
<h1>API Gateway vs. Load Balancer</h1>
<p><strong>Load Balancer (Балансировщик нагрузки)</strong> — это система, отвечающая за распределение входящего трафика между несколькими серверами, скрытыми за одной точкой входа. Его можно представить как регулировщика движения, который направляет автомобили (запросы) по параллельным полосам (серверам), не допуская образования заторов в сети.</p>
<p>Когда балансировщик обнаруживает всплеск API-запросов, он может автоматически перенаправлять их на подходящие серверы приложений, не позволяя одному узлу быть перегруженным пользовательскими запросами.</p>
<p>Цель балансировщика нагрузки — оптимизировать использование ресурсов, снизить задержки и устранить единые точки отказа. Балансировщики могут работать на разных уровнях модели OSI — от уровня 4 (транспортного) до уровня 7 (прикладного).</p>
<p>На уровне 4 балансировщики опираются на базовую сетевую информацию, такую как IP-адреса и порты, и принимают решения о распределении трафика на основе простых правил. Балансировщики уровня 7 работают на уровне приложения, анализируя HTTP-заголовки, cookies и другие данные запросов, чтобы более осмысленно распределять трафик.</p>
<p>В реальных системах широко используются такие инструменты, как Nginx, HAProxy, а также механизмы балансировки нагрузки, предоставляемые публичными облачными провайдерами. Балансировщики особенно важны в средах с высоким трафиком, например на e-commerce-платформах, где они помогают гарантировать, что ни один сервер не будет перегружен запросами, особенно в периоды пиковых нагрузок — во время флеш-распродаж или сезонов активных покупок.</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/12/Load_Balancer.jpeg"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2518" src="https://datatalks.ru/wp-content/uploads/2025/12/Load_Balancer.jpeg" alt="" width="1312" height="871" srcset="https://datatalks.ru/wp-content/uploads/2025/12/Load_Balancer.jpeg 1312w, https://datatalks.ru/wp-content/uploads/2025/12/Load_Balancer-300x199.jpeg 300w, https://datatalks.ru/wp-content/uploads/2025/12/Load_Balancer-1024x680.jpeg 1024w, https://datatalks.ru/wp-content/uploads/2025/12/Load_Balancer-768x510.jpeg 768w, https://datatalks.ru/wp-content/uploads/2025/12/Load_Balancer-450x299.jpeg 450w, https://datatalks.ru/wp-content/uploads/2025/12/Load_Balancer-780x518.jpeg 780w" sizes="(max-width: 1312px) 100vw, 1312px" /></a></p>
<p><strong>API Gateway</strong> — это центральная точка управления, которая контролирует и направляет API-трафик между клиентами и микросервисами, обеспечивая эффективное и безопасное взаимодействие. В отличие от балансировщика нагрузки, который лишь распределяет запросы, API Gateway интеллектуально оркестрирует API-вызовы, маршрутизирует запросы к соответствующим backend-сервисам и позволяет применять единые политики управления API. Вместе API Gateway и продвинутый подход к API-менеджменту позволяют объединить микросервисы за единым интерфейсом.</p>
<p>API Gateway также предоставляет расширенные возможности, такие как ограничение скорости запросов (rate limiting), управление аутентификацией и авторизацией, трансформацию запросов, кеширование, аналитику и многое другое. В микросервисных средах внедрение gateway снижает сложность системы за счёт централизации сквозных задач и упрощения управления. Вместо того чтобы реализовывать эти механизмы в каждом микросервисе по отдельности, gateway выступает в роли единой точки входа, упрощая эксплуатацию и повышая сопровождаемость системы.</p>
<p>В типичном сценарии клиент отправляет запрос в API Gateway. Gateway анализирует запрос, проверяет токены безопасности, при необходимости переписывает или трансформирует данные и затем маршрутизирует запрос в соответствующий микросервис. Когда микросервис возвращает ответ, gateway может преобразовать полезную нагрузку обратно для клиента или применить логику кеширования для повышения производительности. Таким образом, API Gateway — это не просто инструмент для равномерного распределения запросов, а компонент, который контролирует и посредничает во всём взаимодействии между клиентами и backend-сервисами.</p>
<p>Во многих зрелых микросервисных экосистемах API Gateway часто используется совместно с решениями для API-менеджмента, чтобы упростить управление, анализ использования и применение политик.</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/12/api_gateway.jpeg"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2519" src="https://datatalks.ru/wp-content/uploads/2025/12/api_gateway.jpeg" alt="" width="1198" height="867" srcset="https://datatalks.ru/wp-content/uploads/2025/12/api_gateway.jpeg 1198w, https://datatalks.ru/wp-content/uploads/2025/12/api_gateway-300x217.jpeg 300w, https://datatalks.ru/wp-content/uploads/2025/12/api_gateway-1024x741.jpeg 1024w, https://datatalks.ru/wp-content/uploads/2025/12/api_gateway-768x556.jpeg 768w, https://datatalks.ru/wp-content/uploads/2025/12/api_gateway-450x326.jpeg 450w, https://datatalks.ru/wp-content/uploads/2025/12/api_gateway-780x564.jpeg 780w" sizes="(max-width: 1198px) 100vw, 1198px" /></a></p>
<table>
<thead>
<tr>
<th>Аспект</th>
<th><strong>Load Balancer</strong></th>
<th><strong>API Gateway</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Основная роль</strong></td>
<td>Распределяет входящий трафик между серверами/инстансами для равномерной нагрузки и отказоустойчивости.</td>
<td>Центральная точка управления API-вызовами между клиентами и микросервисами, с возможностью политики и обработки запросов.</td>
</tr>
<tr>
<td><strong>Что делает</strong></td>
<td>Перенаправляет запросы на свободные узлы, балансирует нагрузку.</td>
<td>Анализирует запросы, маршрутизует, проверяет безопасность, трансформирует данные.</td>
</tr>
<tr>
<td><strong>Безопасность</strong></td>
<td>Может выполнять SSL/TLS termination, базовое SSL шифрование.</td>
<td>Поддерживает аутентификацию, авторизацию, rate limiting, WAF-правила.</td>
</tr>
<tr>
<td><strong>Границы видимости</strong></td>
<td>В основном невидим для клиента, работает «за кулисами».</td>
<td>Выступает официальной клиентской точкой входа, видной внешнему миру.</td>
</tr>
<tr>
<td><strong>Уровень OSI</strong></td>
<td>Часто слой 4 (TCP/UDP), может быть и Layer 7 (HTTP маршрутизация).</td>
<td>В основном Layer 7, работает с API-уровнем (HTTP, WebSocket, иногда gRPC).</td>
</tr>
<tr>
<td><strong>Обработка запросов</strong></td>
<td>Простейшие правила распределения, иногда по URL/заголовкам.</td>
<td>Гибкое роутирование по пути, заголовкам, версиям API, преобразование запросов.</td>
</tr>
<tr>
<td><strong>Мониторинг и аналитика</strong></td>
<td>Базовый health-check, статистика распределения.</td>
<td>Глубокие метрики API: latency, использование, ошибки.</td>
</tr>
<tr>
<td><strong>Сложность и стоимость</strong></td>
<td>Проще, обычно дешевле.</td>
<td>Сложнее, богаче функционально, может быть дороже.</td>
</tr>
<tr>
<td><strong>Расширяемость</strong></td>
<td>Масштабирует распределение нагрузки.</td>
<td>Масштабирует API-контроль, политики, безопасность.</td>
</tr>
</tbody>
</table>
<p><strong>Когда выбирать Load Balancer:</strong></p>
<p>Если ваша задача — просто распределять трафик между несколькими серверами или контейнерами, обеспечивать отказоустойчивость и равномерное использование ресурсов, то балансировщик — это ваш инструмент. Он отлично подходит для классических приложений с высокой нагрузкой, контейнерных сервисов, где важна горизонтальная масштабируемость, а логика распределения не требует глубокого анализа запросов.</p>
<p><strong>Когда выбирать API Gateway:</strong></p>
<p>Если вы строите микросервисную архитектуру и хотите единый интерфейс для всех API, централизовать безопасность, авторизацию, throttle/rate limiting, логирование, трансформацию запросов или предоставлять разные версии API, то API Gateway — более подходящее решение. Он выступает «консьержем API», скрывая сложность микросервисов от клиентов и обеспечивая единообразное управление трафиком и политиками.</p>
<p><strong>Когда использовать оба вместе:</strong></p>
<p>В больших продуктах часто применяют сочетание API Gateway + Load Balancer. Например, Load Balancer распределяет трафик по нескольким экземплярам API Gateway для отказоустойчивости и масштабирования, а сам Gateway выполняет логические проверки и маршрутизацию к микросервисам. В других архитектурах Gateway сначала обрабатывает запрос, а затем передаёт его на Load Balancer внутри кластера микросервисов.</p>
<h1>Rate limiting / throttling</h1>
<p><strong>Rate limiting / throttling</strong> — это механизм контроля количества запросов, которые клиент или группа клиентов может отправить в систему за определённый промежуток времени. Его цель — защитить сервисы от перегрузки, злоупотреблений и неравномерного использования ресурсов, сохранив стабильность и предсказуемость работы системы.</p>
<p>В основе rate limiting лежит простая идея: любые вычислительные ресурсы конечны, и если не ограничивать входящий поток запросов, один пользователь, бот или ошибка в коде могут исчерпать эти ресурсы и повлиять на всех остальных. Throttling позволяет системе вежливо, но жёстко сказать: «Ты уже отправил достаточно запросов, подожди немного».</p>
<p>С точки зрения поведения системы rate limiting проявляется в том, что при превышении лимита новые запросы либо отклоняются с ошибкой вроде HTTP 429 Too Many Requests, либо обрабатываются с задержкой. Это создаёт предсказуемый потолок нагрузки и позволяет сервису продолжать работу даже в условиях пикового трафика или атаки.</p>
<p>В распределённых системах rate limiting часто применяется на границе системы — в API Gateway, балансировщике нагрузки или edge-инфраструктуре. Это позволяет отсекать избыточный трафик ещё до того, как он достигнет внутренних сервисов и баз данных. Например, публичное API может разрешать 100 запросов в минуту на один API-ключ, чтобы ни один клиент не мог монополизировать систему.</p>
<p>Существует несколько распространённых алгоритмов rate limiting. <strong>Fixed window</strong> ограничивает количество запросов в жёстких временных окнах, но может допускать всплески на границе окон. Sliding window сглаживает эти эффекты, учитывая реальное распределение запросов во времени. Token bucket и leaky bucket моделируют систему как сосуд, в который поступают «токены» с фиксированной скоростью, позволяя временные пики, но контролируя среднюю нагрузку. Эти модели широко используются в реальных системах, потому что они хорошо балансируют гибкость и контроль.</p>
<p>Важно понимать разницу между rate limiting и throttling. Rate limiting обычно означает жёсткое ограничение — запросы сверх лимита просто отклоняются. Throttling чаще подразумевает мягкое управление скоростью — система может замедлять ответы, ставить запросы в очередь или постепенно снижать пропускную способность. На практике эти термины часто используются вместе, потому что решают одну и ту же задачу — защиту системы от перегрузки.</p>
<p>В реальных продуктах rate limiting применяется повсеместно. Stripe ограничивает частоту запросов к платёжным API, GitHub — к своим публичным endpoint’ам, а облачные провайдеры используют throttling для защиты инфраструктуры от внезапных всплесков нагрузки. Даже внутри микросервисных архитектур <strong>rate limiting</strong> важен: он предотвращает ситуации, когда один сервис начинает агрессивно дергать другой и провоцирует каскадные отказы.</p>
<h1>Kafka и RabbitMQ</h1>
<p><strong>Kafka</strong> и <strong>RabbitMQ</strong> решают похожую задачу — передачу сообщений между компонентами системы, — но делают это принципиально по-разному. Понимание этой разницы важно для системного дизайна, потому что выбор между ними влияет на масштабируемость, модель обработки данных и даже на то, как система будет эволюционировать со временем.</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/12/kafka_rabbitmq_activemq.gif"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2573" src="https://datatalks.ru/wp-content/uploads/2025/12/kafka_rabbitmq_activemq.gif" alt="" width="800" height="1000" /></a></p>
<p><strong>RabbitMQ</strong> — это классический брокер сообщений. Его модель ближе всего к очереди задач: продюсер отправляет сообщение, брокер гарантирует его доставку, а консьюмер забирает сообщение и подтверждает обработку. После подтверждения сообщение исчезает. RabbitMQ активно управляет сообщениями: он знает, кто их получил, кто подтвердил, кому нужно сделать retry, а кому отправить сообщение в dead-letter очередь. Это делает RabbitMQ отличным выбором для систем, где важна надёжная доставка каждой задачи и сложная логика маршрутизации. Например, фоновые задачи вроде отправки email, обработки платежей, генерации отчётов или работы с внешними API часто реализуются через RabbitMQ. Здесь критично, чтобы каждое сообщение было обработано ровно один раз или, как минимум, не потерялось.</p>
<p><strong>Kafka</strong> работает по другой философии. Это не столько очередь, сколько распределённый лог событий. Сообщения в Kafka не удаляются после чтения — они сохраняются в топике в течение заданного времени или до достижения лимита размера. Консьюмеры сами отслеживают, что они прочитали, управляя своим offset. Это позволяет нескольким независимым сервисам читать одни и те же данные, каждый в своём темпе, не мешая друг другу. Kafka отлично подходит для систем, где важен поток событий, высокая пропускная способность и возможность повторного воспроизведения данных. LinkedIn, Uber и Netflix используют Kafka как основу для аналитики, построения фидов, логирования и event-driven архитектур.</p>
<p>С точки зрения производительности Kafka рассчитана на очень большие объёмы данных и высокую скорость записи — сотни тысяч или миллионы сообщений в секунду. Она масштабируется горизонтально за счёт партиций и распределения нагрузки между брокерами. RabbitMQ тоже масштабируется, но его сильная сторона не в throughput, а в гибкости: сложные схемы routing, topic-exchange, приоритеты сообщений, подтверждения и ретраи.</p>
<p>Есть и различие в характере гарантий доставки. RabbitMQ ориентирован на надёжность обработки каждой задачи и богатую семантику доставки. Kafka чаще используется в модели «at-least once» или «exactly once» (при определённой настройке), но с акцентом на потоковую обработку, а не на индивидуальные задания. В Kafka допустима идея, что сообщение может быть обработано повторно, и система должна быть к этому готова.</p>
<p>Если упростить, RabbitMQ — это инструмент для команд и задач, Kafka — инструмент для событий и потоков данных. Если нужно гарантированно выполнить конкретное действие — например, списать деньги или отправить письмо — чаще выбирают RabbitMQ. Если же нужно зафиксировать факт события и позволить многим сервисам независимо на него реагировать — например, «пользователь сделал заказ» или «поездка завершена» — Kafka становится естественным выбором.</p>
<p>В реальных архитектурах эти системы нередко используются вместе. Kafka может служить основным «хребтом» событийной архитектуры, а RabbitMQ — обслуживать операционные задачи и фоновые процессы. Понимание их различий позволяет осознанно выбирать инструмент под конкретную задачу, а не поддаваться моде или привычке.</p>
<table>
<thead>
<tr>
<th>Критерий</th>
<th><strong>Kafka</strong></th>
<th><strong>RabbitMQ</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td>Основная идея</td>
<td>Распределённый лог событий</td>
<td>Классический брокер сообщений</td>
</tr>
<tr>
<td>Тип данных</td>
<td>Поток событий (event stream)</td>
<td>Очереди сообщений (tasks/commands)</td>
</tr>
<tr>
<td>Модель обработки</td>
<td>Consumer сам управляет offset</td>
<td>Broker управляет доставкой</td>
</tr>
<tr>
<td>Удаление сообщений</td>
<td>Хранятся заданное время</td>
<td>Удаляются после ack</td>
</tr>
<tr>
<td>Повторное чтение</td>
<td>Да (replay)</td>
<td>Нет (по умолчанию)</td>
</tr>
<tr>
<td>Throughput</td>
<td>Очень высокий (100k+ msg/s)</td>
<td>Ниже, но стабильный</td>
</tr>
<tr>
<td>Latency</td>
<td>Чуть выше (ms)</td>
<td>Ниже (sub-ms – ms)</td>
</tr>
<tr>
<td>Масштабирование</td>
<td>Горизонтальное (partitioning)</td>
<td>Ограниченное, сложнее</td>
</tr>
<tr>
<td>Гарантии доставки</td>
<td>At-least-once, exactly-once</td>
<td>At-least-once, at-most-once</td>
</tr>
<tr>
<td>Routing</td>
<td>Минимальный</td>
<td>Очень гибкий (exchanges)</td>
</tr>
<tr>
<td>Message ordering</td>
<td>Внутри партиции</td>
<td>Внутри очереди</td>
</tr>
<tr>
<td>Fan-out (1 → many)</td>
<td>Нативно (consumer groups)</td>
<td>Через exchanges</td>
</tr>
<tr>
<td>Retention</td>
<td>По времени / размеру</td>
<td>Нет retention</td>
</tr>
<tr>
<td>DLQ (dead letter queue)</td>
<td>Через отдельные топики</td>
<td>Встроено</td>
</tr>
<tr>
<td>Использование памяти</td>
<td>Disk-first</td>
<td>Memory-first</td>
</tr>
<tr>
<td>Типичные данные</td>
<td>Логи, события, метрики</td>
<td>Задачи, команды</td>
</tr>
<tr>
<td>Сложность эксплуатации</td>
<td>Выше</td>
<td>Ниже</td>
</tr>
<tr>
<td>Типичный стек</td>
<td>Kafka + ZooKeeper/KRaft</td>
<td>RabbitMQ cluster</td>
</tr>
<tr>
<td>Примеры компаний</td>
<td>LinkedIn, Uber, Netflix</td>
<td>GitHub, Instagram, Airbnb</td>
</tr>
</tbody>
</table>
<h2>Когда выбирать Kafka</h2>
<p><strong>Kafka подходит, если:</strong></p>
<ul>
<li>нужен event-driven подход</li>
<li>требуется очень высокий throughput</li>
<li>несколько сервисов должны читать одни и те же события</li>
<li>нужна возможность переигрывать события</li>
<li>строится аналитика или стриминг</li>
<li>данные — это «история событий»</li>
</ul>
<p><strong>Примеры:</strong></p>
<ul>
<li>аналитика пользовательских действий</li>
<li>feed generation</li>
<li>логирование</li>
<li>real-time metrics</li>
<li>event sourcing</li>
</ul>
<h2>Когда выбирать RabbitMQ</h2>
<p><strong>RabbitMQ подходит, если:</strong></p>
<ul>
<li>нужна гарантированная обработка каждой задачи</li>
<li>важна низкая задержка</li>
<li>требуется сложная маршрутизация</li>
<li>есть retry, DLQ, приоритеты</li>
<li>задачи короткие и независимые</li>
</ul>
<p><strong>Примеры:</strong></p>
<ul>
<li>отправка email/SMS</li>
<li>фоновые job’ы</li>
<li>обработка заказов</li>
<li>интеграции с внешними API</li>
<li>task queues (Celery)</li>
</ul>
<h2>Fan-out и Fan-in</h2>
<p><strong>Fan-out и Fan-in</strong> — это базовые паттерны распределённых систем, которые описывают, как запросы или события расходятся и сходятся между компонентами системы. Эти понятия часто встречаются в системном дизайне, очередях сообщений, микросервисах и стриминговых архитектурах.</p>
<ul>
<li><strong>Fan-out:</strong> один источник &#8212; много получателей.</li>
<li><strong>Fan-in:</strong> много источников &#8212; один агрегатор.</li>
</ul>
<h3>Fan-out</h3>
<p><strong>Fan-out</strong> — это ситуация, когда одно событие или запрос «размножается» и отправляется сразу нескольким получателям. Источник генерирует одно сообщение, а система доставки гарантирует, что его получат все заинтересованные сервисы. В этом паттерне отправитель не знает и не должен знать, сколько получателей существует и кто они именно.</p>
<p>Fan-out используется, когда один факт в системе должен вызвать несколько независимых реакций. Например, пользователь зарегистрировался. Это одно событие, но на него могут реагировать разные сервисы: один отправляет приветственное письмо, другой создаёт профиль, третий обновляет аналитику, четвёртый инициирует рекомендации. Все эти действия должны происходить параллельно и не блокировать друг друга.</p>
<p>В Kafka fan-out реализуется естественным образом через consumer groups: одно и то же событие читается разными группами консьюмеров, каждая из которых обрабатывает его по-своему. В RabbitMQ fan-out достигается с помощью exchange, который копирует сообщение во множество очередей. В HTTP-мире fan-out встречается, когда API Gateway делает несколько внутренних вызовов, собирая данные из разных сервисов.</p>
<h3>Fan-in</h3>
<p><strong>Fan-in</strong> — это обратный процесс: несколько источников данных сходятся в одну точку обработки. Система получает события или результаты от множества сервисов и агрегирует их, объединяет или синхронизирует.</p>
<p>Fan-in применяется, когда нужно собрать данные из разных частей системы и получить единый результат. Например, при формировании news feed сервис может получать посты от сервиса подписок, рекламы, рекомендаций и трендов, а затем объединять их в одну ленту. В аналитике fan-in используется для агрегации логов, метрик и событий со множества узлов в один поток для обработки или хранения.</p>
<p>В Kafka fan-in выглядит как множество продюсеров, пишущих в один топик. В RabbitMQ — как несколько продюсеров, отправляющих сообщения в одну очередь. В микросервисах fan-in часто реализуется на уровне сервиса-агрегатора или API Gateway, который собирает ответы от нескольких backend-сервисов и возвращает клиенту единый результат.</p>
<h1>Caching (Кеширование)</h1>
<p><strong>Кеширование</strong> — это один из ключевых инструментов системного дизайна, позволяющий снизить задержки, уменьшить нагрузку на базу данных и повысить устойчивость системы под высокой нагрузкой. В основе кеширования лежит идея временного хранения часто запрашиваемых или дорогих в вычислении данных ближе к месту использования. Однако важно не просто «добавить кеш», а выбрать правильный паттерн, потому что каждый из них по-разному влияет на согласованность данных, сложность системы и сценарии отказов.</p>
<p>Самый распространённый и интуитивно понятный подход — <strong>Cache Aside</strong>. В этом паттерне ответственность за работу с кешем полностью лежит на приложении. Когда приходит запрос, приложение сначала проверяет кеш. Если данные там есть, они сразу возвращаются клиенту. Если данных нет, приложение идёт в базу данных, получает результат и кладёт его в кеш, чтобы последующие запросы были быстрее. При записи или обновлении данных приложение сначала пишет в базу, а затем инвалидирует или обновляет кеш. Этот подход прост, прозрачен и хорошо контролируем, поэтому он используется в большинстве веб-приложений. Его слабое место — риск временной неконсистентности и cache stampede, когда множество запросов одновременно промахиваются мимо кеша.</p>
<p><strong>Read-Through Cache</strong> переносит логику загрузки данных в сам кеш. Приложение всегда обращается только к кешу, а при промахе кеш сам идёт в базу данных, загружает данные и возвращает их приложению. Для разработчика это выглядит как доступ к одному источнику данных, что упрощает код. Такой подход часто реализуется в managed-решениях или библиотеках, тесно интегрированных с хранилищем. Однако он усложняет инфраструктуру и снижает прозрачность: приложение меньше контролирует, когда и как происходят обращения к базе.</p>
<p><strong>Write-Through Cache</strong> ориентирован на согласованность данных при записи. Когда приложение сохраняет данные, оно сначала пишет их в кеш, а кеш синхронно записывает изменения в базу данных. Благодаря этому чтения всегда получают актуальные данные из кеша, а риск рассинхронизации минимален. Цена за это — более высокая задержка на запись, так как операция считается завершённой только после записи в базу. Такой паттерн подходит для систем, где корректность данных важнее скорости записи.</p>
<p><strong>Write-Behind (или Write-Back)</strong> делает шаг в сторону производительности. Запись сначала происходит только в кеш, а в базу данные сохраняются асинхронно, с задержкой или батчами. Это резко ускоряет операции записи и хорошо работает под высокой нагрузкой. Но взамен система становится более сложной и менее надёжной: при падении кеша есть риск потери данных, а база данных временно содержит устаревшее состояние. Этот подход применяют там, где допустима eventual consistency и где кеш можно сделать надёжным, например с журналированием или репликацией.</p>
<p><strong>Materialized Views</strong> выходят за рамки классического кеша, но решают похожую задачу — ускорение чтения. Вместо того чтобы каждый раз выполнять сложные JOIN’ы или агрегации, система заранее вычисляет результат запроса и хранит его в виде отдельной таблицы или представления. При чтении данные отдаются мгновенно, но за это приходится платить сложностью обновления: при изменении исходных данных materialized view нужно пересчитывать полностью или инкрементально. Такой подход часто используется в аналитических системах, отчетах и read-heavy сценариях.</p>
<p><strong>Event Sourcing</strong> — это более фундаментальный архитектурный паттерн, в котором состояние системы не хранится напрямую, а восстанавливается из последовательности событий. Сами события являются единственным источником истины, а текущее состояние и производные представления могут кешироваться или храниться как materialized views. В этом контексте кеширование становится способом ускорить восстановление состояния и чтение, а не источником данных. Event sourcing даёт отличную масштабируемость и трассируемость изменений, но значительно усложняет систему и требует зрелого подхода к консистентности и миграциям.</p>
<p>В реальных системах эти подходы редко используются поодиночке. Чаще всего <strong>Cache Aside</strong> применяется для простых <strong>CRUD-операций</strong>, <strong>write-through или write-behind</strong> — для горячих данных с высокой нагрузкой на запись, <strong>materialized views</strong> — для сложных запросов, а <strong>event sourcing</strong> — для доменных ядер, где важна история изменений.</p>
<h2>Кеширующие системы, которые должен знать каждый разработчик</h2>
<p>Данные кешируются повсюду — от клиентской стороны до backend-систем. Рассмотрим основные уровни кеширования, которые используются для оптимизации производительности.</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/12/Cache_Systems-scaled.jpeg"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2572" src="https://datatalks.ru/wp-content/uploads/2025/12/Cache_Systems-scaled.jpeg" alt="" width="1755" height="2560" srcset="https://datatalks.ru/wp-content/uploads/2025/12/Cache_Systems-scaled.jpeg 1755w, https://datatalks.ru/wp-content/uploads/2025/12/Cache_Systems-206x300.jpeg 206w, https://datatalks.ru/wp-content/uploads/2025/12/Cache_Systems-702x1024.jpeg 702w, https://datatalks.ru/wp-content/uploads/2025/12/Cache_Systems-768x1120.jpeg 768w, https://datatalks.ru/wp-content/uploads/2025/12/Cache_Systems-1053x1536.jpeg 1053w, https://datatalks.ru/wp-content/uploads/2025/12/Cache_Systems-1404x2048.jpeg 1404w, https://datatalks.ru/wp-content/uploads/2025/12/Cache_Systems-450x656.jpeg 450w, https://datatalks.ru/wp-content/uploads/2025/12/Cache_Systems-780x1138.jpeg 780w, https://datatalks.ru/wp-content/uploads/2025/12/Cache_Systems-1600x2333.jpeg 1600w" sizes="(max-width: 1755px) 100vw, 1755px" /></a></p>
<h3>Слои кеширования</h3>
<p>Клиентские приложения. Браузеры кешируют HTTP-ответы. Серверные ответы содержат директивы кеширования в заголовках. При последующих запросах браузер может отдать данные из кеша, если они всё ещё считаются актуальными.</p>
<p>Сети доставки контента (CDN). CDN кешируют статический контент — изображения, стили, JavaScript-файлы. Контент обслуживается с серверов, расположенных ближе к пользователям, что снижает задержки и ускоряет загрузку страниц.</p>
<p>Балансировщики нагрузки. Некоторые балансировщики способны кешировать часто запрашиваемые данные. Это позволяет отдавать ответы без обращения к backend-серверам, снижая нагрузку и уменьшая время отклика.</p>
<p>Брокеры сообщений. Системы вроде Kafka кешируют сообщения на диске в соответствии с политикой хранения (retention). Потребители затем читают сообщения в удобном для них темпе.</p>
<p>Сервисы. Отдельные сервисы часто используют кеширование для ускорения доступа к данным, сначала проверяя in-memory кеш перед обращением к базе данных. Также может применяться дисковое кеширование для больших объёмов данных.</p>
<p>Распределённые кеши. Системы вроде Redis кешируют пары ключ–значение и используются несколькими сервисами одновременно, обеспечивая более быстрые операции чтения и записи по сравнению с традиционными базами данных.</p>
<p>Полнотекстовые поисковые движки. Платформы вроде Elasticsearch индексируют данные для быстрого текстового поиска. Такой индекс по сути является формой кеша, оптимизированного под быстрый поиск по тексту.</p>
<p>Базы данных. Внутри СУБД существуют специализированные механизмы повышения производительности, многие из которых используют идеи кеширования.</p>
<h3>Механизмы кеширования в базах данных</h3>
<p>Buffer Pool. Это внутренний кеш базы данных, который хранит копии страниц данных в памяти. Он позволяет быстро читать и записывать данные во временное хранилище в RAM, снижая необходимость обращаться к диску.</p>
<p>Materialized Views. Материализованные представления похожи на кеши тем, что хранят результаты вычислительно дорогих запросов. База данных может быстро вернуть заранее вычисленные результаты вместо того, чтобы пересчитывать их каждый раз.</p>
<p>В совокупности эти уровни кеширования образуют многоуровневую систему оптимизации, где каждый слой уменьшает задержки и нагрузку на следующий, более «глубокий» уровень инфраструктуры.</p>
<h2>Примеры систем кеширования</h2>
<p>На уровне приложений чаще всего используются специализированные in-memory хранилища, такие как <strong>Redis</strong> и <strong>Memcached</strong>. Они работают как отдельные сервисы, хранящие данные в оперативной памяти и предоставляющие доступ к ним по сети с минимальной задержкой. Приложение при чтении сначала обращается к кешу и только при промахе идёт в основное хранилище. Redis, в отличие от Memcached, поддерживает сложные структуры данных, персистентность, репликацию и механизмы отказоустойчивости, поэтому его часто используют не только как кеш, но и как вспомогательное хранилище состояния. Memcached проще и быстрее в эксплуатации, но ограничен моделью ключ-значение и не сохраняет данные при перезапуске.</p>
<p>Следующий важный уровень — CDN (Content Delivery Network), такие как Cloudflare или AWS CloudFront. CDN кеширует статический и полу-статический контент — изображения, стили, скрипты, видео — на edge-серверах, физически близких к пользователю. Когда клиент запрашивает ресурс, запрос не доходит до основного сервера, а обслуживается ближайшей CDN-нодой. Это резко снижает latency и снимает нагрузку с backend’а. Управление таким кешем обычно осуществляется через HTTP-заголовки Cache-Control, Expires и ETag, которые определяют, сколько времени контент считается валидным.</p>
<p>На стороне пользователя работает кеш браузера, который хранит ресурсы локально и повторно использует их без сетевого запроса. Этот уровень кеширования полностью прозрачен для backend’а, но оказывает огромное влияние на производительность и восприятие скорости приложения. Грамотно настроенный браузерный кеш позволяет загружать страницы практически мгновенно, но требует аккуратной стратегии инвалидирования, особенно при деплое новых версий фронтенда.</p>
<p>Часто кеширование реализуется и на уровне самого приложения — в виде локального in-process кеша. Такие кеши живут в памяти процесса и дают минимальную задержку, но плохо масштабируются и не подходят для распределённых систем без дополнительной синхронизации. Их используют для небольших, редко меняющихся данных или как дополнительный слой поверх Redis для самых горячих запросов.</p>
<p>Отдельный класс — кеши на уровне базы данных и операционной системы. Современные СУБД активно кешируют данные в памяти, используя page cache, buffer pool и другие внутренние механизмы. Операционная система также кеширует файловые операции, снижая количество реальных обращений к диску. Эти кеши управляются автоматически и обычно не контролируются напрямую разработчиком, но они существенно влияют на производительность и должны учитываться при проектировании.</p>
<p>Общим механизмом управления всеми уровнями кеширования являются политики времени жизни данных — TTL (time to live), eviction-алгоритмы вроде LRU или LFU и стратегии инвалидирования. TTL ограничивает срок актуальности данных и предотвращает использование слишком устаревших значений. Eviction-алгоритмы определяют, какие данные будут удалены первыми при нехватке памяти. Инвалидация гарантирует, что изменения в основном хранилище рано или поздно отразятся в кеше.</p>
<h1>Stream Processing</h1>
<p><strong>Коротко:</strong></p>
<ul>
<li><strong>Flink</strong> — для сложного stateful streaming с сильными гарантиями,</li>
<li><strong>Spark Streaming</strong> — для аналитического стриминга с упором на batch-модель,</li>
<li><strong>Storm</strong> — для экстремально низкой задержки ценой сложности и ручного управления состоянием.</li>
</ul>
<h2>Apache Flink</h2>
<p><strong>Apache Flink</strong> — это мощный движок для потоковой обработки данных с сильными гарантиями по состоянию (state) и обработке событий. При этом Flink — скорее фреймворк, чем готовое прикладное решение. Это означает, что почти всю доменную логику разработчик реализует сам: агрегации, оконные функции, управление состоянием, очистку устаревших данных и интеграцию пайплайнов с другими системами.</p>
<p>С точки зрения системного дизайна Flink ценен тем, что он ориентирован на true streaming — обработку событий по мере их поступления, а не батчами. Он поддерживает состояние операторов, таймеры, event-time processing и гарантии exactly-once. Для хранения состояния часто используется RocksDB, что позволяет работать с большими объёмами данных и делать инкрементальные чекпоинты. Однако само управление жизненным циклом состояния — например, TTL для пользовательских профилей или удаление данных после периода неактивности — требует явной реализации со стороны разработчика.</p>
<p><strong>Масштабирование Flink</strong> — нетривиальная задача. Добавление или удаление узлов обычно требует сохранения состояния (savepoint) и перезапуска джоба. Начиная с версии 1.13 появился Reactive Mode, который позволяет полуавтоматически масштабировать систему за счёт перезапуска с новыми ресурсами, но настоящее zero-downtime масштабирование пока недоступно. Поэтому Flink хорошо подходит для долгоживущих стриминговых задач с чётко определённой логикой и высокой ценностью состояния, но требует зрелой эксплуатации.</p>
<h2>Apache Spark Streaming</h2>
<p><strong>Apache Spark Streaming</strong>, особенно в режиме Structured Streaming, реализует потоковую обработку через концепцию микробатчей. Вместо обработки каждого события отдельно Spark группирует события в небольшие временные партии и обрабатывает их как мини-батчи. Это упрощает модель исполнения и повторно использует батчевую инфраструктуру Spark, но накладывает ограничения на задержку.</p>
<p>С точки зрения system design ключевой компромисс Spark Streaming — это latency vs simplicity. Микробатчи означают, что минимальная задержка измеряется секундами, а не миллисекундами. Для задач, где требуется реакция «здесь и сейчас», такой подход не подходит. Зато Spark отлично справляется с тяжёлыми агрегациями, оконными вычислениями и интеграцией с аналитическим стеком.</p>
<p>Как и Flink, Spark Streaming — это фреймворк, а не готовое решение. Разработчику необходимо самостоятельно описывать окна, счётчики, агрегации и заботиться о том, как результаты стриминга будут использоваться дальше — например, в обучении моделей или аналитике. Spark хорошо подходит для систем, где стриминг — это продолжение batch-аналитики, а не основа real-time взаимодействия с пользователем.</p>
<h2>Apache Storm</h2>
<p><strong>Apache Storm</strong> создавался с прицелом на ультранизкую задержку. Он обрабатывает события по одному, без микробатчей, что делает его одним из самых быстрых инструментов для настоящей real-time обработки. Это сильное преимущество в сценариях, где важны миллисекунды.</p>
<p>Однако из коробки Storm — stateless-система. Он не хранит контекст между событиями, и если требуется агрегация, подсчёты или оконные метрики, разработчику нужно самостоятельно реализовать хранение состояния — например, во внешней базе данных. Это сильно усложняет архитектуру и повышает связность компонентов.</p>
<p>Storm также не предоставляет богатого набора агрегатных операторов. Любые счётчики, окна, метрики и сложные вычисления нужно писать вручную. Слой Trident добавляет поддержку состояния и частично вводит микробатчи, смягчая ограничения, но даже с Trident Storm остаётся более низкоуровневым и менее удобным, чем современные стриминговые движки. В результате Storm чаще рассматривают как специализированный инструмент для узких real-time задач, а не универсальную стриминговую платформу.</p>
<h2>Что важно знать про streaming для system design</h2>
<p>С точки зрения системного дизайна <strong>стриминговые системы</strong> — это всегда баланс между задержкой, состоянием и сложностью эксплуатации. True streaming (как во Flink или Storm) даёт минимальную latency и точный контроль над event-time, но усложняет масштабирование и управление состоянием. Микробатчевый подход (Spark Streaming) проще в эксплуатации и интеграции с аналитикой, но не подходит для интерактивных сценариев.</p>
<p>Важно также понимать, что стриминговые движки редко работают в одиночку. Обычно они строятся поверх брокеров событий вроде Kafka, используют внешние хранилища для долговременного состояния и формируют materialized views для быстрого чтения. В интервью ценится не знание API конкретного инструмента, а понимание, когда нужен streaming вообще, почему нельзя обойтись batch-обработкой и какие компромиссы система делает ради низкой задержки или высокой надёжности.</p>
<h1>Графовые базы данных (Graph DB)</h1>
<p><strong>Графовые базы данных (Graph DB)</strong> — это специализированные системы хранения данных, оптимизированные для работы со связями между сущностями. В отличие от реляционных баз, где связи реализуются через JOIN’ы, в графовых БД связи являются первоклассными объектами и хранятся напрямую. Это делает операции обхода связей, поиска путей и анализа графов на порядки эффективнее и концептуально проще.</p>
<p>В основе графовой модели лежат вершины (nodes), рёбра (edges) и свойства (properties). Вершины представляют сущности — пользователей, товары, документы. Рёбра описывают отношения между ними — «друг», «купил», «подписан». Свойства дополняют и вершины, и рёбра атрибутами. Такая модель особенно хорошо подходит для доменов, где ключевую роль играют связи, а не табличные агрегаты: социальные сети, рекомендательные системы, antifraud, knowledge graph, IAM и сетевые топологии.</p>
<p>Рынок графовых баз данных сформировался вокруг нескольких зрелых решений. <strong>Neo4j</strong> остаётся безусловным лидером и фактическим стандартом де-факто. Он предлагает зрелую экосистему, язык запросов Cypher, богатые инструменты визуализации и оптимизирован для OLTP-нагрузок с большим количеством обходов графа. Neo4j часто выбирают в enterprise-проектах, где важна стабильность, документация и опыт эксплуатации.</p>
<p><strong>Amazon Neptune</strong> — облачно-ориентированная графовая база от AWS, ориентированная на managed-подход. Она поддерживает модели property graph и RDF, а также языки Gremlin и SPARQL. Neptune хорошо вписывается в экосистему AWS, обеспечивает автоматическое масштабирование и высокую доступность, но при этом уступает Neo4j в гибкости и выразительности запросов. Это типичный выбор для компаний, которые уже глубоко сидят в AWS и хотят минимизировать операционные издержки.</p>
<p><strong>ArangoDB</strong> представляет собой мультимодельную базу данных, сочетающую в себе документную, ключ-значение и графовую модели. Такой подход удобен для систем, где граф — лишь часть общей архитектуры, а не единственный способ доступа к данным. ArangoDB позволяет работать с разными типами данных в рамках одной СУБД, что снижает сложность инфраструктуры, но может быть менее специализированным по сравнению с «чистыми» графовыми решениями.</p>
<p><strong>Dgraph</strong> ориентирован на распределённость и масштабирование «из коробки». Он изначально проектировался как distributed graph database и тесно интегрирован с GraphQL. Это делает его привлекательным для cloud-native и API-ориентированных систем, где важны горизонтальное масштабирование и простой доступ к данным через GraphQL-интерфейсы. При этом Dgraph требует более серьёзного понимания внутренних механизмов и пока менее распространён в enterprise-сегменте.</p>
<p><strong>TigerGraph</strong> специализируется на глубокой аналитике связей и обработке очень больших графов. Его сильная сторона — сложные многопереходные запросы, графовая аналитика и сценарии, где нужно анализировать большие объёмы взаимосвязанных данных за минимальное время. TigerGraph часто используется в антифроде, телеком-аналитике и финансовых системах, где глубина обхода графа имеет критическое значение.</p>
<p>Помимо зрелых решений, рынок активно развивается за счёт новых подходов. PuppyGraph предлагает интересную концепцию — выполнять графовые запросы поверх уже существующих хранилищ, без необходимости миграции данных в отдельную графовую БД. Это снижает барьер входа и упрощает интеграцию. RelationalAI, в свою очередь, объединяет реляционную модель с логическими и AI-ориентированными подходами, расширяя традиционные базы возможностями вывода и анализа сложных зависимостей.</p>
<p>С точки зрения системного дизайна графовые базы данных выбирают тогда, когда основная ценность системы — в связях, а не в отдельных записях. Они плохо подходят для простого CRUD или тяжёлых агрегатов, но незаменимы для задач поиска связей, рекомендаций, обнаружения аномалий и анализа сложных структур. В реальных архитектурах Graph DB часто используются вместе с реляционными и документными базами, выполняя роль специализированного компонента для работы с отношениями.</p>
<h1>Метрики производительности системы</h1>
<p>Ваш API работает медленно. Но насколько именно медленно? Нужны числа. Реальные метрики, которые показывают, что именно сломалось и где это нужно чинить.</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/12/System_Performance_Metrics-scaled.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2574" src="https://datatalks.ru/wp-content/uploads/2025/12/System_Performance_Metrics-scaled.png" alt="" width="2041" height="2560" srcset="https://datatalks.ru/wp-content/uploads/2025/12/System_Performance_Metrics-scaled.png 2041w, https://datatalks.ru/wp-content/uploads/2025/12/System_Performance_Metrics-239x300.png 239w, https://datatalks.ru/wp-content/uploads/2025/12/System_Performance_Metrics-816x1024.png 816w, https://datatalks.ru/wp-content/uploads/2025/12/System_Performance_Metrics-768x963.png 768w, https://datatalks.ru/wp-content/uploads/2025/12/System_Performance_Metrics-1225x1536.png 1225w, https://datatalks.ru/wp-content/uploads/2025/12/System_Performance_Metrics-1633x2048.png 1633w, https://datatalks.ru/wp-content/uploads/2025/12/System_Performance_Metrics-450x564.png 450w, https://datatalks.ru/wp-content/uploads/2025/12/System_Performance_Metrics-780x978.png 780w, https://datatalks.ru/wp-content/uploads/2025/12/System_Performance_Metrics-1600x2007.png 1600w" sizes="(max-width: 2041px) 100vw, 2041px" /></a></p>
<p>Вот четыре ключевые метрики, которые должен понимать каждый инженер при анализе производительности системы:</p>
<ul>
<li><strong>Queries Per Second (QPS)</strong> — количество входящих запросов, которые система обрабатывает за одну секунду. Если сервер получает 1000 запросов за секунду, значит у него 1000 QPS. Звучит просто, пока не становится ясно, что большинство систем не способны долго выдерживать пиковый QPS без начала деградации.</li>
<li><strong>Transactions Per Second (TPS)</strong> — количество завершённых транзакций, которые система обрабатывает за секунду. Транзакция включает полный цикл обработки: запрос отправляется, доходит до базы данных и возвращается с ответом.<br />
TPS показывает фактически выполненную работу, а не просто принятые запросы. Именно на эту метрику чаще всего ориентируется бизнес.</li>
<li><strong>Concurrency (параллелизм)</strong> — количество одновременно активных запросов, которые система обрабатывает в конкретный момент времени. Например, система может получать 100 запросов в секунду, но если каждый запрос выполняется 5 секунд, то одновременно в работе находится 500 запросов.<br />
Высокий уровень параллелизма означает, что системе требуется больше ресурсов, эффективный пул соединений и грамотное управление потоками.</li>
<li><strong>Response Time (RT)</strong> — время, прошедшее с момента начала обработки запроса до получения ответа. Измеряется как на стороне клиента, так и на стороне сервера.</li>
</ul>
<p><strong>Все эти метрики связывает простое соотношение:</strong></p>
<p><code>QPS = Concurrency ÷ Среднее время ответа</code></p>
<p>Больше параллелизма или меньшее время ответа означает более высокую пропускную способность системы.</p>
<h2>Метрики производительности в highload системах</h2>
<p>В highload-системах метрики производительности нужны не «для графиков», а для ответа на конкретные вопросы: где узкое место, что сломается следующим и почему пользователи чувствуют деградацию. Обычно их рассматривают по уровням системы — от клиента до железа — потому что высокая нагрузка почти всегда проявляется каскадно.</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/12/metrics.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2577" src="https://datatalks.ru/wp-content/uploads/2025/12/metrics.png" alt="" width="1075" height="558" srcset="https://datatalks.ru/wp-content/uploads/2025/12/metrics.png 1075w, https://datatalks.ru/wp-content/uploads/2025/12/metrics-300x156.png 300w, https://datatalks.ru/wp-content/uploads/2025/12/metrics-1024x532.png 1024w, https://datatalks.ru/wp-content/uploads/2025/12/metrics-768x399.png 768w, https://datatalks.ru/wp-content/uploads/2025/12/metrics-450x234.png 450w, https://datatalks.ru/wp-content/uploads/2025/12/metrics-780x405.png 780w" sizes="(max-width: 1075px) 100vw, 1075px" /></a></p>
<h3>Клиентский уровень (Client-Side Performance)</h3>
<p>Этот слой отвечает за то, как пользователь ощущает систему, даже если backend формально «жив».</p>
<p>Ключевые метрики здесь — время установления TCP-соединения, время загрузки HTML, CSS, JavaScript и изображений, а также общее HTTP response time и статус ответа. В highload-сценариях проблемы на backend’е быстро проявляются именно здесь: растёт latency, увеличивается число таймаутов, появляются 5xx-ошибки.</p>
<p>Важно понимать, что клиентская деградация может происходить даже при «нормальных» серверных метриках — например, из-за перегруженной сети, плохой работы CDN или большого количества блокирующих ресурсов на фронтенде.</p>
<h3>Сетевой уровень (Network Metrics)</h3>
<p><strong>Сеть</strong> — это кровеносная система highload-архитектуры. На этом уровне смотрят, выдерживает ли инфраструктура объём соединений и трафика.</p>
<p>Основные показатели — количество установленных TCP-соединений, число TCP-сегментов в секунду, количество reset’ов и неудачных попыток соединения. В условиях высокой нагрузки резкий рост TCP resets или connection failures почти всегда указывает на перегрузку балансировщика, exhaustion портов, проблемы с NAT или неправильные таймауты.</p>
<p>Важный сигнал — рост соединений без роста полезного <strong>throughput</strong>: это признак того, что система тратит ресурсы на сетевую инфраструктуру, но не выполняет реальную работу.</p>
<h3>Уровень веб-сервера (Web Server Metrics)</h3>
<p>Этот слой отвечает за приём и первичную обработку запросов. Именно здесь <strong>highload</strong> чаще всего проявляется в виде очередей и отказов.</p>
<p>Критичны метрики очередей запросов, транзакций в секунду, объёма переданных данных, потребления памяти и <strong>cache hit ratio</strong>. Рост <strong>request queue</strong> — один из самых опасных симптомов: он означает, что система уже не справляется с входящим потоком.</p>
<p><strong>Cache hit ratio</strong> здесь играет ключевую роль: при высокой нагрузке даже небольшое падение процента попаданий в кеш может лавинообразно увеличить нагрузку на backend и базу данных.</p>
<h3>Уровень application-сервера</h3>
<p>Это сердце бизнес-логики, и в highload-системах именно здесь чаще всего «горит».</p>
<p>Здесь смотрят на время установления соединений, ожидание соединений (connection wait time), использование памяти, количество потоков, активные и приостановленные транзакции, таймауты и rollback’и.</p>
<p>Рост connection wait time означает, что пул соединений исчерпан. Увеличение active transactions без роста TPS — сигнал о том, что запросы «залипают» внутри системы. Таймауты и rollback’и часто указывают на перегрузку downstream-сервисов или базы данных.</p>
<h3>Связь метрик между собой</h3>
<p>В highload-системах метрики нельзя анализировать изолированно. Например:</p>
<ul>
<li>рост response time почти всегда ведёт к росту concurrency;</li>
<li>рост concurrency увеличивает потребление памяти и количество открытых соединений;</li>
<li>перегруженные application-серверы создают очереди на веб-сервере;</li>
<li>очереди увеличивают клиентскую latency и таймауты.</li>
</ul>
<p><strong>Поэтому инженеры часто используют базовое соотношение:</strong></p>
<p><code>Throughput = Concurrency ÷ Response Time</code></p>
<p>Оно помогает понять, что именно ограничивает систему — скорость обработки или количество одновременно обрабатываемых запросов.</p>
<h3>Что важно на собеседовании</h3>
<p>В контексте system design от тебя ждут не перечисления всех метрик, а понимания:</p>
<ul>
<li>какие метрики сигнализируют о начале деградации;</li>
<li>какие из них указывают на CPU-bound, IO-bound или network-bound систему;</li>
<li>как метрики разных уровней влияют друг на друга.</li>
</ul>
<h1>Обзор паттернов</h1>
<h2>Архитектурные паттерны</h2>
<ul>
<li><strong>Микросервисы</strong> &#8212; Разбиение системы на независимые сервисы, каждый из которых выполняет свою небольшую задачу. Используют Netflix, Uber, Amazon.</li>
<li><strong>Монолит</strong> &#8212; Единое приложение, внутри которого всё тесно связано. Простой старт, сложное масштабирование.</li>
<li><strong>SOA (Service-Oriented Architecture)</strong> &#8212; Предшественник микросервисов — крупные сервисы с чётко определёнными контрактами.</li>
</ul>
<h3>Чем отличается SOA (Service-Oriented Architecture) от микросервисной архитектуры</h3>
<p>SOA (Service-Oriented Architecture) и микросервисная архитектура основаны на одной идее — разбиении системы на сервисы, — но различаются по масштабу, степени связанности и подходу к разработке и эксплуатации.</p>
<p>SOA возникла как enterprise-подход к интеграции крупных корпоративных систем. Сервисы в SOA обычно достаточно крупные, охватывают значимые бизнес-домены и часто разделяют общую инфраструктуру: базы данных, ESB (Enterprise Service Bus), механизмы безопасности и оркестрации. Взаимодействие между сервисами в SOA нередко строится через централизованный слой интеграции, который отвечает за маршрутизацию, трансформацию сообщений и применение политик. Это упрощает управление и стандартизацию, но повышает связанность системы и создаёт единые точки отказа.</p>
<p>Микросервисная архитектура, напротив, делает акцент на максимальную автономность сервисов. Каждый микросервис обычно небольшой, отвечает за один узкий бизнес-контекст и владеет своими данными. Сервисы взаимодействуют напрямую, чаще всего по лёгким протоколам вроде HTTP или через асинхронные события, без тяжёлого централизованного посредника. Это снижает связанность и позволяет независимо разрабатывать, деплоить и масштабировать компоненты системы.</p>
<p>С точки зрения данных различие особенно заметно. В SOA допускается совместное использование баз данных и схем, что упрощает консистентность, но усложняет эволюцию. В микросервисах принцип «database per service» считается базовым, а согласованность достигается через события и eventual consistency.</p>
<p>Отличается и эксплуатационная модель. SOA ориентирована на стабильные, долго живущие сервисы с редкими изменениями и централизованным управлением. Микросервисы проектируются под частые деплои, автоматизацию, CI/CD, контейнеризацию и горизонтальное масштабирование. Это повышает гибкость, но увеличивает операционную сложность.</p>
<h2>Паттерны масштабирования и доступности</h2>
<ul>
<li><strong>Load Balancer</strong> &#8212; Распределяет нагрузку между узлами. Классическая схема — NGINX + несколько backend-сервисов.</li>
<li><strong>Horizontal Scaling</strong> &#8212; Добавляем больше узлов вместо усиления одного.</li>
<li><strong>Sharding</strong> &#8212; Разделение данных по ключу или диапазону между несколькими нодами.</li>
<li><strong>Replication</strong> &#8212; Создание копий данных для отказоустойчивости и быстрого чтения.</li>
<li><strong>Federation / Partitioning</strong> &#8212; Разделение больших сервисов или БД на тематические домены.</li>
</ul>
<h2>Паттерны взаимодействия между сервисами</h2>
<ul>
<li><strong>Request/Response</strong> &#8212; Типичный REST/gRPC вызов.</li>
<li><strong>Event-Driven Architecture</strong> &#8212; Сервисы общаются через события, как в Uber, LinkedIn.</li>
<li><strong>Pub/Sub</strong> &#8212; Публикация событий в брокер (Kafka, Pulsar), подписчики их потребляют.</li>
<li><strong>CQRS (Command Query Responsibility Segregation)</strong> &#8212; Разделение операций записи и чтения. Полезно в high-load системах.</li>
<li><strong>Saga</strong> &#8212; Управление распределёнными транзакциями при помощи цепочки локальных операций и компенсационных действий.</li>
<li><strong>Circuit Breaker</strong> &#8212; Предотвращение каскадных падений: если сервис долго не отвечает, вызовы отключаются.</li>
<li><strong>Retry / Exponential Backoff</strong> &#8212; Повторная отправка запросов с увеличивающимся тайм-аутом.</li>
<li><strong>Bulkhead</strong> &#8212; Изоляция компонентов, чтобы сбой одного не уронил всю систему.</li>
</ul>
<h2>Паттерны данных и кеширования</h2>
<ul>
<li><strong>Cache Aside</strong> &#8212; Приложение сначала проверяет кеш, затем БД. Самый популярный паттерн.</li>
<li><strong>Read-Through Cache</strong> &#8212; Кеш сам ходит в БД при промахе.</li>
<li><strong>Write-Through Cache</strong> &#8212; Запись идёт сначала в кеш, потом в базу.</li>
<li><strong>Write-Behind</strong> &#8212; Запись в БД происходит асинхронно из кеша.</li>
<li><strong>Materialized Views</strong> &#8212; Предварительно пересчитанные данные для быстрых запросов.</li>
<li><strong>Event Sourcing</strong> &#8212; Состояние системы хранится как поток событий.</li>
</ul>
<h2>Паттерны потоковой обработки</h2>
<ul>
<li><strong>Stream Processing</strong> &#8212; Система обрабатывает данные непрерывным потоком. Kafka Streams, Flink.</li>
<li><strong>Lambda Architecture</strong> &#8212; Комбинация batch-обработки и stream-обработки.</li>
<li><strong>Kappa Architecture</strong> &#8212; Только потоковая обработка, без batch-а.</li>
</ul>
<h2>Паттерны отказоустойчивости</h2>
<ul>
<li><strong>Leader Election</strong> &#8212; Выбор ведущего узла (ZooKeeper, Etcd, Consul).</li>
<li><strong>Failover</strong> &#8212; Переход на резервную ноду при отказе основной.</li>
<li><strong>Redundancy</strong> &#8212; Дублирование критичных узлов или подсистем.</li>
</ul>
<h2>Паттерны API и интеграции</h2>
<ul>
<li><strong>API Gateway</strong> &#8212; Единая точка входа в систему (Netflix Zuul, Kong). Добавляет авторизацию, rate limiting, кеш.</li>
<li><strong>Backend for Frontend (BFF)</strong> &#8212; Отдельный backend для каждого типа клиента — мобильного, веба и т. п.</li>
<li><strong>Service Mesh</strong> &#8212; Автоматизированная сетка сервисных коммуникаций: Envoy, Istio.</li>
</ul>
<h2>Паттерны управления состоянием</h2>
<ul>
<li><strong>Stateless</strong> &#8212; Сервисы без состояния легко масштабируются.</li>
<li><strong>Stateful</strong> &#8212; Сервисы, хранящие состояние, требуют репликации или sticky-сессий.</li>
<li><strong>Sticky Sessions</strong> &#8212; Привязка клиента к конкретному серверу при работе со stateful-компонентами.</li>
</ul>
<h2>Паттерны безопасности</h2>
<ul>
<li><strong>OAuth2 / JWT</strong> &#8212; Стандартные механизмы авторизации.</li>
<li><strong>Zero Trust</strong> &#8212; Каждый запрос проверяется, даже внутри частной сети.</li>
<li><strong>Rate Limiting / Throttling</strong> &#8212; Ограничение запросов, чтобы защитить сервисы. <a href="https://rdiachenko.com/posts/arch/rate-limiting/rate-limiting-basics/" target="_blank" rel="noopener">Статья</a></li>
</ul>
<h1>Глоссарий / Термины</h1>
<h2>Общие термины подготовки к интервью</h2>
<ul>
<li><strong>System Design</strong> — проектирование крупномасштабных распределённых систем и архитектурных решений для сложных приложений.</li>
<li><strong>Coding</strong> — подготовка к собеседованиям по алгоритмам и программированию, включая техники решения задач.</li>
<li><strong>Behavioral Interview</strong> — оценка мягких навыков и коммуникативных качеств кандидата.</li>
<li><strong>Tech Interview</strong> — общий термин для всех технических собеседований: coding, system design, behavioral.</li>
<li><strong>FAANG</strong> — аббревиатура ведущих технологических компаний (Facebook/Meta, Apple, Amazon, Netflix, Google), стандарт отрасли.</li>
<li><strong>STAR Method</strong> — структурированный метод ответа на поведенческие вопросы (Situation, Task, Action, Result).</li>
<li><strong>Mock Interviews</strong> — практические имитации интервью для отработки формата и получения обратной связи.</li>
<li><strong>Interview Roadmap</strong> — структурированный план подготовки: coding, design, behavioral.</li>
<li><strong>Interview Bootcamp</strong> — интенсивная программа подготовки с практическими заданиями и наставниками.</li>
<li><strong>Resume Review</strong> — услуга оценки и улучшения резюме для технических ролей.</li>
<li><strong>AI-Assisted Development / Vibe Coding</strong> — использование AI-инструментов (например, ChatGPT) для улучшения процесса написания кода.</li>
</ul>
<h2>System Design: фундаментальные концепции</h2>
<ul>
<li><strong>System Design Fundamentals</strong> — базовый набор концепций и архитектурных паттернов для проектирования систем.</li>
<li><strong>High-Level Design (HLD)</strong> — общая архитектурная картина, компоненты и их связи.</li>
<li><strong>Low-Level Design (LLD)</strong> — детальная проработка компонентов, структуры данных, классы и модули.</li>
<li><strong>Scalability</strong> — способность системы обрабатывать рост нагрузки.</li>
<li><strong>Performance vs Scalability</strong> — производительность для одного пользователя против эффективности при росте нагрузки.</li>
<li><strong>Latency</strong> — время отклика системы.</li>
<li><strong>Throughput</strong> — количество обработанных операций за единицу времени.</li>
</ul>
<h2>Масштабирование</h2>
<ul>
<li><strong>Вертикальное масштабирование</strong> — увеличение мощности одного сервера (CPU, RAM).</li>
<li><strong>Горизонтальное масштабирование</strong> — добавление новых узлов для распределения нагрузки.</li>
<li><strong>Sharding</strong> — горизонтальное разделение данных на части для масштабирования.</li>
<li><strong>Partitioning</strong> — логическое деление наборов данных для параллельной обработки.</li>
<li><strong>Replication</strong> — копирование данных на несколько узлов для отказоустойчивости и доступности.</li>
<li><strong>Replication factor</strong> — число копий данных в распределённой системе.</li>
</ul>
<h2>Consistency и модели согласованности</h2>
<ul>
<li><strong>CAP Theorem</strong> — компромисс между Consistency, Availability и Partition tolerance.</li>
<li><strong>Weak consistency</strong> — отсутствие немедленной согласованности после записи.</li>
<li><strong>Eventual consistency</strong> — реплики данных со временем сходятся к одному состоянию.</li>
<li><strong>Strong consistency</strong> — все чтения после записи видят обновлённые данные.</li>
<li><strong>Idempotency</strong> — повторный вызов операции даёт одинаковый результат без побочных эффектов.</li>
</ul>
<h2>Отказоустойчивость и доступность</h2>
<ul>
<li><strong>Fail-over</strong> — переключение на резервный узел при сбое.</li>
<li><strong>Active-passive</strong> — один узел активен, другой в standby.</li>
<li><strong>Active-active</strong> — оба узла обслуживают трафик одновременно.</li>
<li><strong>Circuit breaker</strong> — блокировка вызовов к проблемному сервису для отказоустойчивости.</li>
</ul>
<h2>Сетевые коммуникации</h2>
<ul>
<li><strong>HTTP</strong> — протокол прикладного уровня для веб-сервисов.</li>
<li><strong>REST</strong> — архитектурный стиль API поверх HTTP.</li>
<li><strong>WebSockets</strong> — двунаправленный протокол для постоянного соединения.</li>
<li><strong>Long polling</strong> — клиент держит открытый запрос до появления новых данных.</li>
<li><strong>TCP</strong> — надёжный транспортный протокол.</li>
<li><strong>UDP</strong> — лёгкий транспортный протокол без гарантии доставки.</li>
<li><strong>TLS/HTTPS</strong> — шифрование сетевого трафика.</li>
<li><strong>Authentication</strong> — подтверждение личности пользователя/сервиса.</li>
<li><strong>Authorization</strong> — проверка прав доступа.</li>
<li><strong>JWT (JSON Web Token)</strong> — стандарт аутентификации и передачи утверждений.</li>
<li><strong>RPC</strong> — удалённый вызов процедур.</li>
<li><strong>gRPC</strong> — высокопроизводительный RPC-фреймворк.</li>
</ul>
<h2>DNS и балансировка нагрузки</h2>
<ul>
<li><strong>DNS</strong> — система доменных имён.</li>
<li><strong>A record</strong> — связывает доменное имя с IP-адресом.</li>
<li><strong>CNAME</strong> — каноническое имя, указывающее один домен на другой.</li>
<li><strong>NS record</strong> — указывает авторитетные DNS-серверы домена.</li>
<li><strong>MX record</strong> — указывает почтовый сервер домена.</li>
<li><strong>Load Balancer</strong> — распределение трафика между серверами.</li>
<li><strong>Layer 4 load balancing</strong> — балансировка на транспортном уровне.</li>
<li><strong>Layer 7 load balancing</strong> — балансировка на прикладном уровне.</li>
<li><strong>Reverse proxy</strong> — прокси, принимающий запросы от клиентов и перенаправляющий на внутренние сервисы.</li>
<li><strong>Forward proxy</strong> — прокси между клиентом и внешним интернетом.</li>
<li><strong>Service mesh</strong> — инфраструктурный слой для управления взаимодействием микросервисов.</li>
</ul>
<h2>Базы данных и хранение</h2>
<ul>
<li><strong>RDBMS</strong> — реляционная база данных с ACID-транзакциями.</li>
<li><strong>ACID</strong> — атомарность, согласованность, изоляция, долговечность транзакций.</li>
<li><strong>Master-slave replication</strong> — одна база для записи, несколько реплик для чтения.</li>
<li><strong>Master-master replication</strong> — несколько узлов принимают чтение и запись с синхронизацией.</li>
<li><strong>Federation</strong> — разделение базы на функциональные части.</li>
<li><strong>Denormalization</strong> — копирование данных для ускорения чтения.</li>
<li><strong>SQL tuning</strong> — оптимизация запросов и индексов.</li>
<li><strong>NoSQL</strong> — ключ-значение, документные, колонкоориентированные, графовые базы.</li>
<li><strong>Key-value store</strong> — хранилище пар «ключ-значение».</li>
<li><strong>Document store</strong> — база данных, хранящая документы.</li>
<li><strong>Wide-column store</strong> — колонкоориентированная база данных.</li>
<li><strong>Graph Database</strong> — база для хранения графов сущностей и связей.</li>
<li><strong>Indexes</strong> — структуры для ускорения поиска.</li>
<li><strong>Transactional isolation</strong> — уровень согласованности параллельных транзакций.</li>
</ul>
<h2>Кэширование</h2>
<ul>
<li><strong>Cache</strong> — временное хранение данных для ускорения доступа.</li>
<li><strong>Client caching</strong> — кэш на стороне клиента.</li>
<li><strong>CDN caching</strong> — кэш на edge-серверах CDN.</li>
<li><strong>Web server caching</strong> — кэш в веб-сервере.</li>
<li><strong>Database caching</strong> — кэш в базе данных.</li>
<li><strong>Application caching</strong> — кэш в приложении (Redis, Memcached).</li>
<li><strong>Cache-aside</strong> — приложение управляет кэшем самостоятельно.</li>
<li><strong>Write-through</strong> — запись в кэш и основной store одновременно.</li>
<li><strong>Write-behind</strong> — запись сначала в кэш, потом в основной store.</li>
<li><strong>Refresh-ahead</strong> — обновление записи до истечения TTL.</li>
<li><strong>Eviction policy</strong> — правило удаления устаревших данных из кэша.</li>
<li><strong>TTL (Time To Live)</strong> — время жизни записи в кэше.</li>
<li><strong>Cache hit</strong> — запрос найден в кэше.</li>
<li><strong>Cache miss</strong> — запрос не найден в кэше, обращение к основному источнику.</li>
</ul>
<h2>Асинхронность и очереди</h2>
<ul>
<li><strong>Message Queue</strong> — очередь сообщений для обмена между сервисами.</li>
<li><strong>Task Queue</strong> — очередь фоновых задач.</li>
<li><strong>Producer</strong> — отправитель сообщений.</li>
<li><strong>Consumer</strong> — получатель сообщений.</li>
<li><strong>Topic / Partition</strong> — логические каналы и партиции в Kafka для параллелизма.</li>
<li><strong>Retention policy</strong> — правила хранения сообщений в очереди/топике.</li>
<li><strong>Back pressure</strong> — защита системы от перегрузки.</li>
<li><strong>Fan-out / Fan-in</strong> — распределение сообщений и агрегирование результатов.</li>
<li><strong>Rate limiting</strong> — ограничение частоты запросов/операций.</li>
<li><strong>Backoff strategy</strong> — уменьшение частоты повторных попыток при ошибках.</li>
<li><strong>Windowing</strong> — обработка событий во временные окна.</li>
<li><strong>Exactly-once semantics</strong> — гарантия однократной обработки события.</li>
</ul>
<h2>Полезные практические ресурсы</h2>
<ul>
<li><a href="https://static.googleusercontent.com/media/sre.google/ru//static/pdf/rule-of-thumb-latency-numbers-letter.pdf" target="_blank" rel="noopener"><strong>Latency numbers every programmer should know</strong></a> — справочный набор латентностей распределённой системы.</li>
<li><a href="https://bytebytego.com/courses/system-design-interview/back-of-the-envelope-estimation" target="_blank" rel="noopener"><strong>Use back of the envelope calculations</strong></a> — быстрые приближённые оценки для проектирования ресурсов.</li>
</ul>
<h1>Разбор задач по системному дизайну</h1>
<h2>Материалы</h2>
<ul>
<li><a href="https://bytebytego.com/courses/system-design-interview/foreword" target="_blank" rel="noopener">https://bytebytego.com/courses/system-design-interview/foreword</a> &#8212; часть разделов открыты и бесплатны</li>
<li><a href="https://newsletter.systemdesign.one/archive" target="_blank" rel="noopener">https://newsletter.systemdesign.one/archive</a></li>
<li><a href="https://www.geeksforgeeks.org/category/system-design/" target="_blank" rel="noopener">https://www.geeksforgeeks.org/category/system-design/</a></li>
<li><a href="https://www.designgurus.io/blog" target="_blank" rel="noopener">https://www.designgurus.io/blog</a></li>
</ul>
<h2>URL Shortener (аналог bit.ly)</h2>
<p>Классика: генерация коротких ссылок, редиректы, TTL, лимиты.</p>
<h3>Сервис сокращения URL: архитектурный разбор</h3>
<p>Интервью по системному дизайну часто начинается с классических задач, которые позволяют быстро выявить умение кандидата работать с ограничениями, масштабируемостью и отказоустойчивостью. Сервис сокращения ссылок выделяется своей простотой в пользовательском опыте при наличии ряда нетривиальных инженерных нюансов. На первый взгляд требуется лишь принять длинный URL, сгенерировать короткий идентификатор и обеспечить по нему быстрый редирект. На практике подобная система должна выдерживать высокие пики нагрузки, обладать стабильной латентностью и корректно работать с миллиардами объектов.</p>
<h3>Основная функциональность и требования</h3>
<p>Пользователь отправляет длинную ссылку. Система должна вернуть уникальный короткий идентификатор, пригодный к последующему использованию в любом клиенте. Доступ по этому идентификатору должен приводить к мгновенному редиректу на исходный URL. Часто возникает необходимость использования срока жизни. Например, ссылки маркетинговых кампаний живут ограниченный период, тогда как ссылки пользователя могут храниться бессрочно. Ограничения могут быть наложены и на количество запросов со стороны клиента, что защищает от злоупотреблений и контролирует бюджет инфраструктуры.</p>
<p>При формулировании требований стоит учитывать, что путь чтения обычно имеет порядок на один, а то и на два порядка выше, чем путь записи. Это определяет выбор хранилища и стратегию кеширования. В критичных системах редирект должен укладываться в единицы миллисекунд.</p>
<h3>Генерация коротких идентификаторов</h3>
<p>Выбор стратегии генерации идентификатора отражает особенности архитектуры. Наиболее простой подход основан на числовой последовательности. Идентификатор преобразуется в кодированную форму с использованием алфавита, создающего компактную строку. Подобный метод гарантирует уникальность без распределённого консенсуса, но требует централизованного механизма увеличения счётчика или применения шардирования на уровне ID-пространства.</p>
<p>Альтернативный подход основан на хешировании длинного URL. Он не требует глобального состояния и может выполняться на любом узле. Однако применение чистого хеша чревато коллизиями. Поэтому обычно выбирают комбинацию хеша и проверки хранилища. Если коллизия обнаружена, система генерирует новый идентификатор, используя добавочные данные или случайность. В системах с огромным объёмом данных вероятность коллизий становится заметным фактором, поэтому практикуют укороченный хеш с дополнительной энтропией.</p>
<p>Отдельной задачей является требование стабильности. Если пользователь передаёт один и тот же URL, некоторые системы возвращают ранее созданный идентификатор. Это снижает объём базы и делает поведение предсказуемым. Но усложняет кэширование и требует наличия быстрого поиска по URL.</p>
<h3>Хранилище</h3>
<p>В основе сервиса лежит структура вида ключ-значение. Ключом является короткий идентификатор, значением — исходный URL и метаданные. Применение реляционной базы обычно неоправданно. Такие данные масштабируются горизонтально гораздо проще, если использовать распределённые key-value хранилища или базы класса NoSQL.</p>
<p>Зная, что операции чтения преобладают, критичной становится скорость доступа. Поэтому архитектура почти всегда включает слой кеширования. Ближайший к пользователю региональный кеш Redis или аналог снижает задержку и уменьшает нагрузку на основное хранилище. Важно понимать, что кеш должен обновляться при изменении <strong>TTL</strong>, удалении ссылок или продлении срока жизни. Ошибки в синхронизации иногда приводят к выдаче истёкших ссылок, поэтому метаданные TTL хранятся как в кеше, так и в первичном хранилище.</p>
<h3>Обработка редиректов</h3>
<p>Редирект должен быть максимально лёгкой операцией. Сервис получает запрос по короткому идентификатору, определяет актуальность записи, проверяет возможные ограничения и отвечает перенаправлением. На этом пути часто используют CDN как дополнительный уровень защиты. CDN способен обслуживать повторяющиеся запросы без обращения к серверу приложения, что резко снижает задержку на глобальном трафике.</p>
<p>Иногда возникает необходимость подсчитывать клики. Для систем аналитики путь чтения избегают утяжелять синхронной записью. Событие передачи можно отправить в асинхронную очередь, которую downstream-сервисы уже обрабатывают независимо.</p>
<h3>TTL, истечение и очистка</h3>
<p>Срок жизни описывается на уровне метаданных записи. При истечении запись исключается из кеша, но основное удаление из хранилища выполняется периодически, чтобы не создавать пики нагрузки. Подобные задачи часто реализуют через фоновые процессы или распределённые cron-механизмы. Сложность заключается в том, что объём данных может достигать значений, не допускающих прямых итераций. Поэтому используют стратегии сегментации или слабосвязанные структуры хранения.</p>
<h3>Ограничения и защита от злоупотреблений</h3>
<p>Защита от чрезмерного использования сервиса чаще всего реализуется через rate-limiting. Ограничения могут быть наложены как на создание ссылок, так и на переходы. Для глобальных публичных сервисов применяется многоуровневая архитектура, где лимиты проверяются на уровне CDN, API-шлюза и непосредственно в сервисе.</p>
<h3>Масштабирование</h3>
<p>При существенных нагрузках система должна быть способна горизонтально масштабироваться без потери идемпотентности операций. Шардинг идентификаторов позволяет распределять данные по множеству узлов. Распределённый кеш, использующий согласованный хеш, помогает избежать горячих ключей, особенно в случаях, когда популярность конкретной ссылки резко возрастает.</p>
<p>Для систем, работающих по модели eventual consistency, задержки в репликации приемлемы, если не влияют на пользовательский опыт. Однако для хранилища переходов критична именно читающая часть, поэтому используют лидера для записи и несколько реплик для чтения или полностью распределённые хранения, работающие без жёстких ограничений на консистентность.</p>
<h3>Возможные схемы</h3>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/12/url_shortener_system_design_3.jpeg"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2562" src="https://datatalks.ru/wp-content/uploads/2025/12/url_shortener_system_design_3.jpeg" alt="" width="992" height="494" srcset="https://datatalks.ru/wp-content/uploads/2025/12/url_shortener_system_design_3.jpeg 992w, https://datatalks.ru/wp-content/uploads/2025/12/url_shortener_system_design_3-300x149.jpeg 300w, https://datatalks.ru/wp-content/uploads/2025/12/url_shortener_system_design_3-768x382.jpeg 768w, https://datatalks.ru/wp-content/uploads/2025/12/url_shortener_system_design_3-450x224.jpeg 450w, https://datatalks.ru/wp-content/uploads/2025/12/url_shortener_system_design_3-780x388.jpeg 780w" sizes="(max-width: 992px) 100vw, 992px" /></a></p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/12/url_shortener_system_design_2.jpeg"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2563" src="https://datatalks.ru/wp-content/uploads/2025/12/url_shortener_system_design_2.jpeg" alt="" width="1814" height="951" srcset="https://datatalks.ru/wp-content/uploads/2025/12/url_shortener_system_design_2.jpeg 1814w, https://datatalks.ru/wp-content/uploads/2025/12/url_shortener_system_design_2-300x157.jpeg 300w, https://datatalks.ru/wp-content/uploads/2025/12/url_shortener_system_design_2-1024x537.jpeg 1024w, https://datatalks.ru/wp-content/uploads/2025/12/url_shortener_system_design_2-768x403.jpeg 768w, https://datatalks.ru/wp-content/uploads/2025/12/url_shortener_system_design_2-1536x805.jpeg 1536w, https://datatalks.ru/wp-content/uploads/2025/12/url_shortener_system_design_2-450x236.jpeg 450w, https://datatalks.ru/wp-content/uploads/2025/12/url_shortener_system_design_2-780x409.jpeg 780w, https://datatalks.ru/wp-content/uploads/2025/12/url_shortener_system_design_2-1600x839.jpeg 1600w" sizes="(max-width: 1814px) 100vw, 1814px" /></a></p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/12/url_shortener_system_design_1.jpeg"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2564" src="https://datatalks.ru/wp-content/uploads/2025/12/url_shortener_system_design_1.jpeg" alt="" width="1683" height="821" srcset="https://datatalks.ru/wp-content/uploads/2025/12/url_shortener_system_design_1.jpeg 1683w, https://datatalks.ru/wp-content/uploads/2025/12/url_shortener_system_design_1-300x146.jpeg 300w, https://datatalks.ru/wp-content/uploads/2025/12/url_shortener_system_design_1-1024x500.jpeg 1024w, https://datatalks.ru/wp-content/uploads/2025/12/url_shortener_system_design_1-768x375.jpeg 768w, https://datatalks.ru/wp-content/uploads/2025/12/url_shortener_system_design_1-1536x749.jpeg 1536w, https://datatalks.ru/wp-content/uploads/2025/12/url_shortener_system_design_1-450x220.jpeg 450w, https://datatalks.ru/wp-content/uploads/2025/12/url_shortener_system_design_1-780x380.jpeg 780w, https://datatalks.ru/wp-content/uploads/2025/12/url_shortener_system_design_1-1600x781.jpeg 1600w" sizes="(max-width: 1683px) 100vw, 1683px" /></a></p>
<h2>Rate Limiter</h2>
<p>Token bucket / leaky bucket, распределённый rate limiting.</p>
<h3>Этап 1. Постановка задачи и исходный контекст</h3>
<p>Интервьюер формулирует задачу: требуется спроектировать распределённую систему rate limiting, способную ограничивать количество запросов, исходящих от клиента, сервиса или пользователя, в единицу времени. Ограничения должны быть гибкими, управляемыми и устойчивыми к злоупотреблениям. Важно обеспечить предсказуемую нагрузку на backend-части систем, а также защиту от несанкционированных попыток обойти лимиты. Задача должна включать модели token bucket или leaky bucket, а также уметь работать в распределённой среде, что исключает выполнение rate limiting полностью локально.</p>
<p>Нефункциональные требования включают высокую доступность, низкую задержку при проверке лимита, масштабируемость до миллионов или миллиардов запросов в час, гарантированную корректность в условиях распределённости, а также предсказуемый и воспроизводимый алгоритм вычисления оставшихся квот.</p>
<p>После постановки вопроса интервьюер завершает вводную часть и передаёт инициативу кандидату.</p>
<h3>Этап 2. Формализация требований и уточняющие вопросы</h3>
<p>На этапе формализации кандидат уточняет ключевые параметры задачи. Прежде всего уточняется контекст ввода лимитов: должны ли лимиты применяться на уровне отдельного пользователя, IP-адреса, API-ключа или конкретного endpoint. Важно понимать, должны ли лимиты быть одинаковыми для всех или управляться конфигурационно, а также существует ли требование динамически обновлять лимитирующие правила без перезапуска системы.</p>
<p>Параллельно формулируются нефункциональные цели. Высокая доступность для rate limiter является критичной, поскольку ошибки в этом компоненте могут привести к деградации или отказу всего API. Консистентность требует аккуратного подхода: строгая консистентность повышает задержки, а eventual consistency может допускать временные превышения лимита. Выбор зависит от характера сервиса. Пропускная способность должна быть высокой, так как проверка лимита выполняется на каждом запросе. Масштабируемость важна для горизонтального расширения без разделения контекста лимитов или появления горячих ключей. Аудитируемость может стать обязательной в корпоративных системах, где требуется отслеживание нарушений.</p>
<p>После уточнения всех вопросов становится ясно, что система должна обеспечить распределённое хранение счётчиков, минимальную латентность проверок, корректное ограничение частоты запросов и предсказуемое поведение при отказах отдельных узлов.</p>
<h3>Этап 3. Границы системы и публичное API</h3>
<p>Теперь требуется определить публичный интерфейс. Rate limiter по сути предоставляет одну основную операцию: проверку и обновление лимита пользователем. В рамках неё клиент передаёт идентификатор субъекта лимитирования и параметры правила (если они не заранее зафиксированы). Результат представляет собой разрешение или запрет действия, дополненный информацией о текущем состоянии бакета или очереди.</p>
<p>В зависимости от модели бакета API может возвращать количество оставшихся токенов, расчётный момент восстановления лимита или фактическое время ожидания. Если система поддерживает управление правилами, она может предоставлять интерфейсы для создания, изменения и удаления конфигураций лимитов. Однако в большинстве случаев требуется только быстрый и корректный ответ на запрос проверки лимита.</p>
<p>Таким образом граница системы формируется вокруг простой операции check-and-update, являющейся атомарной по смыслу, пусть и распределённой по технической реализации.</p>
<h3>Этап 4. Проектирование: сценарии, потоки данных и компоненты</h3>
<p>На этом этапе начинается построение системы, начиная с основной логики. В основе лежит алгоритм token bucket или leaky bucket. Для token bucket система предполагает наличие ёмкости и скорость восполнения токенов. При каждом запросе проверяется, достаточно ли токенов. Если да, один или несколько токенов списываются. Если нет, запрос отклоняется. Для leaky bucket модель другая: запросы попадают в очередь фиксированной длины, и новая обработка разрешается в соответствии с постоянной скоростью «утечки».</p>
<p>В распределённой системе появляется вопрос синхронизации состояния бакета между узлами. Первым компонентом становится хранилище счётчиков. Оно должно обеспечивать атомарные операции инкремента, декремента, условной записи или хранения временных отметок. Система может использовать in-memory хранилище с распределёнными блокировками, например Redis с Lua-скриптами, либо специализированные распределённые базы данных, предоставляющие операции CAS. Логика rate limiting должна быть максимально компактной, чтобы обработка происходила за десятки микросекунд.</p>
<p>Чтобы избежать горячих ключей, система может распределять состояние лимита по нескольким шардам, использовать согласованное хеширование или локальные промежуточные кеши. Второй сценарий заключается в локальном prefill токенов, что уменьшает обращение к центральному хранилищу и снижает задержку, но требует осторожности в части консистентности.</p>
<p>В случае отказов необходимо предусмотреть graceful degradation. Например, при временной недоступности центрального хранилища система может работать в режиме fail-open или fail-closed. Выбор стратегии зависит от характера сервиса: критичные API обычно предпочитают fail-open, чтобы не блокировать клиентов, но строго внутренние системы могут работать в fail-closed для защиты от перегрузки.</p>
<p>Постепенно формируется общий набор компонентов: модуль конфигурации лимитов, распределённое хранилище счётчиков, локальный компонент принятия решения, механизм синхронизации времени, а также метрики и мониторинг для отслеживания состояния. Потоки данных состоят из последовательного получения состояния бакета, вычисления новых параметров и обновления записи. Всё это должно происходить атомарно в пределах одного логического шага.</p>
<h3>Этап 5. Концептуальная архитектура</h3>
<p>Завершив развитие сценариев, можно представить целостную схему. API получает запрос на проверку лимита и передаёт его в компонент rate limiter. Этот компонент обращается к распределённому хранилищу счётчиков, выполняя атомарную транзакцию: расчёт восстановленных токенов, проверку лимита и обновление состояния. Результат возвращается клиенту. Для высокой производительности система использует локальные кеши, в которых хранит метаданные лимитов и параметры восстановления. Хранилище выполняет роль единственного источника истины, обеспечивая согласованность в распределённой среде. В архитектуре может быть предусмотрен вспомогательный сервис для централизованной конфигурации правил.</p>
<p>Такая схема обеспечивает баланс между производительностью, отказоустойчивостью и правильным соблюдением лимитов.</p>
<h3>Этап 6. Выбор технологий и оценка размера системы</h3>
<p>На этом этапе проводится обсуждение конкретных инструментов. Если требуется минимальная задержка, применяется Redis Cluster, позволяющий выполнять атомарные операции через Lua. Для более строгой консистентности можно использовать CockroachDB или DynamoDB, обеспечивающие линейно масштабируемые операции обновления. В случае экстремальных нагрузок возможно применение in-memory систем, работающих полностью распределённо, например Aerospike.</p>
<p>Оценка размера зависит от количества субъектов лимитирования и числа запросов. Если система обрабатывает сотни тысяч запросов в секунду, хранилище должно поддерживать низкую задержку при высокой конкурентности. Выбор region-aware конфигурации Redis или использование шардированных распределённых атомарных счётчиков становится важной частью sizing. Для глобальных систем можно организовать региональные rate-limiters, выполняющие часть работы локально, а глобальная консистентность достигается распределённым накоплением токенов.</p>
<p>Эта часть обсуждения показывает не только знание технологий, но и способность оценить их применимость.</p>
<h3>Этап 7. Дополнительные вопросы и расширения</h3>
<p>Наконец, можно рассмотреть расширение задачи. Например, реализацию rate limiting на уровне CDN, распределённые токены, синхронизацию лимитов между дата-центрами, защиту от случайных всплесков нагрузки, реализацию soft-limits и mode-switching при деградации. Можно обсудить многоуровневые лимиты, построенные по принципу периметр → API → метод. Ещё одна интересная тема — rate limiting в системах с микросервисной архитектурой, где каждый сервис может выступать одновременно и потребителем, и контролирующим компонентом.</p>
<p>Эти дополнительные рассуждения демонстрируют широту знаний кандидата и умение видеть систему за пределами узкого требования.</p>
<h2>Design a Cache System (Redis-based)</h2>
<p>Кеширование профилей, стратегий eviction, борьба со штампедом.</p>
<h3>Этап 1. Постановка задачи и контекст</h3>
<p>Сервис должен кэшировать профильные данные пользователей для обеспечения низкой латентности чтения и снижения нагрузки на хранилище первичных данных. Профиль включает небольшой набор «горячих» полей (имя, аватар, статус, флаги приватности), а также ряд «холодных» или редко меняющихся полей (история, настройки). Основные функциональные требования: быстрые чтения (миллисекунды), согласованность в рамках разумных ограничений, возможность инвалидации при обновлениях профиля, поддержка TTL для устаревших данных и защита от наводнений запросов (stampede). Нефункциональные требования: высокая доступность, горизонтальная масштабируемость, контролируемое использование памяти, мониторинг и способность обслуживать пики нагрузки.</p>
<h3>Этап 2. Формализация требований и ключевые архитектурные характеристики</h3>
<p>Необходимо уточнить несколько практических допущений, которые влияют на дизайн: допустимо ли eventual consistency между кэшем и базой; частота и критичность обновлений профиля; цель по hit-rate (например ≥95%); ожидаемый QPS чтений и записей; средний размер профиля. Из нефункциональных свойств важно зафиксировать приоритеты: низкая латентность чтения — главный приоритет; консистентность сильнее важна для операций конфиденциальности и удаления, чем для отображения аватара; доступность должна быть высокой, система не должна становиться «узким местом». На основе этого определяем свойства: оптимизация под high throughput (чтения), допустимость eventual consistency для обычных полей, необходимость auditability для операций обновления критичных атрибутов.</p>
<h3>Этап 3. Границы системы и публичный API</h3>
<p>Граница системы — уровень кеша, видимый для прикладных сервисов. Набор операций минимален: GetProfile(userId, options) возвращает профиль (или его часть) и метаинформацию; UpdateProfile(userId, delta) обновляет профиль и обеспечивает инвалидацию/обновление кеша; InvalidateProfile(userId) принудительно удаляет/обновляет запись в кеше; BulkWarm(keys) — опционально для прогрева. GetProfile должен поддерживать параметры частичного чтения (hot fields vs full profile). Ответ должен содержать маркер источника («cache» или «db») и, при необходимости, TTL/версию. API должен быть лёгким и атомарным с точки зрения прикладного кода: вызов GetProfile не обязан вручную реализовывать логику кеширования.</p>
<h3>Этап 4. Проектирование: happy path и exceptional flows, компоненты и потоки данных</h3>
<p>Happy path для чтения: приложению вызывают GetProfile(userId). Клиентский SDK (или middleware на уровне сервиса) проверяет локальный near-cache (опционально), затем Redis. При попадании в кеш (hit) профиль возвращается. При промахе (miss) сервис читает профиль из первичной БД, записывает его в Redis (cache-aside) с нужным TTL/метаданными и возвращает результат. Для ускорения и экономии сети подход cache-aside с централизованной логикой чаще всего лучше, чем write-through, поскольку запись происходит реже.</p>
<p>Обновления: при поступлении UpdateProfile система выполняет запись в первичную БД и затем инвалидиует ключ в Redis. Варианты поведения: синхронная инвалидация после успешной транзакции записи, либо write-through (сначала в кеш, затем в БД) при необходимости строгой консистентности. Для частичных обновлений полезно хранить версию/число смен (version number) в значении кеша — это упрощает детекцию устаревших значений и гонок.</p>
<p>Борьба со штампедом (cache stampede): при большом количестве одновременных запросов на недостающий в кеше ключ нужно предотвратить лавину чтений в БД. Практические техники:</p>
<ul>
<li>Singleflight / mutex per key: первый запрос ставит «замок» (локальный или в Redis с SETNX), остальные ждут или получают ответ с опцией «fallback»; после загрузки из БД и записи в кеш замок снимается. Для распределённой среды применяют Redis с небольшим TTL на замок и гарантированным восстановлением.</li>
<li>Request coalescing на уровне edge: агрегировать запросы внутри процесса или на API-шлюзе.</li>
<li>Probabilistic early recompute: при приближении TTL популярного ключа его предварительно «перепекают» (background refresh) по вероятностному алгоритму, чтобы избежать одновременной просадки.</li>
<li>Negative caching и Bloom filter: для отсутствующих ключей возвращать отрицательный кеш (короткий TTL) и использовать Bloom filter, чтобы фильтровать запросы к БД для несуществующих пользователей.</li>
<li>Lazy locking + timeout: выдержать предел ожидания, а при ошибке чтения возвращать ошибку контролируемо.</li>
</ul>
<p>Hot keys и «горячие» профили: если конкретный профиль получает несоизмеримо много трафика, имеет смысл хранить его в специальной hot-shard или использовать client-side near-cache с TTL и LFU-недельной политикой, либо применить rate limiting на уровне потребителей для этого ключа.</p>
<p>Eviction и стратегии: Redis предлагает политики evictions: LRU, LFU, TTL-based и их комбинации. Для профилей лучше избегать простого allkeys-lru без учета семантики, иначе горячие профили будут вытеснять набор «активных» пользователей. Практические подходы: разделение ключей по namespace (hot vs cold) с разными maxmemory и политиками; хранение самых важных полей отдельно (hot fields) с более длинным TTL; использование LFU для адаптивного удержания часто запрашиваемых профилей. Также можно применять size-aware eviction: учитывать размер значения при принятии решения об удалении (если профили имеют разный размер).</p>
<p>Последовательность для отказов компонента кеша: при недоступности Redis система должна graceful degrade — либо читать напрямую из БД (fail-open), либо использовать локальные копии (near-cache), либо, при критичности защиты БД, temporarily reject с контролируемым backoff (fail-closed). Чаще выбирают fail-open для пользовательского опыта и мониторинг для отслеживания нагрузки на базу.</p>
<h3>Этап 5. Концептуальная архитектура и целостный обзор</h3>
<p>В центре — прикладной API слой с middleware, реализующим логику кеширования. Redis Cluster выступает в качестве распределённого in-memory хранилища. Рядом располагаются вспомогательные сервисы: сервис конфигурации TTL/политик, фоновые воркеры для прогрева/рефреша, очередь событий для инвалидаций (pub/sub или Kafka), мониторинг/alerting. Поток чтения: application → local near-cache → Redis → DB. Поток записи: application → DB → (инвалидация через pub/sub) → Redis (delete) или обновление значения. Для борьбы со штампедом дополнительный слой — lock service (реализуемый через Redis SETNX + Lua) и Bloom filter для non-existent keys.</p>
<h3>Этап 6. Выбор технологий и sizing</h3>
<p>Технологический стек: Redis Cluster для шардирования и масштабирования памяти, с replica-репликами на каждом шардe для отказоустойчивости; Redis Sentinel или встроенные механизмы кластера для failover; использование Redis Modules (RedisJSON) если нужно частичное чтение/запись полей профиля без передачи всего объекта; клиентские библиотеки с поддержкой singleflight/near-cache.</p>
<p>Sizing. Исходим из предположений: N активных пользователей, средний профиль P килобайт, ожидаемый hit-rate H, peak read QPS_r и write QPS_w. Память = N * P * (1 / hit_factor) плюс overhead шардов и metadata; добавляем репликацию (factor 2 или 3). Количество узлов = ceil(memory_total / instance_memory). Для QPS учитываем пропускную способность каждого экземпляра Redis (обычно десятки тысяч ops/s на современном железе), но при большом числе небольших операций важна сетевая задержка и CPU (Lua-скрипты и сериализация). Рекомендуется планировать запас 2–3× по операции и памяти, использовать мониторинг (hit ratio, evictions, latency) и авто-scaling/resharding при росте.</p>
<p>Параметры отказоустойчивости: реплики на каждом шарде, RPO/RTO зависят от выбора persistence (AOF vs RDB) — для кеша обычно persistence выключен или минимален; если важно сохранить кеш между рестартами, включают RDB snapshotting с приемлемыми интервалами.</p>
<h3>Этап 7. Дополнительные расширения и эксплуатационные темы</h3>
<p>Можно рассмотреть следующие улучшения: partial caching с RedisJSON для снижения трафика при обновлениях; adaptive TTL на основе частоты доступа; per-field versioning для минимизации инвалидаций при частичных обновлениях; CDN/edge caching для контента профиля, не требующего приватности; использование managed Redis (AWS ElastiCache, Azure Redis) для снижения операционной нагрузки. Для наблюдаемости — метрики: hit/miss ratio, evictions/sec, memory usage, slowlog; tracing запросов для выявления hot keys. Наконец, политика безопасности: шифрование транспортного уровня, ACL в Redis, ограничение доступа через VPC.</p>
<h2>Messenger/Chat (WhatsApp / Telegram Lite)</h2>
<p>Сохранение сообщений, онлайн-статусы, доставляемость, fan-out.</p>
<h3>Этап 1. Постановка задачи и исходный контекст</h3>
<p>Интервьюеру даётся задача: спроектировать сервис обмена сообщениями — лёгкий мессенджер, обеспечивающий отправку и хранение сообщений, онлайн-статусы, доставляемость и масштабируемый фан-аут как для личных чатов, так и для групповых. Система должна поддерживать множество устройств на один аккаунт, обеспечивать быстрый UX (низкая задержка доставки и отображения новых сообщений), сохранять историю и при необходимости доставлять сообщения оффлайн-пользователям через push. Нефункциональные требования включают высокую доступность, устойчивость к пиковому трафику, масштабирование до миллионов активных пользователей, обработку медиа (attachments) и обеспечение порядка сообщений в пределах чата. Дополнительные пожелания — эффективная борьба с дублированием, поддержка синхронизации между устройствами и возможность расширения (например, голосовые сообщения, шифрование).</p>
<p>Интервьюер на этом этапе обычно не даёт дальнейших уточнений; кандидат должен сам формализовать требования и границы.</p>
<h3>Этап 2. Формализация требований и ключевые архитектурные характеристики</h3>
<p>Кандидат задаёт уточняющие вопросы и формализует поведение системы. Первое — требуемые гарантии доставки: допустима ли at-least-once или требуется exactly-once? В мессенджерах обычно достаточно at-least-once с идемпотентной обработкой на клиенте (удаление дубликатов по message-id) и семантикой «sent → delivered → read». Второе — требования к порядку: строгий порядок обеспечивает удобство в личных чатах; в группах часто достаточно порядка в пределах одного отправителя или условного causality (переменная важность строгого глобального порядка). Третье — моделирование оффлайн-клиентов: сообщения должны сохраняться на сервере до доставки всем активным девайсам или до истечения TTL, а также синхронизироваться при подключении нового устройства. Четвёртое — масштабируемость фан-аута: одновременная доставка в группы с миллионами подписчиков невозможна простым broadcast-режимом; нужно выбирать между eager fan-out (write-time fan-out) и lazy fan-out (read-time fan-out) с гибридными подходами. Пятое — требования к конфиденциальности и безопасности: будет ли требоваться end-to-end шифрование (E2EE) или достаточно транспорта и хранения в зашифрованном виде на сервере. Нефункционально фиксируется высокая доступность, низкая задержка (мс-уровень для онлайн-доставки), масштабируемость и мониторируемость.</p>
<p>На базе этих ответов фиксируются ключевые архитектурные характеристики: больше внимания — latency и availability; консистентность — умеренная (с акцентом на локальный порядок в чатах); throughput — высокий для читателей и средний/низкий для записей; auditability и долговременное хранение — опционально, зависят от политики ретеншна.</p>
<h3>Этап 3. Проектирование границ системы и публичного API</h3>
<p>Границей считается набор API, которым оперируют клиенты и вспомогательные сервисы. Минимальный публичный контракт включает операции аутентификации/привязки устройства, отправки сообщения, получения сообщений (с пагинацией и синхронизацией), отметки доставленных/прочитанных сообщений, получения и публикации presence, загрузки и получения медиа, управление подписками на групповые события.</p>
<p>Примеры API (HTTP/HTTP+WebSocket/гетерогенный протокол):</p>
<p>POST /v1/send<br />
body: { fromUserId, fromDeviceId, conversationId, clientMessageId, payload, timestamp, attachmentsMeta }</p>
<p>WS: SUBSCRIBE /v1/stream?userId=&#8230;<br />
messages stream: server -&gt; client pushes new messages, presence updates, acks</p>
<p>GET /v1/sync?userId&amp;sinceToken<br />
returns: ordered messages, device sync cursors</p>
<p>POST /v1/ack<br />
body: { conversationId, serverMessageId, ackType: delivered|read, deviceId, timestamp }</p>
<p>POST /v1/presence<br />
body: { userId, deviceId, status: online|offline|idle, lastActiveAt }</p>
<p>API должен возвращать достаточную метаинформацию: серверные идентификаторы, пер-сообщение sequence / lamportTimestamp для порядка, курсоры синхронизации и указание устройств, на которые сообщение было доставлено или нет. Публичный контракт отделяет клиентскую видимость от внутренней реализации (например, internal push-gateway, message-broker, storage).</p>
<h3>Этап 4. Проектирование системы: happy path и exceptional flows; компоненты и потоки данных</h3>
<p>Основной happy path: пользователь A отправляет сообщение в чат с пользователем B или в группу. Клиент формирует клиентский идентификатор clientMessageId и локальную метку времени; сообщение отправляется на ближайший frontend (API / WebSocket gateway). Gateway выполняет базовую валидацию и передаёт сообщение в контроллер доставок (dispatcher). Dispatcher назначает серверный идентификатор messageId и sequence/ordering metadata, записывает сообщение в durable log (например, partitioned Kafka topic или аналогичный commit-log) и публикует событие в очередь доставки. Затем dispatcher инициирует фан-аут: для личного чата это список устройств B; для группы — список участников (который может храниться в sharded group-service). Для каждого целевого девайса формируется delivery task, который отправляется в ряды delivery-workers.</p>
<p>Delivery-worker пытается доставить сообщение: если получено активное соединение WebSocket/MQTT, worker шлёт push и ждёт acknowledgement от клиента. При подтверждении отправки (device-level ack) worker помечает устройство как доставленное и, при необходимости, обновляет статус в message-store. Если устройство оффлайн, worker генерирует push-notification через APNs/FCM (через push-gateway) и сохраняет сообщение как pending в per-device queue для будущей доставки. После доставки на все устройства обновляется статус «delivered» для конкретного получателя или всех получателей. При прочтении клиент шлёт read-ack, который обновляет read-state и генерирует уведомление отправителю.</p>
<p>На этом пути формируются ключевые компоненты: front-end gateways (HTTP + WebSocket), dispatcher (assigns ids, writes to commit-log), durable commit-log (Kafka-like), delivery workers (stateless, масштабируемые), presence service (tracking active connections and device mapping), group service (manages membership), message store (persisting messages for retrieval and history), per-device pending queues, push-gateway, media-store / CDN для attachments, sync service (reconciliation and history pagination), and monitoring &amp; metrics.</p>
<p>Exceptional flows: дублирование сообщений при повторной отправке со стороны клиента (network retry) обрабатывается идемпотентностью по clientMessageId; потеря ordering при кросс-шардовых операциях решается назначением per-conversation sequence (dispatcher даёт монотонный sequence в пределах conversation partition). Отказ durable log или delivery-workers компенсируется репликацией commit-log и повторной обработкой событий; в случае недоступности push-gateway система может временно накапливать pending-notifications и применять backoff. Для скоростных всплесков применима backpressure на gateway и rate limiting на уровне sender.</p>
<p>Порядок сообщений. В личных чатах важно сохранить строгий порядок; это достигается partitioning commit-log по conversationId — все сообщения одного разговора попадают в один партишин, получают последовательный offset и обрабатываются в порядке поступления. В группах с большим количеством участников схему с single partition может стать узким местом; тогда применяют per-conversation партиционирование + sharding группы по conversationId и, при необходимости, логические sequence от каждого отправителя (per-sender ordering) с merge-стратегией на клиента.</p>
<p>Хранение истории. Сообщения сохраняются в message-store — выбор между wide-column store (Cassandra/DynamoDB) или append-only blob storage с индексом зависит от требований по latency и доступности. Для пользовательского опыта последние N сообщений размещают также в in-memory cache (Redis) для ускорения sync и initial load. Медиа-файлы сохраняются в object storage (S3) и раздаются через CDN; в базе хранятся ссылки и метаданные. Политика retention управляется на уровне сервиса: удаление по истечению TTL, возможность удаления пользователем и аудит.</p>
<p>Синхронизация между устройствами реализуется через sync cursors. Клиент периодически вызывает /sync?sinceToken и получает все новые события. При привязке нового устройства проводится full-sync (последние N сообщений плюс paginated history). Sequence и cursor гарантируют, что клиент получит все события в нужном порядке.</p>
<h3>Этап 5. Концептуальная схема и целостный обзор</h3>
<p>Система представляется в виде многоуровневой архитектуры. На периферии расположены Gateways, принимающие клиентские соединения. Gateway взаимодействует с Presence Service для определения активных устройств получателя и с Group Service для получения списка участников. Dispatcher делает durable write в commit-log, который является единственным источником правды для событий. Delivery-workers читают из commit-log или получают события через pub/sub и выполняют доставку, сохраняя статусы доставки в Message Store. Message Store даёт возможности чтения истории и согласования при повторной доставке. Media-поток обрабатывается отдельно: uploader сохраняет в object storage, возвращает ссылку, которую включают в сообщение. Push gateway интегрирован с внешними сервисами для уведомлений на мобильные устройства. Синхронная инвалидация/ack flow и метрики обеспечивают поддержку SLA. Такая архитектура обеспечивает надёжную, масштабируемую обработку сообщений и даёт четкие точки расширения.</p>
<h3>Этап 6. Выбор технологий и sizing</h3>
<p>Выбор технологий ориентируется на требования: Kafka-подобный durable log для commit-log; Cassandra или DynamoDB для message-store при требовании write-heavy и линейного масштабирования; PostgreSQL/Spanner — при необходимости транзакционной логики (редко требуется для чатов). Redis применяется для presence, per-device queues и кеширования последних сообщений. WebSocket / MQTT брокеры (NGINX + WebSocket, EMQX, Mosquitto или propietary gateway) используются для удержания постоянных соединений и низкой латентности пуша. Push-gateway интегрируется с APNs/FCM. Object storage (S3) и CDN для медиа.</p>
<p>Sizing делается на основе входных параметров: число активных пользователей (MAU/DAU), среднее число одновременных подключений, средний QPS отправки сообщений, средний размер сообщения и среднее число получателей. Формула для пропускной способности коммита в durable log: required_throughput = messages_per_sec * replication_factor. Для хранения: daily_storage ≈ messages_per_day * avg_message_size. Примерная численная иллюстрация: при 10M DAU, 20% concurrent, средний отправляемых сообщений на пользователя в день 50, avg_size 1KB, получателей в среднем 1.5, потребуется пропускная способность commit-log порядка 10M50/86400 ≈ 5.8k msg/s (при этом с репликацией и overhead ориентируемся на ≈ 20k ops/s). Message-store размер в день: 10M501KB1.5 ≈ 750GB; с репликацией и overhead под 2TB в день; retention 30 дней — порядка 60TB. Эти оценки показывают необходимость shard- и tiered-storage (горячие последние дни в Cassandra / SSD, архивы в object storage). Количество WebSocket gateway’ев рассчитывается исходя из допустимого числа соединений на ноду и пикового трафика; современные инстансы поддерживают десятки тысяч persistent connections.</p>
<p>Выбор по гарантиям: если достаточно at-least-once, Kafka+consumer-groups + idempotent writes на клиенте — простой путь. Для stronger semantics применяют exactly-once processing с эффектацией последовательных обновлений в message-store с помощью transactional writes или conditional writes.</p>
<h3>Этап 7. Дополнительные вопросы, расширения и эксплуатационные аспекты</h3>
<p>Система допускает множество дополнений. End-to-End шифрование требует изменения контрактов: сервер перестаёт иметь доступ к расшифрованным payload’ам, хранения метаданных и доставки осуществляются по зашифрованным blob’ам; sync и поиск усложняются. Масштабирование больших групп оптимизируется через hybrid fan-out: для небольших групп — eager fan-out (разовая запись в per-recipient queues), для очень больших групп — lazy fan-out (recipient pulls recent offsets), а для мега-групп — use of multicast-like delivery через push-to-topic + client-side filtering. Hot-group и hot-user detection помогают выделять горячие участки нагрузки в отдельные шарды. Для борьбы с spam — комбинация rate limiting на стороне sender, content-moderation pipelines и machine-learning фильтров.</p>
<p>Операционно важны мониторинг и alerting: delivery latency, ack-rates, queue-latency, commit-log lag, offline message queue sizes, push-failure rates и hotspot detection. Тестирование отказов и chaos engineering критичны для гарантирования SLA; регулярные drills для push-gateway’а и durable-log failover обязательны. Политика бэкапов для message-store зависит от требований к RPO/RTO; для быстрого recovery используются snapshot’ы и репликация.</p>
<p>Наконец, UX-аспекты: клиентская логика должна обрабатывать дубли, обеспечивать локальную видимость отправки (optimistic UI), корректно показывать статус доставлено/прочитано с учётом нескольких устройств, уметь синхронизироваться после долгого offline периода и уважать privacy settings пользователя.</p>
<h2>News Feed</h2>
<p>Fan-in/fan-out, push vs pull, хранение ленты.</p>
<h3>Этап 1. Постановка задачи и исходный контекст</h3>
<p>Интервьюер формулирует задачу: спроектировать систему формирования и доставки ленты новостей (news feed) уровня соцсети. Система должна принимать события (посты, репоcты, лайки, комментарии), агрегировать их в персональные ленты пользователей и обеспечивать быструю доставку и прокрутку (infinite scroll). Дополнительные требования: персонализация ранжирования, свежесть контента, масштабирование до миллионов активных пользователей, поддержка как текстовых сообщений, так и медиа, гарантия приемлемой латентности для первой страницы и бесшовной подгрузки истории. Нефункциональные требования включают высокую доступность, устойчивость к пиковому трафику, предсказуемое время отклика, возможность A/B тестирования ранжировщиков и экономное использование ресурсов при больших фан-аутах (множество подписчиков у одного автора).</p>
<p>Интервьюер на этом этапе заканчивает ввод, инициатива переходит к кандидату.</p>
<h3>Этап 2. Формализация требований и уточняющие вопросы</h3>
<p>Кандидат задаёт вопросы, чтобы определить границы и приоритеты. Что важнее для продукта: абсолютная свежесть или релевантность ранжирования? Нужно ли показывать «activities» от друзей в строгом хронологическом порядке или применять персонифицированный рейтинг? Какая допустимая задержка от создания события до его появления в ленте: сотни миллисекунд, секунды или минуты? Какова типичная структура социальной графа: большинство пользователей имеют небольшое число подписок или есть много «звёзд» с миллионами подписчиков? Будут ли активные push-уведомления для новых постов или достаточно pull/refresh по требованию? Нужны ли глобальные агрегаты («тренды», «топ за день»), и какова политика ретеншна для истории?</p>
<p>Формализуя нефункциональные характеристики, фиксируют цели: низкая латентность загрузки первой страницы, высокая пропускная способность для фан-аута, приемлемая eventual consistency между источником и пользовательской лентой, гибкость в обновлении ранжировщика, а также возможность поведения при отказах (graceful degradation). Эти уточнения формируют основу для выбора архитектурных паттернов.</p>
<h3>Этап 3. Границы системы и публичное API</h3>
<p>Граница системы — сервис формирования и выдачи ленты. Публичное API включает операции: publishEvent(sourceId, event), getFeed(userId, cursor, policy) и éventuellement subscribeToRealtime(userId) для WebSocket/streaming. API возвращает упорядоченный, постраничный набор feed-items с указанием источника, метаданных ранжирования и курсором для следующей страницы. При публикации события вызывающий компонент может передавать минимальные метаданные (тип события, timestamp, «вес»), а более тяжёлые данные (медиа) хранятся отдельно и инлайнятся ссылками.</p>
<p>Важно надёжно отделить публичный контракт от внутренней реализации: клиент не должен знать, была ли запись сформирована при записи (push fan-out) или собрана при чтении (pull). API должен позволять переключаться между стратегиями без изменения клиентского кода.</p>
<h3>Этап 4. Проектирование: сценарии, потоки данных и компоненты</h3>
<p>Сердце задачи — выбор модели fan-in/fan-out и стратегия push vs pull. При публикации событие поступает в ingest-пайплайн, проходит валидацию и поступает в durable commit-log. Отсюда есть два принципиально разных подхода к формированию персональных лент.</p>
<p>В модели fan-out-on-write (push) система сразу распространяет событие во все персональные ленты подписчиков: для каждого follower создаётся запись в per-user store (materialized feed). Это делает чтение чрезвычайно быстрым — getFeed просто читает precomputed список. Но при высоком числе подписчиков одного источника (celebrity) write-амплификация становится дорогой: публикация одного поста генерирует огромный объём записей. Решение — гибрид: выполнять eager fan-out для авторов с небольшим числом подписчиков, а для «звёзд» переключаться на read-time сборку.</p>
<p>В модели fan-out-on-read (pull) при запросе ленты система собирает события с источников, которых читатель подписан, и ранжирует их на лету. Это экономит записи при публикации, но увеличивает задержку чтения и вычислительную нагрузку на read-path, особенно при большом числе подписок. Практически всегда используют гибрид: материализованные feeds для подавляющего большинства пользователей и lazy fetch для аккаунтов с огромной аудиторией.</p>
<p>Ранжирование делится на два этапа. Offline-пайплайн (batch) формирует базовые сигналы и обученные модели рекомендаций, предвычисляет candidate set для пользователей или для сегментов. Online-компонент (real-time scorer) применяет скоринг с учётом свежих сигналов (взаимодействия в последних минутах, временные boost&#8217;ы, демографические фильтры) и ранжирует кандидатов в момент выдачи. Компонент realtime scoring должен быть быстрым и легковесным; тяжёлые ML-модели выполняются в background и результаты кешируются.</p>
<p>Хранение ленты может быть организовано как per-user materialized view (row per user with ordered list), либо как inverted index в key-value сторе, где ключ = userId, value = списочный указатель на feed-items; вместо хранения полного payload&#8217;а хранятся ссылки на события в центральном event-store и на медиа в object storage. Такой подход упрощает инвалидации и сжатие; при удалении контента проще пометить событие как удалённое, не удаляя все зеркала.</p>
<p>Для борьбы с дублированием и order-issues применяют per-user cursors и deterministic ids (eventId, sequence per source). Параллелизм и шардинг социал-графа выполняют по userId, а для равномерного распределения нагрузки используют согласованное хеширование и переназначение шардов при росте.</p>
<p>Realtime delivery опирается на subscription layer: фронт для WebSocket/HTTP-streaming получает события push из materialized feed (или через pub/sub) и отправляет их подключённым клиентам. Для push-уведомлений мобильным устройствам используется отдельный push-gateway. Для offline-пользователей события помещаются в per-device queue до момента доставки.</p>
<p>При больших всплесках нагрузки система должна применять backpressure: throttling на publish (rate limiting для источников), batching публикаций, а также адаптивную деградацию ранжировщика (упрощённый ранжировщик при пиковой нагрузке).</p>
<h3>Этап 5. Концептуальная архитектура и целостный обзор</h3>
<p>Целостная схема включает следующие зоны: ingest-пайплайн (API gateway → validation → commit-log), materialization layer (fan-out workers и per-user feed storage), ranking layer (offline feature computation + online scoring), storage (event-store, per-user feed store, media storage), realtime delivery (subscription gateways, push-gateway), и monitoring/analytics. Commit-log (Kafka или аналог) служит источником правды и даёт возможность повторной переработки событий для переиндексации лент при изменении ранжировщика. Materialization layer читает из commit-log и актуализирует per-user feeds; он же выполняет hybrid-решение: eager fan-out для «малых» авторов, флаг lazy для «горячих» аккаунтов.</p>
<p>Выдача ленты выглядит просто для клиента — чтение materialized view с последующей дополнительной онлайновой переоценкой топ-N кандидатов. Для мультиязых/мультирегионональных развертываний materialized feeds хранятся в региональных кластерах, а commit-log реплицируется либо через geo-replication, либо через региональные пайплайны с согласованным eventual consistency.</p>
<h3>Этап 6. Выбор технологий и оценка размера системы</h3>
<p>Для commit-log логичным выбором будет Kafka-подобное решение: обеспечивает высокую пропускную способность, retention и возможность переиграть события. Для materialized per-user feeds подходят хранилища, которые эффективно работают с append/prepend и range-reads: wide-column базы (Cassandra, Scylla), key-value сторы с поддержкой списков (Redis Streams для горячих данных, но при большом retention лучше Cassandra). Для медиа — object storage + CDN. Для realtime scoring и низкой латентности используют in-memory сервисы и feature caches (Redis/KeyDB), а для heavy ML — онлайн feature store и fast API для получения признаков (Feast-подобные решения).</p>
<p>Sizing начинается с расчёта: число публикаций в секунду, средний размер события, среднее число фоловеров источника и retention feed-элементов на пользователя. Общий объём записей при eager fan-out приближается к публикациям × average_followers, поэтому для сервисов с большой долей «звёзд» hybrid-подход существенно снижает нагрузку. Хранилище per-user feed должно выдерживать QPS чтений пиковых часов; для первой страницы важно обеспечить латентность мс-уровня, поэтому hot feeds держат в памяти или на SSD. Репликация, резервирование и мониторинг latency, lag в materialization workers и consumer lag в commit-log критичны для sizing.</p>
<p>Важный практический паттерн — стадирование: хранить только N последних элементов в materialized feed (sliding window), старую историю перемещать в холодное хранилище и доставлять при demand-пагинации. Это экономит оперативную память и упрощает инвалидации.</p>
<h3>Этап 7. Дополнительные вопросы, расширения и эксплуатационные аспекты</h3>
<p>Система дает возможности для множества улучшений. Персонализация может эволюционировать от простых heuristics (recency, popularity, social proximity) к сложным ML-рестраферным моделям с bandit/A-B testing. Обеспечение fairness и diversity — важные требования продукта, которые вводят дополнительные сигналы в ранжировщик. Конфиденциальность и модерация — фильтрация контента и «take down» операции требуют быстрых инвализаций materialized feeds по всему кластеру; здесь commit-log с быстрым набором worker-ов и topic для moderation-events помогает.</p>
<p>Для борьбы с накрутками и спамом добавляют throttling на create-event, signal-based detection и adaptive penalties. Для повышения UX применяют optimistic updates на клиенте и клиентскую агрегацию (например, показывать «X новых постов», не пытаясь сразу подтянуть всё).</p>
<p>Операционная сторона включает мониторинг tail-latency, consumer lag, number of hot keys, memory pressure на feed-store, size of per-user feeds и ratio eager-vs-lazy fan-out. Chaos engineering и регулярные проверки корректности ранжирования при обновлении ML-моделей обязательны. Архитектура должна позволять безболезненно «переиграть» события через commit-log для ретроактуализации ранжирования.</p>
<h2>Design a Logging System</h2>
<p>Централизованный сбор логов (ELK / ClickHouse), ingestion pipeline.</p>
<h3>Этап 1. Постановка задачи и исходный контекст</h3>
<p>Интервьюер ставит задачу: проектировать централизованный лог-ингест и хранение для больших систем. Система должна принимать логи и события от тысяч/миллионов источников (приложения, контейнеры, сетевые устройства, облачные сервисы), обеспечивать надёжный сбор, парсинг и обогащение, давать быстрый полнотекстовый поиск и агрегации в реальном времени, поддерживать аналитические запросы по историческим данным, давать возможности для алертинга и интеграции с downstream-pipelines. Требования включают: устойчивая ингерсия при пиковых нагрузках, гарантии доставки (минимум at-least-once), масштабируемое хранение с управлением ретеншном, низкая латентность для &#171;первого поиска&#187; недавно пришедших логов, дешёвое холодное хранение старых данных и обеспечение безопасности/аудита. После объявления контекста интервьюер обычно молчит; кандидат уточняет границы и характеристики.</p>
<h3>Этап 2. Формализация требований и уточняющие вопросы</h3>
<p>Кандидат задаёт набор уточняющих вопросов, определяющих архитектурный вектор. Нужны ли в логах структурированные поля (JSON) или свободный текст; какие объёмы ожидаются (EPS и средний размер записи); требования по retention для разных типов логов (audit — месяцы/годы, app debug — дни); что важнее — полнотекстовый поиск или аналитические агрегации на больших объёмах; требуется ли realtime-alerting с задержкой &lt;1–5 секунд; какова допустимая стоимость хранения; какова политика соответствия (GDPR, HIPAA). От ответа на эти вопросы зависит выбор движков: Elasticsearch/Opensearch хорош для полнотекстовых поисков и интерактивного анализа, ClickHouse эффективен для высокоскоростных аналитических агрегаций и хранения, object-storage (S3) — для холодного архива и сырого лога. Не менее важно уточнить требования по SLA/латентности для поиска свежих логов, и следует ли поддерживать reprocessing (переиграть ингерс-лог) при изменении парсинга или ранжирования.</p>
<p>Ключевые нефункциональные характеристики фиксируются так: высокая доступность ingestion-пайплайна, устойчивость к всплескам с приёмом значительной доли пиков, масштабируемость хранилища и возможности дешёвой архивации, предсказуемая стоимость и наблюдаемость (метрики lag, drop, processing time).</p>
<h3>Этап 3. Границы системы и публичный API</h3>
<p>Границы системы — входные точки для продюсеров логов и внешнего потребителя (search/analytics/alerting). Публичный контракт включает ingestion API (HTTP/S endpoint, syslog/UDP/TCP, beats/forwarder, gRPC), схему для batch-пушей и streaming (безопасный endpoint и возможности ретраев). Для downstream-потребителей публичный API включает query API (search by time/span/fields, aggregation queries, scroll/pagination), subscription API для реального времени (websocket/push on match), и управление retention/reindex/ILM. Также нужны API для schema registry (если используем Avro/Protobuf для структурированных логов), а также endpoints для health/metrics и для административных действий (удаление, экспорт).</p>
<p>API должен выражать, что ингерс — асинхронен: ответ на ingestion даёт подтверждение приёма (accepted/queued) и cursor/offset, а не мгновенную индексацию. Это позволяет упаковать ожидания по латентности.</p>
<h3>Этап 4. Проектирование: сценарии, потоки данных и компоненты</h3>
<p>Сердце системы — ingestion pipeline, построенный вокруг устойчивого commit-log (обычно Kafka или совместимый). На периферии находятся collectors/agents (Beats, Fluentd, Vector, Filebeat, syslog-ng), которые аггрегируют, буферизуют и шипят данные в ingress brokers. Сюда же входят cloud-native shippers (CloudWatch→Kinesis→Kafka), sidecar’ы в контейнерах и сетевые лог-прокси. Этот слой ответственен за backpressure и локальные ретраи — при падении центрального брокера агенты буферизуют локально.</p>
<p>Далее поток идёт в парсеры/normalizers: stream processors (Logstash, Fluent Bit, Kafka Streams, Apache Flink, or a Beam pipeline) выполняют parsing (JSON парсинг, grok), enrichment (geo-IP, service metadata, kubernetes/pod labels), фильтрацию (redaction PII) и категоризацию. Парсинг может быть тяжёлым — поэтому лучше делать его в распределённом стрим-слое, где можно горизонтально масштабироваться и переигрывать события при обновлении схем.</p>
<p>После нормализации данные идут в две основные ветви хранения: fast-search store и analytics store. Fast-search (hot path) — это полнотекстовый индекс (Elasticsearch / OpenSearch), оптимизированный на быстрый поиск по времени и полям. Analytics store (OLAP) — ClickHouse или Parquet/Delta Lake на S3, оптимизированный на массовые агрегации и дешёвое хранение больших объёмов. При этом сырые события и парсинг-оригиналы также отправляются в объектное хранилище (S3) для долговременного архива и возможности переобработки.</p>
<p>Реалтайм-алертинг реализуется на стрим-слое: CEP/streaming engine смотрит за паттернами и пишет alert-events в алерт-систему (AlertManager, PagerDuty) и/или в ES для fast-query. Для исследований и интерактивных дашбордов используется связка Kibana/Grafana, при этом heavy analytics выполняются в ClickHouse (поддерживает быстрые group-by по большим объёмам).</p>
<p>Ключевые инженерные вопросы на этом этапе: partitioning (по source-id, tenant, time), ordering (в пределах partition), idempotency (message-id для дедупа), schema evolution (registry + versioning), задержки (SLA по приёму→доступности в search), гарантия доставки (at-least-once + dedup), и backpressure (agents + brokers + circuit breakers).</p>
<p>Stampede/пики решаются буферизацией на нескольких уровнях: agent-side disk buffer, Kafka topic с retention и многими consumer groups, и масштабирование парсинг/ingest-workers. Если тема — cost, часть данных можно напрямую шорт-листить: критичные логи в ES, менее критичные в ClickHouse или только в S3.</p>
<h3>Этап 5. Концептуальная архитектура и целостный обзор</h3>
<p>В целостном виде архитектура выглядит как многоступенчатый конвейер: источники → collectors/agents (buffer, backpressure) → ingress broker (Kafka) → stream processors (parse, enrich, filter) → fast index (Elasticsearch) + analytics store (ClickHouse) + cold archive (S3 / HDFS). Над этим слоем находятся realtime alerting services, query/visualization layer (Kibana/Grafana/Custom UI), и админ-панель для управления policies/ILM/schema. Commit-log (Kafka) служит одним источником правды и даёт возможность переиграть события для исправления парсинга, регенерации индексных представлений или репроцессинга аналитики.</p>
<p>Для безопасности и мульти-тенантности вписываются шлюзы аутентификации/authorization, TLS, per-tenant topics/indices и RBAC. Для наблюдаемости system emits metrics: ingest-lag, consumer-lag, parsing-errors, index-rate, disk-usage, query-latency.</p>
<h3>Этап 6. Выбор технологий и sizing (с примером расчёта)</h3>
<p>Технологические выборы ориентируются на требования. Для collectors используют Beats/Vector/Fluent Bit; для брокера — Apache Kafka или managed Kafka (Confluent, MSK); для stream processing — Flink/Beam/Flink SQL или Kafka Streams; для быстрого поиска — Elasticsearch/OpenSearch; для аналитики — ClickHouse; для cold storage — S3 + Parquet/ORC; для alerting — Prometheus Alertmanager + custom rule engine; для schema registry — Confluent Schema Registry (Avro/Protobuf). Менеджмент и автоматизация — Kubernetes/Helm и оператор для ES/ClickHouse/Kafka, CI для schema changes.</p>
<p>Чтобы показать подход к sizing, приведу пример: предположим 100k событий в секунду, средний размер события 1 KB. Посчитаем поток сырых данных в сутки и оценим индексную нагрузку.</p>
<p>Шаги расчёта (цифры приведены детерминировано):</p>
<p>EPS = 100 000 событий/с.</p>
<p>Средний размер = 1 000 байт.</p>
<p>Байтов в секунду = EPS × размер = 100 000 × 1 000 = 100 000 000 байт/с.</p>
<p>Байтов в сутки = 100 000 000 × 86 400 = 8 640 000 000 000 байт.</p>
<p>Это 8.64 TB в сутки (десятичные ТБ, 1 TB = 10^12 байт).</p>
<p>Для индексирования в Elasticsearch обычно учитывают фактор overhead для inverted-index / replicas / metadata — практическая оценка 2–4× от сырых данных в зависимости от количества полей, анализаторов и репликации. При 3× overhead ежедневный объём индексируемых данных = 8.64 TB × 3 = 25.92 TB/day.</p>
<p>Отсюда выводы по sizing:</p>
<p>Hot storage (ES) растёт очень быстро; для 30-дневного retention потребуется ≈ 25.92 × 30 ≈ 777.6 TB индекса (без учета реплик и свободного запаса) — это повод держать в ES только последние N дней и делать tiered storage: горячие данные в ES (например последние 7 дней), тёплые в ClickHouse/SSD, холодные в S3 (Parquet).</p>
<p>Kafka throughput должен выдерживать ~100 MB/s входа и репликацию; планирование partition-count и disk throughput критично.</p>
<p>ClickHouse подходит для дешёвых агрегаций: компактное хранение Parquet и эффективная компрессия сильно уменьшают объём долгосрочного хранения.</p>
<p>Количество узлов ES зависит от target IOPS, heap sizing правил (для ES — минимизировать heap, больше RAM для filesystem cache) и репликации; расчёт узлов делается на основе 1) ожидаемой индексации/sec, 2) сред. размера сегментов, 3) retention и 4) оперативной памяти для caching.</p>
<p>Эти численные примеры показывают, почему для больших объёмов логов комбинируют движки: ES для fast-search последних данных, ClickHouse для аналитики и S3 для дешёвого архива.</p>
<h3>Этап 7. Дополнительные расширения, эксплуатация и безопасность</h3>
<p>Наконец, обсуждаем эксплуатационные и расширяющие аспекты. Schema evolution: использовать schema registry, версионировать парсеры и хранить raw-payload для переобработки. Deduplication: предусмотреть message-id и логику дедупа в стрим-слое. TL;DR по отказам: agents с disk buffer + Kafka с репликами + idempotent consumers + возможность переиграть темы. Для защиты приватных данных — redaction pipeline (PII masking) выполняется до записи в индекс; хранение raw-логов в зашифрованном виде и контроль доступа к ним. Monitoring: метрики ingest-latency, parsing-errors, consumer-lag, index-failure-rate, disk/IOPS; логирование самой лог-инфраструктуры отдельно и выделенно. Disaster recovery: snapshot ES/ClickHouse + репликации Kafka topics + объектные snapshot’ы на S3.</p>
<p>Операционные практики включают: ILM/curation для ES (rollover, shrink, delete), автоматический переход индексов между hot/warm/cold tiers, lifecycle для ClickHouse/Parquet архива, регулярные тесты переигрывания событий и проверка возможности reindex, автоматическое масштабирование consumer groups, и security audits. Для multi-tenant deployments — строгое изоляция данных по tenant-id (topics, indices, access control), rate-limiting per-tenant и мониторинг затрат.</p>
<h2>Search Autocomplete</h2>
<p>Система подсказок с высокой скоростью отклика.</p>
<h3>Этап 1. Постановка задачи и контекст</h3>
<p>Интервьюер ставит задачу: спроектировать службу автодополнения для поисковой строки, которая возвращает набор подсказок за очень низкое время отклика (целевые SLO — десятки миллисекунд для p95/p99). Система должна обрабатывать сотни тысяч или миллионы запросов в секунду, обеспечивать релевантность подсказок (popularity, personalization, recency), корректно работать при опечатках и частичных вводах, уважать фильтры безопасности (фильтрация матерных/запрещённых выражений) и позволять быструю инвалидацию/обновление подсказок при поступлении нового контента. Дополнительно важно хранение телеметрии (CTR подсказок, последующий поисковый запрос), A/B тестирование ранжировщиков и GDPR-совместимость при персонализации. После постановки интервьюер замолкает; кандидат переходит к формализации.</p>
<h3>Этап 2. Формализация требований и уточняющие вопросы</h3>
<p>Кандидат формализует функциональные и нефункциональные требования и задаёт уточнения, которые определяют архитектуру. Вопросы касаются допустимой латентности на p50/p95/p99; объёма QPS; допускаемых trade-offs между свежестью и скоростью; уровня персонализации (анонимная подсказка vs per-user); требуемой устойчивости к опечаткам; политики по обновлению словаря и частоте добавления новых фраз; требований к многокультурности и мульти-язычности; и допустимой стоимости (in-memory решение дорогé, но быстро). На основании ответов формируются целевые характеристики: ultra-low latency на read-path, eventual or near-real-time freshness (seconds→minutes), high throughput, per-query ranking с быстрым ранжированием и малой вычислительной нагрузкой.</p>
<p>Ключевое архитектурное решение уже на этом этапе: разделение на быстрый read-path (latency-critical)—использующий пред-вычисленные структуры и кэш, и менее критичный write-path для обновления словаря и сигналов ранжирования. Это диктует выбор data structures и потоков обновления.</p>
<h3>Этап 3. Границы системы и публичный API</h3>
<p>Граница системы — HTTP/gRPC интерфейс для клиентов (web, mobile, backend), плюс административные API для управления словарём и метриками.</p>
<p>Примеры контрактов:</p>
<p>GET /v1/autocomplete?q=nyc%20wea&amp;userId=123&amp;locale=en-US&amp;max=10<br />
Response:<br />
{<br />
&#171;query&#187;: &#171;nyc wea&#187;,<br />
&#171;suggestions&#187;: [<br />
{&#171;text&#187;:&#187;nyc weather&#187;, &#171;type&#187;:&#187;query&#187;, &#171;score&#187;: 98.3, &#171;source&#187;:&#187;global_popular&#187;},<br />
{&#171;text&#187;:&#187;nyc waterfront restaurants&#187;, &#171;type&#187;:&#187;entity&#187;, &#171;score&#187;: 87.1, &#171;source&#187;:&#187;local_index&#187;}<br />
],<br />
&#171;meta&#187;: {&#171;served_from&#187;:&#187;edge_cache&#187;,&#187;latency_ms&#187;:6}<br />
}</p>
<p>POST /v1/suggests/bulk_update<br />
body: {updates: [&#8230;]} // admin API for adding/removing phrases</p>
<p>POST /v1/telemetry<br />
body: {userId, query, suggestionText, clicked}</p>
<p>Get-операция должна быть синхронной и укладываться в заданный SLO; админ-операции асинхронны — они пускают изменения в ingestion pipeline.</p>
<p>Граница системы также подразумевает ответственность за нормализацию входа (lowercase, unicode normalization, tokenization), за rate limiting и за базовую фильтрацию. Клиент не должен знать, использовался ли trie, FST, или search engine внутри; контракт остаётся стабильным.</p>
<h3>Этап 4. Проектирование: happy path и exceptional flows; компоненты и потоки данных</h3>
<p>Happy path для запроса простой: пользователь вводит префикс, клиент посылает /autocomplete, система нормализует префикс, проверяет edge/region cache (в память CDN/edge), затем обращается к локальному suggestion service. Suggestion service читает пред-вычисленные структуры (например FST/finite-state transducer, trie-индексы или inmemory priority lists), получает candidate set и применяет lightweight re-scoring (weighting by popularity, personalization signals, recency boosts, business rules). Результат кэшируется на лепестковом уровне (edge CDN, per-node LRU) и возвращается клиенту.</p>
<p>Основные компоненты:</p>
<p>• Frontend gateways (terminate TLS, auth, basic throttling) и edge caches, сокращающие путь для «горячих» префиксов.<br />
• Suggestion service, реализующий latency-critical read-path: in-memory data structures (FST, prefix trie, or compressed tries stored in memory-mapped files), per-shard caches и fast scorer.<br />
• Offline ingestion pipeline: прием обновлений (popularity events, new phrases, deletions) через commit-log (Kafka), батчевые генераторы FST/indices и incremental updaters (near-real-time).<br />
• Realtime updater/nearline buffer: для немедленной видимости новых фраз поддерживается small in-memory overlay (log-structured memtable) или write-through to fast index; периодическая сшивка overlay → main FST.<br />
• Persistance: backing store for phrase metadata (counts, timestamps, signals) — key-value store или OLAP store.<br />
• Telemetry/analytics: сбор CTR, abandonment, latencies; pipeline feeding ML models for ranking.<br />
• Admin services: profanity lists, blacklists, synonym management.</p>
<p>Выбор структуры данных определяет эффективность. Для дешёвых стоимостных reads эффективен FST (Lucene’s FST), который компактно хранит множество строк и позволяет быстро делать prefix lookup и буферизированную итерацию кандидатов. Для случаев, когда требуется выдать не только exact-prefix, но и fuzzy/typo-tolerant suggestions, используют сочетание FST + n-gram индекс, либо implement Levenshtein automata on the FST, либо предгенерируют n-grams/edge-ngrams в индексе (на чтение это будет быстрый lookup).</p>
<p>Ранжирование: важно держать основной скоринг лёгким: score = α·popularity + β·personalization + γ·recency + δ·business_boost. Сигналы popularity/recency можно инкрементировать в streaming pipeline; personalization сигналы запрашиваются из per-user store, но их извлечение должно быть быстрым (кэши, precomputed user preferences). Тяжёлые ML-модели применяют офлайн или в online-re-rank только на top-K кандидатов (K small, например 50→re-rank→return top 10).</p>
<p>Опечатки и fuzzy matching: варианты реализации разных степеней сложности. Простая, быстрая техника — edge-ngrams: хранить для каждой фразы ее префиксные n-грамы (tri/bi-grams) и при вводе искать по ним. Более точные варианты — Levenshtein automata intersected with FST (поддерживается в Lucene) или использование BK-trees для spell correction. Компромисс: либо дать очень быстрые prefix suggestions с limited fuzzy, либо тратить CPU/latency на глубокий fuzzy-lookup; обычно практикуют hybrid: prefix strict matching + lightweight fuzzy fallback.</p>
<p>Обновление данных и freshness: ingestion pipeline собирает имплицитные сигналы (search logs, clicks) → events → commit-log → streaming counter updates (increment popularity in real-time DB) → periodic rebuild of on-disk/mmapped structures (FST) или incremental update via small delta-FSTs merged regularly. Для immediacy поддерживают in-memory delta layer: new phrases go to memtable visible to reads, and background process compacts memtable into main FST every N seconds/minutes. Это даёт near-real-time видимость без перегенерации всей структуры.</p>
<p>Exceptional flows: сетевые задержки, кеш-мисс, перегрузка backends. При недоступности suggestion-service отвечают из edge cache или возвращают пустой набор с контролируемым fallthrough. Для защиты от широких атак и «helloworld»-пиков — rate limiting и per-key hot-key protection: если префикс слишком «горяч», используем precomputed top suggestions и ограничиваем expensive fuzzy attempts.</p>
<h3>Этап 5. Концептуальная схема и целостный обзор</h3>
<p>В целом система делится на два логических слоя: latency-critical read layer и asynchronous write/ingest layer. Read layer состоит из front gateways, edge caches (CDN / per-region cache), suggestion nodes (sharded, in-memory structures), и per-node caches. Write layer — telemetry collectors, event stream (Kafka), stream processors (real-time counters), offline batch jobs (rebuild FSTs, train ranking models), и admin pipelines (blacklist/synonym updates). Persistance включает columnar/kv store для metadata и object storage для backup of indices.</p>
<p>При таком дизайне путь для типичного запроса минимален: client → edge cache → suggestion node (in-mem FST) → scorer → return. Обновления проходят через stream → counters update → memtable overlay → periodic compaction. Это обеспечивает быстрое чтение и приемлемую свежесть.</p>
<h3>Этап 6. Выбор технологий и оценка размера системы (sizing)</h3>
<p>Технологии, часто применимые в продакшне: Lucene/Solr/Elasticsearch (completion suggester, FST), OpenSearch, Redis (for ultra-low-latency small-cache), RocksDB/LMDB as backing store, Kafka для ingestion, Flink/Kafka Streams for counters, and CDN/edge caches.</p>
<p>Как подходить к sizing без конкретных цифр: исходят из QPS автодополнения и целевых latency. Если система должна выдерживать 1M QPS, то чтение полностью из памяти критично; каждому suggestion-шарду ставим ограничение по числу подключений и throughput. Для FST, хранимого в памяти, важна оценка memory footprint: FST эффективен компрессией; если общий словарь из M фраз (например 100M фраз), и средний cost per phrase в FST ~ few tens bytes (зависит от overlaps and shared prefixes), общая память может быть порядка десятков гигабайт до сотен гигабайт. Для масштабируемости делим словарь на шард-ключи (hash by normalized prefix range or hash(query) with replica routing). Реплики для availability: обычно 2–3 копии sharded data.</p>
<p>Примерный расчёт метрики latency/throughput: допустим p95 latency target = 20ms сетевое + сервисное; suggestion node должна обслуживать lookup + scoring в пределах 10 ms. Это диктует in-memory structures и ограничение на количество вспомогательных запросов (minimize external calls). Если per-query re-rank использует heavy ML, то нужно выполнять re-rank только на top-K, к тому же re-rank model делает inference в специализированных serving nodes (GPU/CPU) с заранее закешированными фичами.</p>
<p>Ключевые практические правила sizing: поддерживать запас на 2–3× пиков для latencies; использовать региональные deployment для снижения сетевого RTT; хранить hot part of dictionary на RAM, warm part — on fast mmapped files; считать память на replica factor; проектировать auto-scaling для suggestion nodes по CPU/latency.</p>
<h3>Этап 7. Дополнительные вопросы и расширения</h3>
<p>Система предлагает много дополнительных функций и сложностей: персонализация подсказок под пользователя (history-based boosting, user segments), A/B тестирование ранжировщиков, поддержка многоязычности и локалей (separate indices or locale-aware normalization), context-aware suggestions (query + current page context), voice/ASR spelling corrections, privacy-aware ranking (обход персонализации по запросу) и safe-search filtering.</p>
<p>Операционные аспекты: мониторинг p50/p95/p99 latency, suggestion CTR, abandonment rate, error rate, hot-prefix heatmap; alerting on cache-evictions and high rebuild times; capability to replay logs to rebuild signals; procedures for blacklist/whitelist and rapid take-down; continuous evaluation pipeline for ranking models; chaos testing for partial failures and cold start.</p>
<p>Безопасность и модерация критичны: real-time profanity/PII filtering in ingestion, rate limiting to prevent abuse, and telemetry retention policies to satisfy privacy regulations.</p>
<h2>File Storage Service (Dropbox / Google Drive Lite)</h2>
<p>Хранение файлов, версионирование, синхронизация, шардинг.</p>
<h3>Этап 1. Постановка задачи и контекст</h3>
<p>Задача: спроектировать сервис для хранения пользовательских файлов с поддержкой версионирования, синхронизации между устройствами и масштабируемым хранением. Сервис должен позволять пользователям загружать и скачивать файлы разного размера, синхронизировать изменения между несколькими устройствами в реальном времени или в режиме фоновой синхронизации, предоставлять историю версий, позволять совместное использование (sharing) и обеспечивать разумную защиту данных и приватность. Нефункциональные требования включают: высокая доступность и надёжность (данные не теряются), масштабируемость по объёму и по числу операций, эффективное использование сети и диска (делта-обновления), гарантия целостности данных и возможность восстановления предыдущих версий, низкая латентность при доступе к «горячим» файлам, и экономичное долговременное хранение больших объёмов. Интерфейс должен поддерживать как веб/мобильный доступ, так и фоновые «sync» клиенты.</p>
<h3>Этап 2. Формализация требований и уточняющие вопросы</h3>
<p>Кандидат уточняет поведение и приоритеты. Насколько строгими должны быть гарантии консистентности между устройствами: нужен ли строгий сериализуемый порядок или достаточно eventual consistency с механизмами разрешения конфликтов? Ожидается ли поддержка больших файлов (десятки гигабайт) и потоковой передачи (streaming)? Как часто изменяются файлы: превалируют небольшие изменения в документах или крупные медиа-файлы? Какая политика версионирования: хранить все версии бесконечно или лимитировать по количеству/времени? Нужны ли блокировки на уровне файлов (advisory locks) или optimistic concurrency с merge-стратегиями? Будет ли служба предоставлять совместное редактирование в реальном времени или достаточно синхронизации версий? Ответы на эти вопросы задают направление по выбору архитектуры: если важна низкая вероятность конфликтов и сильная согласованность, потребуется более сложная координация; если важна пропускная способность и масштаб, стоит ориентироваться на eventually consistent store с семантикой «последнее сохранение выигрывает» и поддержкой версионирования для отмен и слияния.</p>
<p>Ключевые нефункциональные требования фиксируются так: устойчивость и долговечность хранения (RPO≈0), горизонтальная масштабируемость по объёму и по числу клиентов, экономичное холодное хранение, оптимизация сетевого трафика при синхронизации (делта-обновления, chunking), и поддержка офлайн-клиентов с последующей репликацией.</p>
<h3>Этап 3. Границы системы и публичный API</h3>
<p>Границы — сервис хранения и синхронизации, видимый клиентам и администраторам. Публичный API должен покрывать основные сценарии: загрузка (upload), скачивание (download), получение метаданных, создание/удаление/переименование объектов, управление версиями (listVersions, restoreVersion), операции синхронизации (sync cursors, long-poll / push notifications), управление доступом и шаринг (grant/revoke), а также административные операции (lifecycle, retention, audit logs).</p>
<p>Примеры контрактов: PUT /files/{user}/{path}?uploadId=… для chunked multipart upload; GET /files/{user}/{path}?version=… для получения конкретной версии; POST /sync/push {changes, clientCursor, deviceId} возвращает serverCursor и конфликтные файлы; GET /sync/pull?since=cursor возвращает дифф изменений. API должен быть идемпотентен по возможности: клиент при повторной загрузке с тем же uploadId продолжит процесс, при повторном push изменений сервер должен обрабатывать отдельно существующие идентификаторы изменения.</p>
<h3>Этап 4. Проектирование: сценарии, потоки данных и компоненты</h3>
<p>Проектирование начинается с happy path: пользователь создаёт или изменяет файл на устройстве. Клиент определяет изменения и передаёт дельту в сервис. Для эффективного использования сети и диска применяется разбиение на блоки (chunking) и адресуемое по содержанию хранение чанков (content-addressable storage, CAS). Клиент делит файл на фиксированные или переменные по контенту чанки, вычисляет их хеши, запрашивает у сервера какие чанки уже есть (dedup), загружает отсутствующие чанки, затем отправляет метаданные (manifest) с указанием порядка чанков и метаданных файла. Сервер, получив manifest, создаёт или обновляет объект файла, сохраняет ссылку на набор чанков, создаёт новую версию и обновляет метаданные пользователя.</p>
<p>Синхронизация между устройствами основана на механизме cursors или change-log: клиент периодически запрашивает изменения с момента своей последней синхронизации или получает push-уведомление. При приходе изменений клиент запрашивает нужные версии/чанки и применяет их локально. При одновременных модификациях применяется стратегия разрешения конфликтов: оптимистичная стратегия с версионными метками и автоматическим merge для текстовых файлов (via operational transforms или CRDT для real-time), либо сохранение обеих версий и уведомление пользователя о конфликте. Для многих приложений выбран pragmatic подход: если файл не является документом для real-time совместной работы, сохранять обе версии (conflict copy) и позволять пользователю/клиенту разрешить конфликт.</p>
<p>Компоненты системы складываются из нескольких групп. Persistence layer включает chunk-storage (объектное хранилище: S3 или его эквиваленты) и metadata store (ключ-значение или wide-column DB для маппинга файлов→манифестов, прав доступа, индексов). Chunk-store хранит неизменяемые чанки, оптимизированы по throughput и долговечности. Metadata store хранит и индексирует древовидную структуру каталогов, ACL, версии и cursors. Sync service отвечает за ingestion client-изменений, запись в write-ahead log (commit-log) и генерацию уведомлений для подписанных устройств. Background services выполняют сборку версий, очистку orphanChunks (гc), lifecycle и tiering (перемещение старых версий в холодный архив). Authentication/authorization и audit trail — отдельные сервисы. Кроме того, требуется notification service для push-уведомлений, CDN для отдачи больших файлов и streaming service для медиа.</p>
<p>Exceptional flows: потеря соединения при upload → поддержка resumable uploads (uploadId + chunkId), недоступность chunk-store → agents буферизуют с помощью local cache или retry-queue, конфликт версий → автогенерация conflict-file и уведомление пользователя, попытки злоупотреблений → rate-limiting и quota checks. Издержки масштабирования: горячие файлы (часто запрашиваемые) требуют кешей (edge CDN, Redis), холодные — перенос в дешёвую долговременную память.</p>
<p>Шардирование и балансировка. Metadata store шардируется по userId или по namespace, давая силуэшную изоляцию пользователей. Chunk-store шардируется по хешу чанка; хеш-ориентированное распределение даёт естественный баланс и позволяет эффективно дедуплицировать повторяющиеся данные у разных пользователей. Для масштабируемости каталоги больших аккаунтов (с миллионами объектов) необходимо делать paging и ленивую инициализацию каталогов, а также оптимизировать операции list с помощью precomputed index shards.</p>
<p>Версионирование. Версии реализуются как неизменяемые manifest-объекты, указывающие на список чанков и метаданные. При изменении создаётся новый manifest и ссылка на него сохраняется в metadata store. Политика хранения версий управляется lifecycle: хранить последние N версий или все версии за последние T дней; старые версии могут быть перемещены в холодный архив и/или удалены по политике.</p>
<p>Безопасность и целостность. Все чанки и manifest подписываются хешами; при восстановлении/передаче клиент может проверить целостность. Для защиты приватности применяют шифрование на стороне сервера (server-side encryption) либо end-to-end (client-side encryption) по требованию. Access control реализуется через токены и ACL, а аудит операций логируется в отдельную систему логов.</p>
<h3>Этап 5. Концептуальная схема и целостный обзор</h3>
<p>В целостном виде система выглядит как набор взаимосвязанных слоёв. На входе находятся клиенты, которые через API/SDK общаются с front-end gateway. Gateway выполняет аутентификацию, базовую валидацию и маршрутизацию в sync-service. Sync-service пишет операции в commit-log и обращается к metadata-store и chunk-API. Chunk-API взаимодействует с объектным хранилищем для записи и чтения чанков и с кешами для ускорения отдачи. Metadata-store хранит файловую структуру и версии; он шардирован по userId для масштабирования. Background workers читают commit-log и выполняют асинхронные задачи: сбор garbage (удаление неиспользуемых чанков), tiering, репликация и генерацию уведомлений. Notification-service доставляет события на устройства и триггерит pull. CDN/edge ноды обеспечивают быструю доставку больших файлов. Monitoring и alerting покрывают все слои: latency upload/download, storage utilization, gc-lag, error-rates. Такая архитектура разделяет долговременное хранение неизменяемых чанков и динамическую метаинформацию, что упрощает дедупликацию, версионирование и масштабирование.</p>
<h3>Этап 6. Выбор технологий и оценка размера системы (sizing)</h3>
<p>Технологии выбирают с учётом требований: объектное хранилище S3 (или S3-совместимое) как основа chunk-store для долговечности и cheap cold storage; для горячего слоя — SSD-backed storage или специализированные распределённые хранилища (Ceph, MinIO, Google Cloud Storage) с шардированием по хешу. Metadata store — выбор между DynamoDB/Cassandra/Spanner для горизонтальной масштабируемости и низкой латентности; PostgreSQL/MySQL подходят для меньших конфигураций, но масштабирование сложнее. Commit-log — Kafka или управляемые аналоги для гарантий доставки и переигрывания изменений. Для индексов и поиска (по имени, тегам) применим Elasticsearch или dedicated search service. Для edge-каша и распределённого lock-менеджмента используют Redis (для short-lived locks, rate-limits, sessions). Для push-уведомлений — APNs/FCM и internal push gateway.</p>
<p>Sizing: начнём с входных параметров: число пользователей, среднее число файлов на пользователя, средний размер файла, процент активных пользователей, QPS операций. Приведу примерный расчёт: 10M пользователей, среднее файлов на аккаунт 200, средний размер файла 5 MB. Объём данных ≈ 10M * 200 * 5MB = 10M * 1GB = 10 PB. Учитывая репликацию, overhead метаданных и index, итоговый raw storage может быть в районе 20–30 PB. Для такого объёма S3-стратегия с tiered storage необходима: горячие последние изменения держать на SSD/fast object storage, холодные перемещать в Glacier/Archive. По IOPS: если 1% файлов запросы в сутки, это 100k * 200 = 20M file accesses/day ≈ 230 ops/s — требует большого кеширования на edge. Для throughput при параллельных загрузках оцените пиковую QPS и планируйте трансферную пропускную способность и количество воркеров.</p>
<p>Dedup и chunking существенно сокращают объём: при высокой доле повторяющихся данных (например, резервные копии ОС или медиа) дедупликация может снизить объём хранимых данных в разы. Chunk-size trade-off: маленькие чанки дают лучшую дедупликацию и параллелизм, но больше метаданных; большие — меньше метаданных, но хуже дедупликация. Часто выбирают переменные по контенту чанки (content-defined chunking, CDC) с средней величиной 8–64 KB для хорошего компромисса.</p>
<p>Операционные параметры: поддерживать запас по throughput ~2–3× пиков для обеспечения устойчивости при пиковых бёрстах; мониторить gc latency (удаление старых версий) и неизменно иметь процедуры восстановления. RPO/RTO зависят от SLA — для критичных данных делают гео-репликацию и snapshot-ы.</p>
<h3>Этап 7. Дополнительные вопросы и расширения</h3>
<p>Наконец, варианты расширений и усложнений. Поддержка совместного редактирования в реальном времени требует реализации OT/CRDT и более сложной модели хранения изменений; это значительно меняет архитектуру, так как сервер должен поддерживать fine-grained ops и трансформации. End-to-end шифрование (E2EE) на клиенте существенно усложняет дедупликацию и серверные операции (сервер не видит содержимого, значит не может дедуплицировать или индексировать), но обеспечивает высокую приватность; практическая компромиссная модель — client-side encryption с optional server-side metadata indexing. Multi-tenant enterprise features (audit, compliance, retention policies, legal hold) требуют расширений metadata-store и интеграции с SIEM.</p>
<p>Другие полезные темы для продакшн-решения: версии хранения для efficient snapshotting (дедуплицированные manifests), квоты и billing, экспорты/миграции, интеграция с файловыми протоколами (WebDAV, SMB), CDN-integrated streaming для больших медиа, интеграция с CDN+signed-URLs для безопасного доступа, и тонкая моделирование прав доступа для совместного использования. Операционные практики: end-to-end тестирование restore-процессов, регулярные drill’ы по отказу региона, мониторинг tail-latency, и автоматизация lifecycle (tiering, cleanup) с отчетностью об экономике хранения.</p>
<h2>Video Streaming Platform (YouTube Lite)</h2>
<p>CDN, encoding pipeline, рекомендации (поверхностно).</p>
<h3>Этап 1. Постановка задачи и контекст</h3>
<p>Интервьюер объявляет задачу: спроектировать видеоплатформу, которая принимает пользовательские видео, хранит и транскодирует их в форматы, подходящие для воспроизведения в браузере и на мобильных устройствах, раздаёт контент с низкой задержкой глобальной аудитории через CDN, обеспечивает адаптивную потоковую доставку (ABR), ведёт хранение и метаданные, а также поддерживает базовую подсказку рекомендаций. Нефункциональные требования включают возможность масштабирования до миллионов загрузок и просмотров, минимизацию задержки старта воспроизведения (startup latency), эффективное использование сети и диска, экономную долговременную архивацию исходников, надёжность и устойчивость к пиковым нагрузкам (выпуски, вирусный контент). Платформа должна обеспечивать безопасность контента и правообладательский контроль, а также базовую телеметрию (view counts, watch-time, QoS metrics).</p>
<p>Интервьюер завершает ввод, кандидат переходит к уточнению требований и проекту.</p>
<h3>Этап 2. Формализация требований и уточняющие вопросы</h3>
<p>Кандидат формализует функциональность и определяет важные нефункциональные характеристики. Нужно понять ожидаемые SLO: p95 startup latency (например &lt;2 s), допустимую задержку между загрузкой и доступностью в основном качестве (minutes→tens of minutes), поддерживаемые устройства и сети (mobile 3G/4G/Wi-Fi), требования к DRM или платному доступу, допустимые форматы исходников, объёмы хранения и ретеншн исходников, а также глубину рекомендаций (simple collaborative filtering vs сложная ML-пайплайн). Следует уточнить требования по live-streaming: требуется ли live или только VOD; в данном упрощённом кейсе сосредотачиваемся на VOD, упомянув отличия для live.</p>
<p>Нефункционально фиксируется приоритет: минимальная задержка старта и стабильная entrega для зрителя; масштабируемость отдачи и дешёвое cold-хранение исходников. Консистентность метрик и надёжность данных о просмотрах и QoS важны для биллинга и рекомендаций.</p>
<h3>Этап 3. Границы системы и публичное API</h3>
<p>Границы — сервисы загрузки/обработки/хранения/доставки видео, а также API для клиентов и админов. Публичный контракт включает загрузку (multipart / resumable upload), запрос метаданных и статусов обработки, запрос манифеста потоков HLS/DASH, получение статических ресурсов (thumbnails, captions), просмотр счётчиков и отправка телеметрии (player events). API для бек-офиса предоставляет операции для модерации, управления encoding profiles, lifecycle policies и получения аналитики.</p>
<p>Ключевая инвариантность API: клиент не зависит от внутренней реализации транскодирования или CDN; он получает m3u8/DASH manifest и готов к ABR-плееру. Для загрузки поддерживается resumable uploads с uploadId — это упрощает обработку нестабильных сетей.</p>
<h3>Этап 4. Проектирование: happy path, exceptional flows и основные компоненты</h3>
<p>Happy path загрузки: пользователь отправляет исходный файл на Upload Gateway. Gateway аутентифицирует, проверяет квоты и инициирует chunked/streaming загрузку в объектное хранилище (S3-like), возвращая uploadId. После загрузки manifest (metadata) помещается в job-queue для encoding pipeline. Worker подхватывает задачу, извлекает исходник, выполняет транскодирование в набор битрейтов и разрешений, генерирует сегменты и manifest’ы для HLS и/или DASH, создаёт thumbnails и субтитры (если требуется), а затем выгружает результаты в объектное хранилище, обновляет metadata store и помечает video «готово».</p>
<p>Воспроизведение: клиент запрашивает manifest и начинает получать сегменты через CDN. Плеер поддерживает ABR: на основе скорости сети и буфера выбирается подходящий битрейт. Для минимизации startup latency применяется быстрый первоначальный профиль (low-res first) и HTTP/2/3 prefetching. CDN обслуживает горячие части контента; origin хранится в S3 и используется при cache miss.</p>
<p>Ключевые компоненты формируются естественным образом: Upload Gateway (auth, quota), Object Storage (raw + encoded assets), Encoding Pipeline (job queue, workers, transcoders), Manifest/Segment Store (обычно same object storage with structured paths), CDN (edge nodes, cache policies), Metadata Service (video metadata, status, thumbnails, captions, access control), Playback Service (tokenized URLs, signed URLs, DRM gateway), Telemetry Pipeline (player events → ingestion → analytics), Moderation Service (automated checks: copyright/visual detectors + human review), and Recommendation Service (lightweight for YouTube Lite).</p>
<p>Exceptional flows: транскодер падает/ошибается → job возвращается в очередь и выполняется retry с backoff; исходник повреждён → пометить failed и уведомить uploader; spike загрузок → elastic scaling workers; CDN cache miss при первом просмотре → origin-read cost и потенциальное latency bump; нарушение прав → take-down через admin API и invalidation в CDN и storage.</p>
<p>Особенности с учётом live vs VOD: live требует низкой end-to-end latency, incremental chunking, специализированных transcoders (实时分发) и обычно отдельного pipeline; в этом разборе лишь отмечаем, что live меняет требования к pipeline и CDN (chunk-oriented low-latency delivery, WebRTC/LL-HLS).</p>
<h3>Этап 5. Концептуальная архитектура и целостный обзор</h3>
<p>В центре архитектуры лежит объектное хранилище для исходников и артефактов транскодирования, job-queue (Kafka/SQS) и серия worker-кластов для CPU/GPU-транскодинга. Upload Gateway принимает файлы и публикует задачи; Encoding Workers выполняют транскод, упаковку в сегменты и генерацию manifest’ов; результаты хранится в объектном хранилище по структуре виде/ resolution/bitrate/segmentIndex; Metadata Service хранит ссылки на manifest’ы и служит единой точкой запроса для клиента; CDN кэширует сегменты и manifest’ы, снижая нагрузку на origin; Telemetry собирает player events и feed’ит аналитические и recommendation pipelines.</p>
<p>Cache-invalidation для take-down/DMCA осуществляется через CDN purge API и пометки в Metadata Service. Для доставки защищённого контента используется signed URL + short TTL и, при необходимости, DRM gateway, выдающий лицензию.</p>
<p>Важный деталь: манифесты и сегменты должны быть организованы таким образом, чтобы позволять частичную замену/обновление (например, обновить один bitrate не затрагивая другие). Также хранение исходников и encoded assets разделяются lifecycle политикой: исходник можно держать в hot хранении некоторое время, затем архивировать.</p>
<h3>Этап 6. Технологии и sizing</h3>
<p>Технологический стек обычно включает: объектное хранилище (S3 или S3-compatible) для стабильного долговременного хранения; очередь заданий (Kafka, SQS) для coordinate encoding jobs; контейнеризированные encoding workers (FFmpeg-основа, hardware accel via NVENC/VideoCore/TPU) управляемые автоскейлингом; CDN (Cloudflare, Fastly, AWS CloudFront или свой CDN) для глобальной доставки; metadata store (NoSQL — DynamoDB/Cassandra или RDBMS для транзакционных требований); telemetry/analytics (Kafka→Flink/Beam→ClickHouse/BigQuery); signer/service for secure URLs; и опционально DRM providers.</p>
<p>Sizing делается через входные метрики: ожидаемая частота загрузок, средний размер исходника, среднее число просмотров в сутки, медианная длительность просмотра, процент пиковых событий. Примерный расчёт: при 1000 загрузок/сутки и среднем исходнике 500 MB объём исходников ≈ 0.5 TB/сутки. Транскодирование требует CPU/GPU ресурса, пропорционального aggregate encode time. Если средний encode на одном worker занимает 30 минут (CPU-bound), и требуется обрабатывать 1000 задач/сутки равномерно, нужен пул из порядка 30 workers одновременно работающих (30 min per job → 48 jobs/day per worker → ~21 workers; запас 2×→~42). Для пикового поведения важен autoscaling. Хранение сегментированных файлов требует учитывать мультибитрейты: если исходник 500 MB, encoded outputs могут суммарно быть 50–150% от исходника в зависимости от профилей и сегментации; нужно оценивать replication и CDN cache hit ratio.</p>
<p>ABR и плеер: выбирать HLS (в широкой поддержке) и/или MPEG-DASH; сегменты маленького размера (2–4 s) уменьшают startup latency и улучшают адаптации, но увеличивают overhead запросов; trade-off выбирается в зависимости от target devices.</p>
<p>Экономические соображения: CDN cost dominates delivery; origin bandwidth и storage — вторичные. TTL и cache-control играют роль: long cache headers для сегментов, если нет DRM или частых инсертных изменений; для платного/личного контента используют signed URLs с коротким TTL.</p>
<h3>Этап 7. Расширения и эксплуатационные аспекты (рекомендации поверхностно)</h3>
<p>Рекомендации и personalization. Для YouTube Lite достаточно простого рекомендационного слоя: candidate generation via popularity signals (recent views, trending), basic collaborative filtering (user history → nearest neighbours) и content-based boosting (same tags, creator). Pipeline: player events → ingestion → feature store → offline model training → online serving via feature cache + lightweight ranker. Тяжёлые модели выполняются офлайн и результаты кешируются в per-video / per-user buckets.</p>
<p>Мониторинг и SRE. Ключевые метрики: startup latency p50/p95/p99, time-to-first-byte for manifest and first segment, cache hit ratio at CDN, encode queue depth, failed encodes, storage growth, bandwidth cost, QoE metrics (rebuffer rate, average bitrate). Алерты на резкие изменения QoE и падение CDN hit rate. Chaos testing encoding pipeline и CDN purge/responsiveness.</p>
<p>Контент-безопасность и модерация. Автоматическая фильтрация (специализированные детекторы для аудио/видео), human review pipelines, takedown flows с атомарной инвалидацией manifest и purge CDN, и audit trail. Для правообладателей интеграция fingerprinting/ContentID.</p>
<p>Особые случаи. Для live streaming потребуются low-latency ingest (RTMP/WeBRTC), chunking с минимальными сегментами, специализированные packers; для VR/360 и больших разрешений нужны иные encoding профили и CDN-настройки (edge-caching с поддержкой byte-range).</p>
<h2>Distributed Task Queue (как Celery / Kafka Consumer System)</h2>
<p>Отложенные задачи, ретраи, DLQ.</p>
<h3>Этап 1. Постановка задачи и контекст</h3>
<p>Интервьюер ставит задачу: спроектировать распределённую систему для обработки асинхронных задач. Система должна принимать задачи от различных сервисов и пользователей, обрабатывать их с возможностью отложенного запуска и повторных попыток при сбоях, гарантировать доставку и порядок выполнения в рамках потребностей приложения, поддерживать DLQ (Dead Letter Queue) для задач, которые не удалось обработать, и обеспечивать наблюдаемость и мониторинг. Нефункциональные требования включают горизонтальную масштабируемость (обработка миллионов задач в сутки), высокую доступность и устойчивость к сбоям отдельных компонентов, возможность приоритизации задач, эффективность хранения и доставки сообщений, а также контроль количества повторных попыток и backoff стратегий. После объявления контекста интервьюер молчит, кандидат переходит к уточнению требований.</p>
<h3>Этап 2. Формализация требований и уточняющие вопросы</h3>
<p>Кандидат задаёт уточняющие вопросы: какие гарантии доставки нужны — at-most-once, at-least-once или exactly-once; допустим ли reordering задач; требуется ли поддержка отложенных задач на миллисекундном или секундном уровне; сколько разных типов задач и их приоритетов; какова допустимая задержка между постановкой и выполнением задачи; нужен ли persistence задач на диске или допустим in-memory queue; допустим ли потерянный task при полном крахе системы; какая политика для DLQ — фиксированное количество ретраев или динамическая.</p>
<p>Нефункциональные характеристики формулируются так: высокая доступность брокеров/очередей, масштабируемость обработчиков (worker nodes), устойчивость к spike нагрузкам, предсказуемость задержки выполнения задач, auditability (ведём метрики по успехам/ошибкам/retries).</p>
<h3>Этап 3. Границы системы и публичный API</h3>
<p>Границы системы включают API для продюсеров задач и сервисов-обработчиков:</p>
<p>Producer API: enqueue_task(task_payload, type, priority, schedule_time, max_retries), поддерживает синхронный/асинхронный ответ с task_id.</p>
<p>Consumer API: pull_task(worker_id, batch_size), acknowledge_task(task_id, status), nack_task(task_id, reason).</p>
<p>Admin API: inspect queues, DLQ, retry/failure policy, purge tasks, metrics (queue length, task success/failures).</p>
<p>Основной контракт: producer создаёт задачу, worker забирает, выполняет и подтверждает результат; при сбое задача может быть повторена или отправлена в DLQ.</p>
<h3>Этап 4. Проектирование: сценарии, потоки данных и компоненты</h3>
<p>Happy path: клиент создаёт задачу → очередь (broker) ставит её в очередь → worker забирает задачу → выполняет → подтверждает успешное выполнение → broker удаляет задачу.</p>
<p>Retry: если обработка завершается ошибкой, задача возвращается в очередь или отправляется в delay queue с backoff.</p>
<p>DLQ: после превышения max_retries задача помещается в DLQ для ручного или автоматического анализа.</p>
<p>Основные компоненты:</p>
<p>Producer/Task Client — формирует задачу и публикует в broker, опционально может подписываться на статус выполнения.</p>
<p>Broker / Message Queue — хранит задачи, управляет delivery semantics, поддерживает persistence (Kafka, RabbitMQ, Redis Streams). Broker шардируется по типу задач или key hashing для балансировки нагрузки.</p>
<p>Worker Nodes — забирают задачи из очереди, выполняют их, используют concurrency/async для высокой throughput. При падении worker задача возвращается в очередь.</p>
<p>Scheduler / Delay Queue — управляет задачами с отложенным запуском, поддерживает таймеры и приоритетные очереди.</p>
<p>DLQ Handler — обрабатывает задачи, которые не удалось выполнить после заданного числа retries.</p>
<p>Exceptional flows: broker недоступен → producer буферизует локально или fallback; worker падает во время выполнения → задача переходит на повторное выполнение; spike нагрузка → auto-scaling worker nodes; блокировка очереди/падение persistence → задачи остаются в commit log.</p>
<p>Архитектурные решения: partitioning (по task type или hash), idempotency (task_id уникальный для повторных delivery), backoff strategy (fixed, exponential, jitter), visibility timeout (чтобы unacknowledged tasks возвращались), monitoring &amp; alerting (queue depth, retry count, failed tasks).</p>
<h3>Этап 5. Концептуальная схема и целостный обзор</h3>
<p>Целостно система выглядит как три слоя:</p>
<p>Producers: микросервисы, веб-клиенты, cron jobs. Публикуют задачи в broker.</p>
<p>Broker Layer: message queue / streaming system (Kafka/RabbitMQ/Redis Streams). Поддерживает persistence, partitions, priority queues, delay queues.</p>
<p>Workers: масштабируемые группы обработчиков, забирают задачи, выполняют, подтверждают результат. Используется backoff, retry и DLQ.</p>
<p>Дополнительно: scheduler/cron для отложенных задач, monitoring/metrics collector, DLQ analyzer, admin tools для ручного вмешательства.</p>
<h3>Этап 6. Выбор технологий и sizing</h3>
<p>Технологии:</p>
<p>Broker: Kafka (для high throughput, log-based persistence), RabbitMQ (AMQP, priority queues), Redis Streams (low-latency, simple tasks).</p>
<p>Workers: Celery + Python, or custom Go/Java consumers, с автошардингом и concurrency.</p>
<p>Delay queue / scheduler: Redis sorted sets, Kafka + scheduled jobs, RabbitMQ delayed messages.</p>
<p>DLQ: отдельная очередь или топик в Kafka для failed tasks.</p>
<p>Sizing: предположим 1M задач/сутки, средний task payload 1 KB.</p>
<p>Объём данных в broker ≈ 1 GB/day.</p>
<p>Worker throughput: если average task = 100ms, 1 worker = 10 tasks/sec → для 1M tasks/day (~12 tasks/sec average), достаточно 2–3 workers с запасом. Для spike нагрузки используется autoscaling.</p>
<p>Storage: если broker log retention = 7 дней, Kafka топик ~7 GB для persistence.</p>
<p>Для большого масштаба: sharding по task type или key hashing, replication factor ≥2 для HA, мониторинг lag и unacknowledged tasks, alert на превышение retry threshold.</p>
<h3>Этап 7. Расширения и эксплуатация</h3>
<p>Расширения:</p>
<p>Prioritization: отдельные priority queues, worker pools для high-priority tasks.</p>
<p>Rate limiting: ограничение на количество задач от одного producer.</p>
<p>Exactly-once semantics: идемпотентные task_id + deduplication на worker side.</p>
<p>Observability: метрики по выполнению задач, retries, DLQ, latency distribution.</p>
<p>Fault tolerance: multi-region brokers, persistent queues, auto-retry.</p>
<p>Backpressure: если очередь растёт → throttling producers или динамическое масштабирование workers.</p>
<p>Эксплуатация: мониторинг lag, queue depth, DLQ growth, failed tasks, autoscaling workers, SLA по latency обработки, алерты на превышение retries.</p>
<h2>Design a Web Crawler</h2>
<p>Обход страниц, распределение нагрузки, хранение результатов.</p>
<h3>Этап 1. Постановка задачи и контекст</h3>
<p>Интервьюер формулирует задачу: спроектировать веб-краулер, который обходит страницы Интернета и сохраняет их содержимое для последующей индексации или анализа. Система должна масштабироваться на миллионы и миллиарды страниц, обеспечивать распределённый обход без перегрузки сайтов, поддерживать приоритетное сканирование, следовать правилам robots.txt, обрабатывать динамический контент (JS-rendered pages), и эффективно хранить результаты. Нефункциональные требования включают высокую производительность обхода, fault tolerance, управление очередями URL, дедупликацию, поддержание politeness (ограничение числа запросов к одному домену в единицу времени) и мониторинг. После постановки контекста интервьюер молчит, инициатива переходит к кандидату.</p>
<h3>Этап 2. Формализация требований и уточняющие вопросы</h3>
<p>Кандидат уточняет: требуется ли full web (весь Интернет) или ограниченный crawl (например, news sites, заданный набор доменов); какая глубина обхода (BFS vs DFS); допустимая задержка между обнаружением страницы и её обработкой; как часто нужно обновлять уже обработанные страницы; формат хранения контента (HTML, текст, метаданные, ссылки, скриншоты); поддержка мультиязычных сайтов; политика обхода ошибок (HTTP 5xx, timeout); SLA по freshness.</p>
<p>Ключевые архитектурные характеристики: high throughput, scalability (горизонтальное добавление crawlers), politeness, data consistency для очередей URL и хранилища контента, fault tolerance и возможность восстановления после падений, мониторинг и auditability.</p>
<h3>Этап 3. Границы системы и публичный API</h3>
<p>Граница системы — набор сервисов, принимающих URLs, управляемых scheduler, хранение страниц, API для мониторинга и управления.</p>
<p>Публичный API:</p>
<p>enqueue_url(url, priority, metadata) — добавление нового URL в crawl queue.</p>
<p>fetch_next_urls(batch_size, domain_constraints) — worker забирает URLs для обхода с учётом politeness.</p>
<p>store_page(url, content, metadata) — сохранение результата обхода.</p>
<p>admin APIs — stats по crawler’ам, очередь, DLQ для неудачных fetch’ей, управление политиками (robots.txt, crawl delay).</p>
<p>Контракт обеспечивает идемпотентность: URL не должен обрабатываться повторно без нужды; результаты fetch’а должны сохраняться атомарно с метаданными.</p>
<h3>Этап 4. Проектирование: сценарии, потоки данных и компоненты</h3>
<p>Happy path: Scheduler выбирает URL → распределяет его на available worker → worker делает HTTP request → парсит HTML → извлекает ссылки и контент → сохраняет результат в хранилище → новые ссылки возвращает в scheduler → повторение.</p>
<p>Exceptional flows: HTTP ошибки/timeout → retry с backoff → после max_retries в DLQ; блокировка сайта (robots.txt) → skip; rate-limit на домен → delay queue; падение worker → task возвращается в очередь.</p>
<p>Компоненты:</p>
<p>URL Frontier / Scheduler — центральный компонент, управляющий очередями, politeness, приоритетами и дедупликацией. Может быть распределённым для горизонтальной масштабируемости.</p>
<p>Workers / Fetchers — HTTP-клиенты, забирающие страницы, парсеры, извлекают ссылки и метаданные. Worker pool масштабируется горизонтально.</p>
<p>Content Storage — распределённое хранилище HTML/текст/медиа (S3-like), с возможностью индексации и хранения метаданных (URL, crawl time, HTTP headers).</p>
<p>URL Deduplication / Bloom Filters — предотвращение повторного обхода одинаковых URL; хранится на fast-access storage (Redis, Cassandra).</p>
<p>Delay Queues / Politeness Enforcer — контролирует частоту обращений к одному домену; может реализовываться через per-domain timers.</p>
<p>Retry &amp; DLQ — задачи с ошибками помещаются в DLQ после заданного числа попыток.</p>
<p>Monitoring / Metrics — очередь на обработку, latency fetch, success rate, error rate, politeness violations.</p>
<p>Обработка динамического контента: JS-rendered pages могут обрабатываться через headless браузеры (Puppeteer, Playwright) или lightweight JS engines; trade-off: высокая латентность, но больше coverage.</p>
<h3>Этап 5. Концептуальная схема и целостный обзор</h3>
<p>Система делится на слои:</p>
<p>Scheduler / URL Frontier — распределённый, хранит очереди и приоритеты, управляет politeness и дедупликацией.</p>
<p>Worker Layer — масштабируемые fetchers, делают HTTP запросы, парсят страницы, извлекают ссылки, сохраняют content + metadata.</p>
<p>Storage Layer — распределённое хранение контента, метаданных, индексация для дальнейшего анализа или поиска.</p>
<p>Delay / Retry / DLQ Layer — управляет отложенными задачами и ошибками.</p>
<p>Monitoring / Analytics — метрики работы системы и качества crawl.</p>
<p>Поток данных: Scheduler → Worker → Storage → Scheduler (для новых ссылок). Для politeness Scheduler использует per-domain queues и timers; для масштабирования Frontier распределяется по шардированию доменов или хешированию URL.</p>
<h3>Этап 6. Выбор технологий и sizing</h3>
<p>Технологии:</p>
<p>Message Queue / Scheduler: Kafka, RabbitMQ, Redis Streams для распределённого распределения URL.</p>
<p>Workers: контейнеризированные fetchers на Go/Python/Java, headless browsers для JS.</p>
<p>Storage: S3/MinIO для raw content, Cassandra/HBase/Elasticsearch для метаданных и быстрых lookup.</p>
<p>Deduplication: Bloom Filter в Redis/Redis Cluster или Cassandra; scalable filters на уровне shards.</p>
<p>Retry &amp; DLQ: очередь Redis/Kafka для failed fetches.</p>
<p>Sizing:</p>
<p>Допустим, 1B страниц в базе, средний HTML 50 KB → raw storage ~50 TB.</p>
<p>Для throughput 1M pages/day: ~11.5 pages/sec; при среднее fetch 500ms → ~6 workers/worker pool, с запасом 2× → 12 fetchers.</p>
<p>Politeness: max 1 request/sec/domain, Scheduler управляет rate-limit per domain.</p>
<p>Dedup: Bloom filter для 1B URL с false positive 1% → ~1.2 GB RAM; для распределения shard’ы.</p>
<p>Для больших масштабов: Frontier распределён по доменам, Workers горизонтально масштабируемы, Storage tiering (hot/cold), CDN для часто запрашиваемых страниц, batching fetch results для экономии IOPS.</p>
<h3>Этап 7. Расширения и эксплуатационные аспекты</h3>
<p>Возможные расширения:</p>
<p>Prioritization / Relevance: crawl pages based on importance, PageRank or domain weight.</p>
<p>Incremental crawl / freshness: повторное посещение страниц с заданной частотой.</p>
<p>Politeness policies: динамический crawl-delay per domain, adaptive backoff.</p>
<p>Dynamic content handling: headless browsers, AJAX crawling.</p>
<p>Content parsing: extract metadata, structured data (JSON-LD, microdata), media.</p>
<p>Monitoring / Alerting: latency, error rates, politeness violations, queue backlog.</p>
<p>Fault tolerance: multi-region Frontier, persistence for URL queues, checkpointing for resuming after crash.</p>
<h2>Design a Payment System</h2>
<p>Идемпотентность, транзакции, безопасность, подписания запросов.</p>
<h3>Этап 1. Постановка задачи и контекст</h3>
<p>Интервьюер формулирует задачу: спроектировать систему для обработки финансовых транзакций между пользователями и/или сервисами. Система должна позволять инициировать платежи, проверять баланс, проводить трансферы, обеспечивать идемпотентность операций и атомарность списания и зачисления средств, поддерживать безопасность и шифрование запросов, а также логировать операции для аудита. Нефункциональные требования: высокая доступность, консистентность данных, защита от двойного списания (double-spend), масштабируемость по числу пользователей и объёму транзакций, низкая латентность подтверждения платежей и надежная обработка отказов. После постановки контекста интервьюер молчит.</p>
<h3>Этап 2. Формализация требований и уточняющие вопросы</h3>
<p>Кандидат уточняет:</p>
<p>Тип платежей: P2P, P2M, B2B, кэш-аут?</p>
<p>Обязательная строгая консистентность или eventual consistency допустима в некоторых сценариях?</p>
<p>Нужно ли поддерживать multi-currency и конвертацию?</p>
<p>Какие SLA по latency: мгновенное подтверждение vs batch processing?</p>
<p>Какие требования к идемпотентности: на уровне request-id или transaction-id?</p>
<p>Требования к безопасности: шифрование, подпись, токенизация, PCI DSS совместимость?</p>
<p>Что делать с отказами: retry, compensation transactions?</p>
<p>Нефункционально фиксируются ключевые характеристики: atomicity, consistency, isolation, durability (ACID), idempotency, security, high availability, auditability.</p>
<h3>Этап 3. Границы системы и публичный API</h3>
<p>Границы системы включают публичный API для клиентов и внутренних сервисов, backend платёжного движка, интеграцию с банками/платёжными шлюзами, внутреннее хранилище транзакций и баланс-сервис.</p>
<p>Пример API:</p>
<p>POST /payments: инициирует платеж с параметрами (sender, receiver, amount, currency, idempotency_key).</p>
<p>GET /payments/{id}: проверка статуса платежа.</p>
<p>POST /accounts/{id}/topup: пополнение баланса.</p>
<p>POST /accounts/{id}/withdraw: вывод средств.</p>
<p>GET /accounts/{id}/balance: получение текущего баланса.</p>
<p>Admin API: просмотр логов, rollback, reconciliation, мониторинг.</p>
<p>Контракт: каждый request с idempotency_key гарантирует, что повторный вызов не создаст двойную транзакцию.</p>
<h3>Этап 4. Проектирование: сценарии, потоки данных и компоненты</h3>
<p>Happy path: пользователь инициирует платеж → API валидирует request и idempotency_key → проверка баланса → блокировка суммы на счёте отправителя → запись транзакции в транзакционный журнал (commit log) → выполнение списания и зачисления → уведомление клиента → обновление статуса.</p>
<p>Exceptional flows:</p>
<p>Недостаточно средств → отклонение, логирование.</p>
<p>Сбой в процессе списания → retry с idempotency_key.</p>
<p>Сбой при обновлении баланса получателя → compensation transaction.</p>
<p>Дублированный запрос → обработка через idempotency_key.</p>
<p>Основные компоненты:</p>
<p>API Gateway — аутентификация, валидация, rate limiting, подпись/токенизация запросов.</p>
<p>Payment Service / Transaction Engine — core logic: atomic debit/credit, балансировка нагрузки, идемпотентность, retry и compensation.</p>
<p>Account / Ledger Service — хранение актуального баланса и истории транзакций; реализуется как ACID DB или с транзакционной обработкой через Event Sourcing.</p>
<p>Transaction Log / Audit Trail — immutable журнал всех операций, необходим для reconciliation и compliance.</p>
<p>Notification Service — уведомление пользователей о статусе платежей.</p>
<p>External Gateway Integrations — связь с банками, платёжными шлюзами, обработка callback’ов.</p>
<p>Идемпотентность: idempotency_key хранится вместе с записью транзакции; повторный request проверяет наличие ключа, и если транзакция уже выполнена, возвращает текущий результат.</p>
<p>Безопасность: HTTPS/TLS, подпись запросов HMAC или JWT, токенизация чувствительных данных, PCI DSS совместимость для хранения платёжной информации, ограничение доступа через RBAC и audit logging.</p>
<h3>Этап 5. Концептуальная схема и целостный обзор</h3>
<p>Архитектура:</p>
<p>API Gateway принимает и валидирует запросы, проверяет подписи.</p>
<p>Payment Engine получает запрос, проверяет баланс, резервирует средства и записывает транзакцию в Transaction Log.</p>
<p>Ledger / Account DB применяет изменения атомарно (например, в ACID-базе или через Event Sourcing с атомарными projections).</p>
<p>Notification Service сообщает клиентам результат.</p>
<p>External Payment Gateways подключаются через асинхронные callback’и и подтверждают завершение платежей.</p>
<p>Данные движутся в pipeline: Request → Validation → Ledger/Transaction Engine → Commit → Notification. При сбоях используется retry, DLQ и compensation.</p>
<h3>Этап 6. Выбор технологий и sizing</h3>
<p>Технологии:</p>
<p>DB: PostgreSQL/MySQL для ACID, либо распределённые NewSQL (CockroachDB, Spanner) для глобального масштаба.</p>
<p>Transaction Log: Kafka для событийной модели, immutable journal для audit &amp; recovery.</p>
<p>Payment Engine / Microservices: Go/Java/Python с поддержкой concurrency и worker pools.</p>
<p>API Gateway: Nginx/Envoy + JWT/HMAC validation.</p>
<p>Caching: Redis для hot balances и rate-limiting.</p>
<p>Security: TLS, HSM для ключей, tokenization service.</p>
<p>Sizing:</p>
<p>Для 1M пользователей, 10k tx/day, средний payload 1 KB → ~10 MB/day журнал транзакций; 30 days retention → 300 MB; легко управляется любой SQL/NoSQL хранилищем.</p>
<p>Throughput: 10k tx/day ≈ 0.1 tx/sec average, пиковый load 50 tx/sec → 3–5 worker nodes с резервом.</p>
<p>Для глобальной системы требуется распределённая ACID DB с sharding по userId или ledgerId, replication factor ≥2.</p>
<h3>Этап 7. Расширения и эксплуатационные аспекты</h3>
<p>Multi-currency и FX: отдельные ledger per currency, конверсия с актуальными курсами.</p>
<p>Fraud detection: интеграция с ML/Rule engine для аномалий.</p>
<p>Batch processing: payroll, массовые выплаты.</p>
<p>Reconciliation: сверка транзакций с external gateways и internal ledgers.</p>
<p>Monitoring &amp; SLA: latency, failed transactions, retry rate, balance correctness.</p>
<p>High availability: multi-region deployment, leader election for transaction engine.</p>
<p>Compliance: immutable logs, retention policies, audit reports.</p>
<h2>E-commerce Checkout System</h2>
<p>Корзина, ордеринг, инвентаризация, платежи.</p>
<h3>Этап 1. Постановка задачи и контекст</h3>
<p>Интервьюер формулирует задачу: спроектировать систему оформления заказов для интернет-магазина. Система должна обеспечивать работу корзины, формирование заказов, проверку наличия товаров на складе (инвентаризация), интеграцию с платёжной системой, поддержку скидок, налогов и доставки, а также уведомления пользователя о статусе заказа. Нефункциональные требования включают высокую доступность, согласованность данных о товарах и остатках, предотвращение overselling, масштабируемость по числу пользователей и заказов, низкую задержку оформления заказа и поддержку отказоустойчивости. После постановки контекста интервьюер замолкает, кандидат переходит к уточнению деталей.</p>
<h3>Этап 2. Формализация требований и уточняющие вопросы</h3>
<p>Кандидат уточняет:</p>
<p>Нужно ли поддерживать multi-currency и multi-warehouse?</p>
<p>Какова допустимая консистентность между корзиной и складом — строгая или eventual consistency?</p>
<p>Какие сценарии скидок и акций нужно поддерживать: купоны, bundle, loyalty?</p>
<p>Поддержка частичных оплат или отложенных платежей?</p>
<p>Как управлять отменой и возвратами?</p>
<p>SLA по latency checkout: мгновенное подтверждение или batch?</p>
<p>Политика при падении внешней платёжной системы.</p>
<p>Ключевые архитектурные характеристики: atomicity при создании заказа, consistency запасов, high throughput, scalability, fault tolerance, idempotency для повторных запросов, auditability и безопасность платежей.</p>
<h3>Этап 3. Границы системы и публичный API</h3>
<p>Границы системы охватывают корзину, checkout сервис, управление заказами, интеграцию с платёжными провайдерами и управление запасами.</p>
<p>Публичный API:</p>
<p>Cart API: add_item(userId, productId, quantity), remove_item, get_cart, update_quantity.</p>
<p>Checkout API: create_order(cartId, payment_info, shipping_info), confirm_payment(orderId), cancel_order(orderId).</p>
<p>Inventory API: check_stock(productId, quantity), reserve_stock(orderId), release_stock(orderId).</p>
<p>Order API: get_order_status(orderId), list_orders(userId).</p>
<p>Admin API: manage inventory, apply discounts, audit logs.</p>
<p>Контракт: API должен обеспечивать идемпотентность при повторных вызовах (например, повторный create_order с тем же requestId не создаёт дублированный заказ).</p>
<h3>Этап 4. Проектирование: сценарии, потоки данных и компоненты</h3>
<p>Happy path: пользователь добавляет товары в корзину → инициирует checkout → система проверяет наличие товаров → резервирует stock → создаёт запись заказа → инициирует платёж → подтверждает платёж → подтверждает заказ и уведомляет пользователя → обновляет stock и accounting.</p>
<p>Exceptional flows:</p>
<p>Недостаточно товара → отказ с сообщением пользователю.</p>
<p>Платёж отклонён → заказ остаётся в pending, stock возвращается.</p>
<p>Сбой в резервировании stock → retry или отказ.</p>
<p>Дублированный запрос create_order → обработка через idempotency_key.</p>
<p>Компоненты:</p>
<p>Cart Service — хранит временные корзины, поддерживает session, кеширование, idempotent операции.</p>
<p>Inventory Service — проверка и резервирование stock, поддержка concurrency и транзакционность, репликация для HA.</p>
<p>Order Service — создание заказа, управление статусами, интеграция с Inventory и Payment Service.</p>
<p>Payment Service — безопасная интеграция с платёжными шлюзами, подтверждение транзакций, retry и idempotency.</p>
<p>Notification Service — уведомления о статусе заказа.</p>
<p>Discount / Pricing Service — вычисляет финальную цену, применяет купоны и акции.</p>
<p>Архитектурные решения:</p>
<p>Atomicity: при создании заказа важна транзакция между Order и Inventory (можно реализовать через distributed transaction / saga / two-phase commit).</p>
<p>Idempotency: create_order с уникальным requestId.</p>
<p>Concurrency: оптимистическая блокировка stock или pessimistic lock при high contention.</p>
<p>Resilience: retry и compensating transactions при сбоях в Payment Service.</p>
<h3>Этап 5. Концептуальная схема и целостный обзор</h3>
<p>Система делится на слои:</p>
<p>Frontend / API Gateway — принимает запросы от клиентов, аутентифицирует, валидирует и применяет idempotency.</p>
<p>Cart &amp; Order Services — управляют корзинами, заказами, резервированием stock и статусами.</p>
<p>Inventory Service — консистентное хранилище остатков, интеграция с warehouses.</p>
<p>Payment Service — платёжный движок с idempotent API и retry.</p>
<p>Discount / Pricing Service — вычисление финальной суммы, налоги, акции.</p>
<p>Notification &amp; Audit — уведомления, логирование, мониторинг.</p>
<p>Поток данных: Cart → Checkout → Inventory reservation → Order creation → Payment → Notification → Commit stock changes.<br />
Для fault tolerance используются retry, DLQ и compensating transactions.</p>
<h3>Этап 6. Выбор технологий и sizing</h3>
<p>Технологии:</p>
<p>DB: PostgreSQL/MySQL или распределённые ACID NewSQL (CockroachDB, Spanner) для Order + Inventory.</p>
<p>Caching: Redis/Memcached для Cart и hot-stock.</p>
<p>Message Queue: Kafka/RabbitMQ для интеграции между сервисами и событийной обработки.</p>
<p>Payment Gateway: интеграция с внешними провайдерами.</p>
<p>Notification: push/email/queue.</p>
<p>Sizing:</p>
<p>100k пользователей, средний cart 5 товаров, 10k заказов/день.</p>
<p>Stock updates: 50k ops/day, low-latency queries → Redis hot cache.</p>
<p>Orders: 10k/day → 2–3 Order worker nodes, queue-based processing.</p>
<p>Inventory: масштабируется по productId, репликация для HA.</p>
<p>Trade-offs:</p>
<p>Strong consistency на stock vs high throughput — можно использовать optimistic locking + compensating transactions.</p>
<p>Distributed transaction сложны на scale → Saga pattern предпочтительнее.</p>
<h3>Этап 7. Расширения и эксплуатационные аспекты</h3>
<p>Multi-warehouse &amp; fulfillment: распределение stock по локациям.</p>
<p>Partial shipments: split order по availability.</p>
<p>Flash sales / spikes: rate-limiting, queueing и priority handling.</p>
<p>Audit &amp; compliance: immutable logs, monitoring.</p>
<p>Discount &amp; loyalty programs: сложные правила применения.</p>
<p>Idempotent checkout: защищает от повторных кликов или network retries.</p>
<p>Monitoring &amp; SLOs: latency checkout, stock reservation failures, payment errors.</p>
<h2>Design a Notification System (email + push + SMS)</h2>
<p>Ротация каналов, очереди, массовая доставка.</p>
<h3>Этап 1. Постановка задачи и контекст</h3>
<p>Интервьюер объявляет задачу: спроектировать систему уведомлений, которая поддерживает несколько каналов доставки — email, push, SMS. Система должна обеспечивать массовую рассылку уведомлений, при этом управлять приоритетами и ротацией каналов (fallback: если push недоставлен, отправить SMS), гарантировать высокую доставляемость, обработку отказов и retry, а также мониторинг и аналитическую отчётность. Нефункциональные требования: масштабируемость до миллионов уведомлений в сутки, низкая латентность доставки для критичных уведомлений, устойчивость к пиковым нагрузкам (flash campaigns), идемпотентность отправки, fault tolerance и observability. После постановки контекста интервьюер замолкает.</p>
<h3>Этап 2. Формализация требований и уточняющие вопросы</h3>
<p>Кандидат уточняет:</p>
<p>Какие типы уведомлений: transactional (OTP, alerts) vs marketing (promotions)?</p>
<p>SLA по latency для каждого канала?</p>
<p>Поддержка персонализации: template engine, placeholders?</p>
<p>Ротация каналов: fallback при failure, or prioritized sending?</p>
<p>Требования к retry: количество попыток, backoff strategy?</p>
<p>Политика rate limiting для SMS / email / push?</p>
<p>Нужно ли отслеживать opens/clicks/delivery status?</p>
<p>Ключевые нефункциональные характеристики: high throughput, scalability, reliability, idempotency, low latency, observability, fault tolerance.</p>
<h3>Этап 3. Границы системы и публичное API</h3>
<p>Границы системы включают публичные API для внутренних сервисов и админ-панели, обработку очередей уведомлений и взаимодействие с внешними каналами (SMTP, push providers, SMS gateways).</p>
<p>Пример API:</p>
<p>POST /notifications: создать уведомление с параметрами (recipient, channel(s), templateId, priority, metadata).</p>
<p>GET /notifications/{id}/status: получить статус доставки.</p>
<p>Admin API: просмотр очередей, retry, отмена, статистика delivery.</p>
<p>Контракт: один notification_id = одна логическая цель; повторные вызовы с тем же notification_id должны быть идемпотентны.</p>
<h3>Этап 4. Проектирование: сценарии, потоки данных и компоненты</h3>
<p>Happy path: сервис создаёт уведомление → enqueue в centralized notification queue → dispatcher забирает уведомление → выбирает канал и template → отправляет через provider → обновляет status → логирование и метрики.</p>
<p>Fallback / channel rotation: если push fails (undelivered) → enqueue retry на SMS → при SMS failure → enqueue retry на email (опционально).</p>
<p>Exceptional flows:</p>
<p>Недоступность внешнего провайдера → retry с backoff.</p>
<p>Ограничение rate limit → delay queue.</p>
<p>Повторное получение того же notification_id → skip / return cached status.</p>
<p>Template rendering fail → логирование, alert, skip delivery.</p>
<p>Основные компоненты:</p>
<p>API Gateway / Notification Service — принимает уведомления, валидирует, применяет idempotency, enqueues.</p>
<p>Central Queue / Message Broker — Kafka/RabbitMQ/Redis Streams для распределённой обработки.</p>
<p>Dispatcher / Worker Pool — забирает уведомления, выбирает канал и template, вызывает provider API.</p>
<p>Channel Providers — SMTP/email service, push provider (Firebase/APNs), SMS gateway.</p>
<p>Retry &amp; DLQ — задачи, не доставленные после N попыток, помещаются в DLQ для анализа.</p>
<p>Monitoring / Analytics — delivery rate, latency, errors, opens/clicks.</p>
<p>Ротация каналов и fallback: реализуется в dispatcher с state machine per notification. При failure → next channel → retry → update status.</p>
<h3>Этап 5. Концептуальная схема и целостный обзор</h3>
<p>Система делится на слои:</p>
<p>Producer/API layer — принимает уведомления, enqueues с idempotency key и priority.</p>
<p>Queue / Broker — хранение и шардирование уведомлений по каналам и приоритетам, поддержка delay queues.</p>
<p>Dispatcher / Worker Layer — канал-агностичные workers, выбирают канал и template, выполняют отправку, обрабатывают retries и fallback.</p>
<p>Provider Layer — интеграция с внешними email/SMS/push провайдерами.</p>
<p>Analytics &amp; Monitoring — метрики по доставке, latency, retry rate, failed notifications.</p>
<p>DLQ / Retry Management — отдельные очереди для failed notification, возможность ручного вмешательства.</p>
<p>Поток данных: Notification request → Queue → Dispatcher → Channel provider → Update status → Analytics. Для масштабирования queue шардируется по recipient hash / channel type.</p>
<h3>Этап 6. Выбор технологий и sizing</h3>
<p>Технологии:</p>
<p>Queue: Kafka для high throughput, RabbitMQ или Redis Streams для low-latency.</p>
<p>Workers: контейнеризированные dispatchers на Go/Java/Python, горизонтально масштабируемые.</p>
<p>Template engine: Handlebars, Jinja2, или proprietary templating service.</p>
<p>Provider integration: SMTP servers, Firebase Cloud Messaging, APNs, Twilio/Plivo для SMS.</p>
<p>Monitoring: Prometheus/Grafana, ELK stack или ClickHouse для аналитики.</p>
<p>DLQ &amp; Retry: отдельная топика/queue, retry policy с exponential backoff.</p>
<p>Sizing:</p>
<p>1M notifications/day, смешанный канал: 50% push, 30% email, 20% SMS.</p>
<p>Среднее время доставки push ≈ 1s, email ≈ 5s, SMS ≈ 2s.</p>
<p>Throughput requirement ~12 notifications/sec average, peak 500/sec → horizontal scaling of dispatcher pool (20–30 workers per channel type).</p>
<p>Queue retention: 7 days for retry and monitoring.</p>
<p>Analytics storage: aggregated metrics ~10 MB/day, raw logs ~100 MB/day → manageable with Kafka + ClickHouse.</p>
<p>Trade-offs: для критичных notifications (OTP, alerts) priority queue; marketing notifications can be batched for cost efficiency. Retry и DLQ позволяют выдерживать SLA при отказах провайдера.</p>
<h3>Этап 7. Расширения и эксплуатационные аспекты</h3>
<p>Personalization: dynamic templates, user preferences for channel.</p>
<p>Rate limiting &amp; throttling: per channel, per recipient, global daily cap.</p>
<p>Batching: объединение маркетинговых сообщений для экономии SMS/email cost.</p>
<p>Monitoring &amp; alerting: failed deliveries, latency spikes, retry saturation.</p>
<p>High availability: multi-region queues, autoscaling dispatcher pool.</p>
<p>Audit &amp; compliance: immutable logs, retention policies.</p>
<p>Feedback loop: opens/clicks/events feed into recommendation engine or analytics.</p>
<h2>Real-Time Analytics Dashboard</h2>
<p>Агрегации, окна, стрики, кластер Kafka/ClickHouse.</p>
<h3>Этап 1. Постановка задачи и контекст</h3>
<p>Интервьюер ставит задачу: спроектировать систему для отображения реального времени метрик и событий на дашборде. Система должна обрабатывать события с различных источников (например, веб-сайт, мобильные приложения, IoT), агрегировать их по различным измерениям (user, region, product, event type), строить скользящие окна (tumbling, sliding), поддерживать стриминговые и исторические данные, обеспечивать низкую задержку обновления (sub-second – секунды), и масштабироваться на миллионы событий в минуту. Нефункциональные требования: high throughput, low latency, fault tolerance, горизонтальное масштабирование и возможность добавления новых источников без downtime. После постановки контекста интервьюер молчит.</p>
<h3>Этап 2. Формализация требований и уточняющие вопросы</h3>
<p>Кандидат уточняет:</p>
<p>Какие виды метрик: count, sum, average, unique users, percentiles?</p>
<p>Размер и количество окон: минутные, часовые, sliding?</p>
<p>SLA latency: обновление дашборда в реальном времени (1–2s) или near-real-time (10–30s)?</p>
<p>Историческое хранение: retention событий и агрегатов?</p>
<p>Количество источников событий и ожидаемый EPS (events per second)?</p>
<p>Требования к консистентности: точное vs approximate aggregates (HyperLogLog, sketches)?</p>
<p>Нужно ли поддерживать ad-hoc queries или только predefined dashboards?</p>
<p>Нефункциональные характеристики: high throughput, low latency, scalability, fault tolerance, data retention, query flexibility.</p>
<h3>Этап 3. Границы системы и публичное API</h3>
<p>Границы включают источники событий, стриминговый слой, слой хранения и API/сервис визуализации.</p>
<p>Публичное API:</p>
<p>POST /events: ingestion событий с payload (event_type, userId, timestamp, metadata).</p>
<p>GET /dashboard/{metric}: возвращает агрегированные данные для фронтенда.</p>
<p>Admin API: управление retention, источниками, схемой событий, мониторинг throughput.</p>
<p>Контракт: события не теряются; каждый event_id идемпотентен; агрегаты должны быть согласованы с окном времени.</p>
<h3>Этап 4. Проектирование: сценарии, потоки данных и компоненты</h3>
<p>Happy path: источник отправляет событие → Kafka topic (partitioned by key) → Stream Processor (Flink / Spark Streaming / ksqlDB) → вычисление агрегатов по нужным окнам → запись в ClickHouse (OLAP) → фронтенд дашборда делает запрос → визуализация в реальном времени.</p>
<p>Exceptional flows:</p>
<p>Kafka broker недоступен → producer retry + buffering.</p>
<p>Stream processor node падает → state recovery из checkpoint / Kafka offsets.</p>
<p>ClickHouse shard недоступен → fallback на реплику, частичная доставка.</p>
<p>Высокая нагрузка → autoscaling Stream Processor, горизонтальное масштабирование ClickHouse.</p>
<p>Основные компоненты:</p>
<p>Event Producers — приложения, сервисы, IoT-устройства.</p>
<p>Message Broker / Event Bus — Kafka, sharding и partitioning по ключу (userId, event_type).</p>
<p>Stream Processing / Aggregation — Flink, Spark Streaming, ksqlDB, выполняет windowed aggregations, counts, sums, averages, sketches.</p>
<p>State Store — RocksDB или встроенные state backend stream processors для windowed state.</p>
<p>OLAP Storage — ClickHouse, хранит агрегаты и исторические данные для ad-hoc queries.</p>
<p>Dashboard API / Frontend — REST или GraphQL API, обновление в режиме push (WebSocket) или pull.</p>
<p>Monitoring &amp; Alerting — lag monitoring Kafka, stream processing throughput, ClickHouse query performance.</p>
<p>Windowing &amp; aggregation: tumbling (fixed), sliding (overlapping), session windows. Для unique counts используют approximate structures (HyperLogLog, Count-Min Sketch) для уменьшения памяти.</p>
<h3>Этап 5. Концептуальная схема и целостный обзор</h3>
<p>Система делится на слои:</p>
<p>Producers — источники событий.</p>
<p>Event Bus / Kafka — durable, partitioned, fault-tolerant.</p>
<p>Stream Processor — windowed aggregation, stateful computation, checkpointing for recovery.</p>
<p>OLAP / ClickHouse — хранение агрегатов и исторических данных.</p>
<p>Dashboard API / Frontend — WebSocket/REST API для push обновлений.</p>
<p>Monitoring &amp; Logging — lag metrics, throughput, error rate, alerts.</p>
<p>Поток данных: Event → Kafka → Stream Processor → Aggregates → ClickHouse → Dashboard. При high-throughput partitioning по userId/event_type + replication. Checkpoints обеспечивают recovery на случай сбоя.</p>
<h3>Этап 6. Выбор технологий и sizing</h3>
<p>Технологии:</p>
<p>Message Broker: Kafka cluster с replication factor ≥2, partitioning по event_type/userId.</p>
<p>Stream Processing: Flink, Spark Streaming, ksqlDB — поддержка stateful windowed aggregation, exactly-once semantics.</p>
<p>State Store: RocksDB или Flink state backend для windowed state.</p>
<p>OLAP Storage: ClickHouse, поддержка merge-tree, materialized views, retention policies.</p>
<p>Frontend / API: Node.js / Go / Python + WebSocket для realtime push.</p>
<p>Monitoring: Prometheus + Grafana, Kafka lag monitoring, Flink metrics.</p>
<p>Sizing:</p>
<p>EPS = 1M events/sec, 100 bytes per event → 100 MB/sec ingestion.</p>
<p>Kafka: 100 partitions, replication factor 3 → устойчивость и throughput.</p>
<p>Stream Processor: ~20 nodes, state backend для окон (~1GB per node).</p>
<p>ClickHouse: 5 shards × 2 replicas, хранение агрегатов + исторических данных (~1TB/month).</p>
<p>Dashboard: caching top metrics, WebSocket push, batch updates для heavy queries.</p>
<p>Trade-offs:</p>
<p>Строгая консистентность vs low-latency — выбирается exactly-once processing при critical metrics, approximate counts для high-cardinality metrics.</p>
<p>Materialized views в ClickHouse ускоряют queries, но увеличивают storage.</p>
<h3>Этап 7. Расширения и эксплуатационные аспекты</h3>
<p>Ad-hoc queries: поддержка OLAP-запросов поверх ClickHouse.</p>
<p>Multi-tenancy: разные dashboards для разных клиентов.</p>
<p>Alerts &amp; thresholds: real-time anomaly detection на stream level.</p>
<p>Backfill &amp; replay: возможность пересчитать агрегаты при schema change или баге.</p>
<p>High availability: multi-region Kafka + ClickHouse replication.</p>
<p>Monitoring: latency per aggregation, lag monitoring, state store size, dashboard refresh time.</p>
<p>Approximate algorithms: HyperLogLog, Count-Min Sketch для unique counts и heavy hitters.</p>
<h2>API Rate Control / Throttling Gateway</h2>
<p>API gateway, quotas, metering, JWT.</p>
<h3>Этап 1. Постановка задачи и контекст</h3>
<p>Интервьюер объявляет задачу: спроектировать API Gateway, который обеспечивает контроль частоты вызовов API, лимиты использования (quotas), метрики использования и аутентификацию с JWT. Система должна защищать backend-сервисы от перегрузки, обеспечивать fair usage для клиентов, поддерживать различные типы лимитов (per user, per API key, per endpoint), и быть масштабируемой на десятки тысяч запросов в секунду. Нефункциональные требования включают низкую задержку обработки запросов, горизонтальное масштабирование, консистентное применение лимитов, высокую доступность и observability. После постановки контекста интервьюер молчит.</p>
<h3>Этап 2. Формализация требований и уточняющие вопросы</h3>
<p>Кандидат уточняет:</p>
<p>Какие виды лимитов нужны: rate per second/minute/hour, burst limits, quotas per month?</p>
<p>Аутентификация: JWT с payload (userId, plan, scopes) или API keys?</p>
<p>Как обрабатывать превышение лимита: reject (429) или delay (leaky bucket)?</p>
<p>Нужно ли поддерживать глобальные лимиты или только per-user/client?</p>
<p>SLA latency: sub-ms для gateway или допускается ~10ms overhead?</p>
<p>Нужна ли интеграция с billing / plan enforcement?</p>
<p>Ключевые архитектурные характеристики: high throughput, low latency, consistency (в пределах одного bucket), scalability, fault tolerance, observability, idempotency для повторных запросов.</p>
<h3>Этап 3. Границы системы и публичный API</h3>
<p>Границы включают: API Gateway с rate control, бекенд-сервисы, метрики и админ-интерфейс.</p>
<p>Публичный API:</p>
<p>Incoming client API: проксирование запросов с JWT/ключом, rate limiting и quota enforcement.</p>
<p>Admin API: set/update rate limits, view metrics per user/endpoint, reset quotas.</p>
<p>Metrics API: expose usage stats for monitoring (requests/sec, quota usage, blocked requests).</p>
<p>Контракт: каждый request проверяется на rate limit; превышение лимита → 429 Too Many Requests; лимиты применяются консистентно на shard’ах.</p>
<h3>Этап 4. Проектирование: сценарии, потоки данных и компоненты</h3>
<p>Happy path: клиент делает request → API Gateway проверяет JWT → извлекает userId/plan → проверяет текущий rate/quotas → request allowed → proxy to backend → update counters → respond клиенту.</p>
<p>Exceptional flows:</p>
<p>JWT invalid → reject 401.</p>
<p>Quota exceeded → reject 429 + Retry-After header.</p>
<p>Gateway node failure → replication/consistent counters via distributed store.</p>
<p>Burst traffic → token bucket allows burst, leaky bucket smooths traffic.</p>
<p>Компоненты:</p>
<p>API Gateway / Proxy Layer — принимает и валидирует запросы, извлекает JWT, enforces rate limits.</p>
<p>Rate Limiter / Throttling Engine — per-user, per-endpoint, per-plan; реализует token bucket / leaky bucket; хранение state в Redis/etcd/Consul.</p>
<p>Distributed Counter Store — хранит текущее состояние лимитов (Redis cluster, DynamoDB, Cassandra).</p>
<p>Quota Manager / Plan Engine — хранит план пользователя и лимиты, применяет политики.</p>
<p>Metrics &amp; Monitoring — количество запросов, blocked requests, usage per API key, latency.</p>
<p>Admin API — управление лимитами, просмотр usage stats.</p>
<p>Token Bucket vs Leaky Bucket:</p>
<p>Token Bucket: позволяет burst traffic до определённого размера, хорошо для планов с burst allowance.</p>
<p>Leaky Bucket: smooths traffic, предотвращает spikes на backend.</p>
<p>Distributed Limiter: shard по userId/API key, consistent hashing для масштабирования; при multi-node необходимо согласованное хранение токенов и atomic increment.</p>
<h3>Этап 5. Концептуальная схема и целостный обзор</h3>
<p>Система делится на слои:</p>
<p>Client Requests → JWT / API key authentication.</p>
<p>API Gateway → извлекает claims, проверяет лимиты, proxy to backend.</p>
<p>Rate Limiter → token/leaky bucket per user/endpoint/plan.</p>
<p>Counter Store → Redis cluster или DynamoDB, хранение state.</p>
<p>Metrics &amp; Monitoring → Prometheus/Grafana для observability.</p>
<p>Admin Interface → управление лимитами, просмотр usage stats.</p>
<p>Поток данных: Request → Gateway → Rate Limiter → Counter Store → Backend.<br />
Для horizontal scaling shard’ы распределяются по userId/API key, при необходимости используется replicated state для HA.</p>
<h3>Этап 6. Выбор технологий и sizing</h3>
<p>Технологии:</p>
<p>API Gateway: Envoy, NGINX, Kong, or custom Go/Java gateway.</p>
<p>Rate Limiter Storage: Redis Cluster (fast increment, TTL), optionally DynamoDB/Cassandra for distributed consistency.</p>
<p>Metrics: Prometheus + Grafana, ELK stack for logs.</p>
<p>JWT handling: HMAC or RSA validation, claims extraction in gateway.</p>
<p>Sizing:</p>
<p>Target: 100k requests/sec, average burst 10 req/sec/user.</p>
<p>Redis cluster: 20 shards, replication factor 2, ~100k active buckets per node.</p>
<p>Gateway nodes: 10–20 horizontally scalable instances behind load balancer.</p>
<p>Counters TTL: per-second/minute/hour window → ~5–10 MB per shard for active users.</p>
<p>Trade-offs:</p>
<p>Strong consistency vs low latency → for distributed token buckets можно использовать approximate counters or Lua scripts in Redis.</p>
<p>Burst handling → token bucket preferred; smoothing → leaky bucket.</p>
<h3>Этап 7. Расширения и эксплуатационные аспекты</h3>
<p>Dynamic plan updates: менять лимиты без downtime.</p>
<p>Global vs local limits: per-region or multi-region rate limiting.</p>
<p>Failover / HA: replicated counter store, gateway autoscaling.</p>
<p>Analytics: usage patterns, abusive clients, billing integration.</p>
<p>Backpressure: reject or queue requests on backend saturation.</p>
<p>Quota reset &amp; rollover: daily/monthly quotas with TTL.</p>
<p>Security: protect rate limiter store from malicious manipulation, audit logs.</p>
<h2>Design a Social Graph (friends / followers)</h2>
<p>Хранение графов, рекомендации друзей.</p>
<h3>Этап 1. Постановка задачи и контекст</h3>
<p>Интервьюер объявляет задачу: спроектировать Social Graph для платформы с друзьями и подписчиками. Система должна хранить отношения пользователей (friend / follower), обеспечивать быстрый доступ к спискам друзей, mutual friends, followers/following, поддерживать поиск рекомендаций друзей и подписок, а также обеспечивать масштабируемость на миллионы и миллиарды пользователей. Нефункциональные требования включают высокую доступность, низкую задержку чтения списка друзей, горизонтальное масштабирование, быстрые query на рекомендации (friend-of-friend), и возможность batch или realtime обработки graph analytics. После постановки контекста интервьюер молчит.</p>
<h3>Этап 2. Формализация требований и уточняющие вопросы</h3>
<p>Кандидат уточняет:</p>
<p>Тип графа: directed (followers) vs undirected (friends)?</p>
<p>SLA на чтение списков друзей/followers: sub-second?</p>
<p>Частота обновления графа: высоконагруженные изменения (friend request, follow/unfollow)?</p>
<p>Поддержка рекомендаций: friend-of-friend, collaborative filtering, graph embeddings?</p>
<p>Ограничения на размер списка друзей/followers для одного пользователя?</p>
<p>Исторические данные: нужна ли версия графа в прошлых состояниях?</p>
<p>Ключевые характеристики: high throughput, low latency, horizontal scalability, consistency vs eventual consistency trade-offs, fault tolerance, query efficiency.</p>
<h3>Этап 3. Границы системы и публичный API</h3>
<p>Границы системы охватывают storage для графа, query layer для friends/followers, recommendation engine, и админ/metrics сервисы.</p>
<p>Пример API:</p>
<p>POST /users/{id}/follow/{targetId} — подписка на пользователя.</p>
<p>POST /users/{id}/friend/{targetId} — отправка / подтверждение friend request.</p>
<p>GET /users/{id}/friends — список друзей.</p>
<p>GET /users/{id}/followers — список подписчиков.</p>
<p>GET /users/{id}/recommendations — friend suggestions.</p>
<p>Admin API — просмотр статистики, управление rate limits, audit logs.</p>
<p>Контракт: операция friend/follow должна быть идемпотентной; удаление связи или unfollow обновляет graph state корректно.</p>
<h3>Этап 4. Проектирование: сценарии, потоки данных и компоненты</h3>
<p>Happy path: пользователь A отправляет запрос дружбы / подписки → система обновляет graph storage → обновляет индексы → при необходимости recompute recommendations → уведомление пользователя B.</p>
<p>Exceptional flows:</p>
<p>Конфликт friend request → обработка на уровне application layer.</p>
<p>Массовое добавление / удаление → batch update для efficiency.</p>
<p>Недоступность storage → fallback на read replicas с eventual consistency.</p>
<p>Recommendation engine heavy load → precompute embeddings / materialized views.</p>
<p>Основные компоненты:</p>
<p>Graph Storage — хранение связей; варианты: adjacency list (RDBMS, key-value), graph DB (Neo4j, JanusGraph), or wide-column stores (Cassandra/HBase).</p>
<p>Index Layer — быстрый lookup friends/followers; sharding по userId.</p>
<p>Recommendation Engine — friend-of-friend, collaborative filtering, graph embeddings, caching top-k recommendations.</p>
<p>API Layer — REST/GraphQL API с rate limiting и auth.</p>
<p>Notification Service — события friend request, new follower.</p>
<p>Analytics / Batch Processing — периодическое recompute сложных recommendations.</p>
<p>Storage trade-offs:</p>
<p>RDBMS — ACID, но масштабирование и join-heavy queries проблематично.</p>
<p>Graph DB — fast traversal, query-friendly, но сложнее масштабировать.</p>
<p>Wide-column / key-value — легко shard по userId, fast read of adjacency lists, eventual consistency.</p>
<h3>Этап 5. Концептуальная схема и целостный обзор</h3>
<p>Система делится на слои:</p>
<p>API Layer — принимает friend/follow actions, возвращает friend/follower lists, recommendations.</p>
<p>Graph Storage — adjacency list per user (key = userId, value = list of friends/followers).</p>
<p>Index / Cache Layer — Redis/Memcached для hot users, top friends, mutuals.</p>
<p>Recommendation Engine — friend-of-friend traversal, embeddings, collaborative filtering, batch precompute.</p>
<p>Notification Service — push/email alerts.</p>
<p>Analytics Layer — batch recompute for trending recommendations.</p>
<p>Поток данных: Action → Graph Storage → Index update → Recommendation Engine → Cache → API response → Notification.</p>
<h3>Этап 6. Выбор технологий и sizing</h3>
<p>Технологии:</p>
<p>Graph DB: Neo4j, JanusGraph (backend Cassandra/HBase) для traversal-heavy queries.</p>
<p>Wide-column / KV store: Cassandra, DynamoDB для adjacency lists per user.</p>
<p>Cache: Redis/Memcached для hot users, mutual friends, precomputed recommendations.</p>
<p>Batch processing / Analytics: Spark, Flink, Hadoop for graph embeddings, friend-of-friend counts.</p>
<p>API Layer: Go/Java/Python microservices, GraphQL or REST.</p>
<p>Sizing:</p>
<p>100M users, avg 200 friends → 20B edges.</p>
<p>Adjacency lists: 200 friends × 8B users = 1.6B entries, stored in Cassandra (~50–100 GB depending on replication).</p>
<p>Hot cache: top 10 friends per 1M active users → ~10M entries in Redis (~1–2 GB).</p>
<p>Recommendation engine: precompute daily embeddings, serve top-K recommendations from cache.</p>
<p>Trade-offs:</p>
<p>RDBMS: ACID, joins costly at scale.</p>
<p>Graph DB: fast traversal, harder horizontal scaling.</p>
<p>Wide-column + cache: high throughput reads, eventual consistency acceptable for recommendations.</p>
<h3>Этап 7. Расширения и эксплуатационные аспекты</h3>
<p>Friend suggestions: friend-of-friend, collaborative filtering, content-based, embeddings.</p>
<p>Multi-tier caching: hot users, cold users, precomputed recommendations.</p>
<p>Rate limiting: prevent spam friend requests or follow/unfollow abuse.</p>
<p>Monitoring &amp; observability: edge-case queries, latency, hot shards.</p>
<p>High availability: multi-region replication, read replicas, failover.</p>
<p>Graph analytics: trending users, communities, influence scores.</p>
<p>Eventual consistency vs strong consistency: friend/follow writes consistent per shard, recommendation results can be eventually consistent.</p>
<h2>Ride-Sharing System (Uber Lite)</h2>
<p>Matching riders/drivers, геолокации, очереди.</p>
<h3>Этап 1. Постановка задачи и контекст</h3>
<p>Интервьюер объявляет задачу: спроектировать упрощённую версию сервиса ride-sharing. Система должна обеспечивать поиск и сопоставление водителей и пассажиров, учитывать геолокацию, поддерживать очереди запросов на поездку, уведомлять участников, учитывать ETA (Estimated Time of Arrival) и динамическую загрузку водителей. Нефункциональные требования: низкая латентность matching, масштабируемость по количеству водителей и пассажиров, fault tolerance, горизонтальное масштабирование и возможность обработки пиковых нагрузок. После постановки контекста интервьюер молчит.</p>
<h3>Этап 2. Формализация требований и уточняющие вопросы</h3>
<p>Кандидат уточняет:</p>
<p>Какой географический масштаб: город, регион, страна?</p>
<p>Тип matching: nearest driver vs pooled rides?</p>
<p>SLA по latency matching: sub-second или допустимо несколько секунд?</p>
<p>Требования к persistence: хранение поездок, истории, платежей?</p>
<p>Необходимость ETA для пассажира и водителя?</p>
<p>Политика отказа: водитель отклонил запрос, пассажир отменил поездку?</p>
<p>Поддержка surge pricing или динамических тарифов?</p>
<p>Ключевые характеристики: high throughput, low latency, scalability, fault tolerance, real-time geospatial processing, availability, auditability.</p>
<h3>Этап 3. Границы системы и публичный API</h3>
<p>Границы системы включают:</p>
<p>API для пассажиров: request ride, cancel ride, track ride, rate driver.</p>
<p>API для водителей: accept/reject ride, update location, complete ride.</p>
<p>Matching engine и geospatial service.</p>
<p>Storage: trips, users, drivers, payments, location history.</p>
<p>Notification service.</p>
<p>Пример API:</p>
<p>POST /rides/request — пассажир создаёт запрос на поездку (pickup, dropoff).</p>
<p>POST /drivers/{id}/location — водитель обновляет текущее местоположение.</p>
<p>GET /rides/{id}/status — статус поездки.</p>
<p>POST /rides/{id}/cancel — отмена поездки.</p>
<p>Контракт: ride request обрабатывается атомарно; если request отменён, все связанные ресурсы (driver slot, ETA) освобождаются.</p>
<h3>Этап 4. Проектирование: сценарии, потоки данных и компоненты</h3>
<p>Happy path: пассажир делает запрос → геосервис находит ближайших доступных водителей → matching engine выбирает оптимального водителя → уведомление водителя → водитель принимает → поездка подтверждена → мониторинг статуса (ETA, маршрут) → завершение → оплата.</p>
<p>Exceptional flows:</p>
<p>Водитель отклонил → retry с next nearest driver.</p>
<p>Пассажир отменил → free driver slot, notify driver.</p>
<p>Нет доступных водителей → notify passenger, put request in queue.</p>
<p>Гео-сервис недоступен → fallback на last-known location / cached grids.</p>
<p>Основные компоненты:</p>
<p>API Gateway / Request Service — принимает ride requests, validates, idempotency for retries.</p>
<p>Matching Engine — real-time matching of drivers and riders, prioritizes nearest driver, may use weighted scoring (distance, driver rating, ETA).</p>
<p>Geospatial Service — indexes driver locations (grid-based, quadtrees, geohashes), supports nearest-neighbor queries.</p>
<p>Driver &amp; Rider Queues — temporary queues for requests and available drivers.</p>
<p>Trip Management / State Store — tracks ongoing rides, status, ETA, pricing.</p>
<p>Notification Service — push notifications to drivers/pax.</p>
<p>Monitoring &amp; Analytics — rides completed, latency, driver utilization.</p>
<p>Geospatial indexing:</p>
<p>Partition city into grids (e.g., geohash), store active drivers in grid → fast lookup nearest driver.</p>
<p>Optional in-memory caching (Redis / Hazelcast) for hot zones.</p>
<p>Matching algorithm: nearest-driver-first, weighted by ETA, driver rating, dynamic pricing factors.</p>
<h3>Этап 5. Концептуальная схема и целостный обзор</h3>
<p>Система делится на слои:</p>
<p>Client API / Gateway — принимает ride requests, driver location updates.</p>
<p>Geospatial Service — driver location indexing, nearest-neighbor search.</p>
<p>Matching Engine — selects optimal driver, manages queues, retry on rejection.</p>
<p>Trip / State Management — trip lifecycle, status updates, ETA, pricing.</p>
<p>Notification Service — push notifications for request/accept/cancel.</p>
<p>Monitoring / Analytics — utilization, latency, rides per region, surge events.</p>
<p>Поток данных: Ride request → Matching Engine + Geospatial Service → Driver → ETA &amp; route updates → Trip completion → Payment &amp; rating.</p>
<h3>Этап 6. Выбор технологий и sizing</h3>
<p>Технологии:</p>
<p>API Gateway: Envoy / Nginx / Kong.</p>
<p>Geospatial Storage / Indexing: Redis (geo-indexes), Elasticsearch geo queries, PostGIS.</p>
<p>Matching Engine: in-memory processing, scalable microservices, possibly with queue (Kafka) for decoupling requests.</p>
<p>Trip Store: PostgreSQL / MySQL / NoSQL (Cassandra) for horizontal scaling.</p>
<p>Notification: Firebase / APNs / WebSocket.</p>
<p>Monitoring: Prometheus + Grafana, ELK stack for logs.</p>
<p>Sizing:</p>
<p>City-scale: 100k active users, 10k drivers.</p>
<p>Average 1 ride/sec → 3600 rides/hour.</p>
<p>Geohash grids: 1km × 1km, average 10–50 drivers per grid → fast lookup in Redis.</p>
<p>Matching Engine: ~10–20 nodes for parallel processing of ride requests.</p>
<p>Notification: low latency push, autoscaling per demand.</p>
<p>Trade-offs:</p>
<p>Strong consistency (exact driver availability) vs low latency → prefer optimistic matching + compensation if double-book occurs.</p>
<p>ETA calculation: real-time vs cached routing → balance latency and accuracy.</p>
<h3>Этап 7. Расширения и эксплуатационные аспекты</h3>
<p>Pooling / ride-sharing: multiple passengers per ride.</p>
<p>Dynamic pricing / surge: adjust fares based on supply-demand in grid.</p>
<p>Driver rating &amp; scoring: incorporate in matching decisions.</p>
<p>High availability: multi-region deployment for disaster recovery.</p>
<p>Backpressure handling: queue ride requests if system saturated.</p>
<p>Analytics: driver utilization, heatmaps, demand prediction.</p>
<p>Monitoring &amp; alerting: failed matches, stale driver locations, queue backlog.</p>
<h2>Ads Targeting System</h2>
<p>Сегментация пользователей, high-throughput сервисы.</p>
<h3>Этап 1. Постановка задачи и контекст</h3>
<p>Интервьюер объявляет задачу: спроектировать Ads Targeting System, которая позволяет рекламодателям таргетировать пользователей на основе их интересов, поведения и демографических данных. Система должна поддерживать высокую пропускную способность при показе рекламных объявлений, обеспечивать низкую латентность при запросе на показ (real-time bidding), масштабироваться на миллионы пользователей и миллиардные event-потоки. Ключевые нефункциональные требования: high throughput, low latency, горизонтальное масштабирование, fault tolerance, сегментация аудитории, гибкая конфигурация кампаний. После постановки контекста интервьюер молчит.</p>
<h3>Этап 2. Формализация требований и уточняющие вопросы</h3>
<p>Кандидат уточняет:</p>
<p>Тип таргетинга: демографический, поведенческий, look-alike, ретаргетинг?</p>
<p>SLA latency: сколько времени допускается на решение “показать объявление пользователю”?</p>
<p>Event sources: веб, мобильные приложения, CRM данные?</p>
<p>Поддержка real-time bidding (RTB) или batch-показы?</p>
<p>Частота обновления сегментов: real-time, hourly, daily?</p>
<p>Требования к аутентификации и privacy (GDPR/CCPA)?</p>
<p>Метрики эффективности: CTR, conversions, revenue attribution?</p>
<p>Ключевые характеристики: high throughput, low latency, scalability, flexibility, fault tolerance, observability, data privacy compliance.</p>
<h3>Этап 3. Границы системы и публичный API</h3>
<p>Границы системы включают: ingestion событий, сегментацию пользователей, storage сегментов и аудитории, real-time targeting engine, API для рекламодателей и платформы доставки объявлений.</p>
<p>Пример API:</p>
<p>POST /events — отправка пользовательских событий (page_view, click, purchase).</p>
<p>GET /ads?userId={id} — получение релевантного объявления для конкретного пользователя.</p>
<p>POST /campaigns — создание/обновление кампаний, таргетинг и бюджеты.</p>
<p>Admin API — просмотр сегментов, performance metrics, debug tools.</p>
<p>Контракт: каждый user event обрабатывается и доступен для сегментации в допустимые SLA; запрос на показ объявления должен учитывать актуальные сегменты и кампании.</p>
<h3>Этап 4. Проектирование: сценарии, потоки данных и компоненты</h3>
<p>Happy path:</p>
<p>Пользователь совершает действие → event отправляется в ingestion pipeline.</p>
<p>Pipeline агрегирует события → обновляет user profile / segments.</p>
<p>Ads Engine при показе запроса → получает userId → извлекает сегменты → выбирает релевантные кампании → ранжирует объявления → возвращает ad payload в клиентское приложение.</p>
<p>Exceptional flows:</p>
<p>Недоступность сегментационного хранилища → fallback на cached segments.</p>
<p>High load на Ads Engine → throttle или batch decisions.</p>
<p>Event ingestion backlog → eventual consistency для segment updates.</p>
<p>Privacy constraints → filter PII, opt-out users.</p>
<p>Компоненты:</p>
<p>Event Ingestion / Stream — Kafka, Pulsar; partitioned by userId, high throughput.</p>
<p>User Profile &amp; Segment Store — scalable key-value store (Cassandra, DynamoDB, Redis), хранение атрибутов, сегментов.</p>
<p>Real-Time Ads Engine — принимает userId, извлекает сегменты, выбирает объявления, ranks by priority/CTR/price.</p>
<p>Campaign Management Service — CRUD для кампаний, таргетинг правил, budgets.</p>
<p>Analytics / Monitoring — CTR, conversions, campaign effectiveness.</p>
<p>Admin / API Layer — управление, мониторинг, debug.</p>
<p>Сегментация и таргетинг:</p>
<p>Сегменты вычисляются в потоковом режиме (real-time) и batch (hourly/daily).</p>
<p>Для high-cardinality сегментов используется bitmap indexing, inverted indices или Bloom filters.</p>
<p>Ads Engine использует precomputed top-K candidates per segment, cached in-memory.</p>
<h3>Этап 5. Концептуальная схема и целостный обзор</h3>
<p>Система делится на слои:</p>
<p>Event Producers — веб/мобильные события пользователей.</p>
<p>Ingestion Pipeline — Kafka / Pulsar, partitioned by userId.</p>
<p>Stream Processing — Flink/Spark Streaming для real-time segment updates.</p>
<p>Segment / Profile Store — Cassandra/DynamoDB/Redis, хранение актуальных сегментов per user.</p>
<p>Ads Engine — selects relevant ad based on segments and campaign rules; ranks and returns payload.</p>
<p>Campaign Management — CRUD, targeting rules, budgets.</p>
<p>Analytics / Monitoring — event metrics, ad performance dashboards.</p>
<p>Поток данных: Event → Ingestion → Stream Processing → Profile/Segment Store → Ads Engine → Delivery → Analytics.</p>
<h3>Этап 6. Выбор технологий и sizing</h3>
<p>Технологии:</p>
<p>Event Bus: Kafka / Pulsar, high throughput ingestion (млн+ events/sec).</p>
<p>Stream Processing: Flink / Spark Streaming, stateful real-time computation.</p>
<p>Segment / Profile Store: Cassandra / DynamoDB для scalable key-value storage, Redis for hot segments.</p>
<p>Ads Engine: in-memory processing (Go/Java), low-latency retrieval from KV store.</p>
<p>Analytics: ClickHouse / Druid for ad performance metrics.</p>
<p>Sizing:</p>
<p>EPS = 10M events/sec → partitioned across 100+ Kafka partitions.</p>
<p>User base = 100M, average 20 attributes/segments → 2B segment entries, ~100–200 GB storage.</p>
<p>Ads Engine: top-K candidates cached in Redis (~1–2 GB), hundreds of concurrent requests per node.</p>
<p>Stream processors: ~20 nodes for low-latency segment computation.</p>
<p>Trade-offs:</p>
<p>Strong consistency vs latency → eventual consistency на segment updates допустима для non-critical campaigns.</p>
<p>Real-time vs batch → critical campaigns real-time, long-tail campaigns batch.</p>
<h3>Этап 7. Расширения и эксплуатационные аспекты</h3>
<p>Look-alike / ML-based targeting — precompute embeddings, user similarity.</p>
<p>Personalization — dynamic ranking of ads per user/session.</p>
<p>Dynamic budgets &amp; pacing — campaign spend per time unit.</p>
<p>Privacy compliance — GDPR/CCPA opt-out, data anonymization.</p>
<p>High availability &amp; multi-region — replication for ingestion pipeline, segment store.</p>
<p>Monitoring &amp; alerts — ingestion lag, ad delivery latency, CTR metrics.</p>
<p>Fallback / cold-start — default generic ads for users without sufficient profile data.</p>
<h2>Online Code Execution Service (как LeetCode / Judge0)</h2>
<p>Изоляция, sandboxing, распределение задач по воркерам.</p>
<h3>Этап 1. Постановка задачи и контекст</h3>
<p>Интервьюер объявляет задачу: спроектировать сервис, позволяющий пользователям выполнять код в онлайн-режиме на различных языках программирования. Система должна обеспечивать изоляцию исполняемого кода (sandboxing), поддержку параллельного выполнения задач, обработку различных языков и версий, безопасное управление ресурсами (CPU, memory, disk), а также масштабирование под большое количество одновременных запросов. Нефункциональные требования: безопасность (изоляция и предотвращение злоумышленного кода), low latency, fault tolerance, горизонтальное масштабирование и управление очередями заданий. После постановки контекста интервьюер молчит.</p>
<h3>Этап 2. Формализация требований и уточняющие вопросы</h3>
<p>Кандидат уточняет:</p>
<p>Какие языки и версии поддерживать: Python, Java, C++, JS, Go и др.?</p>
<p>SLA latency: среднее время выполнения кода и максимальная задержка?</p>
<p>Ограничения ресурсов на задачу: CPU time, memory, disk, network?</p>
<p>Persistent storage: нужен ли для тестов, логов, или execution ephemeral?</p>
<p>Нужно ли поддерживать batch execution или только interactive submissions?</p>
<p>Поддержка long-running или бесконечного цикла кода → timeout enforcement?</p>
<p>Отслеживание execution metrics: runtime, memory usage, exit status, stderr/stdout?</p>
<p>Ключевые характеристики: security, isolation, scalability, high throughput, low latency, fault tolerance, multi-language support, observability.</p>
<p>Этап 3. Границы системы и публичный API</p>
<p>Границы включают: веб/API layer для приема submissions, execution engine с sandboxing, storage для логов и результатов, очередь заданий, мониторинг и администрацию.</p>
<p>Пример API:</p>
<p>POST /submissions — загрузка кода с параметрами (language, version, stdin, constraints).</p>
<p>GET /submissions/{id}/status — текущий статус выполнения (queued, running, finished, error).</p>
<p>GET /submissions/{id}/result — stdout, stderr, exit code, runtime metrics.</p>
<p>Admin API — управление execution nodes, monitoring, scaling.</p>
<p>Контракт: каждый submission имеет уникальный id; повторный запрос по id идемпотентен; system enforces resource limits and timeouts.</p>
<h3>Этап 4. Проектирование: сценарии, потоки данных и компоненты</h3>
<p>Happy path:</p>
<p>Пользователь отправляет submission → API validates + enqueues in Job Queue.</p>
<p>Worker забирает задачу из очереди → запускает в sandbox (container, VM, Firecracker microVM, chroot).</p>
<p>Код выполняется с лимитами CPU, memory, disk, timeout → stdout/stderr и exit code сохраняются.</p>
<p>Worker сохраняет результат → updates submission status → notify user.</p>
<p>Exceptional flows:</p>
<p>Code hangs → enforce timeout → terminate sandbox → return timeout error.</p>
<p>Code exceeds memory → terminate → return memory exceeded.</p>
<p>Worker node crashes → job requeued → ensure idempotency.</p>
<p>Security violation (network/file access) → sandbox prevents access, log incident.</p>
<p>Компоненты:</p>
<p>API / Submission Service — принимает submissions, validates payload, enqueues jobs.</p>
<p>Job Queue — Kafka/RabbitMQ/Redis Queue, decouples submissions from workers.</p>
<p>Execution Workers / Sandbox Engine — containerized/microVM execution, resource-limited, isolated.</p>
<p>Language Runtime Images — prebuilt docker/microVM images per language/version.</p>
<p>Result Store — persistent storage for stdout, stderr, exit code, runtime metrics.</p>
<p>Monitoring &amp; Metrics — worker health, queue size, execution latency, sandbox violations.</p>
<p>Admin / Scheduler — scale workers, monitor load, manage language images.</p>
<p>Sandboxing approaches:</p>
<p>Containers (Docker) — lightweight, OS-level isolation.</p>
<p>Firecracker microVMs — stronger isolation, lower overhead than full VM.</p>
<p>chroot / seccomp / cgroups — minimal isolation, less secure for untrusted code.</p>
<p>Resource enforcement: cgroups (CPU, memory), timeout, ephemeral filesystem, network disabled.</p>
<h3>Этап 5. Концептуальная схема и целостный обзор</h3>
<p>Система делится на слои:</p>
<p>Client / API Layer — принимает submissions, applies validation and idempotency.</p>
<p>Job Queue — decouples submission ingestion and worker execution; handles retries.</p>
<p>Execution Worker — picks job → launches sandbox → executes code → collects results.</p>
<p>Language Runtime Images — prebuilt environments for supported languages.</p>
<p>Result Storage — persistent logs, stdout/stderr, metrics.</p>
<p>Monitoring &amp; Admin — metrics collection, autoscaling, health checks.</p>
<p>Поток данных: Submission → Queue → Worker → Sandbox Execution → Result Storage → API response → Monitoring.</p>
<h3>Этап 6. Выбор технологий и sizing</h3>
<p>Технологии:</p>
<p>Job Queue: Kafka / RabbitMQ / Redis Streams — durable, partitioned, high throughput.</p>
<p>Execution Workers: Go / Java / Python microservices, horizontally scalable.</p>
<p>Sandboxing: Docker, Firecracker microVMs, cgroups/seccomp for resource isolation.</p>
<p>Result Storage: S3 / MinIO (stdout/stderr), PostgreSQL / DynamoDB (submission metadata).</p>
<p>Monitoring: Prometheus + Grafana, alerting for worker failures or slow jobs.</p>
<p>Sizing:</p>
<p>100k submissions/day, avg execution 1–2s.</p>
<p>Queue throughput ~2–3 submissions/sec on average, peak 500/sec → partitioned queue.</p>
<p>Workers: 50–100 nodes with multiple concurrent sandboxes per node.</p>
<p>Resource limits: 500MB RAM, 2 CPU cores per sandbox.</p>
<p>Storage: stdout/stderr avg 10KB per submission → ~1GB/day.</p>
<p>Trade-offs:</p>
<p>Strong isolation (microVM) vs latency / resource overhead.</p>
<p>Prebuilt language images reduce startup latency.</p>
<p>Queue decouples spikes, ensures retry on worker failure.</p>
<h3>Этап 7. Расширения и эксплуатационные аспекты</h3>
<p>Multi-language support: добавление новых runtimes и версий.</p>
<p>Autoscaling: workers scale horizontally based on queue depth.</p>
<p>Security monitoring: detect malicious submissions, audit logs.</p>
<p>Timeouts and retries: enforce per submission limits, prevent queue starvation.</p>
<p>Caching: for repeated identical submissions (deduplication).</p>
<p>Interactive execution: support stdin streaming for interactive challenges.</p>
<p>Analytics: submission patterns, execution times, popular languages.</p>
<p>Сообщение <a href="https://datatalks.ru/system-design-interview/">Системный дизайн. Интервью по System Design</a> появились сначала на <a href="https://datatalks.ru">DataTalks.RU. Data Engineering / DWH / Data Pipeline</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://datatalks.ru/system-design-interview/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Apache Kafka Tutorial 101: Архитектура, Consumer, Producer, Topic</title>
		<link>https://datatalks.ru/apache-kafka-tutorial-101-consumer-producer-topic/</link>
					<comments>https://datatalks.ru/apache-kafka-tutorial-101-consumer-producer-topic/#respond</comments>
		
		<dc:creator><![CDATA[Data Engineer (Admin)]]></dc:creator>
		<pubDate>Wed, 22 Oct 2025 12:41:04 +0000</pubDate>
				<category><![CDATA[Apache Kafka]]></category>
		<category><![CDATA[commit offset]]></category>
		<category><![CDATA[Consumer]]></category>
		<category><![CDATA[Consumer Group]]></category>
		<category><![CDATA[Kafka]]></category>
		<category><![CDATA[Kafka Connect]]></category>
		<category><![CDATA[Kafka Streams]]></category>
		<category><![CDATA[kafka-tutorial]]></category>
		<category><![CDATA[ksqlDB]]></category>
		<category><![CDATA[Message Queue]]></category>
		<category><![CDATA[offset]]></category>
		<category><![CDATA[Point-to-Point]]></category>
		<category><![CDATA[Producer]]></category>
		<category><![CDATA[Publish-Subscribe]]></category>
		<category><![CDATA[ZooKeeper]]></category>
		<category><![CDATA[Потоковая обработка данных]]></category>
		<guid isPermaLink="false">https://datatalks.ru/?p=942</guid>

					<description><![CDATA[<p>Введение в Apache Kafka Что такое Apache Kafka? Apache Kafka — это распределённая платформа для обработки потоков данных в реальном времени, которая позволяет приложениям публиковать, хранить и обрабатывать данные в режиме потоков событий. Она обеспечивает высокую пропускную способность, масштабируемость и отказоустойчивость, что делает её популярной для создания событийно-ориентированных архитектур, аналитики в реальном времени и управления [&#8230;]</p>
<p>Сообщение <a href="https://datatalks.ru/apache-kafka-tutorial-101-consumer-producer-topic/">Apache Kafka Tutorial 101: Архитектура, Consumer, Producer, Topic</a> появились сначала на <a href="https://datatalks.ru">DataTalks.RU. Data Engineering / DWH / Data Pipeline</a>.</p>
]]></description>
										<content:encoded><![CDATA[<h1>Введение в Apache Kafka</h1>
<h2>Что такое Apache Kafka?</h2>
<p><strong>Apache Kafka</strong> — это распределённая платформа для обработки потоков данных в реальном времени, которая позволяет приложениям публиковать, хранить и обрабатывать данные в режиме потоков событий. Она обеспечивает высокую пропускную способность, масштабируемость и отказоустойчивость, что делает её популярной для создания событийно-ориентированных архитектур, аналитики в реальном времени и управления большими объёмами данных.</p>
<h2>Подборка материалов по Kafka</h2>
<ul>
<li><a href="https://www.youtube.com/watch?v=hbseyn-CfXY" target="_blank" rel="noopener"><strong>YouTube</strong>: Лучший Гайд по Kafka для Начинающих За 1 Час (Vlad Mishustin)</a> &#8212; классный видос по Kafka</li>
<li><a href="https://www.youtube.com/watch?v=r2QiU0o5kWs" target="_blank" rel="noopener"><strong>YouTube</strong>: 5 Применений Kafka в Реальных Приложениях</a> &#8212; дополнительное видео для понимания использования Kafka в приложениях</li>
<li><a href="https://www.youtube.com/watch?v=UNOkvk_fMmM" target="_blank" rel="noopener"><strong>YouTube</strong>: Kafka и RabbitMQ &#8212; БРОКЕРЫ СООБЩЕНИЙ Простым языком на понятном примере</a></li>
<li><a href="https://www.youtube.com/playlist?list=PLt91xr-Pp57Q50WsXz9r-zmxy5ceu_hp_" target="_blank" rel="noopener"><strong>YouTube</strong>: Playlist Apache Kafka</a> &#8212; большой курс по Kafka от JavaGuru</li>
<li><a href="https://www.youtube.com/watch?v=-AZOi3kP9Js" target="_blank" rel="noopener"><strong>YouTube</strong>: Про Kafka (основы)</a></li>
<li><a href="https://www.youtube.com/playlist?list=PL8D2P0ruohOC5FXjzqVRaTsglQ57iKCCe" target="_blank" rel="noopener"><strong>YouTube</strong>: Kafka со Слёрмом</a></li>
<li><a href="https://www.youtube.com/watch?v=R2yGsEfPKS4" target="_blank" rel="noopener"><strong>YouTube</strong>: Kafka в 2025 для дата-инженера: Полный разбор на практике с Python, S3 и ClickHouse</a></li>
<li><strong><a href="https://softwaremill.com/kafka-visualisation/" target="_blank" rel="noopener">Kafka Visualization</a></strong> &#8212; онлайн симулятор работы Kafka, можно сконфигурировать (ограничено) работу Kafka и посмотреть на &#171;конвейер сообщений&#187; между продюсерами и потребителями данных. Статья на <strong>habr</strong> <a href="https://habr.com/ru/articles/865120/" target="_blank" rel="noopener"><strong>&#171;Симулятор брокера Apache Kafka: Kafka Visualization от компании SoftwareMill&#187;</strong></a> про этот симулятор</li>
<li>Статья на <strong>habr</strong> <a href="https://habr.com/ru/companies/piter/articles/352978/" target="_blank" rel="noopener">&#171;Apache Kafka: обзор&#187;</a></li>
<li><a href="https://datatalks.ru/wp-content/uploads/2025/10/shpargalka_Kafka.pdf">Шпаргалка по Kafka.pdf</a></li>
<li><strong>YouTube</strong>: <a href="https://www.youtube.com/watch?v=FGETRVOPdiA" target="_blank" rel="noopener">Типичные ошибки при работе с Apache Kafka — Виктор Корейша (Ozon)</a></li>
<li><strong>YouTube</strong>: <a href="https://www.youtube.com/@HighLoadChannel/search?query=Kafka" target="_blank" rel="noopener">HighLoadChannel &#171;Kafka&#187;</a></li>
<li><strong>YouTube</strong>: <a href="https://www.youtube.com/watch?v=b42gkdta_6s" target="_blank" rel="noopener">Алексей Кашин — Надежно отправляем события в Apache Kafka. От CDC до паттерна Transactional Outbox</a></li>
<li><strong>YouTube</strong>: <a href="https://www.youtube.com/watch?v=V5t_3RxBals" target="_blank" rel="noopener">Apache Kafka: погружение на 45 минут. Григорий Кошелев, Контур, ведущий разработчик</a></li>
</ul>
<h2>Основная логика работы Kafka</h2>
<p><strong>Kafka</strong> — это распределённая система журналов (distributed commit log). Она состоит из нескольких <strong>брокеров</strong> (серверов), которые вместе образуют <strong>кластер</strong>. Кластер хранит <strong>топики</strong>, которые разделены на <strong>партиции</strong> (partitions). Каждая партиция — это упорядоченный, неизменяемый лог событий.</p>
<p><strong>Producer (Продюсер)</strong> соединяется с кластером и узнаёт, на каком брокере хранится нужный топик и его партиции. Для каждой записи продюсер выбирает <strong>партицию</strong> (по ключу, по хешу или случайно). <strong>Событие сериализуется</strong> (обычно Avro, JSON, Protobuf) и отправляется на брокер. Брокер записывает сообщение <strong>в конец партиции</strong> — фактически в лог-файл на диске. Каждому сообщению присваивается <strong>смещение (offset)</strong> — уникальный порядковый номер. Kafka не переписывает данные, а только добавляет новые в конец файла &#8212; поэтому она очень быстрая.</p>
<p><strong>Consumer (Консюмер)</strong> подключается к брокеру через <strong>Consumer Group Coordinator</strong>. Консюмер читает сообщения по порядку <strong>offset-ов</strong> и может сохранять своё текущее <strong>смещение (commit offset)</strong> в Kafka <strong>(__consumer_offsets)</strong>. Таким образом Kafka знает, до какого места консюмер дочитал поток.</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/10/apache_kafka_common_schema_v2.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2448" src="https://datatalks.ru/wp-content/uploads/2025/10/apache_kafka_common_schema_v2.png" alt="" width="933" height="621" srcset="https://datatalks.ru/wp-content/uploads/2025/10/apache_kafka_common_schema_v2.png 933w, https://datatalks.ru/wp-content/uploads/2025/10/apache_kafka_common_schema_v2-300x200.png 300w, https://datatalks.ru/wp-content/uploads/2025/10/apache_kafka_common_schema_v2-768x511.png 768w, https://datatalks.ru/wp-content/uploads/2025/10/apache_kafka_common_schema_v2-450x300.png 450w, https://datatalks.ru/wp-content/uploads/2025/10/apache_kafka_common_schema_v2-780x519.png 780w" sizes="(max-width: 933px) 100vw, 933px" /></a></p>
<p><strong>Kafka</strong> хранит данные на диске — в виде логов, а не в памяти. Каждая партиция — это директория на диске брокера, содержащая:</p><pre class="urvanov-syntax-highlighter-plain-tag">00000000000000000000.log
00000000000000001000.log
...</pre><p>Каждый <strong>.log файл</strong> — это блок записей. Когда достигается <strong>лимит по размеру (segment.bytes)</strong> или <strong>времени (segment.ms)</strong>, создаётся новый сегмент. Kafka не удаляет сообщения сразу после чтения.</p>
<p><strong>Партиция</strong> сохраняется на диске как каталог сегментов. <strong>Сегменты</strong> — это физические фрагменты одной партиции, а не отдельные логические сущности.</p>
<p><strong>Topic -&gt; Log -&gt; Segment:</strong></p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/10/kafka_topic_partition_segment.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2340" src="https://datatalks.ru/wp-content/uploads/2025/10/kafka_topic_partition_segment.png" alt="" width="1502" height="752" srcset="https://datatalks.ru/wp-content/uploads/2025/10/kafka_topic_partition_segment.png 1502w, https://datatalks.ru/wp-content/uploads/2025/10/kafka_topic_partition_segment-300x150.png 300w, https://datatalks.ru/wp-content/uploads/2025/10/kafka_topic_partition_segment-1024x513.png 1024w, https://datatalks.ru/wp-content/uploads/2025/10/kafka_topic_partition_segment-768x385.png 768w, https://datatalks.ru/wp-content/uploads/2025/10/kafka_topic_partition_segment-450x225.png 450w, https://datatalks.ru/wp-content/uploads/2025/10/kafka_topic_partition_segment-780x391.png 780w" sizes="(max-width: 1502px) 100vw, 1502px" /></a></p>
<p>Сообщения хранятся до истечения <strong>retention policy</strong>:</p>
<ul>
<li>по времени (retention.ms),</li>
<li>по размеру (retention.bytes),</li>
<li>или в режиме compaction — сохраняется только последнее сообщение для каждого ключа.</li>
</ul>
<h2>Глоссарий Apache Kafka</h2>
<p><strong>Основные определения Apache Kafka:</strong></p>
<ul>
<li><strong>Apache Kafka</strong> &#8212; Распределённая платформа потоковой передачи событий (<strong>event streaming platform</strong>), предназначенная для обработки и хранения потоков данных в реальном времени. Используется для построения real-time пайплайнов и стриминговых приложений.</li>
<li><strong>Event / Message / Record (Событие / Сообщение / Запись)</strong> &#8212; Представление факта изменения состояния системы — «что-то произошло». Включает ключ, значение, метку времени и необязательные заголовки. События в Kafka неизменяемы (<strong>immutable</strong>).</li>
<li><strong>Topic (Топик)</strong> &#8212; Логическая категория/канал данных, куда продюсеры публикуют события. Топики делятся на <strong>partitions</strong> (разделы) и могут быть реплицированы.</li>
<li><strong>Partition (Раздел)</strong> &#8212; Физическая часть топика, хранящая события в порядке записи. Базовая единица параллелизма Kafka. Каждая партиция — упорядоченная, неизменяемая последовательность событий, которая записывается в лог. События в партиции строго упорядочены и идентифицируются <strong>offset</strong>-ом.</li>
<li><strong>Broker (Брокер)</strong> &#8212; Сервер Kafka, который принимает события от продюсеров (<strong>producers</strong>), хранит их в партициях и выдаёт консюмерам (<strong>consumers</strong>). Обычно Kafka-кластер состоит из нескольких брокеров.</li>
<li><strong>Producer (Продюсер)</strong> &#8212; Приложение (клиент), которое публикует (отправляет) события в Kafka-топики. Продюсер может определять, в какую партицию топика записать сообщение.</li>
<li><strong>Consumer (Консюмер)</strong> &#8212; Приложение (клиент), которое подписывается на один или несколько топиков и обрабатывает поступающие события. Читает сообщения из партиций в порядке их <strong>offset</strong>-ов.</li>
<li><strong>Consumer Group (Группа Потребителей)</strong> &#8212; Набор консюмеров, совместно обрабатывающих события из одного или нескольких топиков. Каждая партиция назначается только одному консюмеру внутри группы → масштабирование без дублирования.</li>
<li><strong>Offset (Смещение)</strong> &#8212; Уникальный порядковый номер события в пределах партиции. Определяет позицию консюмера и обеспечивает контроль обработки сообщений.</li>
<li><strong>Replication (Репликация)</strong> &#8212; Механизм копирования партиций на несколько брокеров. Обеспечить отказоустойчивость и сохранность данных. Replication factor определяет количество копий каждой партиции.</li>
<li><strong>Leader (Лидер)</strong> &#8212; Основная реплика партиции, которая обрабатывает все операции чтения и записи.</li>
<li><strong>Follower (Фолловер)</strong> &#8212; Реплика, синхронизирующаяся с лидером путём копирования его журнала событий. При отказе лидера фолловер может быть выбран новым лидером.</li>
<li><strong>In-Sync Replica (ISR, Синхронная Реплика)</strong> &#8212; Набор реплик (включая лидера), которые полностью синхронизированы с лидером. Обеспечивает надёжную запись — продюсер может ждать подтверждений от всех ISR.</li>
<li><strong>ZooKeeper</strong> &#8212; распределённая система координации, традиционно использовавшаяся Kafka для хранения метаданных, выборов лидера и конфигурации. KRaft (Kafka Raft) &#8212; новый встроенный протокол консенсуса, заменяющий ZooKeeper в современных версиях Kafka.</li>
<li><strong>Kafka Connect</strong> &#8212; Фреймворк для интеграции Kafka с внешними системами (базы, очереди, хранилища и т. д.). <strong>Source Connectors</strong> (загрузка данных в Kafka) и <strong>Sink Connectors</strong> (выгрузка из Kafka).</li>
<li><strong>Kafka Streams</strong> &#8212; Клиентская библиотека, которая позволяет прямо из приложений читать данные из топиков, обрабатывать их (фильтровать, агрегировать, объединять) и писать результат обратно в другие топики. Это “потоковая логика поверх Kafka”.</li>
</ul>
<h2>Пример сценария использования Apache Kafka</h2>
<p>Здесь разберем небольшой пример, чтобы на нем описать основную логику работы системы.</p>
<p>Допустим, у нас есть крупный интернет-магазин, где ежедневно создаются тысячи заказов. Каждый заказ запускает целую цепочку бизнес-процессов: уведомления клиенту, резервирование товара, доставка, обновление аналитики и многое другое.</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/10/apache_kafka_example.jpg"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2302" src="https://datatalks.ru/wp-content/uploads/2025/10/apache_kafka_example.jpg" alt="" width="845" height="167" srcset="https://datatalks.ru/wp-content/uploads/2025/10/apache_kafka_example.jpg 845w, https://datatalks.ru/wp-content/uploads/2025/10/apache_kafka_example-300x59.jpg 300w, https://datatalks.ru/wp-content/uploads/2025/10/apache_kafka_example-768x152.jpg 768w, https://datatalks.ru/wp-content/uploads/2025/10/apache_kafka_example-450x89.jpg 450w, https://datatalks.ru/wp-content/uploads/2025/10/apache_kafka_example-780x154.jpg 780w" sizes="(max-width: 845px) 100vw, 845px" /></a></p>
<p>Представим, что мы хотим построить эту систему так, чтобы она была масштабируемой, отказоустойчивой и позволяла добавлять новые микросервисы без необходимости переписывать старые. Именно здесь на сцену выходит <strong>Apache Kafka</strong> — платформа, которая превращает поток событий в единое, надёжное “сердце” всей e-commerce-инфраструктуры.</p>
<h3>Шаг 1. Создание заказа</h3>
<p>Всё начинается с того, что пользователь оформляет заказ на сайте. На уровне архитектуры это делает сервис, который мы назовём <strong>Order Service</strong>. Он отвечает за валидацию данных, проверку наличия товаров и создание записи в базе данных. Когда заказ успешно создан, сервис публикует событие <code>OrderCreated</code> в <strong>Kafka</strong> — в специальный топик под названием <strong>orders</strong>.</p>
<p><strong>Выглядит это, как простое сообщение в формате JSON, например:</strong></p><pre class="urvanov-syntax-highlighter-plain-tag">{
  "order_id": 12345,
  "customer_email": "user1@ecom.ru",
  "total": 199.90,
  "items": [
    {"sku": "AB-123", "qty": 2},
    {"sku": "CD-456", "qty": 1}
  ],
  "created_at": "2025-10-22T10:30:00Z"
}</pre><p><strong>Продюсер (Order Service)</strong> обращается к брокеру <strong>Kafka</strong> и помещает событие в конец журнала (partition) топика orders. С этого момента <strong>Kafka</strong> становится источником истины: все сервисы, которым нужно знать о новых заказах, будут читать именно этот поток.</p>
<h3>Шаг 2. Сервис уведомлений</h3>
<p>После того, как событие появляется в Kafka, его получает первый потребитель — <strong>Notification Service</strong>. Это отдельное приложение, которое подписано на тот же топик orders, но принадлежит своей consumer group (например, <strong>notification_group</strong>). Kafka гарантирует, что каждый потребитель внутри группы получает уникальный набор партиций — то есть события распределяются равномерно, а не дублируются.</p>
<p>В данном случае сервис уведомлений получает все события, потому что он один в группе.</p>
<p>Получив сообщение <code>OrderCreated</code>, сервис выполняет простое действие: он берёт адрес электронной почты клиента, формирует письмо и отправляет уведомление:</p><pre class="urvanov-syntax-highlighter-plain-tag">“Ваш заказ №12345 успешно создан! Мы приступаем к обработке.”</pre><p>Kafka здесь выступает как надёжный посредник.</p>
<p>Если сервис уведомлений временно недоступен — ничего страшного. Kafka продолжает хранить сообщения в топике orders. Когда сервис вернётся в строй, он просто дочитает поток с того места, где остановился — по сохранённому offset-у. Таким образом, ни одно уведомление не потеряется, даже если в системе временно что-то пошло не так.</p>
<h3>Шаг 3. Сервис логистики</h3>
<p>Параллельно с Notification Service в системе работает другой потребитель — <strong>Logistics Service</strong>. Он также слушает топик orders, но уже со своей consumer group, например <strong>logistics_group</strong>. Это значит, что Kafka отдаёт ему тот же поток событий, что и сервису уведомлений, но независимо. Каждый сервис получает свои копии событий — и каждый может реагировать по-своему.</p>
<p>Когда Logistics Service получает сообщение <strong>OrderCreated</strong>, он делает совсем другие вещи: резервирует товар на складе, создаёт задачу для курьера, обновляет статус заказа в системе доставки. Таким образом, одно событие запускает два (и потенциально десятки) разных бизнес-процессов — и всё это асинхронно и безопасно.</p>
<h3>Потоковая интеграция вместо хрупких связей через API</h3>
<p>Если бы мы строили такую систему без Kafka, то <strong>Order Service</strong> должен был бы сам вызывать API уведомлений, API логистики и, возможно, ещё десяток других сервисов. Такой подход создаёт сильную связанность: ошибка одного из сервисов может замедлить или остановить весь процесс. Kafka решает эту проблему, превращая коммуникацию в поток событий, где каждый сервис просто подписывается на интересующие его данные.</p>
<p>Теперь <strong>Order Service</strong> не знает, кто именно реагирует на событие <strong>OrderCreated</strong>. Может, только логистика. Может, логистика и уведомления. А может, ещё и аналитика, CRM, биллинг — без разницы.</p>
<p>Он просто публикует факт: “Заказ создан.” И любой другой сервис может использовать эту информацию, не нарушая независимость архитектуры.</p>
<h3>Надёжность и гибкость</h3>
<p>Kafka гарантирует, что ни одно событие не потеряется:</p>
<ul>
<li>Все сообщения хранятся на диске и могут быть реплицированы на несколько брокеров.</li>
<li>Каждый потребитель знает, до какого сообщения он дочитал (<strong>offset</strong>).</li>
<li>Можно “перемотать” поток назад и перечитать историю заказов — например, если в сервисе логистики произошла ошибка и нужно пересоздать статусы.</li>
</ul>
<p>Благодаря этому, система становится не просто асинхронной, а воспроизводимой: каждый бизнес-процесс можно “переиграть”, восстановить данные или проанализировать прошлые заказы.</p>
<p><strong>Kafka</strong> — это не просто брокер сообщений, а фундаментальный слой событийной архитектуры, на котором можно построить всё: от аналитики и уведомлений до машинного обучения и мониторинга.</p>
<h2>Партиционирование (Partitioning)</h2>
<p>В контексте Apache Kafka <strong>партиционирование (partitioning)</strong> — это метод разделения топика на более мелкие, независимые сегменты, называемые разделами (partitions). Каждый раздел представляет собой лог, в котором сообщения хранятся в порядке их поступления. Партиционирование позволяет Kafka параллелизировать обработку данных, что даёт возможность нескольким потребителям (consumers) одновременно читать данные из разных разделов.</p>
<hr />
<blockquote><p>Данные в разделе хранятся последовательно (append-only log) на диске. Это позволяет эффективно писать и читать сообщения с высокой пропускной способностью.</p></blockquote>
<hr />
<p>Стратегия партиционирования Apache Kafka направлена на достижение нескольких целей: высокая доступность, устойчивость к сбоям, балансировка нагрузки и масштабируемость.</p>
<p>Kafka разбивает топики на разделы, и каждый раздел является независимой единицей данных, которую можно реплицировать между несколькими брокерами.</p>
<p>Партиционирование — это основа горизонтального масштабирования Kafka (единица масштабирования). При добавлении новых разделов система может обрабатывать больший объём данных и поддерживать более высокую параллельность потребителей (больше разделов &#8212; больше параллелизма потребителей).</p>
<p>Кроме того, сообщения внутри одного раздела всегда сохраняют порядок, что важно для приложений, где требуется строгая последовательность событий (<strong>offsets</strong> управляются для каждой <strong>consumer group</strong>, что даёт отказоустойчивое параллельное потребление).</p>
<hr />
<blockquote><p><strong>ВАЖНО</strong></p>
<p>Consumer group = логическая группа потребителей, которые совместно читают данные из одного или нескольких топиков, деля между собой партиции. Это значит, что каждая партиция топика обрабатывается только одним потребителем внутри группы. То есть, в одной consumer group обычно находятся все экземпляры одного сервиса, которые выполняют одну и ту же задачу. Kafka отслеживает смещения (offsets) отдельно для каждой группы, чтобы гарантировать, что одно сообщение не будет прочитано двумя потребителями из одной группы.</p></blockquote>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/10/apache_kafka_partition_consumer_group.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2332" src="https://datatalks.ru/wp-content/uploads/2025/10/apache_kafka_partition_consumer_group.png" alt="" width="1222" height="611" srcset="https://datatalks.ru/wp-content/uploads/2025/10/apache_kafka_partition_consumer_group.png 1222w, https://datatalks.ru/wp-content/uploads/2025/10/apache_kafka_partition_consumer_group-300x150.png 300w, https://datatalks.ru/wp-content/uploads/2025/10/apache_kafka_partition_consumer_group-1024x512.png 1024w, https://datatalks.ru/wp-content/uploads/2025/10/apache_kafka_partition_consumer_group-768x384.png 768w, https://datatalks.ru/wp-content/uploads/2025/10/apache_kafka_partition_consumer_group-450x225.png 450w, https://datatalks.ru/wp-content/uploads/2025/10/apache_kafka_partition_consumer_group-780x390.png 780w" sizes="(max-width: 1222px) 100vw, 1222px" /></a></p>
<hr />
<p>Тема разбивается на несколько разделов, что позволяет параллелить чтение/запись и распределять нагрузку по брокерам. Каждому разделу присваивается смещение (<strong>offset</strong>) для каждого сообщения.</p>
<p>Разделы могут быть реплицированы, один из реплик становится лидером (<strong>leader</strong>), остальные — <strong>followers</strong>. Это обеспечивает отказоустойчивость и доступность.</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/10/Kafka_Internals.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2316" src="https://datatalks.ru/wp-content/uploads/2025/10/Kafka_Internals.png" alt="" width="2382" height="1067" srcset="https://datatalks.ru/wp-content/uploads/2025/10/Kafka_Internals.png 2382w, https://datatalks.ru/wp-content/uploads/2025/10/Kafka_Internals-300x134.png 300w, https://datatalks.ru/wp-content/uploads/2025/10/Kafka_Internals-1024x459.png 1024w, https://datatalks.ru/wp-content/uploads/2025/10/Kafka_Internals-768x344.png 768w, https://datatalks.ru/wp-content/uploads/2025/10/Kafka_Internals-1536x688.png 1536w, https://datatalks.ru/wp-content/uploads/2025/10/Kafka_Internals-2048x917.png 2048w, https://datatalks.ru/wp-content/uploads/2025/10/Kafka_Internals-450x202.png 450w, https://datatalks.ru/wp-content/uploads/2025/10/Kafka_Internals-780x349.png 780w, https://datatalks.ru/wp-content/uploads/2025/10/Kafka_Internals-1600x717.png 1600w" sizes="(max-width: 2382px) 100vw, 2382px" /></a></p>
<p>Описание схемы (схема демонстрирует базовый принцип устойчивости Kafka: запись — только через лидера, чтение — обычно из лидера, а синхронизация поддерживается через фолловеров):</p>
<ul>
<li><strong>Производитель (Producer)</strong> всегда записывает сообщения в лидера раздела (partition).</li>
<li><strong>Фолловеры (Followers)</strong> автоматически реплицируют данные с лидера, чтобы поддерживать копии лога в актуальном состоянии.</li>
<li><strong>Потребители (Consumers)</strong> из группы потребителей (Consumer Group) обычно читают данные с лидера, чтобы получать подтверждённые сообщения.</li>
</ul>
<p><strong>Внизу показан список ISR (In-Sync Replicas)</strong> — это набор брокеров, чьи копии данных синхронизированы с лидером (в примере ISR = [101, 102, 103]).</p>
<h2>Транзакции и целостность данных</h2>
<p>Kafka поддерживает транзакции, что позволяет атомарно записывать сообщения в несколько разделов/тем и одновременно фиксировать смещения потребителя. Это важно для гарантии <strong>exactly-once</strong> (или ближе к этому) обработки.</p>
<p>При чтении можно выбирать уровень изоляции: <strong>«read_uncommitted» &#8212; читать все подряд</strong> или <strong>«read_committed» &#8212; читать только закоммиченные сообщения</strong> (т.е. получать только завершённые транзакции).</p>
<ul>
<li><strong>Transaction Coordinator</strong> — модуль в брокере Kafka, который управляет транзакциями и отслеживает их состояние.</li>
<li><strong>Transaction Log</strong> — внутренний топик, куда записывается состояние транзакций (например: “начата”, “готова к коммиту”, “завершена”).</li>
<li>Когда продюсер начинает новую транзакцию, он регистрирует свой <code>transactional.id</code> у коорд. После этого он начинает отправлять сообщения обычным образом, но в рамках транзакции. Когда приходит время — либо <code>commitTransaction()</code>, либо <code>abortTransaction()</code>. При коммите Kafka пишет <strong>“маркер”</strong> транзакции в каждую участвующую партицию и фиксирует, что сообщения этой транзакции видимы.</li>
<li>Потребитель, настроенный в режиме <code>isolation.level=read_committed</code>, будет видеть только те записи, которые относятся к <strong>завершённым (committed) транзакциям</strong>, и игнорировать те, что от <strong>незавершённых</strong> или <strong>aborted</strong>.</li>
</ul>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/10/kafka_transactions.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2343" src="https://datatalks.ru/wp-content/uploads/2025/10/kafka_transactions.png" alt="" width="1676" height="1135" srcset="https://datatalks.ru/wp-content/uploads/2025/10/kafka_transactions.png 1676w, https://datatalks.ru/wp-content/uploads/2025/10/kafka_transactions-300x203.png 300w, https://datatalks.ru/wp-content/uploads/2025/10/kafka_transactions-1024x693.png 1024w, https://datatalks.ru/wp-content/uploads/2025/10/kafka_transactions-768x520.png 768w, https://datatalks.ru/wp-content/uploads/2025/10/kafka_transactions-1536x1040.png 1536w, https://datatalks.ru/wp-content/uploads/2025/10/kafka_transactions-370x250.png 370w, https://datatalks.ru/wp-content/uploads/2025/10/kafka_transactions-740x500.png 740w, https://datatalks.ru/wp-content/uploads/2025/10/kafka_transactions-450x305.png 450w, https://datatalks.ru/wp-content/uploads/2025/10/kafka_transactions-780x528.png 780w, https://datatalks.ru/wp-content/uploads/2025/10/kafka_transactions-1600x1084.png 1600w" sizes="(max-width: 1676px) 100vw, 1676px" /></a></p>
<h2>2 модели обмена сообщениями (очередь сообщений и модель публикации-подписки)</h2>
<p><strong>При наличии только одной группы получателей Kafka функционирует как традиционная система очереди сообщений.</strong> Однако, если на тему подписано несколько групп получателей, Kafka ведёт себя как модель публикации/подписки, когда сообщения получают несколько получателей.</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/10/kafka_model.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2326" src="https://datatalks.ru/wp-content/uploads/2025/10/kafka_model.png" alt="" width="643" height="309" srcset="https://datatalks.ru/wp-content/uploads/2025/10/kafka_model.png 643w, https://datatalks.ru/wp-content/uploads/2025/10/kafka_model-300x144.png 300w, https://datatalks.ru/wp-content/uploads/2025/10/kafka_model-450x216.png 450w" sizes="(max-width: 643px) 100vw, 643px" /></a></p>
<p><strong>1. Очередь сообщений (Message Queue / Point-to-Point)</strong> &#8212; каждое сообщение обрабатывается ровно одним потребителем:</p>
<ul>
<li>Сообщения распределяются по разделам (partitions) темы.</li>
<li>Внутри группы потребителей (Consumer Group) Kafka гарантирует, что каждый раздел назначен только одному потребителю.</li>
<li>Таким образом, сообщения из одного раздела не дублируются между потребителями группы.</li>
<li>Это обеспечивает горизонтальное масштабирование обработки — больше потребителей в группе &#8212; больше параллелизма.</li>
</ul>
<p><strong>Пример:</strong></p>
<ul>
<li>Тема orders с 3 разделами.</li>
<li>Группа потребителей order-service из 3 экземпляров.</li>
</ul>
<p>Каждый экземпляр обрабатывает свой раздел &#8212; каждое сообщение читается только один раз в рамках группы.</p>
<p><strong>2. Публикация-подписка (Publish-Subscribe)</strong> &#8212; одно сообщение может быть прочитано множеством независимых потребителей:</p>
<ul>
<li>Разные группы потребителей могут подписываться на одну и ту же тему.</li>
<li>Каждая группа обрабатывает поток независимо от других — <strong>Kafka</strong> хранит смещения (<strong>offsets</strong>) для каждой группы.</li>
<li>Это позволяет нескольким приложениям читать один и тот же поток событий параллельно, не мешая друг другу.</li>
</ul>
<p><strong>Пример: </strong>Тема user-activity.</p>
<p><strong>Группы:</strong></p>
<ul>
<li><strong>analytics-service</strong> (для анализа поведения),</li>
<li><strong>monitoring-service</strong> (для алертов).</li>
</ul>
<p>Обе группы читают одинаковые события, но Kafka ведёт отдельные смещения для каждой.</p>
<h2>Schema Registry (Реестр схем)</h2>
<p>Как только приложения начнут активно отправлять сообщения в Kafka и получать сообщения из него, произойдут два события.</p>
<ul>
<li>Во-первых, появятся новые потребители существующих топиков. Это будут совершенно новые приложения — возможно, написанные той же командой, которая создала исходный продюсер сообщений, а возможно, и другой командой, — и им потребуется понимать формат сообщений в топике.</li>
<li>Во-вторых, формат этих сообщений будет меняться по мере развития бизнеса. Объекты заказов получат новое поле статуса, имена пользователей будут разделены на имя и фамилию вместо полного имени и так далее.</li>
</ul>
<p><strong>Схема наших объектов предметной области</strong> — это постоянно меняющаяся цель, и нам необходимо найти способ согласовать схему сообщений в любом топике.</p>
<p><strong>Schema Registry</strong> предоставляет централизованный репозиторий для управления и проверки схем данных сообщений топиков, а также для сериализации и десериализации данных по сети. Производители и потребители топиков Kafka могут использовать схемы для обеспечения согласованности и совместимости данных по мере развития схем. <strong>Schema Registry</strong> — ключевой компонент управления данными, помогающий обеспечивать качество данных, соответствие стандартам, прозрачность происхождения данных, возможности аудита, совместную работу между командами, эффективные протоколы разработки приложений и производительность системы.</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/10/kafka_schema_registry.jpg"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2346" src="https://datatalks.ru/wp-content/uploads/2025/10/kafka_schema_registry.jpg" alt="" width="651" height="343" srcset="https://datatalks.ru/wp-content/uploads/2025/10/kafka_schema_registry.jpg 651w, https://datatalks.ru/wp-content/uploads/2025/10/kafka_schema_registry-300x158.jpg 300w, https://datatalks.ru/wp-content/uploads/2025/10/kafka_schema_registry-450x237.jpg 450w" sizes="(max-width: 651px) 100vw, 651px" /></a></p>
<p>Schema Registry работает с:</p>
<ul>
<li><strong>Avro</strong> (классика Kafka-мира, бинарный, компактный)</li>
<li><strong>Protobuf</strong> (Google Protocol Buffers)</li>
<li><strong>JSON Schema</strong> (читаемый, но больше размер сообщений)</li>
</ul>
<p>Реестр схем не включен в Kafka, но существует несколько его вариантов с открытым исходным кодом. Например, <strong><a href="https://github.com/confluentinc/schema-registry" target="_blank" rel="noopener">Реестр Confluent Schema</a></strong>.</p>
<h2>Гарантии доставки сообщений в Kafka</h2>
<p><strong>Под семантической гарантией</strong> понимается <strong>соглашение</strong> между продюсером, брокером и потребителем — как именно сообщения передаются и обрабатываются.</p>
<p>Kafka поддерживает три типа семантики доставки:</p>
<ul>
<li><strong>At most once (не более одного раза)</strong> &#8212; Сообщения доставляются один раз, но при сбое системы часть сообщений может быть потеряна и не будет переотправлена. Минимальная задержка, но есть риск потерь.</li>
<li><strong>At least once (как минимум один раз)</strong> &#8212; Сообщения доставляются один или несколько раз. При сбое система гарантирует, что сообщение не потеряется, но возможны дубликаты. Без потерь, но может потребоваться обработка повторов.</li>
<li><strong>Exactly once (ровно один раз)</strong> &#8212; Каждый сообщение доставляется строго один раз. Оно не теряется и не читается повторно, даже если часть системы выходит из строя. Максимальная надёжность, но выше задержка и сложнее настройка.</li>
</ul>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/10/Kafka_Message_Delivery_Guarantees.jpeg"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2353" src="https://datatalks.ru/wp-content/uploads/2025/10/Kafka_Message_Delivery_Guarantees.jpeg" alt="" width="1565" height="939" srcset="https://datatalks.ru/wp-content/uploads/2025/10/Kafka_Message_Delivery_Guarantees.jpeg 1565w, https://datatalks.ru/wp-content/uploads/2025/10/Kafka_Message_Delivery_Guarantees-300x180.jpeg 300w, https://datatalks.ru/wp-content/uploads/2025/10/Kafka_Message_Delivery_Guarantees-1024x614.jpeg 1024w, https://datatalks.ru/wp-content/uploads/2025/10/Kafka_Message_Delivery_Guarantees-768x461.jpeg 768w, https://datatalks.ru/wp-content/uploads/2025/10/Kafka_Message_Delivery_Guarantees-1536x922.jpeg 1536w, https://datatalks.ru/wp-content/uploads/2025/10/Kafka_Message_Delivery_Guarantees-450x270.jpeg 450w, https://datatalks.ru/wp-content/uploads/2025/10/Kafka_Message_Delivery_Guarantees-780x468.jpeg 780w" sizes="(max-width: 1565px) 100vw, 1565px" /></a></p>
<p>Эти три подхода отражают компромисс между задержкой и надёжностью. Выбор зависит от требований вашего приложения.</p>
<p><strong>Важно:</strong> многие системы заявляют о поддержке exactly-once, но на деле они не учитывают сбои компонентов за пределами самой системы (например, внешнего продюсера или потребителя).</p>
<p><strong>Kafka же реализует эти гарантии на уровне журнала (log):</strong> как только сообщение записано и подтверждено, оно считается зафиксированным (<strong>committed</strong>). После этого оно не потеряется, пока хотя бы один брокер с репликой этого раздела остаётся «живым».</p>
<h3><strong>Доставка сообщений от продюсера</strong></h3>
<ul>
<li><strong>At most once</strong> &#8212; Для минимальной задержки продюсер может отправлять сообщения асинхронно (“fire and forget”) — то есть не ожидая подтверждения от брокера. Можно также дождаться подтверждения от ведущего брокера (leader broker), чтобы снизить риск потери, но увеличить задержку.<br />
В обоих случаях сообщения доставляются один раз, а при сбое — могут быть потеряны и не будут переотправлены.</li>
<li><strong>At least once</strong> &#8212; В этом режиме, если продюсер не получил подтверждение, что сообщение было зафиксировано, он переотправит его. Это гарантирует доставку как минимум один раз, но при этом одно и то же сообщение может попасть в лог дважды, если первый запрос всё-таки был успешным.<br />
Режим идемпотентного продюсера (idempotent producer) гарантирует, что повторная отправка не создаст дубликатов, а порядок сообщений в журнале сохранится. Для этого брокер присваивает продюсеру уникальный ID и использует порядковый номер (sequence number) для каждого сообщения, чтобы исключить повторную запись одного и того же события.</li>
<li><strong>Exactly once</strong> &#8212; продюсеры могут использовать транзакционную доставку (transactional delivery). В этом режиме продюсер получает подтверждение, что сообщения приняты и реплицированы, при повторной отправке сообщение записывается идемпотентно — существующие данные перезаписываются, а не дублируются. Это добавляет задержку и требует больших ресурсов, но обеспечивает наивысший уровень надёжности.<br />
Чтобы реализовать транзакционные гарантии “exactly once”, потребители также должны быть соответственно сконфигурированы (например, использовать <code>isolation.level=read_committed</code>).</li>
</ul>
<h3><strong>Получение сообщений потребителем (Consumer Receipt)</strong></h3>
<p>Каждое сообщение в разделе (partition) топика имеет последовательный идентификатор — offset.<br />
Все реплики одного раздела содержат одинаковый журнал логов с теми же offset’ами, а потребитель сам управляет своей позицией в этом логе — то есть знает, с какого offset’а продолжать чтение.<br />
Если потребитель выходит из строя, и его работу должен перенять другой потребитель, тот должен знать, с какого offset’а начать читать.</p>
<ul>
<li><strong>At most once — «не более одного раза»</strong> &#8212; Потребитель читает группу сообщений. Сначала сохраняет своё положение (offset). Затем обрабатывает сообщения. Если потребитель завалится после сохранения offset, но до завершения обработки,<br />
новый потребитель начнёт читать с сохранённого offset’а, и уже прочитанные, но не обработанные сообщения будут потеряны. Это семантика “at most once” — в случае сбоя часть сообщений может не обработаться вообще.</li>
<li><strong>At least once — «как минимум один раз»</strong> &#8212; Потребитель сначала обрабатывает сообщения и только потом сохраняет offset. Если сбой произойдёт между обработкой и сохранением offset, новый потребитель, который возьмёт на себя задачу,<br />
прочитает те же сообщения повторно. Таким образом, некоторые сообщения могут быть обработаны дважды, но ни одно не будет потеряно.<br />
Чтобы избежать проблем от повторной обработки, можно использовать идемпотентную запись — например, задавать каждой записи первичный ключ (primary key), чтобы повторное получение просто перезаписало старую запись без дубликатов.</li>
</ul>
<ul>
<li><strong>Exactly once — «ровно один раз»</strong> &#8212; Когда Kafka используется для чтения из одного топика и записи в другой (например, в приложениях Kafka Streams), Kafka реализует exactly-once семантику с помощью транзакций.<br />
Позиция потребителя (offset) сохраняется в виде сообщения в специальном топике Kafka. Эти данные об offset’ах записываются в одной транзакции вместе с результатами обработки, отправляемыми в выходные топики.<br />
Если транзакция откатывается (aborted), то и offset возвращается к предыдущему значению. Таким образом, система возвращается в полностью согласованное состояние.<strong>Какие сообщения видны потребителям:</strong></p>
<ul>
<li><code>isolation.level=read_uncommitted</code> — потребитель видит все сообщения, включая из незавершённых транзакций.</li>
<li><code>isolation.level=read_committed</code> — потребитель читает только сообщения из завершённых транзакций (используется по умолчанию в режиме exactly-once).</li>
</ul>
</li>
</ul>
<h3><strong>Как работает механизм подтверждений (ACK) в Kafka?</strong></h3>
<p><strong>ack (acknowledgement) в Kafka</strong> — это механизм, который позволяет производителю (producer) получать подтверждение от брокера о том, что сообщение было успешно отправлено и обработано. Этот параметр, настраиваемый в конфигурации продюсера, влияет на баланс между надежностью (риском потери данных) и производительностью.</p>
<p>Kafka предлагает три уровня подтверждений, каждый из которых балансирует между надежностью и скоростью. Рассмотрим каждый из них:</p>
<p><strong>acks=0 (Производитель не ждет подтверждения)</strong></p>
<ul>
<li>Fire-and-forget: отправил и забыл</li>
<li>Молниеносная пропускная способность</li>
<li>Сообщения могут потеряться</li>
<li>Подходит для метрик или логов, где потеря нескольких данных не критична</li>
</ul>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/10/kafka_acks_0.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2365" src="https://datatalks.ru/wp-content/uploads/2025/10/kafka_acks_0.png" alt="" width="1174" height="630" srcset="https://datatalks.ru/wp-content/uploads/2025/10/kafka_acks_0.png 1174w, https://datatalks.ru/wp-content/uploads/2025/10/kafka_acks_0-300x161.png 300w, https://datatalks.ru/wp-content/uploads/2025/10/kafka_acks_0-1024x550.png 1024w, https://datatalks.ru/wp-content/uploads/2025/10/kafka_acks_0-768x412.png 768w, https://datatalks.ru/wp-content/uploads/2025/10/kafka_acks_0-450x241.png 450w, https://datatalks.ru/wp-content/uploads/2025/10/kafka_acks_0-780x419.png 780w" sizes="(max-width: 1174px) 100vw, 1174px" /></a></p>
<p><strong>acks=1 (Продюсер ждет подтверждения от лидера партиции/leader partition)</strong></p>
<ul>
<li>Ожидает подтверждения от лидера</li>
<li>Хорошая скорость при базовой надежности</li>
<li>Есть риск потери сообщений, если лидер выйдет из строя</li>
<li>Подходит для большинства повседневных сценариев использования</li>
</ul>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/10/kafka_acks_1.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2366" src="https://datatalks.ru/wp-content/uploads/2025/10/kafka_acks_1.png" alt="" width="1187" height="726" srcset="https://datatalks.ru/wp-content/uploads/2025/10/kafka_acks_1.png 1187w, https://datatalks.ru/wp-content/uploads/2025/10/kafka_acks_1-300x183.png 300w, https://datatalks.ru/wp-content/uploads/2025/10/kafka_acks_1-1024x626.png 1024w, https://datatalks.ru/wp-content/uploads/2025/10/kafka_acks_1-768x470.png 768w, https://datatalks.ru/wp-content/uploads/2025/10/kafka_acks_1-450x275.png 450w, https://datatalks.ru/wp-content/uploads/2025/10/kafka_acks_1-780x477.png 780w" sizes="(max-width: 1187px) 100vw, 1187px" /></a></p>
<p><strong>acks=-1 или all (Продюсер ждет подтверждения от всех реплик в синхронизации/In-Sync Replicas, ISR)</strong></p>
<ul>
<li>Ожидает подтверждений от всех реплик</li>
<li>Медленнее, но максимально надежно</li>
<li>Максимальная устойчивость</li>
<li>Идеально подходит для финансовых транзакций</li>
</ul>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/10/kafka_acks_all.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2367" src="https://datatalks.ru/wp-content/uploads/2025/10/kafka_acks_all.png" alt="" width="1194" height="822" srcset="https://datatalks.ru/wp-content/uploads/2025/10/kafka_acks_all.png 1194w, https://datatalks.ru/wp-content/uploads/2025/10/kafka_acks_all-300x207.png 300w, https://datatalks.ru/wp-content/uploads/2025/10/kafka_acks_all-1024x705.png 1024w, https://datatalks.ru/wp-content/uploads/2025/10/kafka_acks_all-768x529.png 768w, https://datatalks.ru/wp-content/uploads/2025/10/kafka_acks_all-450x310.png 450w, https://datatalks.ru/wp-content/uploads/2025/10/kafka_acks_all-780x537.png 780w" sizes="(max-width: 1194px) 100vw, 1194px" /></a></p>
<p><strong>Сравнительная таблица настроек ACKs</strong></p>
<table>
<thead>
<tr>
<th><strong>Характеристика</strong></th>
<th><strong>acks=0</strong></th>
<th><strong>acks=1</strong></th>
<th><strong>acks=all</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Надёжность сообщений</strong></td>
<td>Низкая</td>
<td>Средняя</td>
<td>Высокая</td>
</tr>
<tr>
<td><strong>Задержка отклика</strong></td>
<td>Минимальная</td>
<td>Средняя</td>
<td>Наибольшая</td>
</tr>
<tr>
<td><strong>Пропускная способность</strong></td>
<td>Максимальная</td>
<td>Средняя</td>
<td>Минимальная</td>
</tr>
<tr>
<td><strong>Риск потери сообщений</strong></td>
<td>Возможна потеря сообщений</td>
<td>Потеря возможна только при сбое лидера</td>
<td>Сообщения не теряются</td>
</tr>
<tr>
<td><strong>Сценарии использования</strong></td>
<td>Метрики и логированиеМониторинг производительности</td>
<td>Регулярные обновленияПотоки аналитики</td>
<td>ПлатежиКритически важные данные</td>
</tr>
<tr>
<td><strong>Нагрузка на CPU</strong></td>
<td>Минимальная</td>
<td>Средняя</td>
<td>Наибольшая</td>
</tr>
<tr>
<td><strong>Сетевые накладные расходы</strong></td>
<td>Минимальные</td>
<td>Средние</td>
<td>Наибольшие</td>
</tr>
</tbody>
</table>
<h3><strong>Повторные попытки отправки сообщений (Retry Kafka)</strong></h3>
<p><strong>Retry</strong> — это механизм повторных попыток отправки или обработки сообщений в Apache Kafka. Он помогает обеспечить надежность, повторно отправляя сообщения в случае временных сбоев, вместо того, чтобы терять их. Существуют два основных подхода: <strong>блокирующие</strong> (когда обработчик замирает в ожидании) и <strong>неблокирующие</strong> (когда сообщение перенаправляется в отдельный топик, чтобы освободить основной поток).</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/10/kafka_producer_retry_mechanism.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2375" src="https://datatalks.ru/wp-content/uploads/2025/10/kafka_producer_retry_mechanism.png" alt="" width="1741" height="1766" srcset="https://datatalks.ru/wp-content/uploads/2025/10/kafka_producer_retry_mechanism.png 1741w, https://datatalks.ru/wp-content/uploads/2025/10/kafka_producer_retry_mechanism-296x300.png 296w, https://datatalks.ru/wp-content/uploads/2025/10/kafka_producer_retry_mechanism-1010x1024.png 1010w, https://datatalks.ru/wp-content/uploads/2025/10/kafka_producer_retry_mechanism-768x779.png 768w, https://datatalks.ru/wp-content/uploads/2025/10/kafka_producer_retry_mechanism-1514x1536.png 1514w, https://datatalks.ru/wp-content/uploads/2025/10/kafka_producer_retry_mechanism-450x456.png 450w, https://datatalks.ru/wp-content/uploads/2025/10/kafka_producer_retry_mechanism-780x791.png 780w, https://datatalks.ru/wp-content/uploads/2025/10/kafka_producer_retry_mechanism-1600x1623.png 1600w" sizes="(max-width: 1741px) 100vw, 1741px" /></a>Может реализоваться 2 сценария, когда ретраи требуются:</p>
<p><strong>Сбой сети:</strong></p><pre class="urvanov-syntax-highlighter-plain-tag">Отправка сообщения → <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2716.png" alt="✖" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Сбой сети  
↓  
Ожидание 100 мс и попытка снова  
↓  
Успешная повторная попытка <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Сообщение доставлено</pre><p><strong>Сбой в лидере партиции:</strong></p><pre class="urvanov-syntax-highlighter-plain-tag">Отправка сообщения → <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2716.png" alt="✖" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Лидер недоступен  
↓  
Ожидание 100 мс (идёт выбор нового лидера)  
↓  
Повторная отправка <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Новый лидер на линии, сообщение доставлено</pre><p><strong>Ключевые параметры конфигурации retry:</strong></p><pre class="urvanov-syntax-highlighter-plain-tag"># Количество попыток отправки
retries=3                   # Повторить 3 раза

# Интервал между повторными попытками
retry.backoff.ms=100        # Базовый интервал 100 мс

# Общий тайм-аут доставки сообщения
delivery.timeout.ms=120000  # Ожидание до 2 минут

# Включить идемпотентность, чтобы предотвратить дубликаты сообщений при retry
enable.idempotence=true</pre><p></p>
<h2>Анатомия сообщения Kafka</h2>
<p>Сообщение Kafka состоит из следующих элементов:</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/10/kafka_message_structure.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2361" src="https://datatalks.ru/wp-content/uploads/2025/10/kafka_message_structure.png" alt="" width="1212" height="852" srcset="https://datatalks.ru/wp-content/uploads/2025/10/kafka_message_structure.png 1212w, https://datatalks.ru/wp-content/uploads/2025/10/kafka_message_structure-300x211.png 300w, https://datatalks.ru/wp-content/uploads/2025/10/kafka_message_structure-1024x720.png 1024w, https://datatalks.ru/wp-content/uploads/2025/10/kafka_message_structure-768x540.png 768w, https://datatalks.ru/wp-content/uploads/2025/10/kafka_message_structure-450x316.png 450w, https://datatalks.ru/wp-content/uploads/2025/10/kafka_message_structure-780x548.png 780w" sizes="(max-width: 1212px) 100vw, 1212px" /></a></p>
<p>Структура сообщения Kafka:</p>
<ul>
<li><strong>Key (Ключ).</strong> Ключ является необязательным элементом в сообщении <strong>Kafka</strong> и может быть равен null. Ключ может быть строкой, числом или любым объектом, после чего он сериализуется в бинарный формат.</li>
<li><strong>Value (Значение).</strong> Значение представляет содержимое сообщения и также может быть <code>null</code>. Формат значения произвольный и также сериализуется в бинарный формат.</li>
<li><strong>Compression Type (Тип сжатия).</strong> Сообщения Kafka могут быть сжаты. Тип сжатия можно указать как часть сообщения. Доступные варианты: <code>none</code>, <code>gzip</code>, <code>lz4</code>, <code>snappy</code> и <code>zstd</code>.</li>
<li><strong>Headers (Заголовки).</strong> Может быть список необязательных заголовков сообщения Kafka в виде пар ключ-значение. Обычно заголовки добавляют для указания метаданных о сообщении, особенно для трассировки.</li>
<li><strong>Partition + Offset (Раздел + Смещение).</strong> После того как сообщение отправлено в топик Kafka, ему присваиваются номер раздела и идентификатор смещения (<strong>offset</strong>). Комбинация <strong>topic</strong>+<strong>partition</strong>+<strong>offset</strong> уникально идентифицирует сообщение.</li>
<li><strong>Timestamp (Временная метка).</strong> Временная метка добавляется либо пользователем, либо системой в сообщение.</li>
</ul>
<h2>API Kafka</h2>
<p>Apache Kafka предоставляет пять основных API Java для управления кластерами и клиентами.</p>
<ul>
<li><strong>Producer API</strong> &#8212; позволяет приложениям публиковать (записывать) потоки событий в одну или несколько тем Kafka, предоставляя настройки для подтверждений отправки и сжатия сообщений.</li>
<li><strong>Consumer API</strong> &#8212; позволяет приложениям подписываться на темы и читать поток событий, управляя положением чтения (offset) и количеством извлекаемых данных за один цикл.</li>
<li><strong>Admin Client API</strong> &#8212; предоставляет методы для программного управления кластером Kafka — создания, удаления, описания и изменения ресурсов вроде тем, брокеров и ACL.</li>
<li><strong>Connect API</strong> &#8212; служит фреймворком для встраивания источников и приёмников данных: позволяет перемещать потоки событий между Kafka и внешними системами (СУБД, хранилища, приложения).</li>
<li><strong>Kafka Streams API</strong> &#8212; библиотека для построения приложений и микросервисов, которые читают данные из тем Kafka, преобразуют их (фильтрация, агрегация, join) и записывают результаты обратно в темы.</li>
</ul>
<h1>GitHub проект &#171;kafka-tutorial&#187; &#8212; для понимания основ</h1>
<p>Для статьи я создал отдельный <a href="https://github.com/ivanshamaev/kafka-tutorial" target="_blank" rel="noopener"><strong>тестовый проект &#171;kafka-tutorial&#187; на GitHub</strong></a> для локального развертывания <strong>Kafka</strong> и <strong>Zookeeper</strong> с примерами <strong>producer</strong> и <strong>consumer</strong> на <strong>Python</strong>. Предназначен для изучения и отладки взаимодействия с Kafka.</p>
<h2>Краткое описание библиотеки kafka-python</h2>
<p>Как работает обмен сообщениями между продьюсером и консюмером в библиотеке kafka‑python (клиент для Apache Kafka на Python).</p>
<ul>
<li><strong>Продьюсер:</strong> настраиваем, сериализуем, буферизируем, отправляем через TCP к брокеру-лидеру.</li>
<li><strong>Консюмер:</strong> подключаемся, подписываемся/назначаем партиции, выполняем Fetch запросы, десериализуем, возвращаем сообщения.</li>
<li>Всё взаимодействие реализовано через сетевой протокол <strong>Kafka (TCP)</strong>, библиотека сама строит запросы (Metadata, Produce, Fetch и др), парсит ответы.</li>
</ul>
<h3>Отправка сообщения (Producer)</h3>
<p>Создаётся объект:</p><pre class="urvanov-syntax-highlighter-plain-tag">from kafka import KafkaProducer
producer = KafkaProducer(bootstrap_servers='localhost:9092',
                          key_serializer=…,
                          value_serializer=…)</pre><p>Здесь библиотека соединяется с брокерами Kafka, получает метаданные (какие топики, какие партиции, какие брокеры лидеры) через Metadata API.</p>
<p>Когда вызывается <code>producer.send(topic, key=…, value=…)</code> процесс выглядит следующим образом:</p>
<ul>
<li>Сериализуется ключ и/или значение через переданные <code>key_serializer/value_serializer</code>.</li>
<li>Сообщение помещается в буфер (в память) для соответствующей партиции. Продусер внутренне ведёт накопитель (<code>RecordAccumulator</code>) и фоновый поток I/O (<code>Sender thread</code>) которые группируют (<code>batch</code>) сообщения и отправляют партиями.</li>
<li>В зависимости от <strong>настроек</strong> (<code>linger_ms</code>, <code>batch_size</code>) может быть задержка прежде чем буфер будет отправлен, чтобы накопить больше сообщений.</li>
<li>Когда пакет сообщений отправляется, используется <strong>бинарный протокол Kafka</strong> (через <strong>TCP</strong>) — библиотека вручную формирует запрос <code>ProduceRequest</code>, посылает его брокеру-лидеру партиции. (Это скрыто в коде, но логика за этим есть: <code>метаданные → выбор партиции → сетевой запрос</code>)Ответ (<code>ProduceResponse</code>) возвращается, и если <code>acks</code> настроено (например <code>acks='all'</code>), продьюсер может ждать подтверждения (или получить ошибку/ретрай) от брокера.</li>
<li>Вы можете получить <code>future = producer.send(...)</code> и дальше <code>future.get(timeout=…)</code> чтобы ждать результата.</li>
<li>Необходимо вызвать <code>producer.flush()</code> перед завершением, чтобы дождаться отправки всех накопленных сообщений (иначе программа может завершиться раньше и сообщения не будут отправлены).</li>
</ul>
<p><strong>Резюме:</strong> Python-код → <code>KafkaProducer.send()</code> → буфер → сетевой запрос к брокеру Kafka → сообщение попадает в соответствующую тему/партицию.</p>
<h3>Получение сообщения (Consumer)</h3>
<p>Создаётся объект:</p><pre class="urvanov-syntax-highlighter-plain-tag">from kafka import KafkaConsumer
consumer = KafkaConsumer('some_topic',
                         bootstrap_servers='localhost:9092',
                         group_id='my_group',
                         key_deserializer=…,
                         value_deserializer=…,
                         auto_offset_reset='earliest')</pre><p>Библиотека подключается к брокерам, получает метаданные, подписывается на топик(и), либо явно назначает партиции (<strong>assign</strong>) или через <strong>subscribe</strong> получает назначение в составе группы потребителей.</p>
<p>После подписки (или назначения) потребитель выполняет цикл <code>for msg in consumer:</code> или вызывает <code>consumer.poll()</code> и получает сообщения:</p>
<ul>
<li>Он делает <code>FetchRequest</code> к брокерам для каждой назначенной партиции, ожидая данные. Конфигурации вроде <code>fetch_min_bytes</code>, <code>fetch_max_wait_ms</code> влияют на задержку/объём.</li>
<li>Когда данные приходят, они десериализуются с помощью <code>key_deserializer/value_deserializer</code>.</li>
<li>В сообщении вы получаете объект <code>ConsumerRecord</code>, у которого есть <code>topic</code>, <code>partition</code>, <code>offset</code>, <code>key</code>, <code>value</code>.</li>
<li>Если используется группа (<code>group_id</code> задана), то библиотека взаимодействует с координатором группы (<code>GroupCoordinator</code>) для ребалансировки, назначений партиций и фиксации смещений (<code>offsets</code>) либо вручную либо автоматически (если <code>enable_auto_commit=True</code>).</li>
</ul>
<p>Например:</p><pre class="urvanov-syntax-highlighter-plain-tag">for msg in consumer:
    print(msg.topic, msg.partition, msg.offset, msg.key, msg.value)</pre><p><strong>Резюме:</strong> Python-код → KafkaConsumer опрос брокеров → получает сообщения из Kafka → возвращает вам десериализованные объекты.</p>
<h1>Обработка потоков (Stream processing) с Kafka Streams</h1>
<p><strong>Kafka Streams</strong> — это клиентская Java-библиотека, которая читает данные из Kafka топиков (источников), выполняет преобразования, агрегации, join’ы, оконные операции, и записывает результаты обратно в Kafka (или во внешние хранилища). И все это происходит в реальном времени.</p>
<p><strong>Приложение на Kafka Streams</strong> — это просто обычный сервис (JVM-программа), без отдельного «кластера стримов». Kafka сама масштабирует обработку через партиции и consumer groups.</p>
<p>В Data Engineering Kafka Streams применяют для:</p>
<ul>
<li>очистки и обогащения данных «на лету» перед записью в DWH;</li>
<li>real-time агрегаций (например, “средний чек за последние 10 минут”);</li>
<li>вычислений метрик и алертов в реальном времени;</li>
<li>построения CDC пайплайнов (Change Data Capture).</li>
</ul>
<h2>Подборка материалов по Kafka Streams</h2>
<ul>
<li><a href="https://kafka.apache.org/documentation/streams/" target="_blank" rel="noopener">Официальная документация на английском (видео)</a> + небольшая документация на английском <a href="https://kafka.apache.org/41/documentation/streams/core-concepts" target="_blank" rel="noopener">Core Concepts</a></li>
<li>Статья <a href="https://zh-efimenko.github.io/demo-kafka-streams/" target="_blank" rel="noopener">&#171;Введение в Kafka Streams&#187;</a> + GitHub <a href="https://github.com/zh-efimenko/demo-kafka-streams" target="_blank" rel="noopener">demo-kafka-streams</a></li>
<li>Статья Habr: <a href="https://habr.com/ru/articles/747658/" target="_blank" rel="noopener">&#171;Потоковая обработка данных с помощью Kafka Streams: архитектура и ключевые концепции&#187;</a></li>
<li>YouTube: <a href="https://www.youtube.com/watch?v=uaT_AJujPnw" target="_blank" rel="noopener">Kafka Streams (На примере OUTBOX pattern, Kafka Connect, Debezium)</a></li>
<li>YouTube: <a href="https://www.youtube.com/watch?v=3dFr9IgQSik" target="_blank" rel="noopener">Kafka Streams // Демо-занятие курса «Apache Kafka»</a></li>
<li>YouTube: <a href="https://www.youtube.com/watch?v=4pWAgB4wzhU" target="_blank" rel="noopener">Kafka Streams: лекция 1 2022-10-10</a></li>
<li>YouTube: <a href="https://www.youtube.com/watch?v=wJeaFLgVd7Q" target="_blank" rel="noopener">Запись вебинара &#171;Kafka Streams: для чего еще можно использовать Kafka</a></li>
<li>YouTube: <a href="https://www.youtube.com/watch?v=5e1oFijrKEM" target="_blank" rel="noopener">Создание потоковых приложений с использованием Kafka Streams // «Java Developer. Professional»</a></li>
<li>YouTube ENG: PlayList <a href="https://www.youtube.com/playlist?list=PLa7VYi0yPIH35IrbJ7Y0U2YLrR9u4QO-s" target="_blank" rel="noopener">&#171;Kafka Streams Tutorials | Kafka Streams 101 (2023)&#187;</a> &#8212; и еще куча других видео на канале <a href="https://www.youtube.com/@Confluent/playlists" target="_blank" rel="noopener">@Confluent/playlists</a></li>
<li>YouTube ENG: PlayList <a href="https://www.youtube.com/playlist?list=PL7dZNxCsTH8fCJwRiWKyED-CLKDtZl08-" target="_blank" rel="noopener">&#171;Kafka Streams: Zero to Hero&#187;</a></li>
<li>YouTube ENG: <a href="https://www.youtube.com/watch?v=jItIQ-UvFI4" target="_blank" rel="noopener">Build a Reactive Data Streaming App with Python and Apache Kafka | Coding In Motion</a></li>
<li>YouTube: <a href="https://www.youtube.com/watch?v=OWI6nvIMIH4" target="_blank" rel="noopener">python kafka spark streaming</a></li>
<li>Статья <a href="https://wiki.programstore.ru/kafka-streams-java/" target="_blank" rel="noopener">&#171;Kafka Streams для начинающих. Потоковая обработка данных в мире Java&#187;</a></li>
<li>YouTube: <a href="https://www.youtube.com/watch?v=3f9UmLO6HtY" target="_blank" rel="noopener">Мощь Kafka Streams. Когда использовать? | Александр Кузнецов | Синимекс</a></li>
<li>Статья Habr: <a href="https://habr.com/ru/articles/451160/" target="_blank" rel="noopener">Apache Kafka и потоковая обработка данных с помощью Spark Streaming</a></li>
</ul>
<h2>Что такое Kafka Streams: основные абстракции – KStream, KTable, топология</h2>
<p>Kafka Streams — это библиотека потоковой обработки данных, встроенная в Apache Kafka. Она позволяет создавать приложения, которые обрабатывают данные в реальном времени непосредственно из Kafka-топиков и записывают результаты обратно в Kafka или во внешние системы. В отличие от фреймворков, требующих отдельного кластера (например, Apache Flink или Spark Streaming), Kafka Streams работает как часть обычного клиентского приложения и масштабируется за счёт механизма партиционирования Kafka.</p>
<p>Основной моделью данных в Kafka Streams является поток событий — непрерывная последовательность записей (пар «ключ–значение»), поступающих из Kafka-топика. Для работы с такими потоками библиотека предоставляет две ключевые абстракции: KStream и KTable.</p>
<p><strong>KStream</strong> представляет собой неизменяемый поток событий, где каждая запись рассматривается как отдельное событие. Поток можно фильтровать, преобразовывать, объединять с другими потоками, группировать или агрегировать. Каждая новая запись в топике немедленно обрабатывается, что делает KStream подходящей моделью для событийных данных — например, логов, кликов, заказов или транзакций.</p>
<p><strong>KTable</strong>, напротив, представляет собой табличное представление данных, отражающее их текущее состояние (последнее состояние для каждого ключа). Каждое новое сообщение с тем же ключом обновляет существующую запись, а не добавляет новую. Таким образом, <strong>KTable можно воспринимать как материализованное состояние</strong>, построенное из потока событий. Эта абстракция используется для агрегаций, подсчётов, хранения текущего состояния или выполнения операций объединения (join) между потоками и таблицами.</p>
<p>Связующим элементом между этими абстракциями является <strong>топология (Topology)</strong> — направленный граф, описывающий последовательность операций обработки данных.</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/10/streams_architecture_topology.jpg"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2420" src="https://datatalks.ru/wp-content/uploads/2025/10/streams_architecture_topology.jpg" alt="" width="1080" height="1291" srcset="https://datatalks.ru/wp-content/uploads/2025/10/streams_architecture_topology.jpg 1080w, https://datatalks.ru/wp-content/uploads/2025/10/streams_architecture_topology-251x300.jpg 251w, https://datatalks.ru/wp-content/uploads/2025/10/streams_architecture_topology-857x1024.jpg 857w, https://datatalks.ru/wp-content/uploads/2025/10/streams_architecture_topology-768x918.jpg 768w, https://datatalks.ru/wp-content/uploads/2025/10/streams_architecture_topology-450x538.jpg 450w, https://datatalks.ru/wp-content/uploads/2025/10/streams_architecture_topology-780x932.jpg 780w" sizes="(max-width: 1080px) 100vw, 1080px" /></a></p>
<p>Топология определяет, какие потоки данных читаются, какие преобразования применяются, где сохраняется промежуточное состояние и в какие топики отправляются результаты. Каждая вершина топологии соответствует операции (например, фильтрации, группировке или объединению), а каждое ребро представляет поток данных между этими операциями.</p>
<p><strong>Kafka Streams, таким образом, объединяет концепции потоков и таблиц в единую модель обработки событий</strong>, где данные могут рассматриваться как непрерывный поток изменений или как текущее состояние системы. Это делает библиотеку удобным инструментом для построения реактивных, устойчивых и масштабируемых систем обработки данных в реальном времени.</p>
<h2>Почему и когда использовать: трансформации, фильтрации, агрегаты, соединения потоков</h2>
<p><strong>Kafka Streams стоит рассматривать</strong> не просто как инструмент для обработки событий, а <strong>как логическое продолжение самой Kafka</strong> — средство, которое превращает поток данных в поток знаний.</p>
<p>Главный вопрос, который задаёт себе инженер данных, — зачем использовать Kafka Streams, если уже есть Kafka и, возможно, Spark? Ответ в архитектурной философии: <strong>Kafka Streams — это «ближе к данным», чем большинство других систем.</strong> Она позволяет обрабатывать события там же, где они рождаются, без тяжёлой инфраструктуры и внешних движков. Приложение на Kafka Streams становится умным клиентом Kafka, который не просто читает и пишет сообщения, а выполняет вычисления над ними в реальном времени, сохраняя при этом согласованность состояния и способность к восстановлению.</p>
<p>Использовать Kafka Streams стоит там, где нужны реактивные сценарии обработки: например, <strong>при построении витрин для real-time аналитики, обнаружении аномалий, расчёте метрик на лету, обработке транзакций, обновлении состояния пользователей или формировании рекомендаций.</strong> Она особенно эффективна, когда важна низкая задержка, надёжная обработка каждого события и возможность масштабирования через Kafka partitions, а не через сторонние кластеры.</p>
<p><strong>Когда речь идёт о трансформациях, Kafka Streams превращается в конвейер данных.</strong> С помощью операций <code>map</code>, <code>flatMap</code> и <code>selectKey</code> можно изменять структуру, тип и ключ событий, формируя новые потоки. Это позволяет реализовывать бизнес-логику прямо в коде, а не на уровне ETL-инструментов. Каждое преобразование становится узлом в топологии, и поток данных, проходя через них, постепенно приобретает форму, нужную системе <strong>downstream</strong>.</p>
<p>Фильтрация (<code>filter</code>, <code>filterNot</code>) даёт возможность отсекать ненужные события ещё на раннем этапе. Это особенно важно при работе с большими объёмами данных, где стоимость дальнейшей обработки может быть высока. Потоки становятся чище, а вычисления — экономнее.</p>
<p><strong>Агрегации делают Kafka Streams</strong> мощным инструментом для анализа событий во времени. С помощью операций <code>groupByKey</code>, <code>aggregate</code>, <code>reduce</code> и <code>count</code> можно собирать статистику в реальном времени, отслеживать тренды или поддерживать счётчики. Агрегации в Kafka Streams связаны с концепцией окон — временных диапазонов, внутри которых события группируются. Это позволяет, например, считать количество кликов за последние пять минут или среднюю сумму заказов за день. Такие операции сохраняют состояние в <strong>локальном сторе (RocksDB)</strong>, что делает приложение самодостаточным и отказоустойчивым.</p>
<p>Одной из самых интересных возможностей Kafka Streams являются соединения (<code>joins</code>) — механизм, позволяющий объединять данные из разных потоков или таблиц. Сценарии могут быть разными: объединение двух KStream для корреляции событий из разных систем, соединение <strong>KStream</strong> и <strong>KTable</strong> для добавления справочной информации или <code>join</code> <strong>двух KTable</strong> для синхронизации состояний. При этом Kafka Streams обеспечивает согласованность и упорядоченность данных, что особенно критично для финансовых и аналитических систем.</p>
<p>Таким образом, Kafka Streams стоит использовать там, где данные должны не просто перемещаться, а жить — обновляться, объединяться, фильтроваться и агрегироваться в реальном времени. Это библиотека, которая позволяет думать о данных как о непрерывном процессе, а не о статичном снимке. И в этом её сила: она превращает поток событий в логическую модель, которую можно выразить кодом, а не инфраструктурой.</p>
<h2>Как запустить приложение Streams (конфигурация, запуск, топология)</h2>
<p>Когда речь заходит о запуске приложения Kafka Streams, всё начинается с понимания: мы не поднимаем кластер, мы пишем приложение, которое становится частью распределённой системы.</p>
<p><strong>Kafka Streams</strong> — это не сервис, а библиотека, встроенная в ваше Java-приложение. В этом и заключается её философия — обработка данных должна быть как можно ближе к месту, где выполняется бизнес-логика.</p>
<p>Первым шагом в создании любого приложения Streams является конфигурация. Она определяет, как приложение будет взаимодействовать с Kafka и управлять своим состоянием. Ключевые параметры задаются через объект <code>Properties</code>: <code>application.id</code>, <code>bootstrap.servers</code>, <code>default.key.serde</code>, <code>default.value.serde</code> и другие. Параметр <code>application.id</code> служит не просто идентификатором, а точкой согласования состояния: <strong>Kafka Streams</strong> использует его для хранения метаданных, контрольных точек и топологического состояния в специальных служебных топиках. Поэтому выбор <code>application.id</code> должен быть осознанным — он определяет, сможет ли приложение продолжить работу после перезапуска без потери данных.</p>
<p>Следующим этапом идёт построение топологии — логического графа обработки данных. Для этого используется <strong>StreamsBuilder</strong>, объект, через который определяются источники (<strong>stream</strong> и <strong>table</strong>), преобразования (<code>map</code>, <code>filter</code>, <code>join</code>, <code>aggregate</code>) и выходные точки (<code>to</code>). Каждый вызов метода добавляет новый узел в топологию, формируя конвейер обработки. <strong>Эта топология в итоге становится «дорожной картой» данных:</strong> Kafka Streams компилирует её в <strong>набор задач (tasks)</strong>, каждая из которых отвечает за обработку части данных из определённого раздела Kafka. Таким образом, масштабирование достигается естественным образом — за счёт распределения задач по инстансам приложения.</p>
<p>Когда топология определена, создаётся объект <code>KafkaStreams</code>. Именно он связывает логику обработки с реальной инфраструктурой Kafka. Запуск осуществляется вызовом метода <code>start()</code>, после чего приложение начинает читать сообщения из входных топиков, применять трансформации и записывать результаты в выходные топики. Под капотом Kafka Streams автоматически управляет состоянием: создаёт локальные сторы (например, RocksDB), периодически синхронизирует их с changelog-топиками и обрабатывает сбои с помощью механизма восстановления состояния.</p>
<p>Завершение работы должно быть таким же аккуратным, как и запуск. Метод <code>close()</code> позволяет корректно остановить приложение, завершить обработку текущих сообщений и синхронизировать состояние. В продакшене часто используется <strong>shutdown hook</strong>, чтобы при получении сигнала завершения (например, <code>SIGTERM</code>) приложение Streams успевало завершить все операции.</p>
<p><strong>Таким образом, запуск Kafka Streams можно описать как соединение трёх слоёв:</strong> конфигурация, определяющая контекст выполнения; топология, описывающая бизнес-логику обработки; и исполнение, связывающее код с Kafka и обеспечивающее надёжность, масштабирование и отказоустойчивость.</p>
<p><strong>Приложение Streams</strong> — это не просто потребитель и продюсер, а полноценный участник экосистемы Kafka, способный мыслить в терминах потоков, состояний и событий, превращая данные в реальном времени в управляемый и воспроизводимый процесс.</p>
<h2>Состояние и state stores, оконная обработка (windowing), exactly-once семантика</h2>
<p>Когда приложение Kafka Streams начинает работать с состоянием, оно выходит за рамки простой потоковой обработки и превращается в систему, способную помнить контекст. В этом и заключается фундаментальное отличие Kafka Streams от большинства других библиотек — она не просто реагирует на события, а хранит знание о прошлом, делая возможным агрегаты, join-операции и анализ во времени.</p>
<p><strong>Состояние (state)</strong> — это локальные данные, которые приложение поддерживает между событиями. Когда выполняется агрегирование, подсчёт или обновление значения по ключу, Kafka Streams сохраняет промежуточный результат в специальном хранилище, называемом state store. Это может быть встроенная база RocksDB, in-memory store или кастомное решение. Каждый инстанс приложения хранит своё состояние локально, что позволяет ему работать автономно и с минимальной задержкой. Но, несмотря на локальность, надёжность обеспечивается через механизм changelog-топиков: каждое изменение состояния записывается в Kafka, что даёт возможность полностью восстановить state при сбое или перемещении задачи на другой узел.</p>
<p><strong>Kafka Streams делает состояние «живым».</strong> Это не просто кеш, а часть потокового вычисления. Приложение может напрямую обращаться к локальному стору, использовать интерактивные запросы и даже предоставлять доступ к состоянию внешним системам. Такая архитектура позволяет строить event-driven микросервисы, которые не только реагируют на поток данных, но и опираются на накопленные знания.</p>
<p>Другим ключевым элементом является <strong>оконная обработка (windowing).</strong> В потоковом мире данные бесконечны, и чтобы их агрегировать, необходимо ограничить время наблюдения. Kafka Streams вводит окна — логические границы, разделяющие поток событий на временные сегменты. Окна бывают скользящие, фиксированные и сдвигаемые. Например, можно подсчитывать количество покупок за каждые 10 минут или находить среднее значение температуры за последние 5 секунд.</p>
<p>Важный момент — окно не просто отсекает время, оно управляет тем, какие события считаются «совместимыми». Каждое событие имеет временную метку, и Streams использует её для определения, к какому окну оно относится. При этом предусмотрена гибкость: можно задавать допустимые задержки (grace period), чтобы учесть события, пришедшие с опозданием, но всё ещё относящиеся к нужному окну.</p>
<p>Работа с состоянием и окнами невозможна без гарантии корректности обработки. Здесь вступает в силу exactly-once семантика — одна из важнейших возможностей Kafka Streams. Она обеспечивает, что каждое сообщение будет обработано строго один раз, даже в случае сбоев, перезапусков или дублированных сообщений.</p>
<p><strong>Механизм exactly-once</strong> основан на транзакциях Kafka и согласованной записи в changelog-топики. Каждая операция, затрагивающая состояние и производящая выходные сообщения, выполняется в рамках атомарной транзакции. Если что-то идёт не так — транзакция откатывается, и состояние возвращается в согласованное состояние. Это гарантирует, что ни одно событие не будет потеряно и не будет обработано дважды.</p>
<p><strong>Именно комбинация state stores, окон и exactly-once семантики</strong> превращает Kafka Streams в полноценную платформу для построения детерминированных потоковых приложений. Здесь поток не просто обрабатывается — он управляется, обогащается и осмысляется.</p>
<p>Kafka Streams делает возможным создание систем, где каждое событие не просто проходит сквозь поток, а оставляет след — формируя устойчивую, воспроизводимую и надёжную модель данных во времени.</p>
<h2>Что такое ksqlDB?</h2>
<hr />
<p>YouTube: Курс по KsqlDB на английском <a href="https://www.youtube.com/playlist?list=PLa7VYi0yPIH3ulxsOf5g43_QiB-HOg5_Y" target="_blank" rel="noopener">&#171;ksqlDB and Stream Processing Tutorials | ksqlDB 101&#187;</a></p>
<hr />
<p><a href="https://github.com/confluentinc/ksql" target="_blank" rel="noopener"><strong>ksqlDB</strong></a> — это эволюция идей Kafka Streams, превращённая в полноценную потоковую базу данных. Если <strong>Kafka Streams</strong> — это библиотека для разработчиков, то ksqlDB — это инструмент для инженеров и аналитиков, позволяющий описывать потоковую обработку не в коде, а с помощью знакомого SQL. Она создана, чтобы сделать работу с потоками данных такой же естественной, как запросы к реляционным таблицам.</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/10/ksqldb_kafka.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2423" src="https://datatalks.ru/wp-content/uploads/2025/10/ksqldb_kafka.png" alt="" width="1256" height="910" srcset="https://datatalks.ru/wp-content/uploads/2025/10/ksqldb_kafka.png 1256w, https://datatalks.ru/wp-content/uploads/2025/10/ksqldb_kafka-300x217.png 300w, https://datatalks.ru/wp-content/uploads/2025/10/ksqldb_kafka-1024x742.png 1024w, https://datatalks.ru/wp-content/uploads/2025/10/ksqldb_kafka-768x556.png 768w, https://datatalks.ru/wp-content/uploads/2025/10/ksqldb_kafka-450x326.png 450w, https://datatalks.ru/wp-content/uploads/2025/10/ksqldb_kafka-780x565.png 780w" sizes="(max-width: 1256px) 100vw, 1256px" /></a></p>
<p><strong>В своей сути ksqlDB строится поверх Kafka и Kafka Streams.</strong> Каждый запрос, который вы пишете в виде SQL-команды, компилируется в топологию Streams, выполняемую под капотом. Это значит, что вся надёжность, отказоустойчивость и масштабируемость Kafka Streams автоматически становятся частью вашего SQL-приложения.</p>
<p><strong>Главная идея ksqlDB</strong> — рассматривать потоки и таблицы как первоклассных граждан в SQL-мире. <strong>Потоки (STREAM)</strong> представляют собой последовательность событий, где каждое сообщение фиксирует факт: клик, транзакцию, лог, метрику. <strong>Таблицы (TABLE)</strong> отражают текущее состояние, агрегаты или материализованные результаты — именно как KTable в Streams. Взаимодействие между ними естественно: поток можно агрегировать в таблицу, а таблицу можно обновлять событиями из потока.</p>
<p>ksqlDB позволяет делать всё то же, что Kafka Streams, но декларативно: фильтровать данные (<code>WHERE</code>), преобразовывать (<code>SELECT</code>, <code>CAST</code>), агрегировать (<code>GROUP BY</code>, <code>COUNT</code>, <code>SUM</code>), соединять потоки (<code>JOIN</code>) и работать с окнами (<code>WINDOW</code>). При этом каждый запрос становится живым — он не возвращает статичный результат, а формирует непрерывный поток обновлений.</p>
<p>В отличие от традиционных баз данных, где запросы завершаются, в ksqlDB они живут во времени. Создавая поток или таблицу через SQL-запрос, вы фактически запускаете постоянное вычисление, которое обновляется с приходом новых данных в Kafka. Все результаты можно записывать обратно в топики, использовать для downstream-систем или даже делать запросы напрямую через <strong>REST API</strong>.</p>
<p>Кроме того, <strong>ksqlDB включает встроенное хранилище состояния.</strong> Это значит, что вы можете не только выполнять потоковые операции, но и сохранять результаты, а затем делать к ним запросы, словно к обычной базе данных. Таким образом, <strong>ksqlDB</strong> объединяет концепции потоковой обработки и транзакционного состояния в одном инструменте.</p>
<p>С точки зрения архитектуры, <strong>ksqlDB</strong> — это сервис, который подключается к вашему кластеру Kafka, управляет топологиями Streams и поддерживает API для работы с потоками данных. Вы можете запускать его как единый сервер или в распределённом режиме, масштабируя под нагрузку.</p>
<p><strong>ksqlDB</strong> — это шаг вперёд в эволюции Kafka: она делает потоковую обработку доступной не только программистам, но и аналитикам, DevOps-инженерам и архитекторам данных. Это SQL-язык, который разговаривает с событиями, а не с таблицами — язык, в котором время становится таким же измерением данных, как строки и столбцы.</p>
<p>С помощью ksqlDB поток превращается в понятную, управляемую и интерактивную структуру, где события живут, изменяются и взаимодействуют — а данные текут так же естественно, как запросы к ним.</p>
<p>Сообщение <a href="https://datatalks.ru/apache-kafka-tutorial-101-consumer-producer-topic/">Apache Kafka Tutorial 101: Архитектура, Consumer, Producer, Topic</a> появились сначала на <a href="https://datatalks.ru">DataTalks.RU. Data Engineering / DWH / Data Pipeline</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://datatalks.ru/apache-kafka-tutorial-101-consumer-producer-topic/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Построение архитектуры Medallion для данных Bluesky в формате JSON с помощью ClickHouse</title>
		<link>https://datatalks.ru/building-a-medallion-architecture-for-bluesky-json-data-with-clickhouse/</link>
					<comments>https://datatalks.ru/building-a-medallion-architecture-for-bluesky-json-data-with-clickhouse/#respond</comments>
		
		<dc:creator><![CDATA[Data Engineer (Admin)]]></dc:creator>
		<pubDate>Mon, 20 Oct 2025 20:32:50 +0000</pubDate>
				<category><![CDATA[ClickHouse]]></category>
		<category><![CDATA[Bronze layer]]></category>
		<category><![CDATA[ClickPipes]]></category>
		<category><![CDATA[Gold layer]]></category>
		<category><![CDATA[Medallion architecture]]></category>
		<category><![CDATA[Silver layer]]></category>
		<category><![CDATA[sql.clickhouse.com]]></category>
		<category><![CDATA[движок ReplacingMergeTree]]></category>
		<category><![CDATA[движок S3Queue]]></category>
		<guid isPermaLink="false">https://datatalks.ru/?p=2203</guid>

					<description><![CDATA[<p>Ниже — перевод статьи “Building a Medallion architecture for Bluesky JSON data with ClickHouse” с сайта ClickHouse. Построение архитектуры Medallion для данных Bluesky в формате JSON с помощью ClickHouse Мы так же взволнованы, как и вся остальная дата-сообщество, из-за недавнего всплеска популярности социальной сети BlueSky и её API, который позволяет получать доступ к потоку публикуемого [&#8230;]</p>
<p>Сообщение <a href="https://datatalks.ru/building-a-medallion-architecture-for-bluesky-json-data-with-clickhouse/">Построение архитектуры Medallion для данных Bluesky в формате JSON с помощью ClickHouse</a> появились сначала на <a href="https://datatalks.ru">DataTalks.RU. Data Engineering / DWH / Data Pipeline</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p>Ниже — перевод статьи <a href="https://clickhouse.com/blog/building-a-medallion-architecture-for-bluesky-json-data-with-clickhouse" target="_blank" rel="noopener">“Building a Medallion architecture for Bluesky JSON data with ClickHouse”</a> с сайта ClickHouse.</p>
<h1>Построение архитектуры Medallion для данных Bluesky в формате JSON с помощью ClickHouse</h1>
<p>Мы так же взволнованы, как и вся остальная дата-сообщество, из-за недавнего всплеска популярности социальной сети <strong>BlueSky</strong> и её <strong>API</strong>, который позволяет получать доступ к потоку публикуемого контента.</p>
<p>Этот набор данных содержит поток с высокой пропускной способностью — тысячи <strong>JSON-событий</strong> в секунду, и мы подумали, что будет интересно сделать эти данные доступными для сообщества, чтобы каждый мог выполнять по ним запросы.</p>
<p>Во время исследования данных мы обнаружили, что во многих событиях присутствуют некорректные или повреждённые временные метки. Набор данных также содержит частые дубликаты. Поэтому мы не можем просто импортировать данные и на этом закончить — потребуется некоторая очистка.</p>
<p>Это идеальная возможность попробовать <strong>архитектуру Medallion</strong>, о которой мы недавно <a href="https://datatalks.ru/medallion-architecture-with-clickhouse/" target="_blank" rel="noopener">писали в блоге</a>. В этом посте мы оживим эти концепции на практическом примере.</p>
<p>Мы создадим рабочий процесс, который решает эти задачи, <strong>организуя набор данных в три отдельные уровня: бронзовый, серебряный и золотой</strong>. Мы будем придерживаться принципов архитектуры <strong>Medallion</strong> и активно использовать недавно представленный <strong>тип данных JSON</strong>.</p>
<p>Каждый уровень будет доступен для публичных запросов в нашей демо-среде на <a href="https://sql.clickhouse.com/" target="_blank" rel="noopener">sql.clickhouse.com</a>, где читатели смогут самостоятельно изучить и взаимодействовать с результатами. Мы даже подготовили несколько примерных аналитических запросов, чтобы вам было проще начать!</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_01.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2214" src="https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_01.png" alt="" width="2012" height="704" srcset="https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_01.png 2012w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_01-300x105.png 300w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_01-1024x358.png 1024w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_01-768x269.png 768w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_01-1536x537.png 1536w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_01-450x157.png 450w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_01-780x273.png 780w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_01-1600x560.png 1600w" sizes="(max-width: 2012px) 100vw, 2012px" /></a></p>
<h1>Что такое Bluesky?</h1>
<p>Для тех, кто не так активен в социальных сетях, вы могли пропустить недавний взлёт популярности <strong>Bluesky</strong>, которая в настоящее время набирает почти миллион пользователей в день. <strong>Bluesky</strong> — это социальная сеть, похожая на X (бывший Twitter), но, в отличие от него, она полностью открыта и децентрализована!</p>
<p><strong>Bluesky</strong>, построенная на <strong>AT Protocol (ATProto)</strong>, представляет собой децентрализованную платформу социальных сетей, которая позволяет пользователям самостоятельно размещать свой контент. По умолчанию данные хранятся на <strong>Bluesky Personal Data Server (PDS)</strong>, но пользователи могут выбирать — размещать эти серверы (и свой контент) у себя. Такой подход отражает возврат к принципам раннего Интернета, когда пользователи имели контроль над своим контентом и связями, вместо того чтобы зависеть от централизованных платформ, которые доминируют и владеют пользовательскими данными.</p>
<p>Данные каждого пользователя управляются в лёгкой, открытой программной среде, где одна база данных SQLite используется для хранения. Такая структура обеспечивает взаимодействие между системами (interoperability) и гарантирует, что право собственности на контент остаётся за пользователем, даже если центральная платформа выйдет из строя или изменит свою политику.</p>
<p>И самое главное для нас: как и старый Twitter, Bluesky предоставляет бесплатный способ получать события — например, посты — в реальном времени, что открывает потенциально огромный набор данных для аналитики, по мере того как сеть набирает популярность.</p>
<h1>Чтение данных Bluesky</h1>
<p>Чтобы загрузить данные из Bluesky, мы используем недавно выпущенный <strong>Jetstream API</strong>, который упрощает потребление событий Bluesky, предоставляя потоки, закодированные в формате JSON. В отличие от оригинального <strong>firehose</strong>, который требует обработки <strong>бинарных данных CBOR</strong> и <strong>файлов CAR</strong>, Jetstream снижает сложность, делая процесс доступным для разработчиков, работающих с приложениями в реальном времени. Этот API идеально соответствует нашему случаю использования, позволяя фильтровать и обрабатывать тысячи событий в секунду из постов Bluesky, одновременно решая распространённые проблемы, такие как повреждённые данные и высокий уровень дублирования.</p>
<p>В нашей реализации мы подключаемся к публичному экземпляру <strong>Jetstream</strong>, потребляя непрерывный поток событий в формате JSON для загрузки. Для этого используется простой <strong>bash-скрипт</strong>, который обрабатывает <strong>поток JSON-событий</strong> в реальном времени из <strong>Jetstream</strong>.</p>
<p><a href="https://github.com/ClickHouse/sql.clickhouse.com/blob/main/load_scripts/bluesky/ingest.sh" target="_blank" rel="noopener">Ссылка на полный bash-скрипт.</a></p>
<p>Вкратце, он выполняет следующее:</p>
<ul>
<li>Проверяет <strong>GCS bucket</strong> на наличие самого последнего файла <code>.csv.gz</code>, извлекает его временную метку (используемую как курсор) и применяет её для возобновления подписки <strong>Jetstream</strong> с нужной позиции. Это обеспечивает непрерывность данных и минимизирует дублирование.</li>
<li>Инструмент <code><strong>websocat</strong></code> используется для подключения к <strong>Jetstream API</strong>, подписки на события и передачи <strong>JSON-потока</strong> для обработки. Параметр <code>wantedCollections</code> фильтрует нужные события, а <code>cursor</code> обеспечивает пошаговое (инкрементальное) получение данных, например:</li>
</ul>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">websocat -Un --max-messages-rev $MAX_MESSAGES "$WS_URL/subscribe?wantedCollections=app.*&amp;cursor=$cursor" &gt; "$OUTPUT_FILE"</pre><p></p>
<ul>
<li>Входящие данные <strong>JSON</strong> разбиваются на фрагменты по <strong>500 000 строк</strong>, при этом каждый фрагмент представляет собой файл, где последняя временная метка используется в качестве идентификатора файла. Мы используем <code>clickhouse-local</code> для преобразования файла в CSV, затем сжимаем его в <code>.gz</code> и загружаем в <strong>GCS bucket</strong> с помощью <code>gsutil</code>.</li>
<li>Скрипт выполняется внутри <strong>Docker-контейнера</strong> ClickHouse, который запускается каждые 3 минуты с помощью <code>Google Cloud Run Job</code>.</li>
</ul>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_02.jpg"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2215" src="https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_02.jpg" alt="" width="1129" height="748" srcset="https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_02.jpg 1129w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_02-300x199.jpg 300w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_02-1024x678.jpg 1024w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_02-768x509.jpg 768w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_02-450x298.jpg 450w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_02-780x517.jpg 780w" sizes="(max-width: 1129px) 100vw, 1129px" /></a></p>
<p>Обратите внимание, что файлы естественным образом упорядочены по своим именам, основанным на временной метке последнего события. Это критически важно для последующего эффективного инкрементального чтения из <strong>GCS bucket</strong>. Однако скрипт не гарантирует, что будут зафиксированы все события Bluesky.</p>
<h1>Выборка (sampling) данных</h1>
<p>На момент написания этого поста мы зафиксировали почти <strong>1,5 миллиарда событийных строк</strong>, собранных примерно за 21 день. Мы можем использовать функцию <strong>gcs</strong> в <strong>ClickHouse</strong>, чтобы выполнить запрос к данным напрямую и определить общее количество необработанных строк.</p><pre class="urvanov-syntax-highlighter-plain-tag">clickhouse-cloud :) SELECT count()
FROM gcs('https://storage.googleapis.com/pme-internal/bluesky/*.gz', '', '', 'CSVWithNames')

┌────count()─┐
│ 1484500000 │ -- 1.48 billion
└────────────┘

1 row in set. Elapsed: 72.396 sec. Processed 1.48 billion rows, 205.07 GB (20.51 million rows/s., 2.83 GB/s.)
Peak memory usage: 4.85 GiB.</pre><p>Мы можем взять выборку данных, используя ту же функцию, преобразовав каждую строку в тип <strong>JSON</strong> и применив формат <code>PrettyJSONEachRow</code>, чтобы получить читаемый результат.</p><pre class="urvanov-syntax-highlighter-plain-tag">SET allow_experimental_json_type = 1

SELECT data::'JSON' AS event
FROM gcs('https://storage.googleapis.com/pme-internal/bluesky/*.gz', '', '', 'CSVWithNames')
LIMIT 1
FORMAT PrettyJSONEachRow

{
  "account": {
    "active": true,
    "did": "did:plc:kjealuouxn3l6v4byxh2fhff",
    "seq": "706717212",
    "time": "2024-11-27T18:00:02.429Z"
  },
  "did": "did:plc:kjealuouxn3l6v4byxh2fhff",
  "kind": "account",
  "time_us": "1732730402720719"
}

1 row in set. Elapsed: 0.233 sec.</pre><p>Хотя приведённый выше пример даёт некоторое представление о структуре событий, он не полностью отражает сложность, изменчивость и непоследовательность данных. Столбец <code>kind</code> в значительной степени определяет последующую структуру, при этом <strong>API</strong> передаёт три типа событий: <code>commit</code>, <code>identity</code> и <code>account</code>.</p>
<p><strong>Краткое описание типов событий:</strong></p>
<ul>
<li><code>commit</code> — событие фиксации (commit) указывает на создание, обновление или удаление записи. Этот тип представляет большинство событий и включает посты, лайки и подписки.</li>
<li><code>identity</code> — обновление идентичности учётной записи.</li>
<li><code>account</code> — обновление состояния учётной записи.</li>
</ul>
<p>Мы подробнее исследуем эти данные после их загрузки в бронзовый слой (<strong>Bronze layer</strong>).</p>
<h1>Проблемы с данными Bluesky</h1>
<p>Данные Bluesky, как они поступают через JetStream API, имеют ряд проблем, включая следующее:</p>
<ul>
<li><strong>Повреждённый JSON (Malformed JSON)</strong> — время от времени встречаются некорректно сформированные JSON-события. Хотя они редки, такие записи могут нарушить обработку файла. Мы исключаем их с помощью функции <code>isValidJSON</code>, ограничивая загрузку в бронзовый слой (Bronze layer) только теми строками, для которых функция возвращает значение 1.</li>
<li><strong>Непоследовательная структура (Inconsistent structure)</strong> — хотя временная метка сбора данных (поле time_us) присутствует во всех событиях, путь JSON, содержащий время, когда событие произошло, зависит от типа события. Наш рабочий процесс должен извлекать единую согласованную временную метку, основываясь на этих условиях. Простой анализ показывает, что:
<ul>
<li><code>commit.record.createdAt</code> можно использовать для событий типа commit;</li>
<li><code>identity.time</code> — для событий identity;</li>
<li><code>account.time</code> — для событий account.</li>
</ul>
</li>
<li><strong>Будущие или некорректные временные метки (Future or invalid timestamps)</strong> — некоторые события имеют временные метки из будущего. Например, при выборке событий на момент написания поста 42 тысячи commit-событий имели будущие значения времени. Ещё 4 миллиона commit-событий имели метки времени, относящиеся к периоду до запуска Bluesky как сервиса.</li>
<li><strong>Повторяющиеся структуры (Repeated structures)</strong> — встречаются случаи, когда JSON содержит глубоко рекурсивные структуры. Это приводит к появлению более 1800 уникальных JSON-путей, большинство из которых, вероятно, не имеют существенной ценности для анализа содержимого.</li>
<li><strong>Дубликаты (Duplicates)</strong> — несмотря на использование курсора для поддержания последовательности данных, JetStream API создаёт дубликаты (где содержимое идентично, за исключением временной метки сбора). Удивительно, но такие дубликаты могут появляться в широком диапазоне времени — в некоторых случаях с разницей до 24 часов. Важно отметить, что большинство дубликатов встречаются в интервале около 20 минут.</li>
</ul>
<p>Приведённые выше пункты не представляют собой исчерпывающий список проблем с качеством данных — мы продолжаем находить новые сложности! Однако, в целях наглядности и сжатости примера, в нашем демонстрационном <strong>Medallion workflow</strong> мы сосредоточимся именно на перечисленных проблемах.</p>
<h1>Тип данных JSON в ClickHouse</h1>
<p><strong>JSON</strong> играет ключевую роль в реализации архитектуры <strong>Medallion</strong> для данных Bluesky, позволяя системе хранить высокодинамичную и полуструктурированную информацию в бронзовом слое (<strong>Bronze layer</strong>). Новый тип данных JSON в ClickHouse, представленный в версии 24.8, решил ключевые проблемы, с которыми сталкивались предыдущие реализации.</p>
<p>В отличие от традиционных подходов, которые предполагают единственный тип для каждого JSON-пути (что часто приводит к принудительному приведению типов или их преобразованию), <strong>JSON-тип в ClickHouse</strong> хранит значения каждого уникального пути и типа в отдельных подколонках (sub-columns).</p>
<p>Такой подход обеспечивает эффективное хранение, сводит к минимуму лишние операции ввода-вывода (I/O) и избегает затрат на приведение типов во время выполнения запроса.</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_03-scaled.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2216" src="https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_03-scaled.png" alt="" width="2560" height="1065" srcset="https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_03-scaled.png 2560w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_03-300x125.png 300w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_03-1024x426.png 1024w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_03-768x319.png 768w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_03-1536x639.png 1536w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_03-2048x852.png 2048w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_03-450x187.png 450w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_03-780x324.png 780w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_03-1600x665.png 1600w" sizes="(max-width: 2560px) 100vw, 2560px" /></a></p>
<p>Например, когда в таблицу вставляются два JSON-пути с разными типами данных, ClickHouse сохраняет значения каждого типа в отдельных подколонках. Эти подколонки могут быть запрошены независимо, что снижает ненужные операции ввода-вывода.<br />
При этом, если запросить колонку, содержащую несколько типов данных, её значения всё равно возвращаются как единый столбец в ответе.</p>
<p>Кроме того, благодаря использованию смещений (<strong>offsets</strong>), ClickHouse гарантирует, что подколонки остаются плотными (<strong>dense</strong>) — то есть не хранят значения по умолчанию для отсутствующих JSON-путей. Такой подход максимизирует степень сжатия и дополнительно снижает нагрузку на I/O.</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_04-scaled.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2217" src="https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_04-scaled.png" alt="" width="2560" height="1396" srcset="https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_04-scaled.png 2560w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_04-300x164.png 300w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_04-1024x558.png 1024w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_04-768x419.png 768w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_04-1536x838.png 1536w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_04-2048x1117.png 2048w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_04-450x245.png 450w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_04-780x425.png 780w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_04-1600x873.png 1600w" sizes="(max-width: 2560px) 100vw, 2560px" /></a></p>
<p>Также данный тип данных не страдает от проблемы “взрыва подколонок” (sub-column explosion), возникающей при большом количестве уникальных JSON-путей.<br />
Это особенно важно для данных Bluesky, где при отсутствии фильтрации встречается более 1800 уникальных путей.<br />
При этом это не мешает хранению всех этих путей — новые пути просто сохраняются в общей колонке данных, если превышен лимит (при этом статистика ускоряет выполнение запросов).</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_05-scaled.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2219" src="https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_05-scaled.png" alt="" width="2560" height="1346" srcset="https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_05-scaled.png 2560w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_05-300x158.png 300w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_05-1024x539.png 1024w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_05-768x404.png 768w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_05-1536x808.png 1536w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_05-2048x1077.png 2048w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_05-450x237.png 450w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_05-780x410.png 780w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_05-1600x841.png 1600w" sizes="(max-width: 2560px) 100vw, 2560px" /></a></p>
<p>Такое оптимизированное обращение с JSON обеспечивает эффективное хранение сложных, полуструктурированных наборов данных, таких как данные Bluesky, в бронзовом слое архитектуры.<br />
Для пользователей, заинтересованных в технических деталях реализации этого нового типа колонок, рекомендуется ознакомиться с подробным постом в нашем блоге (ссылка предоставлена в оригинале).</p>
<h1>Бронзовый уровень для необработанных данных (Bronze, сырые данные)</h1>
<p>Хотя исходное описание <strong>бронзового слоя (Bronze layer)</strong> не предполагает фильтрацию или преобразование данных, мы относимся к этому менее догматично и считаем, что минимальная фильтрация и недеструктивные преобразования данных могут быть полезны для исследования проблем и возможности воспроизведения данных в будущем.</p>
<p>Для преобразований мы рекомендуем ограничиться теми, которые можно реализовать с помощью <strong>материализованных колонок (Materialized columns)</strong>, как показано ниже в нашей схеме бронзового слоя:</p><pre class="urvanov-syntax-highlighter-plain-tag">CREATE TABLE bluesky.bluesky_raw
(
  `data` JSON(SKIP `commit.record.reply.root.record`, SKIP `commit.record.value.value`),
  `_file` LowCardinality(String),
  `kind` LowCardinality(String) MATERIALIZED getSubcolumn(data, 'kind'),
  `scrape_ts` DateTime64(6) MATERIALIZED fromUnixTimestamp64Micro(CAST(getSubcolumn(data, 'time_us'), 'UInt64')),
  `bluesky_ts` DateTime64(6) MATERIALIZED multiIf(getSubcolumn(data, 'kind') = 'commit', parseDateTime64BestEffortOrZero(CAST(getSubcolumn(data, 'commit.record.createdAt'), 'String')), getSubcolumn(data, 'kind') = 'identity', parseDateTime64BestEffortOrZero(CAST(getSubcolumn(data, 'identity.time'), 'String')), getSubcolumn(data, 'kind') = 'account', parseDateTime64BestEffortOrZero(CAST(getSubcolumn(data, 'account.time'), 'String')), toDateTime64(0, 6)),
  `dedup_hash` String MATERIALIZED cityHash64(arrayFilter(p -&gt; ((p.1) != 'time_us'), JSONExtractKeysAndValues(CAST(data, 'String'), 'String')))
)
ENGINE = ReplacingMergeTree
PRIMARY KEY (kind, bluesky_ts)
ORDER BY (kind, bluesky_ts, dedup_hash)</pre><p>Некоторые важные замечания по этой схеме:</p>
<ul>
<li><strong>Тип JSON</strong> — колонка data использует новый тип данных JSON и содержит всё событие целиком.<br />
Мы применяем оператор <strong>SKIP</strong>, чтобы исключить определённые пути JSON, которые, как показал анализ, были ответственны за повторяющиеся структуры, отмеченные ранее.</li>
<li><strong>Сохранение метаданных</strong> — колонка <code>_file</code> содержит ссылку на файл, из которого была загружена строка.</li>
<li><strong>Материализованные колонки (Materialized columns)</strong> — остальные колонки являются материализованными и вычисляются из колонки data во время вставки:
<ul>
<li><code>scrape_ts</code> — время, когда событие было доставлено; извлекается из поля JSON time_us.</li>
<li><code>kind</code> — тип события, как упоминалось ранее.</li>
<li><code>bluesky_ts</code> — выполняет условную логику, извлекая временную метку события на основе значения kind; это решает проблему непоследовательной структуры и обеспечивает единый формат временных меток для всех событий.</li>
<li><code>dedup_hash</code> — содержит хеш события.</li>
</ul>
</li>
<li>Для вычисления хеша создаётся массив всех <strong>JSON-путей</strong> и их значений, за исключением <code>time_us</code> (так как это поле отличается у дубликатов), с помощью функции <code>JSONExtractKeysAndValues</code>.<br />
Затем функция <code>cityHash64</code> обрабатывает этот массив, создавая уникальный хеш события.</li>
<li><strong>ReplacingMergeTree</strong> — используется движок <code>ReplacingMergeTree</code>, который позволяет устранять дубликаты записей, имеющих одинаковые значения ключей сортировки (<strong>ORDER BY</strong>).<br />
<strong>Дедупликация выполняется асинхронно</strong> во время фоновых слияний, которые происходят в неопределённое время и не могут быть напрямую контролируемы — то есть дедупликация осуществляется постепенно (<code>eventual deduplication</code>).</li>
</ul>
<p>В нашей схеме ключ <strong>ORDER BY</strong> включает <code>kind</code> и <code>bluesky_ts</code>, что:</p>
<ul>
<li>обеспечивает эффективное чтение;</li>
<li>гарантирует высокую степень сжатия, группируя строки с похожими атрибутами.</li>
</ul>
<p>Мы также добавляем <code>dedup_hash</code>, чтобы уникально идентифицировать строки для дедупликации, но не включаем его в <strong>PRIMARY KEY</strong>.<br />
Это оптимизация, которая предотвращает загрузку индекса по <code>dedup_hash</code> в память — разумное решение, так как мы не выполняем прямые запросы по хешу.</p>
<p>Наш бронзовый слой выполняет минимальные преобразования данных с помощью материализованных колонок, но при этом обеспечивает возможность дедупликации.<br />
Важно отметить, что использование <strong>ReplacingMergeTree</strong> здесь не является обязательным и не влияет на будущие слои.</p>
<p>Пользователи могут предпочесть обычный <strong>MergeTree</strong>, если хотят анализировать дубликаты напрямую.</p>
<p>Наш выбор обусловлен главным образом желанием минимизировать объём хранимых данных.</p>
<h2>Загрузка данных из объектного хранилища (s3, object storage)</h2>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_06.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2221" src="https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_06.png" alt="" width="977" height="616" srcset="https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_06.png 977w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_06-300x189.png 300w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_06-768x484.png 768w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_06-450x284.png 450w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_06-780x492.png 780w" sizes="(max-width: 977px) 100vw, 977px" /></a></p>
<p>Как описано выше, наш конвейер загрузки данных (<strong>ingestion pipeline</strong>) использует инструмент <strong>websocat</strong> для потоковой передачи данных из <strong>JetStream API</strong>, сохраняя события в виде файлов <code>.csv.gz</code> в <strong>Google Cloud Storage (GCS)</strong>.</p>
<p>Этот промежуточный шаг предоставляет несколько преимуществ:</p>
<ul>
<li>он позволяет воспроизводить данные (<strong>data replay</strong>),</li>
<li>сохраняет оригинальную копию необработанных данных (<strong>raw data</strong>)</li>
<li>и имитирует подход, который многие пользователи используют для загрузки данных из объектного хранилища.</li>
</ul>
<p>Чтобы считать эти файлы из <strong>GCS</strong> в нашу таблицу бронзового слоя <strong>bluesky_raw</strong>, мы используем движок таблицы <strong>S3Queue (S3Queue table engine)</strong>. Этот движок считывает данные из объектного хранилища, совместимого с <strong>S3</strong>, автоматически обрабатывает новые файлы по мере их добавления в бакет и вставляет их в указанную таблицу через <strong>материализованное представление (materialized view)</strong>.</p>
<p>Создание этой таблицы требует небольшой <strong>DDL-команды</strong>:</p><pre class="urvanov-syntax-highlighter-plain-tag">CREATE TABLE bluesky.bluesky_queue
(
  `data` Nullable(String)
)
ENGINE = S3Queue('https://storage.googleapis.com/pme-internal/bluesky/*.gz', '', '', 'CSVWithNames')
SETTINGS mode = 'ordered', s3queue_buckets = 30, s3queue_processing_threads_num = 10;</pre><p>Обратите внимание, что мы указываем <strong>GCS-бакет</strong>, содержащий <strong>сжатые (gzipped) файлы</strong>,<br />
и определяем каждую строку как тип String с помощью объявления схемы.</p>
<p>Важно, что мы включаем <strong>&#171;ordered mode&#187;</strong> через настройку <code>mode = 'ordered'</code>. Это заставляет файлы обрабатываться в лексикографическом порядке, обеспечивая последовательную загрузку данных.</p>
<p>Хотя это означает, что файлы, добавленные с более ранним порядком сортировки, игнорируются, такая конфигурация поддерживает эффективную и инкрементальную обработку, и устраняет необходимость выполнять масштабные операции сравнения множеств, если файлы не имеют естественного порядка.</p>
<p>Наше раннее использование временных меток (<strong>timestamps</strong>) для имен файлов гарантирует, что данные обрабатываются в правильной последовательности, а движок <strong>S3Queue</strong> быстро распознаёт новые файлы, которые нужно загрузить.</p>
<p>Наша среда <a href="https://sql.clickhouse.com/" target="_blank" rel="noopener">sql.clickhouse.com</a>, в которую мы загружаем данные, состоит из трёх узлов, каждый из которых имеет 60 виртуальных процессорных ядер (<strong>vCPUs</strong>).</p>
<p>Параметр <code>s3queue_processing_threads_num</code> задаёт количество потоков для обработки файлов на каждом сервере.</p>
<p>Кроме того, при использовании ordered mode вводится дополнительная настройка &#8212; <code>s3queue_buckets</code>. Как рекомендуется, мы устанавливаем её как произведение количества реплик (3) на количество потоков обработки (10).</p>
<p>Чтобы потреблять строки из этой очереди, необходимо присоединить <strong>инкрементальное материализованное представление (Incremental Materialized View)</strong>. Это представление читает данные из очереди, выполняет SELECT-запрос над строками, а результат отправляется в таблицу бронзового слоя <strong>bluesky_raw</strong>.</p><pre class="urvanov-syntax-highlighter-plain-tag">CREATE MATERIALIZED VIEW bluesky.bluesky_mv TO bluesky.bluesky_raw
(
  `data` Nullable(String)
)
AS SELECT
  data,
  _file
FROM bluesky.bluesky_queue
WHERE isValidJSON(data) = 1</pre><p>Обратите внимание, что мы выполняем базовую фильтрацию уже на этом уровне:<br />
в таблицу бронзового слоя передаются только строки, где <code>isValidJSON(data) = 1</code>, то есть содержащие валидный <strong>JSON</strong>.</p>
<p>Также мы добавляем метаданные — колонку <code>_file</code>, чтобы иметь запись о том, из какого <strong>gzip-файла</strong> была загружена каждая строка.</p>
<h2>Потоковая передача Bluesky напрямую в ClickHouse (Streaming)</h2>
<p>Обратите внимание, что <strong>ClickHouse</strong> <strong>может напрямую выполнять потоковую загрузку данных</strong> с использованием <strong>JSON-форматов</strong> ввода, как недавно продемонстрировал наш технический директор (CTO) Алексей Миловидов.</p>
<p>Это можно реализовать, объединив тип данных JSON и формат ввода JSON.</p>
<p>Например:</p><pre class="urvanov-syntax-highlighter-plain-tag">websocat -n "wss://jetstream1.us-east.bsky.network/subscribe?wantedCollections=app.*" | pv -l | split -l 1000 --filter='clickhouse-client --host sql-clickhouse.clickhouse.com --secure --password "" --query "INSERT INTO bluesky.bluesky_raw (data) FORMAT JSONAsObject"'</pre><p></p>
<h2>ClickPipes в ClickHouse Cloud</h2>
<p>Хотя механизм таблиц <strong>S3Queue</strong> позволяет нам выполнять потоковую передачу данных из объектного хранилища в ClickHouse, у него есть определённые ограничения. Помимо того, что он поддерживает только <strong>S3-совместимые хранилища</strong>, он обеспечивает <strong>семантику “по крайней мере один раз” (at-least-once)</strong>.</p>
<p>Пользователи ClickHouse Cloud могут предпочесть использовать <strong>ClickPipes</strong> — управляемое решение для загрузки данных, которое обеспечивает семантику <strong>“ровно один раз” (exactly-once)</strong>, поддерживает больше источников (например, <strong>Kafka</strong>) и разделяет ресурсы загрузки и ресурсы кластера.</p>
<p>Эта технология может быть использована для замены <strong>S3Queue</strong> в описанной выше архитектуре с минимальной настройкой через пошаговый мастер (<strong>guided wizard</strong>).</p>
<h2>Запросы к бронзовому уровню</h2>
<p>Хотя мы не рекомендуем предоставлять доступ к вашей таблице уровня <strong>Bronze</strong> конечным пользователям (<strong>downstream consumers</strong>), выбранный нами ключ сортировки (<strong>ordering key</strong>) позволяет эффективно исследовать данные, выявлять дополнительные проблемы с их качеством или, при необходимости, повторно воспроизводить данные через последующие уровни архитектуры.</p>
<p>Мы отмечали, что во время слияния (merge) <strong>движок ReplacingMergeTree</strong> определяет дубликаты строк, используя значения столбцов, указанных в <strong>ORDER BY</strong>, как уникальный идентификатор, и сохраняет только последнюю версию записи. Однако это обеспечивает лишь постепенную (<strong>eventual</strong>) корректность — то есть не гарантирует, что все дубликаты будут удалены, поэтому полагаться на это не стоит.</p>
<p>Чтобы гарантировать корректные результаты, пользователям необходимо дополнять <strong>фоновое объединение (background merges)</strong> операцией удаления дубликатов во время выполнения запроса, что можно сделать с помощью оператора <strong>FINAL</strong>.<br />
Однако это создаёт дополнительную нагрузку на ресурсы и негативно влияет на производительность запросов, что является ещё одной причиной, по которой мы не советуем предоставлять доступ к Bronze-таблицам потребителям данных.</p>
<p>В приведённых выше примерах запросов мы опускаем оператор <strong>FINAL</strong>, принимая небольшой уровень дублирования, поскольку это допустимо для разведочного анализа данных.</p>
<p>Большинство данных представляют собой <strong>commit-события (commit events)</strong>:</p><pre class="urvanov-syntax-highlighter-plain-tag">SELECT kind, formatReadableQuantity(count()) AS c
FROM bluesky_raw
GROUP BY kind
FORMAT PrettyCompactMonoBlock
┌─kind─────┬─c──────────────┐
│ commit   │ 614.55 million │
│ account  │ 1.72 million   │
│ identity │ 1.70 million   │
└──────────┴────────────────┘

3 rows in set. Elapsed: 0.124 sec. Processed 617.97 million rows, 617.97 MB (5.00 billion rows/s., 5.00 GB/s.)
Peak memory usage: 139.03 MiB.</pre><p>Внутри этих <strong>commit-событий</strong> можно исследовать типы событий с помощью синтаксиса пути <strong>JSON (JSON path syntax)</strong>:</p><pre class="urvanov-syntax-highlighter-plain-tag">SELECT
  data.commit.collection AS collection,
  count() AS c,
  uniq(data.did) AS users
FROM bluesky_raw
WHERE kind = 'commit'
GROUP BY ALL
ORDER BY c DESC
LIMIT 10
FORMAT PrettyCompactMonoBlock

┌─collection───────────────┬─────────c─┬───users─┐
│ app.bsky.feed.like       │ 705468149 │ 7106516 │
│ app.bsky.graph.follow    │ 406406091 │ 8629730 │
│ app.bsky.feed.post       │ 137946245 │ 4323265 │
│ app.bsky.feed.repost     │  90847077 │ 2811398 │
│ app.bsky.graph.block     │  25277808 │ 1523621 │
│ app.bsky.graph.listitem  │   8464006 │  166002 │
│ app.bsky.actor.profile   │   8168943 │ 4083558 │
│ app.bsky.graph.listblock │  643292   │  216695 │
│ app.bsky.feed.threadgate │  559504   │   94202 │
│ app.bsky.feed.postgate   │  275675   │   38790 │
└──────────────────────────┴───────────┴─────────┘

10 rows in set. Elapsed: 19.923 sec. Processed 1.38 billion rows, 122.00 GB (69.50 million rows/s., 6.12 GB/s.)
Peak memory usage: 1003.91 MiB.</pre><p>Мы видим, что основная часть событий — это <strong>“лайки”</strong> и <strong>“подписки (follows)”</strong>, что вполне ожидаемо.</p>
<h1>Серебряный уровень для очищенных данных (Silver Layer)</h1>
<p><strong>Слой Silver (Серебряный)</strong> представляет собой следующий этап в архитектуре <strong>Medallion</strong>, преобразуя сырые данные из слоя <strong>Bronze (Бронзового)</strong> в более согласованную и структурированную форму.</p>
<p><strong>Этот слой решает проблемы качества данных:</strong> выполняет дополнительную фильтрацию, стандартизирует схемы, производит преобразования и обеспечивает полное удаление дубликатов.<br />
В ClickHouse обычно наблюдается прямая связь между таблицами Bronze и их эквивалентами в Silver.</p>
<p>Мы знаем, что дубликаты событий имеют одинаковые значения <code>bluesky_ts</code> (и других столбцов), различаясь лишь по <code>scrape_ts</code>, причём последнее значение может быть значительно позже.<br />
Однако ранее мы установили, что большинство дубликатов появляются в пределах 20 минут.<br />
Чтобы гарантировать, что в <strong>золотой слой (Gold)</strong> не попадут дубликаты, мы вводим понятие конечного <strong>окна дедупликации (finite duplication window)</strong> в слое <strong>Silver</strong>.</p>
<p>События будут распределяться по этим окнам дедупликации, которые смещены относительно текущего времени на основе значения <code>bluesky_ts</code>.<br />
Эти «окна» периодически сбрасываются (<strong>flushed</strong>) в слой Gold, с гарантией, что в каждое окно попадёт только одна копия события.</p>
<p><strong>Использование окон дедупликации избавляет нас от необходимости проводить дедупликацию за бесконечный период времени, что существенно снижает нагрузку на систему и делает задачу более управляемой.</strong></p>
<p>Как мы покажем далее, это можно эффективно реализовать в ClickHouse.</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_07.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2227" src="https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_07.png" alt="" width="998" height="711" srcset="https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_07.png 998w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_07-300x214.png 300w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_07-768x547.png 768w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_07-450x321.png 450w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_07-780x556.png 780w" sizes="(max-width: 998px) 100vw, 998px" /></a></p>
<p>Назначение событий в окна дедупликации, которые синхронизируются с реальным временем и периодически сбрасываются, предполагает, что данные доставляются без значительных задержек.</p>
<p><strong>Анализ таблицы Bronze показывает, что:</strong></p>
<ul>
<li>90% событий имеют значение bluesky_ts, отличающееся от времени их поступления (извлечённого из имени файла в GCS) не более чем на 20 минут.<br />
Это возможно, если:</li>
<li>Обработка 1 миллиона сообщений за один раз не вызывает значительных задержек;</li>
<li>Время чтения и обработки через S3Queue также незначительно (это можно проверить через системные таблицы);</li>
<li>Время, извлечённое из имени файла, близко к реальному времени загрузки, что подтверждается запросами к GCS.</li>
</ul>
<p>Кроме того, более 94% событий имеют разницу между <code>scrape_ts</code> и <code>bluesky_ts</code> меньше 20 минут (в 90% случаев — даже менее 10 секунд).<br />
Это означает, что значение <code>scrape_ts</code> также не отстаёт от времени поступления данных.</p>
<p>Понимая, что события обычно доставляются в течение 20 минут после их <code>bluesky_ts</code>, мы можем надёжно формировать окна дедупликации в слое Silver.<br />
Для этого мы создаём <strong>раздел (partition)</strong> в ClickHouse для каждого 20-минутного интервала — таким образом, раздел фактически соответствует одному окну.</p>
<p>События распределяются по разделам в зависимости от того, в какой интервал они попадают, с помощью функции:</p><pre class="urvanov-syntax-highlighter-plain-tag">toStartOfInterval(bluesky_ts, toIntervalMinute(20))</pre><p><strong>Итоговая схема таблицы Silver выглядит следующим образом:</strong></p>
<ul>
<li>Мы используем ReplacingMergeTree, но выполняем дедупликацию только внутри каждого раздела, то есть слияние происходит только в пределах окна.</li>
<li>Для управления объёмом данных применяется TTL, который удаляет строки, старше 1440 секунд (24 часа).</li>
</ul>
<p>Параметр <code>ttl_only_drop_parts = 1</code> гарантирует, что части удаляются только тогда, когда все строки в них устарели.</p><pre class="urvanov-syntax-highlighter-plain-tag">CREATE TABLE bluesky.bluesky_dedup
(
  `data` JSON(SKIP `commit.record.reply.root.record`, SKIP `commit.record.value.value`),
  `kind` LowCardinality(String),
  `scrape_ts` DateTime64(6),
  `bluesky_ts` DateTime64(6),
  `dedup_hash` String
)
ENGINE = ReplacingMergeTree
PARTITION BY toStartOfInterval(bluesky_ts, toIntervalMinute(20))
ORDER BY dedup_hash
TTL toStartOfMinute(bluesky_ts) + toIntervalMinute(1440) SETTINGS ttl_only_drop_parts=1</pre><p>Так как слишком большое количество разделов может привести к проблемам производительности и ошибкам вроде <strong>“Too many parts”</strong>, мы ограничиваем таблицу Silver только одними сутками данных (всего 72 окна по 20 минут). Старые данные автоматически удаляются с помощью правил TTL, сохраняя эффективность и стабильность системы.</p>
<h2>Инкрементные материализованные представления для фильтрации</h2>
<p>При применении фильтрации и правил дедупликации к данным <strong>уровня Bronze</strong>, пользователи часто сохраняют «негативные совпадения» (то есть записи, не прошедшие фильтры) в отдельной таблице — так называемой <strong>Dead-Letter таблице</strong> — для последующего анализа.</p>
<p>Так как мы планируем периодически отправлять свежие партиции из слоя Silver в слой Gold, нам нежелательно, чтобы события поступали слишком поздно.<br />
По этой причине, а также чтобы продемонстрировать <strong>принцип “dead letter queue”</strong>, мы будем отправлять все события из слоя Bronze, у которых разница между <code>scrape_ts</code> и <code>bluesky_ts</code> превышает 20 минут, в <strong>очередь “dead letter”</strong>.</p>
<p>События же, у которых задержка меньше 20 минут, будут вставляться в соответствующую партицию таблицы Silver, показанную ранее.</p>
<p>Для реализации этого подхода мы используем две <strong>инкрементные материализованные представления (incremental materialized views)</strong>.<br />
Каждое из них выполняет SELECT-запрос к строкам, вставленным в таблицу уровня Bronze (<strong>bluesky_raw</strong>), и отправляет результаты либо:</p>
<ul>
<li>в таблицу <strong>dead letter queue</strong>,</li>
<li>либо в таблицу Silver (<strong>bluesky_dedup</strong>).</li>
</ul>
<p>Основное различие между этими двумя представлениями заключается в их фильтрующих условиях.</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_08.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2229" src="https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_08.png" alt="" width="981" height="637" srcset="https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_08.png 981w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_08-300x195.png 300w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_08-768x499.png 768w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_08-450x292.png 450w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_08-780x506.png 780w" sizes="(max-width: 981px) 100vw, 981px" /></a></p>
<p>Представление для отправки строк в <strong>таблицу Silver</strong>:</p><pre class="urvanov-syntax-highlighter-plain-tag">CREATE MATERIALIZED VIEW bluesky.bluesky_dedup_mv TO bluesky.bluesky_dedup
(
	`data` JSON,
	`kind` LowCardinality(String),
	`scrape_ts` DateTime64(6),
	`bluesky_ts` DateTime64(6),
	`dedup_hash` String
)
AS SELECT
	data,
	kind,
	scrape_ts,
	bluesky_ts,
	dedup_hash
FROM bluesky.bluesky_raw
WHERE abs(timeDiff(scrape_ts, bluesky_ts)) &lt; 1200</pre><p>Схема таблицы <strong>Dead-Letter Queue</strong> и связанное с ней материализованное представление:</p><pre class="urvanov-syntax-highlighter-plain-tag">CREATE TABLE bluesky.bluesky_dlq
(
	`data` JSON(SKIP `commit.record.reply.root.record`, SKIP `commit.record.value.value`),
	`kind` LowCardinality(String),
	`scrape_ts` DateTime64(6),
	`bluesky_ts` DateTime64(6),
	`dedup_hash` String
)
ENGINE = MergeTree
ORDER BY (kind, scrape_ts)

CREATE MATERIALIZED VIEW bluesky.bluesky_dlq_mv TO bluesky.bluesky_dlq
(
	`data` JSON,
	`kind` LowCardinality(String),
	`scrape_ts` DateTime64(6),
	`bluesky_ts` DateTime64(6),
	`dedup_hash` String
)
AS SELECT
	data,
	kind,
	scrape_ts,
	bluesky_ts,
	dedup_hash
FROM bluesky.bluesky_raw
WHERE abs(timeDiff(scrape_ts, bluesky_ts)) &gt;= 1200</pre><p>Обратите внимание, что для <strong>очереди “dead letter”</strong> используется обычный движок <strong>MergeTree</strong>,<br />
так как дедупликация здесь не требуется — эти данные предназначены для анализа проблем и диагностики, а не для основной аналитики.</p>
<h2>Отправка данных на золотой уровень (Gold Layer)</h2>
<p>Описанный выше процесс оставляет <strong>разделы (partitions)</strong>, заполненные на уровне <strong>Silver</strong>.<br />
Периодически нам необходимо переносить данные из этих разделов в уровень <strong>Gold</strong>, гарантируя, что все события были полностью дедуплицированы, и при этом делать это достаточно оперативно, чтобы обеспечить наличие свежих данных в <strong>слое Gold</strong> для аналитики.</p>
<p>Мы реализуем этот периодический <strong>перенос (flushing)</strong> с помощью <strong>Refreshable Materialized View</strong>.<br />
Такие представления выполняются периодически по таблицам уровня Silver и позволяют выполнять сложные преобразования, включая денормализацию данных перед их записью в таблицы уровня Gold.</p>
<p>В нашем случае нам нужно просто периодически вставлять данные из последнего раздела, который больше не получает новых данных, в таблицу <strong>Gold</strong>.<br />
Запрос при этом должен выполняться с использованием оператора FINAL, чтобы гарантировать, что все события дедуплицированы.</p>
<p>Хотя такой запрос обычно более затратен вычислительно, здесь мы можем использовать два преимущества:</p>
<ul>
<li>Запрос выполняется периодически — в нашем случае каждые 20 минут, что смещает нагрузку с пользовательских запросов на уровень загрузки данных.</li>
<li>Мы обрабатываем только один раздел за одно выполнение. Можно ограничить дедупликацию во время выполнения только этим разделом, установив параметр <code>do_not_merge_across_partitions_select_final=1</code>, что дополнительно оптимизирует запрос и снижает нагрузку.</li>
</ul>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_09.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2265" src="https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_09.png" alt="" width="1112" height="840" srcset="https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_09.png 1112w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_09-300x227.png 300w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_09-1024x774.png 1024w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_09-768x580.png 768w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_09-450x340.png 450w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_09-780x589.png 780w" sizes="(max-width: 1112px) 100vw, 1112px" /></a></p>
<p>Для этого требуется определить, какой именно раздел нужно перенести в <strong>Gold</strong> при каждом выполнении.<br />
Эта логика показана на диаграмме выше, а в кратком виде выглядит так:</p>
<ul>
<li>Мы определяем последний раздел в таблице Silver <strong>bluesky_dedup</strong> с помощью служебного поля <code>_partition_id</code>.<br />
Из этого значения вычитаем 40 минут, получая раздел, который был создан два окна назад (X &#8212; 2) — называем его <code>current_partition</code>.</li>
<li>В целевой таблице уровня Gold bluesky есть столбец <code>_rmt_partition_id</code>,<br />
заполняемый <strong>refreshable materialized view</strong>, где хранится, из какого раздела уровня Silver поступило каждое событие.<br />
Мы используем это поле, чтобы определить последний успешно перенесённый раздел, прибавляем 20 минут,<br />
получая раздел, который нужно обработать следующим — <code>next_to_process</code>.</li>
</ul>
<p>Если <code>next_to_process = 1200</code>, это значит, что таблица bluesky пуста<br />
(0 + 1200 секунд = 1200), и ещё не было ни одной передачи данных.<br />
В этом случае мы используем <code>current_partition</code> и вставляем все события, где <code>_partition_id = current_partition</code>.</p>
<p>Если <code>next_to_process &gt; 1200</code>, значит, переносы уже выполнялись.<br />
Если <code>current_partition &gt;= next_to_process</code>, то мы отстаём не менее чем на 40 минут (2 окна),<br />
и используем значение <code>next_to_process</code>, вставляя все события, где <code>_partition_id = next_to_process</code>.<br />
Если же <code>current_partition &lt; next_to_process</code>, выполняется <code>noop</code> (ничего не происходит) — данные не переносятся.</p>
<p>Эта логика устойчива к сбоям, таким как пропуски выполнения каждые 20 минут, повторные запуски или задержки выполнения. В результате формируется <code>Refreshable Materialized View</code>, в SELECT-запросе которого инкапсулирована описанная выше логика.</p><pre class="urvanov-syntax-highlighter-plain-tag">CREATE MATERIALIZED VIEW bluesky.blue_sky_dedupe_rmv
REFRESH EVERY 20 MINUTE APPEND TO bluesky.bluesky
(
  `data` JSON(SKIP `commit.record.reply.root.record`, SKIP `commit.record.value.value`),
  `kind` LowCardinality(String),
  `bluesky_ts` DateTime64(6),
  `_rmt_partition_id` LowCardinality(String)
)
AS WITH
  (
          --step 1
        SELECT toUnixTimestamp(subtractMinutes(CAST(_partition_id, 'DateTime'), 40))
        FROM bluesky.bluesky_dedup
        GROUP BY _partition_id
        ORDER BY _partition_id DESC
        LIMIT 1
  ) AS current_partition,
  (
          --step 2
        SELECT toUnixTimestamp(addMinutes(CAST(max(partition_id), 'DateTime'), 20))
        FROM bluesky.latest_partition
  ) AS next_to_process
SELECT
  data,
  kind,
  bluesky_ts,
  _partition_id AS _rmt_partition_id
FROM bluesky.bluesky_dedup
FINAL
--step 3 &amp; 4
WHERE _partition_id = CAST(if(next_to_process = 1200, current_partition, if(current_partition &gt;= next_to_process, next_to_process, 0)), 'String')
SETTINGS do_not_merge_across_partitions_select_final = 1</pre><p>Это представление выполняется каждые 20 минут, передавая очищенные и дедуплицированные данные в <strong>уровень Gold</strong>. Следует отметить, что данные появляются в <strong>Gold</strong> с задержкой около 40 минут, хотя при необходимости пользователи могут выполнять запросы к уровню <strong>Silver</strong> для получения более свежих данных.</p>
<p>Внимательный читатель заметит, что в шаге 2 и на диаграмме выше наш запрос использует таблицу <code>latest_partition</code>, а не обращается напрямую к <code>_rmt_partition_id</code> в таблице <strong>bluesky</strong> уровня Gold.<br />
Эта таблица создаётся с помощью инкрементного материализованного представления (incremental materialized view) и служит оптимизацией, которая ускоряет определение следующего раздела для обработки.</p>
<p>Это представление отслеживает последний вставленный раздел в таблицу <strong>Gold</strong> и выглядит следующим образом.</p><pre class="urvanov-syntax-highlighter-plain-tag">CREATE MATERIALIZED VIEW bluesky.latest_partition_mv TO bluesky.latest_partition
(
	`partition_id` UInt32
)
AS SELECT max(CAST(_rmt_partition_id, 'UInt32')) AS partition_id
FROM bluesky.bluesky

CREATE TABLE bluesky.latest_partition
(
	`partition_id` SimpleAggregateFunction(max, UInt32)
)
ENGINE = AggregatingMergeTree
ORDER BY tuple()</pre><p></p>
<h1>Золотой уровень для анализа данных (Gold Layer для аналитики)</h1>
<p>Указанное выше <strong>refreshable materialized view</strong> периодически отправляет данные в таблицу уровня Gold — bluesky.</p>
<p>Схема этой таблицы показана ниже:</p><pre class="urvanov-syntax-highlighter-plain-tag">CREATE TABLE bluesky.bluesky
(
	`data` JSON(SKIP `commit.record.reply.root.record`, SKIP `commit.record.value.value`),
	`kind` LowCardinality(String),
	`bluesky_ts` DateTime64(6),
	`_rmt_partition_id` LowCardinality(String)
)
ENGINE = MergeTree
PARTITION BY toStartOfInterval(bluesky_ts, toIntervalMonth(1))
ORDER BY (kind, bluesky_ts)</pre><p>Поскольку данные полностью дедуплицированы до момента вставки, мы можем использовать стандартный <strong>MergeTree</strong>.</p>
<p><strong>Ключ сортировки (ORDER BY)</strong> выбирается исключительно на основе шаблонов доступа потребителей данных и с целью оптимизации сжатия.</p>
<p><strong>Таблица разделена по месяцам (partitioned by month)</strong> &#8212; в первую очередь для удобства управления данными, а также потому, что ожидается, что большинство запросов будут обращаться к самым последним данным.</p>
<p>Обратите внимание: хотя мы по-прежнему используем тип данных <strong>JSON</strong> на этом уровне,<br />
возможно выполнение дополнительных трансформаций данных на этапе предыдущего<br />
<strong>refreshable materialized view</strong> — например:</p>
<ul>
<li>извлечение часто используемых полей в корень таблицы,</li>
<li>использование столбцов типа <strong>ALIAS</strong>, чтобы упростить синтаксис запросов и повысить удобство анализа.</li>
</ul>
<h2>Материализованные представления для общих запросов (Для часто запрашиваемых метрик)</h2>
<p>Этот <strong>слой gold</strong> должен быть полностью оптимизирован для выполнения запросов со стороны прикладных систем и потребителей данных. Хотя наш ключ сортировки направлен на то, чтобы облегчить этот процесс, не все шаблоны доступа будут одинаковыми. До настоящего времени наиболее распространённым применением инкрементных материализованных представлений было выполнение фильтрации и вставки данных между слоями. Однако наше более раннее использование представления для вычисления следующего раздела (partition) намекало на то, как ещё можно оптимизировать другие запросы.</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_10.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2280" src="https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_10.png" alt="" width="1370" height="611" srcset="https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_10.png 1370w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_10-300x134.png 300w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_10-1024x457.png 1024w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_10-768x343.png 768w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_10-450x201.png 450w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_10-780x348.png 780w" sizes="(max-width: 1370px) 100vw, 1370px" /></a></p>
<p>Помимо фильтрации и отправки подмножеств данных в целевую таблицу с другими ключами сортировки (оптимизированными под иные шаблоны доступа), материализованные представления могут использоваться для предварительного вычисления агрегатов во время вставки данных в таблицу gold.</p>
<p>Результаты таких агрегатов будут представлять собой уменьшенную форму исходных данных (частичный набросок, если речь идёт об агрегации). Это не только упрощает последующие запросы к целевой таблице, но и обеспечивает более высокую скорость выполнения, поскольку вычисления переносятся с момента запроса на момент вставки, тем самым снижая время отклика при запросе.</p>
<p>Полное руководство по материализованным представлениям можно найти здесь.</p>
<p>В качестве примера рассмотрим наш предыдущий запрос, который вычисляет наиболее распространённые типы <strong>commit-событий</strong>:</p><pre class="urvanov-syntax-highlighter-plain-tag">SELECT data.commit.collection AS collection, count() AS c, uniq(data.did) AS users
FROM bluesky
WHERE kind = 'commit'
GROUP BY ALL
ORDER BY c DESC
LIMIT 10

┌─collection───────────────┬─────────c─┬───users─┐
│ app.bsky.feed.like       │ 269979403 │ 5270604 │
│ app.bsky.graph.follow    │ 150891706 │ 5631987 │
│ app.bsky.feed.post       │  46886207 │ 3083647 │
│ app.bsky.feed.repost     │  33249341 │ 1956986 │
│ app.bsky.graph.block     │   9789707 │  993578 │
│ app.bsky.graph.listitem  │   3231676 │  102020 │
│ app.bsky.actor.profile   │   1731669 │ 1280895 │
│ app.bsky.graph.listblock │  263667   │  105310 │
│ app.bsky.feed.threadgate │  215715   │   49871 │
│ app.bsky.feed.postgate   │   99625   │   19960 │
└──────────────────────────┴───────────┴─────────┘

10 rows in set. Elapsed: 6.445 sec. Processed 516.53 million rows, 45.50 GB (80.15 million rows/s., 7.06 GB/s.)
Peak memory usage: 986.51 MiB.</pre><p>Для 500 миллионов событий выполнение этого запроса занимает около 6 секунд.<br />
Чтобы преобразовать его в инкрементное материализованное представление, необходимо подготовить таблицу, которая будет получать результаты инкрементной агрегации:</p><pre class="urvanov-syntax-highlighter-plain-tag">CREATE TABLE bluesky.top_post_types
(
  `collection` LowCardinality(String),
  `posts` SimpleAggregateFunction(sum, UInt64),
  `users` AggregateFunction(uniq, String)
)
ENGINE = AggregatingMergeTree
ORDER BY collection</pre><p>Обратите внимание, что нам необходимо использовать <strong>AggregatingMergeTree</strong> и указать ключ сортировки как ключ группировки — результаты агрегации с одинаковыми значениями этого столбца будут объединяться.</p>
<p>Инкрементные результаты должны храниться в специальных типах столбцов <strong>SimpleAggregateFunction</strong> и <strong>AggregateFunction</strong> — для этого необходимо указать саму функцию и связанный с ней тип данных.</p>
<p>Ниже показано соответствующее материализованное представление, которое заполняет эту таблицу при вставке строк в таблицу <strong>gold</strong>. Обратите внимание, что используется суффикс &#8212; <code>State</code>, чтобы явно сгенерировать состояние агрегации:</p><pre class="urvanov-syntax-highlighter-plain-tag">CREATE MATERIALIZED VIEW top_post_types_mv TO top_posts_types
AS
SELECT data.commit.collection AS collection, count() AS posts,
  uniqState(CAST(data.did, 'String')) AS users
FROM bluesky
WHERE kind = 'commit'
GROUP BY ALL

When querying this table, we use the -Merge suffix to merge aggregation states.


SELECT collection,
       sum(posts) AS posts,
       uniqMerge(users) AS users
FROM top_post_types
GROUP BY collection
ORDER BY posts DESC
LIMIT 10

10 rows in set. Elapsed: 0.042 sec.</pre><p>Производительность запроса улучшилась более чем в 150 раз!</p>
<p>Ниже приведена финальная диаграмма архитектуры, показывающая все наши уровни:</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_11.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2281" src="https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_11.png" alt="" width="1795" height="883" srcset="https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_11.png 1795w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_11-300x148.png 300w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_11-1024x504.png 1024w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_11-768x378.png 768w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_11-1536x756.png 1536w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_11-450x221.png 450w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_11-780x384.png 780w, https://datatalks.ru/wp-content/uploads/2025/10/Medallion_with_Bluesky_11-1600x787.png 1600w" sizes="(max-width: 1795px) 100vw, 1795px" /></a></p>
<h2>Примеры запросов и визуализации на sql.clickhouse.com</h2>
<p>Приведённый выше пример представляет собой очень простую демонстрацию. Эти данные доступны на сайте <a href="https://sql.clickhouse.com/" target="_blank" rel="noopener">sql.clickhouse.com</a>, где описанный выше рабочий процесс <strong>Medallion</strong> выполняется непрерывно. Мы также предоставили дополнительные материализованные представления в качестве примеров для эффективного выполнения запросов.</p>
<p>Например, чтобы определить, в какое время суток пользователи чаще всего ставят лайки, публикуют и репостят в Bluesky, можно выполнить следующий запрос:</p><pre class="urvanov-syntax-highlighter-plain-tag">SELECT event, hour_of_day, sum(count) as count
FROM bluesky.events_per_hour_of_day
WHERE event in ['post', 'repost', 'like']
GROUP BY event, hour_of_day
ORDER BY hour_of_day;

72 rows in set. Elapsed: 0.007 sec.</pre><p>Запрос выполняется за 7 миллисекунд.</p>
<p>Вы можете запустить этот запрос в нашем playground, чтобы отобразить результат в виде графика.</p>
<p>Ниже приведено соответствующее материализованное представление и целевая таблица, которая заполняется по мере вставки строк в gold-таблицу:</p><pre class="urvanov-syntax-highlighter-plain-tag">CREATE TABLE bluesky.events_per_hour_of_day
(
    event LowCardinality(String),
    hour_of_day UInt8,
    count SimpleAggregateFunction(sum, UInt64)
)
ENGINE = AggregatingMergeTree
ORDER BY (event, hour_of_day);


CREATE MATERIALIZED VIEW bluesky.events_per_hour_of_day_mv TO bluesky.events_per_hour_of_day
AS SELECT
    extract(data.commit.collection, '\\.([^.]+)</pre><p>Полный список запросов и соответствующих им представлений можно посмотреть здесь.<br />
Кроме того, вы можете напрямую выполнять запросы к <strong>gold</strong> или <strong>silver таблицам</strong>!</p>
<p><strong>Некоторые примеры, с которых можно начать:</strong></p>
<ul>
<li><a href="https://sql.clickhouse.com/?query_id=P2VKEOGYQVHFPA8F5IFZ2P&amp;run_query=true&amp;tab=results" target="_blank" rel="noopener">Общее количество событий (Total events)</a></li>
<li><a href="https://sql.clickhouse.com/?query_id=51KVUJ5FGJUQV9XU13JKL3&amp;run_query=true&amp;tab=charts" target="_blank" rel="noopener">Когда пользователи чаще всего используют Bluesky (When do people use BlueSky)</a></li>
<li><a href="https://sql.clickhouse.com/?query_id=9WMMTPMMP7TAIWO5ZGWZZE&amp;run_query=true&amp;tab=results" target="_blank" rel="noopener">Самые популярные типы событий (Top event types)</a></li>
<li><a href="https://sql.clickhouse.com/?query_id=RJR6SMBYEKJSSWWUXFHP1U&amp;run_query=true&amp;tab=charts" target="_blank" rel="noopener">Топ типов событий по количеству (Top event types by count)</a></li>
<li><a href="https://sql.clickhouse.com/?query_id=5C6SW7OHKEVFLRDMED2WNB&amp;run_query=true&amp;tab=charts" target="_blank" rel="noopener">Топ типов событий по уникальным пользователям (Top event types by unique users)</a></li>
<li><a href="https://sql.clickhouse.com/?query_id=8YAFPZQXXCGD75842UKE2W&amp;run_query=true&amp;tab=results" target="_blank" rel="noopener">Самые залайканные посты (Most liked posts)</a></li>
<li><a href="https://sql.clickhouse.com/?query_id=7ICWGHB7HIIEWCMYFAWIAE&amp;run_query=true&amp;tab=results" target="_blank" rel="noopener">Самые залайканные посты о ClickHouse (Most liked posts about ClickHouse)</a></li>
<li><a href="https://sql.clickhouse.com/?query_id=BP4SSVSKXB4EJCMFHU8B6D&amp;run_query=true&amp;tab=results" target="_blank" rel="noopener">Самые часто reposted посты (Most reposted posts)</a></li>
<li><a href="https://sql.clickhouse.com/?query_id=ATT83TXCQE8DUDCM84GB6E&amp;run_query=true&amp;tab=charts" target="_blank" rel="noopener">Самые используемые языки (Most used languages)</a></li>
<li><a href="https://sql.clickhouse.com/?query_id=H2CYZJDRCXCFYLPXMJVRCR&amp;run_query=true&amp;tab=results" target="_blank" rel="noopener">Самые залайканные пользователи (Most liked users)</a></li>
<li><a href="https://sql.clickhouse.com/?query_id=SH7GXXK4BYPHELXM5AHWDJ&amp;run_query=true&amp;tab=results">Самые часто reposted пользователи (Most reposted users)</a></li>
</ul>
<h1>Заключительные мысли</h1>
<p>В этом блоге мы продемонстрировали полностью реализованную архитектуру Medallion, построенную исключительно на ClickHouse, показав, как его мощные возможности позволяют преобразовывать “сырые”, полуструктурированные данные в качественные, готовые к запросам наборы данных.</p>
<p><strong>Через уровни Bronze, Silver и Gold</strong> мы решили типичные проблемы, такие как повреждённые данные (malformed data), несогласованность структуры и значительное количество дубликатов.<br />
Благодаря использованию типа данных JSON в ClickHouse, нам удалось эффективно обрабатывать по своей природе полуструктурированные и динамичные данные, при этом сохраняя высокую производительность.</p>
<p>Хотя эта архитектура обеспечивает надёжный и гибкий рабочий процесс, она всё же вносит определённые задержки по мере перемещения данных между слоями.<br />
В нашем решении <strong>“окна дедупликации” (deduplication windows)</strong> помогли минимизировать эти задержки, однако остаётся компромисс между скоростью доставки данных в реальном времени и качеством данных.<br />
Поэтому <strong>архитектура Medallion</strong> особенно хорошо подходит для наборов данных с высокой степенью дублирования и менее критичными требованиями к мгновенной доступности данных.</p><pre class="urvanov-syntax-highlighter-plain-tag">) AS event,
    toHour(bluesky_ts) as hour_of_day,
    count() AS count
FROM bluesky.bluesky
WHERE (kind = 'commit')
GROUP BY event, hour_of_day;</pre><p>Полный список запросов и соответствующих им представлений можно посмотреть здесь.<br />
Кроме того, вы можете напрямую выполнять запросы к <strong>gold</strong> или <strong>silver таблицам</strong>!</p>
<p><strong>Некоторые примеры, с которых можно начать:</strong></p>
<ul>
<li><a href="https://sql.clickhouse.com/?query_id=P2VKEOGYQVHFPA8F5IFZ2P&amp;run_query=true&amp;tab=results" target="_blank" rel="noopener">Общее количество событий (Total events)</a></li>
<li><a href="https://sql.clickhouse.com/?query_id=51KVUJ5FGJUQV9XU13JKL3&amp;run_query=true&amp;tab=charts" target="_blank" rel="noopener">Когда пользователи чаще всего используют Bluesky (When do people use BlueSky)</a></li>
<li><a href="https://sql.clickhouse.com/?query_id=9WMMTPMMP7TAIWO5ZGWZZE&amp;run_query=true&amp;tab=results" target="_blank" rel="noopener">Самые популярные типы событий (Top event types)</a></li>
<li><a href="https://sql.clickhouse.com/?query_id=RJR6SMBYEKJSSWWUXFHP1U&amp;run_query=true&amp;tab=charts" target="_blank" rel="noopener">Топ типов событий по количеству (Top event types by count)</a></li>
<li><a href="https://sql.clickhouse.com/?query_id=5C6SW7OHKEVFLRDMED2WNB&amp;run_query=true&amp;tab=charts" target="_blank" rel="noopener">Топ типов событий по уникальным пользователям (Top event types by unique users)</a></li>
<li><a href="https://sql.clickhouse.com/?query_id=8YAFPZQXXCGD75842UKE2W&amp;run_query=true&amp;tab=results" target="_blank" rel="noopener">Самые залайканные посты (Most liked posts)</a></li>
<li><a href="https://sql.clickhouse.com/?query_id=7ICWGHB7HIIEWCMYFAWIAE&amp;run_query=true&amp;tab=results" target="_blank" rel="noopener">Самые залайканные посты о ClickHouse (Most liked posts about ClickHouse)</a></li>
<li><a href="https://sql.clickhouse.com/?query_id=BP4SSVSKXB4EJCMFHU8B6D&amp;run_query=true&amp;tab=results" target="_blank" rel="noopener">Самые часто reposted посты (Most reposted posts)</a></li>
<li><a href="https://sql.clickhouse.com/?query_id=ATT83TXCQE8DUDCM84GB6E&amp;run_query=true&amp;tab=charts" target="_blank" rel="noopener">Самые используемые языки (Most used languages)</a></li>
<li><a href="https://sql.clickhouse.com/?query_id=H2CYZJDRCXCFYLPXMJVRCR&amp;run_query=true&amp;tab=results" target="_blank" rel="noopener">Самые залайканные пользователи (Most liked users)</a></li>
<li><a href="https://sql.clickhouse.com/?query_id=SH7GXXK4BYPHELXM5AHWDJ&amp;run_query=true&amp;tab=results">Самые часто reposted пользователи (Most reposted users)</a></li>
</ul>
<h1>Заключительные мысли</h1>
<p>В этом блоге мы продемонстрировали полностью реализованную архитектуру Medallion, построенную исключительно на ClickHouse, показав, как его мощные возможности позволяют преобразовывать “сырые”, полуструктурированные данные в качественные, готовые к запросам наборы данных.</p>
<p>Через уровни Bronze, Silver и Gold мы решили типичные проблемы, такие как <strong>повреждённые данные (malformed data)</strong>, несогласованность структуры и значительное количество дубликатов.<br />
Благодаря использованию типа данных JSON в ClickHouse, нам удалось эффективно обрабатывать по своей природе полуструктурированные и динамичные данные, при этом сохраняя высокую производительность.</p>
<p>Хотя эта архитектура обеспечивает надёжный и гибкий рабочий процесс, она всё же вносит определённые задержки по мере перемещения данных между слоями.</p>
<p>В нашем решении <strong>“окна дедупликации” (deduplication windows)</strong> помогли минимизировать эти задержки, однако остаётся компромисс между скоростью доставки данных в реальном времени и качеством данных.</p>
<p>Поэтому <strong>архитектура Medallion</strong> особенно хорошо подходит для наборов данных с высокой степенью дублирования и менее критичными требованиями к мгновенной доступности данных.</p>
<p>Сообщение <a href="https://datatalks.ru/building-a-medallion-architecture-for-bluesky-json-data-with-clickhouse/">Построение архитектуры Medallion для данных Bluesky в формате JSON с помощью ClickHouse</a> появились сначала на <a href="https://datatalks.ru">DataTalks.RU. Data Engineering / DWH / Data Pipeline</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://datatalks.ru/building-a-medallion-architecture-for-bluesky-json-data-with-clickhouse/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Глава 5. Кодирование и Эволюция (Encoding and Evolution)</title>
		<link>https://datatalks.ru/chapter-5-encoding-and-evolution/</link>
					<comments>https://datatalks.ru/chapter-5-encoding-and-evolution/#respond</comments>
		
		<dc:creator><![CDATA[Data Engineer (Admin)]]></dc:creator>
		<pubDate>Sat, 06 Sep 2025 17:15:26 +0000</pubDate>
				<category><![CDATA[Data Architecture / Data Modeling]]></category>
		<category><![CDATA[Designing Data-Intensive Applications]]></category>
		<category><![CDATA[Encoding and Evolution]]></category>
		<guid isPermaLink="false">https://datatalks.ru/?p=2113</guid>

					<description><![CDATA[<p>Перевод из книги «Designing Data-Intensive Applications, 2nd Edition» подготовлен автором сайта Глава 5. Кодирование и эволюция Всё изменяется и ничто не стоит на месте. Гераклит Эфесский, цитата у Платона в «Кратиле» (360 г. до н. э.) Приложения неизбежно со временем меняются. Новые фичи добавляются или модифицируются, когда запускаются новые продукты, пользовательские требования становятся лучше понятны [&#8230;]</p>
<p>Сообщение <a href="https://datatalks.ru/chapter-5-encoding-and-evolution/">Глава 5. Кодирование и Эволюция (Encoding and Evolution)</a> появились сначала на <a href="https://datatalks.ru">DataTalks.RU. Data Engineering / DWH / Data Pipeline</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p><em>Перевод из книги «Designing Data-Intensive Applications, 2nd Edition» подготовлен автором сайта</em></p>
<h1>Глава 5. Кодирование и эволюция</h1>
<blockquote><p>Всё изменяется и ничто не стоит на месте.<br />
<strong>Гераклит Эфесский, цитата у Платона в «Кратиле» (360 г. до н. э.)</strong></p></blockquote>
<hr />
<p>Приложения неизбежно со временем меняются. Новые фичи добавляются или модифицируются, когда запускаются новые продукты, пользовательские требования становятся лучше понятны или меняются бизнес-обстоятельства. В Главе 2 мы ввели понятие <strong>Эволюционности (Расширяемости)</strong>: мы должны стремиться строить системы так, чтобы адаптация к изменениям была максимально простой.</p>
<p>В большинстве случаев изменение функциональности приложения также требует изменения данных, которые оно хранит: возможно, нужно захватить новое поле или тип записи, или, возможно, существующие данные должны быть представлены по-новому.</p>
<p>Модели данных по-разному справляются с такими изменениями. <strong>Реляционные базы данных</strong> обычно предполагают, что все данные в базе соответствуют одной схеме: хотя эта схема может быть изменена (через миграции схемы, т. е. операторы ALTER), в любой момент времени действует ровно одна схема. Для сравнения, <strong>базы данных со схемой «на чтение» (schema-on-read, «безсхемные»)</strong> схему не навязывают, поэтому база может содержать смесь старых и новых форматов данных, записанных в разное время (см. «Гибкость схемы в документной модели»).</p>
<p>Когда формат данных или схема меняется, часто требуется соответствующее изменение в коде приложения (например, вы добавляете новое поле в запись, и код приложения начинает читать и записывать это поле). Однако в большом приложении изменения в коде не всегда могут быть выполнены мгновенно:</p>
<p>В серверных приложениях вы можете захотеть выполнить <strong>rolling upgrade</strong> (также называемый <strong>staged rollout</strong>) — выкатывать новую версию на несколько нод за раз, проверять, что новая версия работает стабильно, и постепенно проходить через все ноды. Это позволяет деплоить новые версии без даунтайма сервиса и таким образом стимулирует более частые релизы и лучшую эволюционность (расширяемость).</p>
<p>В клиентских приложениях вы полностью зависите от пользователя, который может не установить обновление ещё какое-то время.</p>
<p>Это означает, что старые и новые версии кода, а также старые и новые форматы данных потенциально могут одновременно сосуществовать в системе. Чтобы система продолжала работать корректно, необходимо поддерживать совместимость в обоих направлениях:</p>
<ul>
<li><strong>Обратная совместимость</strong><br />
Новый код может читать данные, которые были записаны старым кодом.</li>
<li><strong>Прямая совместимость</strong><br />
Старый код может читать данные, которые были записаны новым кодом.</li>
</ul>
<p><strong>Обратная совместимость обычно несложно достигается:</strong> как автор нового кода вы знаете формат данных, записанных старым кодом, и можете явно обрабатывать их (при необходимости просто сохранив старый код для чтения старых данных). Прямая совместимость может быть более сложной, так как она требует, чтобы старый код игнорировал добавления, сделанные более новой версией кода.</p>
<p>Ещё одна проблема с прямой совместимостью проиллюстрирована на рисунке 5-1. Допустим, вы добавляете поле в схему записи, и новый код создаёт запись с этим новым полем и сохраняет её в базу. Затем более старая версия кода (которая ещё не знает о новом поле) читает эту запись, обновляет её и записывает обратно. В этой ситуации желаемым поведением обычно является сохранение старым кодом нового поля нетронутым, даже если оно не может быть интерпретировано. Но если запись декодируется в объект модели, который явно не сохраняет неизвестные поля, данные могут быть утеряны — как на рисунке 5-1.</p>
<p><strong>Рисунок 5-1. Когда более старая версия приложения обновляет данные, ранее записанные более новой версией приложения, данные могут быть потеряны, если не быть осторожным</strong></p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_1-scaled.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2188" src="https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_1-scaled.png" alt="" width="2560" height="1391" srcset="https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_1-scaled.png 2560w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_1-300x163.png 300w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_1-1024x556.png 1024w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_1-768x417.png 768w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_1-1536x835.png 1536w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_1-2048x1113.png 2048w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_1-450x245.png 450w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_1-780x424.png 780w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_1-1600x869.png 1600w" sizes="(max-width: 2560px) 100vw, 2560px" /></a></p>
<p>В этой главе мы рассмотрим несколько форматов кодирования данных, включая <code>JSON</code>, <code>XML</code>, <code>Protocol Buffers</code> и <code>Avro</code>. В частности, мы посмотрим, как они обрабатывают изменения схем и как они поддерживают системы, в которых старые и новые данные и код должны сосуществовать. Затем мы обсудим, как эти форматы используются для хранения данных и для коммуникации: в базах данных, веб-сервисах, REST API, удалённых вызовах процедур (RPC), движках workflow и системах, основанных на событиях, таких как акторы и очереди сообщений.</p>
<h1>Форматы кодирования данных</h1>
<p>Программы обычно работают с данными в (как минимум) двух различных представлениях:</p>
<ol>
<li>В памяти данные хранятся в объектах, структурах (structs), списках, массивах, хэш-таблицах, деревьях и т. д. Эти структуры данных оптимизированы для эффективного доступа и манипуляций со стороны CPU (обычно с использованием указателей).</li>
<li>Когда вы хотите записать данные в файл или отправить их по сети, необходимо закодировать их как некоторую самодостаточную последовательность байт (например, JSON-документ). Так как указатель не имеет смысла для любого другого процесса, это представление в виде последовательности байт зачастую выглядит совсем иначе, чем структуры данных, которые обычно используются в памяти.</li>
</ol>
<p>Таким образом, необходим некий перевод между двумя представлениями. Преобразование из представления в памяти в последовательность байт называется <strong>кодированием</strong> (также известно как <strong>сериализация</strong> или <strong>маршаллинг</strong>), а обратное преобразование — <strong>декодированием</strong> (<strong>парсинг</strong>, <strong>десериализация</strong>, <strong>анмаршаллинг</strong>).</p>
<hr />
<p><strong>КОНФЛИКТ ТЕРМИНОВ</strong></p>
<p><strong>Термин «сериализация»</strong> к сожалению также используется в контексте транзакций, но с совершенно иным значением. Чтобы избежать перегрузки слова, в этой книге мы будем придерживаться термина «кодирование», хотя «сериализация» является, возможно, более распространённым термином.</p>
<hr />
<p>Есть исключения, когда <strong>кодирование/декодирование</strong> не требуется — например, когда база данных работает напрямую с сжатыми данными, загруженными с диска, как обсуждается в разделе «Выполнение запросов: компиляция и векторизация». Существуют также <strong>zero-copy форматы данных</strong>, которые спроектированы для использования как во время выполнения, так и на диске/в сети без явного шага преобразования, такие как <strong>Cap’n Proto</strong> и <strong>FlatBuffers</strong>.</p>
<p>Однако большинство систем нуждаются в преобразовании между объектами в памяти и плоскими последовательностями байт. Так как это настолько распространённая задача, существует множество различных библиотек и форматов кодирования на выбор. Давайте сделаем краткий обзор.</p>
<h2>Языко-специфичные форматы</h2>
<p>Многие языки программирования поставляются со встроенной поддержкой кодирования объектов из памяти в последовательности байт. Например, в <strong>Java</strong> это <code>java.io.Serializable</code>, в <strong>Python</strong> — <code>pickle</code>, в <strong>Ruby</strong> — <code>Marshal</code> и т. д. Также существует множество сторонних библиотек, например <code>Kryo</code> для <strong>Java</strong>.</p>
<p>Эти библиотеки кодирования очень удобны, так как позволяют сохранять и восстанавливать объекты из памяти с минимальным дополнительным кодом. Однако у них есть несколько серьёзных проблем:</p>
<ul>
<li>Кодирование часто привязано к конкретному языку программирования, и чтение данных на другом языке крайне затруднительно. Если вы храните или передаёте данные в таком кодировании, вы фактически связываете себя с текущим языком программирования на очень долгое время и исключаете возможность интеграции ваших систем с системами других организаций (которые могут использовать другие языки).</li>
<li>Чтобы восстановить данные в тех же типах объектов, процесс декодирования должен уметь инстанцировать произвольные классы. Это часто является источником проблем с безопасностью: если злоумышленник сможет заставить ваше приложение декодировать произвольную последовательность байт, он сможет инстанцировать произвольные классы, что в свою очередь зачастую позволяет делать ужасные вещи, такие как удалённое выполнение произвольного кода.</li>
<li>Версионирование данных часто является второстепенной задачей в этих библиотеках: так как они предназначены для быстрого и лёгкого кодирования данных, они часто пренебрегают неудобными проблемами прямой и обратной совместимости.</li>
<li>Эффективность (время CPU на кодирование или декодирование, а также размер закодированной структуры) также часто является второстепенной. Например, встроенная сериализация Java печально известна своей низкой производительностью и раздутым кодированием.</li>
</ul>
<p>По этим причинам, как правило, плохая идея использовать встроенное в язык кодирование для чего-либо, кроме очень временных целей.</p>
<h2>JSON, XML и бинарные варианты</h2>
<p>При переходе на стандартизированные кодирования, которые могут быть записаны и прочитаны многими языками программирования, <strong>JSON</strong> и <strong>XML</strong> — очевидные претенденты. Они широко известны, широко поддерживаются и почти так же широко нелюбимы. <strong>XML</strong> часто критикуют за излишнюю многословность и ненужную сложность. Популярность <strong>JSON</strong> в основном связана с его встроенной поддержкой в веб-браузерах и простотой по сравнению с XML. <strong>CSV</strong> — ещё один популярный формат, независимый от языка, но он поддерживает только табличные данные без вложенности.</p>
<p><code>JSON</code>, <code>XML</code> и <code>CSV</code> являются текстовыми форматами и, таким образом, в какой-то степени человекочитаемыми (хотя синтаксис — популярная тема для дискуссий). Помимо поверхностных синтаксических проблем, у них есть также более тонкие недостатки:</p>
<ul>
<li>Существует много неоднозначностей вокруг кодирования чисел. В XML и CSV невозможно различить число и строку, состоящую из цифр (кроме как ссылаясь на внешнюю схему). JSON различает строки и числа, но не различает целые и числа с плавающей точкой и не указывает точность.<br />
Это становится проблемой при работе с большими числами; например, целые числа больше <strong style="font-size: revert;">2⁵³</strong><span style="font-size: revert;"> не могут быть точно представлены в числе с плавающей точкой двойной точности </span><strong style="font-size: revert;">IEEE 754</strong><span style="font-size: revert;">, поэтому такие числа становятся неточными при парсинге в языке, который использует числа с плавающей точкой, таком как JavaScript. Пример чисел больше </span><strong style="font-size: revert;">2⁵³</strong><span style="font-size: revert;"> встречается в X (ранее Twitter), где для идентификации каждого поста используется </span><strong style="font-size: revert;">64-битное число</strong><span style="font-size: revert;">. JSON, возвращаемый API, включает идентификаторы постов дважды: один раз как JSON-число и один раз как десятичную строку, чтобы обойти тот факт, что числа некорректно парсятся приложениями на JavaScript.</span></li>
</ul>
<ul>
<li><strong>JSON</strong> и <strong>XML</strong> имеют хорошую поддержку строк символов в Unicode (т. е. человекочитаемого текста), но они не поддерживают бинарные строки (последовательности байт без кодировки символов). Бинарные строки — полезная фича, поэтому люди обходят это ограничение, кодируя бинарные данные в текст с помощью Base64. Схема затем используется для указания, что значение должно интерпретироваться как Base64-кодированное. Это работает, но выглядит несколько костыльно и увеличивает размер данных на 33%.</li>
<li><strong>XML Schema</strong> и <strong>JSON Schema</strong> мощные, и поэтому довольно сложные для изучения и реализации. Так как правильная интерпретация данных (например, чисел и бинарных строк) зависит от информации в схеме, приложения, которые не используют схемы XML/JSON, потенциально должны хардкодить соответствующую логику кодирования/декодирования.</li>
<li>У <strong>CSV</strong> вообще нет схемы, поэтому приложение должно само определять значение каждой строки и каждого столбца. Если изменение в приложении добавляет новую строку или столбец, необходимо обрабатывать это изменение вручную. CSV также является довольно размытым форматом (что произойдёт, если значение содержит запятую или символ новой строки?). Хотя его правила экранирования формально задокументированы, не все парсеры корректно их реализуют.</li>
</ul>
<p>Несмотря на эти недостатки, <strong>JSON</strong>, <strong>XML</strong> и <strong>CSV</strong> достаточно хороши для многих целей. Скорее всего, они останутся популярными, особенно как форматы обмена данными (т. е. для отправки данных от одной организации другой). В таких ситуациях, пока стороны согласны относительно формата, часто не имеет значения, насколько красив или эффективен этот формат. Сложность заставить разные организации договориться хоть о чём-то перевешивает большинство других соображений.</p>
<h3>JSON Schema</h3>
<p><strong>JSON Schema</strong> получил широкое распространение как способ моделирования данных всякий раз, когда они обмениваются между системами или записываются в хранилище. Вы найдёте JSON-схемы в веб-сервисах (см. «Web services») как часть спецификации веб-сервисов OpenAPI, в регистрах схем, таких как <strong>Confluent Schema Registry</strong> и <strong>Red Hat Apicurio Registry</strong>, а также в базах данных, таких как расширение валидатора <code>pg_jsonschema</code> в <strong>PostgreSQL</strong> и синтаксис валидатора <code>$jsonSchema</code> в <strong>MongoDB</strong>.</p>
<p><strong>Спецификация JSON Schema</strong> предлагает ряд возможностей. Схемы включают стандартные примитивные типы, включая строки, числа, целые, объекты, массивы, булевы значения или null. Но <strong>JSON Schema</strong> также предоставляет отдельную спецификацию валидации, которая позволяет разработчикам накладывать ограничения на поля. Например, поле порта может иметь минимум 1 и максимум <code>65535</code>.</p>
<p><strong>JSON Schema</strong> может иметь либо открытую, либо закрытую модель содержимого. Открытая модель содержимого допускает существование любых полей, не определённых в схеме, с любыми типами данных, тогда как закрытая модель содержимого разрешает только явно определённые поля. Открытая модель содержимого в JSON Schema включена, когда <code>additionalProperties</code> установлено в <code>true</code>, что является значением по умолчанию. Таким образом, JSON Schema обычно является определением того, что не разрешено (а именно, недопустимые значения для любых определённых полей), а не того, что разрешено в схеме.</p>
<p>Открытые модели содержимого мощные, но могут быть сложными. Например, предположим, вы хотите определить отображение от целых чисел (например, ID) к строкам. JSON не имеет типа «map» или «dictionary», только тип «object», который может содержать строковые ключи и значения любого типа. Вы можете затем ограничить этот тип с помощью JSON Schema так, чтобы ключи могли содержать только цифры, а значения могли быть только строками, используя <code>patternProperties</code> и <code>additionalProperties</code>, как показано в Примере 5-1.</p>
<p><strong>Пример 5-1. Пример JSON Schema с целочисленными ключами и строковыми значениями.</strong> Целочисленные ключи представлены как строки, содержащие только целые числа, так как JSON Schema требует, чтобы все ключи были строками.</p><pre class="urvanov-syntax-highlighter-plain-tag">{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "type": "object",
  "patternProperties": {
    "^[0-9]+$": {
      "type": "string"
    }
  },
  "additionalProperties": false
}</pre><p>В дополнение к открытым и закрытым моделям содержимого и валидаторам, JSON Schema поддерживает условную if/else-логику схемы, именованные типы, ссылки на удалённые схемы и многое другое. Всё это делает язык схем очень мощным. Такие возможности также делают определения громоздкими. Может быть сложно разрешать удалённые схемы, рассуждать об условных правилах или эволюционировать схемы в направлении прямой или обратной совместимости. Подобные проблемы применимы и к XML Schema.</p>
<h3>Бинарное кодирование</h3>
<p><strong>JSON</strong> менее многословен, чем <strong>XML</strong>, но оба всё равно занимают много места по сравнению с бинарными форматами. Это наблюдение привело к разработке множества бинарных кодирований для <strong>JSON</strong> (<code>MessagePack</code>, <code>CBOR</code>, <code>BSON</code>, <code>BJSON</code>, <code>UBJSON</code>, <code>BISON</code>, <code>Hessian</code> и <code>Smile</code>, например) и для <strong>XML</strong> (<code>WBXML</code> и <code>Fast Infoset</code>, к примеру). Эти форматы были приняты в различных нишах, так как они более компактны и иногда быстрее парсятся, но ни один из них не получил такого широкого распространения, как текстовые версии <strong>JSON</strong> и <strong>XML</strong>.</p>
<p>Некоторые из этих форматов расширяют набор типов данных (например, различают целые числа и числа с плавающей точкой или добавляют поддержку бинарных строк), но в остальном они сохраняют модель данных JSON/XML неизменной. В частности, так как они не предписывают схему, они должны включать все имена полей объекта внутри закодированных данных. То есть, в бинарном кодировании JSON-документа из Примера 5-2 им придётся где-то включить строки <code>userName</code>, <code>favoriteNumber</code> и <code>interests</code>.</p>
<p><strong>Пример 5-2. Пример записи, которую мы будем кодировать в нескольких бинарных форматах в этой главе</strong></p><pre class="urvanov-syntax-highlighter-plain-tag">{
    "userName": "Martin",
    "favoriteNumber": 1337,
    "interests": ["daydreaming", "hacking"]
}</pre><p>Давайте посмотрим на пример <strong>MessagePack</strong>, бинарного кодирования для JSON. На рисунке 5-2 показана последовательность байт, которую вы получите, если закодируете JSON-документ из Примера 5-2 с помощью <strong>MessagePack</strong>. Первые несколько байт выглядят так:</p>
<ul>
<li>Первый байт, <code>0x83</code>, указывает, что далее идёт объект (старшие четыре бита = <code>0x80</code>) с тремя полями (младшие четыре бита = <code>0x03</code>). (Если вы задаётесь вопросом, что происходит, если объект содержит больше 15 полей, так что число полей не помещается в четыре бита, тогда используется другой индикатор типа, а число полей кодируется в двух или четырёх байтах.)</li>
<li>Второй байт, <code>0xa8</code>, указывает, что далее идёт строка (старшие четыре бита = <code>0xa0</code>), которая имеет длину восемь байт (младшие четыре бита = <code>0x08</code>).</li>
<li>Следующие восемь байт — это имя поля <code>userName</code> в <strong>ASCII</strong>. Так как длина была указана ранее, нет необходимости в каком-либо маркере, чтобы сказать нам, где строка заканчивается (или в экранировании).</li>
<li>Следующие семь байт кодируют шестибуквенное строковое значение <strong>Martin</strong> с префиксом <code>0xa6</code>, и так далее.</li>
</ul>
<p>Бинарное кодирование имеет длину 66 байт, что всего лишь немного меньше, чем 81 байт, занимаемый текстовым <strong>JSON-кодированием</strong> (с удалёнными пробелами). Все бинарные кодирования JSON похожи в этом отношении. Неясно, стоит ли такое небольшое сокращение пространства (и, возможно, ускорение парсинга) потери человекочитаемости.</p>
<p>В следующих разделах мы увидим, как можно добиться гораздо лучших результатов и закодировать ту же запись всего в 32 байта.</p>
<p><strong>Рисунок 5-2. Пример записи (Пример 5-2), закодированной с использованием MessagePack</strong></p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_2-scaled.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2187" src="https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_2-scaled.png" alt="" width="2560" height="2162" srcset="https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_2-scaled.png 2560w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_2-300x253.png 300w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_2-1024x865.png 1024w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_2-768x649.png 768w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_2-1536x1297.png 1536w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_2-2048x1729.png 2048w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_2-450x380.png 450w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_2-780x659.png 780w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_2-1600x1351.png 1600w" sizes="(max-width: 2560px) 100vw, 2560px" /></a></p>
<h2>Protocol Buffers</h2>
<p><strong>Protocol Buffers (protobuf)</strong> — это библиотека бинарного кодирования, разработанная в <strong>Google</strong>. Она похожа на <strong>Apache Thrift</strong>, который был изначально разработан <strong>Facebook</strong>; большинство того, что говорится в этом разделе о <strong>Protocol Buffers</strong>, также применимо к <strong>Thrift</strong>.</p>
<p><strong>Protocol Buffers</strong> требует наличия схемы для любых данных, которые кодируются. Чтобы закодировать данные из Примера 5-2 в <strong>Protocol Buffers</strong>, вы должны описать схему на языке определения интерфейсов (<strong>IDL</strong>) <strong>Protocol Buffers</strong> следующим образом:</p><pre class="urvanov-syntax-highlighter-plain-tag">syntax = "proto3";

message Person {
    string user_name = 1;
    int64 favorite_number = 2;
    repeated string interests = 3;
}</pre><p><strong>Protocol Buffers</strong> поставляется с инструментом генерации кода, который принимает определение схемы, подобное показанному здесь, и генерирует классы, реализующие схему на различных языках программирования. Код вашего приложения может вызывать этот сгенерированный код для кодирования или декодирования записей схемы. Язык схемы очень прост по сравнению с <strong>JSON Schema</strong>: он определяет только поля записей и их типы, но не поддерживает другие ограничения на возможные значения полей.<br />
Кодирование Примера 5-2 с использованием кодировщика <strong>Protocol Buffers</strong> требует 33 байт, как показано на рисунке 5-3.</p>
<p><strong>Рисунок 5-3. Пример записи, закодированной с использованием Protocol Buffers</strong></p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_3-scaled.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2186" src="https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_3-scaled.png" alt="" width="2560" height="1578" srcset="https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_3-scaled.png 2560w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_3-300x185.png 300w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_3-1024x631.png 1024w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_3-768x473.png 768w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_3-1536x947.png 1536w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_3-2048x1262.png 2048w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_3-450x277.png 450w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_3-780x481.png 780w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_3-1600x986.png 1600w" sizes="(max-width: 2560px) 100vw, 2560px" /></a></p>
<p>Аналогично рисунку 5-2, каждое поле имеет аннотацию типа (чтобы указать, является ли оно строкой, целым числом и т. д.) и, где необходимо, указание длины (например, длина строки). Строки, встречающиеся в данных («Martin», «daydreaming», «hacking»), также закодированы как <strong>ASCII</strong> (точнее, UTF-8), аналогично предыдущему примеру.</p>
<p>Большое отличие по сравнению с рисунком 5-2 заключается в том, что здесь нет имён полей (<code>userName</code>, <code>favoriteNumber</code>, <code>interests</code>). Вместо этого закодированные данные содержат теги полей, которые являются числами (1, 2 и 3). Это те самые числа, которые указаны в определении схемы. Теги полей подобны псевдонимам для полей — это компактный способ указать, о каком поле идёт речь, без необходимости явно указывать имя поля.</p>
<p>Как видно, <strong>Protocol Buffers</strong> экономит ещё больше места, упаковывая тип поля и номер тега в один байт. Используются целые числа переменной длины: число 1337 кодируется в два байта, при этом старший бит каждого байта используется для указания, есть ли ещё байты далее. Это означает, что числа от –64 до 63 кодируются в одном байте, числа от –8192 до 8191 — в двух байтах и т. д. Более крупные числа используют больше байт.</p>
<p>Protocol Buffers не имеет явного типа списка или массива. Вместо этого модификатор repeated у поля interests указывает, что поле содержит список значений, а не одно значение. В бинарном кодировании элементы списка представлены просто как повторяющиеся вхождения одного и того же тега поля внутри одной записи.</p>
<h3>Теги полей и эволюция схем</h3>
<p>Мы уже говорили, что схемы неизбежно должны меняться со временем. Мы называем это эволюцией схемы. Как Protocol Buffers обрабатывает изменения схем при сохранении обратной и прямой совместимости?</p>
<p>Как видно из примеров, <strong>закодированная запись</strong> — это просто конкатенация её закодированных полей. Каждое поле идентифицируется по своему номеру тега (числа 1, 2, 3 в примере схемы) и аннотируется типом данных (например, строка или целое число). Если значение поля не установлено, оно просто опускается из закодированной записи. Из этого видно, что теги полей критически важны для смысла закодированных данных. Вы можете изменить имя поля в схеме, так как закодированные данные никогда не ссылаются на имена полей, но вы не можете изменить тег поля, так как это сделает все существующие закодированные данные недействительными.<br />
Вы можете добавлять новые поля в схему, при условии, что вы присвоите каждому полю новый номер тега. Если старый код (который не знает о новых тегах, которые вы добавили) попытается прочитать данные, записанные новым кодом, включая новое поле с номером тега, который он не распознаёт, он может просто проигнорировать это поле. Аннотация типа данных позволяет парсеру определить, сколько байт нужно пропустить, и сохранить неизвестные поля, чтобы избежать проблемы, показанной на рисунке 5-1. Это сохраняет прямую совместимость: старый код может читать записи, которые были записаны новым кодом.</p>
<p><strong>А как насчёт обратной совместимости?</strong> Пока у каждого поля уникальный номер тега, новый код всегда может читать старые данные, потому что номера тегов всё ещё имеют то же значение. Если поле было добавлено в новой схеме, а вы читаете старые данные, которые ещё не содержат этого поля, оно заполняется значением по умолчанию (например, пустой строкой, если тип поля — строка, или нулём, если это число).</p>
<p><strong>Удаление поля</strong> — это то же самое, что добавление поля, только с обратными требованиями по обратной и прямой совместимости. Вы никогда не можете снова использовать тот же номер тега, так как где-то могут существовать данные, записанные с этим старым номером тега, и это поле должно игнорироваться новым кодом. Номера тегов, использовавшиеся в прошлом, можно зарезервировать в определении схемы, чтобы они не были забыты.</p>
<p><strong>А что насчёт изменения типа данных поля?</strong> Это возможно для некоторых типов — подробности см. в документации — но есть риск, что значения будут усечены. Например, допустим, вы изменяете 32-битное целое число на 64-битное. Новый код может легко читать данные, записанные старым кодом, так как парсер может дополнить отсутствующие биты нулями. Однако если старый код читает данные, записанные новым кодом, старый код всё ещё использует 32-битную переменную для хранения значения. Если декодированное 64-битное значение не помещается в 32 бита, оно будет усечено.</p>
<h2>Avro</h2>
<p><strong>Apache Avro</strong> — ещё один бинарный формат кодирования, который примечателен своей отличием от Protocol Buffers. Он был запущен в 2009 году как подпроект Hadoop в результате того, что Protocol Buffers плохо подходил для кейсов использования Hadoop.</p>
<p>Avro также использует схему для задания структуры кодируемых данных. У него есть два языка схем: один (Avro IDL), предназначенный для редактирования человеком, и один (основанный на JSON), который легче читается машиной. Как и в Protocol Buffers, этот язык схемы определяет только поля и их типы, но не сложные правила валидации, как в JSON Schema.</p>
<p>Наша примерная схема, написанная на Avro IDL, может выглядеть так:</p><pre class="urvanov-syntax-highlighter-plain-tag">record Person {
    string               userName;
    union { null, long } favoriteNumber = null;
    array&lt;string&gt;        interests;
}</pre><p>Эквивалентное JSON-представление этой схемы выглядит следующим образом:</p><pre class="urvanov-syntax-highlighter-plain-tag">{
    "type": "record",
    "name": "Person",
    "fields": [
        {"name": "userName",       "type": "string"},
        {"name": "favoriteNumber", "type": ["null", "long"], "default": null},
        {"name": "interests",      "type": {"type": "array", "items": "string"}}
    ]
}</pre><p>Прежде всего, обратите внимание, что в схеме нет номеров тегов. Если мы закодируем нашу примерную запись (Пример 5-2) с использованием этой схемы, бинарное кодирование Avro занимает всего 32 байта — самое компактное из всех рассмотренных нами кодирований. Подробное разбиение последовательности закодированных байтов показано на рисунке 5-4.<br />
Если вы изучите последовательность байтов, вы увидите, что там нет ничего, что идентифицировало бы поля или их типы данных. Кодировка просто состоит из значений, объединённых вместе. Строка — это просто префикс длины, за которым следуют байты UTF-8, но в закодированных данных нет ничего, что указывало бы, что это строка. Это вполне может быть целое число или что-то ещё. Целое число кодируется с использованием кодирования переменной длины.</p>
<p><strong>Рисунок 5-4. Пример записи, закодированной с использованием Avro</strong></p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_4-scaled.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2185" src="https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_4-scaled.png" alt="" width="2560" height="1689" srcset="https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_4-scaled.png 2560w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_4-300x198.png 300w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_4-1024x676.png 1024w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_4-768x507.png 768w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_4-1536x1013.png 1536w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_4-2048x1351.png 2048w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_4-450x297.png 450w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_4-780x515.png 780w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_4-1600x1056.png 1600w" sizes="(max-width: 2560px) 100vw, 2560px" /></a></p>
<p>Чтобы разобрать бинарные данные, вы проходите по полям в том порядке, в котором они указаны в схеме, и используете схему, чтобы определить тип данных каждого поля. Это означает, что бинарные данные могут быть корректно декодированы только в том случае, если код, читающий данные, использует ту же самую схему, что и код, записывающий данные. Любое несоответствие в схеме между читателем и писателем приведёт к некорректно декодированным данным.<br />
Так каким же образом Avro поддерживает эволюцию схем?</p>
<h3>Схема писателя и схема читателя</h3>
<p>Когда приложение хочет закодировать какие-то данные (записать их в файл или базу данных, отправить по сети и т. д.), оно кодирует данные с использованием той версии схемы, о которой ему известно — например, эта схема может быть встроена в приложение. Это называется схемой писателя.</p>
<p>Когда приложение хочет декодировать какие-то данные (прочитать их из файла или базы данных, получить их из сети и т. д.), оно использует две схемы: схему писателя, которая идентична использованной для кодирования, и схему читателя, которая может отличаться. Это показано на рисунке 5-5. Схема читателя определяет поля каждой записи, которые ожидает код приложения, и их типы.</p>
<p><strong>Рисунок 5-5. В Protocol Buffers кодирование и декодирование могут использовать разные версии схемы.</strong> В Avro для декодирования используются две схемы: схема писателя должна быть идентична использованной при кодировании, но схема читателя может быть более старой или новой версией.</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_5-scaled.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2184" src="https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_5-scaled.png" alt="" width="2560" height="1222" srcset="https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_5-scaled.png 2560w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_5-300x143.png 300w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_5-1024x489.png 1024w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_5-768x367.png 768w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_5-1536x733.png 1536w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_5-2048x978.png 2048w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_5-450x215.png 450w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_5-780x372.png 780w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_5-1600x764.png 1600w" sizes="(max-width: 2560px) 100vw, 2560px" /></a></p>
<p>Если схема читателя и схема писателя совпадают, декодирование простое. Если они различаются, Avro устраняет расхождения, сравнивая схему писателя и схему читателя бок о бок и преобразуя данные из схемы писателя в схему читателя. Спецификация Avro точно определяет, как работает это согласование, и это показано на рисунке 5-6.<br />
Например, не проблема, если схема писателя и схема читателя имеют поля в разном порядке, потому что при согласовании полей они сопоставляются по имени. Если код, читающий данные, встречает поле, которое присутствует в схеме писателя, но отсутствует в схеме читателя, оно игнорируется. Если код, читающий данные, ожидает какое-то поле, но схема писателя не содержит поле с таким именем, оно заполняется значением по умолчанию, объявленным в схеме читателя.</p>
<p><strong>Рисунок 5-6. Читатель Avro устраняет различия между схемой писателя и схемой читателя</strong></p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_6-scaled.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2182" src="https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_6-scaled.png" alt="" width="2560" height="918" srcset="https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_6-scaled.png 2560w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_6-300x108.png 300w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_6-1024x367.png 1024w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_6-768x275.png 768w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_6-1536x551.png 1536w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_6-2048x735.png 2048w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_6-450x161.png 450w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_6-780x280.png 780w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_6-1600x574.png 1600w" sizes="(max-width: 2560px) 100vw, 2560px" /></a></p>
<h3>Правила эволюции схем</h3>
<p>В Avro <strong>прямая совместимость (forward compatibility)</strong> означает, что в качестве писателя вы можете использовать новую версию схемы, а в качестве читателя — старую. <strong>Обратная совместимость (backward compatibility)</strong>, напротив, означает, что в качестве читателя вы можете использовать новую версию схемы, а в качестве писателя — старую.</p>
<p>Чтобы сохранить совместимость, вы можете добавлять или удалять только те поля, которые имеют значение по умолчанию. (Поле favoriteNumber в нашей Avro-схеме имеет значение по умолчанию null.) Например, допустим, вы добавляете поле со значением по умолчанию, так что это новое поле существует в новой схеме, но отсутствует в старой. Когда читатель, использующий новую схему, читает запись, созданную со старой схемой, для отсутствующего поля подставляется значение по умолчанию.</p>
<p>Если вы добавите поле без значения по умолчанию, новые читатели не смогут прочитать данные, созданные старыми писателями, и это нарушит обратную совместимость. Если вы удалите поле без значения по умолчанию, старые читатели не смогут прочитать данные, созданные новыми писателями, и это нарушит прямую совместимость.</p>
<p>В некоторых языках программирования null допустим в качестве значения по умолчанию для любой переменной, но в Avro это не так: если вы хотите, чтобы поле могло быть равно null, вы должны использовать объединённый тип (union type). Например,</p><pre class="urvanov-syntax-highlighter-plain-tag">union { null, long, string } field;</pre><p>указывает, что поле может быть числом, строкой или null. Вы можете использовать null в качестве значения по умолчанию только в том случае, если оно является первой ветвью объединения. Это немного более многословно, чем если бы все поля были допускающими null по умолчанию, но такая явность помогает предотвращать ошибки, точно определяя, что может, а что не может быть null.</p>
<p>Изменение типа данных поля возможно, при условии что Avro может преобразовать этот тип. Изменение имени поля также возможно, но немного сложнее: схема читателя может содержать псевдонимы (aliases) для имён полей, чтобы сопоставлять имена полей старой схемы писателя с этими псевдонимами. Это означает, что изменение имени поля совместимо с прошлыми версиями (backward compatible), но не с будущими (not forward compatible). Аналогично, добавление новой ветви в объединённый тип совместимо с прошлыми версиями, но не с будущими.</p>
<h3>Но что такое схема писателя?</h3>
<p>Здесь есть важный вопрос, который мы до сих пор обходили стороной: как читатель узнаёт схему писателя, с которой были закодированы конкретные данные? Мы не можем просто включать полную схему в каждую запись, потому что схема, скорее всего, будет намного больше самих закодированных данных, что сведёт на нет все преимущества экономии места при бинарном кодировании.</p>
<p>Ответ зависит от контекста, в котором используется Avro. Вот несколько примеров:</p>
<ul>
<li><strong>Большой файл с множеством записей</strong><br />
Распространённый вариант использования Avro — хранение большого файла, содержащего миллионы записей, все закодированные с одной и той же схемой. (Мы обсудим такую ситуацию далее.) В этом случае писатель файла может просто включить схему писателя один раз в начале файла. Avro определяет файловый формат (object container files) для этого.</li>
<li><strong>База данных с индивидуально записанными записями</strong><br />
В базе данных разные записи могут быть записаны в разное время с использованием разных схем писателей — нельзя предполагать, что у всех записей будет одинаковая схема. Самое простое решение — включать номер версии в начало каждой закодированной записи и хранить список версий схем в базе данных. Читатель может получить запись, извлечь номер версии, а затем получить схему писателя для этой версии из базы данных. Используя эту схему писателя, он может декодировать остальную часть записи. Например, реестр схем Confluent для Apache Kafka и Espresso от LinkedIn работают именно так.</li>
<li><strong>Передача записей по сетевому соединению</strong><br />
Когда два процесса обмениваются данными по двунаправленному сетевому соединению, они могут согласовать версию схемы при установке соединения, а затем использовать эту схему в течение всего времени соединения. Протокол RPC Avro (см. «Поток данных через сервисы: REST и RPC») работает именно так.</li>
</ul>
<p>База данных версий схем полезна в любом случае, так как она служит документацией и даёт возможность проверить совместимость схем. В качестве номера версии можно использовать простой увеличивающийся целочисленный идентификатор или хэш от самой схемы.</p>
<h3>Динамически генерируемые схемы</h3>
<p>Одним из преимуществ подхода Avro по сравнению с Protocol Buffers является то, что схема не содержит никаких тегов-полей (tag numbers). Но почему это важно? В чём проблема в том, чтобы хранить в схеме несколько чисел?</p>
<p>Разница в том, что Avro лучше подходит для динамически генерируемых схем. Например, представим, что у вас есть реляционная база данных, содержимое которой вы хотите выгрузить в файл, и вы хотите использовать бинарный формат, чтобы избежать упомянутых ранее проблем с текстовыми форматами (JSON, CSV, XML). Если вы используете Avro, то довольно легко можете сгенерировать Avro-схему (в JSON-представлении, которое мы видели ранее) из реляционной схемы и закодировать содержимое базы данных с использованием этой схемы, выгрузив всё это в объектный контейнерный файл Avro. Вы можете сгенерировать схему записи для каждой таблицы базы данных, и каждый столбец станет полем в этой записи. Имя столбца в базе данных сопоставляется с именем поля в Avro.</p>
<p>Теперь, если схема базы данных изменится (например, в таблицу добавят один столбец и удалят другой), вы можете просто сгенерировать новую Avro-схему из обновлённой схемы базы данных и экспортировать данные в новой Avro-схеме. Процесс экспорта данных не должен обращать внимание на изменения схемы — он может просто выполнять преобразование схемы каждый раз при запуске. Любой, кто будет читать новые файлы данных, увидит, что поля записи изменились, но так как поля идентифицируются по имени, обновлённая схема писателя всё равно сможет быть сопоставлена со старой схемой читателя.</p>
<p>Напротив, если бы вы использовали Protocol Buffers для этой цели, теги полей, скорее всего, пришлось бы назначать вручную: каждый раз, когда схема базы данных меняется, администратору пришлось бы вручную обновлять сопоставление между именами столбцов базы данных и тегами полей. (Теоретически это можно автоматизировать, но генератор схем должен был бы очень осторожно следить за тем, чтобы не назначить уже использовавшиеся ранее теги.) Подобные динамически генерируемые схемы просто не были целью проектирования Protocol Buffers, тогда как для Avro это было одной из задач.</p>
<h2>Преимущества схем</h2>
<p>Как мы видели, Protocol Buffers и Avro используют схему для описания формата бинарного кодирования. Их языки схем намного проще, чем XML Schema или JSON Schema, которые поддерживают гораздо более детализированные правила валидации (например, «строковое значение этого поля должно соответствовать этому регулярному выражению» или «целочисленное значение этого поля должно находиться в диапазоне от 0 до 100»). Так как Protocol Buffers и Avro проще реализовать и проще использовать, они получили широкую поддержку во множестве языков программирования.</p>
<p>Идеи, на которых основаны эти кодировки, отнюдь не новые. Например, у них много общего с ASN.1 — языком описания схем, впервые стандартизованным в 1984 году. Он использовался для определения различных сетевых протоколов, и его бинарное кодирование (DER) до сих пор используется, например, для кодирования SSL-сертификатов (X.509). ASN.1 поддерживает эволюцию схем с помощью тегов-полей, аналогично Protocol Buffers. Однако он также очень сложный и плохо документированный, поэтому ASN.1 вряд ли является хорошим выбором для новых приложений.</p>
<p>Многие системы данных также реализуют собственные проприетарные бинарные форматы кодирования для своих данных. Например, большинство реляционных баз данных имеют сетевой протокол, по которому вы можете отправлять запросы в базу данных и получать ответы. Эти протоколы, как правило, специфичны для конкретной базы данных, и вендор базы данных предоставляет драйвер (например, через API ODBC или JDBC), который декодирует ответы из сетевого протокола базы в структуры данных в памяти.</p>
<p>Таким образом, мы видим, что хотя текстовые форматы данных, такие как JSON, XML и CSV, широко распространены, бинарные кодировки, основанные на схемах, также являются жизнеспособным вариантом. У них есть ряд полезных свойств:</p>
<ul>
<li>Они могут быть гораздо более компактными, чем различные «бинарные JSON»-варианты, поскольку могут опускать имена полей из закодированных данных.</li>
<li>Схема является ценным видом документации, и так как схема требуется для декодирования, можно быть уверенным, что она актуальна (в то время как вручную поддерживаемая документация легко может разойтись с реальностью).</li>
<li>Ведение базы данных схем позволяет проверять прямую и обратную совместимость изменений схем до того, как что-либо будет развернуто.</li>
<li>Для пользователей статически типизированных языков программирования возможность генерировать код из схемы полезна, поскольку она позволяет выполнять проверку типов на этапе компиляции.</li>
</ul>
<p>В итоге, эволюция схем обеспечивает такую же гибкость, как и базы данных JSON без схемы/схемой-на-чтение (см. «Гибкость схемы в документной модели»), при этом предоставляя лучшие гарантии для ваших данных и лучшее инструментальное обеспечение.</p>
<h1>Режимы потоков данных</h1>
<p>В начале этой главы мы сказали, что всякий раз, когда вы хотите отправить какие-то данные другому процессу, с которым вы не разделяете память — например, когда вы хотите отправить данные по сети или записать их в файл, — вам нужно закодировать их в виде последовательности байтов. Затем мы обсудили различные способы кодирования для этого.<br />
Мы поговорили о прямой и обратной совместимости, которые важны для эволюционируемости (возможности легко вносить изменения, позволяя обновлять разные части вашей системы независимо и не вынуждая менять всё сразу). Совместимость — это отношение между одним процессом, который кодирует данные, и другим процессом, который их декодирует.</p>
<p>Это довольно абстрактная идея — существует множество способов, которыми данные могут перемещаться от одного процесса к другому. Кто кодирует данные, а кто их декодирует? В оставшейся части этой главы мы рассмотрим некоторые из наиболее распространённых способов, которыми данные передаются между процессами:</p>
<ul>
<li>Через базы данных (см. «Потоки данных через базы данных»)</li>
<li>Через вызовы сервисов (см. «Потоки данных через сервисы: REST и RPC»)</li>
<li>Через движки рабочих процессов (см. «Долговременное выполнение и рабочие процессы»)</li>
<li>Через асинхронные сообщения (см. «Архитектуры, управляемые событиями»)</li>
</ul>
<h2>Потоки данных через базы данных</h2>
<p>В базе данных процесс, который записывает данные в базу, кодирует их, а процесс, который считывает данные из базы, декодирует их. Может существовать всего один процесс, обращающийся к базе данных, и в этом случае читателем будет просто более поздняя версия того же самого процесса — тогда хранение чего-либо в базе можно рассматривать как отправку сообщения самому себе в будущем.<br />
Обратная совместимость здесь явно необходима; в противном случае вы сами в будущем не сможете декодировать то, что записали раньше.</p>
<p>В общем случае несколько различных процессов часто обращаются к базе данных одновременно. Эти процессы могут быть разными приложениями или сервисами, или же просто несколькими экземплярами одного и того же сервиса (работающими параллельно ради масштабируемости или отказоустойчивости). Так или иначе, в среде, где приложение изменяется, вероятно, что некоторые процессы, обращающиеся к базе данных, будут работать на более новой версии кода, а некоторые — на более старой (например, потому что новая версия развёртывается поэтапно, и часть экземпляров уже обновлена, а часть ещё нет).</p>
<p>Это означает, что значение в базе данных может быть записано более новой версией кода, а затем прочитано более старой версией кода, которая всё ещё работает. Таким образом, прямая совместимость также часто требуется для баз данных.</p>
<h3>Разные значения, записанные в разное время</h3>
<p>База данных в целом позволяет обновлять любое значение в любое время. Это означает, что в одной и той же базе у вас могут быть некоторые значения, записанные пять миллисекунд назад, и некоторые значения, записанные пять лет назад.<br />
Когда вы разворачиваете новую версию своего приложения (по крайней мере серверного приложения), вы можете полностью заменить старую версию на новую за несколько минут. Для содержимого базы данных это не так: пятилетние данные всё ещё будут там, в своём исходном кодировании, если вы их явно не переписали с тех пор. Это наблюдение иногда суммируют выражением «данные переживают код».</p>
<p>Переписать (мигрировать) данные в новую схему, безусловно, возможно, но это дорогостоящая операция на больших объёмах данных, поэтому большинство баз данных избегают её, если это возможно. Большинство реляционных баз данных позволяют выполнять простые изменения схемы, например добавлять новый столбец со значением null по умолчанию, без переписывания существующих данных. Когда старая строка считывается, база данных подставляет null для любых столбцов, которых нет в закодированных данных на диске. Таким образом, эволюция схем позволяет всей базе данных выглядеть так, будто она закодирована с помощью одной схемы, даже если в нижележащем хранилище содержатся записи, закодированные с использованием различных исторических версий схемы.</p>
<p>Более сложные изменения схемы — например, изменение однозначного атрибута на многозначный или перенос части данных в отдельную таблицу — всё ещё требуют переписывания данных, часто на уровне приложения. Поддержание прямой и обратной совместимости при таких миграциях остаётся исследовательской проблемой.</p>
<h3>Архивное хранилище</h3>
<p>Возможно, вы время от времени делаете снимок своей базы данных, скажем, для целей резервного копирования или для загрузки в хранилище данных (см. «Хранилище данных»). В этом случае дамп данных, как правило, будет закодирован с использованием последней схемы, даже если исходное кодирование в исходной базе содержало смесь версий схем разных эпох. Так как вы всё равно копируете данные, имеет смысл закодировать их копию последовательно.</p>
<p>Так как дамп данных записывается за один раз и впоследствии является неизменным, такие форматы, как объектные контейнерные файлы Avro, хорошо подходят. Это также хорошая возможность закодировать данные в аналитически-удобном колонко-ориентированном формате, таком как Parquet (см. «Сжатие колонок»).</p>
<h2>Потоки данных через сервисы: REST и RPC</h2>
<p>Когда у вас есть процессы, которым нужно обмениваться данными по сети, существует несколько способов организации такого взаимодействия. Наиболее распространённый вариант предполагает две роли: клиенты и серверы. Серверы предоставляют API по сети, а клиенты могут подключаться к серверам, чтобы делать запросы к этому API. API, предоставляемый сервером, называется сервисом.</p>
<p>Веб работает именно так: клиенты (веб-браузеры) делают запросы к веб-серверам, выполняя GET-запросы для загрузки HTML, CSS, JavaScript, изображений и т. д., и POST-запросы для отправки данных на сервер. API состоит из стандартизированного набора протоколов и форматов данных (HTTP, URL, SSL/TLS, HTML и т. д.). Поскольку веб-браузеры, веб-серверы и авторы сайтов в основном соглашаются с этими стандартами, вы можете использовать любой веб-браузер для доступа к любому сайту (по крайней мере, в теории!).</p>
<p>Веб-браузеры — не единственный тип клиентов. Например, нативные приложения, работающие на мобильных устройствах и настольных компьютерах, часто взаимодействуют с серверами, а клиентские JavaScript-приложения, работающие внутри веб-браузеров, также могут делать HTTP-запросы. В этом случае ответ сервера обычно не является HTML для отображения человеку, а представляет собой данные в кодировке, удобной для дальнейшей обработки клиентским приложением (чаще всего JSON). Хотя HTTP может использоваться как транспортный протокол, API, реализуемый поверх него, является специфичным для приложения, и клиент с сервером должны договориться о деталях этого API.</p>
<p>В некотором смысле сервисы похожи на базы данных: они обычно позволяют клиентам отправлять и запрашивать данные. Однако, в то время как базы данных позволяют выполнять произвольные запросы с использованием языков запросов, которые мы обсуждали в Главе 3, сервисы предоставляют специфичный для приложения API, который позволяет только те входы и выходы, которые заранее определены бизнес-логикой (кодом приложения) сервиса. Это ограничение обеспечивает определённую степень инкапсуляции: сервисы могут накладывать детальные ограничения на то, что клиенты могут и не могут делать.</p>
<p>Ключевая цель проектирования сервис-ориентированной/микросервисной архитектуры — упростить изменение и сопровождение приложения, сделав сервисы независимо развёртываемыми и эволюционируемыми. Общий принцип заключается в том, что каждый сервис должен находиться в ведении одной команды, и эта команда должна иметь возможность часто выпускать новые версии сервиса, не координируясь с другими командами. Следовательно, мы должны ожидать, что старые и новые версии серверов и клиентов будут работать одновременно, и поэтому кодировка данных, используемая серверами и клиентами, должна быть совместима между версиями API сервиса.</p>
<h3>Веб-сервисы</h3>
<p>Когда HTTP используется как базовый протокол для взаимодействия с сервисом, это называется веб-сервисом. Веб-сервисы обычно применяются при построении сервис-ориентированной или микросервисной архитектуры (обсуждалось ранее в «Микросервисы и serverless»). Термин «веб-сервис» — возможно, немного неточен, потому что веб-сервисы используются не только в вебе, но и в ряде других контекстов. Например:</p>
<ul>
<li>клиентское приложение, работающее на устройстве пользователя (например, нативное приложение на мобильном устройстве или веб-приложение на JavaScript в браузере), отправляющее запросы к сервису по HTTP. Эти запросы обычно идут через публичный интернет;</li>
<li>один сервис, отправляющий запросы другому сервису, принадлежащему той же организации, часто находящемуся в том же дата-центре, как часть сервис-ориентированной/микросервисной архитектуры;</li>
<li>один сервис, отправляющий запросы сервису, принадлежащему другой организации, обычно через интернет. Это используется для обмена данными между бэкенд-системами разных организаций. В эту категорию входят публичные API, предоставляемые онлайн-сервисами, такими как системы обработки кредитных карт или OAuth для совместного доступа к пользовательским данным.</li>
</ul>
<p>Наиболее популярная философия проектирования сервисов — REST, которая строится на принципах HTTP. Она делает акцент на простых форматах данных, использовании URL для идентификации ресурсов и использовании функций HTTP для управления кэшированием, аутентификацией и согласованием типа содержимого. API, спроектированный в соответствии с принципами REST, называется RESTful.</p>
<p>Код, которому нужно вызвать API веб-сервиса, должен знать, к какому HTTP-эндпоинту обращаться и какие форматы данных отправлять и ожидать в ответ. Даже если сервис использует принципы RESTful-дизайна, клиентам всё равно нужно каким-то образом узнавать эти детали. Разработчики сервисов часто используют язык описания интерфейсов (IDL), чтобы определить и задокументировать эндпоинты API своего сервиса и модели данных, а также эволюционировать их со временем. Другие разработчики могут затем использовать описание сервиса, чтобы понять, как делать к нему запросы. Два наиболее популярных IDL для сервисов — это <strong>OpenAPI</strong> (также известный как Swagger) и <strong>gRPC</strong>. OpenAPI используется для веб-сервисов, которые отправляют и принимают JSON-данные, в то время как gRPC-сервисы отправляют и принимают Protocol Buffers.</p>
<p>Разработчики обычно пишут описания сервисов OpenAPI в формате <strong>JSON</strong> или <strong>YAML</strong>; см. Пример 5-3. Определение сервиса позволяет разработчикам задавать эндпоинты сервиса, документацию, версии, модели данных и многое другое. Определения gRPC выглядят похоже, но задаются с использованием описаний сервисов на Protocol Buffers.</p>
<p><strong>Пример 5-3. Пример описания сервиса OpenAPI в YAML</strong></p><pre class="urvanov-syntax-highlighter-plain-tag">openapi: 3.0.0
info:
  title: Ping, Pong
  version: 1.0.0
servers:
  - url: http://localhost:8080
paths:
  /ping:
    get:
      summary: Given a ping, returns a pong message
      responses:
        '200':
          description: A pong
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: Pong!</pre><p>Даже если философия проектирования и IDL приняты, разработчикам всё равно нужно написать код, реализующий вызовы API их сервиса. Чтобы упростить эту задачу, часто используется фреймворк сервисов. Фреймворки сервисов, такие как Spring Boot, FastAPI и gRPC, позволяют разработчикам писать бизнес-логику для каждого API-эндпоинта, в то время как код фреймворка обрабатывает маршрутизацию, метрики, кэширование, аутентификацию и так далее. Пример 5-4 показывает пример реализации на Python сервиса, определённого в Примере 5-3.</p>
<p><strong>Пример 5-4. Пример сервиса FastAPI, реализующего определение из Примера 5-3</strong></p><pre class="urvanov-syntax-highlighter-plain-tag">from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI(title="Ping, Pong", version="1.0.0")

class PongResponse(BaseModel):
    message: str = "Pong!"

@app.get("/ping", response_model=PongResponse,
         summary="Given a ping, returns a pong message")
async def ping():
    return PongResponse()</pre><p>Многие фреймворки связывают определения сервисов и серверный код. В некоторых случаях, например с популярным Python-фреймворком FastAPI, серверы пишутся в коде, а IDL генерируется автоматически. В других случаях, например с gRPC, сначала пишется определение сервиса, а затем генерируется каркас серверного кода. Оба подхода позволяют разработчикам генерировать клиентские библиотеки и SDK на различных языках из определения сервиса. Помимо генерации кода, инструменты IDL, такие как Swagger, могут генерировать документацию, проверять совместимость изменений схемы и предоставлять графический интерфейс для разработчиков для выполнения запросов и тестирования сервисов.</p>
<h3>Проблемы удалённых вызовов процедур (RPC)</h3>
<p><strong>Веб-сервисы</strong> — это лишь последняя инкарнация длинной линии технологий для выполнения API-запросов по сети, многие из которых получили много шума, но имеют серьёзные проблемы. Enterprise JavaBeans (EJB) и удалённые вызовы методов Java (RMI) ограничены только Java. Distributed Component Object Model (DCOM) ограничен платформами Microsoft. Common Object Request Broker Architecture (CORBA) чрезмерно сложен и не обеспечивает обратной или прямой совместимости. SOAP и фреймворк веб-сервисов WS-* нацелены на обеспечение взаимодействия между поставщиками, но также страдают от сложности и проблем совместимости.</p>
<p>Все они основаны на идее удалённого вызова процедуры (RPC), существующей с 1970-х годов. Модель RPC пытается сделать так, чтобы запрос к удалённому сетевому сервису выглядел так же, как вызов функции или метода в вашем языке программирования, внутри одного процесса (эта абстракция называется прозрачность расположения). Хотя RPC поначалу кажется удобным, подход фундаментально ошибочен. Сетевой запрос сильно отличается от локального вызова функции:</p>
<p><strong>Локальный вызов функции</strong> предсказуем и либо выполняется успешно, либо завершается сбоем, в зависимости только от параметров, которые находятся под вашим контролем. Сетевой запрос непредсказуем: запрос или ответ может быть потерян из-за сетевой проблемы, удалённая машина может быть медленной или недоступной, и такие проблемы полностью вне вашего контроля. Сетевые проблемы распространены, поэтому нужно предвидеть их, например, повторяя неудачный запрос.</p>
<p><strong>Локальный вызов функции</strong> либо возвращает результат, либо генерирует исключение, либо не возвращает вовсе (например, зацикливается или процесс падает). У сетевого запроса есть ещё один возможный исход: он может завершиться без результата из-за таймаута. В этом случае вы просто не знаете, что произошло: если вы не получили ответ от удалённого сервиса, у вас нет способа понять, дошёл ли запрос или нет.</p>
<p>Если вы повторите неудачный сетевой запрос, может оказаться, что предыдущий запрос всё-таки дошёл, но был потерян только ответ. В этом случае повтор приведёт к выполнению действия несколько раз, если только вы не встроите в протокол механизм дедупликации (идемпотентность) . У локальных вызовов функций такой проблемы нет.</p>
<p>Каждый раз при вызове локальной функции выполнение обычно занимает примерно одно и то же время. Сетевой запрос намного медленнее вызова функции, и его задержка также крайне вариативна: в хорошие моменты он может выполняться менее чем за миллисекунду, но при перегруженной сети или перегруженном удалённом сервисе выполнение того же самого может занять много секунд.</p>
<p>При вызове локальной функции вы можете эффективно передавать ей ссылки (указатели) на объекты в локальной памяти. При выполнении сетевого запроса все эти параметры нужно закодировать в последовательность байтов, которую можно передать по сети. Это допустимо, если параметры — неизменяемые примитивы вроде чисел или коротких строк, но это быстро становится проблематичным при больших объёмах данных и изменяемых объектах.</p>
<p>Клиент и сервис могут быть реализованы на разных языках программирования, поэтому фреймворк RPC должен преобразовывать типы данных из одного языка в другой. Это может выглядеть некрасиво, так как не все языки имеют одинаковые типы — вспомните, например, проблемы JavaScript с числами больше 2⁵³ (см. «JSON, XML и бинарные варианты»). Внутри одного процесса, написанного на одном языке, такой проблемы не существует.</p>
<p>Все эти факторы означают, что нет смысла пытаться сделать так, чтобы удалённый сервис выглядел слишком похожим на локальный объект в вашем языке программирования, потому что это принципиально разные вещи. Часть привлекательности REST заключается в том, что он рассматривает передачу состояния по сети как процесс, отличный от вызова функции.</p>
<h3>Балансировщики нагрузки, обнаружение сервисов и сервисные mesh</h3>
<p><strong>Все сервисы взаимодействуют по сети.</strong> По этой причине клиент должен знать адрес сервиса, к которому он подключается — эта задача известна как обнаружение сервисов. Самый простой подход — настроить клиент на подключение к IP-адресу и порту, на которых работает сервис. Такая конфигурация будет работать, но если сервер отключится, будет перенесён на новую машину или окажется перегружен, клиент придётся перенастраивать вручную.</p>
<p>Чтобы обеспечить более высокую доступность и масштабируемость, обычно запускается несколько экземпляров сервиса на разных машинах, и любой из них может обработать входящий запрос. Распределение запросов между этими экземплярами называется балансировкой нагрузки. Существует множество решений для балансировки нагрузки и обнаружения сервисов:</p>
<p><strong>Аппаратные балансировщики нагрузки</strong> — это специализированное оборудование, устанавливаемое в дата-центрах. Они позволяют клиентам подключаться к одному хосту и порту, а входящие соединения перенаправляются на один из серверов, на которых запущен сервис. Такие балансировщики выявляют сетевые сбои при подключении к downstream-серверу и переключают трафик на другие серверы.</p>
<p><strong>Программные балансировщики нагрузки</strong> работают почти так же, как аппаратные, но не требуют специализированного устройства. Программные балансировщики, такие как Nginx и HAProxy, представляют собой приложения, которые можно установить на стандартный сервер.</p>
<p><strong>Служба доменных имён (DNS)</strong> используется для разрешения доменных имён в Интернете, когда вы открываете веб-страницу. Она поддерживает балансировку нагрузки, позволяя привязать несколько IP-адресов к одному доменному имени. Клиенты могут быть настроены на подключение к сервису по доменному имени вместо IP-адреса, а сетевая подсистема клиента выбирает, какой IP-адрес использовать при подключении. Недостаток этого подхода в том, что DNS изначально спроектирован для распространения изменений с течением времени и кэширования записей. Если серверы часто запускаются, останавливаются или перемещаются, клиенты могут видеть устаревшие IP-адреса, на которых сервис больше не работает.</p>
<p>Системы обнаружения сервисов используют централизованный реестр вместо DNS для отслеживания доступных конечных точек сервиса. Когда запускается новый экземпляр сервиса, он регистрируется в системе обнаружения, указывая хост и порт, на которых слушает, а также соответствующие метаданные, такие как информация о шардировании, расположение дата-центра и другое. Затем сервис периодически отправляет heartbeat-сигнал системе обнаружения, подтверждая, что он всё ещё доступен.</p>
<p>Когда клиент хочет подключиться к сервису, он сначала запрашивает список доступных конечных точек у системы обнаружения, а затем подключается напрямую. По сравнению с DNS, системы обнаружения лучше подходят для динамичной среды, где экземпляры сервисов часто меняются. Кроме того, они дают клиентам больше метаданных о сервисе, что позволяет им принимать более разумные решения по балансировке нагрузки.</p>
<p><strong>Сервисные mesh</strong> — это сложная форма балансировки нагрузки, сочетающая программные балансировщики и системы обнаружения. В отличие от традиционных программных балансировщиков, работающих на отдельной машине, балансировщики в сервисной mesh обычно разворачиваются как встроенная клиентская библиотека или как процесс/«sidecar»-контейнер как на стороне клиента, так и на стороне сервера. Клиентские приложения подключаются к своему локальному балансировщику сервиса, который соединяется с балансировщиком на стороне сервера. Оттуда соединение маршрутизируется в локальный серверный процесс.</p>
<p>Хотя такая топология сложна, она имеет ряд преимуществ. Так как клиенты и серверы подключаются только через локальные соединения, шифрование соединений может полностью обрабатываться на уровне балансировщика. Это избавляет клиентов и серверы от необходимости разбираться в сложностях SSL-сертификатов и TLS. Mesh-системы также обеспечивают развитую наблюдаемость: они могут отслеживать, какие сервисы вызывают друг друга в реальном времени, выявлять сбои, фиксировать нагрузку трафика и многое другое.</p>
<p>Выбор подходящего решения зависит от потребностей организации. В очень динамичных средах с оркестратором, таким как Kubernetes, часто используют сервисные mesh, например Istio или Linkerd. Специализированная инфраструктура, такая как базы данных или системы обмена сообщениями, может требовать собственных специализированных балансировщиков. Более простые развёртывания лучше всего работают с программными балансировщиками нагрузки.</p>
<h3>Кодирование данных и эволюция для RPC</h3>
<p>Для эволюционности важно, чтобы RPC-клиенты и серверы могли изменяться и развёртываться независимо друг от друга. В отличие от потока данных через базы данных (описанных в предыдущем разделе), в случае потока данных через сервисы можно упростить задачу: разумно предположить, что сначала будут обновлены все серверы, а затем все клиенты. Таким образом, нужна только обратная совместимость для запросов и прямая совместимость для ответов.</p>
<p><strong>Свойства обратной и прямой совместимости схемы RPC наследуются от используемого формата кодирования:</strong></p>
<ul>
<li>gRPC (Protocol Buffers) и Avro RPC могут эволюционировать в соответствии с правилами совместимости их форматов кодирования.</li>
<li>RESTful API чаще всего используют JSON для ответов и JSON или URI-encoded/form-encoded параметры запроса для запросов. Добавление необязательных параметров запроса и новых полей в объекты ответов обычно считается изменениями, сохраняющими совместимость.</li>
</ul>
<p>Совместимость сервисов усложняется тем, что RPC часто используется для взаимодействия между организациями, и поставщик сервиса зачастую не имеет контроля над своими клиентами и не может заставить их обновиться. Поэтому совместимость должна сохраняться долго, возможно, бесконечно. Если требуется несовместимое изменение, поставщик сервиса часто вынужден поддерживать несколько версий API сервиса одновременно.</p>
<p>Нет единого соглашения о том, как должно работать версионирование API (то есть как клиент может указать, какую версию API он хочет использовать). Для RESTful API распространённые подходы — использовать номер версии в URL или в HTTP-заголовке Accept. Для сервисов, которые используют API-ключи для идентификации конкретного клиента, есть ещё один вариант: хранить запрашиваемую клиентом версию API на сервере и позволять обновлять этот выбор версии через отдельный административный интерфейс.</p>
<h2>Долговременное выполнение и рабочие процессы</h2>
<p>По определению, архитектуры на основе сервисов включают несколько сервисов, каждый из которых отвечает за разные части приложения. Рассмотрим приложение для обработки платежей, которое списывает деньги с кредитной карты и зачисляет средства на банковский счёт. Такая система, вероятно, будет иметь отдельные сервисы, отвечающие за обнаружение мошенничества, интеграцию с кредитными картами, интеграцию с банками и так далее.</p>
<p>Обработка одного платежа в нашем примере требует множества вызовов сервисов. Сервис процессинга платежей может вызвать сервис обнаружения мошенничества для проверки, затем вызвать сервис кредитных карт для списания средств, а затем вызвать банковский сервис для зачисления средств, как показано на Рисунке 5-7. Мы называем эту последовательность шагов рабочим процессом (workflow), а каждый шаг — задачей (task). Рабочие процессы обычно определяются как граф задач. Определения рабочих процессов могут быть написаны на языке общего назначения, на предметно-ориентированном языке (DSL) или на языке разметки, таком как Business Process Execution Language (BPEL).</p>
<h3>ЗАДАЧИ, АКТИВНОСТИ И ФУНКЦИИ</h3>
<p>Разные движки рабочих процессов используют разные названия для задач. Temporal, например, использует термин activity (активность). Другие называют задачи durable functions (долговременные функции). Хотя названия различаются, концепции остаются одинаковыми.</p>
<p><strong>Рисунок 5-7. Пример рабочего процесса, выраженного с использованием Business Process Model and Notation (BPMN) — графической нотации</strong></p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_7-scaled.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2183" src="https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_7-scaled.png" alt="" width="2560" height="693" srcset="https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_7-scaled.png 2560w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_7-300x81.png 300w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_7-1024x277.png 1024w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_7-768x208.png 768w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_7-1536x416.png 1536w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_7-2048x555.png 2048w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_7-450x122.png 450w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_7-780x211.png 780w, https://datatalks.ru/wp-content/uploads/2025/09/chapter_5_image_5_7-1600x433.png 1600w" sizes="(max-width: 2560px) 100vw, 2560px" /></a></p>
<p>Рабочие процессы запускаются или выполняются движком рабочих процессов. Движки рабочих процессов определяют, когда запускать каждую задачу, на какой машине задача должна выполняться, что делать, если задача завершилась сбоем (например, если машина вышла из строя во время выполнения задачи), сколько задач допускается выполнять параллельно и многое другое.</p>
<p>Обычно движки рабочих процессов состоят из оркестратора и исполнителя. Оркестратор отвечает за планирование задач для выполнения, а исполнитель отвечает за выполнение задач. Выполнение начинается, когда рабочий процесс запускается. Оркестратор инициирует сам рабочий процесс, если пользователи определили расписание, основанное на времени, например выполнение каждый час. Также запуск выполнения рабочего процесса могут инициировать внешние источники, такие как веб-сервис или даже человек. После запуска вызываются исполнители для выполнения задач.</p>
<p>Существует множество видов движков рабочих процессов, которые решают разные задачи. Некоторые, такие как Airflow, Dagster и Prefect, интегрируются с системами данных и оркестрируют ETL-задачи. Другие, такие как Camunda и Orkes, предоставляют графическую нотацию для рабочих процессов (например, BPMN, используемую на рисунке 5-7), чтобы не-инженеры могли проще определять и выполнять рабочие процессы. Третьи, такие как Temporal и Restate, обеспечивают долговременное выполнение.</p>
<h3>Долговременное выполнение</h3>
<p>Фреймворки долговременного выполнения стали популярным способом построения архитектур на основе сервисов, которым требуется транзакционность. В нашем примере с платежами мы хотим обработать каждый платёж ровно один раз. Сбой во время выполнения рабочего процесса может привести к списанию с кредитной карты без соответствующего зачисления средств на банковский счёт. В архитектуре на основе сервисов мы не можем просто обернуть эти две задачи в транзакцию базы данных. Более того, мы можем взаимодействовать с внешними платёжными шлюзами, над которыми у нас ограниченный контроль.</p>
<p><strong>Фреймворки долговременного выполнения</strong> — это способ обеспечить семантику «ровно один раз» для рабочих процессов. Если задача завершается сбоем, фреймворк перезапустит задачу, но пропустит любые RPC-вызовы или изменения состояния, которые задача успешно выполнила до сбоя. Вместо этого фреймворк «притворится», что совершает вызов, но вернёт результаты из предыдущего вызова. Это возможно потому, что фреймворки долговременного выполнения записывают все RPC и изменения состояния в надёжное хранилище, например в журнал предварительной записи (write-ahead log).</p>
<p><strong>Пример 5-5. Фрагмент определения рабочего процесса в Temporal для платёжного процесса, показанного на рисунке 5-7.</strong></p><pre class="urvanov-syntax-highlighter-plain-tag">@workflow.defn
class PaymentWorkflow:
    @workflow.run
    async def run(self, payment: PaymentRequest) -&gt; PaymentResult:
        is_fraud = await workflow.execute_activity(
            check_fraud,
            payment,
            start_to_close_timeout=timedelta(seconds=15),
        )
        if is_fraud:
            return PaymentResultFraudulent
        credit_card_response = await workflow.execute_activity(
            debit_credit_card,
            payment,
            start_to_close_timeout=timedelta(seconds=15),
        )
        # ...</pre><p>Фреймворки, такие как Temporal, не лишены своих сложностей. Внешние сервисы, такие как сторонний платёжный шлюз в нашем примере, всё равно должны предоставлять идемпотентный API. Разработчики должны помнить о необходимости использовать уникальные идентификаторы для этих API, чтобы предотвратить повторное выполнение. И поскольку фреймворки долговременного выполнения логируют каждый RPC-вызов по порядку, они ожидают, что последующее выполнение будет совершать те же RPC-вызовы в том же порядке. Это делает изменения в коде хрупкими. Вы можете внести неопределённое поведение просто изменив порядок вызовов функций.</p>
<p>Аналогично, поскольку фреймворки долговременного выполнения ожидают воспроизведения всего кода детерминированным образом (одни и те же входные данные дают одни и те же выходные), недетерминированный код, такой как генераторы случайных чисел или системные часы, является проблемой. Фреймворки часто предоставляют собственные, детерминированные реализации таких библиотечных функций, но нужно помнить, чтобы именно их использовать. В некоторых случаях, таких как инструмент workflowcheck в Temporal, фреймворки предоставляют статические анализаторы, чтобы определить, введено ли недетерминированное поведение.</p>
<hr />
<p><strong>ПРИМЕЧАНИЕ</strong><br />
Сделать код детерминированным — мощная идея, но трудно реализуемая надёжным образом.</p>
<hr />
<h2>Архитектуры, управляемые событиями</h2>
<p>В этом заключительном разделе мы кратко рассмотрим архитектуры, управляемые событиями, которые представляют собой ещё один способ передачи закодированных данных от одного процесса к другому. Запрос называется событием или сообщением; в отличие от RPC, отправитель обычно не ждёт, пока получатель обработает событие. Более того, события, как правило, не отправляются получателю через прямое сетевое соединение, а проходят через посредника, называемого брокером сообщений (также event broker, message queue или message-oriented middleware), который временно сохраняет сообщение.</p>
<p><strong>Использование брокера сообщений имеет несколько преимуществ по сравнению с прямым RPC:</strong></p>
<ul>
<li>Он может выступать в роли буфера, если получатель недоступен или перегружен, тем самым повышая надёжность системы.</li>
<li>Он может автоматически повторно доставлять сообщения процессу, который вышел из строя, и тем самым предотвращает их потерю.</li>
<li>Он устраняет необходимость в обнаружении сервисов, так как отправителям не нужно напрямую подключаться к IP-адресу получателя.</li>
<li>Он позволяет отправить одно и то же сообщение нескольким получателям.</li>
<li>Он логически отделяет отправителя от получателя (отправитель просто публикует сообщения и не заботится о том, кто их потребляет).</li>
</ul>
<p>Общение через брокера сообщений является асинхронным: отправитель не ждёт доставки сообщения, а просто отправляет его и забывает. Однако можно реализовать синхронную модель, похожую на RPC, если заставить отправителя ждать ответа на отдельном канале.</p>
<h3>Брокеры сообщений</h3>
<p>В прошлом рынок брокеров сообщений был под контролем коммерческого корпоративного ПО от компаний, таких как TIBCO, IBM WebSphere и webMethods, прежде чем популярность приобрели открытые реализации, такие как RabbitMQ, ActiveMQ, HornetQ, NATS и Apache Kafka. Совсем недавно получили распространение облачные сервисы, такие как Amazon Kinesis, Azure Service Bus и Google Cloud Pub/Sub.</p>
<p>Подробная семантика доставки зависит от реализации и конфигурации, но в целом чаще всего используются два шаблона распределения сообщений:</p>
<p>Один процесс добавляет сообщение в именованную очередь, и брокер доставляет это сообщение потребителю этой очереди. Если потребителей несколько, сообщение получает один из них.</p>
<p>Один процесс публикует сообщение в именованную тему, и брокер доставляет это сообщение всем подписчикам этой темы. Если подписчиков несколько, сообщение получает каждый.</p>
<p>Брокеры сообщений, как правило, не навязывают какой-либо конкретной модели данных — сообщение представляет собой просто последовательность байтов с некоторыми метаданными, поэтому можно использовать любой формат кодирования. Общий подход — использовать Protocol Buffers, Avro или JSON, а вместе с брокером сообщений развёртывать реестр схем, чтобы хранить все допустимые версии схем и проверять их совместимость. Также можно использовать AsyncAPI, эквивалент OpenAPI для обмена сообщениями, чтобы задавать схему сообщений.</p>
<p>Брокеры сообщений различаются по степени долговечности хранения сообщений. Многие записывают сообщения на диск, чтобы они не были потеряны в случае сбоя брокера или необходимости его перезапуска. В отличие от баз данных, многие брокеры сообщений автоматически удаляют сообщения после того, как они были потреблены. Некоторые брокеры можно настроить на хранение сообщений бессрочно — это требуется, если вы хотите использовать event sourcing (см. “Event Sourcing and CQRS”).</p>
<p>Если потребитель публикует сообщения повторно в другую тему, возможно, потребуется позаботиться о сохранении неизвестных полей, чтобы предотвратить проблему, описанную ранее в контексте баз данных (рисунок 5-1).</p>
<h3>Распределённые акторные фреймворки</h3>
<p><strong>Модель акторов</strong> — это модель программирования для организации параллелизма в рамках одного процесса. Вместо того чтобы работать напрямую с потоками (и связанными с ними проблемами гонок, блокировок и взаимоблокировок), логика инкапсулируется в акторах. Каждый актор обычно представляет одного клиента или сущность, может иметь некоторое локальное состояние (которое не разделяется ни с какими другими акторами) и обменивается сообщениями с другими акторами посредством отправки и получения асинхронных сообщений. Доставка сообщений не гарантируется: в определённых сценариях ошибок сообщения будут потеряны. Так как каждый актор обрабатывает только одно сообщение за раз, ему не нужно беспокоиться о потоках, и каждый актор может планироваться независимо фреймворком.</p>
<p>В распределённых акторных фреймворках, таких как Akka, Orleans и Erlang/OTP, эта модель программирования используется для масштабирования приложения на несколько узлов. Механизм передачи сообщений используется тот же, независимо от того, находятся ли отправитель и получатель на одном узле или на разных. Если они находятся на разных узлах, сообщение прозрачно кодируется в последовательность байтов, отправляется по сети и декодируется на другой стороне.</p>
<p>Прозрачность расположения работает лучше в акторной модели, чем в RPC, потому что акторная модель уже предполагает, что сообщения могут быть потеряны даже в пределах одного процесса. Хотя задержка по сети, вероятно, выше, чем в пределах одного процесса, при использовании акторной модели существует меньше фундаментального несоответствия между локальной и удалённой коммуникацией.</p>
<p>Распределённый акторный фреймворк по сути объединяет брокер сообщений и модель акторов в единый фреймворк. Однако если вы хотите выполнять пошаговые обновления вашего акторного приложения, вам всё равно нужно учитывать прямую и обратную совместимость, поскольку сообщения могут отправляться с узла, работающего на новой версии, на узел со старой версией, и наоборот. Это можно реализовать с помощью одного из форматов кодирования, рассмотренных в этой главе.</p>
<h1>Резюме по главе 5</h1>
<p>В этой главе мы рассмотрели несколько способов преобразования структур данных в байты в сети или байты на диске. Мы увидели, что детали этих кодировок влияют не только на их эффективность, но, что более важно, на архитектуру приложений и ваши возможности по их развитию.</p>
<p>В частности, многим сервисам необходимо поддерживать пошаговые обновления, при которых новая версия сервиса развёртывается постепенно на нескольких узлах, а не сразу на всех. Пошаговые обновления позволяют выпускать новые версии сервиса без простоев (тем самым поощряя частые небольшие релизы вместо редких крупных) и делают развёртывания менее рискованными (так как ошибочные релизы могут быть обнаружены и откатаны до того, как они затронут большое количество пользователей). Эти свойства чрезвычайно полезны для эволюционности — простоты внесения изменений в приложение.</p>
<p>Во время пошаговых обновлений или по разным другим причинам мы должны предполагать, что разные узлы запускают разные версии кода нашего приложения. Таким образом, важно, чтобы все данные, циркулирующие в системе, кодировались таким образом, чтобы обеспечивать обратную совместимость (новый код может читать старые данные) и прямую совместимость (старый код может читать новые данные).</p>
<p>Мы обсудили несколько форматов кодирования данных и их свойства совместимости:</p>
<ul>
<li>Зависимые от языка программирования кодировки ограничены одним языком и часто не обеспечивают прямой и обратной совместимости.</li>
<li><strong>Текстовые форматы</strong> вроде JSON, XML и CSV широко распространены, и их совместимость зависит от того, как именно вы их используете. У них есть необязательные языки схем, которые иногда помогают, а иногда мешают. Эти форматы несколько расплывчаты в отношении типов данных, поэтому нужно быть осторожным с числами и бинарными строками.</li>
<li><strong>Бинарные форматы</strong>, управляемые схемами, такие как Protocol Buffers и Avro, позволяют выполнять компактное и эффективное кодирование с чётко определёнными правилами прямой и обратной совместимости. Схемы могут быть полезны для документации и генерации кода в статически типизированных языках. Однако у этих форматов есть недостаток: данные нужно декодировать, прежде чем они станут читаемыми человеком.</li>
</ul>
<p>Мы также обсудили несколько моделей передачи данных, показывающих различные сценарии, в которых кодировки данных имеют значение:</p>
<ul>
<li>Базы данных, где процесс записи в базу кодирует данные, а процесс чтения из базы их декодирует.</li>
<li>RPC и REST API, где клиент кодирует запрос, сервер декодирует запрос и кодирует ответ, а клиент в конце концов декодирует ответ.</li>
<li>Архитектуры, управляемые событиями (с использованием брокеров сообщений или акторов), где узлы обмениваются сообщениями, закодированными отправителем и декодированными получателем.</li>
</ul>
<p>Мы можем заключить, что при должном внимании обратная/прямая совместимость и пошаговые обновления вполне достижимы. Пусть эволюция вашего приложения будет быстрой, а развёртывания — частыми.</p>
<p>Сообщение <a href="https://datatalks.ru/chapter-5-encoding-and-evolution/">Глава 5. Кодирование и Эволюция (Encoding and Evolution)</a> появились сначала на <a href="https://datatalks.ru">DataTalks.RU. Data Engineering / DWH / Data Pipeline</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://datatalks.ru/chapter-5-encoding-and-evolution/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
