PostgreSQL因为性能卓越、运行稳定的特点而广受欢迎,高效和精细的进程与内存管理机制是性能和稳定背后重要的支撑。它采用多进程协同配合架构,进程间通过共享内存进行通信。
在本次直播中,我们与大家分享了PostgreSQL多进程架构和内存管理机制。以下内容根据直播文字整理而成。
PostgreSQL进程架构与内存管理********
PostgreSQL的多进程处理架构,为数据库系统的稳定性、并发性、跨平台能力和安全性提供了支撑。虽然与基于多线程的数据库系统相比,多进程模型可能在上下文切换和资源占用上有所不足,但在众多常用场景下,这种设计选择为PostgreSQL带来了显著的优势。
在PostgreSQL多进程架构体系中,最重要的两个进程是守护进程(Postmaster)与服务进程(Postgres)。其中服务进程可以接收并执行客户端发送的命令,并调用底层存储、事务管理、索引等功能模块完成客户端的各种操作,并返回执行结果。
图1:PostgreSQL多进程架构和内存模型流程示意图
如上图所示,当客户端发起连接时,守护进程会fork单独的服务进程为客户端提供服务,此后由服务进程为客户端执行各种命令,客户端直接和服务进程通信,不再需要守护进程中转,直到客户端断开连接。
守护进程是所有进程的父进程,负责整个数据库系统的启动、关闭、监听、接受新的客户端连接、处理配置变更和恢复和故障处理;服务进程主要职责在于客户端连接认证,并负责处理客户端发出的查询和语句。
除了守护进程的和服务进程外,PG在运行期间还需要一些辅助进程,包括:
- Background writer:负责将共享缓冲池中的脏页逐渐刷入持久化存储中。
- Checkpointer:在PG9.2及其后版本中,该进程负责处理检查点。
- Autovacuum launcher:周期性地启动自动清理工作进程。
- WAL writer:本进程周期性地将WAL缓冲区中的WAL数据刷入持久存储中。
- Statistics Collector:负责收集统计信息,用于诸如pg_stat_activity, pg_stat_database等系统视图。
- Logging collector (logger):负责将错误消息写入日志文件。
- Archiver:负责将日志归档。
在内存模型方面,PostgreSQL的内存体系结构可以分为两大类:本地内存区域(Local memory area)和共享内存区域(Shared memory area)。
本地内存由每个后端服务进程分配供自己使用,当后端服务进程被fork时,每个后端服务进程为查询分配一个本地内存区域,由以下三部分组成:
- work_mem:执行器在执行ORDER BY和DISTINCT时使用该区域对元组做排序,以及存储归并连接和散列连接中的连接表。
- maintenance_work_mem:某些类型的维护操作使用该区域(例如VACUUM、REINDEX)。
- temp_buffers:临时表相关操作使用这部分内存。
共享内存区域由PostgreSQL服务器在启动时分配,由所有后端进程共同使用。这个区域也被划分为几个固定大小的子区域,如下所示:
- Shared buff er pool:PostgreSQL将表和索引中的页面从持久存储加载至此,并直接操作。
- WAL buffer:WAL数据是PostgreSQL中的事务日志;WAL缓冲区是WAL数据在写入持久存储之前的缓冲区。
- Commit log buffer:提交日志为并发控制(CC)机制保存了所需的所有事务状态(例如进行中、已提交、已中止等)。
内存上下文的实现
内存管理是数据库设计的重要环节。在PostgreSQL 7.1之前,大量以指针传值的查询可能会造成严重、不易排查的内存泄漏。从7.1版本开始,PostgreSQL使用内存上下文(MemoryContext)机制来管理内存,解决了内存泄漏的问题,同时可以提高内存分配的效率,并避免内存碎片的产生。对用户来说,可通过palloc和pfree函数在内存上下文中申请、释放内存片。
内存上下文的本质是对SQL执行所需的内存进行阶段性的划分,每个阶段的内存由对应的内存上下文进行管理,内存上下文之间则构成树状的结构,其根节点为TopMemoryContext,在整个进程的生命周期里,TopMemoryContext 都将常驻于内存。
这样,在数据库运行的过程中,可以不断地根据需要创建和释放内存上下文。在释放内存上下文的过程中,所有内存上下文(及其子上下文)中分配的内存也都得以释放,而不必去关心每一块内存的释放。
图2:PostgreSQL内存上下文结构示意图
在上图中,各模块功能如下:
- TopMemoryContext **:**位于内存上下文树型管理结构的顶层,所有其它的内存上下文都是其直接或间接子节点。TopMemoryContext上的内存分配与malloc完全相同,因此TopMemoryContext不会被重置和删除。
- CacheMemoryContext**:**RelCache、CatCache以及相关模块的持久存储,无法重置或删除。
- MessageContext**:**此内存环境持有前台进程传递过来的当前命令消息,以及当前消息衍生出来的并且与当前消息生命周期相同的存储空间。
- TopTransactionContext**:**此内存环境一直持续到最高层事务结束的时候。在每一次最高层事务结束的时候,这个内存环境都会被重设,其所有的子内存环境都会被删除。在大多数情况下,无须在这里分配内存,而应该在CurTransactionContext中分配。注意:此内存环境不会在出错时立即清除,而是直到事务块通过调用COMMIT/ROLLBACK时清除。
- CurTransactionContext**:**此内存环境持有当前事务的数据,直到当前事务结束,特别是在最高层事务提交时需要此内存环境。当处于一个最高层事务中时,此内存环境与TopTransactionContext一致,但是在子事务中,CurTransactionContext则指向一个子内存环境。
- ErrorContext**:**这是一个持久性的内存环境,会在错误恢复过程中切换,在恢复结束时重设。这里安排了8K的空间,保证在其他所有内存用尽之后,也可以顺利地把错误恢复。
图3:PostgreSQL内存上下文数据结构示意图
在PostgreSQL中,MemoryContextData是内存上下文的核心数据结构,通过指针描述了内存上下文之间的关系,并通过虚函数表提供了对内存进行基本操作的接口,包括对内存的分配和释放。上下文之间的关系通过parent、firstchild、prevchild、nextchild等指针进行描述,在释放内存上下文的时候也会根据这些指针,遍历释放当前上下文的所有子上下文。
事实上,内存上下文并不管理实际上的内存分配,仅仅是用作对MemoryContext树的控制。管理内存上下文中的内存块是通过AllocSet结构来完成的,而MemoryContext仅作为AllocSet的头部信息存在,AllocSet是一个指向AllocSetContext结构的类型指针。
AllocSetContext是内存上下文的核心控制结构,本质上是一个高效的内存分配器。在AllocSetContext中,内存分成两个层次:内存块(Block)和内存片(Chunk)。通常,一个内存块会包含多个内存片,内存片则是PG分配内存的最小单元。
在整个内存分配的过程中有一个非常关键的数据结构:freelist,它是减少系统调用的关键所在。
freelist数组的大小默认为11,能够保存11种不同大小的空闲内存片,freelist 数组中最小的内存片大小为8Bytes,最大的内存片为8192bytes。
在向PG申请内存的时候,不会直接调用malloc分配内存,而是由PG先向系统通过malloc申请一块较大的内存块,然后由PG从该较大的内存块中切割出一块合适大小的内存片返回给申请者;申请者释放内存的时候也不会直接返还给操作系统,而是交由PG通过freelist保留不同大小的空闲碎片,在下次申请内存的时候,可以直接从freelist中寻找合适的内存片进行内存分配。这样做的目的主要是为了减少系统调用的次数和内存碎片的产生,提高内存分配和回收的效率。
申请内存大小的上限为allocChunkLimit,如果需要分配的内存超过该值,显然无法从freelist中分配内存,将通过 malloc直接申请一整块内存块(内存对齐处理),并整体作为一个内存片返回给申请者。如果申请内存大小未超 allocChunkLimit 且freelist 中有合适空闲碎片,可直接通过计算,得到 freelistindex,并从freelist中的链表中返回合适的空闲碎片。
在内存释放的时候,会有两种情况:
- ChunkSize > allocChunkLimit,直接调用 free() 进行释放。
- ChunkSize <= allocChunkLimit,将 Chunk直接添加至freelist空闲链表中即可。
结语
PostgreSQL通过使用多进程架构实现了系统的可靠性和健壮性,同时采用内存上下文管理机制,避免了内存泄漏问题的发生,减少内存碎片,提高内存的分配效率。