<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Citus Data Blog - Articles by Andres Freund</title>
  <author>
    <name>Andres Freund</name>
  </author>
  <subtitle>Scaling data and analytics with Postgres</subtitle>
  <id>https://www.citusdata.com/blog/</id>
  <link href="https://www.citusdata.com/blog/"/>
  <link href="https://www.citusdata.com/blog/feed/andres-freund.xml" rel="self"/>
  <updated>2020-10-25T04:14:00+00:00</updated>
  <entry>
    <title>Improving Postgres Connection Scalability: Snapshots</title>
    <link rel="alternate" href="https://www.citusdata.com/blog/2020/10/25/improving-postgres-connection-scalability-snapshots/"/>
    <id>https://www.citusdata.com/blog/2020/10/25/improving-postgres-connection-scalability-snapshots/</id>
    <published>2020-10-25T04:14:00+00:00</published>
    <updated>2020-10-25T04:14:00+00:00</updated>
    <author>Andres Freund</author>
    <content type="html">&lt;p&gt;I recently
&lt;a href="https://techcommunity.microsoft.com/t5/azure-database-for-postgresql/analyzing-the-limits-of-connection-scalability-in-postgres/ba-p/1757266"&gt;analyzed the limits of connection scalability&lt;/a&gt;,
to understand the most effective way to improve Postgres&amp;#39; handling of large
numbers of connections, and why that is important. I
&lt;a href="https://techcommunity.microsoft.com/t5/azure-database-for-postgresql/analyzing-the-limits-of-connection-scalability-in-postgres/ba-p/1757266/#conclusion-start-by-improving-snapshot-scalability-in-postgres"&gt;concluded&lt;/a&gt;
that the most pressing issue is
&lt;a href="https://techcommunity.microsoft.com/t5/azure-database-for-postgresql/analyzing-the-limits-of-connection-scalability-in-postgres/ba-p/1757266/#snapshot-scalability"&gt;snapshot scalability&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This post details the improvements I recently contributed to Postgres 14 (to be
released Q3 of 2021), significantly reducing the identified snapshot
scalability bottleneck.&lt;/p&gt;

&lt;p&gt;As the explanation of the implementation details is fairly long, I thought it&amp;#39;d
be more fun for of you if I start with the results of the work, instead of the
technical details (I&amp;#39;m cheating, I know ;)). &lt;/p&gt;

&lt;hr&gt;

&lt;div class="toc"&gt;
  &lt;nav id="TableOfContents"&gt;
  &lt;ol style="font-size: 85%;"&gt;
    &lt;li&gt;&lt;a href="#first-performance-improvements"&gt;First: Performance Improvements&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href="#unfortunately-a-minimal-transaction-visibility-primer"&gt;Unfortunately: A Minimal Transaction Visibility Primer&lt;/a&gt;
      &lt;ol&gt;
        &lt;li&gt;&lt;a href="#multi-version-concurrency-control-mvcc"&gt;Multi Version Concurrency Control (MVCC)&lt;/a&gt;&lt;/li&gt;
        &lt;li&gt;&lt;a href="#snapshots"&gt;Snapshots?&lt;/a&gt;&lt;/li&gt;
        &lt;li&gt;&lt;a href="#snapping-snapshots"&gt;Snapping Snapshots&lt;/a&gt;&lt;/li&gt;
        &lt;li&gt;&lt;a href="#past-optimizations"&gt;Past Optimizations&lt;/a&gt;&lt;/li&gt;
      &lt;/ol&gt;
    &lt;/li&gt;
    &lt;li&gt;&lt;a href="#finally-addressing-bottlenecks"&gt;Finally: Addressing Bottlenecks&lt;/a&gt;
      &lt;ol&gt;
        &lt;li&gt;&lt;a href="#bottleneck-1-ping-pong"&gt;Bottleneck 1: Ping Pong&lt;/a&gt;&lt;/li&gt;
        &lt;li&gt;&lt;a href="#interlude-removing-the-need-for-recentglobalxminhorizon"&gt;Interlude: Removing the need for &lt;code&gt;RecentGlobalXminHorizon&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
        &lt;li&gt;&lt;a href="#bottleneck-2-density"&gt;Bottleneck 2: Density&lt;/a&gt;&lt;/li&gt;
        &lt;li&gt;&lt;a href="#bottleneck-3-caching"&gt;Bottleneck 3: Caching&lt;/a&gt;&lt;/li&gt;
      &lt;/ol&gt;
    &lt;/li&gt;
    &lt;li&gt;&lt;a href="#conclusion-one-bottleneck-down-in-pg-14-others-in-sight"&gt;Conclusion: One bottleneck down in PG 14, others in sight&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href="#from-easy-to-hard-opportunities-for-further-improvements"&gt;From easy to hard: Opportunities for further improvements&lt;/a&gt;&lt;/li&gt;
  &lt;/ol&gt;
&lt;/nav&gt;
&lt;/div&gt;

&lt;hr&gt;

&lt;p&gt;&lt;a name="first-performance-improvements" id="first-performance-improvements"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;First: Performance Improvements&lt;/h2&gt;

&lt;p&gt;For all of these benchmarks, I compared the Postgres development tree just
&lt;a href="https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=1f42d35a1d6144a23602b2c0bc7f97f3046cf890"&gt;before&lt;/a&gt;
and
&lt;a href="https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=07f32fcd23ac81898ed47f88beb569c631a2f223"&gt;after&lt;/a&gt;
all the connection scalability changes have been merged. There are a few other
changes interspersed, but none that are likely to affect performance in a
significant way.&lt;/p&gt;

&lt;p&gt;First, a before/after comparison of a read-only
&lt;a href="https://www.postgresql.org/docs/current/pgbench.htm"&gt;pgbench&lt;/a&gt;
benchmark, on an
&lt;a href="https://docs.microsoft.com/azure/virtual-machines/fsv2-series"&gt;Azure F72s_v2&lt;/a&gt;
VM:&lt;/p&gt;

&lt;figure&gt;
&lt;picture&gt;
&lt;source srcset="https://cdn.citusdata.com/images/blog/pgbench-read-only-log-scale-prepost.webp" type="image/webp"&gt;
&lt;img src="https://cdn.citusdata.com/images/blog/pgbench-read-only-log-scale-prepost.jpg" alt="Graph showing significant scalability improvements. No evidence of a remaining scalability issues, even at high connection counts. A small dip between ~100 and 500 connections is visible however." loading="lazy" width="850" height="374" /&gt;
&lt;/picture&gt;
&lt;figcaption&gt;&lt;strong&gt;Figure 1:&lt;/strong&gt; Benchmark (read-only pgbench) results comparison, showing the effects of the snapshot scalability improvements.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;These results&lt;sup&gt;&lt;a name="fnref-1" id="fnref-1" href="#fn-1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt; show a significant improvement after the snapshot scalability changes I&amp;#39;ll be talking about in this
post, with little evidence of scalability issues even at very high connection
counts. The dip starting around 100 connections&amp;mdash;for both the pre/post changes
runs&amp;mdash; appears to be caused by OS task scheduling, rather than Postgres
directly.&lt;/p&gt;

&lt;p&gt;Next up, a repeat of the benchmarks I used in my last post on
&lt;a href="/blog/2020/10/08/analyzing-connection-scalability/#snapshot-scalability"&gt;analyzing connection scalability&lt;/a&gt;
to identify snapshot scalability as the primary bottleneck (again executed on
my workstation&lt;sup&gt;&lt;a name="fnref-2" id="fnref-2" href="#fn-2" class="footnote-ref" role="doc-noteref"&gt;2&lt;/a&gt;&lt;/sup&gt;).&lt;/p&gt;

&lt;figure&gt;
&lt;img src="https://cdn.citusdata.com/images/blog/performance-impact-of-idle-connections-1active-prepost.jpg" alt="A graph showing significant performance degradation at higher idle connection counts before the changes, but not after the changes." /&gt;
&lt;figcaption&gt;&lt;strong&gt;Figure 2:&lt;/strong&gt; Benchmark result comparison (1 active connection running read-only pgbench, in presence of a variable number of idle connections), showing the effects of the snapshot scalability improvements.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;figure&gt;
&lt;img src="https://cdn.citusdata.com/images/blog/performance-impact-of-idle-connections-48active-prepost.jpg" alt="A graph showing significant performance degradation at higher idle connection counts before the changes, but not after the changes." /&gt;
&lt;figcaption&gt;&lt;strong&gt;Figure 3:&lt;/strong&gt; Benchmark result comparison (48 active connections running read-only pgbench, in presence of a variable number of idle connections), showing the effects of the snapshot scalability improvements.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;These results (&lt;sup&gt;&lt;a name="fnref-3" id="fnref-3" href="#fn-3" class="footnote-ref" role="doc-noteref"&gt;3&lt;/a&gt;&lt;/sup&gt;,
&lt;sup&gt;&lt;a name="fnref-4" id="fnref-4" href="#fn-4" class="footnote-ref" role="doc-noteref"&gt;4&lt;/a&gt;&lt;/sup&gt;) show the extreme
difference in scalability between the &lt;em&gt;fixed&lt;/em&gt; and &lt;em&gt;unfixed&lt;/em&gt; version of
Postgres. More so than the results above, as the benchmark is chosen to
highlight the snapshot scalability issue.&lt;/p&gt;

&lt;p&gt;&lt;a name="unfortunately-a-minimal-transaction-visibility-primer" id="unfortunately-a-minimal-transaction-visibility-primer"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;Unfortunately: A Minimal Transaction Visibility Primer&lt;/h2&gt;

&lt;p&gt;As seen in the before &amp;amp; after charts above, the performance/scalability
effects of the changes are substantial. To, hopefully, make it a bit easier to
follow along, the next section is an attempt in providing some of the necessary
background.&lt;/p&gt;

&lt;p&gt;Postgres implements &lt;a href="https://www.postgresql.org/docs/devel/mvcc-intro.html"&gt;transaction isolation&lt;/a&gt;,
i.e. the visibility changes made concurrent with a transactions, using snapshot
based
&lt;a href="https://en.wikipedia.org/wiki/Multiversion_concurrency_control"&gt;Multi Version Concurrency Control&lt;/a&gt;
(MVCC).&lt;/p&gt;

&lt;p&gt;Brandur has a good post on &lt;a href="https://brandur.org/postgres-atomicity"&gt;how Postgres makes transactions atomic&lt;/a&gt;
explaining how this works in more detail. A lot of low-level details
of how this works in Postgres are explained in the relevant
&lt;a href="https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/access/transam/README"&gt;README&lt;/a&gt;
inside the Postgres source code.&lt;/p&gt;

&lt;p&gt;&lt;a name="multi-version-concurrency-control-mvcc" id="multi-version-concurrency-control-mvcc"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;Multi Version Concurrency Control (MVCC)&lt;/h3&gt;

&lt;p&gt;In brief, both &lt;em&gt;MVCC&lt;/em&gt; and &lt;em&gt;snapshots&lt;/em&gt; are some of the building blocks used to
implement part of concurrency control in Postgres. &lt;em&gt;MVCC&lt;/em&gt; boils down to having
the ability to have multiple row versions for the same logical row, with
different versions visible to different transactions, increasing
concurrency.&lt;/p&gt;

&lt;p&gt;E.g. imagine one query starting to scan a large table, and subsequently another
query updating a row in that table. &lt;em&gt;MVCC&lt;/em&gt; allows the update to proceed without
affecting the query results by the table scan, by keeping the original row
version available for the scan, and making a new row version with the updated
contents. That obviously is good for concurrency.&lt;/p&gt;

&lt;p&gt;Conceptually this works by each row version having a &amp;quot;visible since&amp;quot; (xmin in
Postgres) and a &amp;quot;visible until&amp;quot; (xmax in Postgres) &amp;quot;timestamp&amp;quot; (not really a
timestamp in Postgres, but rather a transaction identifier). That way a scan
can ignore modifications that have been made after the scan started, by (a)
considering row versions that have since been deleted to be visible and (b)
considering row versions created after the scan started to be invisible.&lt;/p&gt;

&lt;p&gt;&lt;a name="snapshots" id="snapshots"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;Snapshots?&lt;/h3&gt;

&lt;p&gt;Just having two such &amp;quot;timestamps&amp;quot; associated with each row version is not
enough, however. Isolation rules consider which effects by other transactions
should be visible to a transaction not by the time the other transactions
started, but by the time the other transaction commits. Therefore,
fundamentally, a timestamp like &amp;quot;visible since&amp;quot; and &amp;quot;visible until&amp;quot; attached to
row versions at the time of modification cannot alone be sufficient: The order
in which transaction commit is not yet known&lt;sup&gt;&lt;a name="fnref-5" id="fnref-5" href="#fn-5" class="footnote-ref" role="doc-noteref"&gt;5&lt;/a&gt;&lt;/sup&gt;. That is where
&lt;em&gt;snapshots&lt;/em&gt; come into play.&lt;/p&gt;

&lt;p&gt;Postgres uses snapshots to identify which which transactions were running at
the time of snapshot&amp;#39;s creation. That allows statements (e.g. in &lt;code&gt;READ
COMMITTED&lt;/code&gt; mode) or entire transactions (e.g. in &lt;code&gt;REPEATABLE READ&lt;/code&gt; mode) to
decide which rows created by other transactions should be visible, and which
not.&lt;/p&gt;
    &lt;div class="highlight"&gt;
      &lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;typedef&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;SnapshotData&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="err"&gt;…&lt;/span&gt;
    &lt;span class="n"&gt;TransactionId&lt;/span&gt; &lt;span class="n"&gt;xmin&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;         &lt;span class="cm"&gt;/* all XID &amp;lt; xmin are visible to me */&lt;/span&gt;
    &lt;span class="n"&gt;TransactionId&lt;/span&gt; &lt;span class="n"&gt;xmax&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;         &lt;span class="cm"&gt;/* all XID &amp;gt;= xmax are invisible to me */&lt;/span&gt;
