#64: Быстрее Кафки на два порядка: Алексей Лебедев про то как строить сложные системы
Александр Пахомов и Алексей Лебедев — инженер систем низкой задержки, создатель стриминговой платформы AlgoX2 («убийца Kafka») и открытого фреймворка кодогенерации OpenACR — разбирают, как строить большие системы из простых, доказуемо корректных частей. Первые два часа сознательно исключают «агентную разработку»: single entry / single exit и single static action, программа как вложенные циклы и глобальная in-memory база данных, отказ от исключений и многопоточности в пользу однопоточных процессов, общающихся сообщениями (CSP, RDMA/InfiniBand, scale-out). На этом фундаменте построена AlgoX2 — «файловая система из стримов», где всё есть append-only-стрим и управление идёт через тот же стриминг; она обгоняет Kafka по latency в 10–100 раз. Ядро системы генерируется из SSIM-описаний генератором, который на 95% состоит из собственного сгенерированного кода. В финале — как Claude Code усиливает такой подход и какой навык остаётся ключевым для инженера.
Главное
- Single entry / single exit (SESE) делает программу самоподобной: любой блок можно вырезать и вставить как одну строку в другой, а к каждой точке привязать инвариант в духе preconditions Дейкстры; из этого же следует запрет на исключения — `throw` есть многоуровневый `return` на неизвестно сколько уровней вверх.
- Single static action: любое поле любой структуры пишется ровно в одном месте кода, поэтому весь стейт можно держать в единой глобальной in-memory-«базе данных» `db` — глобальные переменные не вредны сами по себе, вредна их модификация из разных мест.
- Программы у Лебедева однопоточные; масштабирование — не потоками, а деревом процессов, общающихся только сообщениями (идея из `Communicating Sequential Processes` Хоара, наследники — каналы Go и Plan 9 / 9P).
- Ставка на однопоточность оправдалась через scale-out: RDMA-карты (InfiniBand/Mellanox, теперь внутри Nvidia) пишут байт в память соседнего хоста за ~600 нс с kernel bypass и умеют multicast — «железо догонит», а latency между дата-центром и одним компьютером объяснять не нужно.
- AlgoX2 — «Distributed OS», где всё есть append-only-стрим (2^64 сообщений, две операции: дописать и прочитать), а управление кластером идёт через служебный стрим `/sys/cmd`, который каждый процесс читает с нуля; Kafka по сути single-host-система, а её `topic` + `partition` — это просто стрим.
- Внутренние компоненты работают с `ACK level` 1 (сообщение существует в одном экземпляре), внешние потребители — с уровнем 2–3 (реплицировано и надёжно); один и тот же механизм подписки реализует и то, и другое.
- Хранение — строго append-only ради NVMe: флеш нельзя перезаписать без стирания блока (wear-leveling), при random-write включается garbage collection и производительность падает в ~100 раз; свой движок пишет и данные, и индекс в один файл (структура «Log Structured Try»).
- Ядро системы задаётся `SSIM`-файлами (реляционные key-value-записи) и генерируется фреймворком OpenACR (`acr`/`amc`): на одну рукописную строку приходится ~20–25 сгенерированных, ~95% кода генерирует генератор, который сам на 95% состоит из собственного сгенерированного кода — без темплейтов, с одним breakpoint на функцию.
- AlgoX2 обгоняет Kafka по latency в 10–100 раз (например 50 мкс против 5 мс) и по throughput в десятки раз под нагрузками с жёстким окном задержки; при насыщении ресурсов, где Kafka упирается в железо, преимущества нет.
- Claude Code для Лебедева — «велосипед»: он написал почти весь движок, попав туда же, куда дошёл бы пешком; маленький код, делающий много, умещается в голову, а дефицитным ресурсом инженера становится время и умение решать, где углубиться, а что делегировать модели.
В выпуске
- Алексей Лебедев — Инженер систем низкой задержки; строит распределённую стриминговую платформу AlgoX2 (позиционируется как «убийца Kafka»). Автор открытого фреймворка кодогенерации OpenACR (инструменты acr/amc, SSIM-файлы), который генерирует ~95% собственного исходного кода. Учился программировать в начале 90-х, ранее занимался высокочастотной торговлей. GitHub ↗ GitHub ↗
Ссылки
- OpenACR — фреймворк кодогенерации (acr/amc, SSIM) Алексея Лебедева
- Communicating Sequential Processes — C.A.R. Hoare
- LevelDB — движок хранения на основе LSM (Jeff Dean, Sanjay Ghemawat)
- Apache Pulsar — распределённая стриминговая платформа
- SPDK — Storage Performance Development Kit (доступ к NVMe из user space)
Расшифровка
[00:00] Алексей: Я не хочу находиться рядом с проектом и заниматься maintenance. Он просто должен работать. Точка. Вот чтобы я к нему не возвращался. Как писать код, чтобы можно было закрыть эту крышку — и писать не для того, чтобы он в какой-то момент… Знаешь, как люди любят: «Я не уверен в этой функции, правильно она работает или нет, поэтому я вставлю print. И — let’s ship it». Потому что когда этот print распечатается, кто-то там где-то пожалуется, до меня этот print, может быть, добежит, я посмотрю на него и скажу: «А, да, вот теперь я знаю, что я тут был неправ. А раньше я был просто не уверен». Это все программисты. Я тебе описал всех программистов. Любой человек, который в коде оставляет print, надеется, рассчитывает именно на это. Он мысленно его не отпустил, он всё ещё с ним в отношениях. А если ты написал правильный код, то там не должно быть никаких принтов. Он должен просто бежать.
[00:56] Алексей: Всех в нашей компании я убеждаю: если вы ещё не пользуетесь Claude — пользуйтесь побольше. Потому что я не боюсь, что у них атрофируется мозг. Если его не было, то его и не будет. А если он был, то он никуда и не денется. То есть это исключительно велосипед: ты на него садишься и едешь — и от этого ходить не разучишься.
[01:38] Александр: Лёша, у меня к тебе первый вопрос. Ты мне рассказывал такую концепцию — single entry, single exit. Расскажи поподробнее: что это, откуда взялось и почему ты считаешь, что это вообще важно в 26-м году?
[01:48] Алексей: Может быть, это и не важно — но это важно. Сейчас компьютер, наверное, может написать за тебя любую программу, но, с другой стороны, ты можешь ему подсказать, в каком стиле писать, и он будет соответствовать твоим пожеланиям.
[02:02] Александр: Давай на секунду отвлечёмся от идеи того, что компьютер генерирует код — ему же всё равно, что писать, на каком языке и как. И всё-таки вернёмся к этой старомодной идее, что код пишет человек. А раз человек пишет, читает и пытается понять код, значит, немаловажны понимаемость, читаемость и какая-то эстетика этого кода.
[02:24] Александр: Нужно прямо сделать паузу и сказать, что мы осознанно убираем всё, что касается агентной разработки, генерации кода и вот этого всего. То есть мы отдаём себе отчёт, где мы находимся, но сейчас, чтобы дискуссия выстраивалась правильно, мы про это не говорим и рассматриваем чисто человеческий подход к разработке: как мы код пишем как люди, как мы его читаем и воспринимаем. Так что такое single entry, single exit и почему мы на этом вообще заострились?
[02:53] Алексей: Это старомодная идея, которая изначально возникла, когда появилось структурированное программирование, — то есть не прямой набор инструкций с goto, а попытка выстроить свои инструкции в некое иерархическое дерево, которое как-то визуально отображает структуру задачи.
[03:12] Александр: То есть мы визуально представляем код как иерархическую, древовидную структуру.
[03:18] Александр: Какую-то графоподобную, и ещё направленную.
[03:21] Алексей: Да, но на самом деле всё намного проще. Код — это всего лишь какое-то количество вложенных циклов. Даже if можно воспринять как цикл от нуля до единицы: цикл по пустому множеству или по одному элементу — это и есть if. if — это частная форма цикла.
[03:37] Александр: Слушай, мне сейчас сразу что-то вспомнилось — вот именно такая модель мышления, скорее функциональная парадигма: у тебя либо рекурсия, либо выполнение функции.
[03:48] Алексей: Если весь код является каким-то количеством вложенных циклов, то этот код является перебором по какому-то пространству, потому что любой цикл — это перебор. Мы идём по оси некоего пространства.
[03:58] Александр: До десяти идём. Или по элементам множества идём, правильно?
[04:02] Алексей: Да. Иногда те элементы, по которым мы идём, — это while-цикл, такое implicit-множество. Это самые интересные циклы, потому что не совсем понятно, почему мы итерируем. Но и с ними можно справиться, туда пока не будем заходить. Вообще программа — это набор циклов. Она чего-то ищет и завершается, когда это что-то нашла или построила. Мы концептуально переходим от идеи калькулятора, где ты говоришь «3, 2, плюс», дёргаешь за ручку, и арифмометр Дзержинского выдаёт тебе ответ 5, — к более визуальному программированию, где моя программа — это такой поиск: сначала я рассматриваю вот это направление, внутри него рекурсивно иду и итерирую по вот этому, и у меня получился двойной цикл. Я теперь могу просуммировать по этим циклам, понять алгоритмическую сложность. Могу рассуждать, насколько плотно программа покрывает это пространство, сколько шагов выполняет, нельзя ли поменять циклы местами, чтобы она иначе итерировала, есть ли шорткат, которым можно резко сократить количество действий. Так или иначе, появляется такая визуальная структура.
[05:23] Алексей: Программа сродни математическому доказательству. Мы идём сверху вниз, и в каждой ключевой точке — это ещё Дейкстра придумал, это классическая работа о правильности написания программ — мы говорим: «Если мы дошли до этой точки, то ситуация такая-то». Он придумал понятие preconditions. Идём по программе, и в каждой точке максимально лаконично описываем ситуацию. Если мы не можем описать, на что здесь можно положиться, значит, программа плохо задизайнена: ты прошёл полпрограммы, и непонятно, чего ты добился. Правильно звучит так: «Ты дошёл до половины программы, и к этому моменту точно установил вот такие факты». То есть к месту в программе мы привязываем этап нашего доказательства.
[06:12] Александр: То есть это такой путь с ветвями, с возможными пересечениями, и в каждый момент выполнения мы находимся в какой-то точке этого пути.
[06:26] Александр: И он как станция: мы на неё приходим, мы здесь сейчас, можем пойти туда, туда или туда, но понимаем, где мы. Это концептуально то, как ты представляешь программу, или это отражается в том числе в коде? Что для этого нужно, кроме слов?
[06:42] Алексей: И то, и другое. Когда я пишу программу, я очень хорошо понимаю, в каком пространстве она оперирует. Потому что, если только твоя программа не решение домашней работы или очень маленькая задачка, она, как правило, — винтик чего-то большого. Я привык работать в больших системах, где много шестерёнок, возможно, десятки или сотни программ. Скажем, ты пишешь самую элементарную программу — какой-то REST-клиент. В ней очень много вещей, из которых ты должен исходить, чтобы она правильно сработала: знать что-то про сервер, про схему, про частоту запросов, что можно и что нельзя запросить. Это всё не содержится в программе напрямую — это твоё боковое знание, которое ты держишь где-то в сторонке. То же самое с алгоритмами — а это самый сложный вид программ: REST-клиент несложно написать, а какой-нибудь движок типа LevelDB посложнее. Всё равно есть эта глубокая структура, которой программа должна соответствовать. Это другой способ сказать, что, выполняя свои вложенные циклы, я ищу в этом другом пространстве какое-то своё значение.
[08:12] Александр: Сейчас немного сложновато стало, и я стал именно визуализировать. Скажу, какая у меня картинка в голове. Есть вход и есть выход у какого-то блока. Внутри блока есть множество путей с этими станциями. А блок — это цикл со вложенными циклами. Он там бегает-бегает, в любой момент я могу остановить и понять, на какой я станции. А потом я делаю зум-аут от этого блока и понимаю, что у меня точно такая же рекурсивная структура, но уже станции — это блоки.
[08:48] Алексей: Да, ты помог мне выбрать, какую фразу произнести. Возьмём программу, которая является каким-то количеством вложенных блоков, и остановим её. Мы где-то находимся — как описать где? На самом деле это несложно. Мы сделали десять шагов, вошли в цикл, и в нём мы на 11-й итерации. Это место можно обозначить как «11-я итерация этого цикла». Если циклы вложены, то, скажем, «13-я итерация, внутри 11-я итерация». Не всегда это можно описать числами, иногда строчками.
[09:24] Александр: Ага, но это как адрес станции: вот в этом блоке — вот эта станция.
[09:28] Алексей: У твоей строчки может быть dotted address, потому что она итерирует, расцивилизует какой-то дискретный многомерный кубик, пробегает по его точкам, что-то ищет. Если мы смотрим на программу как на вложенный поиск, а на трансформации программ — как на передвижение законченных кусков кода, то, если твоя программа всегда входит сверху и выходит снизу, она становится самоподобной. Это значит, что ты можешь взять любую программу, вырезать её и вставить как строчку внутрь другой.
[10:00] Александр: То есть самоподобная — это свойство, которое описывается так: ты можешь программу вставить саму в себя.
[10:10] Алексей: Это значит, что на всех уровнях зума она выглядит одинаково. Шаг, шаг, шаг — она так выглядит. Шаг, шаг, много шагов, шаг. И это тоже шаг.
[10:24] Александр: И это тоже шаг, да. То есть «шаг» равно «шаг», или «шаг, в цикле шаг», или «шаг, шаг, шаг». Или нет шагов.
[10:30] Алексей: Empty, нет шагов. То есть это self-similar. Это single entry, single exit.
[10:38] Александр: Single entry, single exit. Мы входим сверху и выходим снизу. Вот интересно, как ты это визуализируешь. Когда ты сказал «сверху входим, снизу выходим», я такой: а у меня вообще слева вход, справа выход. То есть у меня визуализация такая.
[10:55] Алексей: Не, ну у тебя не может быть такой визуализации, потому что если ты ходил отладчиком по программе, то курсор всегда передвигается вниз, вниз, вниз.
[11:03] Александр: Ну это как мем про то, как люди себе представляют времена года. У кого-то это овал, где внизу зима, сверху лето, справа весна, слева осень. А у кого-то квадрат, у кого-то сектора. И места, где эти времена года располагаются, у людей в разных местах. Я был удивлён, что у кого-то зима не снизу. Мне кажется, тут то же самое. Я когда учил программирование, я учил его по листочкам и представлял интерпретацию программы как тетрадный листок: вот я пишу текст — и точно так же движение программы воспринимаю как письмо ручки. Мне кажется, этот паттерн психологически усваивается и дальше воспроизводится. А у тебя, видишь, с отладчиком связано. Прикольно, то есть мы с тобой по-разному учили программирование.
[11:54] Алексей: Не, ну у меня абсолютно точно программа исполняется сверху вниз. Смотри, когда ты пишешь сложную программу, каждая строчка раскрывается в какое-то количество более мелких шагов. Если ты пишешь ассемблерную программу, каждая строчка тоже раскрывается в шаги, но они намного проще. Их там не десятки тысяч, а в современном процессоре — несколько сотен основных. В ассемблере программа всегда исполняется сверху вниз. Мало того, ты можешь сказать, что она исполняется слева направо, потому что instruction pointer увеличивается на длину инструкции. Если ось X у тебя идёт направо и ты располагаешь инструкции по адресам памяти, то процессор сканирует всё слева направо. И то, и другое верно.
[12:52] Алексей: Для меня программа — это текст. Мы читаем в западном мире слева направо, сверху вниз: строчки — слева направо, программу — сверху вниз, всё совпадает. Единственное, чего я никогда не понимал: почему breakpoint называется point, а не breakline? Между каких букв эта точка находится? Ни разу не встречал отладчик, который позволял бы поставить именно breakpoint. Вот я ставлю breakpoint на строчке, подхожу к ней, а там ещё глубокая вложенность скобок, через которые я должен продраться, пока не попаду в ту функцию или в то место. Если бы я мог поставить breakpoint курсором на экране — вот это было бы круче. Такую фичу хотелось бы иметь. Напомню, что курсор — это тот, который блинкает, а не тот, который у редактора кода.
[13:42] Алексей: Ну да, ты разговариваешь с человеком, который научился программировать где-то в 90-м году.
[13:50] Александр: Возвращаясь: хорошо, что мы сделали паузу о представлении программ. В общем, я понял, что у меня представление транспонированное. Думаю, у меньшинства слушателей может быть примерно такое же. Так что я для вас, ребята, прояснил, что бывает и так, и так. Но мы рассматриваем то, что сверху получается вход один, а снизу — выход. На этом мы остановились.
[14:13] Александр: Можно ещё исторический экскурс сделать. Ты знаешь такой flowchart? Ну, рисуют какие-то ромбики, соединяют линиями, ставят вопросики — визуальный механизм принятия решений. Я вот так часто рисую описание бизнес-процесса или алгоритма: там могут быть циклы, условные операторы, yes/no, ветвления. Ну, flowchart, по сути, это просто диаграмма…
[14:44] Алексей: Структурированная программа в классическом понимании на самом деле была придумана как текстовая репрезентация flowchart. Его пытались нарисовать прямо на экране — с отступами, со всеми делами. То есть если мы возьмём строчки этого flowchart и выпишем их определённым образом, мы получим структурированную программу. Так мы покрыли свойство самоподобности, self-similarity. Очень хорошее свойство. Если ты роботу, Claude, задаёшь какой-то рефакторинг, ему будет намного проще сделать его правильно, если твоя программа самоподобная, потому что он может просто куски кода вырезать и вставлять. Меньше сюрпризов. Первое свойство — что одну программу можно вложить в другую: программа есть шаг, и шаг раскрывается в другие шаги. Это очень красиво. А второе свойство: к месту в программе ты можешь прицепить мысленную аннотацию — что программа выполнила к этому моменту. Теперь, если у тебя за десять строчек стоит какой-то странный return, который перепрыгивает через это место, а после него ещё выполняется какой-то текст, то ты уже не можешь указать пальцем на место в программе и сказать, что там произошло к этому моменту. Ты запутал flow graph.
[16:04] Александр: Вот мне хочется прямо визуально это представить — сформулирую как наброс: в концепции single entry, single exit, если мы так строим системы, return-ов посередине функции быть не должно. Я так это воспринимаю.
[16:24] Алексей: Не должно быть.
[16:25] Александр: И тогда хочется понять, потому что, вообще говоря, я такой код писал, когда писал руками, — у меня бывало. Я представил эту самоподобную программу, где, приближая, зумя всё ближе, мы видим ту же структуру: сверху вход, снизу выход, как flowchart внутри. И вот как именно вставка return внутрь цикла — выход с предусловием, «если я нашёл элемент, то делаю return», не дохожу до конца цикла — как он конкретно ломает концепцию? Почему я всё ещё не могу в этот момент сказать, что в программе произошло?
[17:14] Алексей: Скажем так, ты выбрал буквально тот пример, где я бы и сам этот return иногда вставил. Все функции можно поделить на такие, которые что-то меняют, и такие, которые ничего не меняют. Функция, которая занимается поиском, действительно может в любую секунду этот поиск прекратить: она что-то нашла и вышла через любое количество уровней, и ровным счётом ничего концептуально не меняется, потому что она ничего не открывала, ничего не аллоцировала, ей нечего закрывать и освобождать. Это просто двойной вложенный цикл, и, конечно же, ты туда вставишь return. Что ещё ты туда вставишь?
[17:53] Александр: Мы говорим, что есть такой кубик — введём допущение в эту формальность. Если мы зумим, зумим, зумим и дозумили до какого-то атома, вот дальше кубик — это уже просто цикл, дальше мы дезассемблировать не будем. Просто функция из пяти строчек: проходит по массиву, находит элемент, делает return; не нашла — делает return в конце. Два return-а. Вот это прямо наш атом, внутри которого пускай будет что-то по-другому, но всё, что выше него, — уже single entry, single exit.
[18:36] Алексей: Знаешь, если цикл вложенный, то я действительно не стал бы вводить дополнительную переменную, которую надо проверять во внутреннем цикле, потом как дурак возвращаться в начало, видеть, что я только что её проставил, и лишний раз её проверять. Это кривой случай, когда цикл трудно красиво сделать, не потратив зря компьютерные ресурсы. А вот если цикл одинарный, я никогда не напишу return. Я сначала введу переменную возврата, инициализирую её дефолтным значением; внутри цикла, если поймаю нужное значение, присвою его этой переменной, сделаю break, а в конце всегда верну эту переменную. Таким образом, почти как в Pascal, переменная, которую я возвращаю, декларируется в начале и там же инициализируется. Сразу после неё я могу остановиться и сказать: на этот момент программа верна, дефолтное значение уже есть, возвращать можно. А теперь пойдём попытаемся найти более правильное значение. Я уже ряд вопросов закрыл таким систематическим подходом.
[19:56] Александр: Здесь вопрос мне прямо личного характера. Понятно, что я это воспринял как твой авторский почерк писать функции: ты так пишешь — зашибись, я увижу код, прочту, скажу «ок». Нужно ли, чтобы в этом моменте ты эту функцию написал? А вот если я тебе принесу функцию, где я делаю return в середине, ты попросишь меня поправить? Или скажешь: «Ну ладно, я по-другому написал»?
[20:22] Алексей: Я тебе напишу «SESE» и отвергну твой commit.
[20:26] Александр: Окей, всё, понял. Мне нужно было понять уровень, насколько ты в это вкладываешься.
[20:33] Алексей: Правило декларировано. Оно записано в CLAUDE.md, ты его сам можешь прочитать. Это одно из немногих правил, которым мы следуем. Можем дальше single static action обсудить — оно сродни этому SESE, у него история много лет. Когда много человек пишут большую систему, очень важно, чтобы все следовали одной конвенции. Потому что если ты устроишь хаос, демократию-хаос, никто спасибо не скажет: будет не небоскрёб, а бразильские развалины, фавелы. А нам нужен небоскрёб. Все балки должны втыкаться ровно в этом месте, структура должна быть продуманной, чтобы он рос вверх, а не вбок. И все должны следовать этим конвенциям. Конечно, каждый по-своему немножко решает задачи, но, грубо говоря, если я читаю твой код, я не должен думать, что это ты его написал. Он просто должен быть правильным, лаконичным, понимаемым. А если у тебя барокко в стиле — ты, не знаю, всегда узор из комментариев делаешь, — я это, конечно, зарублю. Ты должен вписываться в общую систему и её превалирующий стиль. Это правило в каком-то смысле и ко мне применимо: я не буду локально в каком-то файле, если у меня настроение игривое, вводить новую конвенцию. Я себя переборю и буду пользоваться конвенциями проекта.
[22:07] Александр: Это очень интересный момент, потому что он меня на размышления наталкивает. Как инженер, который любит строить системы, а потом видеть, как система превратилась в небоскрёб, и что он дальше растёт вверх, а не вширь, как гриб по земле, — ты получаешь какое-то инженерное эстетическое наслаждение. И понимаешь, что этот небоскрёб растёт, потому что все этажи по чертежам, у всех блоков одинаковые размеры. Это не везение, это огромный кропотливый инженерный труд и правила.
[22:56] Алексей: Полностью поддерживаю.
[22:56] Александр: Но в то же время внутри меня есть какой-то бунтарь. Реально, это две сущности внутри меня, я не шучу. Одна говорит: «Ну какая разница, как я функцию написал, задача-то решена». Понятно, если я в комментариях барокко делаю — это уже диссидентство. Но в плане «сделал я return раньше или позже, назвал переменную так или не сяк» — та часть меня, которая протестует, воспринимает это как почерк. И я себе на ревью всегда говорю: кто я такой, чтобы человеку почерк ставить?
[23:34] Алексей: А я тебе скажу следующее. Когда ты натравливаешь Claude на свою репозиторию, он читает твой код и вычленяет из него паттерны. И чем меньше этих паттернов, тем проще ему произвести аналогичный, так, чтобы он был верным. Согласен?
[23:53] Александр: Инженер полностью согласен с тобой.
[23:55] Алексей: Смотри: код был правильный, но все мясные компьютеры… И Claude тоже уже приближается к мощности мясного компьютера — он не железный Феликс, а мясной Вася. И он тоже будет делать ошибки, если ему подкинуть амбивалентный паттерн. Поэтому паттерны должны быть строгими, они должны подсказывать: «Мы именно так выглядим, а не иначе». Мы действительно много раз повторяемся и можем быть даже занудными, но ничего страшного, потому что в конце концов нас интересует, чтобы 45-й этаж нашего небоскрёба был очень похож на 46-й. Мы не хотим заново доказывать теоремы на 46-м этаже, мы хотим, чтобы работала индукция.
[24:48] Алексей: Представь, что ты пишешь проект с очень высокими ставками. Если что-то грохается — всех увольняют, проект закрывается, компания, возможно, банкротится. Окей? Какими принципами ты захочешь воспользоваться, чтобы выжить в такой ситуации?
[25:09] Александр: Давай покрутим, вопрос интересный. Какие бы принципы я принял именно на код? Мне сейчас сложно. Опять же, мы в той парадигме, что мы больше про людей. Но я бы очень много думал про то, что я же это всё буду кодом программировать. Что мне надо сделать в первую очередь, чтобы это на 100% было безопасно? Это какой-то невероятный подход к тестированию — к перформанс-тестированию, к контролю каждого модуля по ресурсам. Тесты, которые говорят мне: «У тебя файлы открылись в этот момент, здесь столько CPU, а вот здесь ты вот так себя повёл». Я реально с этого бы начал. Но понимаю, что твой вопрос больше про подходы к коду. Я так понимаю, что SESE… Я бы до подкаста с тобой SESE точно не заиспользовал. Сейчас уже задумываюсь.
[25:18] Алексей: Давай, наверное, мы достаточно хорошо описали SESE. Мы никого не переубедим, если у них другой стиль. Может, заставим задуматься, но переубедить я не надеюсь. Давай расскажу про аналогичную вещь, которая, мне кажется, ещё менее известна, чем SESE, и, возможно, вообще неизвестна. Она называется single static action.
[26:47] Алексей: Напоследок скажу: если кто-то хочет про SESE почитать, есть такой Бертран Мейер. По-моему, это единственный человек, который вообще об этом заикался. Всерьёз эта тема последний раз поднималась, может быть, Никлаусом Виртом, который создал Pascal. Дальше всё это забросили. И вот только Бертран Мейер со своим Eiffel пытался придумать какие-то паттерны программирования. С ними, на мой взгляд, в индустрии практически никто не знаком. Я не встречал никого, кто бы читал его книги, — из рабочих программистов, которые создают реальные системы, на которых весь мир бежит. И никого, кто бы программировал на Eiffel. Хотя, наверное, его объектная ориентированность и все эти мысли уже устарели. Вообще объектная ориентированность — это, по-моему, просто ложное направление.
[27:53] Алексей: Так или иначе, давай обсудим single static action. В каком-то смысле он очень похож на SESE. Что это такое? Давай отмотаем назад. Твоя программа — это сколько-то вложенных циклов, и каждый её микрошаг либо читает какое-то поле какой-то структуры, либо его пишет. Значит, у тебя в памяти разложена какая-то база данных. Каждый тип структуры, которую ты используешь, — это тип записи в базе данных. Все живущие инстансы этой структуры — это таблица. Неважно, индексирована она или можно ли по ней проитерировать. Есть структура X, и где-то на стеке, в хипе, живут какие-то её экземпляры. И так для каждой структуры. Single static action говорит, что желательно, чтобы, если я дам тебе имя какого-то поля, оно писалось в одном месте — вот в этом файле, на этой строчке.
[29:04] Александр: Так, я понял. Давай теперь немного перескажу, чтобы уложилось у всех слушателей и у меня. Я представил программу — вот эти вложенные циклы. Но это больше как логика, какая-то ветвистая. А эту таблицу я представил как поле X на Y, в котором есть состояние. И цикл говорит: «Сейчас состояние вот такое, а сейчас — вот такое». То есть у нас множество всех состояний, а мы циклами переключаемся из одного в другое. Это представление ошибочно или нет?
[29:46] Алексей: Я не совсем понимаю, что ты имеешь в виду под словом «состояние». Состояние — это либо всё содержимое оперативной памяти, то есть состояние программы. В более раннем понимании этого слова состояние — это просто instruction pointer: под state, если мы обсуждаем state-машины, подразумевается номер инструкции, который сейчас будет исполнен. Но поскольку никто уже не следит за этим курсорчиком, под state часто подразумевается всё состояние всей программы — то есть всё, что в RAM. А ты имел в виду некоторую память, такую как табличка: у неё есть несколько полей, у полей есть значения, и мы эти поля читаем, записываем, апдейтим.
[30:36] Алексей: Давай возьмём, скажем, поиск числа в массиве — тот самый одинарный цикл, написанный на C++. Есть элементы массива — это инстансы вот этой структуры, мы по ним ходим и в цикле их читаем. Что тут ещё обсуждать? Это query, read-only-функция. Давай посмотрим на функцию, которая что-то меняет.
[30:58] Александр: Поиск ничего не меняет. Ну, поиск минимума — у него минимум будет записываться каждый раз, если найден новый.
[31:15] Алексей: Ну, это тоже интересно. Бывает так, что программа переписывает что-то, что ей передали через командную строку, — потому что ей задали набор параметров, который не имеет смысла, и она выбирает разумные дефолты. И если уж переписывать, так один раз, чтобы в программе было одно место, где ты можешь эту переменную заменить.
[31:38] Алексей: Есть такое правило — «давайте не будем иметь глобальных переменных». Чем это обусловлено? Ответ: ничем.
[31:46] Александр: Ты следуешь этому правилу?
[31:48] Алексей: Глобальных переменных у меня в программах нет, если нет другого пути. На Rust, когда я последнее время программировал, статики у меня иногда используются, но они read-only, инициализируются один раз. А глобальных мутируемых переменных нет. Во-первых, это многопоточные вопросы: непонятно, volatile они или нет, гонки. Давай от этого уйдём. А второе — если они глобальные, значит, к ним есть доступ из любой точки программы, и тогда любая точка может менять состояние этой переменной. И я не понимаю её жизненный цикл, не понимаю, что с ней происходит как человек. То есть это правило придумано не против глобальных переменных, а против того, чтобы их менять из разных мест.
[32:46] Алексей: Признаюсь, я на самом деле потерял возможность писать программу без глобальных переменных. Я раньше следовал всем этим принципам, но постепенно вышел на другие. Я пишу программы только с глобальными переменными, и все они растут из одного места. Как программа растёт из main и дальше разветвляется, так у меня все переменные растут из одной структуры, которая называется db, база данных, и тоже разветвляются. Таким образом они все глобальны. Любая переменная, у которой всего один инстанс в программе, просто кладётся в db как поле. А если у неё другая кардинальность, то она через тот же db reachable — в массиве или через какой-нибудь линк-лист. Но они все при этом глобальны.
[33:46] Александр: То есть функции в C++ ты вызываешь, и они обращаются в эту db, берут оттуда нужный им стейт, а если нужно обновить — прямо тут же на месте обновляют? Или как-то вызывают другую функцию?
[34:08] Алексей: Да, они обновляют. Но вводится правило: ты не можешь из файла A пойти и менять переменные, которые меняются в файле B, потому что ты нарушаешь single static action. Вызови функцию, которая написана в файле B, и приди к изменению этой переменной через какой-то стандартный канал.
[34:33] Алексей: Открою секрет. Ты писал какие-то программы на JavaScript. DOM — это разве не глобальная переменная?
[34:39] Александр: Ну да, да.
[34:44] Алексей: То есть она абсолютно глобальная, и под ней сидит дерево всех элементов на странице — и это самый лучший способ жить. Представь, что этого не было бы вообще: ты бы не знал, как найти элемент на странице. Идея, что программа работает с записями, а записи сидят в базе, а база одна, глобальная, — она и делает все переменные глобальными. Нам навязали неправильную идею люди, которые хотели, чтобы программирование было контекстно-свободным. Была такая тема — не хотим, чтобы язык парсировался как контекст-фри-грамматика, чтобы его можно было из любой точки отпарсировать как рекурсивное выражение, чтобы контекста не было. И оттуда выросла идея, что наши функции, чтобы быть корректными, будут просто делать какие-то трансформации. Но это не работает по жизни. Потому что у тебя одно окно на экране, и ты должен заменить картинку на следующую так, чтобы она не мерцала, — ведь ты браузер программируешь. Как тебе следовать этой идиотской мысли, что ничего ниоткуда недоступно, а можно только какую-то трансформацию делать? Не работает это.
[36:04] Александр: У тебя правильный взгляд.
[36:06] Алексей: И он, кстати, самоподобный. У тебя есть компьютер, там есть память, и в памяти сидят байты. Эти байты абсолютно глобальные, у каждого свой уникальный адрес. Дальше твои программы эти байты трансформируют. Твоя маленькая программка в своём виртуальном адресном пространстве изолирована, чтобы ничего не грохнула, но у неё всё равно есть байты, и она их меняет. Мы эти байты структурируем, потому что хотим оперировать более ёмкими понятиями: заводим структуры, массивы структур, какие-то индексы. И мы говорим, что такая-то функция пойдёт и порежет наш DOM, Document Object Model, заменит все кнопки на чекбоксы. Она делает трансформацию в этом объекте — не просто возвращает тебе массив, который ты потом должен куда-то применить, а производит замену. Это транзакция с этой базой данных. Эту идею легко понять, оттестировать, реализовать — и всё хорошо, программа завершена.
[37:18] Александр: То есть все переменные глобальные, все действия над конкретной переменной производятся в одном месте, чтобы можно было подумать, правильно они производятся или нет.
[37:29] Алексей: Весь код идёт из одной точки — через main, и все данные идут из одной точки — через db. На мой взгляд, это правильное расщепление, правильная ортогонализация идей программирования. Абсолютно два разных мира: мир данных и мир инструкций. Мир данных структурирован как таблицы, которые индексированы. Мир инструкций структурирован как вложенные дорожки для CPU, по которым он будет бегать. Процессор трансформирует данные.
[38:05] Александр: И получается, что мы сейчас в контексте принципа single static action говорили. Он говорит, что у тебя есть static — грубо говоря, единственная база данных с данными — и есть единственное место модификации этих данных. Но чтение может быть из разных мест? Или тоже только из одного?
[38:28] Алексей: Сколько угодно. Никаких private, ничего этого. Ни в одной из моих программ я вообще не использую никакие объявления доступа, кроме самых тривиальных. Ничего private, никаких виртуальностей, никаких темплейтов я никогда не использую.
[38:47] Александр: Окей. И эти два принципа — SESE и single static action — раз мы про них говорим, они везде воспроизводятся, ты им везде следуешь, и они задают такой авторский стиль, характер: то, как программа строится, как из пластилина всё вылепляется. И это интересно. Знаешь, чем? У меня возникла мысль, что это то, что абсолютное большинство программистов-слушателей… Многие, наверное, такое даже не писали. Большинство людей так не пишут код: у них либо языки с объектной моделью, ООП-стиль, инкапсуляция, всё такое. И мне какая мысль: это же и есть какая-то истинная инженерная свобода. Нам же говорили, что правильный код — это чистый код Боба Мартина, вот так надо писать. И на собеседовании на заре карьеры постоянно спрашивают, и ты учишься этому…
[40:16] Алексей: И McConnell, разве нет?
[40:19] Александр: McConnell — это, по-моему, Code Complete. Code Complete, точно. Короче, вот эти все ребята — они, по сути, одного поля ягоды, я их так воспринимаю. И ты сейчас просто дорастаешь до уровня, когда… Мартин Фаулер — это Refactoring, а…
[40:38] Александр: Боб Мартин — вот, вспомнил. Всю тройку назвали. Я к тому, что у человека, когда он входит в карьеру, первые проекты — это как будто какая-то истина, что вот такой код надо писать. А сейчас мы говорим с высоты уже другого опыта. Ты построил, я думаю, много систем, и мы поговорим про ту, которую ты сейчас строишь, — это огромный небоскрёб, тот самый. И он построится по совершенно другим правилам, которые в индустрии редко где используются. И для меня это одно слово — инженерная свобода. Это очень круто.
[40:38] Алексей: Может, наоборот, какая-то несвобода. Как говорится, свобода — это осознанная необходимость. Вот у меня скорее есть осознанная необходимость. Меня что интересует? Чтобы небоскрёб стоял, и чтобы я рядом не бегал по этажам, не латал протекающие трубы, не заменял проводку. Я не хочу находиться рядом с проектом и заниматься maintenance. Он просто должен работать, точка. Как писать код, чтобы можно было закрыть эту крышку? А не так, как люди любят: «Я не уверен в этой функции — вставлю print и let’s ship it». Потому что когда этот print распечатается, кто-то пожалуется, до меня он добежит, я посмотрю и скажу: «А, вот теперь я знаю, что был неправ». А раньше я был просто не уверен. Это все программисты, я тебе описал всех программистов. Любой, кто оставляет в коде print, рассчитывает именно на это — он мысленно его не отпустил, он всё ещё с ним в отношениях. А если ты написал правильный код, там не должно быть никаких принтов. Он должен просто бежать.
[42:49] Алексей: Наверное, можно добавить такую страшную тему — исключения. Они очень заразны и «полезны». В программах определённого стиля, которые мы назовём RPC-программами, — ты делаешь какой-то query и возвращаешься, — там можно использовать исключения. Но вообще нельзя. Потому что это следует из SESE. Твой exception — это не что иное, как многоуровневый return на неизвестно сколько уровней вверх. Это хуже, чем return. Может быть, на 150 уровней вверх. И если ты такой гений, что докажешь, что по всем путям, через все возможные возвраты, программа соответствует всем инвариантам, которые ты, дай бог, ещё и назвать должен, — окей, тогда поставь этот throw сюда. Но если ты этого сказать не можешь — а на самом деле ни один программист про свою программу ничего предсказать не может…
[43:46] Алексей: Доказываю мысленным экспериментом. Предложу кому-нибудь написать самую элементарную программу и подписаться, что она скомпилируется и пробежит. Никто не возьмётся даже за программу из двух строчек, потому что ты где-нибудь точку с запятой забудешь. Практически невозможно написать сходу так, чтобы она побежала. А что это значит? Что ты смотришь на эти три строчки и не видишь их. Ты их не можешь интерпретировать, не понимаешь. Ты только приблизительно понимаешь, что написал. Дальше тебе нужно, чтобы она побежала — была ошибка компиляции или неожиданный ответ, — и ты понимаешь, где опечатался. Без этого цикла взаимодействия с тем, как компьютер интерпретирует программу, ты её написать не можешь.
[44:41] Александр: Ты с компьютером, да, вайб-кодишь.
[44:44] Алексей: Самый оригинальный вайб-кодинг — это когда ты просто run, run, run, что-то программа печатает, ты её отлаживаешь. Это тоже ведь вайб-кодинг. Поскольку мы не можем написать правильную программу… Мы вообще придумали терминологию «bug», баг в программе. Нет, не в программе баг, а у тебя в голове. Просто отсутствует понимание того, что ты написал. Так было бы честно сказать, без всяких обид. Мне не обидно, что я не понимаю свою программу, для меня это аксиома. Но из этой аксиомы я делаю вывод: моя программа должна быть максимально приближена к той, которую я всё-таки ещё могу понять. И именно поэтому я исключаю сложные способы написания в пользу простых. Non-local return — это сложный способ, потому что я смотрю на кусок кода, а где-то за первой строчкой, которую я даже не вижу, висит return, который промахивает весь этот экран и оказывается где-то внизу. Это сложно, такую программу я до конца понять не могу — поэтому я её не хочу. Я хочу простую, максимально приближенную к тому, что могу понять, обладающую наименьшим количеством сюрпризов. Отсюда и исключение этих исключений: exception — это то, куда прячется всё непонимание того, что я написал.
[46:10] Александр: Согласен. Я не знаю, какое значение вычислить и вернуть в этой функции, поэтому просто брошу exception.
[46:17] Александр: То есть это отказ от ответственности дальше интерпретировать в голове ход программы и её детерминировать. И ты такой: всё, пофиг.
[46:27] Алексей: Да.
[46:29] Александр: Ну, это же и называется throw — кинуть. Ты мысленно кидаешь это: «Да ну нафиг».
[46:35] Алексей: Буквально. Причём за спину кидаешь. Главное — не оборачиваться после этого. Там поймают.
[46:45] Александр: Слушай, мне кажется, у наших слушателей может возникнуть вопрос — всё-таки инженеры слушают, у меня он тоже был. Вот с этой единой базой данных, где хранится весь стейт программы. Очевидно же — многопоточный доступ. Как он рулится? Какой подход используется в твоих продуктах, чтобы contention не создавать? Они же у тебя многопоточные?
[47:15] Алексей: Нет.
[47:17] Александр: Ага. Вот в этом и ответ кроется: у тебя однопоточный процесс. У процесса есть стейт, и он его в одном потоке фигачит.
[47:26] Алексей: Ну, соответственно, масштабирование. И тут тоже есть семантический трюк. Если я на твоём компьютере запущу 128 однопоточных программ и использую таким образом все ядра, которые у тебя есть, — это всё ещё однопоточная программа?
[47:46] Александр: Да, я именно вот это и хотел. Это уже не программа. По сути, это какой-то ансамбль программ, процессов. Но вопрос в чём: утилизируем ли мы ресурсы, машины — оптимально или нет? Твой ответ — да, но мы это делаем за счёт процессов, а не потоков.
[48:14] Алексей: Я не знаю, тут я прав или нет, но я последние несколько лет работаю с понятием «дерево программ». Я создаю подпрограммы из одной программы, они все однопоточные и общаются друг с другом только путём посылания сообщений. Только один способ общаться — посылание сообщений.
[48:37] Александр: Посылание сообщений — то есть это такая а-ля актёрная модель, плюс-минус.
[48:44] Алексей: Не знаю, как её назвать — актёрная или просто message passing. Но есть такой computer scientist, C.A.R. Hoare. Он написал книжку Communicating Sequential Processes. Книжка в каком-то смысле очень интересная и очень влиятельная, потому что он написал такую алгебру коммуницирующих процессов. Они чуть-чуть не похожи на то, что использую я, потому что у них мгновенная доставка сообщений, а у меня, например, не мгновенная. Но эта книжка повлияла на многих. Язык Go, например, со своими каналами — это прямой наследник этого подхода. Операционная система Plan 9 from Outer Space, 9P — тоже прямой наследник. Идея такая: мы пишем однопоточные программы, потому что мы настолько не можем понять даже однопоточную программу, что давайте не будем писать многопоточные.
[49:52] Алексей: Признаюсь, свою первую многопоточную программу я написал в 93-м году. Мне просто повезло: летом я стажировался в одном институте в Нью-Джерси, куда прислали компьютер SGI Onyx на 16 процессоров, с 64-битной памятью, размером со шкаф. 93-й год. Никто им не пользовался и не знал как. А я умудрился получить доступ администратора, да на него просто никто не смотрел. И я стал писать программы для себя и экспериментировать с многопоточностью. Потом я потерял к нему доступ, но всегда хотел вернуться к этой теме. Появились dual-core-компьютеры, все стали думать, как воспользоваться вторым ядром: создать критическую секцию, mutex, ещё что-то. Я тоже играл с этим лет десять. И потом я понял, что я вообще разучился программировать. Я смотрю на текст на экране и не только не понимаю, что он делает, — я даже не понимаю, в каком порядке это исполнится. Всё, полная жопа. И я решил навсегда закрыть эту тему, больше не писать многопоточные программы и вернуться к однопоточным.
[51:41] Алексей: Мне в этом помогли, потому что сам я не смог бы. Очень трудно отказаться, когда ты программист и тебя всё время мучает идея производительности: у кого программа быстрее? Это реально важно.
[51:58] Александр: Реально важно.
[51:59] Алексей: Если моя программа в два раза быстрее — это может быть неважно, если запустить её один раз. Но если она уже не помещается в компьютер, то возникает разница между одним компьютером и двумя. Сделал программу в два раза быстрее или использующей в два раза меньше памяти — она помещается в компьютер, а другая не помещается. Поэтому, если размер задачи большой, производительность очень важна — это разница между дата-центром и одним компьютером, её не надо объяснять. Отказаться от многопоточности было невозможно. Но мне повезло. Это был, наверное, 2008 или 2009 год, и моя компания занималась высокочастотной торговлей. Я ставил компьютеры в дата-центры рядом с биржами и торговал на них. Эти компьютеры пересылали маркет-данные, квоты по мультикасту по моим собственным сетям, они же торговали, а дальше нужно было собирать весь этот distributed-стейт в одну точку, считать риск. Это торговая система. Я сидел и думал, как всё это убыстрить.
[52:45] Алексей: Самым главным ограничением всегда был interconnect. Тогда он был гигабитный, можно было поставить 10-гигабитный, но если заглянуть в eBay, там продавались страшные девайсы — Myricom 20-гигабитный, 40-гигабитный InfiniBand, — и очень хотелось их купить. Я пошёл, купил, освоил InfiniBand-Ethernet gateway и узнал, что есть целая тема scale-out computing: ты просто пишешь инструкцию move в память прямо у себя, а этот байт оказывается на другом компьютере. Потому что, когда ты кладёшь байт в свою память, он прилетает не в память. Память — это всего лишь девайс, который сидит на шине, и там есть специальный роутер, который этот байт туда направляет. На эту шину можно посадить другой девайс, например карточку, и байт прилетит на карточку. А карточка уже знает: «Ага, сюда записали байт, перешлём его на другой компьютер». Почти так это устроено. Это используется в суперкомпьютерах. Представь: какой-нибудь учёный на Fortran пишет симуляцию погоды. Он не хочет ничего знать о многопоточности, он пишет формулу — заводит массив триллион на триллион и что-то итерирует по этим ячейкам. Вся эта память должна как-то разместиться на суперкомпьютере, и всюду, где ты прочёл ячейку, которая должна быть на другом компьютере, оно должно переслаться, чтобы простая программа исполнялась параллельно. Там есть OpenMPI, аннотации, которые компилятором превращаются в другую программу, которая почти так же быстро бежит на распределённых компьютерах.
[55:29] Алексей: Так или иначе, есть специальные карточки, которыми можно управлять, и они очень быстро пересылают байты. Скорость пересылки — приблизительно 600 наносекунд от одного хоста до другого. То есть ты записал, и через 600 нс на соседнем компьютере в памяти это число можно прочесть, записать обратно — через 600 нс оно будет у тебя. Это, по крайней мере на тот момент, было быстрее, чем обратиться в Linux-kernel: делаешь системный вызов, возвращаешься через микросекунду, а здесь быстрее. Мало того, ты можешь послать сообщение, которое получит не один компьютер, а пять, потому что ты посылаешь его InfiniBand-мультикастом. Это уже никакой kernel не может покрыть, потому что ты из одной точки общаешься с несколькими.
[56:13] Алексей: К чему я всё это рассказываю? Увидев эту технологию, я в каком-то смысле увидел будущее. Я понял, что зря мучаюсь: надо просто тупо писать однопоточные программы, а железо догонит. Оно уже есть. Кстати, все AI сейчас бегут на картах Nvidia. Карты Nvidia — это карты Mellanox, они купили Mellanox. А Mellanox — главная израильская компания, которая разрабатывала InfiniBand. И все эти карты умеют делать kernel bypass, они уже те 600 нс сбрили, уже меньше. Они сейчас со скоростью 800 гигабит в секунду фигачат байты во все стороны.
[56:58] Александр: Удивительно. Это прикольно в том плане, что ты в какой-то момент понял, куда всё идёт, и решил не отвлекаться на условный хайп — потому что вокруг-то все программисты говорили ровно противоположное. Я могу это представить, мне всего 29 лет, но в целом могу. И можно будет потом в конце про это поговорить: а что сейчас ты видишь, куда это идёт? Мне кажется, твой талант видеть что-то наперёд мы применим в текущем разрезе. Но напомню, что мы всё ещё про человеческое программирование. Мне дико понравилась идея однопоточных программ, потому что я начал представлять, как у тебя система выглядит. Вот ты разрабатываешь большой, серьёзный продукт. Я про него услышал впервые как про убийцу Кафки, если что. Ты строишь по таким принципам убийцу Кафки, который сильно быстрее. И мне интересно: как тогда у тебя эта программа выглядит? Есть какой-то один процесс — контроллер, бутстрапер, — который что-то делает, и как происходит жизненный цикл от запуска CLI до момента, когда началась бизнес-логика: юзер начал складывать данные в очереди и процессить?
[58:32] Алексей: Ты как юзер подсоединяешься к gateway этой системы и в RPC-режиме с ним общаешься: говоришь запрос — тебе приходит ответ.
[58:44] Александр: Это я как юзер?
[58:45] Алексей: Это ты как юзер.
[58:46] Александр: Окей, а ты как мейнтейнер, как администратор системы?
[58:49] Алексей: Я как система встал, произвёл интересную процедуру recovery, потому что либо я поднял свежую систему, либо встал после того, как грохнулся.
[59:02] Александр: Давай нарисуем в головах. Ты имеешь в виду — вот ты как бинарь, этот единственный процесс. Ты встаёшь, у тебя есть что-то на вход: файловая система, конфигурации. То есть что такое ты и что ты видишь в момент, когда тебя только запустили?
[59:22] Алексей: Я расскажу про эту систему — она настолько простая, что, думаю, даже без картинки всё можно понять.
[59:31] Александр: Давай, что за система?
[59:31] Алексей: Сначала я называл её Data Streaming Platform, потом понял, что это Distributed OS, — теперь называю Distributed OS. Это файловая система, где единицей хранения является стрим, и есть две операции: дописать сообщение к этому стриму или прочитать уже записанное сообщение из него. Дописать сообщение может любой процесс к любому стриму. Забудем про ACL и доступ на секунду. Ты просто посылаешь его в определённую точку, и оно допишется к стриму. Эта точка одна, она называется transaction- или sequencer-модуль — я называю transaction-модуль, потому что там зашита та единственная транзакционность, которая есть в системе. Сообщение аппендится, добавляется к этому потоку: есть нулевое, первое, второе, 147-е сообщение. Всё состояние системы на данный момент — если её просто грохнуть, выключить кластер из розетки, достать лупу и посмотреть, что на диске, — это сколько-то стримов, и в каждом стриме сколько-то сообщений, от нуля до того, сколько записалось. Всё состояние системы, больше ничего нет.
[1:01:01] Алексей: Теперь интересное свойство. Что такое Kafka? Это сервис, который позволяет дописать сообщение к потоку и потом его прочесть. У них очень много слов, они как-то растопырились на сотни тысяч строчек кода. На самом деле то, что я описал, легко делается небольшими усилиями: дописал, прочитал. Дальше возникают хитрости, когда ты хочешь, чтобы много человек читало, была большая производительность, чтобы лишний раз ничего не копировалось, не было ожиданий, — это всё оптимизации. Концептуально это именно так. Теперь управление такой системой. Я ввожу специальный поток, который называется /sys/cmd, — это как юниксовские пути: /sys — системная директория, cmd — команда. И все процессы в моей системе, когда встают, просто начинают читать /sys/cmd с нуля. У них нет файловой системы в понимании Unix, её не существует — есть только мир этих стримов и только две операции. Любой процесс, который встаёт, читает /sys/cmd с нуля, интерпретируя эти сообщения, и понимает, сколько нод в кластере, какие есть стримы, видит, что зашёл какой-то человек, что человек отсоединился. Все события, связанные с кластером, он видит в этом потоке. Поэтому, чтобы поговорить с системой, ты заходишь в gateway, аутентицируешься и дописываешь сообщения в /sys/cmd. Их получают все потоки, интерпретируют — и ты таким образом управляешь кластером.
[1:02:37] Александр: Есть такая аналогия. По сути, в Linux всё есть файл — у тебя всё есть стрим.
[1:02:46] Алексей: Да. У меня всё есть стрим, и он append-only. Тебе даётся 2 в 64-й сообщения — и больше не даётся.
[1:02:52] Александр: Смотри, у меня как у инженера сразу вопрос. Этот системный стрим, который все читают с нуля, — соответственно, если происходит рестарт, я опять стартую с нуля? То есть всю историю системы я проигрываю, или там есть эвристики, которые позволяют скипнуть?
[1:03:17] Алексей: Есть механизм снапшотов, где ты читаешь не этот стрим, а некий другой, и там содержится некий goto — «продолжи отсюда». Пропустим это, потому что это концептуально загрязняет описание.
[1:03:31] Александр: Да, концептуально загрязняет, но проблемы с невероятно долгим стартом, чтобы это влияло на поддерживаемость, в системе нет — она так или иначе решена. То есть всю историю вселенной каждая нода не проигрывает.
[1:03:45] Алексей: Она должна проиграть что-то, что важно для неё, — эти снапшоты уникальны для каждого процесса. Я говорю, это грязно, снапшоты — грязное дело. Всю систему можно описать и обсуждать без снапшотов, корректность — без снапшотов. Снапшоты нужны, чтобы сократить прочтение триллиона записей до миллиона. Мы не хотим читать триллион записей. Но идея, что ты управляешь системой при помощи той единственной операции, которая в ней есть, — стриминга, — концептуально её упрощает. В Kafka есть специальный metadata-сервер, там используется один механизм, здесь — другой.
[1:04:31] Александр: ZooKeeper, Raft и все эти дела.
[1:04:34] Алексей: Давай просто скажем, что ничего этого нет — есть просто стриминг. Ты пишешь некий поток, и твои модули эти сообщения видят. Элементы твоей системы сами являются консюмерами этих стримов. Есть понятие: продюсер — тот, кто послал сообщение, консюмер — тот, кто прочитал. Так вот, мои модули сами являются и продюсерами, и консюмерами. All the way down. То есть получается такая self-similar-система.
[1:05:03] Александр: SESE тут тоже в каком-то виде сразу вспоминается.
[1:05:06] Алексей: Ну, всё есть стрим, по-другому не скажешь. И через стрим идёт управление. Чтобы обслужить внешних продюсеров-консюмеров, мы в каком-то смысле вводим ещё одних продюсеров и консюмеров.
[1:05:19] Александр: И тогда, чтобы твоя система работала хорошо, основная нагруженная, горячая часть должна быть максимально быстрой. И это тот самый сервис, которым ты и сам пользуешься: «я же сам это консюмлю, у меня логика через это реализована, я по-другому не могу, поэтому это точно хорошо работает». Такой дог-фудинг в каком-то виде.
[1:05:40] Алексей: Это полный дог-фудинг. Там скорости оказываются порядка микросекунд. Сейчас мы поддерживаем три метода коммуникации: InfiniBand, как всегда, Ethernet Multicast и Ethernet Unicast. Это наши три главных способа посылать байты.
[1:06:03] Алексей: Вот Kafka — чем хороша? Тем, что она довольно быстро процессит большое количество сообщений — по крайней мере на момент, когда выпустилась. Она может много сообщений процессить за счёт параллелизма, и одновременно много консюмеров и продюсеров могут быть.
[1:06:21] Александр: Вот в этой концепции твоего подхода, что ты пишешь однопоточные программы, как ты решаешь, что у тебя может быть огромное количество клиентов? Как происходит распределение нагрузки?
[1:06:34] Алексей: Сначала отвечу так: может, они и однопоточные, но их много. А раз их много, значит, и ресурсов много. Но давай рассмотрим конкретный пример — сначала опишу, как работает Kafka. Есть сервер, на нём бежит брокер. Это, собственно, Kafka и есть. Ты подсоединяешься к брокеру и говоришь: «Хочу дописать сообщение вот сюда». И вот это «сюда» — это topic плюс partition number. Вместе это называется… topic — это subject name или stream name, грубо говоря, orders, какое-то слово. А partition number — ещё одно число. Этот брокер говорит: «Знаешь, я сюда не пишу, тебе нужно к другому брокеру. Bye-bye». Ты говоришь: «Ладно, дай мне metadata request — какие есть брокеры и кого они сервируют». «А, окей, понятно». Идёшь в тот брокер: «Хочу добавить сообщение в orders, partition 47». Он говорит: «Без проблем, добавлено, вот тебе base offset, можешь теперь его прочесть». И консюмер приходит к какому-то брокеру: «Дай мне из orders 47 все сообщения, которых я ещё не видел, начиная с такого-то». Он говорит: «Это не ко мне, это leader-реплика, иди туда».
[1:08:10] Алексей: К чему я всё это веду? Kafka на самом деле — это single-host-система. Ты поднял программу, и она каким-то образом получила подмножество вот этих topic-partition. Забудем на секунду. Она дописывает и сервирует сколько-то этих стримчиков, каждый из которых — просто последовательность сообщений. Теперь, понятие topic-partition неправильное. Точнее, понятие topic неправильное, потому что оно не описывает, куда ты пишешь. Вот topic-partition — это то, что я называю стримом. Это просто некий уникальный объект, идентифицирующий последовательность сообщений, к нему можно что-то дописать. Когда ты сдаёшь topic любому продюсеру, дальше в Kafka продюсер должен пойти к серверу, узнать, сколько там этих partition — скажем, 50, — выбрать, в какой partition он на самом деле пишет, сформировать ключ, который есть orders и число 47. И вот orders 47 — это и есть тот объект, куда дописываются сообщения. Можно назвать его стримом.
[1:09:15] Александр: Блин, ты сейчас вскрыл мою внутреннюю подсознательную претензию к Kafka. Я не понимал, почему бессознательно её не люблю. Думаю: ну, вроде система. А потом ты начинаешь — topic, а в какую partition? А consumer group? Я записал, а оно реально… Я не уверен, что, начиная читать из topic, я получу то, что туда записал, потому что не уверен, что читаю из той партиции. И я такой: а нафига мне такая система вообще? Я всю жизнь усложняю.
[1:09:45] Алексей: Это странно. Я понимаю, как они к этому пришли — хотели создать простой интерфейс. Но мы не боимся цифры 47. Если просто сказать, что в системе есть 50 потоков, которые называются orders 1, 2, 3... 50, и если хочешь писать round-robin во все, ты говоришь orders-wildcard, и это значит «пиши во все 50». Вот так было бы понятнее. Короче, для меня есть стрим — одна-единственная append-only-сущность, файл, последовательность в системе. И в систему всё так и пишется. Мы поддерживаем протокол Kafka: мы решили не публиковать свой протокол, а поддерживать протоколы из индустрии. Таким образом мы совместимы со всей экосистемой, потому что иначе нам надо переубеждать людей, почему надо нами пользоваться. А так просто дал им IP и порт — и они уже могут воспользоваться. Так или иначе, у нас есть и native-режим.
[1:10:53] Александр: Вопрос. Что с отказоустойчивостью?
[1:10:55] Алексей: Не, ну ради этого всё и пишется. Давай я отвечу на твой вопрос про load balance. Я начал ответ издалека, описав, как работает Kafka-брокер, — что на самом деле это single-host-система. Он принимает сообщения на публикацию, дописывает их в файл: открывает файл на диске, дописывает туда сообщения. Дальше открывает соседний файл — ну, он у него уже открыт — и дописывает туда offset этого сообщения. Это у него такой индекс. Есть файл с месседжами, туда просто дописывается сообщение, а есть файл-индекс, туда пишется по 8 байт или по 4 байта, я забыл, и каждое вхождение — это позиция сообщения в первом файле. Простой индекс. Теперь, чтобы прочесть сообщение номер миллион 141, я же не знаю, где оно, — они все разной длины. Я открываю индекс-файл, иду на offset миллион 141 помножить на 4 байта, читаю 4 байта, получаю offset, иду в первый файл, читаю оттуда сообщение — и вот я его прочитал и застримил человеку. Вот, собственно, вся Kafka.
[1:11:53] Алексей: Теперь хитрость. Прежде чем сервировать это сообщение, Kafka должна убедиться, что она сделала синхронизацию — отлила все новые сообщения в реплику и получила OK от этой реплики, что те тоже записаны. Под «записаны» имеется в виду, что мы вызвали системную функцию write, а не то, что мы с лупой посмотрели и убедились, что на диске действительно есть эти байты. Это важное и неважное различие. Если оно в памяти — мы, по сути, его получили. Никто не вызывает fsync после каждого сообщения и не ждёт, пока диск всё запишет, потому что иначе производительность падает драматически.
[1:12:56] Александр: Здесь тоже, наверное, важно сделать паузу. У нас появляется реплика у каждого стрима: есть процесс, отвечающий за обработку стрима, и есть процесс, который является репликой этого стрима. И когда к нам приходит запись, мы идём… Я сейчас сказал «стрим», а мы вообще про Kafka говорили — то есть про topic плюс partition. И вот, записав это, Kafka, чтобы была отказоустойчивость, если лидер упадёт, идёт на другую ноду и дожидается ответа, что «да, записано». Что такое вообще запись в Kafka? При разных настройках по-разному. Может быть только подтверждение от лидера, а можно сделать так, чтобы лидер плюс реплика. И вот мы обсуждаем, что такое «да» от реплики и «да» от лидера. «Да» от лидера, я думаю, — это всё-таки что-то, что записалось в WAL. А от реплики мы можем просто сказать, что она приняла сообщение, — то есть процесс начал обработку, но не записал на диск. И это важно, потому что если в этот момент процесс обрубается, электричество вырубает, то записи у него не будет, несмотря на то, что мы получили подтверждение, — потому что fsync не вызывался.
[1:14:32] Александр: А fsync не вызывается почему? Потому что это долго, и тогда ты не можешь позволить себе на каждую запись его вызывать.
[1:14:40] Алексей: Нет смысла. Если хочешь большей надёжности — сильнее реплицируй данные на большее количество реплик. Если они получили, то, скорее всего, электричество не отрубится одновременно во все три, а если отрубится — используй redundant power supply, два источника, или battery backup. Или географически распредели свою сеть, чтобы она в разных регионах бежала. Но все, кто так пробовал, от этого отказались, потому что это очень дорого. Компьютеры, как правило, не грохаются первые несколько лет. Ты весь бюджет компании тратишь на эту странную репликацию, которая тебе вроде бы ничего и не даёт. Поэтому географическую репликацию на Kafka никто не использует, это просто очень дорого. Реплицируют локально для надёжности, а потом, после того как реплицировал, ты можешь стримингом, как консюмер, просто взять этот поток и куда-то его отослать. Это намного менее затратно, чем пытаться дистрибутировать сам кластер, чтобы все его внутренние коммуникации бегали через континенты.
[1:16:00] Алексей: Так или иначе, я тебе сейчас опишу альтернативную систему, как можно посылать байты, чтобы было лучше. С Kafka чуть-чуть надо договорить. Представь, приходит какой-то продюсер и пишет, условно, 100 гигабит в секунду. А у нас сеть такая — 100 гигабит. Предположим, все 100 можем прогнать. Теоретически брокер может весь этот поток получить и его же сервировать. А что, если он будет сервировать его 10 клиентам? Каждый из этих консюмеров получит 10 гигабит. Брокер не может выдать больше 100.
[1:16:41] Александр: Труба ограничена.
[1:16:41] Алексей: А это значит, что данные начнут накапливаться на диске бесконечно, потому что ты их не отливаешь. Стало быть, максимально эффективная пропускная способность этого брокера превратилась в 10 гигабит.
[1:16:54] Александр: Вспоминается теория ограничений Голдратта, где пропускная способность системы равна пропускной способности самого узкого звена.
[1:17:05] Алексей: Ну да. Как это можно вылечить? Например, если бы наш брокер все эти сообщения мультикастил на 10 других серверов, и каждый из них сервировал бы их со скоростью 100 гигабит, а 10 клиентов подсоединились бы туда.
[1:17:23] Александр: То есть делегировать работу с клиентами другим серверам.
[1:17:26] Алексей: Scale-out. Наш однопоточный процесс получил байт, послал его на мультикаст-адрес, а на этот адрес подписаны 10 брокеров — там динамическая подписка, они подписались, когда туда пришли потребители. Head-of-line blocking — не head-of-line, а просто blocking — исчез. Система больше не ограничивает тебя 10 гигабитами, теперь 100. Кстати, если у тебя unicast-коммуникация, то тоже можно получить 50: твой нод посылает сообщение два раза на два других, каждый из них посылает ещё два раза, и так за три прыжка получается 8 нод, каждая из которых может сервировать 100 гигабит. Ты только увеличил latency. Но они, извини, не 100 гигабит — они могут сервировать два раза по 50, так что могут 16 клиентов поддержать. Зато ты увеличил latency на три хопа. Но больше 50 с unicast не получишь.
[1:18:45] Александр: Окей, теперь я понял. То есть мы находимся в этом трейд-оффе: latency против throughput. Мы увеличиваем throughput путём добавления хопов, но latency увеличивается, потому что нужно пройти больший путь байтиками.
[1:19:07] Алексей: В постановке задачи, где есть 10 консюмеров и все хотят эти 100 гигабит, им важнее получить эти данные, чем вообще не получить. Если ты их не можешь послать — это infinite latency. Так что сказать, что latency ухудшилась, неправильно. Она стала не равна бесконечности. Для этого количества консюмеров она улучшилась: была бесконечность — стала три хопа.
[1:19:33] Александр: Да, да. И тут в трейд-оффе мы ещё должны учитывать: latency ты когда говоришь — для одного консюмера на пустой системе, в дистиллированном бенчмарке, пинг-понг? Или на реальной системе с этими консюмерами, для которых она дизайнится, — реальный latency?
[1:19:54] Алексей: Ну да, use case. Тогда реальный latency даже вырастает. Важный use case: если у тебя есть переконфигурация этого роутинга — она у нас есть, но сейчас не такая продвинутая, потому что продукт ещё не супер mature. Базовые кейсы мы поддерживаем, а сложные — пока в роадмапе. У нас есть дизайн-понимание того, как это надо сделать, но так, чтобы теоретически правильно поддержать все возможные конфигурации потребителей и посылателей, — такого ещё нет. И, возможно, никогда и не понадобится. Люди просто покупают достаточно большой кластер, или у них достаточно маленькие потоки, и эти потоки bursty. Не бывает такого, что тебе просто закачивают петабайтный файл нон-стопом. Как правило, если это реальные события, у них есть распределение прибытия: миллион сообщений, а потом вообще ничего. Час целый. А потом опять. Например, все финансовые рынки так устроены: у них есть некое внутреннее время, которое то ускоряется, то замедляется, и меняется пропорционально себе. Если на рынке ничего не происходит — минутами дохляк полный. А потом вдруг, как снежный ком, деятельность начинает нарастать экспоненциально. Это значит, что у рынка есть внутреннее время, изменение которого пропорционально самому себе: чем больше активность, тем больше она нарастает, а потом постепенно сходит почти в ноль. Короче, нагрузка, как правило, нелинейная, и теоретически покрыть все возможные кейсы нам важно, но на практике, может быть, не так важно.
[1:22:04] Александр: Окей. Ты хорошо описал на пальцах, как устроена Kafka. Думаю, там реально 100 строчек кода написал — вот тебе Kafka, по такой логике. Вопрос такой: по такой же схеме можешь рассказать, как у тебя на стримах это происходит? Вот к тебе пришло сообщение в твою систему, на одну из этих нод. Напомню, когда ты рассказывал про Kafka, ты такой: «Эту партицию я не обслуживаю, иди туда». Начинай с этой же точки — как у тебя в системе устроено?
[1:22:38] Алексей: У меня система состоит из нескольких практически идентичных нод. На каждой ноде бежит дерево процессов. В базовой конфигурации это 8 процессов, может быть 12, 16 — можно варьировать. Главная точка вариации — это количество gateway и количество commit-ов. Это то, с чем я играю. Чем больше gateway, тем больше можно поддержать пользователей, потому что у них есть overhead, связанный с TCP. Чем больше commit-ов, тем больше можно самортизировать доступ к диску, потому что не все диски быстрые. Оптимальная конфигурация использует NVMe, у нас есть специальный драйвер под SPDK — это способ доступаться к NVMe самым натуральным образом.
[1:23:31] Алексей: Теперь пришёл байт. Не буду углубляться в дерево процессов — сколько-то процессов бежит на хосте. Ты gateway, к тебе пришёл байт. Человек — все пути открыты, доступ есть, ACL проверены, аутентикация завершена. Тебе нужно опубликовать его. Если этот стрим сервируется на этой ноде — ты отправляешь его в sequencer. Если он сервируется не на этой ноде — ты отправляешь его в sequencer. Single static action. Ты всегда его отправляешь. Дальше этот sequencer, если стрим сервируется на этой ноде, его публикует — то есть назначает ему следующее число — и отправляет. Если стрим сервируется не на этой ноде, он его тоже публикует, но не в тот стрим, куда ты хочешь опубликовать, а в некий вспомогательный стрим, в который он может опубликовать и на который подписан другой sequencer. Публикация всегда состоится, и она должна состояться в том месте, куда можно опубликовать: это будет либо твой стрим, либо какой-то вспомогательный стрим. Тот другой sequencer, который собственно публикует, читает вспомогательный стрим, видит, что там есть запрос на публикацию, публикует и посылает подтверждение.
[1:24:52] Алексей: Теперь сообщение сервируется клиенту после того, как мы получаем доказательство, что оно получено n-ым количеством нод, какими-то commit-ами.
[1:25:05] Александр: Получаем мы это, будучи подписанными на какой-то стрим и ожидая оттуда какого-то подтверждения?
[1:25:10] Алексей: Это у нас фундаментальное понятие, потому что наши внутренние компоненты работают с ACK level 1. Это значит, что они читают сообщения, которые существуют только в одном экземпляре, чтобы вообще их можно было прочесть. А ты как внешний потребитель хочешь читать сообщения, которые существуют в двух или в трёх экземплярах. Таким образом, ты его получаешь не так скоро, но зато он реплицирован и надёжный. То есть один и тот же механизм работает и внутри, и снаружи: просто внутри ACK level низкий, а снаружи высокий. Чтобы реализовать более высокий ACK level, используется низкий. Внутри, как видишь, все эти ноды подписаны друг на друга при помощи того же самого механизма подписки, идентичного.
[1:25:59] Алексей: А механизм подписки устроен так: если ты какой-то модуль и хочешь на что-то подписаться, то ты просто… Что это значит? Ты хочешь увидеть первое сообщение из /sys/cmd, например. Значит, ты посылаешь куда-то в эфир просьбу прислать тебе первое сообщение из /sys/cmd. После того как получил, посылаешь запрос прислать тебе второе сообщение из /sys/cmd, и так далее. То есть все эти ноды всё время друг к другу разговаривают такими запросиками.
[1:26:34] Александр: Вопрос практический. Когда я представляю себе всю эту систему, это какая-то рекурсивная система — точно так же, как я представлял себе программу, с которой мы начали разговаривать. Я вижу это как какие-то ноды, и между ними сообщения перетекают, как в артерии. И вопрос: может же быть очень много сообщений. Как дебажить такую систему? Как понимать, что она работает так, как нужно, и ничего лишнего не происходит? Мне сложно это представить в голове.
[1:27:15] Алексей: Сразу скажу: когда ты запускаешь много процессов вместе и они начинают общаться, возникают паттерны взаимодействия, которые при единичном тестировании просто не проявляются. Мы этим и занимаемся, потому что это интересная область, где какие-то emergent phenomena. Тестируешь ты один компонент за раз. Возьмём, например, commit — это модуль, который пишет стримы на диск. На самом деле это огромная подсистема, но концептуально он очень простой. Он такой же подписчик, как и все. Подписка осуществляется в одном месте, из библиотеки, у которой небольшой API: вызов функции «подписаться» на такой-то стрим, диапазон сообщений в качестве параметров — первое и последнее, например ноль и бесконечность, 2 в 64-й минус один. Ты сформировал подписку на этот стрим от нулевого до последнего сообщения — и всё. Дальше библиотека гарантирует, что тебе будут поставляться сообщения в том порядке, в котором они публикуются, с этим ACK level — в данном случае единица, — и ты их как-то обрабатываешь. Если ты commit, ты должен писать их на диск и оповещать, что такое-то сообщение получено, записано, — это позволяет другим модулям увеличить свой ACK level, и вся система куда-то прогрессирует.
[1:28:47] Алексей: Просмотрим такую простую вещь, как commit. Оказывается, там куча проблем, это очень непростой модуль. Почему? Представь, что у тебя 10 тысяч стримов. Это не такое большое число — если у каждого топика 50 партиций, то 200 топиков на 50 партиций — уже 10 тысяч. Теперь, если для каждого стрима откроешь файл и начнёшь писать данные, а потом ещё откроешь индекс-файл, то тебе нужно 20 тысяч файл-дескрипторов.
[1:29:15] Александр: Ну да.
[1:29:15] Алексей: 20 тысяч файл-дескрипторов — это много. Во-первых, система будет уже немножко кряхтеть с таким числом: FD-лимиты поднимать, особое отношение требовать. А во-вторых, это будет небыстро, потому что если байты прибывают по кругу в каждый из этих стримов, то с точки зрения диска это абсолютно random access, write pattern, и производительность очень сильно падает. Для диска оптимально получать данные в append-only-режиме, то есть подряд. Особенно для NVMe.
[1:29:59] Александр: Особенно для NVMe, потому что у него серьёзные ограничения на random-запись.
[1:30:05] Алексей: Почему у NVMe ограничения? Что такое NVMe? Non-Volatile Memory Express. Это девайс, который сидит на PCI Express, пишет с чудовищной скоростью — типа 20 гигабит, — читает с ещё большей, данные на нём остаются, то есть он перманентный. Это флеш-память. Флеш-память обладает таким свойством: есть блоки, как правило, типа 8 мегабайт, и ты можешь записать туда байты — это пожалуйста, — но перезаписать не можешь. Чтобы перезаписать, ты должен вызвать специальный глагол, erase block, стереть весь этот блок, и каждый блок может быть стёрт типа два-три раза, после чего девайс ломается. Теперь спрашивается: зачем я бы такие деньги платил, если девайс ломается через тысячу переписаний? Ответ: у самого девайса есть мини-компьютер, целая программа, которая делает wear-leveling — распределяет все записи равномерно по блокам так, чтобы количество очищений каждого блока было более или менее равно. Для этого у неё есть мини-мапа, перемапливающая блоки на блоки девайса. Ты думаешь, что записал в 345-й блок два раза, а второй раз тебе выдали какой-то другой, потому что 345-й нельзя стирать.
[1:31:44] Алексей: Теперь, если ты random access в NVMe пишешь байты, в какой-то момент наступает ситуация, где контроллер не может найти тебе блока, куда записать, потому что в блоках сидят уже ненужные, убитые старые данные, но блок ещё не стёрт. Тогда он должен взять живые данные из этого блока, куда-то их разместить, переписать, и потом блок стереть. Эта процедура небыстрая, это сродни garbage collection — на скорости она не исполняется. Ты уже точно не пишешь туда 20 гигабит в секунду, если он в фоновом режиме делает garbage collection. То есть если у тебя дизайн, где ты много файлов пооткрывал и пишешь то в один по 4 байта, то в другой, это всё оказывается в разных кусках диска, и через некоторое время ты все блоки потрогал, включается garbage collection, и твоя производительность падает в 100 раз. Поэтому возникает серьёзная необходимость иметь storage-движок, который все байты пишет только последовательно. Append only.
[1:32:54] Алексей: И это на самом деле очень круто, потому что если ты стриминг-система, которая аппендит файлы, то, казалось бы, рекурсивно нужно их аппендить куда-то, а не писать в отдельные файлы.
[1:33:05] Александр: Да, да. Это как бы вытекающее следствие всего этого, в прямом смысле.
[1:33:10] Алексей: Построение задачи намекает на то, что хорошо бы append only all the way down, чтобы всё аппендилось. Я не смотрел точно, как собран Apache Pulsar, но вроде бы Pulsar решила эту задачу и данные собирает в один большой толстый файл, который append only. Так или иначе, у нас есть свой движок на эту тему.
[1:33:37] Алексей: И это большая область исследования, потому что лучшее, что человечество придумало в этой области, — это Log Structured Merge Tree, LSM-tree, который используется в LevelDB, RocksDB — это производные от LevelDB, только многопоточные. И LevelDB, кстати, тоже многопоточная. RocksDB используется в других, более крупных базах данных, поверх RocksDB много чего делается.
[1:33:59] Александр: Вообще LSM-tree, мне кажется, сейчас это стандарт индустрии баз данных, и мы к нему очень логично подошли. Потому что вот ты сейчас описал, как работает хранилище — флеш-память, SSD-диски, — у них такие же ограничения. У жёсткого диска, грубо говоря, тоже хорошо бы всегда записывать в конец. То есть это следствие тех девайсов, на которые мы в итоге пишем, и из того, как они устроены, у нас появляются структуры данных. Вот Log Structured Merge Tree, в которой вся философия вокруг того, что у тебя есть append-only-лог и структура поверх него, не самая тривиальная.
[1:34:52] Алексей: В предыдущей версии мы использовали LSM-tree. Но сейчас у нас появился новый движок, который мы сами написали, обретя некий опыт. Я смотрел на RocksDB — это несколько сотен тысяч строчек кода. Мне все говорили: «Давай RocksDB, чего ты LevelDB грузишь?» Я посмотрел на LevelDB — это вроде бы 20 тысяч строчек кода. И там было написано «Jeff Dean». И я думаю: ну окей, это круто, это как если бы Дональд Кнут написал тебе библиотеку — берём не задумываясь. RocksDB я не стал брать, потому что там было написано «мы в Meta, вот тот человек, и мы написали 500 тысяч строчек кода». Дальше заглядываешь к ним в коммиты, и видно, что баги у них не исчезают, всё время что-то чинится. А заглядываешь в LevelDB — а там баги исчезли. Там как будто написали — и работает. Это лайфхак: если пытаешься оценить, загляни в историю коммитов. Если старое, и там всё время какие-то баги, — лучше и не пользоваться. Не должно быть багов.
[1:36:05] Александр: Ну вопрос логичный: вы что, не можете LSM-tree написать, чтобы просто работало?
[1:36:09] Алексей: Они просто поставили задачу так, что она пустыню пропылесосит. Они решили взять библиотеку, и в результате это уже не библиотека.
[1:36:18] Александр: Библиотека — это что-то такое… Я RocksDB воспринимаю как базу данных без SQL-движка.
[1:36:22] Алексей: Операционная система целая. Короче, мы написали движок, мы ещё его не зашипили, но пока что он делает всё, что мы делали раньше при помощи LevelDB, и занимает приблизительно тысячу строчек кода.
[1:36:43] Александр: То есть задача решается с той же эффективностью, но меньшим числом строчек?
[1:36:47] Алексей: С большей эффективностью.
[1:36:50] Александр: А это что? Сама структура данных изменилась под ваш паттерн, что стала эффективнее?
[1:36:59] Алексей: Тут, наверное, хитрость с моей стороны, потому что количество строчек кода, которое я написал, — грубо говоря, 500 или 1000, смотря как считать: фигурную скобку на строчке считать за уникальную строчку или нет. Окей, 500–1000 строчек кода. А количество строчек, которое сгенерировал наш кодогенератор, — тоже где-то около 20 тысяч.
[1:37:24] Александр: Ага.
[1:37:24] Алексей: То есть получается похоже на LevelDB, но концептуально для меня сложность в 20 раз меньше, потому что я не заглядываю в сгенерированный код. Я просто специфицирую структуру данных, и под неё мне генерируется код.
[1:37:41] Александр: Вот сейчас давай сделаем паузу, потому что за два часа впервые ты сказал «генерация кода», и это стоит обсудить на 100%, потому что тут есть интересность. Заканчивая тему про систему AlgoX2 и про Kafka, я бы хотел задать один вопрос. Есть ли у вас понимание того, насколько вы быстрее при обслуживании того же количества консюмеров, что и Kafka, с точки зрения latency и throughput? Есть ли какие-то подтверждённые цифры или те, к которым вы стремитесь? Хочется цифры узнать и идти дальше.
[1:37:58] Алексей: При различных нагрузках latency где-то в 100 раз лучше, при некоторых нагрузках throughput в 10 раз больше. Не при всех: есть такие нагрузки, где Kafka может полностью насыщать свои ресурсы, и мы не можем её обогнать, потому что просто ресурсы ограничивают. Но если данные и читаются, и пишутся, и сервируются, и есть определённое количество консюмеров, то там мы начинаем выигрывать, причём сильно.
[1:38:50] Александр: В гибкости всех возможных профилей нагрузки Kafka всё ещё пока выигрывает, просто потому что задизайнена…
[1:39:00] Алексей: Не выигрывает. Нет такого сценария, где она выигрывает. Смотри: если Kafka выдаёт сообщение через 5 миллисекунд, мы выдаём его через 50 микросекунд. Это будет считаться как стократное ускорение. Если мы говорим, что нам нужно, чтобы 99% всех опубликованных сообщений публиковались не позже чем за 200 микросекунд, то мы легко получаем throughput в десятки раз больше, чем Kafka, потому что она просто не может ни одного сообщения пропустить с таким окном. А если мы просто напираем по максимуму — какой-то bursty pattern или просто файл закачиваем и одновременно сервируем клиентам, — то мы выживаем больше bandwidth, потому что наша система делает scale-out: данные считаются не из одной точки, а из нескольких, и идут туда мультикастом. То есть даже при полной загрузке внутренних и внешних каналов мы можем делать scale-out.
[1:40:04] Александр: Окей, хорошо. Тогда сухие цифры, просто чтобы представление в голове было. В зависимости от того, какой паттерн нагрузки, какой профиль и какое насыщение, — от 10 до 100 раз latency, и throughput может быть выше у системы, которую строишь ты, по сравнению с Kafka. Правильно?
[1:40:24] Алексей: Да.
[1:40:27] Александр: Супер. Я думаю, здесь мы можем сделать паузу в обсуждениях и подготовиться к, вообще говоря, тоже не самой стандартной парадигме — кодогенерации. Она везде присутствует, если начать разбираться: компилятор код генерирует. И всё, программа компилируется чаще всего. Но если мы говорим про дефолтное программирование — я открыл редактор кода, начинаю писать, — я редко думаю, что этот код будет из чего-то сгенерирован, что я пишу спецификацию для генератора. Хотя в последнее время, да, опять же — «без кода».
[1:40:42] Александр: Ты сказал, что ваш переписанный аналог LevelDB обладает сильно меньшим числом строчек кода, чем LevelDB, — тех, которые ты написал руками. Отчасти за счёт того, что какая-то часть кода потом генерируется. То есть та тысяча строчек, если я не ошибаюсь, — это те, что ты написал руками в редакторе. А уже работающая программа была скомпилирована из твоего кода каким-то кастомным компилятором. То есть это не C++-код, который ты писал, или это C++ какой-то прокачанный, шаблоны? В общем, как это работало, что за генерация?
[1:42:00] Алексей: Легко расскажу. Это open-source-движок, называется OpenACR. Можешь открыть openacr.org, он перенаправит в GitHub, и там висит эта система. Что это такое? Это реляционная plain-text база данных, которую ты вписываешь на реляционном языке — если его можно назвать языком: ты просто строчки вбиваешь, описываешь свою структуру данных в памяти при помощи этих записей. Дальше включается model-компилятор, amc, он читает эти записи и генерирует C++-код, генерирует библиотеку. Ты говоришь: у меня будет — во-первых, у нас своя номенклатура, мы не говорим «класс», «подкласс», ничего такого нет — у меня здесь будет структура такого типа, мы называем это C-type, потому что C-type, и там будут поля. Видишь, мы стараемся не шифроваться. Там будут какие-то поля, и я хочу в памяти иметь множество, set этих структур, и индексировать их определённым образом. Я хочу иметь хэш по вот этому полю, хочу иметь group by этих записей по тем записям при помощи binary heap.
[1:43:24] Алексей: То есть у тебя есть, скажем, соединения и есть пакеты, которые надо куда-то записать. Ты делаешь таблицу соединений, она описывает соединения. Это не объект, там нет функции внутри, потому что процессор выполняет инструкции, а не объект. У тебя есть структура, которая описывает соединения, и структура, которая описывает байты, которые нужно послать. У тебя есть set этих структур. Теперь можно сделать group by — сказать, что вот эти байты, которые нужно послать, можно собрать при помощи binary heap под каждым соединением, то есть сделать такой cross-reference, чтобы они выстроились по некому приоритетному ключу. А чтобы это соединение было найдено путём подсматривания какого-то ключа в какой-нибудь таблице — короче, ты задаёшь такую структуру данных, а он тебе генерирует код. Дальше ты просто вызываешь функцию «аллоцировать новый пакет с байтами», забиваешь пару полей, вызываешь функцию cross-reference, она вставляется в эту базу данных in-memory, прописывается — она, скажем, 7 раз индексирована — прописывается куча разных указателей. А если ты в каком-то месте что-то удаляешь, то дальше рекурсивно всё удаляется, cascade delete происходит: отписывается, все поинтеры прочищаются. Такая занудная, простая, понятная вещь — проследить, чтобы поинтеры указывали на существующие объекты.
[1:44:55] Алексей: Ты все эти таблички оптимизируешь под свою задачу, потому что не хочешь лишнего иметь — это, собственно, твоя производительность и есть. И дальше возникает сгенерированный файл с функциями и структурами в хедере, и ты просто пользуешься ими. Генератор никогда не лазит в человеческий код, человек никогда не трогает сгенерированный код, но сгенерированный код отформатирован так, чтобы человек мог его прочесть. Каждая функция досконально документирована, через каждую реализацию можно пройти отладчиком, там всё насквозь SESE. Если ты поставишь breakpoint, он ставится в одном месте, а не в 150, как в темплейтах, если ты хоть раз пользовался C++ с темплейтами. Ты в хедере хочешь поставить breakpoint, а их 150, потому что то, что ты видишь в хедере, не является кодом — это темплейт для кода, а исходники для кода тебе не показывают.
[1:45:54] Алексей: Когда-то давно, в 97-м году, компиляторы C++ реально инстанцировали темплейты и превращали их в C++-кусочки кода в отдельных файлах, и в них можно было ходить дебаггером и оставить breakpoint. Больше этого не делают. Они переписывают AST прямо в памяти, и кода нигде нет. Поэтому ты и breakpoint не можешь поставить, и они тебя ещё дальше отодвинули от ассемблера. Теперь ты смотришь на темплейт — даже дизассемблировать его нельзя, не можешь понять, что там. Очень неприятно. Короче, у генератора никаких темплейтов нет, он всегда выписывает конкретный код под твою задачу без всяких установок символов, без переопределений, ничего нигде не резолвится, все символы уникальны. Можешь пройти breakpoint по этому коду — вот его тебе нагенерировали конкретно под твою задачу и положили в файл, с которым ты слинковался. Это генератор кода.
[1:46:50] Александр: Давай сделаем паузу. Ты хорошо описал, но мне интересно чуть-чуть понять, как выглядит оригинальный… назовём его язык шаблонизации, DSL. Та спецификация, которую ты пишешь для компилятора, как она выглядит? Это какие-то синтаксические конструкции?
[1:47:15] Алексей: Это реляционные plain-text-записи в формате key-value pairs.
[1:47:22] Александр: Key-value pairs?
[1:47:23] Алексей: Формат называется SSIM. Расшифровывается как super simple или self-simple, по вкусу.
[1:47:31] Александр: Окей, понятно.
[1:47:34] Алексей: Мы называем их SSIM. Это такие файлики, каждая строчка — одна запись. И эти файлики обладают следующим свойством: объединение любого количества SSIM-файлов является SSIM-файлом, и любое подмножество строчек SSIM-файла является SSIM-файлом. Иначе бы он не был self-simple. Ну, как строчки в таблице. SSIM-формат ещё имеет type tag, поэтому ты можешь смешать строчки из нескольких таблиц в одном файле. Вот в SQL формата строчки в таблице нет: ты открываешь скобку, что-то через запятую пишешь. Нет универсального формата, который аннотировал бы в точности название базы, таблицы, всех колонок и значения. SSIM это даёт: type tag, и дальше key-value, key-value, key-value. Максимально простой формат для описания строчек базы, так, чтобы его можно было коммитить в git, чтобы не было конфликтов на ровном месте, чтобы он читался и писался и человеком, и машиной, был похож на командную строку.
[1:48:41] Александр: Это формат, который ты спроектировал, изобрёл, или какой-то ранее существующий?
[1:48:45] Алексей: Это я спроектировал формат. Ему уже, наверное, больше 15 лет. Он совершенно открытый, максимально простой, пишется и читается на всех языках. Его прекрасно поддерживают. Claude эти SSIM-ы пишет как нефиг делать, и достаточно экономно, потому что приблизительно на каждую строчку кода, которую ты пишешь, генератор выдаёт где-то 20–25 строчек своего кода. Когда мы считаем отношение сгенерированного к рукописному коду, получается, что где-то 95% генерирует генератор.
[1:49:26] Александр: Окей. И получается, что аналог LevelDB, про который ты говорил, эта тысяча строчек — это тысяча строчек кода в формате SSIM?
[1:49:36] Алексей: Нет, тысяча строчек кода написана руками. А SSIM я вообще не считаю.
[1:49:41] Александр: А ты использовал SSIM?
[1:49:43] Алексей: Да, ещё как использовал, конечно.
[1:49:46] Александр: Для структур данных, которые были нужны для реализации Log Structured Merge Tree, ты нагенерил с помощью SSIM-ов, подключил своего генератора как статическую библиотеку, и тысячей строчек кода просто склеил это всё какой-то логикой?
[1:50:02] Алексей: Я только что посчитал у себя в терминале — 100 строчек SSIM-ов.
[1:50:08] Александр: Ага, 100 строчек SSIM-ов. То есть это те структуры данных, которые используются в Log Structured Merge Tree, — наверное, какое-нибудь бинарное дерево in-memory, вот эта memtable, и sstable на диске. Тогда у меня вопрос. Когда ты такой подход используешь, ты можешь нагенерить кучу всего, освоив вот это мастерство написания SSIM-ов, потому что всё-таки нужен навык, чтобы этим языком описывать то, что хочешь, и получать быстро. То есть ты можешь себе библиотеки на ходу нафигачить: тебе нужна библиотека с какой-то структурой данных — ты можешь это SSIM-ом сделать.
[1:50:45] Алексей: Конечно. И ты их делаешь по мере надобности. Дело в том, что библиотеки все немножко прокляты. С одной стороны, пользователи библиотек всё время хотят новых фич, которых у тебя нет. А с другой стороны, все предыдущие пользователи по определению этих фич не хотят, потому что они их не используют. Получается, что ты всё время понижаешь эффективность библиотеки, добавляя эти фичи для каких-то других использований, и потенциально меняешь API, какие-то конвенции.
[1:51:24] Александр: Breaking changes ещё, не дай бог, делать.
[1:51:25] Алексей: Ну да, breaking changes и feature creep. Есть целый класс библиотек, достаточно большой, который фактически состоит из некой хитросплетённой структуры данных: несколько табличек, определённым образом cross-reference сделан, что-то грубо вычисляется, и она находит первые элементы списка вот этих, у которых такой-то атрибут. Такая библиотека для поиска. А алгоритмы на этих библиотеках, как правило, все неуниверсальные — они очень сильно зависят от того приложения, которое ты пишешь. Если ты можешь задать структуру данных безошибочно при помощи каких-то элементарных строчек, которые даже синтаксиса не имеют, — это просто строчки в базе данных, — а алгоритмы всё равно будут уникальны для твоей программы. Вот ты слинковался с библиотекой, и клей, glue code, который нужно создать, чтобы библиотека воспользовалась, часто превышает количество строчек в библиотеке. Она не совсем тебе подходит, ты её начинаешь адаптировать. А если тебе просто создать нужную структуру данных и прямо включить в твой модуль — во-первых, она никогда не отвалится, во-вторых, у тебя возникает критерий, по которому ты можешь добавлять или убирать элементы. Этим критерием ты никогда не можешь воспользоваться в отношении библиотеки: в ней ничего нельзя ни добавить, ни убрать — добавил breaking change, убрал — для кого-то ещё breaking change. Это называется tragedy of the commons, трагедия общего пространства, потому что backtracking отсутствует. Ты не можешь сказать, кому оно нужно и ради кого его оптимизировать, все его оставляют и не трогают, оно становится замусоренным и неоптимизированным.
[1:53:09] Алексей: Если библиотеку можно полностью влить в твой модуль, если ты при этом реально не повторяешь какие-то алгоритмы… Middleware нельзя вливать в твою программу, потому что оно должно быть одно всюду, одна реализация, его надо линковать. А все эти структуры данных обязательно нужно линковать прямо в программу и библиотеками для этого не пользоваться, потому что тогда у тебя всё очень лаконично будет.
[1:53:32] Алексей: По поводу генератора кода я могу сказать следующее. Вот ты рассказал, что компилятор тоже генерирует код — просто ассемблерный, мы его не видим. Действительно так: компилятор берёт исходник, выдаёт объектный код.
[1:53:46] Алексей: Знаешь классический REPL на Lisp?
[1:53:48] Александр: Знаю.
[1:53:53] Алексей: Read-eval-print loop. Ты его, наверное, проходил — это Lisp-овский REPL, который имплементирует Lisp-овский REPL. Это такой mindfuck, когда ты его читаешь и понимаешь, что в нём описывается Lisp-овский REPL, и понимаешь, что это стоило изобрести. Lisp в этом смысле самоподобен: он может интерпретировать себя. Но у него есть одно серьёзное ограничение, из-за которого им никто не пользуется: каждый раз, когда ты интерпретируешь себя, ты теряешь в производительности где-то в 100 раз. То есть даже один раз это сделал — уже непрактично. REPL красивый, но в реальных инженерных задачах ты финансовые exchange-и не напишешь на Lisp-овском REPL. Но это пример самоподобности — язык сам себя интерпретирует. Второй пример самоподобности — компилятор, который сам себя компилирует. Ты запустил объектный код, он прочёл исходник компилятора и выдал тот же самый объектный код. Это известная веха в развитии любого компилятора — когда он наконец компилирует себя, и следующая версия уже скомпилирована собой, а не кем-то другим. Дальше ты можешь откинуть эту лестницу, по которой забрался, и сказать, что GCC компилируется GCC. И сейчас это реально так: если загружаешь исходники GCC, ты его скорее всего каким-то другим GCC компилируешь.
[1:55:22] Алексей: Так вот. У нас есть код, который себя интерпретирует, есть объектный код, который генерирует сам себя, тот же объектный код. Но у нас нет примера исходников, которые генерируют свои исходники.
[1:55:34] Александр: Так, сейчас давай аккуратно. У нас нет примера исходников, которые генерируют себя. Ага, то есть по отношению к компиляторам у нас эта рекурсивная логика применяется, а по отношению к исходникам — нет.
[1:55:53] Алексей: Да. Компилятор — замечательно, что он сам себя скомпилировал, но что мне с этого? Если я не создатель компилятора, я этого никак не чувствую. А генератор кода на 95% состоит из кода, который сгенерировал сам генератор кода. Идея с генерацией кода: ты сначала пишешь генератор кода для чего-то, потом думаешь: «О, он же уже неплохо работает, а давай-ка я заменю структуру в своём генераторе кода на сгенерированную». И ты начинаешь заменять, в результате устраняешь абсолютно все структуры из рукописных файлов, а остаются только SSIM-ы. И теперь в этом компиляторе 95% кода сгенерировано компилятором. Это третья рекурсия, которая не найдена. Она похожа на Lisp, но не теряет производительности при перегенерации.
[1:56:41] Александр: Ты понимаешь, что, мне кажется, у 90% слушателей — у тех трёх людей, которые дослушали этот выпуск до третьего часа, — в этот момент взорвался мозг. Потому что это надо понять. Не поняв, это просто… Ты сам имплементировал генератор кода из описания этих структур в C-шный код — это же тоже код, который берёт на вход файлы и превращает их в C-шный код, это код на C. И часть этого кода на C, вся первая версия генератора, была написана тобой руками. Дальше, пройдя какой-то путь развития, ты понял, что некоторые структуры данных в этом коде и некоторые вызовы функций могли бы быть сгенерированы этой же программой. И тогда ты берёшь, выносишь, делаешь такой extract этого кода в SSIM-файлы, генерируешь предыдущей версией генератора код, линкуешь его статически — и вот у тебя код генератора подсократился. И так итеративно ты его подсокращаешь, и остаётся какое-то кристальное ядро логики. Ну, где-то же код должен быть написан. И интересно, как он выглядит.
[1:58:02] Алексей: Остаётся 5%, которые просто елозят по табличкам и реально читают отсюда поле, пишут туда. Остаётся код, который выглядит как твоя аппликация. Открываешь программу, начинаешь её читать — а там вот хэш-таблица наша, вот мемори-аллокатор наш, вот особая строчка, вот подкласс строчки, который называется «строчка-путь». То есть ты видишь, как программе надстраивают уровни абстракции. А если ты откроешь amc, почитаешь, ты вообще ничего этого не увидишь — увидишь просто прямые трансформации записей в записи: пройти по полям структуры и выписать в этот C++-файл что-то. И там весь bootstrap, который создают в каждой библиотеке, просто отсутствует, потому что это всё вынесено за скобки.
[1:58:57] Алексей: И поэтому, наверное, получается вот этот LST — у нас движок называется LST, потому что он не Merge Tree, а Trie. Потом можем это обсудить. Короче, так и получается, что движок можно написать за тысячу строчек кода, потому что мы вообще никакого времени не тратили на построение абстракции. Напрямую описываешь, какие у тебя есть элементы, каких множеств, как они связаны друг с другом в памяти — их там буквально несколько штук. А потом пишешь алгоритмы, как их сериализовать, десериализовать, какой-то хитрый pointer arithmetic.
[1:59:35] Александр: Я по-инженерному восхитился, потому что это реально круто. Надеюсь, слушатели тоже осознали. Думаю, этот подкаст можно будет послушать пару-тройку раз, в разном состоянии пребывая — гуляя, перед сном, чтобы уснуть покрепче в какой-то момент.
[1:59:56] Алексей: У меня на самом деле подкаст именно про такое.
[2:00:00] Александр: Так что, ребята, я думаю, олды прям кайфанули с этого объяснения. Давненько такого не было. А то в последнее время всё про агентов, и я, и Claude Code, и всё такое. А тут что-то настоящее, инженерное, и с очень мощной мыслью. Вот что я оцениваю — это то, что в этом есть инженерная мысль, реально. И то, что она работает, и то, что она настолько чиста, что по ней выстроен и генератор, и сама система, которая строится с использованием этого. То есть оно всё самоповторяющееся, как ты сказал. И в итоге у тебя и система самоповторяющаяся: одни и те же концепции, один и тот же подход. И, один раз его поняв, тебе понятна система, как она строится. И у тебя как у инженера не возникает вопросов «а как мне тут сделать», потому что ответ, как правило, один. Это очень круто.
[2:01:03] Алексей: Так и стараемся сделать. Мы смотрим на планету Земля: там есть кучки песка, есть горы, и то, и другое — артефакты гравитации, просто на разных масштабах проявленные. И хочется иметь такую систему, где есть небольшое количество маленьких аксиом, которые на разных масштабах себя проявляют. Но мы не хотим на этих масштабах придумывать совершенно разные языки и описания — мы действительно хотим применять вот этот небольшой инструментарий. Однопоточная программа — казалось бы, почему не сделать что-то более мощное? Ну, если я запущу её 2000 раз, это 2000 однопоточных программ, уже целый кластер забил. То есть необходимый и достаточный объект — иметь однопоточную программу, потому что она точно забивает это ядро до отказа, потом можно рядом поставить другую, она забьёт второе ядро. Здесь абстракция заканчивается, мы не делаем больших абстракций: мы просто дали этому ядру набор инструкций, которые надо выполнять, — он их выполняет. То же самое, если опуститься до одной функции: мы дали процессору инструкции, которые надо выполнять. Так структурируется время. А память структурируется при помощи реляционного описания на всех уровнях.
[2:02:22] Алексей: Отдельно можно поговорить, почему мы используем именно реляционное описание, — краткий ответ в том, что это единственное описание, которое позволяет избежать аномалий, то есть логических противоречий. И оно очень хорошо обладает этой самовложенностью, потому что на всех уровнях выглядит одинаково. Иногда добавляется какой-то элемент к ключику — целое измерение добавилось, иногда убирается, но более-менее при помощи этих ключиков абсолютно всё что угодно можно занумеровать и пересчитать. Оказалось, что реляционная нумерация на всех уровнях хорошо работает для структурирования памяти и нумерации объектов: файлы, записи в файлах, поля в структурах, все структуры данных — хэш-таблицы, деревья — всё идеально ложится на реляционное описание. Ты скажешь: «Ну как же дерево, там вроде нода в ноде?» Нет, не нода в ноде — у тебя есть просто структура «нода», и там есть три указателя. Всё нормально. Множество нод при помощи одних ссылок образуют дерево, при помощи других — линк-лист. Реляционное описание на это смотрит проще.
[2:03:57] Александр: Понятное дело, что хэш-таблицы — это в целом дефолтная структура данных. А есть какое-нибудь интересное дерево — я вот хочу придумать, — вообще, какие структуры данных с помощью SSIM-файла я могу описать очень просто?
[2:04:11] Алексей: Зависит от интерпретации генератора. Подсказать генератору, что он должен генерировать, можно при помощи любого носителя информации, в том числе носителя SSIM. Просто вопрос, насколько удобно им пользоваться. Могла бы оказаться кошмарной идея: ты идёшь, пишешь занудные строчки, фактически заполняешь телефонную книгу, и всё лежит в разных местах, крайне неудобно. Но оказалось, что не так — не крайне неудобно, там есть ряд свойств, которые делают систему достаточно удобной. Claude Code прекрасно умеет писать эти отдельные строчки, ему даже объяснять не нужно — просто идёт, пишет, отлаживает. И вот этот рычаг, где из одной строчки вытекает 20, с Claude очень быстро отрабатывает, потому что мы ему конкретно говорим: не читай то, что сгенерировано. Он не заглядывает в сгенерированный код, только в те строчки, которые являются input-ом. В каком-то смысле ни одну структуру данных нельзя задать SSIM-ом, потому что структура данных — это, наверное, алгоритм переписывания указателей, по крайней мере в нашем понимании. И это пишется на C++, при помощи инструкций. Сам на SSIM мы говорим, что есть столько-то программ, и каждой даём имя, и эти имена образуют таблицу имён программ. Мы говорим, что есть структура, в структуре есть поля, и имена этих полей — строчки в таблице, у которых должны быть уникальные ключи. То есть на SSIM мы описываем все различия.
[2:05:49] Александр: То есть примитивы, которыми ты оперируешь, — это действительно строчки и поля?
[2:05:58] Алексей: Примитивы, которыми я оперирую, — это множество и элементы множества. А элементы идентифицируются ключом — так просто принято называть это в теории баз данных. Ключ в самом простом понимании — это набор символов, строчек, скорее всего. В памяти ключ может быть чем угодно. Пойнтер, кстати, тоже ключ, просто суррогатный.
[2:06:21] Александр: Хорошо. Тогда вопрос такой. Мы всё-таки говорим про построение очень нагруженного — одного из самых нагруженных типов систем, которые существуют. И там используется жадная генерация кода. Вопрос к перформансу тогда. Сгенерированный код — насколько он оптимален, и что делать, если сгенерированный код неидеален с точки зрения перформанса, и надо прямо что-то ad hoc написать, чуть ли не ассемблерную вставку сделать? Что тогда? Или такого просто не возникает?
[2:07:00] Алексей: У меня действительно были случаи, когда генерируется дефолтный код, предусмотренный генератором, и я чувствую, что можно было бы немного иначе и что так было бы лучше. Но я говорю: давай это отложим, давай просто не будем об этом думать. Почему? От этого никак не зависит мой алгоритм. Мой алгоритм требует, чтобы структура данных была и какие-то функции «найти». А тот факт, что эти функции чуть медленнее, чем хотелось бы, — это не свойство моего алгоритма. Это как будто меня просто запустили на более медленном компьютере, я на нём пробежал. Этот компьютер можно проапгрейдить, не меняя программу вообще никак. Я запущу отдельную рабочую сессию, зайду, добавлю новое поле в SSIM, которое будет управлять генерацией, по умолчанию оно будет ноль, а я введу режим 1. И в режиме 1 он сгенерирует как раз такой код, который в том случае даст мне нужный результат, — я для той строчки поменяю нолик на единичку и получу новый эффект. И каждый раз, когда я такое хотел, в 90 из 100 случаев потом не шёл и ничего не менял, потому что оно было good enough. И оказывалось, что проблема со скоростью не в этом месте. Когда система большая, bottleneck всегда плавает, и он где-то не там, где ты думаешь.
[2:08:30] Александр: Да. То есть когда мы говорим не про молотилку — «запусти мне на одном файле молотилку данных и дай максимально быстрый результат», где ты упираешься в процессор или в производительность памяти и начинаешь изобретать, — а когда ты строишь большую систему с большим количеством подвижных частей, с большим количеством клиентов, и они делают всё разное, то у тебя узкое место, скорее всего, плавающее, и оно, вероятно, не там, где ты чаще всего ищешь как программист. И если ты действительно хочешь целиком систему ускорить, то, переписав какой-то hot path на ассемблер, наверное, ничего не изменишь.
[2:09:17] Алексей: Вот смотри, например, на этот файловый движок. Там всё было распараллелено — куча файлов, куча индексов. Но это медленно, потому что когда ты параллелишь, ты создаёшь много мест. А много мест — это то, что называется switching cost, стоимость переключения. Ты доступаешься к одному месту, потом к другому — это какое-то переключение. Любое переключение в любой системе стоит чего-то. В памяти, как известно, байты в одной cache-линии — у которых адрес по модулю 64 одинаковый — к ним быстрее доступаться, потому что, доступившись к одному, ты подгружаешь всю cache-линию, и другой теперь рядом. А если он в другой cache-линии, возникает какая-то стоимость. Это пример стоимости переключения. То же самое на более высоком уровне: если каждые 4 килобайта — это одна страничка, в kernel эти странички замаплены, и когда ты первый раз доступаешься к новым 4 килобайтам, это вызывает небольшой page fault, kernel тебе аллоцирует реальную память и возвращает назад, ты даже не замечаешь. Это стоимость переключения. Иметь несколько файлов — это стоимость переключения, потому что где-то в операционной системе есть link-list dirty files, кто-то по нему елозит и пытается их куда-то слить. И в данном случае оказалось, что самое большое улучшение — это слить все эти файлы в один.
[2:10:55] Алексей: А в случае с нашим движком индексации мы и индекс добавляем в этот самый файл. То есть мы сделали его прямо super append only, у нас нет индекс-файла. Ты просто достримливаешь. Концептуально очень просто: ты пишешь сообщение в файл, а периодически пишешь такие сообщения, которые позволяют найти предыдущие по некой формуле.
[2:11:20] Александр: Так. То есть вместо того чтобы иметь отдельный, как в Kafka, индекс-файл, где ты говоришь «offset 100 — это на самом деле 31-й байт», ты просто берёшь этот файл, начинаешь его читать и в какой-то момент довольно быстро находишь записи, которые помогают найти то, что ты ищешь. Правильно?
[2:11:41] Алексей: Да, только ты читаешь его задом наперёд, потому что он append only и информация там появляется только в конце. Ты записал сколько-то сообщений, а теперь дописываешь некий индекс этих сообщений. Записал тысячу сообщений — дописываешь запись, которая индексирует эту тысячу. Записал ещё тысячу — ещё такая запись появляется. Когда ты записал тысячу раз по тысяче, нужно записать запись более высокого уровня, которая позволяет найти предыдущие тысячу индекс-записей. И так возникает структура индексации, которую мы называем Log Structured Trie, которая позволяет все стримы иметь в одном файле. Это единый файл.
[2:12:24] Александр: А у тебя есть какие-то эксперименты, исследования на тему того, что в твоей системе это работает лучше, чем, например, два файла — индекс и обычный файл с данными? Или это больше исследование концепции?
[2:12:44] Алексей: Два файла хорошо работают, не проблема. А вот 10 тысяч вообще не работают. Например, в Kafka ты просто не сможешь создать 100 тысяч топиков — вообще не сможешь. И даже 10 тысяч она будет думать много минут, потому что под каждую партицию она, видимо, делает свой файл. Там какие-то ужасы возникают, квадратичности.
[2:13:14] Александр: Окей, интересно. Слушай, мы тут можем это обсуждать бесконечно, очень интересно, но надо подводить к концу. И я бы вспомнил то, с чего мы начали. А начали мы с того, что установили чёткие границы: мы находимся в 26-м году, но разговариваем больше про то, как мы руками пишем код, как мы его как люди читаем, интерпретируем, как уменьшить количество написанного нами кода, просто чтобы меньше ошибок было. И мы про это хорошо поговорили — и про SESE, и про single static action, и про подход к построению систем.
[2:13:50] Александр: Теперь со всем этим багажом, который мы накапливали три часа, мы возвращаемся, телепортируемся в ту реальность, в которой сейчас находимся, которая нам добавляет много нового. Добавляет она то, что теперь мы программируем с Claude Code, с Opus 4.6. И вот тут интересно, как ты… Мне это очень нравится, потому что мне кажется, что у Claude офигенно будет получаться подобная система. Строить в такой системе ограничений, в таких правилах, он довольно хорошо следует. И, объяснив ему, можно быть чуть более уверенным и спокойным, что ты получил то, что хотел, потому что ты концептуально задал эти границы Claude, и он по-другому никак не мог. И уж лучше пускай код будет сгенерирован проверенным генератором, чем он будет наслоплен Claude Code, засрёт контекст, — и какой от него толк, если это можно сгенерировать? В этом плане это круто, потому что это какая-то амплификация: ты с помощью Claude можешь ещё на уровень выше в такой системе подняться и фигачить ещё лучше, не жертвуя качеством, не усложняя систему. Потому что сейчас основная проблема у большинства людей: нагенерить код мы можем, а что с этим делать дальше?
[2:15:05] Алексей: Если бы я тебе предложил увеличить контекстное окно в 20 раз, ты бы сказал: «Ох, 20 миллионов токенов контекстное окно — это супермодель будущего», правильно? Ну, если ты предлагаешь Claude работать с намного меньшим объёмом данных, чем раньше, это как если бы ты увеличил его контекстное окно. Он намного больше помещает в свой контекст, потому что ему не нужно держать какие-то вспомогательные вещи, которые можно было сгенерировать. Слушай, я не знаю, насколько много мусора генерирует наш генератор — какая-то часть кода вообще не используется, наверное, 10% кода ты никогда и не вызовешь. Остальная часть выглядит достаточно тривиально — какой-то повторённый boilerplate. Есть нетривиальная часть, когда возникают зависимости: ты вставил поле, а в коде появляются 4 проверки, потому что он нашёл какие-то зависимости и за тебя их учёл. Вот это нетривиальная часть. Я не знаю, может быть, на какой-то библиотеке можно мегакомпактный код написать и получить тот же эффект, но, наверное, это будет какой-то ненастоящий язык, не супер-производственный, не C++. Для меня аксиома, что это должен быть C++, потому что только так я получаю производительность компьютера.
[2:16:49] Алексей: Claude действительно хорошо работает с системой — ему достаточно просто объяснить некие правила: про single entry, single exit — это эстетика, конечно, но всё равно, — про single static action, как эти SSIM-ы читать и писать. Блестяще отлаживает, особенно однопоточные программы, потому что программа запускается с командной строки, ей элементарным параметром можно задать, по каким веткам она пробежит и что сделает. Claude это очень хорошо делает, вставляет сам принты, что-то распечатывает. Иногда, конечно, он путается, но вообще эта машина просто не перестаёт меня впечатлять. Феноменально. Абсолютно кардинально изменила все мои отношения с программированием. Я даже не понимаю — если бы я родился сейчас, у меня не было бы никаких шансов прийти к каким-то в мучениях полученным идеям. Наверное, к каким-то другим бы пришёл. Это закрывает какие-то двери, которые были открыты, но и открывает новые горизонты для полёта мысли, про который мы пока даже и не думаем.
[2:18:00] Александр: Я недавно рефлексировал — не помню, говорил ли я про это в подкасте. Довольно простая вещь: я себя осознал человеком, который запрыгнул в последний вагон тех, кому ещё хоть как-то имело смысл изучать Rust. Потому что я его полтора года назад начал изучать для себя — просто вечерами, стримил на YouTube, как я базу данных на нём программирую, там лексер, парсер писал, структуры данных, мучился с borrow-checker, прям страдал с Rust, потому что хотел писать системный код. И тогда это имело смысл: ну да, а как, я буду программистом на Rust. И буквально только я его изучил — выходит Sonnet, по-моему, 4, который уже более-менее начинает на нём программировать нормально, я начинаю с ним фигачить вперёд, а потом выходят Opus 4 и 5, и — ну понятное дело, что код руками ты уже не будешь писать. И вот сейчас, при Opus, какой смысл хоть одному программисту сидеть и изучать Rust? Откуда найти мотивацию, кроме «мне делать нечего, хочу изучить»? Я бы себе сейчас ни за что не объяснил, зачем этим заниматься. А я вот в последний вагон запрыгнул. И это продолжает твою мысль, что сейчас уже абсолютно всё поменялось.
[2:19:37] Алексей: Слушай, у меня где-то в to-do написано изучить Rust, потому что я хотел бы прочувствовать мысль создателя — что конкретно он хотел получить. Я так понимаю, что это определённая лексическая структуризация кода, чтобы исключить неправильные паттерны доступа. Ну, для меня это вряд ли станет моим языком, потому что я так просто не думаю. Я считаю, что исключить неправильные паттерны доступа при помощи лексических средств можно, но нежелательно. Потому что я хочу иметь полную свободу: иметь циркулярные структуры данных, корни, не лежащие в каком-то одном месте, и делать на них произвольные замены и трансформации. Если доказано, что это правильно, значит, этим можно пользоваться. Наверное, не надо постоянно держать в руках незащищённую циркулярную пилу, нужно иметь правильные способы её крепить, очки на глаза надевать, держать доску так, чтобы себе руку не отпилить. То есть если есть мощный инструмент, он обрастает техникой безопасности, но не хочется закрыть себе все возможности. Я всё-таки хочу пользоваться законами физики — всё, что мне потенциально предлагают сделать компьютеры, я хочу такую возможность иметь. Поэтому я не очень хочу иметь такой безопасный язык программирования. Опять-таки, может, я неправильно мыслю, может, просто не прочитал и не проникся этой мыслью, но надо не лениться, прочитать. Нельзя судить, не посмотрев. Что ты расскажешь про Rust?
[2:21:35] Александр: Слушай, у меня совершенно другой карьерный путь, кардинально отличающийся от твоего. Мы с разных концов пришли в программирование. Я оригинально Java-программист вообще — несколько лет на ней писал и горя не знал, и базы данных на ней до сих пор пишу. Но в целом я понял, что хочу уйти от Java, и у меня был вопрос: понятное дело, что я умею программировать на плюсах и так далее, но что сейчас интересного, куда мне двинуться по индустрии? Было очень логично, что Rust.
[2:22:10] Алексей: Java душит — как стеклопакет такой пластмассовый: заходишь в комнату и чувствуешь, что воздух неправильный.
[2:22:15] Александр: Духота какая-то, да. Это было очевидно, и как раз на тот момент я задался вопросом, что делать, и решил сделать вот это. Было бы странно мне пойти писать на плюсах, хотя если бы была возможность прямо попробовать продакшн-код, я бы, наверное, даже был рад.
[2:22:35] Алексей: Ну да, современные плюсы — это какое-то барокко, куда-то они не туда зашли. Вычисляем простые числа при помощи компилятора на фазе компиляции, используя экспоненциально много времени и места, — но зато вычисляем, простое число или нет, на языке темплейтов. Вот это — нет. Для меня C++ — это способ трансформировать строчку кода в ассемблер, и он даёт мне определённые чехлы от этой циркулярной пилы. Я могу при помощи него присваивать строчки — не хочу я memcpy писать, хочу просто сказать A = B. То есть для меня копирование структуры — хорошая фича, возврат структур — хорошая фича. В остальном C идеален, он практически всё даёт, что мне нужно. Overload — хорошая фича, хотя я им редко пользуюсь: я стараюсь, чтобы функции были более-менее уникально названы. Даже Claude это помогает — ему проще, когда функции не повторяются, потому что иначе он начинает думать, что это за функцию я вызвал, резолвить что-то. Проще, когда он сходу понимает.
[2:23:29] Алексей: Я тут должен признаться, что я сам Claude пользуюсь чуть больше месяца. Но как только я его сгрузил и начал пользоваться, он у меня перманентно висит на экране, и большую часть заданий я отдаю именно ему. Документацию писать, git commit сделать — это только Claude.
[2:23:42] Александр: Да, хороший клиент к git.
[2:23:46] Алексей: Прекраснейший. Когда я писал движок, я практически всё, что написал, написал при помощи Claude. Какие-то вещи я прекрасно знал, как написать. Но надо же было потренировать язык спецификации, чтобы объяснить ему: сделай это, потом это, последовательно, так-то и так-то. Почувствовать, как система рассуждает. Она очень-очень мощная, просто шокирующе мощная. И есть спекуляции, что у Anthropic появилась какая-то ещё более думающая система, которую даже не выпускают, потому что это будет подарок black hat-ам: если её запустить, она всех захакает. Дальше будет только сильнее. И я считаю это правильным. Смотри: мог я при помощи Claude написать движок типа LevelDB, если бы не понимал, что пишу? Нет, не мог бы. То есть возникло некое понимание, как я хочу это сделать, а дальше при помощи этого инструмента я проехал, как на велосипеде. Мог пешком пройти с рюкзаком, а вот проехал на велосипеде — туда же попал. И я считаю, что это очень-очень хорошо. И всех в нашей компании я убеждаю: если вы ещё не пользуетесь Claude — пользуйтесь побольше. Потому что я не боюсь, что у них атрофируется мозг: если его не было, то его и не будет, а если был — никуда не денется. Это исключительно велосипед: ты на него садишься и едешь, и от этого ходить не разучишься.
[2:26:10] Александр: Абсолютно верно. Я очень рад, что мы приходим к консенсусу в сообществе программистов: это круто, и давайте обсуждать, как нам с этим инструментом лучше и эффективнее работать. Давайте делиться, прокачиваться, а не сидеть и фигнёй страдать, ревьюить код и говорить: «А я напишу лучше Claude». Я рад, потому что раньше — ты говоришь, ты месяц с этим, а я уже почти год, но полгода уже совсем full-time. Полгода назад на меня с Claude смотрели как на дурачка: «Ты что, этот весь pull request писал с Claude? А тут вот бага». А я говорю: ну так и у меня в коде иногда баги бывают, давай решать, при чём здесь Claude. В общем, я искренне рад.
[2:27:01] Александр: И, наверное, завершаю таким вопросом. Что ты считаешь: с этим велосипедом, с этим инструментом в текущих реалиях какие навыки нам, инженерам, ключевые стоит в себе подчёркивать, развивать и ценить, а какие — от которых стоит реально так посмотреть и сказать: «Слушай, наверное, full-time программировать руками уже и не надо»? От чего отказаться, а что, наоборот, прокачать — с точки зрения личностных, профессиональных навыков?
[2:27:29] Алексей: Может быть, я самый неправильный человек, чтобы давать такой совет, потому что я работаю на достаточно фундаментальном уровне — пишу инфраструктурные программы, которыми потом пользуются другие люди и делают что-то более высокоуровневое. Результат моего труда — это какой-то data plane, для того чтобы люди писали самые разные приложения: мониторили температуру на какой-нибудь фабрике или порнуху прокачивали, не знаю, что они будут делать. У меня область понятная: я должен скейлить, не терять данные, простой интерфейс. Но раз совет попросили, значит, надо его дать. Как я пользуюсь Claude? Я, например, иду к нему и говорю: давай обсудим вот такую тему. И дальше смотрю, не выдаст ли он что-то, что сдвинет моё понимание. Иногда я придумываю слово, которым хочу что-то назвать, и говорю: как тебе такое слово? И он говорит «хорошее», или предлагает варианты: «А вот это слово можно спутать с этим, потому что оно где-то ещё используется, значит что-то другое». Я думаю: блин, ты прав, ты лучше придумал вариант. Но я должен к нему обратиться, потому что сам он не посмотрит на твой исходник и не скажет: «Давай это переименуешь». Такого он не делает.
[2:28:57] Алексей: Я некоторое время пользовался… Ну вот я месяц сижу в Claude Code, чуть больше месяца, который прямо мою репозиторию видит, git-репу, и это фундаментально меняет отношение. Раньше я, естественно, какие-то сниппеты приклеивал в чат, но это совершенно другое — то есть это просто неинтересно.
[2:29:15] Александр: Вообще другое, да. Он помогает тебе думать, потому что не всегда я могу углубиться. У тебя есть некий уровень концентрации, которого ты можешь достичь. Звуки тебя отвлекают, домашние дела отвлекают. Не можешь ты погрузиться на 10-й уровень, а с 3-го уровня ты уже можешь Claude дать инструкцию, и он тебя до 6-го доведёт, а может и до 10-го. То есть он позволяет тебе работать на ходу, суммарная эффективность очень сильно возрастает.
[2:29:49] Алексей: Я недавно ездил в горы, очень много времени провёл за рулём, но тем не менее что-то с Claude написал, потому что он просто думал, пока я ехал.
[2:29:57] Александр: Я поделюсь тем, что у меня был Claude-фома, как я его называл. Я вдруг в моменте осознал, что могу весь софт для себя написать сам, и начал просто в 8 параллельных окон в tmux фигачить с Claude 5 продуктов. Естественно, мозги у меня вытекли примерно недели через две. Я осознал: ну да, вот у меня есть подкастинговая платформа, вот я себе плеер для подкастов нафигачил, вот тут VPN нафигачил, вот ещё здесь делал, а дальше чё? И дальше я сижу такой: а всё. То есть я прям выгорел, у меня был этот момент. Теперь я вернулся назад и понимаю, что да, инструмент офигенный, я его люблю, но надо знать меру, потому что всё равно я упираюсь в свои ресурсы. Моя думалка напрягается как будто бы больше в последнее время при эффективной работе с Claude, чем до этого, потому что мне сейчас нужно контекст Claude загружать в моменте.
[2:30:57] Алексей: Знаешь, что у меня забрали? У меня забрали время. Раньше, чтобы понять, как работает код, и решить задачу, у меня было время: я мог потратить день-два на то, чтобы просто вникать, исследовать, пробовать, и со временем мой мозг пережёвывал информацию и выдавал результат. Сейчас я такой: ну вообще-то с Claude я за полчаса-час это сделаю, и теперь у моего мозга нет этих двух дней. У него есть один час, чтобы пробежаться на этом велосипеде быстрее, но всё ещё надо уметь фильтровать, где углубиться, а где нет, на чём сконцентрироваться, что отдать Claude. На самом деле очень тяжёлая работа.
[2:31:29] Александр: И вот это как мышца должна прокачиваться у нас как у программистов, потому что раньше как будто бы она была востребована, но не настолько, по ощущениям.
[2:31:37] Алексей: Если кода мало, а делает он много, тогда всё хорошо. Ты можешь с ним проводить много времени. Код должен умещаться в голову, если ты над ним работаешь. Вся операционная система Linux у тебя не умещается в голову, и у меня тоже. Но её исходники — пожалуйста, если бы нас было, как там, миллион Claude-ов, мы бы её разом всю поняли, со всеми драйверами. Ядро можно понять. Оно очень богато на разные структуры данных, там просто бесконечные кладези изобретательства. Но само ядро-ядро понять ничего не стоит: простые, временем проверенные концепции, и кода мало, а делает он много. Идея, что есть процесс, который пишет в буфер, а другой из него читает, и он блокируется, когда нет байтов, блокируется, когда буфер переполнен, а посередине сидит kernel, который просто выключает первый процесс и включает другой, — вот это и есть, собственно, пол-Linux. Дальше просто приклеиваются диск, page fault и так далее.
[2:32:46] Александр: Ты вот сейчас рассказывал про ядро, и у меня сразу появилась идея. Надо бы открыть код Linux-ядра с Claude и просто поизучать — не прочитать сверху вниз, потому что я офигею, а вот с Claude: «А для меня это важно, а как вот это работает, а тут распиши». Вообще этот инструмент с точки зрения изучения существующей кодовой базы — просто восхитительно. Ты задаёшь вопрос: «Покажи ссылку, а вот на уровне интерфейсов объясни». Ещё в одну копилочку, как ты сказал про то, что ты с ним брейнштормишь, — он такой бадди для думанья, а для изучения тоже отличный бадди.
[2:33:29] Александр: Кучу можно ещё юзкейсов Claude описывать, но, думаю, здесь тоже надо делать хардстоп, чтобы остановиться, потому что тема интересная, два энтузиаста могут это бесконечно, но подкаст должен быть ограничен. Три часа хронометража — это максимум, наверное, который мы можем себе позволить. Я бы хотел тебя поблагодарить, Алексей, за то, что ты пришёл, рассказал. Будет что рассказать — приходи. И традиционное слово — просто free call. Даю тебе микрофон, можешь сказать слушателям что хочешь.
[2:34:03] Алексей: Я скажу слушателям, что если вам интересна какая-либо из тем, которые мы обсуждали, то можете мне написать, и мы поговорим. Можешь высветить мой имейл, я не против обсудить.
[2:34:19] Александр: Да, все ссылочки на технологии, которые мы обсуждали, на AlgoX2, на твой имейл — всё будет в шоу-нотах, в описании, в телеграм-канале подкаста «Тысяча фичей». Так что заходите, пишите, можем продолжать дискуссию. Спасибо. А на этом всё, всем пока.