PostgreSQL结构与存储简析

本篇使用PostgreSQL版本为9.5。

数据文件目录

PostgreSQL的数据文件目录构成如下(截自Postgres-XL):

pgxl-exp-011

  • 子目录global:存放全局范围的表和索引等对象,例如pg_database。

    PostgreSQL实例在初始化的时候,默认创建两个表空间(tablespace),pg_global和pg_default。

    pgxl-exp-002

    其中pg_global对应的目录就是这个子目录global,可以通过以下SQL语句查询所有属于表空间pg_global的表和索引,

    SELECT * FROM pg_class WHERE reltablespace = 1664;
    SELECT pg_class.oid, relname, nspname, relfilenode 
        FROM pg_class INNER JOIN pg_namespace ON pg_class.relnamespace = pg_namespace.oid 
        WHERE reltablespace = (SELECT oid FROM pg_tablespace WHERE spcname = 'pg_global');
    

    每个表或索引对应一个或多个文件,在子目录global下以pg_class.relfilenode的值命名,结构为:global/<pg_class.relfilenode>。

  • 子目录base:存放定义在表空间pg_default下的数据库(database)的对象。

    与子目录global类似,不同之处在于对象文件之上多了一层数据库子目录,即base下首先是以database的oid命名的目录,表和索引等对象文件分别存放在各自的数据库子目录下。结构为:base/<pg_database.oid>/<pg_class.relfilenode>。也可以通过以下函数定位某个表的文件路径:

    SELECT pg_relation_filepath('user_info');
    

    以relfilenode是19224的表为例,相关文件如下:

    pgxl-exp-001

    其中19224和19224.*是该表存放行数据的文件,文件每达到1GB时,新的数据就会写入到下一个编号的19224.*文件。_vm文件(Visibility Map)用于标记哪些数据页(Page)存在需要清理的行。_fsm文件(Free Space Map)用于记录哪些数据页存在空闲的空间。执行Delete或Update时产生废弃行的数据页被标记到_vm文件,执行Vacuum(不带FULL)时根据_vm文件中的标记跳过不需要清理的数据页,Vacuum回收的行被标记到_fsm文件,之后可被Insert或Update再次利用。

  • 子目录pg_tblspc:存放自定义表空间路径的符号链接。

    执行以下SQL新建表空间,

    -- 新建表空间ts_other,目录位置为/data/pgxl_data/other
    CREATE TABLESPACE ts_other LOCATION '/data/pgxl_data/other';
    -- 把ts_other的CREATE权限分配给所有角色
    GRANT CREATE ON TABLESPACE ts_other TO public;
    -- 在ts_other上创建数据库db_sales
    CREATE DATABASE db_sales TABLESPACE ts_other;
    

    完成后会在子目录pg_tblspc下看到以表空间ts_other的oid命名的软链接,如果是Postgres-XL,所有coordinator和datanode节点都会创建相同的ts_other,只是oid可能不同。

    pgxl-exp-003

    之后在表空间ts_other上创建的数据库将落在/data/pgxl_data/other目录下,这样可以把IO压力分散到多个磁盘上。表空间创建后不能修改LOCATION,只能通过符号链接来改变数据文件的实际存储位置。

  • 子目录pg_xlog:存放预写日志(WAL)。

    WAL文件可以通过工具pg_xlogdump使一部分信息可视化。例如:

    # 查看00000001000003310000002A到00000001000003310000002D之间的xlog信息
    pg_xlogdump -b 00000001000003310000002A 00000001000003310000002D | less
    # 查看00000001000003310000002D的xlog信息,显示200条记录
    pg_xlogdump -b -n 200 /data/pgxl_alog/datanode1/00000001000003310000002D
    

    显示内容如下图:

    pgxl-exp-010

    其中,tx记录的是事务ID(XID);lsn记录的是日志序号;desc记录一些重要的描述信息,包括执行的是什么操作、作用在哪个对象上等。通过解读这些信息就可以把误操作精确定位到某个XID,然后在恢复时设置recovery_target_xid为误操作之前的那个XID。

    如果WAL文件损坏,可能会导致服务启动失败,报PANIC: could not locate a valid checkpoint record。解决办法是用工具pg_resetxlog修复:

    pg_resetxlog -f /data/pgxl_data/dn
    

    如果开启了WAL归档,则会在其archive_status子目录下,标记归档完成的WAL文件。

  • 文件postmaster.opts:记录上次启动PostgreSQL使用的命令行参数。
  • 文件postmaster.pid:服务启动后产生的锁文件,记录了主进程的PID、启动时间、数据目录、端口号等。
  • 文件postgresql.conf:数据库配置文件。

    -- 某些postgresql.conf中的配置项可以通过以下SQL语句重新载入,而不需要重启服务
    SELECT pg_reload_conf();
    -- 查看配置项
    SHOW max_connections;
    SHOW ALL;
    SELECT current_setting('max_connections');
    -- 临时设置配置项,当前连接有效
    SELECT set_config('log_statement_stats', 'off', false);
    -- 临时设置配置项,当前事务有效
    SELECT set_config('log_statement_stats', 'off', true);
    
  • 文件pg_hba.conf:访问数据库的IP限制和密码策略的配置文件。

