Заметки 0.1x инженера
53 subscribers
4 photos
13 links
Download Telegram
Мигрень

Большую часть моей взрослой жизни меня преследуют пресловутые мигрени.

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

А потом приступы стали очень частыми и сильными — по 12–15 раз в месяц. И с этим я уже не мог мириться, посему через пару месяцев пошел к неврологу.

Примерно тогда я узнал, что у меня хроническая мигрень и что лечится она разными способами:
- таблетки: антидепрессанты, противоэпилептические препараты;
- работа с триггерами мигрени: выстраивание в жизни work-life баланса, определенности и постоянства;
- уколы: моноклональные антитела и ботокс.

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

Что это значит?

На самом деле мигрень — это реакция мозга на неожиданные изменения.

Предположим, вы всегда обедаете в плюс-минус одно время, а тут вам вдруг пришлось пообедать позже из-за встречи — это уже явный триггер для мигрени.

По своей сути мигрень как бы намекает: живи размеренной slow life, в которой каждый день похож на предыдущий.

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

Такие дела.
💊6
О Jupyter

На работе мне часто приходится иметь дело с JupyterLab'ом и экосистемой вокруг него.

Мы с командой строим Managed JupyterLab as a Service в компании.
По сути, мы предоставляем возможность поднять свой собственный инстанс JupyterLab'а по кнопке.

Сам JupyterLab широко известен как в научных кругах, так и в DS/ML-тусовке. По сути, это инструмент для интерактивных вычислений, работы с данными и хранения результатов работы кода вместе с ним.

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

Всё это невероятно удобно и работает с разными языками: Python, Ruby, JS, Scala, R, даже Go и C++. Почти для каждого языка найдётся готовый кернел.

К слову, JupyterLab — это лишь один из инструментов, помогающих сделать работу с данными и кодом удобнее. Я бы точно сказал, что он подходит не всем и не во всех случаях.

А какие инструменты вы используете для экспериментов, работы с данными или быстрых прототипов? Есть ли что-то, без чего вы не можете обойтись в своей работе?

#jupyterlab
🔥31
Пропадающие результаты вычислений в Jupyter

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

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

Бесит, не правда ли? Хочется пойти и наорать на тех ребят, которые делают эту штуку. Ведь теперь придётся запускать всё заново и снова ждать кучу времени, чтобы получить результат.

Для того чтобы разобраться с проблемой, мне в какой-то момент пришлось погрузиться в исходники JupyterLab UI и jupyter-server.

Что я ожидал увидеть с точки зрения моего профессионального опыта? Я ожидал, что код из ячейки отправляется на сервер, тот сохранит его у себя в каком-то хранилище, асинхронно отправит в Kernel и так же асинхронно получит из Kernel результаты. Примерно так, как это выглядит на первой картинке: схема, похожая на классическую трёхзвенную архитектуру, где каждое звено общается только со следующим и не имеет права перескакивать через уровни.

Но внезапно оказалось, что jupyter-server вообще не отвечает за исполнение кода и нигде его не сохраняет. Более того, он даже не в курсе, что отсылается в Kernel и что Kernel отдаёт в ответах. Сам jupyter-server, по сути, делает две вещи:
1. Запускает Kernel и создаёт к нему соединение.
2. Поднимает WebSocket-прокси, через который уже Jupyter UI (🤬) будет общаться напрямую с Kernel.

Собственно, в этот момент весь пазл для меня сложился. Тут есть явное нарушение коммуникации между архитектурными слоями, которое, по сути, и приводит к проблеме. Чем плоха схема, реализованная создателями JupyterLab?

1. Любое отсоединение клиента или помехи на сети будут приводить к потере соединения с Kernel.
2. Сам session ID, который определяет канал общения с Kernel, не сохраняется между перезапусками UI, а после каждого перезапуска, UI ещё и генерирует новый ().
3. В целом получается, что UI должен знать протокол Kernel, уметь общаться по ZMQ/WS, что создаёт высокую связанность компонентов в системе.

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

