Segfault у продакшені: чому один збій може зіпсувати квартал

Було корисно?

Збій сегментації здається дрібницею. Один процес помирає. Супервізор перезапускає його. Хтось пожимає плечима і каже «космічний промінь»,
потім повертається до випуску фіч. І саме так один краш може зіпсувати квартал: не через сам segfault,
а через ланцюгову реакцію, для якої ви не спроєктували систему.

У продакшені segfault рідко буває «помилкою в одному бінарнику». Це системна подія. Вона перевіряє вашу гігієну деплою,
модель довговічності даних, механізми зниження навантаження, спостережуваність і організаційну чесність. Якщо ви ставитесь до неї як
до незручності локального розробника, ви ще побачите це — під час наради ради директорів.

Що насправді означає segfault (і чого він не означає)

Segfault — це спосіб операційної системи застосувати захист пам’яті. Ваш процес спробував звернутися до пам’яті, якої не мав:
недійсна адреса, сторінка без дозволів або адреса, яка була дійсною, поки ви її не звільнили і не почали знову використовувати.
Ядро посилає сигнал (зазвичай SIGSEGV), і якщо ви його не обробляєте (що майже ніколи не слід робити), процес помирає.

«Segfault» — це ярлик симптома. Підлягаюча причина може бути:

  • Use-after-free та інші помилки життєвого циклу (класика).
  • Переповнення буфера (запис за межі, корупція метаданих, падіння пізніше в іншому місці).
  • Дереференціювання нульового вказівника (дешево, прикро, все ще трапляється у 2026).
  • Переповнення стеку (рекурсія або великі стек-кадри, часто викликається несподіваним вводом).
  • ABI mismatch (плагін лінкується з невірною версією бібліотеки; код «працює», поки не перестає).
  • Апаратні проблеми (рідко, але не міфічно: погана ОЗП і ненадійні CPU існують).
  • Порушення контракту ядра/простору користувача (наприклад, seccomp-фільтри, незвичні відображення пам’яті).

Ось чого це зазвичай не означає: «ОС випадково нас вбила». Linux не налаштований проти вашого сервісу.
Якщо процес сегфолтить, хтось записав у пам’ять те, чого не слід, або виконав код, який не треба було, або повернувся за недійсною адресою.
Ядро тут просто охоронець.

Суха операційна істина: якщо ваша відповідь на інцидент починається з «це, мабуть, просто один краш», ви вже відстаєте.
Краші — це те, як некоректна поведінка проявляє себе.

Жарт №1: Segfault — це спосіб програми сказати «я хочу поговорити з менеджером», тільки менеджер — це ядро, і воно не веде перемовини.

Краш ≠ аутедж, якщо ви так не спроєктували

Один краш може бути незначною подією, якщо ваша система до цього готова:
граціозне деградування, повтори з джиттером, ідемпотентні операції, обмежені черги та надійний стан.
Але якщо ваш краш виводить шард, корумпує кеш у пам’яті, від якого залежать база даних, блокує вибори лідера
або спричинює шторм перезапусків, ви перетворили «поганий вказівник» на подію з фінансовими наслідками.

Ключові концепти для триажу

  • Місце крашу vs. місце помилки: інструкція, яка впала, часто далеко від запису, що пошкодив пам’ять.
  • Детермінізм: якщо падіння відбувається на тій самій інструкції при тому самому запиті, це, ймовірно, проста помилка; якщо ні — підозрюйте корупцію пам’яті, гонки чи апарат.
  • Радіус ураження: технічна помилка може бути малою; операційна помилка (зв’язність) — ось що болить.
  • Час до першого сигналу: ваша перша задача не «встановити корінь причини». Це «зупинити витік крові, зберігши докази».

Парафразована ідея від John Gall (мислитель систем): складні системи, які працюють, часто еволюціонують з простіших систем, що працювали. Якщо ви не можете пережити один краш, ваша система ще не завершила еволюцію.

Чому один краш може зіпсувати квартал

Частина про «закінчення кварталу» — це не мелодрама. Це те, як сучасні системи і бізнеси підсилюють невеликі технічні події:
тісна зв’язність, спільні залежності, агресивні SLO і звичка бізнесу нашаровувати релізи.

Стандартна каскада

