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. Poza pozostawieniem włączonego SHARED_ENABLED 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.

SHARED_ENABLED=false również usuwa obie te powierzchnie, razem z resztą podsystemu. Używaj powyższych przełączników, aby zachować dostępność prymitywów, a jednocześnie ukryć to, co przechowują.

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 }, "saturation": { "entries": 0.00127, "bytes": 0.00231 }, "diagnostics": { "lock_diagnostics_level": "warn", "cycle_detect_depth": 16 } }

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 { max_size, count, in_use, idle, waiting, idle_by_thread, rebalance_strategy }, Channel udostępnia { capacity, count, 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), z zaokrągleniem w dół do najbliższej granicy znaku, więc tekst wielobajtowy nigdy nie jest ucinany w połowie znaku; 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": 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" } ] }

Odpowiedź nie zawiera Shared\Mutex.

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_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

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_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

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.

Detektor zakleszczeń

Zadanie działające w tle wyszukuje międzywątkowe cykle oczekiwania (wait-for) na Shared\Mutex: dwa lub więcej wątków, z których każdy czeka na muteks trzymany przez inny z nich. Co SHARED_LOCK_POLL_INTERVAL_MS (domyślnie 100) skanuje graf oczekiwań. SHARED_LOCK_DIAGNOSTICS określa, co robi, gdy skan znajdzie cykl:

Wartość Detektor Przy cyklu
off Nie jest uruchamiany. SHARED_LOCK_POLL_INTERVAL_MS nie jest używane, a oxphp_shared_deadlock_detected_total pozostaje na 0. Nic. withLockTimeout() w cyklu poddaje się z OperationTimeoutException, gdy upłynie jego limit czasu. Cykl złożony wyłącznie z wywołań withLock() nie ma własnego limitu czasu, który by go zakończył: każde z jego żądań czeka, aż zostanie zakończone, przez max_execution_time, jeśli je ma, a blokady trzymane przez zakończone żądanie są zwalniane w chwili jego zakończenia (zobacz Shared\Mutex).
warn (domyślnie w buildach release) Działa. Loguje i zlicza (patrz niżej); oczekiwania pozostają bez zmian, z takim samym skutkiem jak przy off.
strict (domyślnie w buildach debug binarki) Działa. Loguje i zlicza, a także sygnalizuje wątkom w cyklu: withLock() w cyklu rzuca DeadlockException w ciągu 100 ms od skanu, a withLockTimeout() odczekuje swój limit czasu i dopiero wtedy rzuca DeadlockException zamiast OperationTimeoutException. Oczekujący, którego blokada zostanie zwolniona wcześniej (gdy wyjątek innego oczekującego odwinie domknięcie, które ją trzymało), przejmuje ją i nie rzuca wyjątku.
Tylko małe litery

Wartość jest dopasowywana wyłącznie małymi literami. Każda inna wartość, w tym Strict czy trace, po cichu daje wartość domyślną.

Każdy cykl znaleziony przez skan daje co najmniej jeden rekord WARN z event=shared.deadlock_detected, a każdy z nich wskazuje jeden wątek w cyklu (start_thread, powtórzony jako holder_thread); rekord nie wskazuje muteksów. oxphp_shared_deadlock_detected_total rośnie z każdym rekordem.

Ponowne wejście w obrębie tego samego wątku to osobne sprawdzenie, niezależne od tego ustawienia: ponowne wejście w muteks na wątku, który już go trzyma, rzuca DeadlockException w miejscu wywołania przy każdej wartości, łącznie z off, i nigdy nie staje się cyklem.

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"}. Serwer niczego nie odrzuca ani nie degraduje w miarę wzrostu zużycia. Jego pierwszą reakcją jest odmowa utworzenia nowego obiektu Shared po osiągnięciu SHARED_MAX_BYTES lub SHARED_MAX_ENTRIES; obiekty, które już istnieją, rosną dalej bez ponownego sprawdzania tego limitu (Map bez ograniczenia, chyba że została utworzona z maxEntries), więc wskaźnik bytes może pokazywać wartość powyżej 1.0. Aby dostać wczesne ostrzeżenie, dodaj regułę alertu, na przykład oxphp_shared_capacity_saturation > 0.7. 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: detektor zakleszczeń zobaczył dwa lub więcej wątków, z których każdy czeka na Shared\Mutex trzymany przez inny z nich. Poszukaj w logach serwera event=shared.deadlock_detected; rekordy wskazują wątki, nie muteksy. Odzyskiwanie:

  1. Znajdź ścieżki kodu, które biorą dwa muteksy jeden wewnątrz drugiego, i spraw, by wszędzie brały je w tej samej kolejności, albo połącz tę parę w jeden muteks.
  2. Przy SHARED_LOCK_DIAGNOSTICS=warn, domyślnym w buildach release, cykl jest tylko logowany i zliczany. Ustaw SHARED_LOCK_DIAGNOSTICS=strict w środowisku staging, aby detektor go przerywał; co wtedy robi każde oczekujące wywołanie, opisuje Detektor zakleszczeń.

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:

  • /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.

Prompty dla Twojego agenta

Te prompty zakładają agenta z podłączonym serwerem MCP dokumentacji OxPHP — sam przeczyta strony, których potrzebuje.

prompt
Zbuduj dashboard Prometheusa i reguły alertów dla prymitywów Shared\*, których faktycznie używa mój kod. Przeczytaj stronę `shared-state/shared-observability` przez serwer MCP dokumentacji OxPHP. Znajdź każdy `Shared\Map`, `Shared\Pool`, `Shared\Channel` i inny prymityw, który konstruuje to repozytorium, i zbuduj panele oraz progi alertów z ich udokumentowanych metryk — nasycenia, oczekiwania i odrzuceń.

Powiązane

Znalazłeś błąd? Zgłoś go →