Live data from Hacker News

Serving a half billion requests per day with Rust and CGI

jacob.gold

61–70 of 118 posts

Re: Serving a half billion requests per day with Rust and CGI

#61

Looks like CGI was recently removed from python 3. https://docs.python.org/3/library/cgi.html What is a modern python-friendly alternative?

Python has a policy against maintaining compatibility with boring technology. We discussed this at some length in this thread the other day at https://news.ycombinator.com/item?id=44477966; many people voiced their opposition to the policy. The alternatives suggested for the specific case of the cgi module were:

- wsgiref.handlers.CGIHandler, which is not deprecated yet. gvalkov provided example code for Flask at https://news.ycombinator.com/item?id=44479388

- use a language that isn't Python so you don't have to debug your code every year to make it work again when the language maintainers intentionally break it

- install the old cgi module for new Python from https://github.com/jackrosenthal/legacy-cgi

- continue using Python 3.12, where the module is still in the standard library, until mid-02028

Re: Serving a half billion requests per day with Rust and CGI

#62
post #5

I really like the code that accompanies this as an example of how to build the same SQLite powered guestbook across Bash, Python, Perl, Rust, Go, JavaScript and C: https://github.com/Jacob2161/cgi-bin

Checked the rust version, it has a toctou error right at the start, which likely would not happen in a non-cgi system because you’d do your db setup on load and only then would accept requests. I assume the others are similar. This neatly demonstrates one of the issues with CGI: they add synchronisation issues while removing synchronisation tooling.

Had to look that up: Time-Of-Check-to-Time-Of-Use