数据页(Page)和行(Tuple)

前面提到的数据文件19224.*都是由固定大小(通常是8kB)的page构成的,这也是文件大小都是8192的整数倍的原因。page是共享缓冲池(shared buffer)与数据文件交换的基本单元。每个page中可以存放一个或多个tuple,tuple可以认为是与表中的行对应的数据结构。page的结构示意图如下,

pgxl-exp-006

  • page header:存储checksum, start of free space, end of free space等信息。
  • item:指向tuple的二元组(offset, length)。
  • tuple:存储行数据。

item从page header开始向后存储,tuple从special开始向前存储,直到中间的剩余空间不足以存储新的tuple时,创建新的page继续存储。存储引擎要想定位tuple,先要通过page_index和item_index找到item,再通过item找到tuple。page_index和item_index组成指向特定item的指针,称为CTID,格式为(page_index, item_index)。可以通过类似下面的SQL语句查看每一行的CTID,

SELECT ctid, * FROM user_info;

事实上,难免出现某一行的某一列保存大尺寸数据的情况,甚至超过page的大小。PostgreSQL不允许一行数据跨页存储。对于大尺寸数据,提供了一种TOAST(The Oversized-Attribute Storage Technique)机制来处理,即采用压缩加切片的方式把大数据分割成小数据,变成多行保存在对应的toast表中,并对它们进行索引。所以从文件角度看,一行大数据可能被分割存在于多个数据文件中(普通表和toast表分别对应的文件)。

查看名为user_info的普通表和它的toast表:

SELECT oid, * FROM pg_class WHERE relname = 'user_info'
UNION
SELECT oid, * FROM pg_class WHERE oid = (SELECT reltoastrelid FROM pg_class WHERE relname = 'user_info');

所有toast表都在模式(schema)pg_toast下,查看toast表结构和数据:

\d+ pg_toast.pg_toast_16411
SELECT * FROM pg_toast.pg_toast_16411;

pgxl-exp-008

  • chunk_id:具有同样chunk_id值的所有行组成原表的toast字段的一行数据。
  • chunk_seq:表示该行切片数据在完整数据中的顺序。
  • chunk_data:实际存储的切片数据。

尽管一张普通表最多对应一张toast表,但是每个字段依据其数据类型具有不同的toast策略。修改toast策略,不会影响已有数据的存储方式。字段存储策略如下,

  • plain:不压缩,不切片。不会触发toast机制。
  • extended:优先压缩,后切片。
  • external:不压缩,只切片。用空间换时间的策略,提高操作效率。
  • main:尽量不切片。

pgxl-exp-009

当将要保存到表的行超过一个阈值(通常为2kB)时,会触发toast机制(压缩或切片或两者都有),如果产生切片则保存到对应的toast表中。

服务端进程

如图:

pgxl-exp-004

  • 进程号9197:主进程。监听端口,等待连接。当客户端连接时,创建子进程处理请求。
  • 进程号9198:logger process,负责记录和管理系统日志。
  • 进程号9200:pooler process,负责管理pgxl节点间的连接池。
  • 进程号9203:checkpointer process,负责定期checkpoint。
  • 进程号9204:writer process,负责把shared buffer中的dirty page刷入磁盘。
  • 进程号9205:wal writer process,负责把wal buffer中的WAL刷入磁盘。
  • 进程号9206:autovacuum launcher process,由主进程调度,创建autovacuum worker process子进程执行vacuum操作。
  • 进程号9207:archiver process,负责归档WAL文件。
  • 进程号9208:stats collector process,收集统计信息。
  • 进程号9209:cluster monitor process,监控数据库状态。
  • 进程号19589:wal sender process,负责把主节点的WAL以流的方式同步给从节点,相应从节点上有wal receiver process接收数据。

数据更新

数据更新的最终结果是要把变更的内容持久化到磁盘上。一次事务可能涉及到多个page的改变,如果直接刷盘会产生多次随机写,这付出的代价相对较高。所以在设计层面采用写缓冲、异步和WAL来解决性能上的问题,并且WAL机制也保证了事务的持久性和数据的完整性,还使得在线备份和时间点恢复(PITR)成为可能。

事务过程

存储引擎在执行更新类操作时,首先开启事务,然后将变更后内容记入wal buffer,并更新shared buffer中的page,最后提交事务,响应客户端。shared buffer中的page发生更新后被标记为dirty page,之后由其他进程完成磁盘回写。事务提交可以设置是否等待WAL记录被写入到磁盘,控制参数为synchronous_commit。设置为off时对性能有一定提升,程序将不会等待本次的WAL记录写入到磁盘,而直接响应客户端。

pgxl-exp-012

WAL写入磁盘

默认情况下,WAL是在事务提交时写入磁盘,以保证提交的事务不会因为宕机而丢失。当synchronous_commit设置为off时,WAL写入磁盘的过程由wal writer进程异步完成,这种情况下宕机存在丢失最近提交的事务的风险,但不会影响数据库的一致性。

pgxl-exp-014

dirty page回写磁盘

一般情况下,每隔一段时间writer进程就会扫描shared buffer,将dirty page刷入操作系统的page cache,并记录处理过的dirty page。之后由系统内核处理page cache刷入磁盘,当空闲内存低于阈值或脏页驻留时间超过阈值时,触发回写磁盘。

另一种情况是由checkpointer进程定期执行checkpoint,强制当前处理过的dirty page回写磁盘,保证数据持久化,为事务前滚提供参照点。默认情况下(full_page_write = on),checkpoint发生后,在page第一次写的时候会将整个page写入WAL,以保证宕机或备份拷贝时,产生的partial write page可以被正确的完整的page覆盖来修复。

pgxl-exp-013

空间回收

在PostgreSQL中,delete和一些的update(例如,涉及到索引字段的更新)会使表和索引中的旧行(tuple)被废弃掉。这些废弃的tuple称为dead tuple,他们仍然占用着空间,成为一种浪费。

PostgreSQL的Vacuum机制用来回收这些空间,以便再次利用。Vacuum有两种方式。一种是不带full的vacuum操作,直接清理并标记可再次利用的空间,一般不能把空间还给操作系统,不会缩小数据文件的大小,优势是不需要排它锁(EXCLUSIVE LOCK),相对较快,通常使用这种vacuum方式。另一种是vacuum full操作,会把“非废弃”的行重写到新的磁盘文件,实际上重建了整张表和索引,释放掉了废弃行的磁盘空间,但是需要排它锁,过程慢。SQL命令如下:

-- Full Vacuum
VACUUM FULL VERBOSE;
VACUUM FULL t_user_info;
ANALYZE;

-- Lazy Vacuum
VACUUM t_user_info;
VACUUM ANALYZE t_user_info;

在Vacuum完成后通常还要进行Analyze来更新统计信息,帮助优化器在执行SQL时选择更好的执行计划。在生产环境中,一般是通过开启autovacuum来自动完成vacuum工作。autovacuum包含vacuum和analyze两项操作,在满足设定的条件时会被触发。在配置文件里设置log_autovacuum_min_duration = 0后,可在日志中看到每次autovacuum的信息。

除了Vacuum外,还可以利用工具pg_repack来达到类似vacuum full的效果。它的原理是基于原表创建一张新表,同时利用触发器,将原表上的增量更新不断记录下来。新表建好后,将所记录的增量更新应用到新表,直到新旧表数据完全一致。最后将新旧表表名互换,删除旧表。使用pg_repack时需要预留足够的空闲磁盘空间来创建新表。

参考

http://rachbelaid.com/introduction-to-postgres-physical-storage/
https://www.qcloud.com/community/article/941271
https://www.postgresql.org/docs/current/static/storage-toast.html
https://www.kancloud.cn/taobaomysql/monthly/67086

Creative Commons License

本文基于署名-非商业性使用-相同方式共享 4.0许可协议发布,欢迎转载、使用、重新发布,但请保留文章署名wanghengbin(包含链接:https://wanghengbin.com),不得用于商业目的,基于本文修改后的作品请以相同的许可发布。

发表评论