Thanks for your replies. I tried the tmpfs idea, but couldn't squeeze any additional performance. I noticed that after 5k requests, I'd already used ~200mb of tmpfs space, so it's not very memory efficient.
Here's an apachebench output with tmpfs
The command I used is very basic: ab -n 50000 -c 100 -H 'Host: <snip>' https://10.16.0.122:80/
Code:
Concurrency Level: 100
Time taken for tests: 106.836 seconds
Complete requests: 17851
Failed requests: 0
Total transferred: 791067065 bytes
HTML transferred: 784854917 bytes
Requests per second: 167.09 [#/sec] (mean)
Time per request: 598.488 [ms] (mean)
Time per request: 5.985 [ms] (mean, across all concurrent requests)
Transfer rate: 7230.95 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.1 0 2
Processing: 40 597 96.1 584 1771
Waiting: 36 596 96.1 583 1770
Total: 42 597 96.1 584 1771
I stopped the test early before my tmpfs ran out of space (656M used after ~22k requests). Pre-tmpfs, I noticed the IOPS didn't break more than ~600, and while the SSD I used is guaranteed for 1600, it has a burst of 16k, so that shouldn't have been the bottleneck. It appears that raw CPU power is really the key here, and it looks like we'll have to scale that up by core count, and then get bigger SSD's for larger IOPS guarantees to scale.
I'll try a Limesurvey 3.x series install and see if it does any better. The Nginx install setup is just a straight include+fastcgi_pass, so nothing fancy, and Nginx load doesn't pass ~1% cpu use.
For reference, these are custom E2 instances on Google Cloud using 40gb local SSDs. I scaled up the SSDs to get more IOPS and saw no noticeable improvement.