在 Crunchy,我们谈论了很多关于内存、共享缓冲区和缓存命中率的内容。甚至我们新的 Playground 教程 也可以帮助用户了解内存使用情况。许多这些对话的要点是,您希望将大部分经常访问的数据放在离数据库最近的内存池中,即共享缓冲区缓存中。
使用 Postgres 的应用程序的数据流远不止这些。数据库前面可能有应用程序级别的池和 Redis 缓存。即使在数据库服务器上,数据也存在于多个层,包括内核和各种磁盘缓存。因此,对于那些想了解整个故事的人来说,这篇文章将 Postgres 读取和写入的完整数据流汇总在一起,从头到尾。
应用服务器
应用程序服务器将查询发送到单个 PostgreSQL 后端并取回结果集。然而,实际上这里可能有多个数据层在起作用。
应用程序缓存
应用程序缓存可以有很多层/地方:
- 浏览器级缓存:客户端可以重新使用先前请求的资源,而无需从应用程序服务器请求新副本。
- 反向代理缓存,即Cloudflare、Nginx:用户请求资源,但甚至不需要命中应用服务器即可返回结果。
- 单独的每个工作进程缓存:在特定的应用程序代码中,每个后端都可以存储一些状态以减少对数据库的查询。
- 特定于框架的结果或片段缓存:可以存储和返回部分资源的全部部分,或者可以将整个数据库结果集存储在本地或数据库本身之外的共享资源中,以完全消除访问数据库的需要。这可能是 Redis 或 memcached 之类的东西,仅举几个例子。
应用程序连接池
当应用程序使用上述方法之一请求未缓存的数据时,应用程序将启动与数据库的上游连接。许多应用程序框架并不总是直接连接,而是支持应用程序级池。应用程序池允许多个工作人员在它们之间共享少量的数据库连接。这减少了所需的内存等资源。同时,重用打开的连接减少了创建新数据库连接的平均时间。
PostgreSQL 服务器
一旦我们达到数据库连接的级别,我们就可以看到数据在那里流动的一些方式。到数据库的连接可以是直接的或通过数据库池。
连接池
与应用程序级池类似,数据库池可以放置在传入的数据库连接和 PostgreSQL 服务器后端之间。 pgBouncer 是事实上的连接池工具。连接池允许请求在具有类似连接要求的其他请求之间共享数据库资源。这还可以确保您更有效地使用更少的连接,而不是拥有许多空闲连接。
数据库池充当某种代理,将客户端请求与少量上游 PostgreSQL 连接混合在一起。
客户端后端
当与 PostgreSQL postmaster 建立连接时,将启动客户端后端与它进行通信。这个单独的后端为特定连接的所有查询提供服务并返回结果集。客户端后端通过使用 shared_buffers 内存段来协调对表或索引数据的访问来做到这一点。这是请求和返回的数据不再是“逻辑”请求并深入到文件系统的点。
共享缓冲区/缓冲区缓存
当查询需要来自特定表的数据时,它将首先检查 shared_buffers 以查看目标块是否已存在于那里。如果没有,它会将块从磁盘 IO 系统读入 shared_buffers。缓冲区是所有 PostgreSQL 后端使用的共享资源。当为一个后端加载磁盘块时,稍后请求它的查询会发现它已经加载到内存中。
共享缓冲区和数据更改
如果查询更改表中的数据,它必须首先将数据页加载到 shared_buffers 中(如果尚未加载)。然后在共享内存页面上进行更改,将修改的磁盘块写入预写日志(假设我们是 LOGGED 关系),并将页面标记为脏。一旦 WAL 页面在 COMMIT 时间成功写入磁盘,则事务在磁盘上是安全的。
脏页的块更改被异步写出,最终的写入者在 shared_buffers 中将其标记为干净。可能的写入程序包括其他(或相同的)客户端、数据库的后台写入程序和系统 CHECKPOINT 进程。当在短时间内对相同的磁盘页面进行多次更改时,如果内存足够,这种设计可以实现加速的写入路径。每次脏页更改时,只需要写入额外的 WAL 增量。理想情况下,块的全部内容只写入磁盘一次:在下一个检查点期间。
Linux 子系统
从 shared_buffers 中删除页面
如果 Postgres 需要加载其他页面来回答查询并且 shared_buffers 已满,它将选择当前未使用的页面并将其驱逐。即使这个页面现在不在 shared_buffers 中,它可能仍然在来自原始磁盘读取的文件系统缓存中。
文件系统缓存/操作系统缓冲区缓存/内核缓存
在 Linux 中,程序未使用的内存会缓存最近使用的磁盘页面。这透明地加速了重新读取数据的工作负载。将页面保存在内存中意味着我们不需要从磁盘读取它,如果另一个客户端请求它,这是一个相对较慢的过程。索引是频繁重读数据库热点的典型例子。
当系统的其他部分需要时,缓存内存可用,因此它不会阻止程序请求额外的内存。如果发生这种情况,内核只会从操作系统缓存中删除一些缓冲区,以便内核满足内存请求。
对于读取缓冲区,在此处转储内存内容没有问题;最坏的情况是它只会从磁盘重新加载原始数据。当写入 WAL 或磁盘块更改时,PostgreSQL 通过适当的系统内核调用等待写入完成,即fsync(). 这可确保更改的磁盘缓冲区的内容已到达硬件 I/O 控制器并可能进一步到达。
磁盘缓存
一旦你进入 I/O 层,你可能会认为你已经完成了缓存,但缓存无处不在。大多数磁盘都有一个用于读/写的内部 I/O 缓存,它将缓冲和重新排序 I/O 访问,以便设备可以管理最佳访问/吞吐量。
如果您从操作系统读取磁盘块,内部磁盘缓存层可能会预读周围的块以将它们放在内部磁盘缓存中,以供后续读取。当您写入磁盘时,即使您 fsync,驱动器本身也可能有一个缓存层(无论是电池支持的控制器、SSD 还是 NVMe 缓存),它将缓冲这些写入,然后在某个时间点刷新到物理存储中不久的将来。
物理存储
恭喜,如果你做到了这一步,那么你的磁盘写入实际上已经保存在底层介质上。如今,这是某种形式的 SSD 或 NVMe 存储。在这一层,硬件磁盘缓存将数据变化持久地写入磁盘并从块地址读取数据。这通常被认为是我们数据层的最低级别。
在内部,SSD 和 NVMe 硬件可以在数据库运行的位置下方拥有自己的缓存(是的,甚至更多!)。示例包括用于闪存映射表的 DRAM 元数据缓存和/或使用更快 SLC 闪存单元的写入缓存。
结论
.....以及您一直在滚动的图表
感觉就像你刚刚去地球中心旅行?Postgres 的数据流涉及所有这些部分,以最快地获得最常用的数据:
- 应用
- 可能的应用程序池
- 单个客户端后端(Postgres 连接)
- 共享缓冲区
- 文件系统缓存
- 磁盘缓存
- 物理磁盘存储
原文标题:Postgres Data Flow
原文作者:David Christensen
原文链接:https://www.crunchydata.com/blog/postgres-data-flow




