mirror of
https://github.com/dani-garcia/vaultwarden.git
synced 2026-02-05 00:29:40 +03:00
N+1 query problem on api/sync #938
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Originally created by @raphaelcoutu on GitHub (Feb 18, 2021).
Subject of the issue
Syncing/Loading ciphers with a remote PostgreSQL can be really slow (~ 50-60 seconds) due to a very large amount of SQL queries. The problem doesn't seem to affect users who are using SQLite or docker/local SQL databases due to no/minimal latency roundtrips. Browser extension could simply not sync.
Your environment
I have about ~300 ciphers in my Bitwarden wallet. My remote PostgreSQL server is about 30-50 ms from my local docker Bitwarden_rs setup (for testing purposes).
Steps to reproduce
I used different setups to identity the problem. I have no problem with local SQLite or docker PostgreSQL.
I finally found the problem using a PostgreSQL log configuration in postgresql.conf. There were a lot of requests written for a single sync (2400 lines so I estimate about 1000 requests):
Docker-compose used:
Expected behaviour
Should load faster (~up to 5-10 seconds could be acceptable)
Actual behaviour
Really slow. (~1 min)
Relevant code blocks
(I don't usally code in Rust but I found the place where we might start looking) Is there a way to use query joins instead of doing many requests?
d69be7d03a/src/api/core/ciphers.rs (L86-L106)d69be7d03a/src/db/models/collection.rs (L60-L67)d69be7d03a/src/db/models/collection.rs (L227-L247)etc.
d69be7d03a/src/db/models/cipher.rs (L82-L85)@jjlin commented on GitHub (Feb 19, 2021):
You're welcome to investigate if there's a clean way to reduce the number of queries, but otherwise it's a relatively niche use case that I don't think the main contributors are willing to spend much time on.
@raphaelcoutu commented on GitHub (Feb 19, 2021):
Yes, sure, I'll try to investigate this by my own! I just wanted to flag this problem so people who are used to code in rust can help me find a cleaner way of doing those requests!
@dislazy commented on GitHub (May 18, 2021):
I also encountered the same problem, can I consider introducing a caching mechanism?
@jjlin commented on GitHub (May 18, 2021):
Feel free to propose something, but I suspect that there won't be a clean solution to this that isn't effectively a rewrite of the project.
@BlackDex commented on GitHub (May 18, 2021):
Also, keep in mind that some people are running multiple vaultwarden instances with one database, which makes caching done internally a bit of a pain. You then would need to go to tools like redis or memcached or something, which i doubt would be beneficial to the project.
@attie commented on GitHub (Nov 3, 2021):
I'm not sure if I'm seeing this effect too, or if it's unrelated... I've just migrated from the official Bitwarden server to Vaultwarden (docker, v1.23.0, SQLite database).
I have around 400 items in my vault, and the syncs are noticeably slower than before...
I will try to dig further when I get the chance.
@BlackDex commented on GitHub (Nov 3, 2021):
Keep in mind that it also matters on what kind of hardware it is running, the network connection etc.. etc...
The Bitwarden Cloud server is probably running multiple servers, and if not, it is probably large enough to handle a lot of load.
Also it's network connection is probably big enough.
But, any help on improving speed in any way would be great.
Best thing to check is the response time from the server via the Developer Console for example.
And to be fair, make sure it is always done in a clean incognito/private browser so that no previous cache is used which makes it all look different.
@attie commented on GitHub (Nov 3, 2021):
Ah, apologies, these are both self hosted - Bitwarden in docker in a VM, Vaultwarden on native docker (no VM), both on the same physical hardware.
@attie commented on GitHub (Nov 10, 2021):
Apologies for my noise here, it seems that the performance issue I was seeing is fully addressed by moving away from SQLite to MariaDB (not totally unexpected).
More info
Test setup:
/api/ciphers, measuring TTFB (~1.15 MiB payload)Tests:
@bendem commented on GitHub (Dec 2, 2021):
I've investigated this a bit with some test data (200 collections, 600 ciphers), here is a rough estimate:
to_json_detailsis_writable_by_user: 200find_by_user_and_org: 0-200hide_passwords_for_user: 200find_by_user_and_org: 0-200to_json:Attachment::find_by_cipher: 600get_access_restrictions: 0-600is_in_full_access_org: 0-600get_collections: 600That's between 1062 and 2122 sql queries to prepare the full json blob. And that's not counting the queries for Folders, Policies and Sends. The sync query takes about 6s to load.
@lifeofguenter commented on GitHub (Jan 7, 2022):
@raphaelcoutu, @BlackDex: 30-50ms latency for a database connection is not a good thing in any case. Usually you would want to keep it below 10ms, however, as @bendem discovered, doing 1-2k SQL queries for a single API call might not be ideal either and will definitely cause issues no matter the hardware / connectivity on larger collections. Definitely not a niche issue, unless the majority of users have less than 50 passwords.
This can be avoided by making use of
JOINs orSELECT .. IN (1, 2, 3, ...)or even just aSELECTby some parent-id.@BlackDex commented on GitHub (Jan 7, 2022):
i can take a better look into the queries. But JOIN's arn't that easy with all the different tables and rust structs etc...
@bendem commented on GitHub (Feb 1, 2022):
We indeed need an optimised code path for that route. The danger lies in duplicating all the (already quite complex) code that ensures users only see what they are authorised to see. That duplication would mean there is a chance that part of the code gets updated and the optimised query does not (or the other way around).
@bendem commented on GitHub (Feb 1, 2022):
I just tried looking into this but I can't for the life of me figure out how to add
diesel_logger. Where doesDbConnget transformed into an actual connection ? It's just macros over macros and I'm not a rust dev.@BlackDex commented on GitHub (Feb 1, 2022):
It's relatively simple actually:
I didn't have time my self to check this out on how to optimize this unfortunately.
Also, do not forget the to set the log_level to DEBUG, else you will not see the output.
@bendem commented on GitHub (Feb 8, 2022):
Thanks, I'm sure it's easy if you know how macros work, that's why I asked :)
@BlackDex commented on GitHub (May 11, 2022):
Solved via #2429
@jtsang4 commented on GitHub (May 12, 2022):
@BlackDex Amazing work! The user experience is improved so much!
@bendem commented on GitHub (May 12, 2022):
Very nice, can't wait for the next release!