5.3 Lifecycle KEMI — коли і що бачить¶
[!IMPORTANT] Кожен воркер Kamailio має свій інстанс інтерпретатора. Вони не діляться станом через інтерпретатор. Вони діляться станом через shm Kamailio — рівно так, як cfg-only-розгортання. Забути про це і тягтися до Lua-global'а, щоб «запам'ятати» щось між викликами — найпоширеніший KEMI-баг у продакшні.
Де живе інтерпретатор¶
KEMI-інтеграція слідує тій самій per-process-дисципліні, що й усе інше в Kamailio (див. розділ 2.1):
flowchart TB
Main[main-процес]
Main --> W1["Worker 1<br/>Lua state #1<br/>script.lua завантажено"]
Main --> W2["Worker 2<br/>Lua state #2<br/>script.lua завантажено"]
Main --> Wn["Worker N<br/>Lua state #N<br/>script.lua завантажено"]
Main --> Mod["Модульні helper'и<br/>(може мати/не мати<br/>власні інтерпретатори)"]
SHM[("Kamailio shm<br/>htable · $shv · tm · usrloc")]
W1 -.-> SHM
W2 -.-> SHM
Wn -.-> SHM
classDef main fill:#1f6feb,stroke:#1f6feb,color:#fff
classDef worker fill:#238636,stroke:#238636,color:#fff
classDef shm fill:#bf8700,stroke:#bf8700,color:#fff
class Main main
class W1,W2,Wn worker
class Mod worker
class SHM shm
Кожен воркер форкається з main з власною копією C-level-простору адрес, потім у child_init() language-модуль bootstrap'ить свіжий інтерпретатор всередині цього воркера. Script-файл завантажується в цей інтерпретатор. Інтерпретатор живе весь час життя воркера — він не пере-будується per message.
Наслідки:
- Lua-global, виставлений у воркері 3, невидимий воркеру 7. У них буквально різні Lua state'и. Прямого способу шарити Lua-level-стан між воркерами не існує.
- Усе, чим треба ділитися, має бути у shm. Це означає htable, $shv чи БД. Те саме правило, що й для cfg.
- Інтерпретатор переживає повідомлення в межах воркера. local cache = {}, оголошений на верхньому рівні скрипта, тримає стан між викликами — але лише для повідомлень, які підхопив цей самий воркер. Worker affinity — це фікція (див. розділ 2.1), тож покладатися на те, що сусідні повідомлення попадуть в той самий інтерпретатор, не можна.
Послідовність старту¶
Коли Kamailio стартує з налаштованим KEMI, порядок — той самий з розділу 2.4, з вставленими хуками language-модуля:
- Парсинг cfg — cfg-файл згадує
loadmodule "app_lua.so"іmodparam("app_lua", "load", "/etc/kamailio/kamailio.lua"). - Виділення shm, завантаження модулів.
mod_init()вapp_lua— ініціалізує власний стан language-модуля. Не створює per-worker-інтерпретатори.- Bind слухачів.
- Fork воркерів.
child_init()вapp_lua— біжить всередині кожного воркера. Тут створюється власне інтерпретатор (luaL_newstate(),Py_InitializeEx()тощо), зареєстровані glue-функції binду'ються в global namespace, script-файл читається і виконується. Після цього всі script-level-globals (функції, top-level local'и) — в інтерпретаторі.
Якщо скрипт має синтаксичну помилку чи не завантажується, воркер виходить із startup-помилкою. Main-процес логує, який воркер не зміг init'нутися, і не пере-форкає його — поломаний скрипт це не recoverable runtime-failure. Рестарт після фіксу.
[!TIP] Більшість language-модулів дзвонять спеціальну top-level-функцію під час
child_init, якщо вона визначена:ksr_mod_init()у Lua,mod_init()у Python тощо. Це місце для налаштування per-interpreter-кешів, парсингу конфіг-файлів, які читає скрипт, відкриття file-handle'ів. Бігає раз на воркера, після завантаження globals'ів.
Що відбувається на повідомлення¶
Коли прилітає запит і request_route в cfg диспетчеризує до скрипта:
- KEMI-диспетчер у воркері шукає per-worker-handle інтерпретатора (маленька C-struct).
- Поточний
sip_msg*ставиться у «context for this call» інтерпретатора — доступний зі скрипта черезKSR.*-namespace. - Іменована функція (
ksr_request_route) викликається всередині інтерпретатора. - Працюючи, скрипт може дзвонити будь-яку
KSR.*-функцію, інші Lua/Python-функції, читати й писатиsip_msgчерез псевдо-змінні, ставити lumps у чергу, диспетчеризувати назад у cfg sub-route'и. - Коли функція повертається, інтерпретатор лишається живим — лише per-call-context чиститься.
Кілька неочевидних деталей:
- Інтерпретатор однопоточний у своєму воркері. Це нормально, бо воркер — теж однопоточний. Жодної GIL-драми, жодного локінгу всередині скрипта.
- Script-level-стан виживає.
local count = 0; function ksr_request_route() count = count + 1; ... endбуде інкрементитиcountна кожне повідомлення, яке обробив цей воркер. Корисно як per-worker-counter, безглуздо як global-counter. - Пам'ять інтерпретатора йде з libc-malloc, не pkg Kamailio. Language-модуль вбудовує інтерпретатор зі стандартним аллокатором інтерпретатора. Ця пам'ять обмежена GC інтерпретатора; не звільняється з кінцем повідомлення.
- Lump queue працює рівно так само. Виклики
KSR.hdr.append(...)ставлять lump у чергу на C-side-sip_msg. Lump applier'у байдуже, звідки lump прилетів — з cfg чи зі скрипта.
Стан між повідомленнями — правильні та неправильні способи¶
Три патерни для шарінгу даних між повідомленнями в KEMI-скрипті:
| Патерн | Lifetime | Scope | Коли |
|---|---|---|---|
| Локальна змінна всередині функції | Один виклик | Один стек-фрейм | Per-message |
| Top-level скрипт-змінна | Lifetime воркера | Один інтерпретатор | Per-worker-кеші, статистика; ніколи для стану, що має бути узгодженим між воркерами |
KSR.htable.sht_get(...) / sht_set(...) |
До рестарту чи expiry | Усі воркери в інстансі | Кросворкерний стан — auth-кеші, rate-limiter'и, per-call-рішення |
$shv(...) через KSR.pv.sets("$shv(x)", ...) |
До рестарту | Усі воркери | Маленькі іменовані globals'и, менш гнучкі за htable |
| Database | Назавжди | Усі воркери, усі інстанси | Persistent-стан |
Wrong-way-патерн, який кусає: думати, що Lua-global спільний. Він — ні. Два послідовні REGISTER'и того самого юзера швидше за все потраплять у різні воркери, точно матимуть різні interpreter-state'и, і будь-який Lua-side-кеш дасть одну відповідь у воркері 3 і іншу — у воркері 7.
Reload скрипта — як оновити без рестарту¶
KEMI-скрипти можна перезавантажити у runtime, не рестартуючи Kamailio. Механізм залежить від language-модуля, але експонується уніформно через RPC:
Що відбувається: кожен воркер на наступному повідомленні викидає поточний interpreter-state і пере-bootstrap'ує інтерпретатор із (тепер пере-читаного) script-файлу. Globals'и пере-ініціалізуються, per-worker-кеші втрачаються. In-flight-транзакції у tm не зачеплені — вони живуть у shm і не залежать від interpreter-state'у.
[!WARNING] Reload не транзакційний між воркерами. Воркери пере-bootstrap'ляться незалежно, у міру того, як кожен підбирає наступне повідомлення. Якщо воркер 3 reload'нувся і одразу обробив
INVITE, а воркер 7 ще не reload'нувся — обидва бігтимуть різні версії вашого скрипта кілька секунд rollover'у. Для змін, що мають бути атомарними по всіх воркерах — рестартуйте.
Failure-режими, які бувають тільки в продакшні¶
Короткий каталог того, що дивує:
- Lua/Python-exception всередині скрипта ловиться language-модулем і логується, далі функція скрипта по суті повертає «нічого не робити». Повідомлення може тихо дропнутися, бо неочікуваний exception стрельнув на третьому рядку handler'а.
- Нескінченний loop у скрипті паркує один воркер назавжди (його непросто перервати
SIGTERM'ом). Одне погане повідомлення, що тригерить нескінченний loop, вбиває 1/N throughput'у до рестарту. - Memory leak у скрипті накопичується з часом. Lua-GC автоматичний, але cyclic C-referenced-об'єкти можуть leak'ати; Python-refcount обробляє більшість випадків, але не всі. Стежте за зростанням RSS воркера у вимірі днів, не хвилин.
- Typo в імені
KSR.*-функції — не syntax error, а runtime-nil-dispatch. Lua тихо нічого не робить наnil:call(); Python кидає, і ви бачите в логу; JS залежить від інтерпретатора.
Наступний розділ дивиться на те, коли вся ця будова — вбудований інтерпретатор, bridge, per-worker-стан — варта своєї ціни, а коли native cfg-шлях просто швидший.