Конечно, проблема не новая, и, как оказалось, в jupyter-server даже есть issue на эту тему. Я покопал его дальше и нашёл следующие интересные моменты:

- jupyter-server-nbmodel — расширение для jupyter-server от datalayer.tech, которое подменяет Executor кода в UI-части и добавляет в серверную часть функциональность с персистированием результатов выполнения и сессий.

- issuecomment#1657944667 — комментарий о том, что в новой реализации jupyter-server (которая, к слову, ещё и не совместима со старыми расширениями) команда, которая делает Jupyter, решила пойти ещё дальше и не только производить вычисления на backend-части, но и записывать результаты напрямую в ipynb тоже на backend-части (да-да, всё это сейчас делает UI).

И естественно, чтобы соблюдать консистентность при записи, они решили использовать CRDT (👀 их обычно используют для Google Docs-like коллаборации).

А всех этих приколов можно было избежать, если сразу делать исполнение кода через backend и не нарушать архитектурные границы компонентов. Такие дела

#jupyterlab #architecture
Please open Telegram to view this post
VIEW IN TELEGRAM
Please open Telegram to view this post
VIEW IN TELEGRAM
4👍1
Формат ipynb в Jupyter

Раньше я писал про Jupyter в разрезе архитектурных решений в классическом плане, но есть в нем и некоторые, о которых начинаешь задумываться только когда возникает потребность.

Снова попробую начать с проблемы.

Представьте: вы пишете код в Jupyter, всё работает, результаты выводятся, ничего не предвещает беды. И тут, в момент, когда вы нажимаете заветное Ctrl+S, вам вылетает ошибка — "Не удалось сохранить файл". Как это так, думаете вы, я же сделал изменения всего в одной ячейке в коде...

А ноги у этой проблемы растут на стыке концепции ipynb-формата и инфраструктурных ограничений.

Когда вы работаете в браузере — за UI и всеми запросами на сервер в 99,9% случаев будет скрываться какой-либо балансер, предположим, это NGINX. У балансера есть свои ограничения на то, какие запросы и как он обработает: чаще всего это таймаут запроса и максимальный размер запроса.

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

И вот мы и приходим к root-cause проблемы — ipynb-файл с кодом по объёму оказывается либо больше ограничения балансера, либо загружается на сервер дольше, чем таймаут выставленный на балансере.

Но я же изменил только малую часть файла, почему не отправлять diff, спросите вы? И будете правы с вопросом, вот только JupyterLab UI на каждое сохранение отправляет весь ipynb-файл.

Собственно, у проблемы нет внятного решения:

- Либо крутить настройки балансировщика — что на самом деле не решение проблемы, а её замалчивание.
- Либо просить пользователей не сохранять большие результаты в ipynb — что опять не решает проблему.
- Либо переделывать в JupyterLab и Jupyter Server работу с сохранением и исполнением ipynb — самый правильный, долгий и не факт, что реализуемый вариант.

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

Такие дела.

#jupyterlab
5
Системный подход к решению проблем

Сегодня я хочу поделиться с вами невероятно крутой, на мой взгляд, статьей о том, как инженер из Google искал root-cause проблемы с SIGSEGV в node_exporter. Небольшой спойлер: проблема оказалась в стандартной библиотеке Go на стыке работы с Linux Kernel.

Но сейчас я хочу написать не о проблеме в Golang или багах в разработке. Меня в этой статье поразило другое: то, как системно инженер подошёл к решению проблемы, как он находил выход из сложных ситуаций, чтобы сэкономить время, и как расследование проблем с hardware переросло в поиск реальной проблемы в рантайме Golang.

Мне кажется, что именно системный подход делает любого инженера с базовыми знаниями и достаточной любознательностью 10x инженером. Это как раз то, чему нельзя просто взять и научить. Я думаю, это тот самый навык, который развивается у каждого со временем — у кого-то в большей степени, у кого-то в меньшей.

