Сохранёнки программиста
6.55K subscribers
1.16K photos
59 videos
10 files
1.81K links
Заметки и ссылки на будущее, чтобы изучить когда будет время.

Разместить рекламу: @tproger_sales_bot

Правила общения: https://tprg.ru/rules

Другие каналы: @tproger_channels

Другие наши проекты: https://tprg.ru/med
Download Telegram
Полгода Tailscale ловила порчу баз: 19 случаев, ни одного общего признака — ни шарда, ни клиента, ни времени суток. Воспроизвести не удавалось, оставалось ждать следующего раза с диагностикой наготове.

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

Виновата гонка в самом SQLite: при совпадении записи с контрольной точкой часть страниц считается перенесённой в основной файл, хотя туда не попала. Багу оказалось не меньше шестнадцати лет, он настолько редкий, что для тестов его приходится вызывать намеренно. Tailscale попадала в него чаще других, потому что берёт контрольные точки под ручное управление и делает их очень часто.

Вывод авторов: скучная технология в нестандартном режиме перестаёт быть скучной. Всё было по документации — просто в стороне от протоптанной дороги.

Как искали и что нашли

@prog_stuff
👍3
Новак Калуджерович писал арифметику больших чисел для криптографической библиотеки и нашёл ошибку в Algorithm D — том самом длинном делении из книги Кнута, которое перепечатано в сотнях реализаций.

Суть места, где всё ломается: очередная цифра частного сначала оценивается по старшим разрядам, а потом оценка исправляется. Кнут утверждает, что после коррекции она гарантированно помещается в один разряд. Контрпример нашёлся в системе счисления по основанию 3: делим 45 на 16, правильное частное 2, а пробная оценка 5. После двух коррекций оценка всё ещё двухразрядная, и дальнейшее умножение берёт только младший разряд, вычитая фактически ноль.

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

Автор написал Кнуту и получил тот самый гексадецимальный доллар — чек за найденную ошибку — с рукописной пометкой, что Algorithm D читают чаще многих других алгоритмов книги.

@prog_stuff
👏3
Что будет, если в одной транзакции PostgreSQL накопится больше 64 вложенных? Просядет весь кластер — включая соединения, которые об этой транзакции ничего не знают и работают с другими таблицами.

PlanetScale разобрали механику. У каждого процесса есть кэш идентификаторов вложенных транзакций, обычно на 64 записи. Переполнился — снимок помечается переполненным, и проверка видимости каждой строки идёт через служебную структуру pg_subtrans: блокировка, а при промахе ещё и диск.

Вложенные транзакции при этом появляются сами: их создаёт каждый блок PL/pgSQL с секцией EXCEPTION, а не только явный SAVEPOINT.

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

Следить можно через pg_stat_get_backend_subxact() и счётчики pg_stat_slru. Готового способа предотвратить переполнение в PostgreSQL 18 нет.

@prog_stuff
👍2
Колонка status в таблице пользователей отвечает ровно на один вопрос: что сейчас. На вопросы «кого отклонили во вторник», «сколько человек провёл в статусе» и «кому после отказа всё-таки одобрили» она не отвечает никак.

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

Дальше идёт разбор, ради которого стоит читать. Флаг current автор отвергает: его придётся сбрасывать и выставлять при каждом переходе, то есть возвращается второй источник истины. MAX(id) тоже не годится — ломается при заливке исторических данных. Правило выражается через дату и идентификатор как устойчивый разделитель при одинаковых отметках времени.

Четыре способа достать последнюю запись сравниваются честно, вплоть до планов: DISTINCT ON на списке пользователей заставляет базу прочитать и отсортировать всю таблицу статусов, а LATERAL JOIN с подходящим индексом читает по одной строке на пользователя.

@prog_stuff
Простой запрос — сумма колонки на 500 миллионов чисел. PostgreSQL считает его 20 секунд, тот же цикл на Rust — 358 миллисекунд. Автор проекта pgrust показывает по шагам, из чего складывается разрыв, собирая уменьшенную копию исполнителя запросов PostgreSQL и добавляя к ней оптимизации по одной.

