-
-
Notifications
You must be signed in to change notification settings - Fork 4.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Db connection management - performances #290
Comments
A couple of things i want to point out:
You can't really handle more requests at any given time than the amount of connections your thread pool can give you (15 conns for 100 requests means you have to start with 85 requests queued up), nor can you have more sync requests waiting than you have threads in your thread pool (if you have a 4 core + 4 "logical threads" machine, that's 12 threads in your thread pool). |
Hi, @sm-Fifteen, thank you very much for the additional information (I am not a Fastapi expert, I was just trying to make sense of these results). So, basically:
Thanks and kind regards! |
Given how FastAPI works (with dependencies being injected on demand, so ideally wou wouldn't have a whole lof of stuff existing outside of functions, which is
No, if you go over that limit, the async thread will most likely stall and pause handling incoming connections while waiting for a thread to be freed. If your application is behind a reverse proxy, that would likely cause you to run into 502 errors (bad gateway) after a few seconds as the proxy tried contecting the application and the application didn't even "pickup the phone" (if you want a more visual metaphor), which is what usually happens when the event loop blocks. |
Thanks, do you have an example of using Session directly? I see this in one of your comments in the thread you linked: fastapi/fastapi#726 (comment)
Ok, this is clear, thank you! |
Bringing over my comments from fastapi/fastapi#726: Starlette discourages There seems to be a lot of confusion about the best way to setup and teardown DB connections, and whether some of these ways are performance traps. @tiangolo Could you provide guidance on how to best manage DB lifecycle in consideration of all the various factors ( A few additional questions:
Putting it all together, we have something like this:
Usage:
|
@pikeas :
FastAPI tries to run routes and dependencies that are defined as _db_conn: Optional[Database]
async def get_db_conn() -> Database:
assert _db_conn is not None
return _db_conn ...because spinning up a thread for such a trivial getter would definitely be wasteful, and it's not calling any function I can't tell for sure wouldn't block. If that's what you were asking, given how your
It means you can actually call the function that takes
I believe rollback is always implicit if you exit a sesion without comitting, and I personnally prefer explicit commits on the caller's side than doing it implicitly via the context manager because it gives the caller more control on how the transaction flows (and it lets you do quick and dirty smoke tests by running a route that writes to DB and just commenting the part that commits the transaction). |
Thanks for the thorough reply! One quick note about
It's actually right in Starlette's code at https://github.com/encode/starlette/blob/master/starlette/applications.py#L114-L115
|
@sm-Fifteen is your recommendation here benchmarked ? fastapi/fastapi#726 (comment) will it work in all situations without lockups, etc. we are planning to use this with postgresql |
I have not run any load-testing benchmarks on it, no. When I posted this comment, it was more of an architectural suggestion to avoid initializing anything in the global scope (outside the ASGI application's scope) to take advantage of dependency injection everywhere.
@sandys : Like I mentionned in #290 (comment), another thing you'll want to check when testing for performance is whether FastAPI's threadpool ends up filling itself up when trying to offload the main thread of all the non-async
@pikeas: Ah, that's regarding the declarative API Starlette added in 0.13 (see encode/starlette#704), which was agreed to be a poor fit for FastAPI when the question of supporting that was brought up in fastapi/fastapi#687 (I think the API is great, it's just that it wouldn't mesh very well with FastAPI's very function-oriented style, see my comment in that thread). The best approach in the future would be the context manager approach I mentionned earlier, which has been implemented in Starlette, but still needs support to be added on the FastAPI side of things. |
@sm-Fifteen thanks for this! your comment here is pure gold ... as well as the previous one #290 (comment) would you be willing to add some color to this ? i know it may be asking too much, but how to turn the knobs would be very welcome (and there seem to be quite a few of them) |
@sandys: Unfortunately, that's about the limit of what I know regarding FastAPI performance. You can probably further reduce blocking by starting uvicorn with FastAPI could really use a "Performance" section in its documentation, you might want to mention that in the FastAPI developper survey if you haven't taken it yet. |
@sm-Fifteen I have a new way to do it instead of global sqlalchemy.engine - to use lru_cache and contextmanager. https://gist.github.com/sandys/671b8b86ba913e6436d4cb22d04b135f do you think it works just as well ? |
The reason I was recommending With that said, the exact way you initialize your DB engine isn't how you're going to improve performance, I was pointing that out more for the sake of correctness. |
Thank you. That makes total sense.
However what is your opinion on the contextmanager stuff ?
…On Wed, 17 Mar, 2021, 20:44 sm-Fifteen, ***@***.***> wrote:
@lru_cache() on a function with no parameters only creates the database
engine as a (lazily initialized) global singleton, which is basically
equivalent to just running create_engine in the global scope. The main
difference between the two is that the first request calling get_engine()
will have to wait for the connection to be established, as that step is no
longer performed before the FastAPI application goes up.
The reason I was recommending on_event() lifecycle managers earlier is
because these make sure that your SQL Alchemy engine is initialized before
the application runs, but also that it's managed with the lifecycle of the
application, which enables your ASGI server life Uvicorn to run proper
cleanup of both sync and async ressources before exiting, instead of
assuming that it will happen automatically (which it might not, Python
tries to run garbage collection before exiting but this is not perfect
<fastapi/fastapi#617 (comment)>,
and uvicorn's --reload option causes means your code gets reloaded
without exiting from the python interpreter first). lru_cache doesn't
really resolve that, it merely makes sure that get_engine() only runs
once.
With that said, the exact way you initialize your DB engine isn't how
you're going to improve performance, I was pointing that out more for the
sake of correctness.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#290 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAASYU3TULO6I34VOGVJPIDTEDBNTANCNFSM4SIR67IA>
.
|
It's a documented feature of FastAPI and generally a pretty good pattern to adopt for request-scoped resources. However:
|
@sm-Fifteen now that SQLAlchemy has async support, I was wondering if Sessionmaker still uses thread locals when creating AsyncSession objects. I'd hope not, at least. Has your recommendation around using
What my question amounts to, is whether you still recommend following your approach from tiangolo/fastapi#726 and your comment in this issue on using Session directly instead of sessoinmaker still apply today, if I"m doing the stuff I mentioned above. Edit: Added some more references, and a summary. |
@bharathanr: |
Hi ! I don't get it completely. I see that fastapi/fastapi#726 issue is closed. I had to use FastAPI-utils get rid of the famous error: But I got some trouble to bind declarative_base with the engine. If this repo still have performance issues, is there a way to make a application without this limitation thanks to FastAPI doc or must we wait for update on FastAPI side ? |
@oliviernguyenquoc: You can't make an application completely free of that sort of limitations, since you'll always have a connection limit somewhere (if not on SQLAlchemy, then Postgres' default of 150 open connections, and if you removed that then said database will run out memory opening threads to handle connections if you keep pushing it). It's just a limited resource, just like anything. The problem that's new to FastAPI is how its specific mix of running async on the main thread and running sync in a thread pool, which runs into issues that frameworks using the more traditional "one thread per request" model like Flask never needed to deal with (Armin Ronacher has a pretty good write up on that), namely that blocking I/O or processing anywhere you didn't expect it (such as checking out a connection from a connection pool, or maybe even closing it) will completely kill your server's performance (but then any blocking I/O needs to be done in the thread pool), and also that the async loop being unaware of the thread pool's size means it can keep accepting connections even when the thread pool cannot (which will also kill your server's performance). This introduces new issues like fastapi/fastapi#3205, which is about how a mismatch between your connection pool and your thread pool can lead to deadlocks when using SQLAlchemy in sync mode (async mode seems to solve this).
I would still recommend the configuration from fastapi/fastapi#726. You also don't need to bind your declarative_base objects to a SQL Alchemy engine or an existing Metadata object for them to work, you can just create an unbound declarative base. from sqlalchemy.ext.declarative import declarative_base
MyDBBase = declarative_base(name='MyDBBase')
class FooDBModel(MyDBBase):
__tablename__ = 'foo'
__table_args__ = {'schema':'example'}
foo_id = Column(Integer, primary_key=True) The engine is only needed when doing |
Adding to this discussion: I have performed some tests with synchronous SQLAlchemy session and have documented my findings in this repo: I haven't tested with the async SQLAlchemy session but from my tests, it seems that the problem is within how fastapi implements Summary of my load testing: |
@sm-Fifteen in ur comment in the other bug, u mentioned sqlalchemy 1.4 as one of the solutions to this issue. In that context, and given SA 1.4 is stable, what would be the recommended configuration for fastapi now ? |
@harsh8398: Those are some pretty troubling numbers, I wasn't aware that this cocktail of CM dependencies, thread pools and blocking I/O could kill server performance this badly. That certainly makes a convincing argument against using sync SQLAlchemy in FastAPI application, and makes the discussion in fastapi/fastapi#3205 extra important. I was going to say that "the docs are luckily unaffected by this as they still keep their SQLA objects in the global namespace", but on closer look, the docs do use CM deps to initialize ORM sessions, and haven't been updated to use async SQLA, so this problem also affects official documentation.
@sandys: I haven't had the opportunity to try async SQLA on the FastAPI applications I'm working on at the moment, so I can't say. |
harsh8398/fastapi-dependency-issue#2 The issue seems to be hitting the default capacity limiter for the threadpool used to run sync dependencies/endpoints. |
@adriangb on the other hand, i want to submit my code https://gist.github.com/sandys/671b8b86ba913e6436d4cb22d04b135f with a single gunicorn worker, im getting the following performance.
P.S. this uses Depends. Also the endpoint is actually a database insert. not just a database query. |
There's way too much code in there for me to go through. I'm also not sure why you're tagging me? |
Hi guys,
this is based on the discussion here: #104 .
I tried a few benchmarks, with:
scoped_session
that I'll describeI found out that - as seen by others - Depends doesn't work under load, it just exhausts the connections. So I'll leave out this solution.
SessionManager works, but i found out an (apparently) more performant solution:
SessionManager
solution (only relevant parts):scoped_session
solution (only relevant parts):Results:
Doing some tests with jmeter, the second implementation seems faster, for example, with 20 concurrent threads in jmeter (left: SessionMaker, right: scopedsession):
My question is: is the approach with
scopedsession
correct or is there some flaw that i can't see? I didn't get any connection errors during tests even with 100 threads. I am not a fastapi expert (or an expert python programmer), so if there is some silly error, I would be happy to know! Thanks in advance!The text was updated successfully, but these errors were encountered: