Спостережуваність Shared*
Кожен екземпляр OxPHP\Shared\* — це запис реєстру, для якого середовище виконання вже відстежує лічильник посилань і місткість. Ці дані відкрито операторам як JSON-інтроспекцію за адресами /__ox_shared/* і як метрики Prometheus із префіксом oxphp_shared_*. Ця сторінка — водночас довідник і практичний посібник.
Увімкнення
Спостережуваність працює поверх внутрішнього сервера. Щоб його запустити, задайте INTERNAL_ADDR:
INTERNAL_ADDR=127.0.0.1:9090Після цього і JSON-ендпоінти, і /metrics доступні за цією адресою. Жодних інших налаштувань не потрібно, достатньо не вимикати SHARED_ENABLED.
Кожну з поверхонь можна вимкнути окремо:
| Змінна середовища | Типово | Дія |
|---|---|---|
SHARED_INTROSPECTION_ENABLED |
true |
Вмикає або вимикає JSON API /__ox_shared/*. |
SHARED_INTROSPECTION_PREVIEW_ENABLED |
true |
Вмикає або вимикає /preview (попередній перегляд значень може розкрити дані). |
SHARED_METRICS_ENABLED |
true |
Вмикає або вимикає метрики Prometheus oxphp_shared_*. |
У розгортаннях із недовіреними орендарями вимикайте інтроспекцію; метрики лише агреговані, тож їх можна безпечно залишити.
SHARED_ENABLED=false також прибирає обидві поверхні разом з усією підсистемою. Перемикачі вище дають змогу залишити примітиви доступними, приховавши те, що вони зберігають.
Ендпоінти інтроспекції
Усі відповіді мають Content-Type: application/json; charset=utf-8. Параметри запиту кодуються стандартно, як у URL.
GET /__ox_shared/summary
Загальний знімок стану: сумарні кількості за типами, пам'ять, частота операцій і насичення відносно налаштованих лімітів.
{
"total_entries": 127,
"total_bytes": 2_481_664,
"by_type": {
"Counter": { "count": 48, "bytes": 3_072, "ops": 1_402_391 },
"Map": { "count": 12, "bytes": 1_638_400, "ops": 48_201 },
"Pool": { "count": 4, "bytes": 16_384, "ops": 67_014 }
},
"limits": { "max_entries": 100_000, "max_bytes": 1073741824 },
"saturation": { "entries": 0.00127, "bytes": 0.00231 },
"diagnostics": {
"lock_diagnostics_level": "warn",
"cycle_detect_depth": 16
}
}Використовуйте summary на дашбордах і в сповіщеннях за розкладом. Один запит дає стан кожного типу та запас місткості.
GET /__ox_shared/entries?limit=N
Перелічує живі записи (не більше limit, типово 100, максимум 500). Один рядок на запис:
{
"items": [
{ "id": 42, "type": "Map", "refcount": 2, "ops": 1820, "mem_bytes": 204_800, "age_sec": 612 },
{ "id": 43, "type": "Counter", "refcount": 3, "ops": 48_014, "mem_bytes": 64, "age_sec": 612 }
],
"next_cursor": null,
"total_matching": 127
}refcount — це зовнішній лічильник утримань: скільки PHP-обгорток і вкладених записів Shared тримають цей запис. Якщо ви очікуєте, що запис має бути прибраний збирачем сміття, а цього не стається, перевіряйте саме це поле.
GET /__ox_shared/entry?id=N
Подробиці про один запис, специфічні для його типу:
{
"id": 42,
"type": "Map",
"refcount": 2,
"ops": 1820,
"mem_bytes": 204_800,
"age_sec": 612,
"type_specific": {
"key_count": 1_240,
"max_entries": 50_000,
"saturation": 0.0248,
"sample_keys": ["tenant:acme", "tenant:beta", "..."]
}
}Вміст type_specific залежить від типу: Pool показує { max_size, count, in_use, idle, waiting, idle_by_thread, rebalance_strategy }, Channel — { capacity, count, closed, senders_blocked, receivers_blocked }, Counter — { value } і так далі.
GET /__ox_shared/preview?id=N
Попередній перегляд скалярних значень і невеликих масивів. Рядки обрізаються до SHARED_PREVIEW_STRING_LIMIT (типово 256 байтів) з округленням донизу до найближчої межі символу, тож багатобайтовий текст ніколи не розрізається посеред символу; для масивів показано перші SHARED_PREVIEW_ARRAY_LIMIT елементів (типово 20). Керується прапорцем SHARED_INTROSPECTION_PREVIEW_ENABLED.
{ "id": 42, "type": "Counter", "preview": "1420" }Користуйтеся preview під час розробки; у продакшені вимикайте його, якщо значення можуть містити дані користувачів.
GET /__ox_shared/types
Перелічує каталог типів v1; це зручно для згенерованих інструментів, яким потрібна відповідність тег → клас:
{
"types": [
{ "tag": 10, "name": "Counter", "php_class": "OxPHP\\Shared\\Counter" },
{ "tag": 11, "name": "Flag", "php_class": "OxPHP\\Shared\\Flag" },
{ "tag": 12, "name": "Once", "php_class": "OxPHP\\Shared\\Once" },
{ "tag": 13, "name": "Atomic", "php_class": "OxPHP\\Shared\\Atomic" },
{ "tag": 20, "name": "Map", "php_class": "OxPHP\\Shared\\Map" },
{ "tag": 31, "name": "Channel", "php_class": "OxPHP\\Shared\\Channel" },
{ "tag": 50, "name": "Pool", "php_class": "OxPHP\\Shared\\Pool" }
]
}Відповідь не містить Shared\Mutex.
GET /__ox_shared/graph?id=N[&depth=D][&edges=E]
Обхід у ширину вихідних посилань Shareable, починаючи з id=N. Повертає вузли й ребра досяжного підграфа. Типові значення: depth=16, edges=500. Якщо обхід вичерпує свій бюджет, у відповіді з'являється truncated: true.
{
"root": 42,
"nodes": [
{ "id": 42, "type": "Map", "refcount": 2, "mem_bytes": 204_800 },
{ "id": 51, "type": "Counter", "refcount": 1, "mem_bytes": 64 }
],
"edges": [
{ "from": 42, "to": 51, "key": "hits" }
],
"truncated": false
}Звертайтеся до graph після CycleException, щоб побачити досяжний шлях, яким пройшов обхід, або коли з'ясовуєте, «чому цей Counter не прибирається збирачем сміття»: граф показує кожного батька, що утримує на нього посилання.
Метрики Prometheus
Усі метрики доступні за GET /metrics поряд з основними метриками сервера.
Для всього реєстру
| Метрика | Тип | Мітки | Опис |
|---|---|---|---|
oxphp_shared_objects_total |
gauge | type |
Кількість живих записів кожного типу. |
oxphp_shared_operations_total |
counter | type |
Сумарна кількість операцій, надісланих кожному типу. |
oxphp_shared_bytes |
gauge | type |
Приблизна кількість байтів на тип (±30% порівняно з mallinfo). |
oxphp_shared_total_bytes |
gauge | — | Сума за всіма типами. |
oxphp_shared_capacity_saturation |
gauge | kind |
entries і bytes як частки від їхніх лімітів. |
oxphp_shared_deadlock_detected_total |
counter | — | Виявлені міжпотокові цикли очікування. |
Channel
| Метрика | Тип | Мітки |
|---|---|---|
oxphp_shared_channel_count |
gauge | channel_id |
oxphp_shared_channel_senders_blocked |
gauge | channel_id |
oxphp_shared_channel_receivers_blocked |
gauge | channel_id |
oxphp_shared_channel_items_sent_total |
counter | channel_id |
oxphp_shared_channel_items_dropped_total |
counter | channel_id |
Map
| Метрика | Тип | Мітки |
|---|---|---|
oxphp_shared_map_entries |
gauge | map_id |
oxphp_shared_map_max_entries |
gauge | map_id |
oxphp_shared_map_saturation |
gauge | map_id |
Pool
| Метрика | Тип | Мітки |
|---|---|---|
oxphp_shared_pool_count |
gauge | pool_id |
oxphp_shared_pool_in_use |
gauge | pool_id |
oxphp_shared_pool_idle |
gauge | pool_id |
oxphp_shared_pool_waiting |
gauge | pool_id |
oxphp_shared_pool_acquire_total |
counter | pool_id |
oxphp_shared_pool_evicted_total |
counter | pool_id, reason |
oxphp_shared_pool_wait_seconds |
histogram | pool_id |
Мітки oxphp_shared_pool_evicted_total: reason=idle_timeout | evict | shutdown. idle_timeout — автоматичне витіснення простійного слота, evict — явний виклик Pool::evict(), а shutdown — звільнення ресурсів під час завершення процесу.
Counter / Flag / Once / Mutex
Для окремих лічильників, прапорців, once-об'єктів і м'ютексів окремі ряди метрик не публікуються: вони роздули б кардинальність міток. Для інспекції окремих екземплярів натомість користуйтеся загальним лічильником oxphp_shared_operations_total{type=...} і JSON із /__ox_shared/entry?id=….
Це заплановано як подальша робота; сьогодні стан видно через /__ox_shared/entry.
Детектор взаємоблокувань
Фонове завдання шукає міжпотокові цикли очікування на Shared\Mutex: два чи більше потоків, кожен з яких чекає на м'ютекс, що його тримає інший із них. Кожні SHARED_LOCK_POLL_INTERVAL_MS (типово 100) воно сканує граф очікувань. SHARED_LOCK_DIAGNOSTICS визначає, що відбувається, коли сканування знаходить цикл:
| Значення | Детектор | Коли знайдено цикл |
|---|---|---|
off |
Не запускається. SHARED_LOCK_POLL_INTERVAL_MS не використовується, а oxphp_shared_deadlock_detected_total лишається 0. |
Нічого. withLockTimeout() у циклі здається з OperationTimeoutException, коли спливає його тайм-аут. Цикл, що складається лише з викликів withLock(), не має власного тайм-ауту, який би його завершив: кожен його запит чекає, доки його не буде перервано — через max_execution_time, якщо він заданий, — а блокування, які тримав перерваний запит, звільняються, коли він завершується (див. Shared\Mutex). |
warn (типово в релізних збірках) |
Запускається. | Записує в журнал і рахує (див. нижче); очікування лишаються як є, з тим самим результатом, що й за off. |
strict (типово в налагоджувальних збірках бінарника) |
Запускається. | Записує в журнал і рахує, а також сигналізує потокам у циклі: withLock() у ньому кидає DeadlockException протягом 100 мс після сканування, а withLockTimeout() вичікує свій тайм-аут і потім кидає DeadlockException замість OperationTimeoutException. Очікувач, чиє блокування звільнилося раніше (бо виняток іншого очікувача розгорнув замикання, яке його тримало), захоплює блокування й винятку не кидає. |
Значення розпізнається лише в нижньому регістрі. Будь-яке інше значення, зокрема Strict чи trace, мовчки дає типове.
Кожен цикл, знайдений скануванням, породжує щонайменше один запис WARN з event=shared.deadlock_detected; кожен такий запис називає потік у циклі (start_thread, повторений як holder_thread), але не називає м'ютекси. oxphp_shared_deadlock_detected_total зростає з кожним записом.
Повторний вхід у тому самому потоці — окрема перевірка, яка не залежить від цього налаштування: повторний вхід у м'ютекс у потоці, що вже його тримає, кидає DeadlockException у точці виклику за будь-якого значення, зокрема off, і ніколи не перетворюється на цикл.
Діагностичні сценарії
Пул насичено (429, а повторні спроби не вдаються)
Симптоми: HTTP-клієнти отримують тайм-аути, oxphp_shared_pool_waiting зростає, oxphp_shared_pool_count застиг на maxSize.
Перевірка:
curl -s http://localhost:9090/__ox_shared/entry?id=<pool_id> | jq .type_specificПодивіться на idle_by_thread. Якщо там {} або сильний перекіс (у воркера 0 вісім простійних, у воркера 3 — жодного), захоплення конкурує за потоки, які саме зайняті деінде. Прив'язка до потоків у v1 не перерозподіляє ресурси. Або збільште maxSize, або зменште навантаження на захоплення в межах одного потоку.
Якщо idle_by_thread збалансований, але все перебуває в in_use, збільште maxSize.
Насичення пам'яті
Перевірте oxphp_shared_total_bytes і oxphp_shared_capacity_saturation{kind="bytes"}. Коли використання зростає, сервер нічого не скидає й не погіршує. Його перша реакція — відмовити у створенні нового об'єкта Shared, щойно досягнуто SHARED_MAX_BYTES або SHARED_MAX_ENTRIES; уже наявні об'єкти продовжують рости без повторної перевірки цього ліміту (Map без обмежень, якщо його не створено з maxEntries), тож показник bytes може перевищувати 1.0. Для раннього попередження додайте правило сповіщення, наприклад oxphp_shared_capacity_saturation > 0.7. Якщо будь-який із показників високий:
- Виконайте
curl /__ox_shared/entries?limit=500і відсортуйте заmem_bytes, щоб знайти найбільших споживачів. - Виконайте
curl /__ox_shared/entry?id=<N>для кожного, щоб перевірити їхню будову. Для Map порівняйтеkey_countізmax_entries. - Найпоширеніша причина — необмежений
Shared\Mapіз ключами з користувацького введення. Рішення — лімітmaxEntriesі політика зберігання.
Обгортку не прибирає збирач сміття
refcount у /__ox_shared/entries показує, скільки утримань лишається. Якщо після виходу PHP-обгортки з області видимості він лишається більшим за 1, запис живим тримає інший запис Shared.
curl -s http://localhost:9090/__ox_shared/graph?id=<N> | jq .nodesПройдіть граф у зворотному напрямку. Кожен вузол, з якого досяжний завислий запис, утримує посилання на нього. Приберіть посилання ($map->remove($key), закрийте канал, видаліть запис Mutex) — і лічильник посилань зменшиться.
У продакшені спрацював CycleException
Повідомлення винятку містить досяжний шлях, який дослідив детектор циклів. Зіставте ці ідентифікатори з типами через /__ox_shared/entries і запитайте повну будову в /__ox_shared/graph?id=<root>:
# Exception message: "cycle would form: #42 → #51 → #42"
curl -s http://localhost:9090/__ox_shared/graph?id=42 | jqРезультат наочно показує ланцюжок, тож видно, де з'явилося ненавмисне зворотне посилання.
Спрацював детектор взаємоблокувань
oxphp_shared_deadlock_detected_total зростає: детектор взаємоблокувань побачив два чи більше потоків, кожен з яких чекає на Shared\Mutex, що його тримає інший із них. Шукайте в журналах сервера event=shared.deadlock_detected; записи називають потоки, а не м'ютекси. Як виправити:
- Знайдіть ділянки коду, що захоплюють два м'ютекси один усередині іншого, і зробіть так, щоб усюди вони захоплювалися в однаковому порядку, або об'єднайте пару в один м'ютекс.
- За
SHARED_LOCK_DIAGNOSTICS=warn, типового значення в релізних збірках, цикл лише записується в журнал і рахується. ЗадайтеSHARED_LOCK_DIAGNOSTICS=strictна стейджингу, щоб детектор натомість розривав цикл; що тоді робить кожен виклик в очікуванні, див. у розділі Детектор взаємоблокувань.
Тривалий стрес-тест на витривалість
tests/soak/pool_soak.sh — ручний (не для CI) інструмент для перевірки стабільності Shared\Pool протягом годин або днів безперервного навантаження. Він:
- Запускає dev-образ із динамічним масштабуванням воркерів (типово
PHP_WORKERS=4:40) і короткимidleTimeoutпулу, щоб планувальник витіснення спрацьовував безперервно. - Завантажує
tests/soak/workload.phpяк bootstrap воркера; той створює 10 пулів ×maxSize=8і на кожному запиті виконує захоплення й повернення. - Подає трафік через
wrkпротягомSOAK_DURATION_MINхвилин (типово 1440 = 24 год). - Кожні 60 с збирає
/metricsі RSS контейнера уtests/soak/out/<timestamp>/metrics.csv. - Наприкінці записує
verify.txtз результатом «пройдено/не пройдено» для п'яти критеріїв готовності до випуску (дрейф RSS у межах ±5%, жодної паніки через застарілі дескриптори, жодного запису, що витік, на момент завершення, рівномірне зростання витіснень заidleTimeout, жодного спрацювання детектора взаємоблокувань).
Що потрібно на хості: docker, wrk, curl, awk.
Типові запуски:
# 24h full soak before a release
tests/soak/pool_soak.sh
# 1h smoke for validating the harness itself
SOAK_DURATION_MIN=60 tests/soak/pool_soak.sh
# Heavier concurrency
SOAK_CONCURRENCY=400 SOAK_THREADS=8 tests/soak/pool_soak.shРезультати зберігаються в tests/soak/out/<timestamp>/:
metrics.csv— один рядок на хвилину (unix ts, RSS, кількість записів за типами, лічильники витіснень для кожного пулу, кількість взаємоблокувань, операції).server.log— stdout/stderr контейнера, зокрема сліди застарілих дескрипторів або панік.wrk.out/wrk.err— сирий вивід генератора навантаження.metrics.final— останній знімок/metrics, зроблений безпосередньо перед зупинкою контейнера. Потрібен, щоб підтвердити, що після припинення навантаження кількість записів і байтів повернулася до базового рівня (жоден запис Shared не витік).verify.txt— звіт «пройдено/не пройдено» за п'ятьма критеріями готовності.
Не підключайте це до CI. 24-годинний прогін недешевий, і його мета — впевненість перед випуском, а не безперервна перевірка.
Частота збирання метрик
Ендпоінти реєстру обходять живий стан під блокуваннями на читання, тож збирання дешеве, але не безкоштовне. Рекомендована частота:
/metrics— кожні 15 с (типове значення Prometheus). Лише агреговані дані; накладні витрати мізерні./__ox_shared/summary— кожні 60 с для дашбордів. Трохи важчий за/metrics./__ox_shared/entries— лише на вимогу. Обходить усі шарди; не збирайте його на кожному такті./__ox_shared/entry//preview//graph— на вимогу під час розслідувань.
Промпти для вашого агента
Ці промпти розраховані на агента з підключеним MCP-сервером документації OxPHP — потрібні сторінки він прочитає сам.
Побудуй дашборд Prometheus і правила сповіщень для примітивів Shared\*, які справді використовує мій код. Прочитай сторінку `shared-state/shared-observability` через MCP-сервер документації OxPHP.
Знайди кожен `Shared\Map`, `Shared\Pool`, `Shared\Channel` та інші примітиви, які створює цей репозиторій, і підготуй панелі та пороги сповіщень на основі їхніх задокументованих метрик — насичення, очікування та втрат.Пов'язані сторінки
- Спільний стан — ментальна модель і огляд примітивів.
- Метрики Prometheus — основні метрики сервера на тому самому ендпоінті
/metrics. - Внутрішній сервер — як ендпоінти
/__ox_shared/*підключаються доINTERNAL_ADDR. - Перехід на зовнішнє сховище — коли насичення структурне, а не вирішується налаштуванням.