Исходная модель отдаёт строки по одной, через метод next() у каждого узла плана — так устроен и настоящий PostgreSQL. Это 1,3 секунды. Дальше: выдавать сразу пачку по 1024 значения в буфер на стеке — 480 мс. Слепить сканирование и суммирование в один узел — 358 мс, то есть ровно обычный цикл. Векторные инструкции — 135 мс.

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

@prog_stuff
👍2
За неделю сервер отдал 1,28 млн полных страниц, а аналитика насчитала 5977 просмотров: на каждый видимый просмотр приходится около 214 загрузок, которых в статистике нет. Ник Грей воевал со скраперами на сайте из 1,5 млн профилей и описал, что сработало.

22 апреля пришло 3,6 млн запросов с 361 844 IP-адресов, почти все из Китая; Cloudflare за десять часов выдал 1,18 млн проверок. Потом headless Chrome из дата-центров США и сеть домашних IP с Chrome 118–120 образца 2023 года.

Метрика, к которой пришёл автор: страниц на одного приведённого посетителя. Google — 46. Поисковый бот Anthropic за неделю прочитал 420 680 страниц и привёл 12 человек, бот Amazon читал 117 000 страниц в день и не привёл никого; оба заблокированы, первый сбавил с 60 000 запросов в день до 25.

Скрипт JavaScript Detections от Cloudflare стоил 2875 мс на среднем телефоне при собственном JS сайта в 278 мс; без него Lighthouse вырос с 58 до 99. Из 106 437 проверок за двое суток решены 252, то есть 0,24%. Правила WAF в конце.

@prog_stuff
2
В интернете появился репозиторий, опровергающий гипотезу Коллатца на Lean — без единой заглушки, то есть формально проверенный. Оказалось, что это не прорыв в математике, а эксплуатация ошибки в самом проверяющем ядре.

Леонардо де Моура, автор Lean, написал разбор случившегося. Киран Гопинатан свёл находку к короткому доказательству False — то есть в системе можно было доказать что угодно. Ошибка сидела в обработке вложенного индуктивного типа с фиктивными параметрами: параметры терялись во вспомогательном типе и не проверялись. Исправление выпустили примерно через час после минимального воспроизведения.

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

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

@prog_stuff
🔥1
Servo — попытка написать браузерный движок с нуля на Rust. Проект то замирал, то оживал, и по ежемесячным отчётам хорошо видно, как выглядит эта работа изнутри.

В июньском отчёте — 558 коммитов, снова рекорд, и вчетверо больше, чем на перезапуске в 2023 году. Совместимость измеряется не процентами, а конкретными сайтами: Google Photos работает, Google Maps и OpenStreetMap рисуются, но плохо реагируют на нажатия.

Из внутренних изменений: структура, описывающая прямоугольник в разметке, ужалась с 288 до 240 байт, двумерный холст стал потреблять до 23 процентов меньше энергии, одинаковые векторные картинки перестали растеризоваться повторно.

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

@prog_stuff
❤‍🔥1
Перевести номер дня в день недели — это (rd + 4) % 7 с поправкой на отрицательные числа, и разговаривать вроде не о чем. Бен Джоффе показал, что под капотом это задача про быстрый остаток от деления, и собрал набор функций, которые обгоняют вывод компилятора.

Любимая у автора — три инструкции плюс загрузка константы: imul на (1 << 32) / 7, lea с константой 0x93000000 и сдвиг на 29. Работает на всём знаковом 32-битном диапазоне, а константа в третьей строке по сути задаёт угол поворота: замена одного числа даёт ISO-нумерацию [1..7] вместо [0..6] бесплатно.

Всего собрано 280 вариантов под разные цели: пропускная способность против задержки, x86 против ARM. 8-, 16- и 32-битные версии проверены полным перебором заявленного диапазона, 64-битные — четырьмя блоками по миллиарду дат. По сводке автора на Ryzen 9 и M4 Pro новые функции тратят 0,3–0,5 времени алгоритма Нери 2024 года, который до этого считался пределом; на отдельных сочетаниях функции и платформы разрыв доходит до 5,9 раза. Техника обобщается на x % (2^N − 1), а отдельно выведены быстрые остатки для 24 и 60.

@prog_stuff
Автор впервые всерьёз берётся за быстрое преобразование Фурье и по шагам показывает, что видит.

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

