« BackRearchitecting: Redis to SQLitewafris.orgSubmitted by thecodemonkey 8 hours ago
  • simonw 4 hours ago

    I’m really interested in this model where each application server has a copy of a SQLite database file which is then replaced on a scheduled basis.

    Here it’s being used for web application firewall rules.

    Another place I’ve thought about using this is feature flag configuration. Feature flags can be checked dozens of times per request and often need the kind of queries (user is a member of group A and has an IP located in country B) which could be well served by a local SQLite - and feature flags have a tolerance for updates taking a few seconds (or longer) to roll out.

    • supriyo-biswas 3 hours ago

      > I’m really interested in this model where each application server has a copy of a SQLite database file which is then replaced on a scheduled basis.

      BTW, this is also the model used by all CDNs, where the global configuration file containing the certificates, HTTP routing rules etc. for all customers will be updated into into a single-file b-tree structure*, and that "bundle" is distributed among all edge locations frequently.

      * I'm yet to see someone use sqlite for this purpose, it's usually DBM style databases like LMDB or Kyoto Cabinet.

      • quesera 2 hours ago

        > Feature flags can be checked dozens of times per request

        My strategy for resolving this is to fetch the flag value once, but to store it in the request object, so that a) you never have to take the expensive lookup hit more than once per request, and b) there's no risk of an inconsistent value if the flag is updated mid-request.

        • jitl an hour ago

          Where is the “session object” stored?

          • quesera 19 minutes ago

            Apologies, I meant "request object". Corrected above.

          • CraigJPerry an hour ago

            What’s the use case for re-checking the same feature flag in a single session?

            I can see why you need to check multiple different flags in a session and I understand the parent point about looking in SQLite for them (effectively a function call into a library in process address space rather than a call over the network for each flag).

            • quesera 15 minutes ago

              Sorry, s/session/request/g; corrected above.

              One example is a multistep transaction processing request. The feature flag could gate several branch points.

              A memory-mapped SQLite file is great too, but the strategy I describe above is less code, quicker to implement, and should get you a very similar performance improvement.

          • jitl an hour ago

            We used this model to distribute translations, feature flags, configuration, search indexes, etc at Airbnb. But instead of SQLite we used Sparkey, a KV file format developed by Spotify. In early years there was a Cron job on every box that pulled that service’s thingies; then once we switched to Kubernetes we used a deamomset & host tagging (taints?) to pull a variety of thingies to each host and then ensure the services that use the thingies only ran on the hosts that had the thingies.

            In Ruby we called this “hammerspace” https://github.com/airbnb/hammerspace

            • nnf an hour ago

              I've wanted to implement this on a distributed web server environment I manage. Right now there's a centralized MySQL database that the web servers read from when rendering a web page, but there can be lots of queries for a single render (page, sections, snippets, attributes, assets, etc.), and sending that all over the wire, while fast, is slower than reading from a database running on the same host. It'd be great to be able to copy the "master" database onto each web server instance, maybe once per minute, or just on-demand when a change to the data is made. I imagine this would make reads much faster.

              • michaelbuckbee 3 hours ago

                SQLite for distribution is neat. FWIW - this is at least partially inspired by your datasette project which we may still try and do something with later on the reporting and data exploration side of things.

                • bob1029 2 hours ago

                  > a SQLite database file which is then replaced on a scheduled basis.

                  You could look into WAL replication if you wanted an efficient way to update the copies. Something like Litestream.

                  • chipdart an hour ago

                    > Feature flags can be checked dozens of times per request and often need the kind of queries (user is a member of group A and has an IP located in country B) which could be well served by a local SQLite - and feature flags have a tolerance for updates taking a few seconds (or longer) to roll out.

                    This doesn't sound right. A feature flag only requires checking if a request comes from a user that is in a specific feature group. This is a single key:value check.

                    The business logic lies in assigning a user to a specific feature group, which the simplest way means pre assigning the user and in the most complex cases takes place at app start/first request to dynamically control dialups.

                    Either way, it's a single key: value check where the key is user ID+feature ID, or session ID + feature ID.

                    I mean, I guess you can send a boat load of data to perform the same complex query over and over again. I suppose. But you need to not have invested any thought onto the issue and insisted in making things very hard for you, QAs, and users too. I mean, read your own description: why are you making the exact same complex query over and over and over again, multiple times in the same request? At most, do it once, cache the result, and from therein just do a key:value check. You can use sqlite for that if you'd like.

                    • nnf an hour ago

                      GP's comment is talking about checking multiple feature flags, not checking a single feature flag multiple times.

                      • chipdart an hour ago

                        GP referred specifically to queries checking if "user is a member of group A and has an IP located in country B".

                        The number of feature flags is irrelevant. In fact, the feature flag and A/B testing services I used always returned all default treatment overrides in a single request.

                  • aquilaFiera 3 hours ago

                    Somewhat related: for the Neon internal hackathon a few weeks ago I wrote a little Node.js server that turns Redis's wire protocol (RESP) into Postgres queries. Very fun hack project: https://github.com/btholt/redis-to-postgres

                    • keybits 2 hours ago

                      People reading this might be interested in Redka - Redis re-implemented with SQLite in Go: https://github.com/nalgeon/redka

                      • meowface an hour ago

                        Was interested and considering switching until I saw this part:

                        >According to the benchmarks, Redka is several times slower than Redis.

                        Still a cool project, don't get me wrong. But this kind of doesn't give me any incentive to switch.

                        • anonzzzies 40 minutes ago

                          We (keydb users; it's much faster than redis for all our cases) use redka for our dev machines; we develop everything on sqlite so there is no install of anything and in prod, we just switch to our mysql, clickhouse, redis etc cluster and it all works while having a light experience for dev.

                      • matharmin 4 hours ago

                        It sounds like a niche use case where SQLite does work quite well server-side without needing any replication, since the database is read-only.

                        Other alternatives may use static files loaded in-memory, but I'm guessing the data is more than you'd want to keep in memory in this case, making SQLite a nice alternative.

                        • michaelbuckbee 4 hours ago

                          (article author here) - yes 100% and I hope that came through in the article that this is great solution given our particular use case and that it's not a 1:1 swap out of Redis or Postgres.

                          • chipdart an hour ago

                            > Other alternatives may use static files loaded in-memory, but I'm guessing the data is more than you'd want to keep in memory in this case, making SQLite a nice alternative.

                            Ultimately a RDBMS like SQLite is what you'd get if you start with loading static files into memory and from that point onward you add the necessary and sufficient features you need to get it to work for the most common usecases. Except it's rock solid, very performant, and exceptionally tested out.

                          • codingbot3000 2 hours ago

                            It's posts like this explaining architecture decisions in detail I am reading HN for. Thank you!

                            • macspoofing 3 hours ago

                              >While Redis is "fast" in comparison to traditional RDBMS, it's still a database that you have to manage connections, memory, processes, etc., which introduces more brittleness into the stack (the opposite of what we're trying to achieve).

                              Every database, Relational or Nonrelational, requires approximately the same level of management and maintenance when you start dealing with non-toy levels of transactions.

                              The "Fast" part is a little funny. If you don't care about joins, then row inserts and retrievals are pretty damn fast too =)

                              • chipdart an hour ago

                                > The "Fast" part is a little funny. If you don't care about joins, then row inserts and retrievals are pretty damn fast too =)

                                What makes SQLite exceptionally fast in a server environment is that you do not require a network call to do the query or even retrieve the data. Your remarks about joins and transactions are meaningless once you understand you're just reading stuff from your very own local HD, which is already orders of magnitude faster.

                                • gwbas1c 2 hours ago

                                  SQLite has its vacuum operation, which is kind-of like running a garbage collection. Every time I read the docs about when to run a vacuum, I end up confused.

                                  The last time I shipped an application on SQLite, I ended up just using a counter and vacuuming after a large number of write operations.

                                  • prirun an hour ago

                                    HashBackup author here, been using SQLite for about 15 years.

                                    Doing a vacuum after a large number of deletes might make sense. The only real purpose of vacuum IMO is to recover free space from a database. Vacuum may also optimize certain access patterns for a short while, though I have never tested this, and it would be highly dependent on the queries used. If fragmentation is a bigger concern for you than recovering free space, you can also compute the fragmentation to decide whether to vacuum by using the dbstat table:

                                    https://www.sqlite.org/dbstat.html

                                    Then again, computing this will require accessing most of the database pages I'm guessing, so might take nearly as long as a vacuum. The other gotcha here is that just because db pages appear to be sequential in a file doesn't mean they are sequential on a physical drive, though filesystems do strive for that.

                                    SQLite has pragma commands to tell you the number of total and free db pages. When the percentage of free pages is greater than x% and it's a convenient time, do a vacuum. For a highly volatile db, you can add a table containing this percentage, update it every day, and make your decision based on an average, but IMO it's easier just to check for more than 50% free (or whatever) and do the vacuum.

                                    Vacuums used to be (circa 2019) pretty slow operations, but the SQLite team has sped them up greatly since then. Vacuuming a 3GB SQLite db on a SSD takes less than a minute these days. That's with the db 100% full; with only 50% used pages, it would be considerably faster.

                                    Vacuums are done in a statement transaction, so you don't have to worry about a "half vacuum that runs out of disk space" screwing up your database.

                                • dangoodmanUT 3 hours ago

                                  I have a hard time believing that Redis local was beat by SQLite local unless the workload was poorly fit for Redis structures, or the integration code wasn't well written.

                                  But always happy to see a discovery of a better solution. I agree removing the network is a win.

                                  • michaelbuckbee 3 hours ago

                                    In Redis, the data is a sorted-set that we forced into being lexicographically ordered by setting all the scores to 0. We went through a lot of iterations of it and to be clear it's not _slow_ it's just not as fast as essentially `fopen`

                                    1 - Redis sorted sets - https://redis.io/docs/latest/develop/data-types/sorted-sets/

                                    • epcoa 2 hours ago

                                      I do agree it is somewhat fishy of the large performance difference not being explained by comparatively fundamentally poor data access patterns.

                                      However, Redis runs as an out of process server with marshaling and unmarshaling of data across sockets. SQLite is in process and with a prepared query is basically one library call to a purpose built data access VM. So I’m not sure why it would be hard to believe this cache and TLB friendly setup can beat Redis.

                                    • ten13 7 hours ago

                                      Nice post! I’m curious how the SQLite-per-instance model works for rate-limiting in the scale-out scenario. I took a cursory glance at the docs but nothing jumped out at me about how it works.

                                      • michaelbuckbee 4 hours ago

                                        Post author and Wafris co-founder here. Conceptually "rate limiting to prevent abuse" (what we're doing here) and "rate limiting for API throttling" have different levels for tolerance.

                                        With that in mind, it's setting higher levels of limiting and doing the math to push that out over many machines/instances/dynos. That helps for things like scraping prevention, etc.

                                        For issues like credential stuffing attacks, you'd want a lower limit but also coupled with mitigations like IP bans, IP reputation, etc. to deal with underlying issue.

                                      • gwbas1c 2 hours ago

                                        How large is the SQLite database you're syncing?

                                        Is it even "worth" using SQLite at this point? What about a configuration file, and straight-up code that works with in-memory data structures?

                                        • michaelbuckbee 2 hours ago

                                          This is something we seriously considered. The SQLite dbs are several hundred megabytes in size (millions of IP ranges) so while it would be technically doable to send around rules files as JSON or something more specifically suited there's still a number of wins that SQLite gives us:

                                          - Really strong support across multiple platforms (we have clients for most of the major web frameworks)

                                          - Efficiency, sure we have lots of RAM on servers nowdays but on some platforms it's constrained and if you don't have to burn it, we'd just rather not.

                                          - When we started mapping this out, we ended up with something that looked like a JSON format that we were adding indexes to....and then we were re-inventing SQLite.

                                          • wormlord 39 minutes ago

                                            I don't know how it works exactly, but I believe you can have a fully in-memory SQLite database. Bun's sqlite library and SqlAlchemy both let you operate on in-memory SQLite db's which you can then write to disk.

                                            Edit: reading the docs it looks like it operates the same way, just reading sections of the db from memory instead of disk

                                            https://www.sqlite.org/atomiccommit.html

                                          • masfoobar 3 hours ago

                                            NICE!

                                            I have not used Redis myself, but have been using Sqlite more and more over the years.. and found a perfect application I wrote using Sqlite under the hood.

                                            Powerful and convienient database system!

                                            • ragu4u 3 hours ago

                                              So is the sqlite file on disk or in memory somehow?

                                              • michaelbuckbee 3 hours ago

                                                The sqlite db is on disk sync'd down to the clients from our service.

                                                The client is responsible for checking with our servers and, if rule updates are found, downloading a new database file. To avoid locking and contention issues, these are each uniquely named, and which DB is "current" is just updated.

                                                Note: This is only in "managed" mode. If you'd rather, you can distribute a SQLite database of the rules alongside your app.

                                                • TheDong an hour ago

                                                  > on disk or in memory somehow?

                                                  Due to the magic of the page cache, the answer to that can be "both".

                                                  If the sqlite database is being read often and not being written, the page cache will be valid and reads will pretty much never go to the filesystme.

                                                • rini17 8 hours ago

                                                  If you need writes, can just use second sqlite database.

                                                  • HelloNurse 3 hours ago

                                                    It would be a small command log (a batch of requested changes from that client) with a completely different schema from the main database.

                                                    But if we are sending deltas to a central server performance isn't critical: there can be a traditional web service to call, without uploading databases.

                                                  • justinclift 3 hours ago

                                                    Wonder if they had indexes on their SQLite tables?

                                                    Not seeing a mention of that in the article.

                                                    • michaelbuckbee 3 hours ago

                                                      The answer is "yes." We had indexes - but it's also a little more complicated than that, as we're storing IPv4 and IPv6 ranges in a single table in a format _designed_ to be indexed a particular way.

                                                      In the article, we refer to this as "decimal lexical" formatting, where we're taking the IPs and making them integers but actually treating them as strings. We're doing this in both Redis with sorted sets and then in a single table in SQLite.

                                                      I was going to explain all this in the article, but it was too long already, so it will be a future blog post.

                                                      • filleokus 2 hours ago

                                                        Really great article and I really appreciate seeing this "flavour" of "distributed" sqlite, think it can be useful in many no/low-write scenarios.

                                                        But about the formatting of the data, is it completely inherent to the rest of the system / unchangeable? Spontaneously I would have guessed that for example a bitfield in redis would have performed better. Did you test any other formattings?

                                                        • epcoa 2 hours ago

                                                          Curious, what is the advantage of decimal? Why not base-64 or some larger and power of 2 base?

                                                          • a12b 2 hours ago

                                                            You should definitely write an article with all tricks you used to make it fast!