Но, помимо системного подхода, меня поразил объём различных областей знаний, которые пришлось задействовать, чтобы докопаться до реального root-cause и не остановиться в тупике.

Представьте: от SIGSEGV в бинарнике дойти до нагрева DRAM компьютера, оттуда — до QEMU VM, сборки initramfs и воспроизведения проблемы, чтобы в итоге собрать десятки различных вариаций Linux Kernel и GCC, докопавшись до vDSO и рантайма Golang.

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

Такие дела.
4
А вы точно знаете как работает память?

Сегодня хочу рассказать об одной детали, навеянной предыдущим постом. Мы, как software developers, часто совсем не задумываемся о hardware-части — она предоставляет нам некоторый контракт, и мы с ним работаем. С RAM-памятью в компьютерах всё точно так же: на уровне контракта мы ожидаем, что в память можно записать какое-то значение и затем прочитать его обратно. Но так ли это на самом деле?

Короткий ответ: "Да, но не всегда".

А теперь попробую объяснить, почему в ответе появилось это самое «не всегда» и что за ним скрывается. Представьте, что вы хотите создать ячейку памяти. Для этого придётся разобраться в электротехнике и физике, понять, как работают транзисторы и как с их помощью можно реализовать память. И вот тут начинаются интересные особенности реального мира.

Для простоты будем считать, что память — это одна ячейка, в которую можно записать что-то и затем прочитать это же значение.

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

А вот чтобы прочитать эту единицу, придётся считать заряд с конденсатора. Но тут есть нюанс: операция чтения разрушает заряд. Пока мы считываем данные, заряд уходит, и чтобы восстановить значение, нам придётся снова подать напряжение.

И вот тут скрывается один из подводных камней. На физическом уровне чтение из памяти — это уничтожающая операция.

Кроме того, заряд на конденсаторе постепенно рассеивается, если его не поддерживать. То есть у нас больше нет гарантии, что через какое-то время мы сможем прочитать ровно то, что записали.

Чтобы этого избежать, используются два подхода:

- Error Correction Codes (ECC)
- Регулярная перезапись памяти блоками

Со вторым всё понятно: мы просто обновляем заряды на конденсаторах, чтобы они не исчезли. А вот первый метод защищает нас в тех случаях, когда заряды всё же «убегают».

ECC бывает двух видов:
- Программный (software) — коррекция ошибок на уровне ядра
- Аппаратный (hardware) — в саму память встраиваются дополнительные транзисторы и механизмы исправления ошибок. Такая память способна восстановить данные, если в одном машинном слове (32 или 64 бита) произошла потеря или ошибка одного бита.

Моё напутствие коллегам: если у вас случился SEGFAULT, то в очень редких случаях (например, из-за высоких температур или космических лучей) виноват может быть и hardware.

Такие дела.
🔥81
Лог данных

Я тут решил начать рассказывать о концептах, которые используются при построении распределенных систем. Сегодня расскажу о концепции лога данных. Не путать с обычным логированием в приложениях!

Что такое лог данных?

Лог данных — это последовательность записей с двумя ключевыми свойствами:

1. Ordering (упорядоченность) — записи в логе всегда следуют в строго определённом порядке.
2. Append Only — новые записи можно только добавлять, но нельзя изменять или удалять уже существующие.

Зачем это нужно?

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

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

- Лог содержит три операции: +12, *2, -16.
- Если изменить порядок выполнения (*2 → +12 → -16), итоговый результат будет другим.

Аналогично, если бы лог не был Append Only и из него удалили операцию, один исполнитель мог бы применить её, а другой — нет. Это нарушило бы согласованность.

Как это работает в реальном мире?

Если у нас один сервер, задача довольно проста: мы просто записываем данные в лог в определённом порядке. Например, можно использовать mutex при записи в файл.

Так работают традиционные реляционные СУБД:

- PostgreSQL хранит WAL (Write-Ahead Log) — в него сначала записываются все изменения, а затем пользователю подтверждают их выполнение.
- MySQL использует BinLog, выполняя схожую функцию.

В следующем посте расскажу, как эти свойства обеспечиваются уже в распределённой среде.
👍61
Лог данных - обеспечиваем надежность

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

В чём проблема с одним сервером?

В первую очередь нас поджидает проблема Single Point of Failure. Если сервер с логом выйдет из строя, мы потеряем к нему доступ. А если произойдёт сбой диска (HDD/SSD), данные могут быть утеряны полностью.

Бэкапы — не панацея

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

Этот вариант подходит не всегда. Чтобы минимизировать потери, можно записывать изменения не периодически, а в реальном времени. И тут мы подходим к репликации данных.

Репликация данных

Репликация — это дублирование данных на несколько серверов одновременно. В зависимости от потока передаваемых данных, она бывает двух типов:
1. Логическая — дублируются операции в том же порядке, благодаря чему реплика получает то же состояние, что и основной сервер.
2. Физическая — передаются не сами операции, а их результаты, фактически формируя лог переходов состояния.

С точки зрения способа записи, репликация бывает:
1. Синхронная — перед подтверждением операции клиенту данные сначала записываются на все реплики.
2. Асинхронная — данные отправляются на реплики в фоновом режиме.

Плюсы и минусы разных типов репликации

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

Кроме того, при сбоях синхронной репликации важно уметь откатывать частично применённые изменения.

Как решаются эти проблемы и какие ещё есть trade-offs при проектировании распределённых систем — расскажу в следующих постах.
👍73
Что-то в последнее время я редко пишу в канал — постараюсь исправить. А пока хочу завершить выходные постом, который давно собирался написать.

Некоторые из вас знают, что я работаю в команде ML-платформы в Т-Банке. Этот пост как раз про ML, AI и копилотов.

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

Поэтому я убеждён, что копилоты не заменят программистов, пока распределённые системы проектируются так, как сейчас.

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

Единственные рабочие варианты — либо жёстко ограничивать контекст (например, через монорепу и Cursorrules), либо объяснять всё в промптах вручную. Но тогда inline-подсказки копилотов превращаются в бесполезный шум.

Недавно как раз наткнулся на пост, который разбирает эту тему ещё глубже - https://www.shadaj.me/writing/distributed-programming-stalled
👍6
Optimistic vs Pessimistic Concurrency

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

А вот с оптимистичными — долгое время было непонятно, в чём суть. Название вроде бы говорит о чём-то «позитивном», но что это значит на практике — было неясно.

Недавно я наконец наткнулся на статью про оптимистичные блокировки в распределённых системах — и всё встало на свои места.

Оптимистичные блокировки — это... не блокировки вовсе.

Представьте: вы хотите изменить некое значение. Но делать это можно только при определённом условии — например, что данные на момент изменения всё ещё в том же состоянии, что и были, когда вы их получили.

Сценарий такой: вы читаете ресурс, обрабатываете его у себя, а потом отправляете обновление, указывая, что работали с версией X. Если за это время кто-то другой уже обновил этот ресурс, ваша операция завершится с ошибкой. Тогда вы можете повторить всё заново — пока не попадёте в нужную версию.

Вся суть подхода в том, что каждая новая версия ресурса получает свой номер. И изменить его можно только в том случае, если версия не изменилась с момента чтения. Если изменилась — извини, пробуй ещё раз.

Самое интересное — я не раз сталкивался с этим подходом на практике, просто не осознавал, что это оно:

- Kubernetes API и все операторы, которые с ним работают — используют именно этот подход
- Confluence API — то же самое, обновление контента работает по версии
- S3 API - и тот самый ETag в нем

Такие дела. Пазл наконец сложился.
👍5
Давно я не писал в этом канале постов, исправляюсь