&lt;span class="err"&gt;…&lt;/span&gt;
    &lt;span class="cm"&gt;/*
     * For normal MVCC snapshot this contains the all xact IDs that are in
     * progress, unless the snapshot was taken during recovery in which case
     * it's empty. …
     * note: all ids in xip[] satisfy xmin &amp;lt;= xip[i] &amp;lt; xmax
     */&lt;/span&gt;
    &lt;span class="n"&gt;TransactionId&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;xip&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;uint32&lt;/span&gt;      &lt;span class="n"&gt;xcnt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;           &lt;span class="cm"&gt;/* # of xact ids in xip[] */&lt;/span&gt;
&lt;span class="err"&gt;…&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
      &lt;button class="copy-button" data-clipboard-action="copy" data-clipboard-text="typedef struct SnapshotData
{
…
    TransactionId xmin;         /* all XID &amp;lt; xmin are visible to me */
    TransactionId xmax;         /* all XID &amp;gt;= xmax are invisible to me */
…
    /*
     * For normal MVCC snapshot this contains the all xact IDs that are in
     * progress, unless the snapshot was taken during recovery in which case
     * it&amp;#39;s empty. …
     * note: all ids in xip[] satisfy xmin &amp;lt;= xip[i] &amp;lt; xmax
     */
    TransactionId *xip;
    uint32      xcnt;           /* # of xact ids in xip[] */
…
"&gt;Copy&lt;/button&gt;
    &lt;/div&gt;

&lt;p&gt;The &lt;code&gt;xip&lt;/code&gt; array contains all the transaction IDs (which Postgres uses instead
of plain timestamps) that were running at the time the snapshot was taken. When
encountering a row version with a certain xmin, it will be invisible if that
transaction was still running when the snapshot was taken and conversely may be
visible if xmin is a transaction that already had finished at that time. And
conversely, a row version with an xmax is still visible if the associated
transaction that was running at the time of the snapshot was taken, invisible
otherwise.&lt;/p&gt;

&lt;p&gt;&lt;a name="snapping-snapshots" id="snapping-snapshots"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;Snapping Snapshots&lt;/h3&gt;

&lt;p&gt;To understand the performance problems and the improvements it is
necessary to understand how snapshots were built before. The core
routine for this is &lt;code&gt;GetSnapshotData()&lt;/code&gt;, which unsurprisingly is the
function we saw high up in profiles
&lt;a href="/blog/2020/10/08/analyzing-connection-scalability/#cause"&gt;earlier&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Every connection to Postgres has an associated &lt;code&gt;struct PGPROC&lt;/code&gt; and,
until now, a &lt;code&gt;struct PGXACT&lt;/code&gt; entry. These structs are pre-allocated at
server based on &lt;code&gt;max_connections&lt;/code&gt; (and &lt;code&gt;max_prepared_xacts&lt;/code&gt;,
&lt;code&gt;max_autovacuum_workers&lt;/code&gt;, …).&lt;/p&gt;
    &lt;div class="highlight"&gt;
      &lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;typedef&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;ProcArrayStruct&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt;         &lt;span class="n"&gt;numProcs&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;       &lt;span class="cm"&gt;/* number of valid procs entries */&lt;/span&gt;
&lt;span class="err"&gt;…&lt;/span&gt;
    &lt;span class="cm"&gt;/* indexes into allPgXact[], has PROCARRAY_MAXPROCS entries */&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt;         &lt;span class="n"&gt;pgprocnos&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;FLEXIBLE_ARRAY_MEMBER&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="err"&gt;…&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;ProcArrayStruct&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;


&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;PGPROC&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="err"&gt;…&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;…&lt;/span&gt;

&lt;span class="cm"&gt;/*
 * Prior to PostgreSQL 9.2, the fields below were stored as part of the
 * PGPROC.  However, benchmarking revealed that packing these particular
 * members into a separate array as tightly as possible sped up GetSnapshotData
 * considerably on systems with many CPU cores, by reducing the number of
 * cache lines needing to be fetched.  Thus, think very carefully before adding
 * anything else here.
 */&lt;/span&gt;
&lt;span class="k"&gt;typedef&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;PGXACT&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;TransactionId&lt;/span&gt; &lt;span class="n"&gt;xid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;          &lt;span class="cm"&gt;/* id of top-level transaction currently being
                                 * executed by this proc, if running and XID
                                 * is assigned; else InvalidTransactionId */&lt;/span&gt;

    &lt;span class="n"&gt;TransactionId&lt;/span&gt; &lt;span class="n"&gt;xmin&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;         &lt;span class="cm"&gt;/* minimal running XID as it was when we were
                                 * starting our xact, excluding LAZY VACUUM:
                                 * vacuum must not remove tuples deleted by
                                 * xid &amp;gt;= xmin ! */&lt;/span&gt;

    &lt;span class="n"&gt;uint8&lt;/span&gt;       &lt;span class="n"&gt;vacuumFlags&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;    &lt;span class="cm"&gt;/* vacuum-related flags, see above */&lt;/span&gt;
    &lt;span class="n"&gt;bool&lt;/span&gt;        &lt;span class="n"&gt;overflowed&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;uint8&lt;/span&gt;       &lt;span class="n"&gt;nxids&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;PGXACT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;
      &lt;button class="copy-button" data-clipboard-action="copy" data-clipboard-text="typedef struct ProcArrayStruct
{
    int         numProcs;       /* number of valid procs entries */
…
    /* indexes into allPgXact[], has PROCARRAY_MAXPROCS entries */
    int         pgprocnos[FLEXIBLE_ARRAY_MEMBER];
…
} ProcArrayStruct;


struct PGPROC
{
…
}
…

/*
 * Prior to PostgreSQL 9.2, the fields below were stored as part of the
 * PGPROC.  However, benchmarking revealed that packing these particular
 * members into a separate array as tightly as possible sped up GetSnapshotData
 * considerably on systems with many CPU cores, by reducing the number of
 * cache lines needing to be fetched.  Thus, think very carefully before adding
 * anything else here.
 */
typedef struct PGXACT
{
    TransactionId xid;          /* id of top-level transaction currently being
                                 * executed by this proc, if running and XID
                                 * is assigned; else InvalidTransactionId */

    TransactionId xmin;         /* minimal running XID as it was when we were
                                 * starting our xact, excluding LAZY VACUUM:
                                 * vacuum must not remove tuples deleted by
                                 * xid &amp;gt;= xmin ! */

    uint8       vacuumFlags;    /* vacuum-related flags, see above */
    bool        overflowed;

    uint8       nxids;
} PGXACT;

"&gt;Copy&lt;/button&gt;
    &lt;/div&gt;

&lt;p&gt;To avoid needing to grovel through all PGPROC/PGXACT entries
&lt;code&gt;ProcArrayStruct-&amp;gt;pgprocnos&lt;/code&gt; is a sorted array of the &lt;code&gt;-&amp;gt;maxProc&lt;/code&gt; established
connections. Each array entry is the index into &lt;code&gt;PGPROC&lt;/code&gt;/&lt;code&gt;PGXACT&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;To build a snapshot
&lt;a href="https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/storage/ipc/procarray.c;hb=5cbfce562f7cd2aab0cdc4694ce298ec3567930e#l1468"&gt;GetSnapshotData()&lt;/a&gt;
iterates over all &lt;code&gt;maxProc&lt;/code&gt; entries in &lt;code&gt;pgprocnos&lt;/code&gt;, collecting &lt;code&gt;PGXACT-&amp;gt;xid&lt;/code&gt;
for all connections with an assigned transaction ID.&lt;/p&gt;

&lt;p&gt;There are a few aspects making this slightly more complicated than the
simple loop I described:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Because it was, at some point, convenient, &lt;code&gt;GetSnapshotData()&lt;/code&gt; also computes
the globally oldest &lt;code&gt;PGXACT-&amp;gt;xmin&lt;/code&gt;. That is, most importantly, used to
remove dead tuples on access.&lt;/li&gt;
&lt;li&gt;To implement &lt;code&gt;SAVEPOINT&lt;/code&gt;, a backend can have multiple assigned transaction
IDs. A certain number of these are stored as part of &lt;code&gt;PGPROC&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Some backends, e.g. ones executing &lt;code&gt;VACUUM&lt;/code&gt;, are ignored when building a
snapshot, for efficiency purposes.&lt;/li&gt;
&lt;li&gt;On a replica, the snapshot computation works quite differently.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a name="past-optimizations" id="past-optimizations"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;Past Optimizations&lt;/h3&gt;

&lt;p&gt;In 2011 &lt;code&gt;GetSnapshotData()&lt;/code&gt; was seen as a bottleneck. At that point all the
relevant data to build a snapshot was stored in &lt;code&gt;PGPROC&lt;/code&gt;. That caused
performance problems, primarily because multiple cache-lines were accessed for
each established connection.&lt;/p&gt;

&lt;p&gt;This was
&lt;a href="https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=ed0b409d223"&gt;improved&lt;/a&gt;
by splitting out the most important fields into a new data-structure
&lt;code&gt;PGXACT&lt;/code&gt;. That significantly decreases the total number of cache-lines that
need to be accessed to build a snapshot. Additionally the order of accesses to
&lt;code&gt;PGXACT&lt;/code&gt; was improved to be in increasing memory order (previously it was
determined by the order in which connections are established and disconnect).&lt;/p&gt;

&lt;p&gt;&lt;a name="finally-addressing-bottlenecks" id="finally-addressing-bottlenecks"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;Finally: Addressing Bottlenecks&lt;/h2&gt;

&lt;p&gt;It&amp;#39;s not too hard to see that the approach described above, i.e. iterating over
an array containing all established connections, has a
&lt;a href="https://en.wikipedia.org/wiki/Time_complexity"&gt;complexity&lt;/a&gt; of
&lt;code&gt;O(#connections)&lt;/code&gt;, i.e. the snapshot computation cost increases linearly with
the number of connections.&lt;/p&gt;

&lt;p&gt;There are two fundamental approaches to improving scalability here: First,
finding an algorithm that improves the complexity, so that each additional
connection does &lt;em&gt;not&lt;/em&gt; increase the snapshot computation costs linearly. Second,
perform less work for each connection, hopefully reducing the total time taken
so much that even at high connection counts the total time is still small
enough to not matter much (i.e. reduce the constant factor).&lt;/p&gt;

&lt;p&gt;One approach to improve the algorithmic complexity of &lt;code&gt;GetSnapshotData()&lt;/code&gt; that
has been worked on in the Postgres community for quite a few years are
&lt;a href="https://www.postgresql.org/message-id/CA%2BCSw_tEpJ%3Dmd1zgxPkjH6CWDnTDft4gBi%3D%2BP9SnoC%2BWy3pKdA%40mail.gmail.com"&gt;commit sequence number&lt;/a&gt;
based snapshots (also called CSN based snapshots). Unfortunately implementing
CSN snapshots has proven to be a very large project, with many open non-trivial
problems that need to be solved. As I was looking for improvements that could
be completed in a shorter time frame, I discarded pursuing that approach, and
other similarly fundamental changes.&lt;/p&gt;

&lt;p&gt;Back in 2015, I had previously tried to attack this problem by
&lt;a href="https://www.postgresql.org/message-id/20150202152706.GD9201%40alap3.anarazel.de"&gt;caching snapshots&lt;/a&gt;,
but that turned out to not be easy either (at least not yet…).&lt;/p&gt;

&lt;p&gt;Therefore I chose to first focus on improving the cost each additional
connection adds. Iterating over an array of a few thousand elements and
dereferencing fairly small content obviously is not free, but compared to the
other work done as part of query processing, it should not be quite as prominent
as in the
&lt;a href="/blog/2020/10/08/analyzing-connection-scalability/#cause"&gt;CPU profile&lt;/a&gt; from the previous post:&lt;/p&gt;

&lt;figure&gt;
&lt;img src="https://cdn.citusdata.com/images/blog/profile_1active_with_5000_idle.png" alt="50% of the CPU time is spent in GetSnapshotData()" /&gt;
&lt;figcaption&gt;Profile of one active connection running read-only pgbench concurrently with 5000 idle connections, bottleneck is clearly in &lt;code&gt;GetSnapshotData()&lt;/code&gt;&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;The core snapshot computation boils down to, in pseudo code, the following:&lt;/p&gt;
    &lt;div class="highlight"&gt;
      &lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;xmin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;global_xmin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;inferred_maximum_possible&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="n"&gt;connections&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;procno&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;shared_memory&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;connection_offsets&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="n"&gt;PGXACT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;pgxact&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;shared_memory&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;all_connections&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;procno&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="c1"&gt;// compute global xmin minimum&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pgxact&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;xmin&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;pgxact&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;xmin&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;global_xmin&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;global_xmin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pgxact&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;xmin&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// nothing to do if backend has transaction id assigned&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;pgxact&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;xid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// the global xmin minimum also needs to include assigned transaction ids&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pxact&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;xid&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;global_xmin&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;global_xmin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pgxact&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;xid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// add the xid to the snapshot&lt;/span&gt;
    &lt;span class="n"&gt;snapshot&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;xip&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;snapshot&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;xcnt&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pgxact&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;xid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// compute minimum xid in snapshot&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pgxact&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;xid&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;xmin&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;xmin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pgxact&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;xid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;snapshot&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;xmin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;xmin&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// store snapshot xmin unless we already have built other snapshots&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;MyPgXact&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;xmin&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;MyPgXact&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;xmin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;xmin&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;RecentGlobalXminHorizon&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;global_xmin&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;
      &lt;button class="copy-button" data-clipboard-action="copy" data-clipboard-text="xmin = global_xmin = inferred_maximum_possible;
for (i = 0; i &amp;lt; #connections; i++)
{
    int procno = shared_memory-&amp;gt;connection_offsets[i];
    PGXACT *pgxact = shared_memory-&amp;gt;all_connections[procno];

    // compute global xmin minimum
    if (pgxact-&amp;gt;xmin &amp;amp;&amp;amp; pgxact-&amp;gt;xmin &amp;lt; global_xmin)
        global_xmin = pgxact-&amp;gt;xmin;

    // nothing to do if backend has transaction id assigned
    if (!pgxact-&amp;gt;xid)
        continue;

    // the global xmin minimum also needs to include assigned transaction ids
    if (pxact-&amp;gt;xid &amp;lt; global_xmin)
        global_xmin = pgxact-&amp;gt;xid;

    // add the xid to the snapshot
    snapshot-&amp;gt;xip[snapshot-&amp;gt;xcnt++] = pgxact-&amp;gt;xid;

    // compute minimum xid in snapshot
    if (pgxact-&amp;gt;xid &amp;lt; xmin)
        xmin = pgxact-&amp;gt;xid;

}

snapshot-&amp;gt;xmin = xmin;
// store snapshot xmin unless we already have built other snapshots
if (!MyPgXact-&amp;gt;xmin)
    MyPgXact-&amp;gt;xmin = xmin;
RecentGlobalXminHorizon = global_xmin;

"&gt;Copy&lt;/button&gt;
    &lt;/div&gt;

&lt;p&gt;One important observation about this is that the main loop does not just
compute the snapshot contents, but also the &amp;quot;global xmin horizon&amp;quot;. Which is not
actually part of the snapshot, but can conveniently be computed at the same
time, for a small amount of added cost. Or so we thought...&lt;/p&gt;

&lt;p&gt;I spent a lot of time, on and off, trying to understand why iterating over a
few thousand elements of an array, even taking the indirection into account,
turns out to be so costly in &lt;em&gt;some&lt;/em&gt; workloads.&lt;/p&gt;

&lt;p&gt;&lt;a name="bottleneck-1-ping-pong" id="bottleneck-1-ping-pong"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;Bottleneck 1: Ping Pong&lt;/h3&gt;

&lt;p&gt;The main problem turns out to be &lt;code&gt;MyPgXact-&amp;gt;xmin = xmin;&lt;/code&gt;. A
connection&amp;#39;s xmin is is set whenever a snapshot is
&lt;a href="https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/storage/ipc/procarray.c;h=18a0f62ba67ca931babc60a3fe9a0db626e58045;hb=REL_12_STABLE#l1710"&gt;computed&lt;/a&gt;
(unless another snapshot already
&lt;a href="https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/utils/time/snapmgr.c;h=d07ca1b0b242f289db940a334cb8e4f22b4d5d74;hb=REL_12_STABLE#l6"&gt;exists&lt;/a&gt;),
when a transaction is committed/aborted (
&lt;a href="https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/storage/ipc/procarray.c;h=18a0f62ba67ca931babc60a3fe9a0db626e58045;hb=REL_12_STABLE#l434"&gt;1&lt;/a&gt;,
&lt;a href="https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/storage/ipc/procarray.c;h=18a0f62ba67ca931babc60a3fe9a0db626e58045;hb=REL_12_STABLE#l456"&gt;2&lt;/a&gt;
).&lt;/p&gt;

&lt;p&gt;On the currently most common multi-core CPU micro-architectures each CPU core
has its own private L1 and L2 caches and all cores within a CPU socket share an
L3 cache.&lt;/p&gt;

&lt;p&gt;Active backends constantly update &lt;code&gt;MyPgXact-&amp;gt;xmin&lt;/code&gt;. Simplifying a bit, that in
turn requires that the data is in a core-local cache (in &lt;a href="https://en.wikipedia.org/wiki/MESI_protocol"&gt;exclusive /
modified&lt;/a&gt; state). In contrast to
that, when building a snapshot, a backend accesses all other connection&amp;#39;s
&lt;code&gt;PGXACT-&amp;gt;{xid,xmin}&lt;/code&gt;. Glossing over a few details, that, in turn, requires that
the cache-lines containing the &lt;code&gt;PGXACT&lt;/code&gt; cannot be in another core&amp;#39;s private
caches. &lt;strong&gt;Head on head collision alert&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Transferring the modified contents of a cache-line from a cache in another core
to either a local cache or the shared L3 cache has a fairly high latency cost,
compared to accessing shared and unmodified data in the L3 (and even more so in
the local L1/L2, obviously).&lt;/p&gt;

&lt;p&gt;The kicker is that, to build the snapshot, &lt;code&gt;-&amp;gt;xmin&lt;/code&gt; does &lt;strong&gt;not actually need to
be accessed&lt;/strong&gt;. It is only needed to compute &lt;code&gt;RecentGlobalXminHorizon&lt;/code&gt;. However,
just removing the read access itself doesn&amp;#39;t improve the situation
significantly: As &lt;code&gt;-&amp;gt;xid&lt;/code&gt;, which does need to be accessed to build a snapshot,
is on the same cache line as &lt;code&gt;-&amp;gt;xmin&lt;/code&gt; modifying &lt;code&gt;-&amp;gt;xmin&lt;/code&gt; causes &lt;code&gt;-&amp;gt;xid&lt;/code&gt;
accesses to be slow.&lt;/p&gt;

&lt;p&gt;&lt;a name="interlude-removing-the-need-for-recentglobalxminhorizon" id="interlude-removing-the-need-for-recentglobalxminhorizon"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;Interlude: Removing the need for &lt;code&gt;RecentGlobalXminHorizon&lt;/code&gt;&lt;/h3&gt;

&lt;p&gt;The reason that &lt;code&gt;GetSnapshotData()&lt;/code&gt; also re-computes &lt;code&gt;RecentGlobalXminHorizon&lt;/code&gt;
is that we use that for the cleanup of dead table and index entries (see
&lt;a href="https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/access/heap/README.HOT"&gt;When can/should we prune or defragment?&lt;/a&gt;
and &lt;a href="https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/access/nbtree/README"&gt;On-the-Fly Deletion Of Index Tuples&lt;/a&gt;).
The horizon is used as a threshold below which old tuple versions are not
accessed by any connection. If older than the horizon row versions, as well as
index entries pointing to them, can safely be deleted.&lt;/p&gt;

&lt;p&gt;The crucial observation&amp;mdash;after quite a long period of trying
things&amp;mdash;that allowed me to avoid the costly re-computation, is that we
don&amp;#39;t necessarily need a accurate value most of the time.&lt;/p&gt;

&lt;p&gt;In most workloads the majority of accesses are to &lt;em&gt;live&lt;/em&gt; tuples, and when
encountering non-live tuple versions they are either very old, or very
new. With a bit of care we can lazily maintain a more complex threshold: One
value that determines that everything older than it is definitely dead, and a
second value that determines that everything above it is definitely too new to
be cleaned up.&lt;/p&gt;

&lt;p&gt;When encountering a tuple in between these thresholds we compute accurate
values, valid for the current transaction. If we had to recompute the threshold
in every short transaction, that would be more expensive than pre-computing the
accurate value in &lt;code&gt;GetSnapshotData()&lt;/code&gt;&amp;mdash;but it&amp;#39;s very hard to construct
such workloads.&lt;/p&gt;

&lt;p&gt;The main commit implementing this new approach is
&lt;a href="https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=dc7420c2c9274a283779ec19718d2d16323640c0"&gt;dc7420c2c92&lt;/a&gt;
&lt;strong&gt;snapshot scalability: Don&amp;#39;t compute global horizons while building snapshots&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;After that commit we do not access &lt;code&gt;-&amp;gt;xmin&lt;/code&gt; in &lt;code&gt;GetSnapshotData()&lt;/code&gt; anymore. To
avoid the cache-line ping-pong, we can move it out of the data used by
&lt;code&gt;GetSnapshotData()&lt;/code&gt;. That alone provides a substantial improvement in
scalability.&lt;/p&gt;

&lt;p&gt;The commit doing so,
&lt;a href="https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=1f51c17c68d05c28d5b9294d8013cb9e7e653160"&gt;1f51c17c68d&lt;/a&gt;
&lt;strong&gt;snapshot scalability: Move PGXACT-&amp;gt;xmin back to PGPROC.&lt;/strong&gt;
includes some rough numbers:&lt;/p&gt;
    &lt;div class="highlight"&gt;
      &lt;pre class="highlight "&gt;&lt;code&gt;  For highly concurrent, snapshot acquisition heavy, workloads this change alone
  can significantly increase scalability. E.g. plain pgbench on a smaller 2
  socket machine gains 1.07x for read-only pgbench, 1.22x for read-only pgbench
  when submitting queries in batches of 100, and 2.85x for batches of 100
  'SELECT';.  The latter numbers are obviously not to be expected in the
  real-world, but micro-benchmark the snapshot computation
  scalability (previously spending ~80% of the time in GetSnapshotData()).
&lt;/code&gt;&lt;/pre&gt;
      &lt;button class="copy-button" data-clipboard-action="copy" data-clipboard-text="  For highly concurrent, snapshot acquisition heavy, workloads this change alone
  can significantly increase scalability. E.g. plain pgbench on a smaller 2
  socket machine gains 1.07x for read-only pgbench, 1.22x for read-only pgbench
  when submitting queries in batches of 100, and 2.85x for batches of 100
  &amp;#39;SELECT&amp;#39;;.  The latter numbers are obviously not to be expected in the
  real-world, but micro-benchmark the snapshot computation
  scalability (previously spending ~80% of the time in GetSnapshotData()).
"&gt;Copy&lt;/button&gt;
    &lt;/div&gt;

&lt;p&gt;&lt;a name="bottleneck-2-density" id="bottleneck-2-density"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;Bottleneck 2: Density&lt;/h3&gt;

&lt;p&gt;Above I showed some simplified pseudo-code
(&lt;a href="https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/storage/ipc/procarray.c;hb=5cbfce562f7cd2aab0cdc4694ce298ec3567930e#l1577"&gt;real code&lt;/a&gt;)
for snapshot computations. The start of the pseudo code:&lt;/p&gt;
    &lt;div class="highlight"&gt;
      &lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;xmin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;global_xmin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;inferred_maximum_possible&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="n"&gt;connections&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;procno&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;shared_memory&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;connection_offsets&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="n"&gt;PGXACT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;pgxact&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;shared_memory&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;all_connections&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;procno&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
      &lt;button class="copy-button" data-clipboard-action="copy" data-clipboard-text="xmin = global_xmin = inferred_maximum_possible;
for (i = 0; i &amp;lt; #connections; i++)
{
    int procno = shared_memory-&amp;gt;connection_offsets[i];
    PGXACT *pgxact = shared_memory-&amp;gt;all_connections[procno];
"&gt;Copy&lt;/button&gt;
    &lt;/div&gt;

&lt;p&gt;shows that accesses to &lt;code&gt;PGXACT&lt;/code&gt; have to go through an indirection. That
indirection allows to only look at the &lt;code&gt;PGXACT&lt;/code&gt; of established connections,
rather than also having to look at the connection slots for inactive
connections.&lt;/p&gt;

&lt;p&gt;Instead of having to go through an indirection, we can instead make the
contents of &lt;code&gt;PGXACT&lt;/code&gt; dense. That makes connection establishment/disconnections
a tiny bit slower, now having to ensure not just that the &lt;code&gt;connection_offsets&lt;/code&gt;
array is dense, but also that the &lt;code&gt;PGXACT&lt;/code&gt; contents are.&lt;/p&gt;

&lt;p&gt;A second, and related, observation is that none of the remaining &lt;code&gt;PGXACT&lt;/code&gt;
members need to be accessed when &lt;code&gt;-&amp;gt;xid&lt;/code&gt; is not valid (&lt;code&gt;-&amp;gt;xmin&lt;/code&gt;
&lt;a href="#bottleneck-1-ping-pong"&gt;previously&lt;/a&gt; did need to be accessed). In many
workloads most transactions do not write, and in most write heavy workloads,
most transactions do not use savepoints.&lt;/p&gt;
    &lt;div class="highlight"&gt;
      &lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;typedef&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;PGXACT&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;TransactionId&lt;/span&gt; &lt;span class="n"&gt;xid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;uint8&lt;/span&gt;       &lt;span class="n"&gt;vacuumFlags&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;bool&lt;/span&gt;        &lt;span class="n"&gt;overflowed&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;uint8&lt;/span&gt;       &lt;span class="n"&gt;nxids&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;PGXACT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
      &lt;button class="copy-button" data-clipboard-action="copy" data-clipboard-text="typedef struct PGXACT
{
    TransactionId xid;
    uint8       vacuumFlags;
    bool        overflowed;
    uint8       nxids;
} PGXACT;
"&gt;Copy&lt;/button&gt;
    &lt;/div&gt;

&lt;p&gt;As a consequence, it is better not to make the entire &lt;code&gt;PGXACT&lt;/code&gt; array dense, but
instead to split its members into separate dense arrays. The array containing
the &lt;code&gt;xids&lt;/code&gt; of all established connections nearly always needs to be
accessed&lt;sup&gt;&lt;a name="fnref-6" id="fnref-6" href="#fn-6" class="footnote-ref" role="doc-noteref"&gt;6&lt;/a&gt;&lt;/sup&gt;. But only if the connection has an assigned &lt;code&gt;xid&lt;/code&gt; the
other members need to be accessed.&lt;/p&gt;

&lt;p&gt;By having a separate array for &lt;code&gt;xids&lt;/code&gt; the CPU cache hit ratio can be increased,
as most of the time the other fields are not accessed. Additionally, as the
other fields change less frequently, keeping them separate allows them to be
shared in an unmodified state between the cache domains (increasing access
speed/decreasing bus traffic).&lt;/p&gt;

&lt;p&gt;Theses changes are implemented in Postgres commits&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=941697c3c1ae5d6ee153065adb96e1e63ee11224"&gt;941697c3c1a&lt;/a&gt;
&lt;strong&gt;snapshot scalability: Introduce dense array of in-progress xids&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=5788e258bb26495fab65ff3aa486268d1c50b123"&gt;5788e258bb2&lt;/a&gt;
&lt;strong&gt;snapshot scalability: Move PGXACT-&amp;gt;vacuumFlags to ProcGlobal-&amp;gt;vacuumFlags&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=73487a60fc1063ba4b5178b69aee4ee210c182c4"&gt;73487a60fc1&lt;/a&gt;
&lt;strong&gt;snapshot scalability: Move subxact info to ProcGlobal, remove PGXACT.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This yields quite a bit of benefit, as commented upon in one of the
&lt;a href="https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=73487a60fc1063ba4b5178b69aee4ee210c182c4"&gt;commit messages&lt;/a&gt;:&lt;/p&gt;
    &lt;div class="highlight"&gt;
      &lt;pre class="highlight "&gt;&lt;code&gt;  On a larger 2 socket machine this and the two preceding commits result
  in a ~1.07x performance increase in read-only pgbench. For read-heavy
  mixed r/w workloads without row level contention, I see about 1.1x.
&lt;/code&gt;&lt;/pre&gt;
      &lt;button class="copy-button" data-clipboard-action="copy" data-clipboard-text="  On a larger 2 socket machine this and the two preceding commits result
  in a ~1.07x performance increase in read-only pgbench. For read-heavy
  mixed r/w workloads without row level contention, I see about 1.1x.
"&gt;Copy&lt;/button&gt;
    &lt;/div&gt;

&lt;p&gt;&lt;a name="bottleneck-3-caching" id="bottleneck-3-caching"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;Bottleneck 3: Caching&lt;/h3&gt;

&lt;p&gt;Even with all the preceding changes, computing a snapshot with a lot of
connections still is not cheap. While the changes improved the constant factor
considerably, having to iterate through arrays with potentially a few thousand
elements still is not cheap.&lt;/p&gt;

&lt;p&gt;Now that &lt;code&gt;GetSnapshotData()&lt;/code&gt; does not need to maintain &lt;code&gt;RecentGlobalXmin&lt;/code&gt;
&lt;a href="#interlude-removing-the-need-for-recentglobalxminhorizon"&gt;anymore&lt;/a&gt;, a huge
improvement on the table: We can avoid re-computing the snapshot if we can
determine it has not changed. Previously that was not viable, as
&lt;code&gt;RecentGlobalXmin&lt;/code&gt; changes much more frequently than the snapshot contents
themselves.&lt;/p&gt;

&lt;p&gt;A snapshot only needs to change if a previously running transaction has
committed (so its effect are visible): Because all transactions bigger-or-equal
than &lt;code&gt;-&amp;gt;xmax&lt;/code&gt; are treated as running, and because all transactions starting
after snapshot has been computed are guaranteed to be assigned a transaction
id larger then &lt;code&gt;-&amp;gt;xmax&lt;/code&gt;, we need not care about newly started transactions.&lt;/p&gt;

&lt;p&gt;Therefore a simple in-memory counter of the number of completed (i.e. committed
or aborted) transactions can be used to invalidate snapshots. The &lt;em&gt;completion
counter&lt;/em&gt; is stored in the snapshot, and when asked to re-compute the snapshot
contents, we just need to check if the snapshot&amp;#39;s &lt;code&gt;snapXactCompletionCount&lt;/code&gt; is
the same as the current in-memory value
&lt;code&gt;ShmemVariableCache-&amp;gt;xactCompletionCount&lt;/code&gt;. If they are, the contents of the
snapshot can be reused, otherwise the snapshot needs to be built from scratch.&lt;/p&gt;

&lt;p&gt;This change was implemented in Postgres commit
&lt;a href="https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=623a9ba79bbdd11c5eccb30b8bd5c446130e521c"&gt;623a9ba79bb&lt;/a&gt;: &lt;strong&gt;snapshot scalability: cache snapshots using a xact completion counter.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The commit message again describes the gains:&lt;/p&gt;
    &lt;div class="highlight"&gt;
      &lt;pre class="highlight "&gt;&lt;code&gt;  On a smaller two socket machine this gains another ~1.03x, on a larger
  machine the effect is roughly double (earlier patch version tested
  though).
&lt;/code&gt;&lt;/pre&gt;
      &lt;button class="copy-button" data-clipboard-action="copy" data-clipboard-text="  On a smaller two socket machine this gains another ~1.03x, on a larger
  machine the effect is roughly double (earlier patch version tested
  though).
"&gt;Copy&lt;/button&gt;
    &lt;/div&gt;

&lt;p&gt;As the last sentence alludes to, currently we test for cache-ability holding a
lock. It likely is possible to avoid that, but there are a few complexities
that need to be addressed&lt;sup&gt;&lt;a name="fnref-7" id="fnref-7" href="#fn-7" class="footnote-ref" role="doc-noteref"&gt;7&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a name="conclusion-one-bottleneck-down-in-pg-14-others-in-sight" id="conclusion-one-bottleneck-down-in-pg-14-others-in-sight"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;Conclusion: One bottleneck down in PG 14, others in sight&lt;/h2&gt;

&lt;p&gt;The improvements presented here significantly improve Postgres&amp;#39; handling of
large numbers of connections, particularly when&amp;mdash;as is often the
case&amp;mdash;a large fraction are idle. This addresses the most pressing issue
identified in my previous post on
&lt;a href="/blog/2020/10/08/analyzing-connection-scalability/#conclusion-start-by-improving-snapshot-scalability-in-postgres"&gt;Analyzing the Limits of Connection Scalability in Postgres&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;To be clear: These improvements do not address all connection scalability
issues in Postgres. Nor are snapshot computations eliminated as a scalability
factor. But I do think this project has improved the situation considerably.&lt;/p&gt;

&lt;p&gt;For read-mostly workloads, snapshot computation is nearly entirely eliminated
as an overhead&amp;mdash;and even for read-write workloads the overhead is
significantly reduced.&lt;/p&gt;

&lt;p&gt;On a higher level, the changes outlined should allow applications to scale up
more easily once using Postgres 14, without having to worry about hitting
Postgres connection limits as much. Of course it still is important to pay some
attention to not use too overly many connections&amp;mdash;as outlined
&lt;a href="/blog/2020/10/08/analyzing-connection-scalability/"&gt;before&lt;/a&gt;
there are other limitations one can hit.&lt;/p&gt;

&lt;p&gt;&lt;a name="from-easy-to-hard-opportunities-for-further-improvements" id="from-easy-to-hard-opportunities-for-further-improvements"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;From easy to hard: Opportunities for further improvements&lt;/h2&gt;

&lt;p&gt;There are plenty additional snapshot scalability improvements that could be made
on top of these changes. Without moving to an entirely different snapshot
representation, even.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;As outlined above, the check whether a cached snapshot is still valid
acquires a lock. It is very likely possible to remove that lock acquisition,
and experiments show that to be a significant improvement.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Currently the snapshot caching is done for individual snapshot types, within
each backend. It may be worthwhile to optimize it, so that each backend only
has one cached snapshot. It also might be worthwhile to try to share the
cached snapshot between backends, although the inter-process coordination
that would require, makes that not too promising.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The snapshot computation is currently not very pipeline friendly. Initial
experiments show that the computation could be made more efficient by
re-arranging the computation to first assemble the set of running
transactions, then check &lt;code&gt;vacuumFlags&lt;/code&gt; and subtransaction counters in a
second loop.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Looking further into the future, it may very well be worthwhile to maintain
efficient &amp;quot;running transactions with xids&amp;quot; data structure, instead of the
current &amp;quot;xids of all established connections&amp;quot; (commonly filled largely with
invalid xids).&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;hr&gt;

&lt;section class="ref-footnotes" role="doc-endnotes"&gt;
    &lt;p&gt;&lt;strong&gt;Footnotes&lt;/strong&gt;&lt;/p&gt;
    &lt;ol role="list"&gt;
        &lt;li role="listitem"&gt;&lt;div id="fn-1"&gt;
            &lt;p&gt;Pgbench read-only results, pre/post changes:&lt;/p&gt;
            &lt;table&gt;
                &lt;thead&gt;
                    &lt;tr&gt;
                        &lt;th style="text-align:right"&gt;clients&lt;/th&gt;
                        &lt;th style="text-align:right"&gt;TPS pre&lt;/th&gt;
                        &lt;th style="text-align:right"&gt;TPS post&lt;/th&gt;
                    &lt;/tr&gt;
                &lt;/thead&gt;
                &lt;tbody&gt;
                    &lt;tr&gt;
                        &lt;td style="text-align:right"&gt;1&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;28,842&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;28,728&lt;/td&gt;
                    &lt;/tr&gt;
                    &lt;tr&gt;
                        &lt;td style="text-align:right"&gt;10&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;236,287&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;260,960&lt;/td&gt;
                    &lt;/tr&gt;
                    &lt;tr&gt;
                        &lt;td style="text-align:right"&gt;20&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;472,479&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;486,659&lt;/td&gt;
                    &lt;/tr&gt;
                    &lt;tr&gt;
                        &lt;td style="text-align:right"&gt;30&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;584,984&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;598,863&lt;/td&gt;
                    &lt;/tr&gt;
                    &lt;tr&gt;
                        &lt;td style="text-align:right"&gt;40&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;678,770&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;693,314&lt;/td&gt;
                    &lt;/tr&gt;
                    &lt;tr&gt;
                        &lt;td style="text-align:right"&gt;50&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;788,529&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;806,085&lt;/td&gt;
                    &lt;/tr&gt;
                    &lt;tr&gt;
                        &lt;td style="text-align:right"&gt;60&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1,031,483&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;986,730&lt;/td&gt;
                    &lt;/tr&gt;
                    &lt;tr&gt;
                        &lt;td style="text-align:right"&gt;70&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1,254,570&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1,332,258&lt;/td&gt;
                    &lt;/tr&gt;
                    &lt;tr&gt;
                        &lt;td style="text-align:right"&gt;80&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1,341,188&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1,438,881&lt;/td&gt;
                    &lt;/tr&gt;
                    &lt;tr&gt;
                        &lt;td style="text-align:right"&gt;90&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1,496,374&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1,673,668&lt;/td&gt;
                    &lt;/tr&gt;
                    &lt;tr&gt;
                        &lt;td style="text-align:right"&gt;100&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1,538,186&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1,651,516&lt;/td&gt;
                    &lt;/tr&gt;
                    &lt;tr&gt;
                        &lt;td style="text-align:right"&gt;125&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1,504,833&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1,621,912&lt;/td&gt;
                    &lt;/tr&gt;
                    &lt;tr&gt;
                        &lt;td style="text-align:right"&gt;150&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1,428,711&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1,570,070&lt;/td&gt;
                    &lt;/tr&gt;
                    &lt;tr&gt;
                        &lt;td style="text-align:right"&gt;175&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1,433,643&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1,572,395&lt;/td&gt;
                    &lt;/tr&gt;
                    &lt;tr&gt;
                        &lt;td style="text-align:right"&gt;200&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1,404,691&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1,523,175&lt;/td&gt;
                    &lt;/tr&gt;
                    &lt;tr&gt;
                        &lt;td style="text-align:right"&gt;250&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1,368,605&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1,541,316&lt;/td&gt;
                    &lt;/tr&gt;
                    &lt;tr&gt;
                        &lt;td style="text-align:right"&gt;300&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1,315,812&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1,490,701&lt;/td&gt;
                    &lt;/tr&gt;
                    &lt;tr&gt;
                        &lt;td style="text-align:right"&gt;400&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1,305,039&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1,520,501&lt;/td&gt;
                    &lt;/tr&gt;
                    &lt;tr&gt;
                        &lt;td style="text-align:right"&gt;500&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1,390,359&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1,639,884&lt;/td&gt;
                    &lt;/tr&gt;
                    &lt;tr&gt;
                        &lt;td style="text-align:right"&gt;600&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1,364,976&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1,715,232&lt;/td&gt;
                    &lt;/tr&gt;
                    &lt;tr&gt;
                        &lt;td style="text-align:right"&gt;700&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1,323,205&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1,716,550&lt;/td&gt;
                    &lt;/tr&gt;
                    &lt;tr&gt;
                        &lt;td style="text-align:right"&gt;800&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1,362,618&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1,698,511&lt;/td&gt;
                    &lt;/tr&gt;
                    &lt;tr&gt;
                        &lt;td style="text-align:right"&gt;900&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1,324,593&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1,705,670&lt;/td&gt;
                    &lt;/tr&gt;
                    &lt;tr&gt;
                        &lt;td style="text-align:right"&gt;1000&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1,273,755&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1,722,917&lt;/td&gt;
                    &lt;/tr&gt;
                    &lt;tr&gt;
                        &lt;td style="text-align:right"&gt;1500&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1,246,604&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1,651,516&lt;/td&gt;
                    &lt;/tr&gt;
                    &lt;tr&gt;
                        &lt;td style="text-align:right"&gt;2000&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1,171,879&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1,680,384&lt;/td&gt;
                    &lt;/tr&gt;
                    &lt;tr&gt;
                        &lt;td style="text-align:right"&gt;3000&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1,074,248&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1,651,516&lt;/td&gt;
                    &lt;/tr&gt;
                    &lt;tr&gt;
                        &lt;td style="text-align:right"&gt;4000&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1,001,631&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1,683,714&lt;/td&gt;
                    &lt;/tr&gt;
                    &lt;tr&gt;
                        &lt;td style="text-align:right"&gt;5000&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;732,530&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1,589,232&lt;/td&gt;
                    &lt;/tr&gt;
                    &lt;tr&gt;
                        &lt;td style="text-align:right"&gt;7500&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;674,862&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1,669,350&lt;/td&gt;
                    &lt;/tr&gt;
                    &lt;tr&gt;
                        &lt;td style="text-align:right"&gt;10000&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;642,042&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1,656,006&lt;/td&gt;
                    &lt;/tr&gt;
                    &lt;tr&gt;
                        &lt;td style="text-align:right"&gt;12500&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;541,565&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1,612,269&lt;/td&gt;
                    &lt;/tr&gt;
                &lt;/tbody&gt;
            &lt;/table&gt;
            &lt;a href="#fnref-1" class="footnote-backref" role="doc-backlink"&gt;↩︎&lt;/a&gt;&lt;/div&gt;
        &lt;/li&gt;
        &lt;li role="listitem"&gt;&lt;div id="fn-2"&gt;2x Xeon Gold 5215, 192GiB of RAM, Linux 5.8.5, Debian Sid &lt;a href="#fnref-2" class="footnote-backref"
                    role="doc-backlink"&gt;↩︎&lt;/a&gt;&lt;/div&gt;
        &lt;/li&gt;
        &lt;li role="listitem"&gt;&lt;div id="fn-3"&gt;
            &lt;p&gt;Idle Connections vs Active Connections, pre/post changes:&lt;/p&gt;
            &lt;table&gt;
                &lt;thead&gt;
                    &lt;tr&gt;
                        &lt;th style="text-align:right"&gt;Idle Connections&lt;/th&gt;
                        &lt;th style="text-align:right"&gt;Active Connections&lt;/th&gt;
                        &lt;th style="text-align:right"&gt;TPS pre&lt;/th&gt;
                        &lt;th style="text-align:right"&gt;TPS post&lt;/th&gt;
                    &lt;/tr&gt;
                &lt;/thead&gt;
                &lt;tbody&gt;
                    &lt;tr&gt;
                        &lt;td style="text-align:right"&gt;0&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;33599&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;33406&lt;/td&gt;
                    &lt;/tr&gt;
                    &lt;tr&gt;
                        &lt;td style="text-align:right"&gt;100&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;31088&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;33279&lt;/td&gt;
                    &lt;/tr&gt;
                    &lt;tr&gt;
                        &lt;td style="text-align:right"&gt;1000&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;29377&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;33434&lt;/td&gt;
                    &lt;/tr&gt;
                    &lt;tr&gt;
                        &lt;td style="text-align:right"&gt;2500&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;27050&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;33149&lt;/td&gt;
                    &lt;/tr&gt;
                    &lt;tr&gt;
                        &lt;td style="text-align:right"&gt;5000&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;21895&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;33903&lt;/td&gt;
                    &lt;/tr&gt;
                    &lt;tr&gt;
                        &lt;td style="text-align:right"&gt;10000&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;16034&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;33140&lt;/td&gt;
                    &lt;/tr&gt;
                    &lt;tr&gt;
                        &lt;td style="text-align:right"&gt;0&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;48&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1042005&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1125104&lt;/td&gt;
                    &lt;/tr&gt;
                    &lt;tr&gt;
                        &lt;td style="text-align:right"&gt;100&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;48&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;986731&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1103584&lt;/td&gt;
                    &lt;/tr&gt;
                    &lt;tr&gt;
                        &lt;td style="text-align:right"&gt;1000&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;48&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;854230&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1119043&lt;/td&gt;
                    &lt;/tr&gt;
                    &lt;tr&gt;
                        &lt;td style="text-align:right"&gt;2500&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;48&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;716624&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1119353&lt;/td&gt;
                    &lt;/tr&gt;
                    &lt;tr&gt;
                        &lt;td style="text-align:right"&gt;5000&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;48&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;553657&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1119476&lt;/td&gt;
                    &lt;/tr&gt;
                    &lt;tr&gt;
                        &lt;td style="text-align:right"&gt;10000&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;48&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;369845&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1115740&lt;/td&gt;
                    &lt;/tr&gt;
                &lt;/tbody&gt;
            &lt;/table&gt;
            &lt;a href="#fnref-3" class="footnote-backref" role="doc-backlink"&gt;↩︎&lt;/a&gt;&lt;/div&gt;
        &lt;/li&gt;
        &lt;li role="listitem"&gt;&lt;div id="fn-4"&gt;
            &lt;p&gt;Mostly Idle Connections vs Active Connections, pre/post changes:&lt;/p&gt;
            &lt;table&gt;
                &lt;thead&gt;
                    &lt;tr&gt;
                        &lt;th style="text-align:right"&gt;Mostly Idle Connections&lt;/th&gt;
                        &lt;th style="text-align:right"&gt;Active Connections&lt;/th&gt;
                        &lt;th style="text-align:right"&gt;TPS pre&lt;/th&gt;
                        &lt;th style="text-align:right"&gt;TPS post&lt;/th&gt;
                    &lt;/tr&gt;
                &lt;/thead&gt;
                &lt;tbody&gt;
                    &lt;tr&gt;
                        &lt;td style="text-align:right"&gt;0&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;33837&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;34095&lt;/td&gt;
                    &lt;/tr&gt;
                    &lt;tr&gt;
                        &lt;td style="text-align:right"&gt;100&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;30622&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;31166&lt;/td&gt;
                    &lt;/tr&gt;
                    &lt;tr&gt;
                        &lt;td style="text-align:right"&gt;1000&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;25523&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;28829&lt;/td&gt;
                    &lt;/tr&gt;
                    &lt;tr&gt;
                        &lt;td style="text-align:right"&gt;2500&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;19260&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;24978&lt;/td&gt;
                    &lt;/tr&gt;
                    &lt;tr&gt;
                        &lt;td style="text-align:right"&gt;5000&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;11171&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;24208&lt;/td&gt;
                    &lt;/tr&gt;
                    &lt;tr&gt;
                        &lt;td style="text-align:right"&gt;10000&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;6702&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;29577&lt;/td&gt;
                    &lt;/tr&gt;
                    &lt;tr&gt;
                        &lt;td style="text-align:right"&gt;0&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;48&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1022721&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1133153&lt;/td&gt;
                    &lt;/tr&gt;
                    &lt;tr&gt;
                        &lt;td style="text-align:right"&gt;100&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;48&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;980705&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1034235&lt;/td&gt;
                    &lt;/tr&gt;
                    &lt;tr&gt;
                        &lt;td style="text-align:right"&gt;1000&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;48&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;824668&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1115965&lt;/td&gt;
                    &lt;/tr&gt;
                    &lt;tr&gt;
                        &lt;td style="text-align:right"&gt;2500&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;48&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;698510&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1073280&lt;/td&gt;
                    &lt;/tr&gt;
                    &lt;tr&gt;
                        &lt;td style="text-align:right"&gt;5000&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;48&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;478535&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1041931&lt;/td&gt;
                    &lt;/tr&gt;
                    &lt;tr&gt;
                        &lt;td style="text-align:right"&gt;10000&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;48&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;276042&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;953567&lt;/td&gt;
                    &lt;/tr&gt;
                &lt;/tbody&gt;
            &lt;/table&gt;
            &lt;a href="#fnref-4" class="footnote-backref" role="doc-backlink"&gt;↩︎&lt;/a&gt;&lt;/div&gt;
        &lt;/li&gt;
        &lt;li role="listitem"&gt;&lt;div id="fn-5"&gt;
            &lt;p&gt;Note that commit order is not always the right order for some
                higher isolation levels. But for the purpose of this post that is not relevant. &lt;a href="#fnref-5"
                    class="footnote-backref" role="doc-backlink"&gt;↩︎&lt;/a&gt;&lt;/p&gt;&lt;/div&gt;
        &lt;/li&gt;
        &lt;li role="listitem"&gt;&lt;div id="fn-6"&gt;
            &lt;p&gt;Except in case of the PGXACT for a backend running &lt;code&gt;VACUUM&lt;/code&gt;
                or performing logical decoding, but that number usually will be small. &lt;a href="#fnref-6"
                    class="footnote-backref" role="doc-backlink"&gt;↩︎&lt;/a&gt;&lt;/p&gt;&lt;/div&gt;
        &lt;/li&gt;
        &lt;li role="listitem"&gt;&lt;div id="fn-7"&gt;
            &lt;p&gt;Without acquiring the lock it is not easily
                possible to ensure that the global xmin horizon cannot temporarily go
                backwards. That likely is OK, but requires a careful analysis. &lt;a href="#fnref-7"
                    class="footnote-backref" role="doc-backlink"&gt;↩︎&lt;/a&gt;&lt;/p&gt;&lt;/div&gt;
        &lt;/li&gt;
    &lt;/ol&gt;
&lt;/section&gt;

&lt;style&gt;
#TableOfContents ol li, #TableOfContents ol li ol {
    margin-bottom: 0;
}
&lt;/style&gt;
&lt;p&gt;&lt;em&gt;This article was originally published on &lt;a href='https://www.citusdata.com/blog/2020/10/25/improving-postgres-connection-scalability-snapshots/'&gt;citusdata.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Analyzing the Limits of Connection Scalability in Postgres</title>
    <link rel="alternate" href="https://www.citusdata.com/blog/2020/10/08/analyzing-connection-scalability/"/>
    <id>https://www.citusdata.com/blog/2020/10/08/analyzing-connection-scalability/</id>
    <published>2020-10-08T18:30:00+00:00</published>
    <updated>2020-10-08T18:30:00+00:00</updated>
    <author>Andres Freund</author>
    <content type="html">&lt;p&gt;One common challenge with Postgres for those of you who manage busy Postgres
databases, and those of you who foresee being in that situation, is that
Postgres does not handle large numbers of connections particularly well.&lt;/p&gt;

&lt;p&gt;While it is possible to have a few thousand established connections without
running into problems, there are some real and hard-to-avoid problems.&lt;/p&gt;

&lt;p&gt;Since
&lt;a href="https://techcommunity.microsoft.com/t5/azure-database-for-postgresql/microsoft-azure-welcomes-postgresql-committers/ba-p/1207612"&gt;joining Microsoft&lt;/a&gt;
last year in the &lt;a href="https://azure.microsoft.com/services/postgresql/"&gt;Azure Database for PostgreSQL&lt;/a&gt;
team&amp;mdash;where I work on open source Postgres&amp;mdash;I have spent a lot of
time analyzing and addressing some of the issues with connection scalability in
Postgres. &lt;/p&gt;

&lt;p&gt;In this post I will explain why I think it is important to improve Postgres&amp;#39;
handling of large number of connections. Followed by an analysis of the
different limiting aspects to connection scalability in Postgres.&lt;/p&gt;

&lt;p&gt;In an &lt;a href="https://techcommunity.microsoft.com/t5/azure-database-for-postgresql/improving-postgres-connection-scalability-snapshots/ba-p/1806462"&gt;upcoming post&lt;/a&gt;, I will show the results of the work we’ve done to &lt;a href="https://techcommunity.microsoft.com/t5/azure-database-for-postgresql/improving-postgres-connection-scalability-snapshots/ba-p/1806462"&gt;improve
connection handling and snapshot scalability&lt;/a&gt; in Postgres&amp;mdash;and go into
detail about the identified issues and how we have addressed them in Postgres
14.&lt;/p&gt;

&lt;hr&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="#why-connection-scalability-in-postgres-is-important"&gt;Why connection scalability in Postgres is important&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#surveying-connection-scalability-issues"&gt;Surveying connection scalability issues&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#memory-usage"&gt;Memory usage&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#constant-connection-overhead"&gt;Constant connection overhead&lt;/a&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="#conclusion-connection-memory-overhead-is-acceptable"&gt;Conclusion: connection memory overhead is acceptable&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#cache-bloat"&gt;Cache bloat&lt;/a&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="#problem-illustration"&gt;Problem illustration&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#conclusion-cache-bloat-is-not-the-major-issue-at-this-moment"&gt;Conclusion: cache bloat is not the major issue at this moment&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#query-memory-usage"&gt;Query memory usage&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#snapshot-scalability"&gt;Snapshot scalability&lt;/a&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="#cause"&gt;Cause&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#conclusion-snapshot-scalability-is-a-significant-limit"&gt;Conclusion: Snapshot scalability is a significant limit&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#connection-model--context-switches"&gt;Connection model &amp;amp; context switches&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#conclusion-start-by-improving-snapshot-scalability-in-postgres"&gt;Conclusion: Start by improving snapshot scalability in Postgres&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;hr&gt;

&lt;p&gt;&lt;a name="why-connection-scalability-in-postgres-is-important" id="why-connection-scalability-in-postgres-is-important"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;Why connection scalability in Postgres is important&lt;/h2&gt;

&lt;p&gt;In some cases problems around connection scalability are caused by
unfamiliarity with Postgres, broken applications, or other issues in the same
vein. And as I already mentioned, some applications can have a few thousand
established connections without running into any problems.&lt;/p&gt;

&lt;p&gt;A frequent counter-claim to requests to improve Postgres&amp;#39; handling of large
numbers of connection counts is that there is nothing to address. That the
desire/need to handle large numbers of connection is misguided, caused by
broken applications or similar. Often accompanied by references to the server
only having a limited number of CPU cores.&lt;/p&gt;

&lt;p&gt;There certainly are cases where the best approach is to avoid large numbers of
connections, but there are&amp;mdash;in my opinion&amp;mdash;pretty clear reasons for
needing larger number of connections in Postgres. Here are the main ones:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Central state and spikey load require large numbers of connections&lt;/strong&gt;: It is
common for a database to be &lt;strong&gt;the&lt;/strong&gt; shared state for an application (leaving
non-durable caching services aside). Given the cost of establishing a new
database connection (TLS, latency, and Postgres costs, in that order) it is
obvious that applications need to maintain pools of Postgres connections that
are large enough to handle the inevitable minor spikes in incoming
requests. Often there are many servers running [web-]application code using
one centralized database.&lt;/p&gt;

&lt;p&gt;To some degree this issue can be addressed using Postgres connection poolers like
&lt;a href="https://www.pgbouncer.org/"&gt;PgBouncer&lt;/a&gt; or more recently
&lt;a href="https://github.com/yandex/odyssey"&gt;Odyssey&lt;/a&gt;. To actually reduce the number
of connections to the database server such poolers need to be used in
transaction (or statement)
&lt;a href="https://www.pgbouncer.org/features.html"&gt;pooling modes&lt;/a&gt;.  However, doing so
precludes the use of many useful database features like
&lt;a href="https://github.com/yandex/odyssey/issues/16"&gt;prepared statements&lt;/a&gt;, temporary
tables, &amp;hellip;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Latency and result processing times lead to idle connections&lt;/strong&gt;: Network
latency and application processing times will often result in individual
database connections being idle the majority of the time, even when the
applications are issuing database requests as fast as they can.&lt;/p&gt;

&lt;p&gt;Common OLTP database workloads, and especially web applications, are heavily
biased towards reads. And with OLTP workloads, the majority of SQL queries
are simple enough to be processed well below the network latency between
application and database.&lt;/p&gt;

&lt;p&gt;Additionally the application needs to process the results of the database
queries it sent. That often will involve substantial work (e.g. template
processing, communication with cache servers, &amp;hellip;).&lt;/p&gt;

&lt;p&gt;To drive this home, here is a simple experiment using
&lt;a href="https://www.postgresql.org/docs/13/pgbench.htm"&gt;pgbench&lt;/a&gt; (a simple
benchmarking program that is part of Postgres). In a memory-resident,
read-only pgbench workload (executed on my workstation&lt;sup&gt;&lt;a name="fnref-1" id="fnref-1" href="#fn-1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;, 20/40
CPU cores/threads) I am comparing the achievable throughput across increasing
client counts between a non-delayed pgbench and a pgbench with simulated
delays. For the simulated delays, I used a 1ms network delay and a 1ms
processing delay. The non-delayed pgbench peaks around 48 clients, the
delayed run around 3000 connections. Even comparing on-machine TCP
connections to a 10GBe between two physically close machines moves the peak
from around 48 connections closer to 500 connections.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Scaling out to allow for higher connection counts can increase cost&lt;/strong&gt;: Even
in cases where the application&amp;#39;s workload can be distributed over a number of
Postgres instances, the impact of latency combined with low maximum
connection limits will often result in low utilization of the database
servers, while exerting pressure to increase the number of database servers
to handle the required number of connections. That can increase the
operational costs substantially.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;hr&gt;

&lt;p&gt;&lt;a name="surveying-connection-scalability-issues" id="surveying-connection-scalability-issues"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;Surveying connection scalability issues&lt;/h2&gt;

&lt;p&gt;My goal in starting this project was to improve Postgres&amp;#39; ability to handle
substantially larger numbers of connections. To do that&amp;mdash;to pick the right
problem to solve&amp;mdash;I first needed to understand which problems were most
important, otherwise it would have been easy to end up with micro-optimizations
without improving real-world workloads.&lt;/p&gt;

&lt;p&gt;So my first software engineering task was to survey the different aspects of
connection scalability limitations in Postgres, specifically:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="#memory-usage"&gt;Memory usage&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#snapshot-scalability"&gt;Snapshot scalability&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#connection-model--context-switches"&gt;Connection model&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;By the end of this deep dive into the connection scalability limitations in Postgres, I
hope you will understand why I
&lt;a href="#conclusion-start-by-improving-snapshot-scalability"&gt;concluded&lt;/a&gt;
that snapshot scalability should be addressed first.&lt;/p&gt;

&lt;hr&gt;

&lt;p&gt;&lt;a name="memory-usage" id="memory-usage"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;Memory usage&lt;/h2&gt;

&lt;p&gt;There are 3 main aspects to problems around memory usage of a large numbers of
connections:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="#constant-connection-overhead"&gt;Constant connection overhead&lt;/a&gt;, the amount of
memory an established connection uses&lt;/li&gt;
&lt;li&gt;&lt;a href="#cache-bloat"&gt;Cache bloat&lt;/a&gt;, the increase in memory usage due to large
numbers of database objects&lt;/li&gt;
&lt;li&gt;&lt;a href="#query-memory-usage"&gt;Query memory usage&lt;/a&gt;, memory used by query execution
itself&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a name="constant-connection-overhead" id="constant-connection-overhead"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;Constant connection overhead&lt;/h2&gt;

&lt;p&gt;Postgres, as many of you will know, uses a process-based connection model.
When a new connection is established, Postgres&amp;#39; supervisor process creates a
dedicated process to handle that connection going forward. The use of a &amp;quot;full
blown process&amp;quot; over the use of of threads has some advantages like increased
isolation/robustness, but also some disadvantages.&lt;/p&gt;

&lt;p&gt;One common complaint is that each connection uses too much memory. That is, at
least partially, a common observation because it is surprisingly hard to
measure the increase in memory usage by an additional connection.&lt;/p&gt;

&lt;p&gt;In a recent post about measuring the
&lt;a href="https://blog.anarazel.de/2020/10/07/measuring-the-memory-overhead-of-a-postgres-connection/"&gt;memory overhead of a Postgres connection&lt;/a&gt;
I show that it is surprisingly hard to accurately measure the memory
overhead. And that in many workloads, and with the right configuration&amp;mdash;most
importantly, using
&lt;a href="https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-SHARED-BUFFERS"&gt;huge_pages&lt;/a&gt;&amp;mdash;the memory overhead of each connection is
&lt;a href="https://blog.anarazel.de/2020/10/07/measuring-the-memory-overhead-of-a-postgres-connection#conclusion"&gt;below 2 MiB&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a name="conclusion-connection-memory-overhead-is-acceptable" id="conclusion-connection-memory-overhead-is-acceptable"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;Conclusion: connection memory overhead is acceptable&lt;/h3&gt;

&lt;p&gt;When each connection only has an overhead of a few MiB, it is quite possible to
have thousands of established connections. It would obviously be good to use
less memory, but memory is not the primary issue around connection scalability.&lt;/p&gt;

&lt;p&gt;&lt;a name="cache-bloat" id="cache-bloat"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;Cache bloat&lt;/h2&gt;

&lt;p&gt;Another important aspect of memory-related connection scalability issues can be
that, over time, the memory usage of a connection increases, due to long-lived
resources. This particularly is an issue in workloads that utilize long-lived
connections combined with schema-based multi-tenancy.&lt;/p&gt;

&lt;p&gt;Unless applications implement some form of connection &amp;lt;-&amp;gt; tenant association,
each connection over time will access all relations for all tenants. That leads
to Postgres&amp;#39; internal catalog metadata caches growing beyond a reasonable size,
as currently (as of version 13) Postgres does not prune its metadata caches of
unchanging rarely-accessed contents.&lt;/p&gt;

&lt;p&gt;&lt;a name="problem-illustration" id="problem-illustration"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;Problem illustration&lt;/h3&gt;

&lt;p&gt;To demonstrate the issue of cache bloat, I created a simple test bed
with 100k tables, with a few columns and single primary serial column
index&lt;sup&gt;&lt;a name="fnref-2" id="fnref-2" href="#fn-2" class="footnote-ref" role="doc-noteref"&gt;2&lt;/a&gt;&lt;/sup&gt;. Takes a while to create.&lt;/p&gt;

&lt;p&gt;With the
&lt;a href="https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=3e98c0bafb28de87ae095b341687dc082371af54"&gt;recently added&lt;/a&gt;
&lt;code&gt;pg_backend_memory_contexts&lt;/code&gt; view it is not too difficult to see the
aggregated memory usage of the various caches (although it would be
nice to see more of the different types of caches broken out into
their own memory contexts). See &lt;sup&gt;&lt;a name="fnref-3" id="fnref-3" href="#fn-3" class="footnote-ref" role="doc-noteref"&gt;3&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;

&lt;p&gt;In a new Postgres connection, not much memory is used:&lt;/p&gt;

&lt;table&gt;&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;name&lt;/th&gt;
&lt;th&gt;parent&lt;/th&gt;
&lt;th style="text-align: right"&gt;size_bytes&lt;/th&gt;
&lt;th style="text-align: right"&gt;size_human&lt;/th&gt;
&lt;th style="text-align: right"&gt;num_contexts&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CacheMemoryContext&lt;/td&gt;
&lt;td&gt;TopMemoryContext&lt;/td&gt;
&lt;td style="text-align: right"&gt;524288&lt;/td&gt;
&lt;td style="text-align: right"&gt;512 kB&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;index info&lt;/td&gt;
&lt;td&gt;CacheMemoryContext&lt;/td&gt;
&lt;td style="text-align: right"&gt;149504&lt;/td&gt;
&lt;td style="text-align: right"&gt;146 kB&lt;/td&gt;
&lt;td style="text-align: right"&gt;80&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;relation rules&lt;/td&gt;
&lt;td&gt;CacheMemoryContext&lt;/td&gt;
&lt;td style="text-align: right"&gt;8192&lt;/td&gt;
&lt;td style="text-align: right"&gt;8192 bytes&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;

&lt;p&gt;But after forcing all Postgres tables we just created to be accessed&lt;sup&gt;&lt;a name="fnref-4" id="fnref-4" href="#fn-4" class="footnote-ref" role="doc-noteref"&gt;4&lt;/a&gt;&lt;/sup&gt;, this
looks very different:&lt;/p&gt;

&lt;table&gt;&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;name&lt;/th&gt;
&lt;th&gt;parent&lt;/th&gt;
&lt;th style="text-align: right"&gt;size_bytes&lt;/th&gt;
&lt;th style="text-align: right"&gt;size_human&lt;/th&gt;
&lt;th style="text-align: right"&gt;num_contexts&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CacheMemoryContext&lt;/td&gt;
&lt;td&gt;TopMemoryContext&lt;/td&gt;
&lt;td style="text-align: right"&gt;621805848&lt;/td&gt;
&lt;td style="text-align: right"&gt;593 MB&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;index info&lt;/td&gt;
&lt;td&gt;CacheMemoryContext&lt;/td&gt;
&lt;td style="text-align: right"&gt;102560768&lt;/td&gt;
&lt;td style="text-align: right"&gt;98 MB&lt;/td&gt;
&lt;td style="text-align: right"&gt;100084&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;relation rules&lt;/td&gt;
&lt;td&gt;CacheMemoryContext&lt;/td&gt;
&lt;td style="text-align: right"&gt;8192&lt;/td&gt;
&lt;td style="text-align: right"&gt;8192 bytes&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;

&lt;p&gt;As the metadata cache for indexes is created in its own memory context,
num_contexts for the &amp;quot;index info&amp;quot; contexts nicely shows that we accessed the
100k tables (and some system internal ones).&lt;/p&gt;

&lt;p&gt;&lt;a name="conclusion-cache-bloat-is-not-the-major-issue-at-this-moment" id="conclusion-cache-bloat-is-not-the-major-issue-at-this-moment"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;Conclusion: cache bloat is not the major issue at this moment&lt;/h3&gt;

&lt;p&gt;A common solution for the cache bloat issue is to drop &amp;quot;old&amp;quot; connections from the
application connection pooler after a certain age. Many connection pooler
libraries/web frameworks support that.&lt;/p&gt;

&lt;p&gt;As there is a feasible workaround, and as cache bloat is only an issue
for databases with a &lt;em&gt;lot&lt;/em&gt; of objects, cache bloat is not &lt;em&gt;the&lt;/em&gt; major issue at the
moment (but worthy of improvement, obviously).&lt;/p&gt;

&lt;p&gt;&lt;a name="query-memory-usage" id="query-memory-usage"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;Query memory usage&lt;/h2&gt;

&lt;p&gt;The third aspect is that it is hard to limit memory used by queries. The
&lt;a href="https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-WORK-MEM"&gt;work_mem&lt;/a&gt;
setting does not control the memory used by a query as a whole, but only of
individual parts of a query (e.g. sort, hash aggregation, hash join). That
means that a query can end up requiring &lt;code&gt;work_mem&lt;/code&gt; several times over&lt;sup&gt;&lt;a name="fnref-5" id="fnref-5" href="#fn-5" class="footnote-ref" role="doc-noteref"&gt;5&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;

&lt;p&gt;That means that one has to be careful setting &lt;code&gt;work_mem&lt;/code&gt; in workloads requiring
a lot of connections. With larger &lt;code&gt;work_mem&lt;/code&gt; settings, practically required for
analytics workloads, one can&amp;#39;t reasonably use a huge number of concurrent
connections and expect to never hit memory exhaustion related issues
(i.e. errors or the OOM killer).&lt;/p&gt;

&lt;p&gt;Luckily most workloads requiring a lot of connection don&amp;#39;t need a high
&lt;code&gt;work_mem&lt;/code&gt; setting, and it can be set on the user, database, connection, and
transaction level.&lt;/p&gt;

&lt;hr&gt;

&lt;p&gt;&lt;a name="snapshot-scalability" id="snapshot-scalability"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;Snapshot scalability&lt;/h2&gt;

&lt;p&gt;There are a lot of recommendations out there strongly recommending to not set
&lt;a href="https://www.postgresql.org/docs/current/runtime-config-connection.html#GUC-MAX-CONNECTIONS"&gt;max_connections&lt;/a&gt;
for Postgres to a high value, as high values can cause problems. In fact, I&amp;#39;ve argued that
myself many times.&lt;/p&gt;

&lt;p&gt;But that is only half the truth.&lt;/p&gt;

&lt;p&gt;Setting max_connections to a very high value alone only
leads at best (worst?) to a very small slowdown in itself, and wastes some
memory. E.g. on my workstation&lt;sup&gt;&lt;a href="#fn-1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt; there is no measurable
performance difference for a read-only pgbench between &lt;code&gt;max_connections=100&lt;/code&gt;
and a value as extreme &lt;code&gt;max_connections=100000&lt;/code&gt; (for the same pgbench client
count, 48 in this case). However the memory required for Postgres does increase
measurable with such an extreme setting. With &lt;code&gt;shared_buffers=16GB&lt;/code&gt;
&lt;code&gt;max_connections=100&lt;/code&gt; uses 16804 MiB, &lt;code&gt;max_connections=100000&lt;/code&gt; uses 21463 MiB
of shared memory. That is a large enough difference to potentially cause a
slowdown indirectly (although most of that memory will never be used, therefore
not allocated by the OS in common configurations).&lt;/p&gt;

&lt;p&gt;The real issue is that currently Postgres does not scale well to having a large
number of established connections, even if nearly all connections are idle.&lt;/p&gt;

&lt;p&gt;To showcase this, I used two separate pgbench&lt;sup&gt;&lt;a name="fnref-6" id="fnref-6" href="#fn-6" class="footnote-ref" role="doc-noteref"&gt;6&lt;/a&gt;&lt;/sup&gt; runs. One of them just
establishes connections that are entirely idle (using a test file that just
contains &lt;code&gt;\sleep 1s&lt;/code&gt;, causing a client-side sleep). Another to run a normal
pgbench read-only workload.&lt;/p&gt;

&lt;p&gt;This is far from reproducing the worst possible version of the issue, as
normally the set of idle connections varies over time, which makes this issue
considerably worse. This version is much easier to reproduce however.&lt;/p&gt;

&lt;p&gt;This is a very useful scenario to test, because it allows us to isolate the
cost of additional connections pretty well isolated. Especially when the count
of active connections is low, the system CPU usage is quite low. If there is a
slowdown when the number of idle connections increases, it is clearly related
to the number of idle connections.&lt;/p&gt;

&lt;p&gt;If we instead measured the throughput with a high number of active connections,
it&amp;#39;d be harder to pinpoint whether e.g. the increase in context switches or
lack of CPU cycles is to blame for slowdowns.&lt;/p&gt;

&lt;figure&gt;
&lt;img src="https://cdn.citusdata.com/images/blog/performance-impact-of-idle-connections-1active.png" alt="graph showing significant performance degradation at higher idle connection counts" /&gt;
&lt;figcaption&gt;Throughput of one active connection in presence of a variable number of idle connections&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;figure&gt;
&lt;img src="https://cdn.citusdata.com/images/blog/performance-impact-of-idle-connections-48active.png" alt="graph showing significant performance degradation at higher idle connection counts" /&gt;
&lt;figcaption&gt;Throughput of 48 active connections in presence of a variable number of idle connections&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;These results&lt;sup&gt;&lt;a name="fnref-7" id="fnref-7" href="#fn-7" class="footnote-ref" role="doc-noteref"&gt;7&lt;/a&gt;&lt;/sup&gt; clearly show that the
achievable throughput of active connections decreases significantly when the
number of idle connections increases.&lt;/p&gt;

&lt;p&gt;In reality &amp;quot;idle&amp;quot; connections are not entirely idle, but send queries at a
lower rate. To simulate that I&amp;#39;ve used the the below to simulate clients only
occasionally sending queries:&lt;/p&gt;
    &lt;div class="highlight"&gt;
      &lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;sleep&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="n"&gt;ms&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
      &lt;button class="copy-button" data-clipboard-action="copy" data-clipboard-text="\sleep 100ms
SELECT 1;
"&gt;Copy&lt;/button&gt;
    &lt;/div&gt;

&lt;figure&gt;
&lt;img src="https://cdn.citusdata.com/images/blog/performance-impact-of-mostly-idle-connections-1active.png" alt="graph showing significant performance degradation at higher mostly-idle connection counts" /&gt;
&lt;figcaption&gt;Throughput of one active connection in presence of a variable number of mostly-idle connections&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;figure&gt;
&lt;img src="https://cdn.citusdata.com/images/blog/performance-impact-of-mostly-idle-connections-48active.png" alt="graph showing significant performance degradation at higher mostly-idle connection counts" /&gt;
&lt;figcaption&gt;Throughput of 48 active connections in presence of a variable number of mostly-idle connections&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;The results&lt;sup&gt;&lt;a name="fnref-8" id="fnref-8" href="#fn-8" class="footnote-ref" role="doc-noteref"&gt;8&lt;/a&gt;&lt;/sup&gt; show that the slightly
more realistic scenario causes active connections to slow down even worse.&lt;/p&gt;

&lt;p&gt;&lt;a name="cause" id="cause"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;Cause&lt;/h3&gt;

&lt;p&gt;Together these results very clearly show that there is a significant issue
handling large connection counts, even when CPU/memory are plentiful. The fact
that a single active connection slows down by more than 2x due to concurrent
idle connections points to a very clear issue.&lt;/p&gt;

&lt;p&gt;A CPU profile quickly pinpoints the part of Postgres responsible:
&lt;figure&gt;
&lt;img src="https://cdn.citusdata.com/images/blog/profile_1active_with_5000_idle.png" alt="50% of the CPU time is spent in GetSnapshotData()" /&gt;
&lt;figcaption&gt;Profile of one active connection running read-only pgbench concurrently with 5000 idle connections, bottleneck is clearly in &lt;code&gt;GetSnapshotData()&lt;/code&gt;&lt;/figcaption&gt;
&lt;/figure&gt;&lt;/p&gt;

&lt;p&gt;Obviously the bottleneck is entirely in the &lt;code&gt;GetSnapshotData()&lt;/code&gt; function. That
function performs the bulk of the work necessary to provide readers with
&lt;a href="https://www.postgresql.org/docs/devel/mvcc-intro.html"&gt;transaction isolation&lt;/a&gt;.
&lt;code&gt;GetSnapshotData()&lt;/code&gt; builds so called &amp;quot;snapshots&amp;quot; that describe which effects of
concurrent transactions are visible to a transaction, and which are not. These
snapshots are built very frequently (at least once per transaction, &lt;em&gt;very&lt;/em&gt;
commonly more often).&lt;/p&gt;

&lt;p&gt;Even without knowing its implementation, it does make some intuitive sense (at
least I think so, but I also know what it does) that such a task gets more
expensive the more connections/transactions need to be handled.&lt;/p&gt;

&lt;p&gt;Two blog posts by Brandur explain the mechanics and issues surrounding this in
more detail:
- &lt;a href="https://brandur.org/postgres-atomicity"&gt;How Postgres Makes Transactions Atomic&lt;/a&gt;
- &lt;a href="https://brandur.org/postgres-connections"&gt;How to Manage Connections Efficiently in Postgres, or Any Database&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a name="conclusion-snapshot-scalability-is-a-significant-limit" id="conclusion-snapshot-scalability-is-a-significant-limit"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;Conclusion: Snapshot scalability is a significant limit&lt;/h3&gt;

&lt;p&gt;A large number of connections clearly reduce the efficiency of other
connections, even when idle (which as explained above, is very common). Except
for reducing the number of concurrent connections and issuing fewer queries,
there is no real workaround for the snapshot scalability issue.&lt;/p&gt;

&lt;hr&gt;

&lt;p&gt;&lt;a name="connection-model--context-switches" id="connection-model--context-switches"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;Connection model &amp;amp; context switches&lt;/h2&gt;

&lt;p&gt;As mentioned &lt;a href="#constant-connection-overhead"&gt;above&lt;/a&gt;, Postgres uses a
one-process-per-connection model. That works well in a lot of cases, but is a
limiting factor for dealing with 10s to 100s of thousands of connections.&lt;/p&gt;

&lt;p&gt;Whenever a query is received by a backend process, the kernel needs to perform
a context switch to that process. That is not cheap. But more importantly, once
the result for the query has been computed, the backend will commonly be idle
for a while&amp;mdash;the query result has to traverse the network, be received and
processed by the application, before the application sends a new query. That
means on a busy server another process/backend/connection will need to be
scheduled&amp;mdash;another context switch (cross-process context switches are more
expensive than doing process-kernel-same process, e.g. as part of a syscall).&lt;/p&gt;

&lt;p&gt;Note that switching to a one-thread-per-connection model does not address this
issue to a meaningful degree: while some of the context switches may get cheaper,
context switches still are the major limit. There &lt;em&gt;are&lt;/em&gt; reasons to consider
switching to threads, but connection scalability itself is not a major one
(without additional architectural changes, some of which may be easier using
threads).&lt;/p&gt;

&lt;p&gt;To handle huge numbers of connections a different type of connection model is
needed. Instead of using a process/thread-per-connection model, a fixed/limited
number of processes/threads need to handle all connections. By waiting for
incoming queries on many connections at once and then processing many queries
without being interrupted by the OS CPU scheduler, efficiency can very
significantly be improved.&lt;/p&gt;

&lt;p&gt;This is not a brilliant insight by me. Architectures like this are in wide use,
and have widely been discussed. See e.g. the
&lt;a href="https://en.wikipedia.org/wiki/C10k_problem"&gt;C10k problem&lt;/a&gt;, coined in 1999.&lt;/p&gt;

&lt;p&gt;Besides avoiding context switches, there are many other performance benefits
that can be gained. E.g. on higher core count machines, a lot of performance
can be gained by increasing locality of shared memory, e.g. by binding specific
processes/threads and regions of memory to specific CPU cores.&lt;/p&gt;

&lt;p&gt;However, changing Postgres to support a different kind of connection model like this is a huge
undertaking. That does not just require carefully separating many dependencies
between processes and connections, but also user-land scheduling between
different queries, support for asynchronous IO, likely a different query
execution model (to avoid needing a separate stack for each query), and much
more.&lt;/p&gt;

&lt;hr&gt;

&lt;p&gt;&lt;a name="conclusion-start-by-improving-snapshot-scalability-in-postgres" id="conclusion-start-by-improving-snapshot-scalability-in-postgres"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;Conclusion: Start by improving snapshot scalability in Postgres&lt;/h2&gt;

&lt;p&gt;In my opinion, the memory usage issues are not as severe as the other issues
discussed. Partially because the memory overhead of connections is less big
than it initially appears, and partially because issues like Postgres&amp;#39; caches
using too much memory can be worked around reasonably.&lt;/p&gt;

&lt;p&gt;We could, and &lt;strong&gt;should&lt;/strong&gt;, make improvements around memory usage in Postgres,
and there are several low enough hanging fruits. But I don&amp;#39;t think, as things
currently are, that improving memory usage would, on its own, change the
picture around connection scalability, at least not on a fundamental level.&lt;/p&gt;

&lt;p&gt;In contrast, there is no good way to work around the snapshot scalability
issues. Reducing the number of established connections significantly is often
not feasible, as explained above. There aren&amp;#39;t really any other workarounds.&lt;/p&gt;

&lt;p&gt;Additionally, as the snapshot scalability issue is very localized, it is quite
feasible to tackle it. There are no fundamental paradigm shifts necessary.&lt;/p&gt;

&lt;p&gt;Lastly, there is the aspect of wanting to handle many tens of thousands of
connections, likely by entirely switching the connection model. As outlined,
that is a huge project/fundamental paradigm shift. That doesn&amp;#39;t mean it
should not be tackled, obviously.&lt;/p&gt;

&lt;p&gt;Addressing the snapshot scalability issue first thus seems worthwhile,
promising significant benefits on its own.&lt;/p&gt;

&lt;p&gt;But there&amp;#39;s also a more fundamental reason for tackling snapshot scalability
first: While e.g. addressing some memory usage issues at the same time,
switching the connection model would not at all address the snapshot issue. We
would obviously still need to provide isolation between the connections, even
if a connection wouldn&amp;#39;t have a dedicated process anymore.&lt;/p&gt;

&lt;p&gt;Hopefully now you understand why I chose to focus on Postgres snapshot scalability
first. More about that in my next &lt;a href="https://techcommunity.microsoft.com/t5/azure-database-for-postgresql/improving-postgres-connection-scalability-snapshots/ba-p/1806462"&gt;blog post on improving Postgres connection scalability&lt;/a&gt;.&lt;/p&gt;

&lt;hr&gt;

&lt;section class="ref-footnotes" role="doc-endnotes"&gt;
    &lt;p&gt;&lt;strong&gt;Footnotes&lt;/strong&gt;&lt;/p&gt;
    &lt;ol role="list"&gt;
        &lt;li role="listitem"&gt;&lt;div id="fn-1"&gt;
            2x xeon gold 5215, 192GiB of RAM, kernel 5.8.5, debian Sid &lt;a href="#fnref-1" class="footnote-backref" role="doc-backlink"&gt;↩︎&lt;/a&gt;
        &lt;/li&gt;
        &lt;li role="listitem"&gt;&lt;div id="fn-2"&gt;
            &lt;p&gt;Creating 100k tables with psql:&lt;/p&gt;
            &lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="gp"&gt;postgres[2627319][1]=# &lt;/span&gt;SELECT format&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'begin;create table foo_%1s(id serial primary key, data1 int, data2 text, data3 json);commit;'&lt;/span&gt;, g.i&lt;span class="o"&gt;)&lt;/span&gt; FROM generate_series&lt;span class="o"&gt;(&lt;/span&gt;1, 100000&lt;span class="o"&gt;)&lt;/span&gt; g&lt;span class="o"&gt;(&lt;/span&gt;i&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="se"&gt;\g&lt;/span&gt;&lt;span class="nb"&gt;exec
&lt;/span&gt;COMMIT
COMMIT
&amp;hellip;
&lt;/code&gt;&lt;/pre&gt; &lt;a href="#fnref-2" class="footnote-backref" role="doc-backlink"&gt;↩︎&lt;/a&gt;&lt;/div&gt;
        &lt;/li&gt;
        &lt;li role="listitem"&gt;&lt;div id="fn-3"&gt;
            &lt;p&gt;Query cache memory usage:&lt;/p&gt;
            &lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="k"&gt;RECURSIVE&lt;/span&gt; &lt;span class="n"&gt;contexts&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_backend_memory_contexts&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;caches&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
        &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;contexts&lt;/span&gt;
        &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'CacheMemoryContext'&lt;/span&gt;
      &lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;
        &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;contexts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;
        &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;caches&lt;/span&gt;
        &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;contexts&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;contexts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;caches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;size_bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;pg_size_pretty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_bytes&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="n"&gt;size_human&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;num_contexts&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;caches&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parent&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt; &lt;a href="#fnref-3" class="footnote-backref" role="doc-backlink"&gt;↩︎&lt;/a&gt;&lt;/div&gt;
        &lt;/li&gt;
        &lt;li role="listitem"&gt;&lt;div id="fn-4"&gt;
            &lt;p&gt;Query to access all tables named &lt;code&gt;foo*&lt;/code&gt;:&lt;/p&gt;
            &lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;DO&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;
  &lt;span class="k"&gt;DECLARE&lt;/span&gt;
    &lt;span class="n"&gt;cnt&lt;/span&gt; &lt;span class="n"&gt;int&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;BEGIN&lt;/span&gt;
    &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_class&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;relkind&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'r'&lt;/span&gt; &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="n"&gt;relname&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'foo%'&lt;/span&gt; &lt;span class="n"&gt;LOOP&lt;/span&gt;
        &lt;span class="k"&gt;EXECUTE&lt;/span&gt; &lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'SELECT count(*) FROM %s'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;oid&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;regclass&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;cnt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cnt&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="n"&gt;cnt&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
            &lt;span class="k"&gt;COMMIT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="n"&gt;LOOP&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;RAISE&lt;/span&gt; &lt;span class="n"&gt;NOTICE&lt;/span&gt; &lt;span class="s1"&gt;'tables %1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cnt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="err"&gt;$$&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt; &lt;a href="#fnref-4" class="footnote-backref" role="doc-backlink"&gt;↩︎&lt;/a&gt;&lt;/div&gt;
        &lt;/li&gt;
        &lt;li role="listitem"&gt;&lt;div id="fn-5"&gt;
            &lt;p&gt;Even worse, there can also be several queries in progress
                at the same time, e.g. due to the use of cursors. It is however not common
                to concurrently use many cursors. &lt;a href="#fnref-5" class="footnote-backref" role="doc-backlink"&gt;↩︎&lt;/a&gt;
            &lt;/p&gt;&lt;/div&gt;
        &lt;/li&gt;
        &lt;li role="listitem"&gt;&lt;div id="fn-6"&gt;
            &lt;p&gt;This is with pgbench modified to wait until all connections are
                established. Without that pgbench modification, sometimes a subset of
                clients may not be able to connect, particularly before the fixes described
                in this article. See this
                &lt;a href="https://www.postgresql.org/message-id/20200227180100.zyvjwzcpiokfsqm2%40alap3.anarazel.de"&gt;mailing
                    listpost&lt;/a&gt;
                for details. &lt;a href="#fnref-6" class="footnote-backref" role="doc-backlink"&gt;↩︎&lt;/a&gt;&lt;/p&gt;&lt;/div&gt;
        &lt;/li&gt;
        &lt;li role="listitem"&gt;&lt;div id="fn-7"&gt;
            &lt;p&gt;Idle Connections vs Active Connections:&lt;/p&gt;
            &lt;table&gt;
                &lt;thead&gt;
                    &lt;tr&gt;
                        &lt;th&gt;PG Version&lt;/th&gt;
                        &lt;th style="text-align:right"&gt;Idle Connections&lt;/th&gt;
                        &lt;th style="text-align:right"&gt;Active Connections&lt;/th&gt;
                        &lt;th style="text-align:right"&gt;TPS&lt;/th&gt;
                    &lt;/tr&gt;
                &lt;/thead&gt;
                &lt;tbody&gt;
                    &lt;tr&gt;
                        &lt;td&gt;12&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;0&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;33457&lt;/td&gt;
                    &lt;/tr&gt;
                    &lt;tr&gt;
                        &lt;td&gt;12&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;100&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;33705&lt;/td&gt;
                    &lt;/tr&gt;
                    &lt;tr&gt;
                        &lt;td&gt;12&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1000&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;30558&lt;/td&gt;
                    &lt;/tr&gt;
                    &lt;tr&gt;
                        &lt;td&gt;12&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;2500&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;26075&lt;/td&gt;
                    &lt;/tr&gt;
                    &lt;tr&gt;
                        &lt;td&gt;12&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;5000&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;23284&lt;/td&gt;
                    &lt;/tr&gt;
                    &lt;tr&gt;
                        &lt;td&gt;12&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;10000&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;14496&lt;/td&gt;
                    &lt;/tr&gt;
                    &lt;tr&gt;
                        &lt;td&gt;12&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;0&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;48&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1032435&lt;/td&gt;
                    &lt;/tr&gt;
                    &lt;tr&gt;
                        &lt;td&gt;12&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;100&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;48&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;960847&lt;/td&gt;
                    &lt;/tr&gt;
                    &lt;tr&gt;
                        &lt;td&gt;12&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1000&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;48&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;902109&lt;/td&gt;
                    &lt;/tr&gt;
                    &lt;tr&gt;
                        &lt;td&gt;12&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;2500&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;48&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;759723&lt;/td&gt;
                    &lt;/tr&gt;
                    &lt;tr&gt;
                        &lt;td&gt;12&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;5000&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;48&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;702680&lt;/td&gt;
                    &lt;/tr&gt;
                    &lt;tr&gt;
                        &lt;td&gt;12&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;10000&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;48&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;521558&lt;/td&gt;
                    &lt;/tr&gt;
                &lt;/tbody&gt;
            &lt;/table&gt;
            &lt;a href="#fnref-7" class="footnote-backref" role="doc-backlink"&gt;↩︎&lt;/a&gt;&lt;/div&gt;
        &lt;/li&gt;
        &lt;li role="listitem"&gt;&lt;div id="fn-8"&gt;
            &lt;p&gt;Mostly Idle Connections vs Active Connections:&lt;/p&gt;
            &lt;table&gt;
                &lt;thead&gt;
                    &lt;tr&gt;
                        &lt;th&gt;PG Version&lt;/th&gt;
                        &lt;th style="text-align:right"&gt;Less active Connections&lt;/th&gt;
                        &lt;th style="text-align:right"&gt;Active Connections&lt;/th&gt;
                        &lt;th style="text-align:right"&gt;TPS&lt;/th&gt;
                    &lt;/tr&gt;
                &lt;/thead&gt;
                &lt;tbody&gt;
                    &lt;tr&gt;
                        &lt;td&gt;12&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;0&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;33773&lt;/td&gt;
                    &lt;/tr&gt;
                    &lt;tr&gt;
                        &lt;td&gt;12&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;100&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;29074&lt;/td&gt;
                    &lt;/tr&gt;
                    &lt;tr&gt;
                        &lt;td&gt;12&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1000&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;25327&lt;/td&gt;
                    &lt;/tr&gt;
                    &lt;tr&gt;
                        &lt;td&gt;12&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;2500&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;19752&lt;/td&gt;
                    &lt;/tr&gt;
                    &lt;tr&gt;
                        &lt;td&gt;12&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;5000&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;9807&lt;/td&gt;
                    &lt;/tr&gt;
                    &lt;tr&gt;
                        &lt;td&gt;12&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;10000&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;6049&lt;/td&gt;
                    &lt;/tr&gt;
                    &lt;tr&gt;
                        &lt;td&gt;12&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;0&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;48&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1040616&lt;/td&gt;
                    &lt;/tr&gt;
                    &lt;tr&gt;
                        &lt;td&gt;12&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;100&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;48&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;953755&lt;/td&gt;
                    &lt;/tr&gt;
                    &lt;tr&gt;
                        &lt;td&gt;12&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;1000&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;48&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;759366&lt;/td&gt;
                    &lt;/tr&gt;
                    &lt;tr&gt;
                        &lt;td&gt;12&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;2500&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;48&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;733000&lt;/td&gt;
                    &lt;/tr&gt;
                    &lt;tr&gt;
                        &lt;td&gt;12&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;5000&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;48&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;636057&lt;/td&gt;
                    &lt;/tr&gt;
                    &lt;tr&gt;
                        &lt;td&gt;12&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;10000&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;48&lt;/td&gt;
                        &lt;td style="text-align:right"&gt;416819&lt;/td&gt;
                    &lt;/tr&gt;
                &lt;/tbody&gt;
            &lt;/table&gt;
            &lt;a href="#fnref-8" class="footnote-backref" role="doc-backlink"&gt;↩︎&lt;/a&gt;&lt;/div&gt;
        &lt;/li&gt;
    &lt;/ol&gt;
&lt;/section&gt;

&lt;style&gt;.blog-article-content__text&gt;ol:first-of-type{font-size:85%}.blog-article-content ol:first-of-type li,.blog-article-content ol:first-of-type li ol{margin-bottom:0}.blog-article-content__text&gt;table:first-of-type th:first-child,.blog-article-content__text&gt;table:first-of-type th:nth-child(2),.blog-article-content__text&gt;table:nth-of-type(2) th:first-child,.blog-article-content__text&gt;table:nth-of-type(2) th:nth-child(2){width:25%} .blog-article-content__text&gt;table:first-of-type, .blog-article-content__text&gt;table:nth-of-type(2) {font-size: 0.73em!important; margin-bottom: 3em;}&lt;/style&gt;
&lt;p&gt;&lt;em&gt;This article was originally published on &lt;a href='https://www.citusdata.com/blog/2020/10/08/analyzing-connection-scalability/'&gt;citusdata.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;</content>
  </entry>
</feed>
