2.4 Життєвий цикл¶
[!NOTE] У Kamailio є три lifecycle-події, що мають значення для оператора: старт, runtime-перезавантаження конфігу і завершення роботи. Кожна з них тонша, ніж здається, і помилитися в будь-якій — це різниця між сервісом, що живе роками без рестарту, і сервісом, що дропає виклики кожного вівторка.
Старт, по порядку¶
Коли ви запускаєте kamailio -f /etc/kamailio/kamailio.cfg, відбувається не «демон стартує». Це чітко впорядкована послідовність, яка має завершитися до того, як прилетить перше SIP-повідомлення:
flowchart TD
A["1 Парсинг<br/>command-line прапорів<br/>(-M, -m, -L, …)"]
B["2 Парсинг kamailio.cfg<br/>побудова in-memory<br/>AST route'ів"]
C["3 Виділення shm-пулу<br/>(mmap, до будь-якого forka)"]
D["4 Завантаження кожного<br/>loadmodule"]
E["5 mod_init() для<br/>кожного модуля"]
F["6 Bind слухачів<br/>(відкриття сокетів)"]
G["7 Fork дітей:<br/>main → воркери,<br/>таймери, helpers"]
H["8 child_init() у<br/>кожному child"]
I["9 receive_msg-цикл<br/>стартує у воркерах"]
A --> B --> C --> D --> E --> F --> G --> H --> I
classDef pre fill:#1f6feb,stroke:#1f6feb,color:#fff
classDef post fill:#238636,stroke:#238636,color:#fff
class A,B,C,D,E,F pre
class G,H,I post
Два кроки в цій послідовності — несучі:
mod_init() виконується один раз, у main-процесі, до форка. Кожен завантажений модуль отримує шанс виділити shm-структури, зареєструвати RPC-команди, поставити таймери й розпарсити власні параметри конфігу. Після цієї точки memory layout усього інстансу зафіксований — кожна shm-алокація, яку модуль колись зробить, уже порахована. Розмір shm-пулу через -m має покрити це плюс runtime-навантаження. Якщо mod_init() зафейлився — зазвичай через замалий shm або зіпсований параметр — увесь старт абортиться, до форка справа не доходить.
child_init() виконується раз на child, після форка. Тут модулі відкривають per-process-ресурси: підключення до БД, file descriptor'и, все, що не можна шарити між процесами. Аргумент — rank child'а. Модулі використовують його, щоб робити роботу рівно в одному child'і (if (rank == PROC_MAIN) { ... }) або рівномірно розподіляти роботу між дітьми.
Порядок важливий через два інваріанти:
- shm виділяється до будь-якого fork'у. Це й робить той самий вказівник валідним у кожному процесі — усі вони наслідують ту саму mapped область з main'а. Виділення shm після fork'у дало б кожному child'у свою копію. Модулі це знають і всю shm-роботу роблять у
mod_init(). - Сокети bind'аться до fork'у. Усі child'и наслідують ті самі listening-сокети й викликають
recvfrom()на них. Kernel балансує вхідні пакети між тими child'ами, що зараз заблоковані в системному виклику. Жодного userspace-диспетчера не треба.
[!TIP] Якщо
kamailioстартує, друкує кілька рядків і тихо виходить — це майже завжди вичерпаний shm під часmod_init(). Підіймайте-mі пробуйте ще раз. Помилка осідає в stderr або syslog, не в основному логу: основний лог у цей момент ще не відкритий.
Runtime-reload — що можна, а що ні¶
Погана новина: ви не можете перезавантажити kamailio.cfg, поки Kamailio працює. Routing-скрипт парситься раз на старті, компілюється в AST, і саме цей AST виконує кожен воркер. Змінити cfg-файл на диску й послати сигнал — нічого корисного не дасть. Щоб підхопити зміни конфігу, потрібен рестарт.
Хороша новина: багато з того, що виглядає як «конфіг», насправді ним не є. Є три runtime-mutable осі:
Параметри модулів (деякі з них). Багато параметрів модулів оголошені як MODULE_PARAM_USE_FUNC або читаються на кожен виклик функції — їх можна міняти через RPC:
Сімейство cfg. — cfg.set_now_int, cfg.set_now_str, cfg.commit, cfg.rollback — експонує runtime-mutable-підмножину. Чи входить конкретний параметр у цю підмножину, вирішує автор модуля. kamcmd cfg.help <module> дає список кандидатів.
Module-managed таблиці. Список шлюзів у dispatcher, правила в permissions, переписування номерів у dialplan, записи в htable, контакти в usrloc — усе це живе у shm і має RPC-команди для перезавантаження з БД або прямої модифікації:
kamcmd dispatcher.reload # перечитати таблицю dispatcher'а з БД
kamcmd htable.dump my_table # подивитися вміст htable
kamcmd permissions.addressReload # перезавантажити permissions
Це й є операційна «hot-reload»-поверхня — більшість продакшн-змін відбуваються тут, а не в kamailio.cfg.
Рівень логування. kamcmd log.level <N> міняє runtime log level глобально. Значення може бути від'ємним (тихіше) або великим (debug-шум). Корисно, щоб упіймати минущу проблему без рестарту.
Що не можна змінити без рестарту:
- Кількість UDP/TCP-воркерів.
- Розміри shm або pkg.
- Набір завантажених модулів.
- Routing-скрипт (request_route, branch_route і т. д.).
- Слухачі — додати нову IP-адресу чи порт.
Усі ці зміни потребують повного рестарту, що означає дроп in-flight UDP-транзакцій і TCP-з'єднань.
Graceful shutdown¶
Послати SIGTERM main-процесу. Що далі:
- Main розповсюджує
SIGTERMкожному child'у. - Кожен воркер виходить з
recvfrom-циклу — але тільки після того, як завершив поточне повідомлення. Воркер, що зараз посеред route'у, тримає лок або чекає DB-запит, доведе цей route до кінця, і лише потім вийде. - Функція
destroy()кожного модуля виконується у main-процесі. Тут модулі флушать in-memory стан у БД (найважливіше —usrloc, який синхронізує in-memory cache контактів у таблицюlocation, щоб реєстрації пережили рестарт) і звільняють shm. - Main-процес виходить.
Уся послідовність типово займає 1–3 секунди на тихому боксі — довше, якщо є route, заблокований на повільному виклику (HTTP, БД). Тайм-аут shutdown'у обмежений tcp_wait_data і db_*-тайм-аутами того модуля, що зволікає.
[!WARNING]
SIGKILL(kill -9) — деструктивний. Він оминає destroy-хуки. Контакти в in-memoryusrloc, ще не зафлешені в БД, втрачаються — кожен зареєстрований користувач фактично має заново REGISTER'итися після рестарту. У продакшні використовуйте лише як крайній випадок, а для нормальних рестартів —SIGTERM.
SIGINT (Ctrl-C у foreground) поводиться як SIGTERM і тригерить той самий graceful-шлях. SIGUSR1 історично дампив стан пам'яті, але сучасно — kamcmd core.shmmem.
Crash-відновлення (неформально)¶
Що відбувається, коли один воркер падає — segfault, abort, OOM-killed:
- Kernel шле
SIGCHLDmain'у з exit-статусом дитини. - Сигнал-хендлер main'а логує смерть (
child process N exited normally|by signal). - Для воркерів, що входять у core-флот обробки повідомлень (UDP, TCP, timer), main пере-форкає заміну. Новий воркер виконує
child_init()і входить у пул. - Для модульних helper-воркерів (наприклад, dialog'овий keepalive-сендер) поведінка залежить від модуля — багато з них не пере-спавняться, і відсутній helper лишається відсутнім до рестарту.
Саме це й робить Kamailio «відчутно стійким»: одне погане повідомлення, що segfault'нуло один воркер, не кладе сервіс. Інші N-1 воркерів продовжують обробляти, мертвий — замінюється, у лог щось пишеться, а ви про це дізнаєтесь на on-call-рев'ю в понеділок.
[!IMPORTANT] Повторюваний crash воркера (раз на кілька секунд) — це найгірший вид збою, бо сервіс «начебто живий»: main-процес живий, listener bind'нутий, телеметрія каже, що трафік іде — але кожне N-те повідомлення дропається, тому що воркер, який його підхопив, помер до того, як зафорвардити. Алертіть на rate
SIGCHLD, а не лише на aliveness процесу.
Наступна частина посібника виходить з рантайму у життєвий цикл SIP-повідомлення — що відбувається між тим, як пакет прилетів на сокет, і викликом функції в routing-скрипті.
← Зміст · ← 2.3 Примітиви конкурентності · Далі: 2.5 Sizing & tuning →