There are a lot of headaches involved with the "tenant per schema" approach. Certainly it was nice to never have to worry about the "customer is seeing data from another customer" bug (a death knell if you're in enterprisish B2B software), but it added so many problems:
- Migrations become a very expensive and time-consuming process, and potentially fraught with errors. Doing continious-deployment style development that involves database schema changes is close to impossible without putting a LOT of effort into having super-safe migrations.
- You'll run into weird edge cases due to the fact that you have an absolutely massive schema (since every table you have is multiplied by your number of tenants). We had to patch Rails to get around some column caching it was doing.
- Cloud DB hosting often doesn't play nice with this solution. We continually saw weird performance issues on Heroku Postgres, particularly with backup / restores (Heroku now has warnings against this approach in their docs)
- It doesn't get you any closer to horizontal scalability, since connecting to a different server is significantly different than connecting to another schema.
- It will probably push the need for a dedicated BI / DW environment earlier than you would otherwise need it, due to the inability to analyze data cross-schema.
I still think there's maybe an interesting approach using partioning rather than schemas that eliminates a lot of these problems, but apartment probably isn't the library to do it (for starters, migrations would be entirely different if partioning is used over schemas)
Is there something between the two solutions or pieces that could be modified that collapse the problem?
What about going with DB account per app account and using views to limit exposure to data. If user level views are applied before the business logic has access, then the death knell scenario should be avoided.
Migrations seem hard regardless, they have to be automated anyway and have a verified rollback/undo mechanism. Code should be multiversion aware if you are doing live upgrades.
I am personally a fan of read-only mode while doing the last phase of an ETL upgrade on the backend. Snapshot databases, run a bunch of Hive jobs and throw it back down on a new set of DBs.
In the cases you outlined where it was dangerous or fragile, I think those systems were already that way, and that fragility is a system of a different problem. Fix that and lots of other stuff disappears.
- Migrations will first of all just take longer - you're multiplying the number of schema changes by the number of tenants you have.
- While in an ideal world migrations should be purely run within a transaction, occasionally performance considerations mandate that you run without DDL transactions - when some tenants fail and your migrations are in a partially completed state for some of your tenants and not others, it can be scary and painful.
- In my experience, almost no one approaches data migrations in a way that is purely backwards compatible 100% of the time without exception. You certainly can, but there's a significant tax associated with this, and if you're in a traditional scoped environment, you can often get away with the potential for errors in the minuscule time that a schema change is operating (of course, some schema changes aren't run in minuscule times, but those are the ones you're more likely to plan for)
Going read only during migrations is an interesting approach, but there's real business costs associated with that (particularly if your migration speed is multiplied by running it across tenants).
I don't want to say that you should never isolate data on a schema level, but I do think it's something that shouldn't be a standard tool to reach for. For the vast majority of companies, the costs outweigh the benefits in my mind.
Instead of a DB per tenant, or a table per tenant, just add a TenantId column on every table from day 1.
Have a tenant_id column in every table.
This gives me flexibility to either host each client separately or club them together.
Edit: on reading the link you included, it seems like a lot of the problems are on the Rails implementation of the architecture with ActiveRecord and Apartment rather than with the architecture itself.
What if you need to release some feature, but that feature requires a database feature enabled? Normally you enable it once, in a transaction hopefully, and then roll out your application. With this style you have to wait for N database servers to connect, enable, validate, then go live before you can even attempt the application being deployed, much less if you get it wrong.
As I say I’m no expert but this is what I’ve done previously.
It becomes a non-issue if you're able to go down for a few moments. Not necessarily viable for some software though, it just depends on what you're running.
(Especially because you can apply the migration on slave and subsequently switch master, making this downtime as quick as the time your system needs to do a database switch.)
This doesn't apply to big services with millions of users, however.
The parent mentioned having to support ~150 B2B customers, so the effort is amplified x100 — more than 100 individual customers databases have to be coddled as you’ve described, albeit they are stuck with poor tooling to manage changes across their unusual architecture.
We have written our own tool to upgrade the DB schema, it's nothing fancy, just takes an XML description, compares with the current DB and makes changes. However it ensures the upgrade process is automated.
New versions of our application come with a new DB schema, and we also have an auto-updater for our software. We have a small launcher application which allows the users to select a few older versions of our software, so upgrades are almost always safe in the sense that if the user encounters a bug with the new version, they can try in the previous one.
If the DB schema upgrade tool fails for some reason, the application upgrade will be held back and we'll get a notification.
Combined this allows us to be quite aggressive with pushing out new versions, and to do so without manual intervention.
A limitation with this setup is that DB changes have to be backwards compatible. So we do have a few cases of "if this table exists do this, else do that" type logic and similar, but mostly it's not a big deal.
For example I recently had to refactor some old table where they had used five columns for some additional references (ref_1, ref_2 etc), into a new table of additional references. To handle this I just made a new view which would return either the refs from the new table, or the data from the columns in case the new table didn't have any data for that record.
I then changed the application to use a grid instead of five separate edit controls, and to use this new view. If the user had to use an older version, that version would just write to the five fields and not populate a row in the new table, and the view would then return this data if the user later viewed it in the new version of our application.
So the "overhead" of backwards compatibility in this case was just ensuring this would be sufficient and writing that view, so just a few minutes.
I managed a group that had a dozen customers on a single multi-tenant database and some migrations took more than a day - we needed to set maintenance windows on weekends (and once we had to roll back after an error).
Having a dozen databases we could roll out updates to on a client per client basis would have saved us from some enormous headaches.
OTOH, no two companies are alike. You do you.
If you get successful enough in this type of application space, you reach a mature state in which you need to be able to:
* Run large dedicated instances for your largest customers, because either their performance or security requirements mandate it.
* Share resources among a large number of smaller customers, for efficiency and fault tolerance reasons.
You can get there in two ways:
* You start with a massive multi tenant application, and you figure out a way to shard it and separate in pieces later.
* You start with multiple small applications, and you develop the ability to orchestrate the group, and scale the largest of them.
I would argue the latter is more flexible and cost efficient, and requires less technical prowess.
This is not true if your primary keys are int or bigint.
It's also not true if you have any sort of unique indexes that are scoped to a table.
For example, HubSpot runs a multitenant system. URLs look like:
https://app.hubspot.com/section/$ACCOUNT_ID/etc/etc
In the simple, YAGNI implementation of this, when you create a new HubSpot account, most likely that will insert a new row into the accounts table, and the auto generated ID of that row will be your account ID. Therefore you need uniqueness to be enforced at that level.
If you want to start running a separate copy of the system, you need to refactor the system to move that sequence out of the database so that two different customers running on different clusters don't end up with the same account ID. This is just an example, but there are many problems like this that are caused by the assumption that the production system is a single unique system.
https://${customer}.hubspot.com/... https://app.hubspot.com/${customer}/...
You'd do this at the proxy/forwarder level.
In my experience by the time you reach this point you have a lot of operational complexity because you and your team are used to your production cluster being a single behemoth, so chances are it's not easy to stand up a new one or the overhead for doing so is massive (i.e. your production system grew very complex because there is rarely if ever a need to stand up a new one).
Additionally, a multi tenant behemoth might be full of assumptions that it's the only system in town therefore making it hard to run a separate instance (i.e. uniqueness constraints on names, IDs, etc).
That's hard enough and then add to it that most clients want to BYOK to those instances
My pushback focuses on two things:
1: The Rails implementation of this, specifically with off the shelf gems and integration with ActiveRecord. Presents a lot of unknown framework complexity down the road. We are currently running on a fork of the Apartment gem as it breaks under newer versions of Rails.
2: Long term support for evolving business case. Now, this is very much a problem unique to our company and how our business/product has evolved. We started out with a very clear separation of account concerns, and as we've grown and pivoted things have changed. You are unlikely to experience the same evolution we have. HOWEVER, all startups face these unknowns, and giving yourself as much flexibility early in the game will pay off down the road. There are no right answers here, you just have to think it through and weigh the costs/benefits. Maybe for you that flexibility is found in single tenant. For us it appears in hindsight that multi-tenant would have been the best long term choice.
https://github.com/bernardopires/django-tenant-schemas
All I can say, that it was a nightmare to do deployment and migration and some backward incompatibility features restrained us in some ways.
The company valued data isolation as a priority rather than ease of developing it. Hard to work with but great for data isolation.
What DB are you using?
How mature are your automation tools for managing these DB instances?
Do you use a “single” DB server instance with a database defined per customer?
Do you require all your customers to have identical schema, and do upgrade them all irrespective of individual customer concerns?
Thanks in advance!
This architecture decisions is one of my biggest regrets, and we are currently in the process of rebuilding into a single database model.
FWIW, this process has worked well for what it was originally intended to do. Data-security has a nice db level stopgap and we can keep customer data nicely isolated. It's nice for extracting all data from a single customer if we have extended debugging work or unique data modeling work. It saves a lot of application layer logic and code. I'm sure for the most part it makes the system slightly faster.
However as we have grown this has become a huge headache. It is blocking major feature refactors and improvements. It restricts our data flexibility a lot. Operationally there are some killers. Data migrations take a long time, and if they fail you are left with multiple databases in different states and no clear sense of where the break occurred.
Lastly, if you use the Apartment gem, you are at the mercy of a poorly supported library that has deep ties into ActiveRecord. The company behind it abandoned this approach as described here: https://influitive.io/our-multi-tenancy-journey-with-postgre...
Happy to expand on this if anybody is interested. It's currently a cause of major frustration in my life.