Luther Monson, Staff Software Engineer at SUSE

Luther Monson

Go by way of PHP · Containers · VMs · Windows · Kubernetes

My Benchmark Was 4x Wrong Because of WSL

Launching the server with its working directory on /mnt/c cost ePHPm about 4x on a light workload. The report built a wrong explanation on top of it, and the strace shows why.

8-Minute Read

Bar chart of ePHPm requests per second by working directory: native ext4 far ahead of DrvFs, with DrvFs throughput falling as the cwd path gets deeper

I do most of my benchmarking on a Windows box in WSL2. It’s the machine on my desk with 32 threads and 62 GB of RAM. Everybody who does that learns the first rule early: don’t put your data on /mnt/c. DrvFs is the 9p-backed mount that exposes the Windows drives inside WSL, and it’s slow for file-heavy work. So the databases, the docroot and the build output all live on the WSL ext4 disk. My scaling harness for ePHPm even says so in capital letters in its README.

Clustered SQLite in ePHPm, Part 2: Adding Turso

How I decided a beta database engine was ready by testing it instead of reading its version number. ePHPm v0.7.0 replaces both rusqlite and sqld with Turso.

9-Minute Read

Bar chart of four SQLite lanes at 16 concurrent connections: rusqlite and Turso single node, sqld and Turso CDC clustered

Part 1 ended with ePHPm running two SQLite engines. rusqlite ran in-process for a single node, and an embedded sqld child process ran for a cluster. The sidecar fell off a cliff past four concurrent writers. The best fix I could ship was a semaphore set to 1. This post is about replacing both engines with one, and about how I decided that engine was ready.

Requests That End Wrong: A 200 That Was Really a Crash

Three ePHPm bugs lived in how an embedded PHP request starts and ends. A bailout returned 200, $_POST came up empty and a SIGTERM corrupted the heap.

10-Minute Read

Diagram of a bare zend_bailout being absorbed by php_execute_script's own zend_try instead of reaching ePHPm's guard

The scariest response a server can send isn’t a 500. It’s a 200 that should have been a 500. A crash is loud. The pod restarts, an alert fires, somebody looks. A truncated page with a success status code is quiet. Your CDN caches it and the health check calls the box healthy. A client parses half a document as if it were the whole thing.

Database Connection Pooling with Smart Reset

PHP opens a new MySQL connection on every request, but ePHPm reuses them and cleans up only the ones that need it. Here's how that works, two bugs it had and how much load it takes off MySQL.

10-Minute Read

PHP request connecting to the ePHPm MySQL proxy, which lends it a pooled backend connection and resets it only if the session was dirtied

PHP’s share-nothing model is one of its best properties. It has one expensive side effect: a request can’t keep a database connection for the next request. php-fpm gives you PDO::ATTR_PERSISTENT and mysqli’s p: hosts. But those are per-worker-process. PDO’s keep whatever session state the last request left behind. mysqli at least resets the session with COM_CHANGE_USER on reuse. And almost nobody turns them on. So in practice every PHP request does a TCP connect, a MySQL…

Clustered SQLite in ePHPm, Part 1: Replication by Sidecar

ePHPm first made SQLite highly available by embedding sqld as a child process. This is how that worked and the list of things that went wrong with it.

9-Minute Read

Two ePHPm nodes, each running an embedded sqld child process, connected by gossip, a KV data plane and sqld gRPC

Most PHP sites I have run in the last twenty years did not need a MySQL server. They needed a database, and MySQL was the one every framework spoke. With ePHPm I have been trying to get rid of that server for the common case. The piece that makes it possible is litewire. PHP connects with plain pdo_mysql. litewire speaks the MySQL wire protocol, and the queries land in a SQLite file. WordPress and Laravel don’t know the difference. No mysqld, no child processes, just ephpm and a .db file.

Gossip-Based Clustering for PHP Apps

How ePHPm servers find each other, share data and handle a server joining or leaving. It also measures how fast a write reaches the other servers and covers a bug I found while measuring it.

11-Minute Read

Three ePHPm nodes: small values replicated to every node over gossip, a large value stored on its owner and one replica over a TCP data plane

Most PHP developers never touch clustering primitives. That’s by design. PHP is share-nothing. Each request starts from zero, and anything shared lives somewhere else. Sessions go in Redis. Cache goes in Redis or Memcached. Locks go in Redis. When you run two app servers, the “cluster” is really two stateless boxes pointed at the same Redis. That works until Redis is the thing you’re trying not to run.

Building a Custom SAPI - How PHP Actually Talks to the Outside World

php-fpm, mod_php and the PHP command line all plug into PHP the same way. Here's how ePHPm does it, and the shortcut that broke WordPress.

9-Minute Read

Table mapping PHP operations like echo, header(), $_POST, $_COOKIE and $_SERVER to SAPI callbacks and to the Rust side of ePHPm

If you’ve only ever run PHP behind nginx, you’ve probably never had to think about how echo reaches a socket. When you embed PHP like ePHPm does, it’s the first thing you have to answer. The answer is a SAPI. There is very little written about implementing one. Most of what I know came from reading sapi/cli, sapi/cgi and sapi/embed in php-src. Then I found out which parts I’d misread. This is the write-up I wanted back in March.

Fixing Windows Archive Extraction in SPC

"The pipe has been ended" wasn't a pipe problem. It was the wrong tar program and Windows symlinks, and fixing one line of static-php-cli took three tries.

7-Minute Read

Diagram: 7za decompressing into a pipe read by MinGW tar, which exits on a C:\ path and leaves 7za reporting The pipe has been ended

One of the first things that broke when I started building PHP on Windows with static-php-cli (SPC) wasn’t PHP at all. It was unpacking the source tarballs. libxml2, xz and a couple of others would die partway through extraction with:

The Pragmatism of ePHPm - Every Decision Has a "Because" Behind It

ePHPm hit v0.1.0 this week. It isn't a rewrite-it-in-Rust project. Here's what I built, what I borrowed, what I sent upstream, and what I deliberately haven't built.

7-Minute Read

ePHPm in the center with six boxes: Rust and C wrapper, KV store, chitchat gossip, static-php-cli upstream fixes, litewire, and a dashed box of features not built yet

ePHPm tagged v0.1.0 this week, three months after the first commit. Before the feature list grows any further I want to write down how decisions get made on it. This is mostly for myself. The easiest way to ruin a project like this is to let it turn into a checklist of things other projects have.

Recent Posts

Categories

About

Luther is a 20+ year software veteran specializing in backend APIs and architecting products. Employed at SUSE as the Team Lead for the Containers on Windows Team working on Rancher + Kubernetes