Obserwowalność Shared*
Każda instancja OxPHP\Shared\* to wpis w rejestrze, który runtime i tak już śledzi pod kątem refcount i pojemności. To śledzenie jest udostępniane operatorom jako introspekcja JSON pod /__ox_shared/* oraz jako metryki Prometheus pod oxphp_shared_*. Ta strona jest zarazem materiałem referencyjnym i praktycznym przewodnikiem.
Włączanie
Obserwowalność korzysta z serwera wewnętrznego. Ustaw INTERNAL_ADDR, aby ją uruchomić:
INTERNAL_ADDR=127.0.0.1:9090Zarówno endpointy JSON, jak i /metrics są wtedy dostępne pod tym adresem. Nie jest wymagana żadna dodatkowa konfiguracja.
Każdą z nich można wyłączyć niezależnie:
| Zmienna środowiskowa | Domyślnie | Efekt |
|---|---|---|
SHARED_INTROSPECTION_ENABLED |
true |
Przełącza API JSON /__ox_shared/*. |
SHARED_INTROSPECTION_PREVIEW_ENABLED |
true |
Przełącza /preview (podglądy kształtu wartości mogą ujawniać dane). |
SHARED_METRICS_ENABLED |
true |
Przełącza metryki Prometheus oxphp_shared_*. |
Wyłącz introspekcję we wdrożeniach z wrogo nastawionymi najemcami; metryki są wyłącznie zagregowane i można je bezpiecznie pozostawić włączone.
Endpointy introspekcji
Wszystkie odpowiedzi mają Content-Type: application/json; charset=utf-8. Parametry zapytania są w standardowym kodowaniu URL.
GET /__ox_shared/summary
Migawka najwyższego poziomu: zagregowane liczniki na typ, pamięć, tempo operacji oraz saturacja względem skonfigurowanych limitów.
{
"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, "soft_ratio": 0.7 },
"saturation": { "entries": 0.00127, "bytes": 0.00231 },
"diagnostics": {
"lock_diagnostics_level": "warn",
"cycle_detect_depth": 16,
"poison_strict": false
}
}Używaj summary w dashboardach i alertach cron. Pojedynczy scrape daje ci kondycję w rozbiciu na typy oraz zapas pojemności.
GET /__ox_shared/entries?limit=N
Wypisuje aktywne wpisy (ograniczone przez limit, domyślnie 100, maksymalnie 500). Jeden wiersz na wpis:
{
"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 to zewnętrzna liczba przetrzymań (retain count): ile wrapperów PHP oraz zagnieżdżonych wpisów Shared przetrzymuje ten wpis. Gdy spodziewasz się, że dany wpis powinien już zostać zebrany przez GC, a tak się nie dzieje, to właśnie to pole warto sprawdzić.
GET /__ox_shared/entry?id=N
Szczegóły specyficzne dla typu, dla pojedynczego wpisu:
{
"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 różni się w zależności od typu: Pool udostępnia { size, in_use, idle, waiting, idle_by_thread, max_size }, Channel udostępnia { capacity, pending, closed, senders_blocked, receivers_blocked }, Counter udostępnia { value }, i tak dalej.
GET /__ox_shared/preview?id=N
Podgląd kształtu wartości dla wartości skalarnych i małych tablic. Wartości tekstowe są przycinane do SHARED_PREVIEW_STRING_LIMIT (domyślnie 256 bajtów); tablice pokazują pierwsze SHARED_PREVIEW_ARRAY_LIMIT wpisów (domyślnie 20). Kontrolowane przez SHARED_INTROSPECTION_PREVIEW_ENABLED.
{ "id": 42, "type": "Counter", "preview": "1420" }Używaj preview podczas programowania; wyłącz go na produkcji, gdy wartości mogą zawierać dane użytkowników.
GET /__ox_shared/types
Wylicza katalog typów v1, przydatny dla generowanych narzędzi, które potrzebują mapowania tag → klasa:
{
"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": 20, "name": "Map", "php_class": "OxPHP\\Shared\\Map" },
{ "tag": 30, "name": "Mutex", "php_class": "OxPHP\\Shared\\Mutex" },
{ "tag": 31, "name": "Channel", "php_class": "OxPHP\\Shared\\Channel" },
{ "tag": 50, "name": "Pool", "php_class": "OxPHP\\Shared\\Pool" }
]
}GET /__ox_shared/graph?id=N[&depth=D][&edges=E]
Przejście BFS po wychodzących referencjach Shareable, począwszy od id=N. Zwraca węzły i krawędzie osiągalnego podgrafu. Wartości domyślne: depth=16, edges=500. Wyczerpanie budżetu przechodzenia grafu ustawia w odpowiedzi 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
}Sięgnij po graph po wystąpieniu CycleException, aby zobaczyć osiągalną ścieżkę, którą przeszedł walker, albo podczas diagnozowania „dlaczego ten Counter nie jest zbierany przez GC”: graf pokazuje każdego rodzica, który przetrzymuje na nim retain.
Metryki Prometheus
Wszystkie metryki są udostępniane pod GET /metrics obok podstawowych metryk serwera.
Dla całego rejestru
| Metryka | Typ | Etykiety | Opis |
|---|---|---|---|
oxphp_shared_objects_total |
gauge | type |
Liczba aktywnych wpisów na typ. |
oxphp_shared_operations_total |
counter | type |
Skumulowana liczba operacji skierowanych do każdego typu. |
oxphp_shared_bytes |
gauge | type |
Przybliżona liczba bajtów na typ (±30% względem mallinfo). |
oxphp_shared_total_bytes |
gauge | — | Suma dla wszystkich typów. |
oxphp_shared_capacity_saturation |
gauge | kind |
entries i bytes jako ułamki swoich limitów. |
oxphp_shared_deadlock_detected_total |
counter | — | Wykryte międzywątkowe cykle oczekiwania (wait-for). |
Channel
| Metryka | Typ | Etykiety |
|---|---|---|
oxphp_shared_channel_count |
gauge | channel_id |
oxphp_shared_channel_pending (przestarzałe) |
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 |
oxphp_shared_channel_pending to dawna nazwa
oxphp_shared_channel_count; obie serie niosą tę samą wartość podczas
okna deprecacji i rozejdą się, gdy alias zostanie usunięty w
przyszłym wydaniu. Nowe dashboardy podłączaj do _count.
Map
| Metryka | Typ | Etykiety |
|---|---|---|
oxphp_shared_map_entries |
gauge | map_id |
oxphp_shared_map_max_entries |
gauge | map_id |
oxphp_shared_map_saturation |
gauge | map_id |
Pool
| Metryka | Typ | Etykiety |
|---|---|---|
oxphp_shared_pool_count |
gauge | pool_id |
oxphp_shared_pool_size (przestarzałe) |
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_size to dawna nazwa
oxphp_shared_pool_count; obie serie niosą tę samą wartość podczas
okna deprecacji i rozejdą się, gdy alias zostanie usunięty w
przyszłym wydaniu. Nowe dashboardy podłączaj do _count.
Etykiety oxphp_shared_pool_evicted_total: reason=idle_timeout | evict | shutdown. idle_timeout to automatyczna eksmisja bezczynnego slotu, evict to jawne wywołanie Pool::evict(), a shutdown to sprzątanie przy zakończeniu procesu.
Counter / Flag / Once / Mutex
Poszczególne instancje Counter, Flag, Once i Mutex nie mają własnych, indywidualnych serii metryk: rozdęłyby kardynalność etykiet. Zamiast tego do inspekcji pojedynczych instancji użyj obejmującego cały rejestr licznika oxphp_shared_operations_total{type=...} oraz JSON-a z /__ox_shared/entry?id=….
Śledzone jako praca uzupełniająca; dzisiejsza widoczność odbywa się przez /__ox_shared/entry.
Poradniki diagnostyczne
Pool jest nasycony (kody 429 z nieudanymi ponowieniami)
Objawy: klienci HTTP widzą przekroczenia limitu czasu, oxphp_shared_pool_waiting rośnie, oxphp_shared_pool_count jest przyszpilony na maxSize.
Sprawdź:
curl -s http://localhost:9090/__ox_shared/entry?id=<pool_id> | jq .type_specificPrzyjrzyj się idle_by_thread. Jeśli jest {} lub mocno niezrównoważone (worker 0 ma 8 bezczynnych, worker 3 ma 0), operacja acquire rywalizuje o wątki, które akurat są zajęte gdzie indziej. Powiązanie z wątkiem (affinity) w v1 nie równoważy obciążenia. Albo zwiększ maxSize, albo zredukuj punkt zapalny operacji acquire przypadający na dany wątek.
Jeśli idle_by_thread jest zrównoważone, ale wszystko jest w in_use, zwiększ maxSize.
Nasycenie pamięci
Sprawdź oxphp_shared_total_bytes oraz oxphp_shared_capacity_saturation{kind="bytes"}. Jeśli któreś z nich jest wysokie:
curl /__ox_shared/entries?limit=500i posortuj wedługmem_bytes, aby znaleźć największych sprawców.curl /__ox_shared/entry?id=<N>dla każdego z nich, aby sprawdzić kształt. Dla Map przyjrzyj siękey_countwzględemmax_entries.- Najczęstsza przyczyna: nieograniczona
Shared\Mapz kluczami pochodzącymi z danych wejściowych użytkownika. Rozwiązaniem jest limitmaxEntriesoraz polityka retencji.
Wrapper nie chce zostać zebrany przez GC
refcount w /__ox_shared/entries mówi ci, ile jest zaległych przetrzymań (retain). Jeśli pozostaje powyżej 1 po tym, jak wrapper PHP wychodzi z zasięgu, przy życiu trzyma go inny wpis Shared.
curl -s http://localhost:9090/__ox_shared/graph?id=<N> | jq .nodesPrzejdź graf wstecz. Każdy węzeł prowadzący do zablokowanego wpisu przetrzymuje retain. Usuń referencję ($map->remove($key), zamknij kanał, porzuć wpis Mutex), a refcount spadnie.
CycleException wystąpił na produkcji
Komunikat wyjątku zawiera osiągalną ścieżkę, którą zbadał detektor cykli. Zmapuj te identyfikatory z powrotem na typy przez /__ox_shared/entries i poproś /__ox_shared/graph?id=<root> o pełny kształt:
# Exception message: "cycle would form: #42 → #51 → #42"
curl -s http://localhost:9090/__ox_shared/graph?id=42 | jqWynik wizualizuje łańcuch, dzięki czemu widać, gdzie wprowadzono niezamierzoną referencję zwrotną.
Zadziałał detektor zakleszczeń
oxphp_shared_deadlock_detected_total tyka. Sprawdź logi serwera. Detektor emituje jeden rekord logu na każdy cykl, wraz z identyfikatorami zaangażowanych muteksów oraz wątkami będącymi ich właścicielami. Odzyskiwanie:
curl /__ox_shared/entry?id=<mutex_id>dla każdego i potwierdźpoisoned=false. Jeśli jest zatruty (poisoned), detektor już przerwał cykl.- Jeśli cykl to prawdziwy błąd ponownego wejścia (reentry), przeprowadź refaktoryzację, aby użyć osobnych muteksów na każdy zakres blokady.
- Ustaw
SHARED_LOCK_DIAGNOSTICS=strictw środowisku staging, aby zamienić przyszłe ponowne wejście w szybką awarię (fast-fail) zamiast wykrytego cyklu.
Długodziałający szkielet testów soak
tests/soak/pool_soak.sh to ręczny (poza CI) szkielet do weryfikacji stabilności Shared\Pool przez godziny lub dni ciągłego obciążenia. Działa następująco:
- Uruchamia obraz deweloperski z dynamicznym skalowaniem workerów (domyślnie
PHP_WORKERS=4:40) oraz krótkimidleTimeoutpuli, tak aby harmonogram eksmisji odpalał się bez przerwy. - Ładuje
tests/soak/workload.phpjako bootstrap workera, który tworzy 10 pul ×maxSize=8i obsługuje acquire/release przy każdym żądaniu. - Generuje ruch za pomocą
wrkprzezSOAK_DURATION_MINminut (domyślnie 1440 = 24h). - Co 60 s pobiera (scrape)
/metricsoraz RSS kontenera dotests/soak/out/<timestamp>/metrics.csv. - Na końcu zapisuje
verify.txtz wynikiem pass/fail dla pięciu kryteriów wyjścia do wydania (dryf RSS w granicach ±5%, zero panik na nieaktualnych uchwytach, zero wyciekniętych wpisów przy zamknięciu, płynnie rosnące eksmisje wskutek idle-timeout, zero zadziałań detektora zakleszczeń).
Wymagania na hoście: docker, wrk, curl, awk.
Typowe wywołania:
# 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.shArtefakty trafiają do tests/soak/out/<timestamp>/:
metrics.csv— jeden wiersz na minutę (unixowy ts, RSS, liczby wpisów per typ, liczniki eksmisji per pula, liczba zakleszczeń, operacje).server.log— stdout/stderr kontenera, w tym wszelkie ślady nieaktualnych uchwytów lub panik.wrk.out/wrk.err— surowe wyjście generatora obciążenia.metrics.final— ostatni scrape/metricspobrany tuż przed rozbiórką kontenera. Służy do potwierdzenia, że po ustaniu obciążenia liczby wpisów i bajtów wróciły do poziomu bazowego (brak wyciekniętych wpisów Shared).verify.txt— raport pass/fail dla pięciu kryteriów wyjścia.
Nie podłączaj tego do CI. Uruchomienie na 24h nie jest tanie, a jego celem jest pewność przed wydaniem, nie ciągła walidacja.
Częstotliwość scrape'owania
Endpointy rejestru chodzą po stanie na żywo pod blokadami odczytu, więc scrape'owanie jest tanie, ale nie darmowe. Zalecane częstotliwości:
/metrics— co 15 s (typowa domyślna wartość Prometheus). Wyłącznie zagregowane; narzut jest pomijalny./__ox_shared/summary— co 60 s dla dashboardów. Nieco cięższe niż/metrics./__ox_shared/entries— tylko na żądanie. Iteruje po wszystkich shardach; nie scrape'uj przy każdym takcie./__ox_shared/entry//preview//graph— na żądanie podczas analiz.
Powiązane
- Stan współdzielony — model myślowy i przegląd prymitywów.
- Metryki Prometheus — podstawowe metryki serwera pod tym samym endpointem
/metrics. - Serwer wewnętrzny — jak endpointy
/__ox_shared/*podłączają się doINTERNAL_ADDR. - Migracja do zewnętrznego magazynu — gdy saturacja jest strukturalna, a nie do wyregulowania.