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ć:

bash
INTERNAL_ADDR=127.0.0.1:9090

Zaró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.

json
{ "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:

json
{ "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:

json
{ "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.

json
{ "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:

json
{ "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.

json
{ "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
Note

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
Note

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=….

Metryki Mutex to kandydat na v1.x

Ś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ź:

bash
curl -s http://localhost:9090/__ox_shared/entry?id=<pool_id> | jq .type_specific

Przyjrzyj 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:

  1. curl /__ox_shared/entries?limit=500 i posortuj według mem_bytes, aby znaleźć największych sprawców.
  2. curl /__ox_shared/entry?id=<N> dla każdego z nich, aby sprawdzić kształt. Dla Map przyjrzyj się key_count względem max_entries.
  3. Najczęstsza przyczyna: nieograniczona Shared\Map z kluczami pochodzącymi z danych wejściowych użytkownika. Rozwiązaniem jest limit maxEntries oraz 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.

bash
curl -s http://localhost:9090/__ox_shared/graph?id=<N> | jq .nodes

Przejdź 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:

bash
# Exception message: "cycle would form: #42 → #51 → #42" curl -s http://localhost:9090/__ox_shared/graph?id=42 | jq

Wynik 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:

  1. curl /__ox_shared/entry?id=<mutex_id> dla każdego i potwierdź poisoned=false. Jeśli jest zatruty (poisoned), detektor już przerwał cykl.
  2. Jeśli cykl to prawdziwy błąd ponownego wejścia (reentry), przeprowadź refaktoryzację, aby użyć osobnych muteksów na każdy zakres blokady.
  3. Ustaw SHARED_LOCK_DIAGNOSTICS=strict w ś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:

  1. Uruchamia obraz deweloperski z dynamicznym skalowaniem workerów (domyślnie PHP_WORKERS=4:40) oraz krótkim idleTimeout puli, tak aby harmonogram eksmisji odpalał się bez przerwy.
  2. Ładuje tests/soak/workload.php jako bootstrap workera, który tworzy 10 pul × maxSize=8 i obsługuje acquire/release przy każdym żądaniu.
  3. Generuje ruch za pomocą wrk przez SOAK_DURATION_MIN minut (domyślnie 1440 = 24h).
  4. Co 60 s pobiera (scrape) /metrics oraz RSS kontenera do tests/soak/out/<timestamp>/metrics.csv.
  5. Na końcu zapisuje verify.txt z 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:

bash
# 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

Artefakty 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 /metrics pobrany 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.
Warning

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:

  • /metricsco 15 s (typowa domyślna wartość Prometheus). Wyłącznie zagregowane; narzut jest pomijalny.
  • /__ox_shared/summaryco 60 s dla dashboardów. Nieco cięższe niż /metrics.
  • /__ox_shared/entriestylko na żądanie. Iteruje po wszystkich shardach; nie scrape'uj przy każdym takcie.
  • /__ox_shared/entry / /preview / /graphna żądanie podczas analiz.

Powiązane