Самое полезное — про грабли. Если оборвать сигнал не на трёх секундах, а на 2,9, чистый пик размазывается по соседним частотам. Оконная функция это лечит, но добавляет собственную размытость. Импульс в данных поднимает шумовой пол на всех частотах сразу, ступенька собирается у низких, прямоугольная волна даёт нечётные гармоники.

В конце он применяет всё это к годовому ряду почасовых данных с придуманной историей про стройку: в спектре видны годовой тренд, суточный и недельный циклы, а рабочая неделя проявляется гармониками с периодами 3,5, 2,4, 1,4 и 1,2 дня.

@prog_stuff
Видеокарта на 8 ГиБ, игра просит 9 — работает, средний кадр 19,6 мс. Просит 10 — кадр уже 29,8 мс со всплесками за 33,3.

Автор разбирает, что происходит за границей физической видеопамяти в Linux, на уровне amdgpu и TTM. Как только данные начинают ездить через PCIe, бюджет считается в лоб: у PCIe 4.0 x16 меньше 32 ГиБ/с, значит при 30 кадрах в секунду на кадр приходится около 1,075 ГиБ обмена. На RDNA3 обращение через шину оказалось примерно в 7,3 раза медленнее попадания в Infinity Cache и в 4,6 раза медленнее чтения из видеопамяти.

Отдельная беда в буферах вывода на экран: они обязаны лежать физически непрерывно, и ради примерно 32 МиБ такой памяти драйвер выселял до 4 ГиБ, тратя на одну передачу около 130 мс.

Цифры автор просит не считать универсальными: всё зависит от шаблона обращений и конкретной карты, часть работы в ядре ещё не прошла upstream, а экспериментальные ветки для SteamOS он предлагает трогать на свой риск.

@prog_stuff
1
Участник команды Zig описал устройство инкрементальной сборки: компилятор понимает, какие объявления изменились, пересобирает только их и вписывает готовые байты прямо в собранный бинарник. Холодная сборка приложения — 5 секунд, каждая следующая — 50–70 миллисекунд.

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

На картинке трассировка одного обновления. Обновление заняло 37 мс, из них полезной работы примерно 1,6 мс, а 31 мс ушла на обход графа ссылок, который в этот раз вообще не менялся. Автор пишет, что мог бы это оптимизировать перед публикацией ради красивой цифры, но показывает как есть: видно, сколько запаса лежит на поверхности.

Работает пока только под Linux на x86_64, возможны ложные ошибки компиляции.

@prog_stuff
🤔3
Автор трижды в трёх компаниях строил одну и ту же систему: принять вебхуки от Stripe или провайдера авторизации и держать у себя копию их данных. Каждый раз задача на вечер разрасталась в неделю: проверка подписи, таблица дедупликации, буфер для событий, пришедших не по порядку, стартовый импорт с блокировками и ночной cron сверки. Он разобрал, почему так выходит всегда.

Вебхук — это уведомление «что-то случилось» с доставкой at-least-once, без порядка, без полноты и без способа узнать, что ты что-то пропустил. Для запуска побочного эффекта это идеально, а для репликации данных нет: у автора отмена подписки клиента потерялась по дороге, и база месяцами считала его активным. Провайдер при этом держит у себя упорядоченный лог, режет его на POST-запросы и рассылает, а каждый получатель собирает лог обратно, со своими багами.

Ночной cron автор называет письменным признанием: «я не доверяю копии и буду пересобирать её с нуля каждую ночь». Альтернатива в статье — перевернуть стрелку: один URL на коллекцию, курсор, полное состояние объекта в каждом событии, tombstone на удаление, Prefer: stream для живого потока и checksum состояния в конце. Дедуп, буфер и стартовый импорт исчезают. Stripe и WorkOS уже отдают похожие Events API, но каждый со своей семантикой; автор оформил идею как черновик протокола SCROLL и сам оговаривает, что это предложение, а не стандарт.

@prog_stuff
🔥2
GitHub разобрал аварию 17 августа. Инцидент длился 7 часов 47 минут и задел Issues, пул-реквесты, API, Actions и Copilot; на пике веб и API отдавали примерно 20% ошибок, а скачивание архивов и сырых файлов — около 50%. Большинство сервисов пришло в норму к 16:36 UTC, Actions деградировал примерно до 18:03, а выдача токенов Copilot восстановилась к 21:02.

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

