Go and SQLite in the Cloud
golang.dk
golang.dk
Happy to answer any questions. Although SQLite is so dead-simple-but-awesome that you probably don't have any. :D
Anyway, hope you enjoy it and learn a thing or two.
Markus
The APIs can even support authentication and authorization with the help of JWT tokens. SQLite may not have row-level security, but even a convention (eg: if a row has user_id column, the JWT must have the same user_id value to get access to a row) would go a long way.
[0]: https://docs.datasette.io/en/latest/json_api.html#the-json-w...
[1]: https://simonwillison.net/2022/Dec/2/datasette-write-api/
https://github.com/subzerocloud/showcase/tree/main/flyio-sql...
Although I definitely would like to use SQLite just for the cost savings for something. Litefs/litestream looks great.
You would have trouble scaling up to fives of authors, though, which would be a deal breaker for any serious production app.
Expensify got 4 million request _per second_ out of a custom SQLite-based setup (on a huge machine, but still): https://blog.expensify.com/2018/01/08/scaling-sqlite-to-4m-q...
I would call those serious production apps.
Nothing fancy, just a small table that holds some blog posts with an ID, a title, some content, and a creation timestamp.
I ran the benchmark with and without WAL enabled on my Macbook Air (2020, M1) with some SSD drive inside. Results:
$ make benchmark
go test -bench=.
goos: darwin
goarch: arm64
pkg: sqlite
BenchmarkWriteBlogPost/write_blog_post_without_WAL-8 6441 191735 ns/op
BenchmarkWriteBlogPost/write_blog_post_with_WAL-8 102559 11205 ns/op
PASS
ok sqlite 3.725s
That's around 89k writes per second in parallel on all available cores with WAL enabled. I know this is a trivial setup, but adjust to your liking. You'll find that SQLite probably doesn't crash and burn with dozens of writes.Yes, I was incorrect when I said SQLite couldn't handle lots of writes quickly; what I should have said is that it can't handle lots of writes from multiple threads quickly.
Here's one for 64 parallel writes: https://gist.github.com/markuswustenberg/f35ab7e191137dca5f7...
$ make benchmark
go test -bench=.
goos: darwin
goarch: arm64
pkg: sqlite
BenchmarkWriteBlogPost/write_blog_post_without_WAL-8 100 16317415 ns/op
BenchmarkWriteBlogPost/write_blog_post_with_WAL-8 1196 1198679 ns/op
BenchmarkWriteBlogPost/write_blog_post_with_WAL_and_Go_mutex-8 58455 17461 ns/op
PASS
ok sqlite 4.557s
1e9/1198679 = 834 writes per second is still far from crash and burn territory when using just WAL mode.Of course, it gets more interesting when there are also concurrent readers, as people are trying out elsewhere in the discussion. But the point still stands: it can handle way, waaaay better than "fives of authors" in a not-at-all scary way.
Without the WAL enabled, I get a around 400req/sec.
edit: clarity
I don't know how it's done in PHP, I would think it's similar.
Go + Lambda + EFS + SQLite work great for that.
And you have had no issues writing to a single SQLite file over EFS from multiple instances?
Here is a page with quite some queries to render: https://www.chemeo.com/cid/58-801-8/Pentane
I think we’ve collectively forgotten how fast dynamic websites can be on modern hardware!
This concern is a bit overblown, the Cgo boundary is heavier than an ordinary function call but still a zillion times faster than doing disk I/O on the database.
But with sqlite you'd have to perform a hot-upgrade, surely? i.e. shut down the old version and quickly fire up the new version, with a small window of downtime in between.
Note: I see Litestream/LiteFS allows distributed deployment (both in beta).
And yes, LiteFS just selects a new leader and does a rolling deploy (or whatever else you want).
I'm curious what experience you or others have dealing specifically with the write concurrency elements of this setup, should you find it is actually an issue. I've occasionally seen mention of restructuring things to queue writes within the app (e.g. having a single writer goroutine fed with a channel), but I be interested in more details about when folks hit the point of needing to do that, and what they did (and did it help?)
I did some trivial benchmarking around this recently: https://simonwillison.net/2022/Oct/23/datasette-gunicorn/
But yeah most of the time it isn't an issue.
AFAIK, SQLite is fully and completely ACID also in WAL mode.
When synchronous is NORMAL (1), the SQLite database engine will still sync at the most critical moments, but less often than in FULL mode. There is a very small (though non-zero) chance that a power failure at just the wrong time could corrupt the database in journal_mode=DELETE on an older filesystem. WAL mode is safe from corruption with synchronous=NORMAL, and probably DELETE mode is safe too on modern filesystems. WAL mode is always consistent with synchronous=NORMAL, but WAL mode does lose durability. A transaction committed in WAL mode with synchronous=NORMAL might roll back following a power loss or system crash. Transactions are durable across application crashes regardless of the synchronous setting or journal mode. The synchronous=NORMAL setting is a good choice for most applications running in WAL mode.
https://sqlite.org/forum/info/00312b3d02bc0583
Sqlite's internal one has to work this way to support their multi-process guarantees. In comparison, an in-process Go mutex will be readied immediately.
Results vary a bit on my machine, but it's pretty much the same (unless I'm doing something wrong, please point it out if so):
$ make benchmark
go test -bench=.
goos: darwin
goarch: arm64
pkg: sqlite
BenchmarkWriteBlogPost/write_blog_post_without_WAL-8 6036 186719 ns/op
BenchmarkWriteBlogPost/write_blog_post_with_WAL-8 102567 11169 ns/op
BenchmarkWriteBlogPost/write_blog_post_with_WAL_and_Go_mutex-8 104228 11226 ns/op
PASS
ok sqlite 3.833sI'm going to clean up the code and post some results in gist, but here is a sample:
Write-only (SQL statements/s):
direct: 45314
w/mutex: 45304
Read/Write mix:
direct: read: 85229 write: 22664
w/mutex: read: 139053 write: 1382I'm interested to see your gist, but just to note this specific part seems to contradict https://news.ycombinator.com/item?id=33911491 , where at 64-wide parallelism for a write-only load, the Go mutex outperforms plain WAL by a factor of ~70x,
https://gist.github.com/markuswustenberg/f35ab7e191137dca5f7...
The built-in busy timeout basically takes care of write queueing, so a standard setup like this will take you very, very far.
Any recommendations for a macOS GUI for sqlite that comes close to Sequel Ace? I have tried DB Browser for SQLite, but that feels a bit outdated to be honest.
Concurrent readers is no problem. No concurrent writes right now.
I would only say this is a quick way to run up an application with a cheap VPS for a beginner.
Normally I'm on the side of 'do you actually get that much traffic?' but yeah, a dashboard view can generate dozens of requests alone, each with multiple queries.
If your app averages 100 req/sec then that’s 8.4M requests per day. That’s more than most applications out there.
And IMO, what you lose in the cloud layer you gain by having a deployment really close to your user. Speed of light and all that.
Plus SQLite is super cool and fun! :D
Conceptually, RDS is little more than SQLite with a clever networking layer built on top. In context, your application is also just a clever networking layer, so the middleman doesn't necessarily add any value. Of course it depends on exactly what you are trying to do and what tradeoffs you are willing to accept. There is no free lunch.
Most things will never need to be scaled up
Can you expand on that? I thought it supported multiple readers, but only a single writer. They do all have to be on the same host, though.
Since you mentioned Django - I think there are some complexities where the Python driver can't really do concurrent access if using threads instead of processes, but this is due to Python/GIL limitations, not sqlite limitations.
Our django app sits behind Gunicorn that will spin up multiple django instances, as long as Sqlite3 has WAL mode on, concurrent access (read) isn't a problem. For write, we basically use a lock.
But unless you're using that specific tool, and you're probably not unless you yourself are typing "sqlite3" into a bash shell, you're using the library rather than this query tool. And that library interacts in a similar way to a socket connect or a fopen call. You can have multiple connections open to the same sqlite database, and those connections can operate concurrently (subject to various limitations, but "you need better single core box" is not the summary)
https://www.sqlite.org/cgi/src/doc/begin-concurrent/doc/begi...
I don't think BEGIN CONCURRENT is out yet. AFAIK it's only available if you build the experimental wal2 branch yourself.
Confusingly you can't run 2 lines of Python code at the same time, but you can run 2 SQL queries (sqlite, remote server, etc) at the same time. Python threads are true pthreads, they just need to hold the GIL while running Python code. They release it when running C code or doing I/O. So you can have 2 queries running at the same time no problem, but you'll only get a single core of compute processing their results.
It's more complicated than "doesn't support multi-core" which you can see in the other replies here: generally unlimited concurrent readers are allowed with single or at least finite writers. Depending on a bunch of settings (e.g. WAL) you might also get concurrent writers, multicore sorting, and some other things that can cross cores too. Those things do have their own tradeoffs.
But my actual claim is that most things don't need to be "scaled" at all. And if you do get there, and again statistically you won't, then you're definitely going to be doing some other rearchitecting anyway and moving sqlite->postgres might as well be part of that.
If you say, sqlite3 is good enough for most webapps, then I probably agree. But I would not use it for a lot of things that I use Postgres for such as pubsub, or really high transaction rate.
We don’t all operate with unlimited VC money.
On the opposite I've seen many cases where sqlite3 scales better than postgres, depending on the scale and the application.
If you're doing horizontal scaling, sqlite3 is a very good alternative.