Я все это время готовился к GolangConf 2025, и откровенно говоря постоянно ничего полноценно не успевал.

Но конференция наконец прошла, я выступил, и теперь потихоньку буду возвращаться с новыми постами.

Сам мой доклад был про расширение DNS в Kubernetes, которое мы делали в нашей ML платформе в Т-Банке.

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

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

Такие дела
🔥10👍51
Подход Contract-First

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

Явный контракт, определённый до начала разработки — must-have для любой системы.

Раскрою мысль:
Контракт, определённый заранее, позволяет не блокировать работу других команд. Бэкенд может быть ещё в процессе разработки, но фронтенд уже может писать код на основе контракта и использовать моки.
Уже на этапе описания контракта можно выявить логические ошибки и проблемы с UX.
Контракт заставляет думать о системе целиком и, что особенно важно, о её потребителях. Он минимизирует ошибки коммуникации и помогает зафиксировать договорённости.


Разумеется, всё это относится к качественным контрактам, которые, как мне кажется, должны соответствовать нескольким важным принципам:

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

2. Контракт должен быть полным
Контракт должен явно описывать все поля, их типы и ожидаемые значения.
Поле типа "строка", в которое складывается JSON — это антипаттерн.
Пример:
Когда-то я работал над интеграцией с российским подразделением AliExpress — TMall. У ребят были контракты с документацией на китайском, но это была лишь часть проблемы. API пронизывали неструктурированные JSON, которые передавались внутри строковых полей.
Интеграция с такой системой была адом: поля могли измениться в любой момент, не было версионирования, и любая ошибка всплывала не в разработке, а уже в эксплуатации.

На ум приходит аналогия с NoSQL: технология, которая на пике хайпа обещала schema-less-хранилища — просто складываешь данные как есть и работаешь.
В начале — быстрая разработка, потом — боль.
Почти всегда всё заканчивается возвращением к строгим схемам. Потому что невозможно работать с тем, что непредсказуемо.

Контракт — это не просто формальность. Это инструмент, который помогает синхронизироваться, проектировать, проверять гипотезы, планировать и в конечном итоге — строить надёжные системы.
🔥82
Open Source и инженерный подход

Читал тут статью о том, как чувак пришёл в компанию, чтобы починить бесящий его баг.

В ней есть один важный тейк, о котором хочу сегодня порефлексировать:
Open Source позволяет исправлять баги

(цитата не точная, это лишь идея, как я её понял).

И я на самом деле полностью согласен с этой мыслью.
Месяц назад мне пришлось интегрировать Falco Security в нашу платформу для мониторинга событий через eBPF.

Инструмент мощный, но инфраструктурные компоненты, просто боль.

Helm-чарт с миллионом неочевидных параметров, странный подход к плагинам и конфигам - и всё это явно не то, что хочется держать в проде.

Рядом есть k8s-оператор, который выглядит перспективно, но пока что довольно сырой. Много багов, плохая документация, почти нет поддержки кастомных сценариев.

Можно было бы смириться, разворачивать через Helm, писать костыли, страдать. Но я не считаю это полноценным решением и инженерным подходом.

Я сделал форк k8s-оператора, пофиксил всё, что мешало, добавил недостающие фичи, и всё полетело!

Когда тебе нужно «просто чтобы оно работало», ты не ждёшь - ты берёшь и чинишь.

Хороший инженер, это тот, кто не боится влезть в чужой код и переделать его под задачу.
Сломать, понять, собрать заново.

Вот это и есть инженерный подход.
🔥63👍32
На выходных я удалил зуб мудрости - так что теперь официально стал чуть менее мудрым 😂

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

У меня вообще с пет-проектами всё непросто: хочется красиво, с конфигами, пайплайнами, идеальной архитектурой. А потом всё тормозится и умирает. В этот раз решил попробовать подход "well-enough" - и внезапно это сработало.