Дальше вмешались повторные попытки. Часть трафика перевели из Central US в Северную Вирджинию, где задержка ответов вскрыла скрытый баг ретраев в VS Code, и нагрузка выросла примерно в десять раз. Помогло не масштабирование, а обратное: паузу на проблемных узлах HAProxy и блокировка запросов кодом 403 на балансировщиках, после чего трафик возвращали постепенно.

@prog_stuff
❤‍🔥1👌1
Джулия Эванс рассказывает, чему научилась, гоняя небольшой сайт на SQLite. Это не «десять причин выбрать SQLite», а список мест, где она споткнулась.

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

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

Отдельная история с резервными копиями: VACUUM INTO с архивацией, restic в S3, который иногда падал по памяти, потом Litestream — и снова честное «не знаю, работает ли заданное хранение».

@prog_stuff
Инженер из Тринидада и Тобаго объясняет, почему спор об элегантности набора инструкций выглядит совсем иначе, когда доставка микросхемы за доллар стоит от 60 до 200 долларов.

Весь его аргумент про доступность железа. CH32V003 стоит десять центов: RV32EC, 16 регистров, без умножения и деления, 2 КБ SRAM и 16 КБ флеша, только machine mode. На другом конце того же RISC-V у него CH32H417 с двумя ядрами на 400 и 144 МГц, 896 КБ SRAM, 960 КБ флеша, USB 3.2 Gen1 и стомегабитным Ethernet, а ещё дальше VexRISC-V с MMU, на котором запускают Xous, seL4 и Linux. Одна база набора команд и один toolchain на всём этом пути, тогда как ARM разводит Cortex-M и Cortex-A по разным профилям и лицензиям.

С критикой RISC-V по существу автор не спорит: фрагментацию расширений и неудобства вокруг Zcb и Zicsr он признаёт сам и решения комитета не защищает. Это личный инженерный ответ с примерами собственных проектов, а не нейтральное сравнение архитектур.

@prog_stuff
👍1
Есть характеристика процессора, которой нет ни в одной спецификации, а на скорость программы она влияет сильнее модных цифр: сколько промахов мимо кэша ядро способно держать одновременно.

Поход в оперативную память стоит около 100 наносекунд — примерно 300 тактов простоя. Задержка эта за десять лет не улучшилась, а ухудшилась. Спасает то, что ядро не ждёт первый ответ, а успевает отправить следующие запросы.

Даниэль Лемир измерил, сколько именно, гоняя погоню по указателям в массиве на гигабайт: сначала одну цепочку, потом всё больше независимых, пока добавление новой не перестаёт помогать. У Intel предел вырос с 10 до 30 запросов, у AMD — с 15 до 58, у Graviton — с 6 до 19.

Практический смысл: структура с длинными цепочками указателей упирается не в задержку памяти, которая почти не меняется, а вот в этот предел. И он у разных процессоров различается в разы.

@prog_stuff
На iOS браузеры за пределами ЕС обязаны работать на WebKit, а прокси-браузеры вроде Tor-клиентов настраивают прокси через WKWebsiteDataStore.proxyConfigurations: весь трафик страницы должен уходить через прокси. Исследователи Mysk нашли три функции WebKit, которые эту настройку обходят и ходят в сеть напрямую с устройства.

<link rel="dns-prefetch"> резолвит имя через системный DNS, а не через прокси: сайт вставляет уникальное имя на посетителя и смотрит, с какого резолвера прилетает запрос. WebAuthn Related Origin Requests заставляет системный сервис учётных данных скачать файл проверки напрямую, раскрывая реальный IP. WebTransport поднимает прямое HTTP/3-соединение мимо прокси. Функции появились в iOS 26.0, 18.0 и 26.4 соответственно.

Всё это происходит вне обычного цикла загрузки страницы, поэтому утечки задевают и iCloud Private Relay; VPN не затронуты, они тянут трафик на уровне системы. Началось с одного бага от пользователя, у которого DNS утекал только на некоторых сайтах: без тега prefetch запроса просто не было. Авторы делают собственный браузер Psylo: в 1.3.1 prefetch блокируется, а WebTransport и WebAuthn выключены по умолчанию и включаются отдельной настройкой на сайт. PoC лежит на leaks.psylo.app.

@prog_stuff