Ви оновилися до Ubuntu 24.04, ваш pipeline розгортання залишився зеленим, а потім сервіс упав з різкою підказкою:
Illegal instruction. Немає довіреного стеку викликів. Немає корисних логів. Лише core dump і pager, який тепер володіє вашим вечором.
Це зазвичай не «Ubuntu поводиться дивно». Це фізика: CPU спробував виконати opcode, який він не підтримує, бо ваш бінарник (або одна з його бібліотек) припускав інший набір прапорців CPU, ніж фактично є на машині. Виправлення — не «перевстановити пакет» або «спробувати інший ядро». Потрібно узгодити вашу збірку та розгортання щодо того, які інструкції допустимі.
Швидкий план діагностики
Коли процес падає з Illegal instruction, допоки не доведено зворотнє — ви дебагуєте невідповідність набору інструкцій. Не починайте з
перевстановлення пакетів. Почніть з доведення, який opcode викликав помилку і які можливості CPU доступні.
По-перше: підтвердіть, що це справді SIGILL і зафіксуйте місце
- Перевірте journald / повідомлення ядра на наявність SIGILL і адреси faulting instruction pointer.
- Підтвердіть бінарник, який впав (не лише wrapper-скрипт).
- Отримайте backtrace (навіть грубий) з core-файлу.
По-друге: порівняйте прапорці CPU із тим, що очікує бінарник
- Зберіть прапорці CPU з
/proc/cpuinfo(а також зсередини контейнера/VM, якщо це релевантно). - Визначте, чи було бінарник скомпільовано з
-march=native,-mavx2або з мікроархітектурним baseline для x86-64. - Перевірте, чи ваша libc обирає оптимізований варіант через hwcaps директорії.
По-третє: оберіть чистий шлях виправлення
- Якщо це ваш код: пересоберіть з правильним baseline, публікуйте кілька таргетів або додайте runtime dispatch.
- Якщо це сторонній бінарник: отримайте сумісну збірку, зафіксуйте сумісну версію або змініть модель CPU, яку оголошує гіпервізор.
- Якщо це проблема гетерогенності флота: сегментуйте розгортання за можливостями CPU і дозвольте scheduler застосовувати це правило.
Парафразована ідея (приписується практикам надійності): Надія — не стратегія
.
У цьому випадку: «Ми сподівалися, що всі хости мають AVX2» — це не план. Це хронологія інциденту.
Що насправді означає «Illegal instruction» у Linux
У Linux «Illegal instruction» майже завжди відповідає сигналу SIGILL, який доставляється процесу.
CPU спробував декодувати/виконати opcode, який недійсний для поточного рівня ISA, або потрапив на привілейовану/заборонену інструкцію в користувацькому просторі.
На практиці часта причина — невідповідність розширень ISA: ваш бінарник використовує AVX/AVX2/AVX-512, SSE4.2, BMI1/2, FMA тощо,
але CPU (або віртуальний CPU, представлений гостьовій системі) цього не підтримує.
Підпис невдачі грубий: немає корисної помилки, часто немає логів додатка, іноді навіть немає корисного backtrace, якщо ви не ввімкнули core dumps.
Виправляється це вирівнюванням того, для чого ви збирали, з тим, на що ви розгорнули.
Маленька, але повторювана плутанина: SIGILL не те саме, що segfault. Segfault — це доступ до пам’яті.
SIGILL — це «CPU відмовляється». Це також може статися через пошкоджені бінарники або помилковий JIT-вивід, але у флоті це в основному невідповідність можливостей.
Жарт №1: Неприпустима інструкція — це спосіб CPU сказати «я не розумію цей діалект», але він виражається тим, що перекидає стіл.
Факти та контекст для постмортему
Це короткі, конкретні деталі, які допомагають команді перейти від «таємничого краху» до «зрозумілої моделі відмови».
- SIGILL старший за вашу систему збірки. Unix-сигнали як SIGILL існують десятиліттями; це стандартний механізм ОС для повідомлення про нелегальні opcodes.
- x86-64 вже не «єдине ціле». Сучасні дистрибутиви все частіше розрізняють baseline-версії x86-64 (часто звані x86-64-v1/v2/v3/v4).
- SSE2 фактично став обов’язковим на 64-bit x86. Ось чому бінарники з «x86-64 baseline» зазвичай припускають принаймні SSE2.
- AVX та AVX2 не «безкоштовна швидкість». Вони можуть спричиняти зниження частоти на деяких CPU, тому «збірка з AVX2» може бути як швидшою, так і повільнішою в залежності від завдання.
- Віртуалізація може брехати опусканням можливостей. VM може працювати на хості з AVX2, але представити гостю модель CPU без AVX2 — і тоді бінарники з AVX2 впадуть.
- Контейнери ділять ядро, а не рішення про набір CPU-функцій. Вони бачать той самий CPU, але образ контейнера міг бути зібраний на іншій машині з іншими припущеннями.
- glibc може вибирати оптимізовані шляхи під час виконання. З «hardware capabilities» (hwcaps) libc може завантажувати оптимізовані реалізації залежно від функцій CPU, змінюючи поведінку після оновлень.
- «Працює на моїй машині» часто буквально означає «працює на моєму CPU». Build-бокси зазвичай новіші за production-ноди; ця невідповідність — класична прихована пастка.
- Деякі рантайми постачають кілька кодових шляхів. Інші — ні. Якщо рантайм не має runtime dispatch, ваші розширення або модулі можуть бути тими, хто спричиняє SIGILL.
Прапорці CPU проти бінарників: звідки беруться невідповідності
Три способи отримати SIGILL у реальних розгортаннях
-
Ви зібрали «занадто ново». Бінарник містить інструкції, які не підтримує частина флоту.
Типові винуватці:-march=native, збірка на ноутбуці/станції розробника, або використання оптимізованої збірки від вендора. -
Ви розгорнули на «занадто старому». Оновлення апаратного забезпечення відбуваються шматками. Флот може бути змішаним:
нові ноди з AVX2, старі — без; або нові Intel проти старих AMD; або сімейства інстансів у хмарі з різними наборами можливостей. -
Платформа замаскувала можливості CPU. Гіпервізори, політики live migration або консервативні моделі CPU можуть приховувати можливості.
Ви компілюєте під один набір прапорців, але середовище виконання не відповідає.
Чому Ubuntu 24.04 з’являється в цих інцидентах
Ubuntu 24.04 не «ламає CPU». Воно приносить новіший тулчейн, нові бібліотеки та більш сучасне пакування.
Це важливо, тому що:
- Новий компілятор може генерувати інші auto-vectorization патерни при тому ж рівні оптимізації.
- Новіші бібліотеки можуть постачати більш агресивні оптимізовані варіанти і обирати їх за допомогою hwcaps.
- Ваші пересбірки, викликані оновленням ОС, могли змінити baseline-флаги (наприклад, CI-ранери оновилися і тепер компілюють для новіших CPU).
Що робити з цією інформацією
Ставтеся до рівня ISA як до контракту API. Якщо ви його не вкажете, тулчейн зробить висновок сам. І зробить його, виходячи з машини, на якій збирали,
яка майже ніколи не є найгіршим CPU, на який ви розгортаєте.
Практичні завдання: команди, виводи, рішення (12+)
Це ті завдання, які я фактично виконую, коли сервіс починає падати з SIGILL. Кожне завдання містить команду, реалістичний вивід,
що означає вивід, і яке рішення ухвалити далі.
Завдання 1: Підтвердити SIGILL у journald
cr0x@server:~$ sudo journalctl -u myservice --since "10 min ago" -n 50
Dec 30 10:11:02 node-17 systemd[1]: Started myservice.
Dec 30 10:11:03 node-17 myservice[24891]: Illegal instruction (core dumped)
Dec 30 10:11:03 node-17 systemd[1]: myservice.service: Main process exited, code=killed, status=4/ILL
Dec 30 10:11:03 node-17 systemd[1]: myservice.service: Failed with result 'signal'.
Значення: systemd показує status=4/ILL. Це SIGILL, не segfault.
Рішення: припиніть шукати теорії про корупцію пам’яті; переходьте до workflow невідповідності ISA і зафіксуйте core.
Завдання 2: Перевірити, чи ядро зафіксувало faulting RIP (вказівник інструкції)
cr0x@server:~$ sudo dmesg --ctime | tail -n 20
[Mon Dec 30 10:11:03 2025] myservice[24891]: trap invalid opcode ip:000055d2f2b1c3aa sp:00007ffeefb6f1d0 error:0 in myservice[55d2f2b00000+1f000]
Значення: «invalid opcode» — це спосіб ядра сказати «CPU відхилив інструкцію».
Рішення: користуйтеся IP з addr2line або gdb пізніше; також підтвердіть, який образ бінарника виконується.
Завдання 3: Визначити точний бінарник і його архітектуру
cr0x@server:~$ file /usr/local/bin/myservice
/usr/local/bin/myservice: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=a1b2c3..., for GNU/Linux 3.2.0, not stripped
Значення: Це 64-bit x86-64 ELF. Нічого екзотичного, як образ іншої архітектури.
Рішення: переходьте до порівняння прапорців CPU і перевірок вибору бібліотек.
Завдання 4: Зібрати прапорці CPU з хоста
cr0x@server:~$ lscpu | sed -n '1,25p'
Architecture: x86_64
CPU op-mode(s): 32-bit, 64-bit
Address sizes: 46 bits physical, 48 bits virtual
Byte Order: Little Endian
CPU(s): 32
Vendor ID: GenuineIntel
Model name: Intel(R) Xeon(R) CPU E5-2670 v2 @ 2.50GHz
Flags: fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ss ht syscall nx lm constant_tsc rep_good nopl xtopology cpuid tsc_known_freq pni pclmulqdq ssse3 fma cx16 pcid sse4_1 sse4_2 popcnt aes xsave avx
Значення: Цей CPU має AVX, але не AVX2 (немає прапорця avx2).
Рішення: якщо ваш бінарник використовує AVX2, тут він викличе SIGILL. Далі: перевірити, чи бінарник або бібліотека очікують AVX2.
Завдання 5: Підтвердити прапорці з /proc/cpuinfo (корисно і в контейнерах)
cr0x@server:~$ grep -m1 -oE 'flags\s*:.*' /proc/cpuinfo | cut -c1-180
flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ss ht syscall nx lm constant_tsc rep_good nopl xtopology cpuid pni pclmulqdq ssse3 fma cx16 pcid sse4_1 sse4_2 popcnt aes xsave avx
Значення: Погоджується з lscpu. AVX2 відсутній.
Рішення: трактуйте цей хост як «x86-64 з AVX, але без AVX2» для цілей таргетингу розгортання.
Завдання 6: Перевірити, чи бінарник містить інструкції AVX2 (швидкий скан)
cr0x@server:~$ objdump -d /usr/local/bin/myservice | grep -m1 -E '\bvpmaddubsw\b|\bvpbroadcastd\b|\bvpandd\b'
000000000000f7c0: vpbroadcastd 0x10(%rdi),%ymm0
Значення: Це YMM-інструкція, часто асоційована з AVX2. Не доказ остаточно, але підозріло.
Рішення: підтвердити через gdb на faulting address або перевірити флаги збірки, якщо у вас є контроль над збіркою.
Завдання 7: Використати gdb з core-файлом, щоб підтвердити faulting instruction
cr0x@server:~$ coredumpctl gdb myservice
PID: 24891 (myservice)
UID: 1001 (svc-myservice)
Signal: 4 (ILL)
Timestamp: Mon 2025-12-30 10:11:03 UTC (3min ago)
Command Line: /usr/local/bin/myservice --config /etc/myservice/config.yml
Executable: /usr/local/bin/myservice
Control Group: /system.slice/myservice.service
Unit: myservice.service
Message: Process 24891 (myservice) of user 1001 dumped core.
(gdb) info registers rip
rip 0x55d2f2b1c3aa
(gdb) x/6i $rip
=> 0x55d2f2b1c3aa: vpbroadcastd 0x10(%rdi),%ymm0
0x55d2f2b1c3b0: vpmaddubsw %ymm1,%ymm0,%ymm0
0x55d2f2b1c3b5: vpmaddwd %ymm2,%ymm0,%ymm0
Значення: Крах на vpbroadcastd, AVX2-інструкція. Ваш CPU не має AVX2. Випадок закрито.
Рішення: розгорніть non-AVX2 збірку, додайте runtime dispatch або обмежте планування на ноди з AVX2.
Завдання 8: Дізнатися, які shared libraries завантажує бінарник (і звідки)
cr0x@server:~$ ldd /usr/local/bin/myservice | head -n 20
linux-vdso.so.1 (0x00007ffeefbf9000)
libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f4b5a9d0000)
libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f4b5a8e9000)
libstdc++.so.6 => /usr/lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007f4b5a6d0000)
libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007f4b5a6b0000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f4b5a4a0000)
/lib64/ld-linux-x86-64.so.2 (0x00007f4b5aa0f000)
Значення: Нормальне динамічне лінкування; нічого явно не поза шляхом.
Рішення: якщо сам бінарник не AVX2, але якась бібліотека — так, вам потрібно інспектувати бібліотеки. Інакше зосередьтеся на пересборці додатка.
Завдання 9: Перевірити вибір glibc через hwcaps (діагностика «змінилося після оновлення»)
cr0x@server:~$ LD_DEBUG=libs /usr/local/bin/myservice 2>&1 | head -n 25
24988: find library=libc.so.6 [0]; searching
24988: search path=/lib/x86_64-linux-gnu/glibc-hwcaps/x86-64-v3:/lib/x86_64-linux-gnu/glibc-hwcaps/x86-64-v2:/lib/x86_64-linux-gnu/tls:/lib/x86_64-linux-gnu
24988: trying file=/lib/x86_64-linux-gnu/glibc-hwcaps/x86-64-v3/libc.so.6
24988: trying file=/lib/x86_64-linux-gnu/glibc-hwcaps/x86-64-v2/libc.so.6
24988: trying file=/lib/x86_64-linux-gnu/libc.so.6
Значення: Лоадер шукає hwcaps директорії першими. На деяких системах може завантажитись v3/v2 варіант.
Рішення: якщо SIGILL з’явився після оновлення glibc і ви на старих CPU, переконайтеся, що обрано правильний варіант (або приберіть несумісний оверрайд).
Завдання 10: Перевірити, який варіант libc фактично завантажено
cr0x@server:~$ LD_DEBUG=libs /bin/true 2>&1 | grep -E 'trying file=.*/glibc-hwcaps' | head -n 5
25033: trying file=/lib/x86_64-linux-gnu/glibc-hwcaps/x86-64-v3/libc.so.6
25033: trying file=/lib/x86_64-linux-gnu/glibc-hwcaps/x86-64-v2/libc.so.6
Значення: На цьому хості принаймні присутні директорії; вибір залежить від можливостей CPU.
Рішення: якщо ви дебагуєте контейнер або chroot, підтвердіть libc у цьому filesystem tree, а не хостову.
Завдання 11: Визначити рівень baseline ISA, який підтримується (швидкий эвристичний тест)
cr0x@server:~$ python3 - <<'PY'
import re
flags = open("/proc/cpuinfo").read()
m = re.search(r'^flags\s*:\s*(.*)$', flags, re.M)
f = set(m.group(1).split()) if m else set()
need_v2 = {"sse3","ssse3","sse4_1","sse4_2","popcnt","cx16"}
need_v3 = need_v2 | {"avx","avx2","bmi1","bmi2","fma"}
print("has_v2:", need_v2.issubset(f))
print("has_v3:", need_v3.issubset(f))
print("missing_for_v3:", sorted(list(need_v3 - f))[:20])
PY
has_v2: True
has_v3: False
missing_for_v3: ['avx2', 'bmi1', 'bmi2']
Значення: Цей хост приблизно «v2-ish», але не v3 (відсутні AVX2/BMI).
Рішення: не деплойте x86-64-v3 бінарники сюди; збирайте для v2 або надайте v2 запасний варіант.
Завдання 12: Інспектувати метадані збірки у Go-бінарнику (приклад самодекларації)
cr0x@server:~$ go version -m /usr/local/bin/myservice | head -n 30
/usr/local/bin/myservice: go1.22.2
path example.com/myservice
build -ldflags="-s -w"
build CGO_ENABLED=1
Значення: Це Go-бінарник і CGO увімкнено. Це означає, що нативні C/C++ бібліотеки або розширення могли додати CPU-специфічні інструкції.
Рішення: якщо SIGILL виникає, інспектуйте CGO-зʼєднані бібліотеки або пересоберіть з контрольованими CFLAGS (без -march=native).
Завдання 13: Для C/C++ проектів — доведіть, чи просочився «native» у флаги збірки
cr0x@server:~$ strings /usr/local/bin/myservice | grep -E -- '-march=|-mavx|-mavx2|-msse4\.2' | head
-march=native
-mavx2
Значення: Бінарник ймовірно містить вбудовані флаги компілятора (не завжди, але часто в збірках з метаданими).
Рішення: вважайте збірку непортативною. Пересоберіть з явним baseline: наприклад, -march=x86-64-v2 або консервативним таргетом.
Завдання 14: Переконатися, що контейнер бачить ті ж прапорці CPU (зловити «падає тільки в одному середовищі»)
cr0x@server:~$ docker run --rm ubuntu:24.04 bash -lc "lscpu | grep -E 'Model name|Flags' | head -n 2"
Model name: Intel(R) Xeon(R) CPU E5-2670 v2 @ 2.50GHz
Flags: fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ss ht syscall nx lm constant_tsc rep_good nopl xtopology cpuid pni pclmulqdq ssse3 fma cx16 pcid sse4_1 sse4_2 popcnt aes xsave avx
Значення: Контейнери бачать прапорці хоста (як і очікується).
Рішення: якщо образ контейнера падає з SIGILL на цьому хості, то проблема в бінарниках образу, не в маскуванні CPU контейнером.
Завдання 15: Перевірити модель CPU і видимі можливості у VM (приклад KVM/libvirt)
cr0x@hypervisor:~$ sudo virsh dominfo appvm-03 | sed -n '1,12p'
Id: 7
Name: appvm-03
UUID: 9c6d1c9e-3a9a-4f62-9b61-0a0f3c7a2c11
OS Type: hvm
State: running
CPU(s): 8
CPU time: 18344.1s
cr0x@hypervisor:~$ sudo virsh dumpxml appvm-03 | grep -nE '
58: Haswell-noTSX
59:
60:
Значення: VM явно сконфігурована з відключеним AVX2. Гостьовий бінарник, зібраний під AVX2, отримає SIGILL, навіть якщо хост має AVX2.
Рішення: виправте модель CPU для VM (якщо безпечно) або будуйте/розгорніть non-AVX2 бінарник для цієї VM.
Три міні-історії з корпоративного життя (анонімізовані)
Історія 1: Інцидент через хибне припущення
Компанія мала невеликий on-prem Kubernetes кластер з різними серверами. Деякі були нові з AVX2; деякі — старі, але надійні, залишені через «там ще багато RAM» і ніхто не хотів чіпати стійку.
Команда оновила свій CI runner fleet першими. Це тихо змінило середовище компіляції для latency-sensitive сервісу, написаного на C++. Вони також включили опцію збірки «native», бо побачили покращення в microbench на CI-машині.
Злиття пройшло гладко. Тести пройшли. Сервіс розгорнули.
Потім rollout дійшов до одного зі старих вузлів. Pod перезапускався миттєво і продовжував перезапускатися. Логи показували один рядок: Illegal instruction.
On-call робив стандартні кроки: видаляти pod, реседулити, дренувати вузол. Scheduler продовжував ставити роботу на старі вузли, бо не було обмежень.
Приховане припущення було простим: «Усі x86_64 — приблизно однакові». Це не так. Виправлення було теж простим, але потребувало дисципліни:
пересобрати з явним baseline-target і опублікувати його як дефолтний артефакт. Вони також додали label вузлів для AVX2 і прив’язали AVX2-збірку тільки до промаркованих вузлів. Після цього клас інцидентів зник.
Історія 2: Оптимізація, що обернулась проти
Інша організація використовувала вендорний бінарник для агента обробки даних. Вендор пропонував дві завантаження:
«standard» і «optimized». Хтось обрав «optimized», бо слово звучить як безкоштовні переваги, і агент дійсно був швидшим у staging.
Production була поділена між двома сімействами інстансів у хмарі. Одне сімейство відкривало AVX2; інше — ні (або точніше, певне покоління — ні).
Оптимізований бінарник припускав AVX2. Половина флоту впала при старті з SIGILL, але тільки після того, як rolling upgrade дійшов до старішого сімейства.
Наслідок був ширший, ніж просто крах. Цикли рестарту забили логування. Автоскейл намагався компенсувати.
Черга на вході накопичилася, бо частина агентів померла, а «здорові» не справлялися.
Оптимізація перетворилася на розподіленого підсилювача відмов.
Виправлення було не героїчним. Вони відкотилися на standard build і влаштували «capability-aware» деплой:
окремі node pools за сімейством інстансів і окремий вибір артефактів. Урок був нудний, але дорогий: якщо ви не знаєте baseline CPU вашого флоту, ви не маєте права запускати «optimized» бінарники за замовчуванням.
Історія 3: Нудна, але правильна практика, яка врятувала ситуацію
Третя команда мала політику: кожен сервісний артефакт повинен декларувати baseline CPU у метаданих релізу, а кожен кластер — опублікувати «мінімальний ISA» факт, який scheduler примусово перевіряє. Це не було гламурно. Люди нарікали. Здавалося бюрократією.
Потім Ubuntu 24.04 зайшов у середовище. Нові версії тулчейну підштовхнули performance-oriented збірки до емісії інших векторних інструкцій в «гарячих» шляхах.
Декілька сервісів пересобрали. Один з них впав би на старіших вузлах, якби його розгорнули ширше.
Він не впав. Система розгортання відмовилася планувати його на несумісних вузлах, бо метадані артефакту вказували «requires x86-64-v3».
Rollout пішов тільки на вузли, що відповідали вимозі. Користувачі нічого не помітили. On-call не отримав сторожового виклику. Команда зберегла сон — саме те, що має відбуватися у більшості інженерної роботи.
Постмортем був не подією: тикет на розширення v3 пулу, запис про підтримку v2 запасного варіанту для legacy-пулів і тихе визнання за шлагбауми, які не піддаються компромісам.
Жарт №2: Єдина річ швидша за AVX2-збірку — це AVX2-збірка, яка миттєво падає на CPU з AVX-only.
Контейнери та ВМ: CPU, який ви думаєте, що маєте, проти того, який отримаєте
Історія можливостей CPU відрізняється в залежності від того, чи ви на bare metal, у контейнерах чи у віртуальних машинах.
Виробничі відмови відбуваються, коли команди застосовують невірну ментальну модель.
Контейнери: той самий CPU, різні припущення
Контейнери не емулюють CPU. Якщо ваш контейнер виконує vpbroadcastd на CPU без AVX2, він впаде так само, як процес на хості.
Невідповідність зазвичай походить від середовища збірки:
образи зібрані на новіших раннерах, або multi-stage збірки компілюють з «native» оптимізаціями, бо ніхто не зафіксував CFLAGS.
Чистий підхід: збирайте портативні артефакти, потім опціонально постачайте додаткові «акселеративні» варіанти і обирайте їх під час виконання або через планувальник.
Не відвантажуйте один «optimized» образ і надійтесь, що кластер однорідний. Він ніколи довго таким не буває.
ВМ: моделі CPU, міграція та консервативні дефолти
Гіпервізори часто показують віртуальну модель CPU. Це не просто бренд; це визначає, які розширення ISA доступні в гості.
Деякі середовища навмисно ховають можливості, щоб дозволити live migration по більш широкому набору хостів.
Якщо ви компілюєте всередині VM, яка показує AVX2, а потім деплойте до VM без AVX2, ви отримаєте SIGILL. Якщо компілюєте на bare metal з AVX2 і розгортаєте в VM з консервативною моделлю CPU — також отримаєте SIGILL. Якщо ви компілюєте в однаковому типі VM, але конфігурація гіпервізора різна — отримаєте SIGILL.
Виправлення — управління: визначте моделі CPU для кожного середовища, документуйте їх як сумісні контракти і змушуйте pipeline таргетувати ці контракти.
«Що дає нам хмара» — не контракт для CPU. Це генератор сюрпризів.
glibc hwcaps і чому Ubuntu 24.04 може «раптом» вибрати швидший код
glibc довго підтримує кілька оптимізованих реалізацій функцій через механізми на кшталт IFUNC резолверів.
Останнім часом дистрибутиви активніше використовують hwcaps директорії:
файлові шляхи, що містять оптимізовані збірки бібліотек для конкретних baseline-версій x86-64.
Динамічний лоадер шукає ці директорії першим (як ви бачили у виводі LD_DEBUG=libs).
На CPU, що відповідає критеріям, glibc може завантажити v2/v3 варіант бібліотеки, включивши швидші реалізації.
На CPU, що не відповідає — має відкотитися назад.
Коли операційно це йде не так, часто це пов’язано з однією з таких схем:
- chroot/container образ містить hwcaps варіанти, які не відповідають фактичному CPU, на якому він запускається (наприклад, скопійовані з іншого rootfs).
- Змінна середовища або конфігурація лоадера призводить до неочікуваних шляхів пошуку.
- Стороння бібліотека пакує оптимізований код і робить власну детекцію погано.
Практичний висновок: якщо SIGILL з’явився після оновлення базового образу, не припускайте, що це лише «ваш бінарник».
Це може бути «варіант бібліотеки, який ви тепер завантажуєте». Доведіть це через аналіз core і вивід налагодження лоадера.
Поширені помилки: симптом → корінна причина → виправлення
1) Крах одразу при старті після оновлення
Симптом: сервіс стартує й одразу падає з Illegal instruction (core dumped).
Корінна причина: новонароджена збірка припускає AVX2/SSE4.2/FMA через флаги збірки хоста або рішення нового тулчейну.
Виправлення: пересобрати з явним baseline (-march=x86-64-v2 або консервативний таргет), і примусити CI дотримуватися цього baseline.
2) Лише деякі вузли падають; реседулінг «вирішує» проблему
Симптом: той самий образ контейнера працює на деяких вузлах і падає на інших.
Корінна причина: гетерогенність CPU-функцій у флоті; образ зібрано під «краще» пів.
Виправлення: проставити label вузлів за можливостями; обмежити планування; постачати кілька варіантів образу або портативний baseline з runtime dispatch.
3) Працює на bare metal, падає в VM (або навпаки)
Симптом: бінарник працює на робочій станції, але SIGILL у гостьовій VM.
Корінна причина: модель CPU гостя ховає можливості (AVX2 відключено, консервативний baseline для міграції).
Виправлення: вирівняти модель CPU VM з вимогами, або компілювати під прапорці, які представлені гостю; не компілюйте в середовищі з іншими прапорцями, ніж production.
4) Лише один кодовий шлях падає під навантаженням
Симптом: сервіс працює деякий час, потім SIGILL під час конкретних операцій.
Корінна причина: runtime dispatch/JIT-код або плагін обирає AVX2-шлях умовно; або рідко використовувана функція викликається.
Виправлення: зловіть core при падінні; ідентифікуйте бібліотеку/функцію; вимкніть оптимізований шлях або виправте логіку диспатчу, щоб перевіряти прапорці коректно.
5) «Але хост CPU підтримує AVX2» і все одно падає
Симптом: хтось вказує на специфікацію і наполягає, що сервер підтримує інструкцію.
Корінна причина: мікрокод/налаштування BIOS, маскування у VM, або контейнер запущено на іншому вузлі, ніж припускали.
Виправлення: довіряйте lscpu і core dump, а не даташиту; перевірте прапорці у фактичному середовищі виконання.
6) Сторонній бінарник з інтернету падає на старому залізі
Симптом: вендорський інструмент працює в staging, а в legacy-середовищі падає.
Корінна причина: вендор зібрав під новіший baseline (x86-64-v3) без портативного варіанту.
Виправлення: наполягайте на сумісній збірці; якщо її немає — ізолюйте на сумісні вузли або замініть компонент.
Чеклісти / покроковий план
Чекліст A: Коли вас пейджать через SIGILL (workflow оператора)
-
Підтвердьте SIGILL: перевірте
journalctlі статус systemd. Якщо це неstatus=4/ILL, зупиніться і змініть фокус. -
Зафіксуйте місце краху: шукайте
invalid opcode ip:уdmesg. - Переконайтесь, що core dumps існують: якщо відсутні, тимчасово увімкніть їх і відтворіть у контрольованому середовищі.
-
Відкрийте core: використайте
coredumpctl gdb, дизасемблюйте за RIP, ідентифікуйте інструкцію. - Порівняйте з прапорцями CPU: чи має CPU розширення, потрібне для цієї інструкції?
- Ідентифікуйте, чи це ваш бінарник чи shared library: перевірте стек викликів та завантажені об’єкти.
- Оберіть міграцію: відкотити на портативну збірку, закріпити на сумісні вузли або змінити модель CPU VM.
- Запишіть baseline контракт і примусьте CI/CD його дотримуватись, щоб не повторювати цього наступного тижня.
Чекліст B: Чиста політика збірки та релізу (workflow команди)
- Задекларуйте fleet baseline ISA (для кожного середовища). Приклад: «prod-x86 має підтримувати x86-64-v2; деякі пулі підтримують v3».
-
Збирайте артефакти з явними таргетами; забороніть
-march=nativeу релізних збірках. - Якщо потрібна швидкість, постачайте кілька збірок: baseline + accelerated (v3/v4). Робіть вибір явним (через labels scheduler-у або runtime dispatch).
- Додайте startup self-check: логувати виявлені прапорці CPU і відмовлятись стартувати, якщо вимоги не задоволені (відмовлятися голосно, а не випадково).
- Підтримуйте тестовий хост сумісності (або VM CPU model), що імітує найстаріший підтримуваний CPU. Запускайте там smoke-тести.
- Фіксуйте і версіонуйте тулчейни в CI, щоб зменшити «мовчазний дрейф пересборок».
Чекліст C: Kubernetes capability-aware деплой (практичне)
- Проставте label вузлів за прапорцями CPU (наявність AVX2 чи ні).
- Використовуйте node selectors/affinity для прискорених робочих навантажень.
- Тримайте baseline image за замовчуванням; використовуйте окреме розгортання для accelerated image.
- Моніторьте crash loops і корелюйте з label-ами вузлів, щоб вчасно помічати дрейф.
FAQ
1) Чи завжди «Illegal instruction» означає невідповідність прапорців CPU?
Ні, але у продакшні це домінуюча причина. Є інші причини: пошкоджені бінарники на диску, погана RAM, зламаний JIT-кодген або виконання даних як коду.
Ваше перше завдання — довести faulting instruction через core dump.
2) Чому це почалося після переходу на Ubuntu 24.04?
Бо оновлення змінюють тулчейни і поведінку вибору бібліотек. Навіть якщо ваш вихідний код не змінився, артефакти збірки можуть змінитися.
Також glibc hwcaps і оптимізовані шляхи коду можуть обиратися інакше після апгрейду дистрибутива.
3) Якщо я компілюю з -O2, чи може компілятор все одно емісувати AVX2?
Ні, якщо ваш target цього не дозволяє. Дефолтний таргет залежить від конфігурації компілятора і флагів.
Справжня пастка — коли вбудовані системи збірки додають -march=native або ви збираєте на CPU з ширшими можливостями і потім шипите бінарник кудись інде.
4) Який найчистіший варіант «один артефакт для всіх»?
Збирати для консервативного baseline (часто x86-64-v2 у сучасних флотах, іноді ще старіше залежно від заліза) і використовувати runtime dispatch у гарячому коді.
Ви втрачаєте частковий піковий perf, але отримуєте передбачуваність і простоту експлуатації.
5) Як дізнатися, чи бінарник вимагає AVX2 без того, щоб його запускати і чекати падіння?
Можете дизасемблювати і шукати мнемоніки, пов’язані з AVX2, але найнадійніший метод: запустити в контрольованих умовах, зібрати core на SIGILL і інспектувати faulting instruction на RIP.
6) Чому воно падає тільки під певними запитами?
Деякі бібліотеки використовують runtime dispatch: перевіряють прапорці CPU і обирають швидкий шлях. Якщо ця детекція неправильна, або швидкий шлях — у плагіні, який використовують лише для певних навантажень, ви бачите «випадкові» крахи, прив’язані до конкретних входів.
7) Чи може віртуалізація спричинити це, навіть якщо хост CPU підтримує інструкцію?
Так. Гість бачить віртуальну модель CPU, а не хостову. Гіпервізори можуть вимикати функції (навмисно, для сумісності або міграції).
Завжди перевіряйте прапорці CPU всередині гостя.
8) А як щодо ARM-серверів і «Illegal instruction»?
Те саме повідомлення, інші розширення. На ARM ви будете бачити невідповідності між більш новими ARMv8.x можливостями і старішими ядрами, або використання crypto-розширень, яких немає.
Робочий процес той же: доведіть opcode, порівняйте з можливостями CPU, вирівняйте таргети збірки.
9) Чи не варто стандартизувати флот на AVX2 і забути?
Якщо можете — так, однорідність зменшує кількість відмов. Але стандартизація — це програма, а не побажання. Поки вона не виконана, припускайте гетерогенність і розгортайте відповідно.
10) Яке найкраще довготривале пом’якшення?
Ставте вимоги ISA як обмеження розгортання. Декларуйте їх, тестуйте, примушуйте. «Найкращий» фікс — це той, що запобігає класу інцидентів, а не тимчасово гасять пожежу.
Наступні кроки, які можна виконати цього тижня
- Виберіть baseline ISA для кожного середовища (prod/staging/dev) і задокументуйте це в платформному контракті.
-
Проведіть аудит релізних збірок на предмет
-march=nativeта інших флагів, залежних від хоста. Приберіть їх з production-артефактів. - Додайте CI job «lowest-common-denominator», який виконує тести на VM з моделлю CPU, що імітує найстаріші вузли.
- Для гетерогенних флотів — маркуйте вузли за можливостями і примушуйте scheduler дотримуватись цих правил. Перестаньте дозволяти scheduler-у «шукати» сумісність жорстким шляхом.
- Увімкніть core dumps контрольовано для сервісів, де SIGILL може бути катастрофою; зберігайте цей механізм напоготові для incident response.
- Якщо ви мусите шипити accelerated збірки — робіть це свідомо: baseline + v3/v4, явна логіка вибору і зрозумілі шляхи відкату.
Чисте розгортання — це не «ми ніколи не використовуємо просунуті CPU-функції». Це «ми використовуємо їх навмисно». Ubuntu 24.04 вас не підвів.
Ваш pipeline зробив те, що ви неявно попросили. Тепер зробіть запит явним.