Идея простая:
AI генерирует рецепт - футуристичный, возможно, кринжовый
AI (другая модель) по нему рисует картинку или логотип
Всё это постится в канал
Сначала я пошёл по классике:
yaml-конфиги каналов
пайплайн с графом зависимостей
промпты, обёртки...

А потом сел, выкинул всё это и написал минимальный прототип:
- генерация текста
- генерация картинки
- отправка в Telegram

Весь код - это один простой Python-скрипт под конкретный канал:
логика инкапсулирована
промпты лежат прямо в коде константами

И что вы думаете? Всё полетело.
Такой подход оказался суперудобным и для отладки, и для подбора моделей, и для тестов.

Что под капотом:
Для текста пробовал разные модели через OpenRouter: DeepSeek, Gemma, Qwen. Все работали ок.

Сначала хотел генерить картинки локально через SDXL, но:
- качество слабое (мыло какое-то)
- промпт из LLM'ки часто не влезал в ограничение контекста

Заменил на ReCraft API - и стало сильно лучше. 10 баксов за 500 генераций - более чем норм.

Запускается всё по крону, постит в канал - и не трогает меня вообще.

Мелочь, а приятно. С одной стороны, просто развлёкся, с другой - на себе прочувствовал, насколько мощно работает паттерн Structured Outputs у LLM'ок, и как далеко можно уехать на самом простом решении.

В целом, теперь думаю превратить всё это в фабрику автоканалов - архитектура уже складывается.

А пока - вот первый прототип: @neuro_recipes

Подписывайтесь, ставьте лайки - пусть нейросети кормят нас вкусным (и странным) контентом 🍲🤖
🔥8
Рецепт дедупликации: Kafka, RocksDB и немного хитрости

Хочу сегодня разобрать одну статью про exactly-once доставку: https://www.twilio.com/en-us/blog/insights/exactly-once-delivery

Её написала команда Segment (позже их купил Twilio). У них была классическая проблема: мобильные клиенты присылают сообщения, и часть из них дублируется. Нужно было построить пайплайн так, чтобы downstream-системы получали сообщения ровно один раз.

Что они сделали:
- выделили отдельный топик для "чистого" потока
- написали сервис, который читает сырые данные из Kafka, проверяет дубликаты и пишет в новый топик

Ключевое решение — встроенная RocksDB. Они не стали использовать Redis или Memcached, потому что сетевой hop сразу добавил бы задержки. А в RocksDB всё локально, lookup быстрый, bloom-фильтр фильтрует 90% ключей, так что дедупликация работает с очень низкой задержкой.

Самое элегантное место — source of truth. Локальная база при рестарте пустая, и сервис не знает, что уже было. И тут они сделали хитро: использовали Kafka с очищенным топиком как единственный источник правды. При запуске сервис перечитывает историю и восстанавливает состояние. Мне эта идея кажется очень красивой.

Конечно, не без минусов:
- прогрев RocksDB после рестарта занимает время, и если рестартов много, это больно по задержкам
- на каждом инстансе нужно локальное хранилище, а диск не бесконечен
- и самое спорное для меня: если потребитель "чистого" топика перечитает его после падения без коммита оффсета, то он снова получит старые сообщения.

Формально пайплайн держит exactly-once, но downstream всё равно может споткнуться


Такие дела
🔥3👍2
Не боги горшки обжигают

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

Сама бага интересная:
- нужна 32-битная система
- нужен overlayfs
- нужен кросс-компилированный bash под ARM
- в итоге все это вместе приводит к багу, из-за которого getcwd выкидывает ошибку ENOENT

Как по мне, в статье есть три ключевые детали:
- overlayfs дает стабильные inode numbers только с включенной фичей xino, а она доступна исключительно на 64-битных системах
- bash плохо дружит с кросс-компиляцией и из-за этого использует не сискол getcwd, а свою имплементацию, которая как раз и взаимодействует с inode
- и самое главное - баг живет в исходниках bash'a уже много лет, и никто раньше его не замечал