Типова послідовність виглядає так:

  1. Один процес сегфолтить під певною формою запиту або патерном навантаження.
  2. Супервізор швидко перезапускає його (systemd, Kubernetes, ваш watchdog).
  3. Стан втрачається (запити в роботі, сесійне сховище в пам’яті, cached auth tokens, офсети черг, оренди лідера).
  4. Клієнти повторюють запити (іноді коректно, часто синхронізовано у хвилю).
  5. Латентність зростає, бо перезапущений процес гріється, відновлює кеші, пул підключень, реплеїть логи.
  6. Низькорівневі залежності піддаються удару (бази даних, об’єктне сховище, зовнішні API).
  7. Автомасштаб реагує запізніло (або не реагує), бо метрики відстають, а політики консервативні.
  8. Більше падінь відбувається через тиск пам’яті, таймаути і накопичення черг.
  9. Оператори панікують і перезапускають вручну, видаляючи докази (core dump’и та логи зникають).
  10. Фінанси помічають, бо змінюється поведінка, що видно клієнтам: відмовлені покупки, пропущені аукціони реклами, затримані розрахунки.

Як сховище робить усе гірше (так, навіть якщо «це краш»)

Як інженер зі сховищ скажу прямо: сховище перетворює краші на грошові проблеми, бо саме там «тимчасовий» стан стає довготривалим. Segfault може:

  • Пошкодити локальний стан, якщо ваш процес пише неатомарно і не виконує fsync відповідно.
  • Пошкодити віддалений стан через часткові записи, неправильне використання протоколу або баги в реплеї.
  • Спровокувати реплеї (споживачі Kafka, WAL-перепроігрування, повтори з листингом об’єктів), що множать навантаження й витрати.
  • Призвести до мовчазної втрати даних, коли процес помирає між визнанням і комітом.

Багато команд «вирішують» краші додаванням повторів. Повтори — це не надійність; це множник навантаження.
Якщо ваша політика повторів не обмежена, не має джиттера і не враховує ідемпотентність, ви творите генератор аутеджів.

Бізнес-рівень: чому це з’являється на звітах про прибутки

Бізнес не переймається вашим стек-трейсом. Він переймається:

  • Падінням коефіцієнта конверсії, коли латентність підскакує і таймаути зростають.
  • Штрафами по SLA/SLO (явні штрафи або відтік клієнтів).
  • Витратами на підтримку і шкодою бренду, коли клієнти бачать непослідовну поведінку.
  • Втраченими можливостями: ви зупиняєте деплои і відкладаєте релізи, щоб стабілізувати.
  • Відволіканням інженерів: найкращі люди тягнуться в інцидент-респонс на дні.

Segfault може бути «одна лінія коду». Але в продакшені це тест ваших амортизаторів удару.
Більшість систем не проходять цей тест, бо амортизатори ніколи не були встановлені — їх просто обіцяли.

Факти та історія: чому ми продовжуємо робити це собі

  • Захист пам’яті старший за вашу компанію. Аппаратно реалізований захист сторінок і фолти були мейнстрімом задовго до Linux; segfault — це «фіча, яка працює».
  • Ранній Unix популяризував файл core. Видача образу процесу при падінні була прагматичним інструментом налагодження, коли інтерактивний дебаг був складнішим.
  • SIGSEGV не те саме, що SIGBUS. Segfault — недійсний доступ; bus error часто вказує на проблеми вирівнювання або з mmap-файлами/пристроями.
  • ASLR змінив гру. Address Space Layout Randomization ускладнив експлойтність і зробив налагодження трохи незручнішим; символізовані бектрейси стали важливішими.
  • Аллокатори купи еволюціонували, бо краші дорого обходилися. Сучасні аллокатори (ptmalloc, jemalloc, tcmalloc) по-різному торгують швидкістю, фрагментацією та можливостями для налагодження.
  • Дебаг-символи стали продакшн-концерном. Зі зростанням continuous deployment «ми відтворимо локально» стало фантазією; вам потрібні символи і build ID, щоб дебажити те, що реально запускалося.
  • Контейнеризація ускладнила core dump-и. Namespace-и і ізоляція файлових систем означають, що core-файли можуть зникнути, якщо ви навмисно їх не маршрутизували кудись.
  • «Fail fast» зрозуміли неправильно. Failing fast — добре, коли це запобігає корупції; погано, коли воно викликає координовані повтори і втрату стану без запобіжних механізмів.
  • Сучасні ядра люб’язні й інформативні. dmesg може містити адресу фолту, інструкційний покажчик і навіть офсети бібліотек — якщо ви зберегли логи.

Segfault-ів не стало більше. Ми просто побудували навколо них вищі вежі залежностей.

