Dit is het tweede deel in onze reeks over moderne PHP application servers. Lees eerst deel 1 als je dat nog niet gedaan hebt.
Moderne PHP application servers
Er zijn verschillende keuzes voor moderne PHP application servers. Wij begonnen op Swoole, en zijn aan het overstappen op FrankenPHP. Maar veel van wat we hier schrijven is op alle moderne PHP runtimes van toepassing, zoals bijvoorbeeld ook RoadRunner.
Het onderliggende principe is namelijk overal hetzelfde: een lang-draaiend PHP proces, waarbij je de
optuigende stappen aan het begin van dat proces eenmalig uitvoert (zoals het optuigen en opwarmen van
je Framework), voordat je in een oneindige loop begint te wachten en requests gaat verwerken.

Swoole werkend krijgen
Swoole werkend krijgen was in het begin een flinke uitdaging. Mede door een gebrek aan goede documentatie, in combinatie met de hoeveelheid permutaties en mogelijkheden die Swoole biedt.
We eindigden uiteindelijk bij een aanpak waarbij Swoole een instelbare hoeveelheid workers start en
iedere worker 1 request per keer laat afhandelen. Swoole kent meerdere dispatch_mode's, en dit was
nummer 3: Preemptive mode: Dispatches connections to only the idle worker processes.
Het starten van de workers leek via een soort fork-aanpak te verlopen, waarbij iedere worker dus een soort kopie kreeg van de huidige state, maar hier was ik nooit zeker van - want ik kon er destijds niks over terugvinden in de documentatie.
Naast dispatch_mode nummer 3, zijn er nog 8 andere mogelijkheden. Oorspronkelijk draaiden we op een
andere mode (ik weet niet meer welke - ik vermoed nummer 1 - Round Robin). Ook hierbij start Swoole
een instelbare hoeveelheid workers, maar was het toegestaan voor workers om meerdere requests tegelijk
te verwerken, op asynchrone wijze. In dispatch_mode kan je dus met 8 workers maximaal 8 requests
tegelijk afhandelen, maar met veel van de andere dispatch_mode's kunnen dit er meer zijn, bijvoorbeeld
20, waarbij sommige workers dus 2 of zelfs 3 requests tegelijk afhandelen.
Helaas had ik dat oorspronkelijk niet door: ik had onvoldoende documentatie gelezen, te veel aannames gedaan en ik was überhaupt al begonnen met een verkeerd mentaal model. Met als gevolg: een lange debugsessie.

Groeipijnen
In veel van de dispatch_mode's, waaronder degene die wij onwetend gebruikten, kunnen dus situaties
ontstaan waarbij workers dus meerdere requests tegelijk zullen verwerken.
Tijdens het ontwikkelen in mijn lokale omgeving merkte ik dit niet - want je opent steeds 1 pagina per keer. Zelfs toen het live op onze staging-omgeving stond, ging het heel vaak goed. Maar af en toe gaf een request een vreemde onverklaarbare error, die we niet konden reproduceren. Dit bleken natuurlijk klassieke concurrency bugs te zijn.
Dit soort concurrency bugs ontstaan omdat een worker een 'state' heeft. Zo houden wij bijvoorbeeld logging bij van ieder request: welke URL werd aangeroepen, welke event-listeners werden afgevuurd, etc - heel handig voor debugging. Aan het einde van de request slaan we dit lijstje van verzamelde logs op, en daarna gooien we dit lijstje leeg.
Maar als dezelfde worker toevallig 2 mensen toegewezen krijgt op ongeveer hetzelfde moment kan dit fout gaan, omdat ze allebei hetzelfde lijstje vullen. Als je geluk hebt krijg je dan een foutmelding. Maar als je pech hebt merk je niks, en worden er gemixte logs weggeschreven.
Waarom is dit zo moeilijk om te ontdekken? Een gemiddelde test-server kan makkelijk tientallen workers hebben, en veel requests werden binnen 50 milliseconden afgehandeld. Dat betekent dus dat je honderden requests per seconde moet verwerken, voordat je toevallig precies 2 verzoeken hebt die elkaar zo weten te kruisen. En dan moet je ook nog het 'geluk' hebben dat het een merkbare foutmelding veroorzaakt, en niet gewoon ongemerkt dingen doet die niet kloppen.
Gelukkig hadden we ook tests uitgevoerd met een heel klein aantal workers, en ontdekten we toch snel
dit probleem. Eenmaal ontdekt, en de dispatch_mode aangepast, was dit probleem snel verholpen.
Kinderziektes
Met de dispatch_mode op 3 gingen alle requests nu wel goed. Maar zelfs met de juiste dispatch_mode
blijft het van essentieel belang om ervoor te zorgen dat je de volledige state van je worker opruimt,
aan het einde van een request. Dit is iets wat we in PHP niet gewend waren - omdat in het traditionele
model je altijd met een schone lei begint. Maar hier liepen we tijdens development regelmatig ook tegen
kleine shared-state bugs aan, zoals een profiler die een deel van zijn data bewaarde, of bijvoorbeeld
een pagina nog oude meta-data toonde, omdat die alleen overschreven werd als er unieke data beschikbaar
was.
Zoals we wel vaker schrijven: bijna alles bij development is een trade-off: in dit geval vereist de toename in snelheid ook een toename in complexiteit.
De meest lastige bug, die het meer dan een jaar in productie had overleefd, was een mysterieuze terugkerende situatie waarin onze workers plotseling geen requests meer accepteerden. Onze monitoring informeert ons dan gelukkig altijd heel snel, en het herstellen is een kwestie van het (systemd) Swoole-proces restarten, maar we kwamen er maar niet achter waar het door kwam. Tot ik op een dag een ingeving kreeg, weer ging debuggen en de oorzaak had gevonden: het bleek dat onze Redis-connectie geen timeout had gekregen. Heel af en toe kon de Redis-connectie wel eens haperen. Maar onze applicatie hield dan deze verbinding vast, terwijl Redis niet meer aanwezig was, en omdat er geen timeout was, hield hij de verbinding vast tot in lengte der dagen.

FrankenPHP migratie
Er waren natuurlijk nog veel meer punten die tijdens onze initiële Swoole-integratie uitgezocht moesten worden, zoals communicatie tussen de verschillende workers, de werking van de Swoole Tables (voor gedeelde data), of bijvoorbeeld de issues die optreden bij MySQL-connecties wanneer een worker geforked werd.
Gelukkig waren we, toen we begonnen met de overstap naar FrankenPHP, al flink genezen van al deze kinderziektes. Natuurlijk bracht ook deze runtime weer zijn eigen uitdagingen en verrassingen met zich mee. Er moest bijvoorbeeld een nieuw mechanisme gebouwd worden waarop iedere FrankenPHP-worker zich kon identificeren, zodat er communicatie kon zijn over bepaalde dingen, zoals het opschonen van de cache. Maar omdat we al zoveel gebouwd hadden ging het geheel een stuk soepeler.
En de voordelen dan?
Het was niet alleen kommer en kwel: er waren natuurlijk belangrijke redenen waarom we deze stap hadden gemaakt naar moderne application servers. Die bieden namelijk heel veel voordelen, en daar gaan we het over hebben in het volgende artikel.

Jeroen, op jacht naar bugs. Een veelvoorkomend spektakel.