В итоге и получается заголовок поста - не боги горшки обжигают.

Ошибки делают все, и даже такой зрелый проект, как bash, может десятилетиями носить в себе неожиданные баги.
2👍2🔥21
Почему async/await так и не стал мейнстримом?

На бумаге идея классная: чуть сахара в синтаксис — и вот у нас асинхронность. Но по моему мнению это одна из худших вещей, которые случились с Python, Rust и ещё кучей языков.

- Основная проблема разделение всего кода в языке на два множества.

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

Go пошёл другим путём. Там есть go f(), и вроде красиво: запускать горутины дёшево и просто.

- Но красота заканчивается, когда дело доходит до graceful shutdown и механизмов отмены.

Во-первых, горутина живёт своей жизнью. Если main завершится, она просто грохнется в произвольном месте.

Во-вторых приходится протаскивать context.Context через каждую функцию.

Даже если функция принимает ctx, нет гарантии, что она реально слушает его отмену.

Результат: дедлоки и goroutine-leaks.

Все это - костыли и попытки сделать удобно, в неудобном мире, мне это очень не нравится.

Такие дела.
👍721
Почему async/await так и не стал мейнстримом?

В прошлом посте, я начал раскручивать идею о том почему async/await так мне не нравится, сегодня продолжу накидывать на вентилятор.

Наконец поговорим про дивный мир Future.

Исторически Future появились как способ "управлять результатом асинхронной операции". Казалось бы, порядок, контроль, предсказуемость. На деле Future просто создаёт ещё один уровень абстракции, но не абстрагирует от самой проблемы. Кто их дожидается? Кто отменяет?

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

Structured concurrency могла бы быть ответом на все три боли: async/await, orphaned горутины и Future.

Async/await, горутины и future-зоопарк — это промежуточные механизмы. Они дают иллюзию контроля, но модель конкуренции остаётся неполной.

Я хочу мир, где конкурентный код выглядит как обычный, а надёжность обеспечивается самой моделью (рантаймом языка), а не миллионом проверок на ctx.Done() и бесконечным await fut.result().

Но пока я не видел ни одного языка, или примера где structured concurrency была бы first-class citizen в языке.

Такие дела.
🔥31
Уже второй раз за месяц натыкаюсь на статьи от разных компаний о том, как они переделывали свои системы логирования и мониторинга.

Похоже, тема становится горячей, так что решил немного осветить её и заодно поделиться полезными ссылками.

Все чаще компании уходят от классического ELK-стека в сторону кастомных решений на базе S3 и метаданных в Postgres (или другом SQL-хранилище).

Причины понятны и вполне рациональны:
- Логов становится всё больше, независимо от размера компании
- Поля в логах часто имеют высокую кардинальность
- Основные запросы — почти всегда по последним записям, а не по историческим данным

В итоге привычные решения вроде Elasticsearch/OpenSearch начинают трещать по швам:
- запросы по шардам распределяются неравномерно
- индексация запаздывает
- инженеры страдают
- объём данных растёт, а TTL наоборот: никто не хочет сокращать (а иногда ещё и увеличивают)

Почему S3-подход набирает популярность
-
Не нужно думать о durability и обслуживать кластер OpenSearch
- Стоимость хранения кратно ниже
- При грамотной архитектуре можно ещё и ускорить запросы за счёт многоуровневого каскада хранения

Несколько отличных материалов по теме
-
Our Journey to Affordable Logging: история о том, как логи начали стоить компании 17% бюджета, и что они сделали, чтобы это исправить.
- Building Haydex - Axiom: глубоко технический разбор с фокусом на I/O, скейлинге и продуманной архитектуре хранения.

Ну и немного про нас 😉
Коллеги из Т-Банка тоже идут по похожему пути — вот материалы о нашем внутреннем инструменте Sage:
- 7 петабайт логов в Elastic
- Когда нужно делать свою базу данных
🔥4