Why we built yet another Postgres connection pooler

(pgdog.dev)

93 points | by levkk 6 hours ago

7 comments

  • petters 3 hours ago
    > Since connection poolers reuse connections between clients, the connection state of one client “leaks” into the connection state of another.

    Wow this is very bad. This actually happens in typical Postgres setups?

    • vizzier 3 hours ago
      by definition connection poolers re-use connections so it it can happen with any connection pooling setup, PG or no.

      in pgbouncer the connection is reset via a customisable command [0] which should reset the connection to a clean state.

      [0] https://www.pgbouncer.org/config.html#server_reset_query

    • nijave 18 minutes ago
      pgbouncer has different pool modes you can pick from that have some impact over what's possible to leak https://www.pgbouncer.org/features.html
    • danudey 1 hour ago
      It's not unique to postgres, as others have said; the same thing can happen with e.g. MySQL poolers/proxies/etc., since the behavior of the connection can be changed dynamically and it persists for the lifetime of the connection.

      Example: legacy client A connects to MySQL via the bouncer and says 'I want all of our conversations to use latin-1, not utf-8'. This changes the character set that MySQL parses queries with and returns responses in. The legacy client does some queries and then disconnects.

      Now a new client connects to MySQL, and the bouncer just assigns it to the still-open connection from before. The new client is fully UTF-8 compatible and since this is the default for our database it doesn't explicitly say so; it just assumes that UTF-8 is the way to go. Unfortunately, the database server is still thinking in latin-1, meaning that if this new client sends UTF-8 data it will be parsed as latin-1; latin-1 is a subset of UTF-8, meaning that queries will actually work fine unless they need to use a character outside of latin-1, in which case they will get an error, or corrupted data, from the server.

      The only solutions around this are:

      1. Ensure that every client is using the same settings; if your database is for a single app that uses the same ORM, then this is automatic.

      2. Ensure that every client is always explicit about everything it might need to change e4very time, so that every UTF-8 client explicitly sets UTF-8 connections even when that's the default; clients that need utf8mb4 ask for it explicitly and clients that can't handle it ask for something else. One way of ensuring this happens is to configure the server (or the bouncer) to use defaults which are not valid for anyone, or which are going to cause errors frequently and not rarely (e.g. setting the default character set to 7-bit swedish, which would cause frequent errors).

      3. Use a bouncer which can either disallow these changes or detect and revert them after the original client has disconnected. I'm not sure if this exists for MySQL at least.

      4. Use separate bouncers for each application that might be different (extension of #1); in other words, instead of having a bouncer or set of bouncers for each pool of database servers, you have them for each application; your web app gets one, your legacy reporting tool gets one, your ODBC connector gets one, and so on.

      It's kind of a huge mess in theory; in practice, a lot of installations fall into the #1 case so it never matters, but that makes the occasional instance where it does matter extremely difficult to debug.

      • drdexebtjl 2 minutes ago
        > 4. Use separate bouncers for each application that might be different (extension of #1)

        I wonder if clients send something equivalent to a User-Agent, such that the connection pooler could assign them to different pools automatically.

      • nijave 21 minutes ago
        Recently ran into a related bug in duckdb. They implemented a basic http connection pooler (described as a parking lot) but there are edge cases where broken connections get returned to the pool then the next thing tries to use a busted connection and fails.
    • llimllib 3 hours ago
      Yes, as a consequence of how aggressively transparent to the postgres wire protocol pgbouncer wants to be. This article does a good job explaining it: https://www.augusteo.com/blog/how-pgbouncer-works
    • McGlockenshire 3 hours ago
      You'll see this kind of fun in other databases that support "persistent connections." When you start up, you have absolutely no idea what the state of the database is. If a previous process errored out, you might find yourself in the middle of a broken transaction for example. Did the last session do some weird SET magic to make things work? Did it create temporary tables? Well guess what, it's all still there!
  • khurs 1 hour ago
    Any plans to add Query Caching for Selects?

    as per:

    https://www.pgpool.net/docs/latest/en/html/runtime-in-memory...

    • levkk 1 hour ago
      I don't think so. Too much trouble. Caching is a really hard problem without context, and the context, typically, is app-specific.
      • khurs 1 hour ago
        Thanks for the reply and good work on pgDog!!
  • babayega2 1 hour ago
    Is there a pooler handling schema switching in PostgreSQL? like something in front of django-tenant ?
  • inigyou 2 hours ago
    Doesn't this NOTIFY performance fix mean that it isn't transactional any more?
    • levkk 2 hours ago
      From the strictest CAP theorem definition, that's correct, it is not. But, it's pretty close. I know that in the database world, that's not a good answer, but in practice, it will deliver the vast majority of messages, so maybe that's good enough? We'll see.

      We show that it's possible to come close without breaking the DB or the app, but I suspect, it's not quite yet at the level you'd expect from a _durable_ work queue, e.g., Kafka. Not going to replace that one anytime soon.

  • jauntywundrkind 3 hours ago
    Clickhouse also just put out a fun article on scaling pgbouncer too, talking about scaling out so_reuseport while not having to shard so harshly (a major limitation pgdog here is addressing via rewrite), https://clickhouse.com/blog/pgbouncer-clickhouse-managed-pos... https://news.ycombinator.com/item?id=48814152
    • merb 2 hours ago
      Well tbf pgdog looks extremely amazing on paper and goes way beyond multi threading.

      The notify/listen fix and automatic query routing to read replicas and auto sharding might bringt Postgres finally closer to vitess

  • danielsmori 1 hour ago
    [flagged]
  • mmakeev 4 hours ago
    we moved our django app behind pgbouncer transaction pooling a few days ago and the surprise wasn't SET so much as queryset.iterator(). it relies on server side cursors, which don't survive being pooled, so we had to disable it everywhere and let it fall back to client side. also had to move statement_timeout out of the app's connection options into the pooler's own connect query, since libpq startup params just get silently ignored behind it.
    • levkk 38 minutes ago
      Handling cursors is tough - they are very much session-level objects, so even if we, say, pinned your client while it uses that cursor, which would work, that would decrease the performance of connection pooling overall.

      So, what's better, breaking your app initially so you know to remove that feature, or letting it work silently while the connection pool isn't 100% in transaction mode? Tough call.