Швидкий план діагностики (перший/другий/третій)

Це 15–30-хвилинний план, щоб знайти вузьке місце і обрати правильну стратегію стримування.
Не щоб назавжди вирішити баг — ще ні.

Перший: зупинити шторм перезапусків і зберегти докази

  • Стабілізувати: масштабувати назовні, тимчасово відключити агресивні повтори та додати ліміти швидкості.
  • Зберегти: переконатися, що core dump-и та логи переживуть перезапуски (або хоча б зберегти останнє падіння).
  • Перевірити радіус ураження: це один хост, одна зона доступності, одна версія, одне навантаження?

Другий: ідентифікувати підпис крашу

  • Де впало? ім’я функції, модуль, офсет, фолтова адреса.
  • Коли почалося? корелюйте з деплоєм, зміною конфігурації, патерном трафіку.
  • Чи детерміновано? той самий запит викликає, той самий стек, той самий хост?

Третій: оберіть гілку на основі найсильнішого сигналу

  • Тільки одна версія бінарника: відкат або відключення feature gate; продовжуйте з аналізом core.
  • Тільки певні хости: підозрюйте апарат, ядро, libc або дрейф конфігурації; дренуйте і порівнюйте.
  • Тільки високі навантаження: підозрюйте гонку, тиск пам’яті, таймаути, що ведуть до небезпечних шляхів очищення.
  • Після проблем із залежностями: підозрюйте баг обробки помилок (null deref на невдалу відповідь тощо).

Пастка: одразу заводитися в GDB, поки система ще флапає. Виправіть операційний режим помилки спочатку.
Вам потрібен стабільний пацієнт перед операцією.

Практичні завдання: команди, виводи, рішення (12+)

Це реальні завдання, які я очікую від інженера on-call під час розслідування крашу. Кожне містить: команду, реалістичний вивід,
що це означає, і яке рішення прийняти далі.

Завдання 1: Підтвердити краш у journal і отримати сигнал

cr0x@server:~$ sudo journalctl -u checkout-api.service -S "30 min ago" | tail -n 20
Jan 22 03:11:07 node-17 checkout-api[24891]: FATAL: worker 3 crashed
Jan 22 03:11:07 node-17 systemd[1]: checkout-api.service: Main process exited, code=dumped, status=11/SEGV
Jan 22 03:11:07 node-17 systemd[1]: checkout-api.service: Failed with result 'core-dump'.
Jan 22 03:11:08 node-17 systemd[1]: checkout-api.service: Scheduled restart job, restart counter is at 6.
Jan 22 03:11:08 node-17 systemd[1]: Stopped Checkout API.
Jan 22 03:11:08 node-17 systemd[1]: Started Checkout API.

Що означає вивід: статус 11/SEGV підтверджує SIGSEGV. code=dumped натякає, що core dump існує (або systemd так вважає).

Рішення: уповільнити цикл перезапусків (RestartSec) і знайти core dump, поки його не прокрутили.

Завдання 2: Перевірити повідомлення ядра для фолтової адреси і IP

cr0x@server:~$ sudo dmesg -T | tail -n 8
[Wed Jan 22 03:11:07 2026] checkout-api[24891]: segfault at 0000000000000038 ip 000055c1b2f9a4d2 sp 00007ffd7b5d1a10 error 4 in checkout-api[55c1b2f82000+3e0000]
[Wed Jan 22 03:11:07 2026] Code: 48 8b 47 38 48 85 c0 74 0a 48 8b 00 48 8b 50 10 ff 52 08 48 8b 47 10 <48> 8b 40 08

Що означає вивід: фолт на 0x38 — класичний патерн нульового вказівника + зсув (дереференціювання поля структури від нульової бази). ip вказує в сегмент тексту вашого бінарного файлу.

Рішення: пріоритезувати гіпотези «поганий вказівник в шляху обробки помилок»; витягнути core і символи, щоб підтвердити яке саме поле.

Завдання 3: Знайти core через systemd-coredump

cr0x@server:~$ coredumpctl list checkout-api | tail -n 3
TIME                            PID   UID   GID SIG COREFILE  EXE
Wed 2026-01-22 03:09:55 UTC   24602  1001  1001  11 present   /opt/checkout/bin/checkout-api
Wed 2026-01-22 03:11:07 UTC   24891  1001  1001  11 present   /opt/checkout/bin/checkout-api

Що означає вивід: core файли присутні для тих PID-ів. Добре — докази існують.

Рішення: негайно витягнути найновіший core у безпечне місце (cores видаляються).

Завдання 4: Витягнути core на диск, щоб він пережив ротацію

cr0x@server:~$ sudo coredumpctl dump 24891 --output=/var/tmp/checkout-api.core
/var/tmp/checkout-api.core

Що означає вивід: core записано у стабільний шлях.

Рішення: зробіть снапшот саме того виконуваного файлу та його бібліотек, що відповідають build ID.

Завдання 5: Перевірити build ID бінарника (щоб символи збігалися)

cr0x@server:~$ readelf -n /opt/checkout/bin/checkout-api | grep -A2 "Build ID"
    Build ID: 9b7c6fd3b2a9cdb5d3d1c9e0a4f2f7aa12c0f2ab

Що означає вивід: цей build ID має збігатися з пакетом символів/артефактом символів.

Рішення: отримайте символи для цього build ID; якщо не можете — доведеться працювати з офсетами, що повільніше й ризикованіше.

Завдання 6: Швидкий бектрейс з core (best-effort)

cr0x@server:~$ gdb -q /opt/checkout/bin/checkout-api /var/tmp/checkout-api.core -ex "set pagination off" -ex "thread apply all bt" -ex "quit"
Reading symbols from /opt/checkout/bin/checkout-api...
(No debugging symbols found in /opt/checkout/bin/checkout-api)
[New LWP 24891]
Core was generated by `/opt/checkout/bin/checkout-api --config /etc/checkout/config.yaml'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0  0x000055c1b2f9a4d2 in ?? ()
#1  0x000055c1b2f63c10 in ?? ()
#2  0x00007f2b8e31c1f5 in __libc_start_main () from /lib/x86_64-linux-gnu/libc.so.6
#3  0x000055c1b2f6456a in ?? ()

Що означає вивід: немає символів, тому фрейми невідомі. Але у вас є адреса крашу 0x55c1b2f9a4d2.

Рішення: перетворіть адресу крашу у функцію за допомогою addr2line, коли символи будуть; тим часом використайте мапу бінарника, щоб обчислити офсет.

Завдання 7: Підтвердити мапінги пам’яті, щоб обчислити офсет

cr0x@server:~$ gdb -q /opt/checkout/bin/checkout-api /var/tmp/checkout-api.core -ex "info proc mappings" -ex "quit"
Mapped address spaces:

          Start Addr           End Addr       Size     Offset objfile
      0x000055c1b2f82000 0x000055c1b3362000 0x003e0000 0x0000000000000000 /opt/checkout/bin/checkout-api
      0x00007f2b8e2f0000 0x00007f2b8e4d0000 0x001e0000 0x0000000000000000 /lib/x86_64-linux-gnu/libc.so.6

Що означає вивід: бінарник змеплений починаючи з 0x55c1b2f82000. Ваш IP крашу в цьому діапазоні.

Рішення: обчислити офсет: 0x55c1b2f9a4d2 - 0x55c1b2f82000. Використайте цей офсет з інструментами символів.

Завдання 8: Перевірити, чи core не обрізуються лімітами

cr0x@server:~$ ulimit -c
0

Що означає вивід: ліміт розміру core для поточної оболонки — нуль; залежно від менеджера сервісів, unit може перевизначати це, але часто це означає «жодних core».

Рішення: переконайтеся, що systemd unit має LimitCORE=infinity або налаштуйте coredump.conf правильно; інакше ви будете дебажити всліпу.

Завдання 9: Перевірити політику збереження core у systemd

cr0x@server:~$ sudo grep -E '^(Storage|ProcessSizeMax|ExternalSizeMax|MaxUse|KeepFree)=' /etc/systemd/coredump.conf
Storage=external
ProcessSizeMax=2G
ExternalSizeMax=2G
MaxUse=8G
KeepFree=2G

Що означає вивід: core зберігаються зовні з лімітами. Якщо ваш процес має >2G RSS під час крашу, core можуть бути обрізані або відсутні.

Рішення: якщо cores відсутні або обрізані, тимчасово підніміть ліміт на уражених вузлах або відтворіть у контрольованих умовах з меншим споживанням пам’яті.

Завдання 10: Виключити OOM-kill (інша причина загибелі з тим же симптомом)

cr0x@server:~$ sudo journalctl -k -S "1 hour ago" | grep -i -E "oom|killed process" | tail -n 5

Що означає вивід: порожній вивід натякає на відсутність OOM-kill за останню годину.

Рішення: продовжуйте фокус на segfault; якщо ви бачите OOM-kill, спочатку розглядайте тиск пам’яті (і segfault може бути вторинною корупцією під стресом).

Завдання 11: Перевірити шторм перезапусків і обмежити їх

cr0x@server:~$ systemctl show checkout-api.service -p Restart -p RestartUSec -p NRestarts
Restart=always
RestartUSec=200ms
NRestarts=37

Що означає вивід: затримка перезапуску 200ms і 37 перезапусків — це самонаведений стрес-тест. Це підсилює downstream-повтори і може виснажити хост.

Рішення: збільшити затримку перезапуску (секунди або хвилини) і розглянути StartLimitIntervalSec/StartLimitBurst, щоб уникнути флапу, що виводить вузол з ладу.

Завдання 12: Перевірити, чи краш корелює з деплоєм (не гадати)

cr0x@server:~$ sudo journalctl -u checkout-api.service -S "6 hours ago" | grep -E "Started|Stopping|version" | tail -n 15
Jan 21 22:10:02 node-17 systemd[1]: Started Checkout API.
Jan 22 02:58:44 node-17 checkout-api[19822]: version=2.18.7 git=9b7c6fd3 feature_flags=pricing_v4:on
Jan 22 03:11:08 node-17 systemd[1]: Started Checkout API.

Що означає вивід: у логах зафіксовано версію і стан feature-flag. Це золото.

Рішення: якщо краш почався після вмикання прапорця, вимкніть його спочатку. Якщо після оновлення версії — відкотіться, поки аналізуєте.

Завдання 13: Перевірити резолюцію спільних бібліотек (ловимо ABI mismatch)

cr0x@server:~$ ldd /opt/checkout/bin/checkout-api | head -n 12
linux-vdso.so.1 (0x00007ffd7b7d2000)
libssl.so.3 => /lib/x86_64-linux-gnu/libssl.so.3 (0x00007f2b8e600000)
libcrypto.so.3 => /lib/x86_64-linux-gnu/libcrypto.so.3 (0x00007f2b8e180000)
libstdc++.so.6 => /lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007f2b8df00000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f2b8e2f0000)

Що означає вивід: підтверджує, які бібліотеки завантажені. Якщо ви очікували інші версії (наприклад, в /opt), у вас проблема пакування або середовища виконання.

Рішення: якщо є дрейф залежностей, зафіксуйте і перейдеплойте; дебажити краш на невідповідному ABI — марна трата часу.

Завдання 14: Перевірити аномалії на хості (заповнений диск може зіпсувати core і логи)

cr0x@server:~$ df -h /var /var/tmp
Filesystem                         Size  Used Avail Use% Mounted on
/dev/mapper/vg0-var                60G   58G  1.6G  98% /var
/dev/mapper/vg0-var                60G   58G  1.6G  98% /var/tmp

Що означає вивід: ви майже витратили весь простір там, куди пишуться cores. Це означає «скоро жодних доказів», і, можливо, проблеми зі службою, якщо логи не зможуть писатися.

Рішення: негайно звільніть простір (очистити журнал, ротувати cores) або перенаправте сховище core на більшу файлову систему на час інциденту.

Завдання 15: Підтвердити зв’язок крашу з конкретним запитом (логи застосунку + семпли)

cr0x@server:~$ sudo journalctl -u checkout-api.service -S "10 min ago" | grep -E "request_id|panic|FATAL" | tail -n 10
Jan 22 03:11:07 node-17 checkout-api[24891]: request_id=9f2b4d1f path=/checkout/confirm user_agent=MobileApp/412.3
Jan 22 03:11:07 node-17 checkout-api[24891]: FATAL: worker 3 crashed

Що означає вивід: request ID і шлях одразу перед крашем — сильна підказка. Не доказ, але напрямок.

Рішення: витягніть семпл недавніх запитів для цього endpoint’а, проінспектуйте форму payload-ів і розгляньте тимчасове обмеження або feature-gating цього коду.

Три корпоративні історії з місць падіння

Міні-історія 1: Неправильне припущення (нульовий вказівник, якого «не могло бути»)

Сервіс, що торкається платежів, почав сегфолтити раз на день. Потім раз на годину. Команда on-call ставилася до цього як до шумного, але керованого бага:
процес швидко перезапускався, і більшість клієнтів не помічали — поки не помітили. Латентність повільно росла, кількість помилок підскакувала в пікові години,
і канал інцидентів став стилем життя.

Перше розслідування пішло за інфраструктурою: оновлення ядра, прошивки хостів, нещодавній патч бібліотеки. Розумні припущення, але не влучні.
Єдиною послідовною підказкою була фолтова адреса: завжди малий офсет від нуля. Класичний нульовий deref.
Та в кодовій базі «звісно» перевіряли на null. «Звісно» — дороге слово.

Баг виявився в неправильному припущенні про upstream-залежність: «це поле завжди присутнє». Воно було присутнє 99.98% часу.
Потім партнер випустив зміну, яка опустила його для нішевого тарифного рівня. Код парсив JSON у структуру, лишав вказівник незадефайненим,
а згодом використовував його в шляху очищення, який ніхто не навантажував тестами, бо знову ж «не могло статись».

Виправлення — одна рядкова перевірка і краща модель помилок. Операційне рішення було цікавішим:
вони додали розривальник (circuit breaker) на інтеграцію партнера, перестали повторювати на пошкодженому вводі і ввели канарку, що симулювала випадок «відсутнього поля».
Segfault перестав бути ризиком для кварталу, бо система перестала вважати світ ввічливим.

Міні-історія 2: Оптимізація, що зіграла злий жарт (швидший аллокатор, повільніша компанія)

Пайплайн високопропускного вводу поміняв аллокатор, щоб знизити витрати CPU і поліпшити хвости латентності. У бенчмарках це спрацювало.
У продакшені частота крашів повільно зростала, потім різко. Segfault-и. Іноді double free. Іноді недійсне читання.
Команда підозрювала свій код, і це було справедливо.

Причина не в тому, що аллокатор «поганий». Зміна змінила розклад пам’яті і таймінги достатньо, щоб вивести на поверхню існуючу гонку.
Раніше гонка писала в пам’ять, до якої ніхто довго не звертався. З новим аллокатором той самий регіон швидше перевикористовувався.
Невизначена поведінка перестала бути латентною і стала голосною.

Потім з’явився операційний мультиплікатор: їх оркестратор мав агресивні лівнес-чекі і негайні перезапуски.
Під краш-лупами запізнювався ingestion. Споживачі відставали. Backpressure не проходив правильно.
Пайплайн намагався наздогнати, забираючи ще роботу. Він став самошкідливою машиною.

Довгострокове виправлення — корекція гонки і додавання покриття TSAN/ASAN у CI для ключових компонентів.
Короткострокове, що врятувало тиждень бізнесу — відкат аллокатора і встановлення жорсткої межі паралелізму, поки лаг був вище порогу.
Вони дізналися нудну істину: зміни продуктивності — це зміни коректності в капюшоні.

Міні-історія 3: Нудна, але правильна практика, що врятувала день (символи, core і спокійний ролбек)

Внутрішній RPC-сервіс сегфолтився після звичного апдейту залежності. Інженер on-call не почав зі спелеології кодової бази.
Він почав зі впевненості, що система перестане флапати і що краш буде дебажебельним.
Збільшив затримку перезапуску. Злинював уражені вузли. Зберіг core-и.

Команда місяцями раніше зробила нудну, непримітну річ: кожна збірка публікувала дебаг-символи, зафіксовані по build ID, а рантайм логував build ID при старті.
Був також рукбук «якщо процес викидає core, скопіюй його з вузла перед будь-якими хитрощами».
Ніхто не святкував цю практику при введенні. Треба було.

За годину вони мали символізований бектрейс, що вказував на конкретну функцію, яка обробляла помилкову відповідь.
Залежність тепер повертала порожній список там, де раніше був null; код трактував «порожнє» як «є принаймні один елемент» і індексував його.
Одна рядкова правка, одна таргетована тестова, і канарковий деплой.

Сервіс залишався стабільним весь час, бо шлях відкату був чистим і відпрацьованим.
Краш не став історією про квартал, бо компанія інвестувала в докази, а не в героїчні витівки.

Жарт №2: Нічого так не прискорює «вирівнювання команди», як краш-луп о 3 ранку; раптом всі погоджуються з пріоритетами.

Поширені помилки: симптом → корінь → виправлення

Це патерни, що повторюються знову і знову. Вивчіть їх раз — і заощадите повтори.

1) Симптом: segfault на адресі 0x0 або малий офсет (0x10, 0x38, 0x40)

Корінь: нульовий вказівник, часто в шляхах обробки помилок або очищення.

Виправлення: додати явні перевірки на null; але також виправити інваріанти — чому цей вказівник може бути null? Додайте тести для відсутніх/недійсних вводів.

2) Симптом: місце крашу змінюється щоразу; стеки виглядають випадково

Корінь: корупція пам’яті (переповнення буфера, use-after-free), часто раніше за момент крашу.

Виправлення: відтворіть з ASAN/UBSAN; включіть жорсткіший аллокатор у стаджингу; зменшіть конкурентність, щоб звузити часові вікна; аудитуйте небезпечний код і межі FFI.

3) Симптом: segfault з’являється тільки під навантаженням, зникає при додаванні логів

Корінь: гонка; зміна таймінгів її ховає. Логи, що «виправляють» краш — класичний знак гонки.

Виправлення: запуск TSAN у CI; додати дисципліну блокувань; використовувати потокобезпечні структури; нав’язати правила власності (особливо для колбеків).

4) Симптом: тільки одна AZ/пул хостів бачить краш

Корінь: дрейф конфігурації, різні версії бібліотек, особливості CPU, відмінності ядра або поганий хардвер.

Виправлення: порівняйте пакунки і версії ядра; перемістіть навантаження; запустіть тести пам’яті, якщо підозра лишається; перебудуйте золоті образи і усуньте дрейф.

5) Симптом: краш співпав з «успішною» оптимізацією продуктивності

Корінь: оптимізація змінила розклад пам’яті або таймінги, виявивши UB; або видалила перевірки меж.

Виправлення: спочатку відкотити; потім повернути з запобіжними механізмами і санітайзерами; ставитися до оптимізації як до ризикової зміни, як і до міграції схеми.

6) Симптом: ніяких core dump-ів ніде, незважаючи на повідомлення «core-dump»

Корінь: ліміти core встановлені в нуль, core надто великі і обрізаються, диск заповнений, runtime контейнера не дозволяє писати core або systemd-coredump налаштований на відкидання.

Виправлення: встановити LimitCORE=infinity; підняти ліміти розміру core; забезпечити простір; протестувати дамп core навмисно у стаджингу.

7) Симптом: «segfault», але лог ядра показує general protection fault або illegal instruction

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

Виправлення: перевірити таргет збірки, CPU-флаги і ABI бібліотек; перевірити корупцію пам’яті; упевнитися, що ви не деплойте бінарники для іншої мікроархітектури.

8) Симптом: краші зупиняються, коли ви відключаєте один endpoint або feature flag

Корінь: конкретна форма вводу триггерить undefined behavior; часто парсинг, межі або обробка опціональних полів.

Виправлення: залишайте прапорець вимкненим поки не виправите; додайте валідацію вводів; додайте фузінг для того парсера і контракт-тести для upstream-залежностей.

Чек-листи / покроковий план

Чек-лист стримування (перша година)

  1. Зменшити радіус ураження: дренувати уражені вузли, зменшити трафік або направити його від версії.
  2. Зупинити шторм перезапусків: збільшити затримку перезапуску; обмежити рестарти; уникати трешингу залежностей.
  3. Зберегти докази: скопіювати core з вузла; зробити снапшот логів; зафіксувати build ID та конфіг/прапорці.
  4. Отримати підпис крашу: лінія ядра (адреса фолту, IP), хоча б один бектрейс (навіть без символів), частота і тригери.
  5. Обрати найбезпечніший шлях: відкат краще за «живий дебаг», коли клієнти горять.

Чек-лист діагностики (цей же день)

  1. Символізувати краш: співставити build ID, отримати символи, отримати читабельний бектрейс.
  2. Класифікувати: нульовий deref vs. корупція пам’яті vs. гонка vs. середовище/апарат.
  3. Відтворити: захопити тригерний запит; збудувати мінімальний репро; запускати під санітайзерами.
  4. Підтвердити охоплення: які версії, які пулли хостів, які входи уражені.
  5. Патчити безпечно: додати тести; канарка; поступовий rollout; перевірити час без крашів перед повним деплоєм.

Чек-лист запобігання (квартальна гігієна, що платить рахунки)

  1. Завжди публікуйте build ID і символи. Зробіть отримання символів нудним і автоматизованим.
  2. Тримайте feature flags для ризикових шляхів коду. Не все потребує прапорця; код, що падає, — потребує.
  3. Визначте ідемпотентність і політики повторів. «Повторювати все» — це як DDoS самому собі.
  4. Фузьте парсери і граничні ділянки. Краї — це місця, де народжуються segfault-и.
  5. Використовуйте санітайзери в CI для ключових компонентів. Особливо для C/C++ і FFI-країв.
  6. Плануйте смерть процесу. По можливості — stateless; там, де потрібно — durable; всюди — граціозно.

FAQ

1) Чи завжди segfault — це програмний баг?

Майже завжди так. Аппаратні причини можливі (погана пам’ять), але вважайте апарат підозрюваним лише після наявності доказів:
краші, що повторюються на конкретній машині, виправлені помилки пам’яті або повторні збої на тому самому хості.

2) У чому різниця між SIGSEGV і SIGBUS?

SIGSEGV — недійсний доступ до пам’яті (дозволи або немепована пам’ять). SIGBUS часто пов’язаний з проблемами вирівнювання або помилками з mmap-файлами/пристроями.
Обидва означають «процес зробив те, чого не слід», але шлях налагодження відрізняється.

3) Чому краш відбувається «десь інакше», ніж помилка?

Корупція пам’яті — це хаос з відкладеною дією. Ви перезаписуєте метадані або сусідній об’єкт, програма продовжує працювати, поки не звернеться до отруєної області.
Місце крашу — там, де система це помітила, а не там, де ви вчинили «злочин».

4) Чи варто ловити SIGSEGV і продовжувати роботу?

Ні, не для загального прикладного коду. Безпечно відновитися практично неможливо, бо стан процесу недовірливий.
Обробники SIGSEGV використовують лише для логування мінімального діагностичного контексту і потім виходу.

5) Чому core dump-и відсутні в контейнерах?

Часто тому, що ліміти core встановлені в нуль, файловий простір доступний тільки для читання або обмежений за розміром, або runtime контейнера блокує дамп core.
Необхідно явним чином налаштувати поведінку дампу core для конкретного рантайму і забезпечити наявність сховища.

6) Якщо в мене немає дебаг-символів, чи core марний?

Не марний, але повільніший. Ви все ще можете використовувати інструкційні покажчики, офсети мапінгу і build ID, щоб локалізувати код.
Але ви витратите більше часу і ризикуєте неправильно приписати причину. Символи дешевші, ніж простої сервісу.

7) Чи може segfault пошкодити дані?

Так. Якщо ви падаєте під час запису без атомарності і гарантій довговічності, можете залишити частковий стан.
Якщо ви підтверджуєте роботу до коміту, ви можете втратити дані. Якщо ви повторюєте без ідемпотентності, можете дуплікувати дані.

8) Яка найшвидша безпечна міра, коли краші ростуть після деплою?

Відкотіться або відключіть feature flag. Робіть це рано. Потім аналізуйте, маючи збережені докази.
Героїчний дебаг поки клієнти горять продовжує інцидент і породжує міфи замість виправлень.

9) Як зрозуміти, чи це гонка?

Краші, що зникають при додаткових логах, змінюються з кількістю CPU або показують різні стеки під навантаженням — сильні індикатори.
TSAN і контрольовані стрес-тести — ваші друзі.

Висновок: наступні кроки, які справді зменшують збої

Segfault — це не просто «баг». Це системна відмова, що відкриває те, що ви припускали про пам’ять, входи, залежності і відновлення.
Краш — найменша частина історії. Решта — це зв’язність, повтори, обробка стану і те, чи зберегли ви докази.

Зробіть наступне, у такому порядку:

  1. Зробіть cores і символи обов’язковими. Якщо ви не можете дебажити те, що запускалося, ви працюєте надією.
  2. Загартуйте поведінку перезапусків. Повільні перезапуски, обмеження флапу і уникнення штормів повторів.
  3. Проєктуйте під смерть процесу. Ідемпотентність, межі довговічності і граціозне деградування — як один краш залишається одним крашем.
  4. Інвестуйте в санітайзери і фузінг там, де треба. Особливо в парсерах, FFI і гарячих точках конкуренції.
  5. Пишіть постмортеми так, ніби ви будете їх читати знову. Бо так і буде — якщо ви не виправите операційний підсилювач, а не тільки рядок коду.

Мета не в тому, щоб назавжди усунути всі segfault-и. Мета — зробити segfault нудним: локалізованим, дебажебельним і нездатним пограбувати квартал.

← Попередня
AVX-512: чому одні його люблять, а інші бояться
Наступна →
MySQL проти PostgreSQL: «випадкові таймаути» — мережа, DNS і пулінг як винуватці

Залишити коментар