Ask HN: Has anybody shipped a web app at scale with 1 DB per account?
A common way of deploying a web application database at scale is to setup a MySQL or Postgres server, create one table for all customers, and have an account_id or owner_if field and let the application code handle security. This makes it easier to run database migrations and upgrade code per customer all at once.
I’m curious if anybody has taken the approach of provisioning one database per account? This means you’d have to run migrations per account and keep track of all the migration versions and statuses somewhere. Additionally, if an application has custom fields or columns, the differences would have to be tracked somehow and name space collisions managed.
Has anybody done this? Particularly with Rails? What kinda of tools or processes did you learn when you did it? Would you do it again? What are some interesting trade offs between the two approaches?
263 comments
[ 3.0 ms ] story [ 288 ms ] threadTechnically, there are DBs that let you do cross shard queries. See Azure Elastic DB.
If you want to do this for security, why not layer the DB behind some system that requires and verifies the users access tokens for each request?
The only situation where such a setup might make sense is when you actually need per-user migrations to cater to specific customer's needs, but then you'll make it very hard to work with all customer's data through a generic interface.
[1] https://www.youtube.com/watch?v=z3HZZHXXo3I
As one example, New Relic had a table per (hour, customer) pair for a long time. From http://highscalability.com/blog/2011/7/18/new-relic-architec... (2011):
> Within each server we have individual tables per customer to keep the customer data close together on disk and to keep the total number of rows per table down.
In a situation like that, all queries and table operations are customer- and time-specific anyway. At the time, dropping an entire table took less I/O than deleting specific rows in a multi-customer table (for MyISAM tables, this may still be true: https://mariadb.com/kb/en/big-deletes/). Also, there was no risk from locking the table.
https://www.slideshare.net/newrelic/how-to-build-a-saas-app-... has a bit more. I think Lew Cirne gave that presentation in 2011 but I can’t find a video of it.
In the example you gave, if the goal is to support customer-defined fields, I don't think most people would map the customer-defined fields directly to SQL columns. Consider something like Postgres hstore (with indices as needed) or the many similar implementations in other data stores.
Depending on that answer, you may be interested in using row-level security: https://www.postgresql.org/docs/current/ddl-rowsecurity.html
Easier restores if needed
Depends on the reason why you need a restore. If something botches many databases at once (because the filer holding them dies or whatever), you might be looking at a fun time restoring hundreds or thousands of databases with a playbook that was meant for one or maybe ten databases and thus isn't sufficiently automated.
Not saying that these kinds of errors are likely. But you cannot just make these assertions without the context of your actual threat model. Same for the "better separation" part. How much separation you need depends on what you're protecting against what.
I believe it will be helpful if it's necessary to separate customers into "pods" as we grow.
The main advantage I feel we get however is that it was quite easy to write a wrapper around mysqldump to retrieve data for development purposes.
I worked at a company that stored all customer data in a single database. The performance was comparable but the agility was poor. Firstly, you had to download all customer data to get a copy to debug a problem. This was a security concern I had, and eventually we had to build a serialisation format to retrieve the "slice" of data we needed. This tool needed frequent updating as new tables were added.
You might argue that we should just try to imagine the bug and recreate it but we have some pretty complicated data structures which can make investigations very hard.
In a traditional webapp backend, you have a pool of connections to the database. User01 hits your service, and grabs a connection off the pool. User02 does the same, and so on. These connections get put back in the pool for reuse once a user is done with them.
In your design, every time a user hits your service, a new connection, specific to that user, will have to be made. This will incur network traffic and the overhead of logging in to the DBMS.
If you're thinking about using something like SQLite, you will hit a hard wall when the OS isn't able to open any more file descriptors, as well.
Like you said, DB administration will be a huge pain in the ass. Rather than having Flyway or Liquidbase or whatever run a migration on one database, you'll have to run it on thousands of databases. There will be significant downtime when your databases are not in a consistent state with one another. There will also be bloat from the migration tool's record keeping, which will be duplicated for every user, rather than every database.
A lot of the tools a database gives you for free will also need to be implemented in application logic, instead. For example, you might want to run a query that says "show me every user using more than 1GB of storage," but under your schema, you'll have to log into every user's database individually, determine storage used, and add it to an in-memory list.
If you ever want to allow users to collaborate, you will end up replicating the owner_id field type metadata anyway, and the entire benefit of this schema will evaporate.
Most frameworks are not set up to handle this style of database access, either. I don't use Rails, but Spring Boot would fight you every step of the way if you tried to do this.
Also it’s still a terrible idea.
It’s be just as dumb to do it in Rust as it would be in PHP.
Skimming through the updated comments I do not see many claiming it was a good idea or successful at scale. It may work fine for 10s or even 100s of customers, but it quickly grows out of control. Trying to maintain 100,000 customer schemas and running database migrations across all of them is a serious headache.
> ...so it's likely not as dumb as you assume.
I'm not just assuming, I've tried out some of the ideas proposed in this thread and know first hand they do not work at scale. Index page caching in particular is a killer as you lose most benefits of a centralized BTREE structure when each customer has their own top level pages. Also, writing dynamic SQL to perform 100K "... UNION ALL SELECT * FROM customer_12345.widget" is both incredibly annoying and painfully slow.
Extremely few companies that sell B2B SaaS software for enterprises have 10K customers, let alone 100K (that's the kind of customer base that pays for a Sauron-looking tower in downtown SF). Service Now, Workday, etc, are publicly traded and have less than 5000 customers each.
All of them also (a) don't run a single multitenant cluster for all their customers and (b) are a massive pain in the ass to run in every possible way (an assumption, but a safe one at that!).
Rails migrations work reasonably well with apartment gem. Never had a problem with inconsistent database migrations. Sometimes a migration will fail for a tenant, but ActiveRecord migrations records that, you fix the migration, and reapply, a no-op where it's already done.
We don't use a single mysqld for every tenant mind, it's not like migrating tenants is completely serialized.
But if the idea is to isolate accounts form each other, the different schemas would be available to different DB users. You would have to re-authenticate to get access to the other DB.
The same query run across all databases should either return 1 query (for the valid tenant) and empty set for all other databases, OR it should return the same result set regardless.
I just realized what I am proposing, a hidden out of band column that is effectively the "database id" for that row.
With schemas, it's definitely possible to use the same generator for ids across schemas (at least, I'm 90% sure it is in Postgres), but you'll probably end up fighting against ORM libraries to get it to work properly (Rails for instance makes a LOT of assumptions about how the id column works), and you aren't technically guaranteed uniqueness since you'll still have distinct PK columns.
Exactly.
(It's not actually simpler when query execution over 100s of millions of rows is a perf bottleneck, and each tenant has several billion rows in the main tables. Then you're grateful you can schlep them around, and keep small tenants fast, etc. Even now, Postgres would still be a dubious choice due to the unpredictability of its query planner, though I use it for all my hobby projects.)
If connecting to your DB is significantly increasing your page times, you've got seriously fast pages. Even back when I was working with a MySQL database regularly in 2010, connect + login was 5 ms at maximum (and I think it was much less, I just don't remember that far).
> If you're thinking about using something like SQLite, you will hit a hard wall when the OS isn't able to open any more file descriptors, as well.
You could always just only keep open the most recently used 1 Million databases. It's pretty easy to tune the FD limit, FreeBSD lets you set it to one FD per 16k of ram without modifying the kernel, but it's a challenge to be so memory and cpu efficient that that's a limit.
All that said, it really depends on the application. If this is consumer facing, and each account is an end user, one database per user is probably excessive overhead; one database (or sharded, if you've got the users) or one (flatish) file per user makes a lot more sense.
If it's business facing, than one database per account could make sense. You would have isolation between customers and could put big customers on dedicated boxes and let the customer drive upgrades and migrations etc. Just please please please consider that corporations merge and divide all the time, don't be like G Suite and not offer a way to merge and divide accounts to reflect their corporate ownership.
I try to get all my endpoints under 10ms! 5ms per call would be huge.
(Obviously I can't succeed all the time, but 5ms is big numbers imo)
If your target is 10 ms, then you probably should worry about db connection time.
One of the major ways I helped Mastodon was improving their connection pooling situation. If you haven't encountered connection size issues with your database count yourself lucky.
is that with creating a new connection in the pool? we work with microservices so we have a "db gateway" so any request to the DB goes through that and it routes to the correct db server for that tenent. our latency for an already "hot" connection is about 40-50 on average but i belive the lowest number i got (for query by primary key in a kinda small table) was no less than 20-30 and opening a new connection added a couple of 10's atleast to that number
On my home system with a database I happen to be running anyway, I see:
Server is running a Celeron(R) CPU 1007U @ 1.50GHz, client is Celeron(R) 2955U @ 1.40GHz, networking is 1GBps. I don't have a super easy way to measure just the connect + login time, so this is connect + login + indexed query. The server is lightly loaded, and I warmed up the table, but it's also a laptop chip on a desktop oriented board with a lowend NIC.For instance, if you're Slack, the proposal would be to have one database per Slack workspace, not one database per person who has a login to any workspace. You absolutely need to have data relationships between multiple users in a workspace. You don't necessarily need relationships between multiple workspaces (as it happens, Slack only added this feature very recently), and having different databases means that if company 1 triggers pathological behavior on their Slack, company 2 is unaffected.
Or, if you're Wikia^WFandom, you'd have one database per site. You could certainly run one big database for the entire site, but you could also have one database for Wookieepedia, one for Memory Alpha, one for the Harry Potter Wiki, etc.
In these situations, you wouldn't have the problem about the performance hit or about making it work with frameworks - you'd run separate web application servers per workspace/site/customer, too. Some of your machines would be running Rails/Spring Boot/Django/PHP/whatever with an environment file for Wookieepedia. Some would run Memory Alpha. Ideally you'd throw this in some sort of autoscaler (anything from Apache-managed FastCGI processes to Kubernetes would work fine). But User02, when they visit Wookieepedia, would hit a Wookieepedia application server that User01 has previously used and already has a connection to the Wookieepedia DB.
Yes, you would need to deal with doing a DB migration per site/customer instead of one big migration - but on the other hand, you get to to a DB migration per customer instead of one big migration. Each one is unaffected by the others. You'd spend a bit of time automating the process of migrations and you'd hit your test sites first, and then migration is much more reliable for your customers. If you really need downtime, you can set separate downtime per customer, and if one customer has a bunch of data that takes forever to migrate, no other customer cares. It's a tradeoff, and it may be useful.
If you want to allow customers to collaborate, you need to build it as an API, as if they were two different products interacting at arms' length - but you can certainly do that.
I think their primary reasoning is that Meteor doesn't scale super easy, so it was easier to just "shard" the whole stack per customer.
Personally, it's a lot of work. It depends on what you're doing to make the tradeoffs worthwhile.
I see this as an optimization. Build everything so you can deploy once for all your customers. If you need to shard by customer later, it's just an infrastructure problem.
Additionally, like someone else pointed out, trying to run any reporting data across multiple customers will become difficult code wise and less performant.
Realistically, if you are handling the sort of scale that would require more interesting scaling solutions for typical db software, you are most certainly making enough money to implement better approaches.
FWIW, I worked for a company that was handling a few hundred thousand customers with millions of order records on a relatively small AWS RDS server. Set up a database cluster and you're rolling for a while.
Snapshotting / restoring entire accounts to a previous state was easy, and debugging data issues was also much easier when you could spin up an entire account's DB from a certain point in time locally.
We also could run multiple versions of the product on different schema versions. Useful when certain customers only wanted their "software" updated once every 6 months.
If I were to make a Salesforce competitor that’s one thing I would do differently, with tools like Kubernetes it’s a lot easier to just give every customer their own instances. Yes, it can take up more resources - but I cannot imagine the security nightmare involved with letting multiple customers execute code (even if it’s theoretically sandboxed) in the same process, plus the headache that is their database schema.
I assume most of them end up as a few rows in some set of DBs, which is far cheaper.
A full-copy sandbox past the included one for Unlimited Edition orgs is something like 30% your annual spend.
For a site I run, I have one large shared read-only database everyone can access, and then one database per user.
The per-user DB isn't the most performant way of doing things, but it made it easier to:
+ Encrypt an entire user's data at rest using a key I can't reverse engineer. (The user's DB can only be accessed by the user whilst they're logged in.)
+ Securely delete a user's data once they delete their account. (A backup of their account is maintained for sixty days... But I can't decrypt it during that time. I can restore the account by request, but they still have to login to access it).
There are other, better, ways of doing the above.
The encoded key is sent, once, in a header, and then stored in a secure session cookie, that has a reasonable timeout on it, and is user-revokable, and is encrypted in memory server-side, unless it is being accessed.
(Setting up session cookies to only decrypt when being accessed sort of required reinventing the wheel, as that's apparently not something anyone goes to the effort of usually, and sends you down some optimisation paths around timing that will have you pulling out your hair).
User-revokable session cookies are simple enough - each user gets their own session cookie key, and they can roll that over from a settings page.
Worth noting: This is a great way to decimate your server performance, because most websites aren't constantly handling decryption.
The prototype was written for Flask [0], then rewritten for Bottle [1] when it was clear I wasn't using 90% of the Flask stack, and monkeypatching most of what I was using. Nowadays it's a strange mix of Hug [3] and Bottle.
But there's nothing there that's unique to Python or even the framework. It's easily doable in just about any language. I made three prototypes when I was coming up with this batty idea, the Flask prototype, one for vibe.d (D), and one for Go. I settled on Python for no particular reason. They all had similar performance, because encryption became the bottleneck.
[0] https://flask.palletsprojects.com/en/1.1.x/
[1] http://bottlepy.org/
[2] https://hugapi.github.io/hug/
Which, of course, means "forgot my password" doesn't work.
We do it, but everyone gets the same schema and same app. .net/IIS/sql server and when we update the schema, we apply to all dbs via in-house scripts. It provides security warm fuzzies to our clients that their data is separated from other clients. If you want to try and version the app/schema for different accounts, that’s where your headaches are, regardless of db model
I currently work at a company where customers have similar concerns around data privacy, but we've been to continue using a single multitenant DB instance by using PostgreSQL's row level security capabilities where rows in a table are only accessible by a given client's database user:
https://www.postgresql.org/docs/9.5/ddl-rowsecurity.html
We customized both ActiveRecord and Hibernate to accommodate this requirement.
Another comment suggested doing queries across so many databases is challenging, and it's just not, we have both adhoc query capabilities across the fleet and a traditional data warehouse...
Now, trying to make additional infrastructure changes is challenging, but the architecture is robust enough to solve all the immediate business needs.
That said, there are some interesting challenges with this model like schema migration and DB backups etc. some of which can be easily overcome by smartly using workers and queuing. We run migration per schema using a queue to track progress and handle failures. We also avoid migrations by using Postgres JSON fields as much as possible. For instance, creating two placeholder fields in every table like metadata and data. To validate data in JSON fields we use JSONSchema extensively and it works really well.
Probably you also need to consider application caching scenarios. Even you managed to do one database per customer running Redis instance per customer will be a challenge. Probably you can run Redis as a docker container for each customer.
MySQL connections can be reused with database per tenant. Rack middleware (apartment gem) helps with managing applying migrations across all databases, and with the mechanics of configuring connections to use a tenant based on Host header as requests come in.
For async jobs in Java, allocating a connection can idiomatically only be done via a callback - e.g. `executeInTenantContext(Tenant tenant, Runnable block)` - which ensures that all connections handed out have the right tenant selected, and everything is reset to the default tenant (an empty database) when exiting. Per-tenant jobs either iterate through all tenants, or have a tenant parameter under which they get executed, and the rest of the code can be more or less tenant unaware.
It gives you the "freedom" to move tenants to separate servers, or consolidate them into single servers, if tenants are much larger or much smaller than expected. In reality this is sufficiently painful it's mostly illusory. We're looking at Vitess to help scale out, and move away from a mix of multi-tenant servers and single-tenant servers.
Scaling is super easy since you can move a db to another host.
https://rubygarage.org/blog/three-database-architectures-for...
We've never lost a single piece of data since we started using it.
https://docs.microsoft.com/en-us/azure/sql-database/sql-data...
Schema migrations are kind of a pain, we roll out changes, so on auth there is this blue/green decision.
Custom fields in EAV data-tables or jsonb data.
Backups are great, small(er) and easier to work with/restore.
Easier to move client data between PG nodes. Each DB is faster than one large one. EG: inventory table is only your 1M records, not everyone's 600M records so even sequential scan queries are pretty fast.