Disclaimer: that is, iff I haven’t written anything bad while authoring the benchmark below, (which is likely because I’m not sure what this means Server · HTTP.jl, and whether it’s “recommended” to do this another way). If that is so, Julia is very bad at doing web.
TechEmpower’s Framework benchmarks
(GitHub - TechEmpower/FrameworkBenchmarks: Source for the TechEmpower Framework Benchmarks project) is a framework for easily and homogeneously evaluating different languages and frameworks on simple web scenarios that are non trivial. It provides a set of tools, build scripts and dockerfiles to make reproducibility and development easy. I’ve made a new PR with the latest and greatest Julia and toolset and made a PR out of it: feat(benchmark): updated Julia/HTTP.jl benchmark by pankgeorg · Pull Request #8370 · TechEmpower/FrameworkBenchmarks · GitHub
Previous work
There was already some previous work on HTTP.jl and Julia, (http-jl performance updates by mcmcgrath13 · Pull Request #6215 · TechEmpower/FrameworkBenchmarks · GitHub “HTTP.jl”) and (Jewelia : Plaintext, JSON Serialization, Single Database Query, Multiple Database Queries by donavindebartolo · Pull Request #6829 · TechEmpower/FrameworkBenchmarks · GitHub “Jewelia”); this work is basically a more idiomatic rewrite of the Jewelia on top of the (now defunct, in the sense that it doesn’t ‘verify’ for me) HTTP.jl entry.
The benchmark
The packages used are pretty straightforward for Julia:
- HTTP.jl for internet
- JSON3.jl for json
- LibPQ.jl for postgres communication
- HypertextLiteral.jl for HTML interpolation
And that’s it.
Important
There is also a “hack” that I kept around; the run.sh
script launches # cores processes, each accepting connections based on a Linux kernel esoteric load balancing on sockets that have SO_REUSEPORT
enabled (reuseaddr=true on HTTP.jl). I don’t think anyone deploys services like that, and I would really like to get rid of this in the future. Unfortunately though, without this hack, the server doesn’t verify (./tfb --mode verify --test http-jl
) (which means it doesn’t serve enough requests to be thought of as a pass)
The rivals
We run this against
- Python’s fastapi
- Python’s flask
- Javascript’s fastify (the default benchmark uses mongodb, check
fastify-postgres
where applicable)
All set up with the postgres database.
Do ./tfb --mode benchmark --test http-jl fastapi flask fastify-postgres
, if you want to follow through)
The hardware
I run this on an Ampere Altra q80-30 with 80 CPUs and 256GB of RAM and the results are kind of disappointing (verified on other hardware too, but every number I refer to, will be from this computer). Julia is 3-10 times slower than any of the other frameworks we benchmark against - only faster in JSON serialization under medium load.
Result Summary
fortune
, Query the db, sort and output HTML: 4-7 times slower
plaintext
, Respond with a String asap: 4-60 times slower
db
, Perform a single db query: 4-8 times slower
update
, Perform db updates, 5-7 times slower
query
, Perform variable number of db reads, 5-7 times slower
json
, Serialize a json and return it, *0.5-5 times slower (0.5 = faster)
+------------------------------------------------------------------------------+
| Type: fortune, Result: latencyAvg |
+-------------------+---------+---------+------------------+---------+---------+
| concurrencyLevels | fastapi | fastify | fastify-postgres | flask | http-jl |
+-------------------+---------+---------+------------------+---------+---------+
| 16 | 0.89ms | 0.88ms | 477.13us | 1.38ms | 3.83ms |
| 32 | 0.92ms | 0.93ms | 497.69us | 1.64ms | 4.66ms |
| 64 | 1.09ms | 1.52ms | 626.20us | 2.30ms | 6.09ms |
| 128 | 1.21ms | 1.68ms | 752.12us | 2.33ms | 6.64ms |
| 256 | 4.05ms | 4.87ms | 2.89ms | 6.17ms | 28.84ms |
| 512 | 10.37ms | 7.61ms | 7.19ms | 12.51ms | 47.33ms |
+-------------------+---------+---------+------------------+---------+---------+
+-----------------------------------------------------------------------+
| Type: plaintext, Result: latencyAvg |
+---------------------------+----------+----------+----------+----------+
| pipelineConcurrencyLevels | fastapi | fastify | flask | http-jl |
+---------------------------+----------+----------+----------+----------+
| 256 | 3.73ms | 3.20ms | 6.35ms | 244.96ms |
| 1024 | 11.68ms | 8.22ms | 22.60ms | 246.80ms |
| 4096 | 52.84ms | 30.18ms | 83.89ms | 360.46ms |
| 16384 | 222.26ms | 338.33ms | 287.10ms | 837.97ms |
+---------------------------+----------+----------+----------+----------+
+-------------------------------------------------------------------------------+
| Type: db, Result: latencyAvg |
+-------------------+----------+----------+------------------+--------+---------+
| concurrencyLevels | fastapi | fastify | fastify-postgres | flask | http-jl |
+-------------------+----------+----------+------------------+--------+---------+
| 16 | 745.29us | 808.95us | 514.45us | 0.89ms | 3.36ms |
| 32 | 749.84us | 0.91ms | 542.55us | 1.15ms | 3.61ms |
| 64 | 0.89ms | 1.27ms | 769.67us | 1.54ms | 4.62ms |
| 128 | 0.96ms | 1.46ms | 0.89ms | 1.63ms | 4.95ms |
| 256 | 3.37ms | 3.46ms | 3.00ms | 3.96ms | 31.11ms |
| 512 | 8.82ms | 6.16ms | 6.98ms | 8.46ms | 43.71ms |
+-------------------+----------+----------+------------------+--------+---------+
+---------------------------------------------------------------------------------+
| Type: update, Result: latencyAvg |
+-------------------+---------+----------+------------------+----------+----------+
| concurrencyLevels | fastapi | fastify | fastify-postgres | flask | http-jl |
+-------------------+---------+----------+------------------+----------+----------+
| 16 | 11.38ms | 11.98ms | 11.14ms | 14.51ms | 48.79ms |
| 32 | 17.94ms | 34.00ms | 42.87ms | 34.83ms | 222.12ms |
| 64 | 27.98ms | 61.12ms | 79.74ms | 61.99ms | 440.19ms |
| 128 | 43.71ms | 86.99ms | 120.61ms | 96.97ms | 669.13ms |
| 256 | 70.74ms | 113.50ms | 156.69ms | 155.97ms | 848.30ms |
+-------------------+---------+----------+------------------+----------+----------+
+---------------------------------------------------------------+
| Type: json, Result: latencyAvg |
+-------------------+----------+----------+----------+----------+
| concurrencyLevels | fastapi | fastify | flask | http-jl |
+-------------------+----------+----------+----------+----------+
| 16 | 224.01us | 85.99us | 381.38us | 152.81us |
| 32 | 265.37us | 86.95us | 433.82us | 199.09us |
| 64 | 268.03us | 132.31us | 437.30us | 1.72ms |
| 128 | 275.18us | 177.15us | 484.25us | 4.78ms |
| 256 | 1.08ms | 0.93ms | 0.87ms | 8.73ms |
| 512 | 1.57ms | 1.47ms | 2.23ms | 10.68ms |
+-------------------+----------+----------+----------+----------+
+----------------------------------------------------------------------------+
| Type: query, Result: latencyAvg |
+----------------+---------+---------+------------------+---------+----------+
| queryIntervals | fastapi | fastify | fastify-postgres | flask | http-jl |
+----------------+---------+---------+------------------+---------+----------+
| 1 | 11.37ms | 6.11ms | 6.95ms | 9.76ms | 44.70ms |
| 5 | 16.46ms | 22.26ms | 29.39ms | 20.85ms | 150.07ms |
| 10 | 22.56ms | 42.61ms | 46.50ms | 33.79ms | 281.09ms |
| 15 | 27.77ms | 62.12ms | 62.88ms | 46.93ms | 403.34ms |
| 20 | 34.98ms | 81.63ms | 88.61ms | 58.67ms | 524.15ms |
+----------------+---------+---------+------------------+---------+----------+
+----------------------------------------------------------------------------------+
| Type: fortune, Result: totalRequests |
+-------------------+-----------+-----------+------------------+---------+---------+
| concurrencyLevels | fastapi | fastify | fastify-postgres | flask | http-jl |
+-------------------+-----------+-----------+------------------+---------+---------+
| 16 | 271,393 | 274,710 | 503,555 | 174,686 | 62,738 |
| 32 | 523,920 | 518,147 | 976,489 | 295,919 | 104,850 |
| 64 | 892,275 | 709,285 | 1,548,450 | 444,227 | 158,516 |
| 128 | 1,003,488 | 747,625 | 1,605,884 | 518,438 | 181,736 |
| 256 | 1,202,922 | 917,032 | 1,590,039 | 621,660 | 212,334 |
| 512 | 1,115,387 | 1,058,308 | 1,605,699 | 600,512 | 213,201 |
+-------------------+-----------+-----------+------------------+---------+---------+
+------------------------------------------------------------------------------+
| Type: plaintext, Result: totalRequests |
+---------------------------+------------+------------+------------+-----------+
| pipelineConcurrencyLevels | fastapi | fastify | flask | http-jl |
+---------------------------+------------+------------+------------+-----------+
| 256 | 10,932,009 | 17,726,288 | 8,052,246 | 8,526,728 |
| 1024 | 12,632,536 | 19,756,110 | 8,443,728 | 9,153,410 |
| 4096 | 12,057,721 | 21,832,874 | 9,120,014 | 6,898,582 |
| 16384 | 11,377,496 | 22,162,879 | 10,346,123 | 5,799,038 |
+---------------------------+------------+------------+------------+-----------+
+----------------------------------------------------------------------------------+
| Type: db, Result: totalRequests |
+-------------------+-----------+-----------+------------------+---------+---------+
| concurrencyLevels | fastapi | fastify | fastify-postgres | flask | http-jl |
+-------------------+-----------+-----------+------------------+---------+---------+
| 16 | 323,173 | 297,546 | 469,079 | 274,389 | 71,569 |
| 32 | 644,724 | 529,115 | 899,553 | 426,079 | 133,436 |
| 64 | 1,098,160 | 772,316 | 1,289,557 | 634,178 | 212,017 |
| 128 | 1,253,803 | 824,269 | 1,356,583 | 751,095 | 244,968 |
| 256 | 1,493,079 | 1,130,161 | 1,412,778 | 961,526 | 241,939 |
| 512 | 1,393,675 | 1,263,982 | 1,439,682 | 903,827 | 248,318 |
+-------------------+-----------+-----------+------------------+---------+---------+
+------------------------------------------------------------------------------+
| Type: update, Result: totalRequests |
+-------------------+---------+---------+------------------+---------+---------+
| concurrencyLevels | fastapi | fastify | fastify-postgres | flask | http-jl |
+-------------------+---------+---------+------------------+---------+---------+
| 16 | 716,373 | 634,709 | 726,061 | 520,125 | 160,672 |
| 32 | 420,562 | 214,406 | 172,978 | 213,297 | 33,285 |
| 64 | 271,223 | 119,172 | 91,227 | 120,104 | 16,905 |
| 128 | 177,156 | 83,380 | 60,063 | 78,563 | 11,175 |
| 256 | 129,688 | 63,871 | 46,175 | 50,472 | 8,459 |
+-------------------+---------+---------+------------------+---------+---------+
+--------------------------------------------------------------------+
| Type: json, Result: totalRequests |
+-------------------+-----------+------------+-----------+-----------+
| concurrencyLevels | fastapi | fastify | flask | http-jl |
+-------------------+-----------+------------+-----------+-----------+
| 16 | 1,083,658 | 2,761,853 | 670,526 | 1,681,566 |
| 32 | 1,825,652 | 5,617,677 | 1,163,340 | 3,167,683 |
| 64 | 3,593,179 | 8,901,280 | 2,261,998 | 4,986,692 |
| 128 | 4,372,007 | 9,130,665 | 2,520,284 | 5,294,742 |
| 256 | 8,197,688 | 10,977,348 | 5,356,670 | 5,931,912 |
| 512 | 9,917,663 | 11,663,041 | 5,696,215 | 6,382,643 |
+-------------------+-----------+------------+-----------+-----------+
+-----------------------------------------------------------------------------+
| Type: query, Result: totalRequests |
+----------------+---------+-----------+------------------+---------+---------+
| queryIntervals | fastapi | fastify | fastify-postgres | flask | http-jl |
+----------------+---------+-----------+------------------+---------+---------+
| 1 | 915,169 | 1,265,911 | 1,418,118 | 796,352 | 247,699 |
| 5 | 518,331 | 327,757 | 330,922 | 363,645 | 55,519 |
| 10 | 355,150 | 170,420 | 176,096 | 222,329 | 28,009 |
| 15 | 290,846 | 116,681 | 124,172 | 162,128 | 19,342 |
| 20 | 230,072 | 88,826 | 85,962 | 126,880 | 14,594 |
+----------------+---------+-----------+------------------+---------+---------+
+----------------------------------------------------------------------------------+
| Type: fortune, Result: latencyMax |
+-------------------+----------+----------+------------------+----------+----------+
| concurrencyLevels | fastapi | fastify | fastify-postgres | flask | http-jl |
+-------------------+----------+----------+------------------+----------+----------+
| 16 | 11.94ms | 5.28ms | 3.17ms | 16.36ms | 30.31ms |
| 32 | 14.44ms | 6.89ms | 14.11ms | 22.11ms | 82.40ms |
| 64 | 17.26ms | 24.29ms | 15.03ms | 28.59ms | 95.06ms |
| 128 | 18.03ms | 23.11ms | 19.64ms | 28.62ms | 133.23ms |
| 256 | 70.93ms | 78.35ms | 72.66ms | 78.20ms | 392.05ms |
| 512 | 197.71ms | 117.00ms | 172.46ms | 173.53ms | 531.57ms |
+-------------------+----------+----------+------------------+----------+----------+
+----------------------------------------------------------------------+
| Type: plaintext, Result: latencyMax |
+---------------------------+----------+----------+----------+---------+
| pipelineConcurrencyLevels | fastapi | fastify | flask | http-jl |
+---------------------------+----------+----------+----------+---------+
| 256 | 40.80ms | 62.20ms | 102.05ms | 4.50s |
| 1024 | 124.68ms | 148.06ms | 382.68ms | 3.88s |
| 4096 | 543.97ms | 660.62ms | 1.20s | 4.85s |
| 16384 | 1.48s | 8.00s | 2.55s | 8.00s |
+---------------------------+----------+----------+----------+---------+
+---------------------------------------------------------------------------------+
| Type: db, Result: latencyMax |
+-------------------+----------+----------+------------------+---------+----------+
| concurrencyLevels | fastapi | fastify | fastify-postgres | flask | http-jl |
+-------------------+----------+----------+------------------+---------+----------+
| 16 | 9.91ms | 6.58ms | 10.52ms | 16.57ms | 17.22ms |
| 32 | 17.21ms | 7.34ms | 14.60ms | 6.56ms | 49.28ms |
| 64 | 19.50ms | 17.32ms | 16.71ms | 13.56ms | 110.67ms |
| 128 | 15.07ms | 23.07ms | 14.88ms | 16.79ms | 92.80ms |
| 256 | 75.14ms | 66.80ms | 66.10ms | 67.44ms | 369.63ms |
| 512 | 142.55ms | 127.03ms | 140.99ms | 70.41ms | 520.01ms |
+-------------------+----------+----------+------------------+---------+----------+
+----------------------------------------------------------------------------------+
| Type: update, Result: latencyMax |
+-------------------+----------+----------+------------------+----------+----------+
| concurrencyLevels | fastapi | fastify | fastify-postgres | flask | http-jl |
+-------------------+----------+----------+------------------+----------+----------+
| 16 | 262.21ms | 190.21ms | 149.73ms | 314.95ms | 489.07ms |
| 32 | 197.06ms | 169.57ms | 207.58ms | 289.64ms | 1.36s |
| 64 | 254.84ms | 237.61ms | 294.31ms | 349.07ms | 3.03s |
| 128 | 507.05ms | 261.51ms | 304.46ms | 719.62ms | 4.59s |
| 256 | 1.93s | 347.90ms | 390.00ms | 1.55s | 5.26s |
+-------------------+----------+----------+------------------+----------+----------+
+------------------------------------------------------------+
| Type: json, Result: latencyMax |
+-------------------+---------+---------+---------+----------+
| concurrencyLevels | fastapi | fastify | flask | http-jl |
+-------------------+---------+---------+---------+----------+
| 16 | 1.76ms | 3.83ms | 13.74ms | 9.81ms |
| 32 | 13.85ms | 13.34ms | 14.34ms | 180.55ms |
| 64 | 10.94ms | 18.60ms | 13.79ms | 248.09ms |
| 128 | 13.82ms | 20.02ms | 16.43ms | 268.00ms |
| 256 | 25.00ms | 48.13ms | 28.01ms | 387.59ms |
| 512 | 31.31ms | 50.61ms | 62.14ms | 384.00ms |
+-------------------+---------+---------+---------+----------+
+-------------------------------------------------------------------------------+
| Type: query, Result: latencyMax |
+----------------+----------+----------+------------------+----------+----------+
| queryIntervals | fastapi | fastify | fastify-postgres | flask | http-jl |
+----------------+----------+----------+------------------+----------+----------+
| 1 | 240.92ms | 121.50ms | 121.01ms | 144.19ms | 508.16ms |
| 5 | 196.28ms | 103.58ms | 282.77ms | 330.64ms | 1.02s |
| 10 | 369.35ms | 115.44ms | 388.54ms | 469.51ms | 1.56s |
| 15 | 481.79ms | 148.37ms | 336.69ms | 661.66ms | 2.48s |
| 20 | 566.23ms | 157.20ms | 373.01ms | 601.39ms | 2.67s |
+----------------+----------+----------+------------------+----------+----------+
Help please! Next steps:
So, as I mentioned above, I’m not sure if I’m missing anything huge in how to deploy HTTP.jl services. Maybe there is a super smart way to push all work to non-interactive threads and keep the scheduler responsive and good, as Remove in threaded region and add a thread that runs the UV loop by gbaraldi · Pull Request #50880 · JuliaLang/julia · GitHub does. So julia needs your help in:
- Making sure the benchmark is as good as it can be (realistically - things an average user with internet and a love for reading docs would do) That includes looking for different potential culprits:
- Is the GC the bottleneck? Does the benchmark become better if the GC is off? Does it become worse in low memory settings?
- Are there gotchas I’ve missed? Am I using the connection pool correctly? Is there an initialization overhead somewhere?
- Is Julia performing better in other Hardware? This was tested in ARM (and validated from a colleague in arm64) but different hardware may have significantly different results. Please run tests and let us know if you see something different!
- Is there an easy and documented way to keep the “main” thread free to schedule away tasks?
- Identify the remaining issues
- Make a plan to address them
Julia can be as good as node at this, and even better if we consider we have more tools and primitives and cool things at our disposal! Let’s make it happen!
If enough people feel that this is a priority, we can spin off this discussion to a community call to address these issues
Speculation Zone
👆🏾👆🏾👆🏾👉🏾👉🏾👉🏾
There are a few reasons I believe these results are like what they are (NOTE: We’re in the **speculation zone** here, nothing from now on is validated/verified/official, just sharing my gut feelings):-
Unexpected `sleep` function behavior in relation to main thread activity in multithreading context · Issue #50643 · JuliaLang/julia · GitHub is really bad for HTTP services, related discussions include Bug in sleep() function - main thread work affecting sleep duration on running tasks and How to run a HTTP.jl server in parallel, while doing computations in the foreground?. It would be very interesting to test if Remove in threaded region and add a thread that runs the UV loop by gbaraldi · Pull Request #50880 · JuliaLang/julia · GitHub from Gabriel Baraldi makes this better, and how much better.
-
Julia is very bad at scheduling the TCP connection
accept
s, in relation to the default backlog (511 - even though I do raise this). Even if the scheduler was working fine (which I don’t think it is), in a high core/high tasks scenario, the “task” that accepts only gets 1/n of the time, and it only accepts one connection before it yields. A solution to that could be similar to feat(Sockets.jl): accept as many connections as possible in one loop by pankgeorg · Pull Request #1 · pankgeorg/julia · GitHub, aggressively accept as many connections as you can (up to a constant that’s proportional to the available threads?) (without blocking) and then share them to tasks/threads). -
Sockets/HTTP.jl don’t have the notion of 429/Too many connections; never give up and never know that they can’t, and shouldn’t accept new connections (see the very draft PR above for a thought on this)
HTTP doesn’t handleConnection: keep-alive
(I think?) and neither does it handle closed connections from upstream very well (these are both not validated; the symptom is a lot ofEPIPE
errors whensiege
times-out the connection, 5 seconds after it opens them) Again, 429s would help -
There is a rumor on the web that if the unaccepted connections reach 511, libuv stops listening: Mention in documentation that not accepting will disable listening · Issue #3071 · libuv/libuv · GitHub. It’s not entirely true (as the service seems to recover after a while?), but it’s alarming.
-
GitHub - iamed2/LibPQ.jl: A Julia wrapper for libpq feels to be blocking the scheduler a lot (see that the db-intensive benchmarks are collapsing after some concurrency is being added, so there may be some low hanging fruits in making the async interface with libpq better (see the intro to Multi-Threading · The Julia Language)
(heartbeat poll)
- This is consistent with my Julia services performance
- I have not had this much traffic so I don’t know
- I have had a lot of traffic (~ hundreds+ requests per second + database) and I didn’t have issues
- I am not using Julia for web services
Thanks for reading all this
– Panagiotis