Here's that code:

  let new = !Path::new(DB_PATH).exists();
  let conn = Connection::open(DB_PATH).expect("open db");

  // ...
  if new {
      conn.execute_batch(
          r#"
          CREATE TABLE guestbook(
So the bug here would occur only the very first time the script is executed, IF two processes run it at the same time such that one of them creates the file while the other one assumes the file did not exist yet and then tries to create the tables.

That's pretty unlikely. In this case the losing script would return a 500 error to that single user when the CREATE TABLE fails.

Honestly if this was my code I wouldn't even bother fixing that.

(If I did fix it I'd switch to "CREATE TABLE IF NOT EXISTS...")

... but yeah, it's a good illustration of the point you're making about CGI introducing synchronization errors that wouldn't exist in app servers.

Re: Serving a half billion requests per day with Rust and CGI

#63
post #53
post #8

Honestly, I'm just trying to understand why people want to return to CGI. It's cool that you can fork+exec 5000 times per second, but if you don't have to, isn't that significantly better? Plus, with FastCGI, it's trivial to have separate privileges for the application server and the webserver. The CGI model may still work fine, but it is an outdated execution model that we left behind for more than one reason, not j…

Serverless is a marketing term for CGI, and you can observe that serverless is very popular. A couple of years ago my (now) wife and I wrote a single-event Evite clone for our wedding invitations, using Django and SQLite. We used FastCGI to hook it up to the nginx on the server. When we pushed changes, we had to not just run the migrations (if any) but also remember to restart the FastCGI server, or we would waste ti…

> Serverless is a marketing term for CGI, and you can observe that serverless is very popular.

No, it's not.

CGI is Common Gateway Interface, a specific technology and protocol implemented by web servers and applications/scripts. The fact that you do a fork+exec for each request is part of the implementation.

"Serverless" is a marketing term for a fully managed offering where you give a PaaS some executable code and it executes it per-request for you in isolation. What it does per request is not defined since there is no standard and everything is fully managed. Usually, rather than processes, serverless platforms usually operate on the level of containers or micro VMs, and can "pre-warm" them to try to eliminate latency, but obviously in case of serverless the user gets a programming model and not a protocol. (It could obviously be CGI under the hood, but when none of the major platforms actually do that, how fair is it to call serverless a "marketing term for CGI"?)

CGI and serverless are only similar in exactly one way: your application is written "as-if" the process is spawned each time there is a request. Beyond that, they are entirely unrelated.

> A couple of years ago my (now) wife and I wrote a single-event Evite clone for our wedding invitations, using Django and SQLite. We used FastCGI to hook it up to the nginx on the server. When we pushed changes, we had to not just run the migrations (if any) but also remember to restart the FastCGI server, or we would waste time debugging why the problem we'd just fixed wasn't fixed. I forget what was supposed to start the FastCGI process, but it's not running now. I wish we'd used CGI, because it's not working right now, so I can't go back and check the wedding invitations until I can relogin to the server. I know that password is around here somewhere...

> A VPS would barely have simplified any of these problems, and would have added other things to worry about keeping patched. Our wedding invitation RSVP did need its own database, but it didn't need its own IPv4 address or its own installation of Alpine Linux.

> It probably handled less than 1000 total requests over the months that we were using it, so, no, it was not significantly better to not fork+exec for each page load.

> You say "outdated", I say "boring". Boring is good. There's no need to make things more complicated and fragile than they need to be, certainly not in order to save 500 milliseconds of CPU time over months.

To be completely honest with you, I actually agree with your conclusion in this case. CGI would've been better than Django/FastCGI/etc.

Hell, I'd go as far as to say that in that specific case a simple PHP-FPM setup seems like it would've been more than sufficient. Of course, that's FastCGI, but it has the programming model that you get with CGI for the most part.

But that's kind of the thing. I'm saying "why would you want to fork+exec 5000 times per second" and you're saying "why do I care about fork+exec'ing 1000 times in the total lifespan of my application". I don't think we're disagreeing in the way that you think we are disagreeing...

Re: Serving a half billion requests per day with Rust and CGI

#64
post #48
post #45

Earlier quoted context omitted.

How do you protect against concurrency bugs when two visitors make guestbook entries at the same time? With a lockfile? Are you sure you won't write an empty guestbook if the machine gets unexpectedly powered down during a write? To me, that's one of the biggest benefits of using something like SQLite.

That is exactly what I do, and it works well enough because if the power-loss were to happen, I wouldn't have lost anything of crucial value. But that is admittedly a very instance-specific advantage I have.

There's a fsync/close/rename dance that ext4fs recognizes as a safe, durable atomic file replacement, which is often sufficient for preventing data loss in cases like this.

Re: Serving a half billion requests per day with Rust and CGI

#65
post #5

I really like the code that accompanies this as an example of how to build the same SQLite powered guestbook across Bash, Python, Perl, Rust, Go, JavaScript and C: https://github.com/Jacob2161/cgi-bin

Checked the rust version, it has a toctou error right at the start, which likely would not happen in a non-cgi system because you’d do your db setup on load and only then would accept requests. I assume the others are similar. This neatly demonstrates one of the issues with CGI: they add synchronisation issues while removing synchronisation tooling.

[deleted]

Re: Serving a half billion requests per day with Rust and CGI

#66
post #62

Earlier quoted context omitted.

Checked the rust version, it has a toctou error right at the start, which likely would not happen in a non-cgi system because you’d do your db setup on load and only then would accept requests. I assume the others are similar. This neatly demonstrates one of the issues with CGI: they add synchronisation issues while removing synchronisation tooling.

Had to look that up: Time-Of-Check-to-Time-Of-Use Here's that code: let new = !Path::new(DB_PATH).exists(); let conn = Connection::open(DB_PATH).expect("open db"); // ... if new { conn.execute_batch( r#" CREATE TABLE guestbook( So the bug here would occur only the very first time the script is executed, IF two processes run it at the same time such that one of them creates the file while the other one assumes the fil…

That sounds correct to me, but I think I would apply your suggested fix.

Re: Serving a half billion requests per day with Rust and CGI

#67
post #46

Earlier quoted context omitted.

Easy uploading of new versions is a good point, and I agree that the likely security holes in the bash script are less of a concern if only trusted users have access to it. However, about 99% of embedded devices lack an MMU, much less 50K of storage, which makes it hard to run Unix shells on them.

Busybox runs MMU-less and has ash built in. It also has a web server! It can be a little chonky but you can remove unneeded components. Things like wireless routers and other devices that have a decent amount of storage are a good platform for it

Yeah, a lot of wireless routers would have no trouble. A lot of them do in fact have MMUs. I wonder if you could get Busybox running on an ESP32? Probably not most 8051s, though, or AVR8s.

Re: Serving a half billion requests per day with Rust and CGI

#68
post #56
post #36

Earlier quoted context omitted.

The part of the execution model that is dated is this: > having the web server execute stuff in a specific folder inside the document root just seems like a recipe for problems

What kind of problems? Like, if the administrator put something inside that directory (Unix doesn't have folders) that the web server shouldn't execute? That kind of problems? I've literally never had that problem in my life and I've had web pages for 30 years.

> Like, if the administrator put something inside that directory

Path traversal bugs allowing written files to land in the cgi-bin used to be a huge exploit vector. Interestingly, some software actually relied on being able to write executable files into the document root, so the simple answer of making the permissions more limited is actually not a silver bullet.

If you've never seen or heard of this, ¯\_(ツ)_/¯

> Unix doesn't have folders

Great and very important point. Someone should go fix all of these bugs:

https://github.com/search?q=repo%3Atorvalds%2Flinux%20folder...

Re: Serving a half billion requests per day with Rust and CGI

#69
post #18
post #8

Honestly, I'm just trying to understand why people want to return to CGI. It's cool that you can fork+exec 5000 times per second, but if you don't have to, isn't that significantly better? Plus, with FastCGI, it's trivial to have separate privileges for the application server and the webserver. The CGI model may still work fine, but it is an outdated execution model that we left behind for more than one reason, not j…

Having completely isolated ephemeral request handlers with no shared state and no persistent runtime makes very clean and nice programming model. It also makes deployments simple because there is no graceful shutdown or service management to worry about; in simplest case you can just drop in new executables and they will be automatically taken into use without any service interruption. Fundamentally CGI model allows…

> there is no ... service management to worry about

Service management:

    systemctl start service.app
or

    docker run --restart=unless-stopped --name=myservice myservice:version
If it isn't written as a service, then it doesn't need management. If it is written as a service, then service management tools make managing it easy.

> there is no graceful shutdown ... to worry about

Graceful shutdown:

    kill -9
or

    docker kill -9 myservice
If your app/service can't handle that, then it's designed poorly.

Re: Serving a half billion requests per day with Rust and CGI

#70
We used CGI to add support for extensions in Disco (https://disco.cloud/).

It's so simple and it can run anything, and it was also relatively easy to have the CGI script run inside a Docker container provided by the extension.

In other words, it's so flexible that it means the extension developers would be able to use any language they want and wouldn't have to learn much about Disco.

I would probably not push to use it to serve big production sites, but I definitely think there's still a place for CGI.

In case anyone is curious, it's happening mostly here: https://github.com/letsdiscodev/disco-daemon/blob/main/disco...

Post reply on HN