PostgreSQL源码分析之shared buffer与磁盘文件
2015-08-05 09:12
204 查看
我们知道,PostgreSQL数据库中的信息,最终是要写入持久设备的。那么PostgreSQL是怎么将信息组织存储在磁盘上的呢? Bruce Momjian有一个slide 《Insider PostgreSQL shared memory》,里面的图片非常直观的描述了,shared buffer,page ,磁盘文件之间的关系,请看下图。 接下来几篇博客,从不同层面讲述PostgreSQL存储相关的的内存:
上图中左下角是page的组织形式。PostgreSQL 8K为一个页面,从share buffer写入relation 对应的磁盘文件,或者从relation对应的磁盘文件读入8K到shared buffer。shared buffers是一组8K的页面,作为缓存。对于数据库的relation而言,一条记录(Item或者叫Tuple),大小不一,不会恰好占据8K的空间,可能只有几十个字节,所以,如何将多条记录存放进8K的shared buffer,这就是page的组织形式了,我会在另一篇博文介绍。
对于Linux 我们知道,读文件,会首先将磁盘上的内容读入内存,写文件会首先写入cache,将cache标记成dirty,在合适的时机写入磁盘。对于这个不太熟悉的,可以阅读我前面的一篇博文 file 和page cache的一些事,PostgreSQL中shared buffers 之于relation file in disk 就相当于Linux 中page cache之于file in disk。
查看/设置 shared buffers大小:
首当其冲的是,PostgreSQL中shared buffers有多大,多少个8KB的buffers,当然这是可以配置的,我们通过如下方法查看配置:
show shared_buffers
或者:
select name,unit,setting,current_setting(name) from pg_settings where name = 'shared_buffers' ;
上面讲述的是查看,如何修改呢?需要修改配置文件postgresql.conf :
root@manu:/usr/pgdata# cat postgresql.conf | grep ^shared_buffers
shared_buffers = 24MB # min 128kB
我们可以将shared_buffers改成一个其他的值,至于改成多大的值是合理的,则取决与你的硬件环境,比如你的硬件很强悍,16GB内存,那么这个值设置成24MB就太抠门了。至于shared buffers多大才合理,网上有很多的说法,有的说内存总量的10%~15%,有的说内存总量的25%,幸好PostgreSQL提供了一些performance measure的工具,让我们能够监测PostgreSQL运行的performance,我们实际情况可以根据PostgreSQL的性能统计信息,调大或者调小这个shared buffers的大小。
但是又有个问题,shared buffer是以共享内存的形式分配的,如果在配置文件中配置的值超过操作系统对share memory的最大限制,会导PostgreSQL初始化失败。如下图,我将postgresql.conf中shared_buffers = 64MB,就导致了启动失败如下图所示:
原因是kernel的SHMMAX最大只有32MB,下面我查看并且修改成512MB
改过之后,就可以启动PostgreSQL了,我们可以查看shared_buffers已经变成了64MB:
manu_db=# show shared_buffers ;
shared_buffers
----------------
64MB
(1 row)
简单的内容结束了,我们需要深入代码分析shared buffers的原理了,如何组织内存,如何分配,如何page replacement,都在源码之中查找答案。详细的内容,我打算在下一篇博文里面介绍,因为原理部分本身就会内容有很多,必然会导致我这篇文章比较长。我本文剩下的内容想介绍内存中的shared buffer 如何得知对应的磁盘的文件。因为shared buffer中的8K内容,最终会sync到磁盘文件。PostgreSQL是将内存中的shared buffer和磁盘上的某个文件对应起来的呢。
shared buffer与relation的磁盘文件的对应关系
本文的第一个图,上半部分讲述的是shared buffer的结构,分两部分
1 赤果果的buffer,N个8K块,每个块存放从relation对应磁盘文件读上来的某个8K的内容。
2 管理buffer的结构,也是N个,有几个buffer,就有几个管理结构。Of Course,管理结构占用的内存空间要远小于赤果果的buffer,否则内存利用率太低了。
这是初始化的时候,为这两个部分分配空间:
BufferDescriptors = (BufferDesc *)
ShmemInitStruct("Buffer Descriptors",
NBuffers * sizeof(BufferDesc), &foundDescs);
BufferBlocks = (char *)
ShmemInitStruct("Buffer Blocks",
NBuffers * (Size) BLCKSZ, &foundBufs);
这个管理buffer的结构体叫BufferDesc,我智商不高,也知道肯定也知道会记录对应的buffer有没有被使用,对应的是哪个磁盘文件的第几个8K block,为了应对并发,肯定会有锁。我们看下这个结构体的定义:
typedef struct sbufdesc
{
BufferTag tag; /* ID of page contained in buffer */
BufFlags flags; /* see bit definitions above */
uint16 usage_count; /* usage counter for clock sweep code */
unsigned refcount; /* # of backends holding pins on buffer */
int wait_backend_pid; /* backend PID of pin-count waiter */
slock_t buf_hdr_lock; /* protects the above fields */
int buf_id; /* buffer's index number (from 0) */
int freeNext; /* link in freelist chain */
LWLockId io_in_progress_lock; /* to wait for I/O to complete */
LWLockId content_lock; /* to lock access to buffer contents */
} BufferDesc;
OK,我们回到我们最初关系的问题,当前这个shared buffer和which db ,which table,which type(后面解释type),which file的which 8KB block对应。第一个 BUfferTag类型的tag字段就是确定这个对应关系的:
typedef enum ForkNumber
{
InvalidForkNumber = -1,
MAIN_FORKNUM = 0,
FSM_FORKNUM,
VISIBILITYMAP_FORKNUM,
INIT_FORKNUM
/*
* NOTE: if you add a new fork, change MAX_FORKNUM below and update the
* forkNames array in catalog.c
*/
} ForkNumber;
typedef struct RelFileNode
{
Oid spcNode; /* tablespace */
Oid dbNode; /* database */
Oid relNode; /* relation */
} RelFileNode;
/*
* Buffer tag identifies which disk block the buffer contains.
*
* Note: the BufferTag data must be sufficient to determine where to write the
* block, without reference to pg_class or pg_tablespace entries. It's
* possible that the backend flushing the buffer doesn't even believe the
* relation is visible yet (its xact may have started before the xact that
* created the rel). The storage manager must be able to cope anyway.
*
* Note: if there's any pad bytes in the struct, INIT_BUFFERTAG will have
* to be fixed to zero them, since this struct is used as a hash key.
*/
typedef struct buftag
{
RelFileNode rnode; /* physical relation identifier */
ForkNumber forkNum;
BlockNumber blockNum; /* blknum relative to begin of reln */
} BufferTag;
我们可以看到BufferTag中的rnode,表征的是which relation。这个rnode的类型是RelFileNode类型,包括数据库空间/database/relation,从上到下三级结构,唯一确定了PostgreSQL的一个relation。对于relation而言并不是只有一种类型的磁盘文件,
-rw------- 1 manu manu 270336 6月 3 21:31 11785
-rw------- 1 manu manu 24576 6月 3 21:31 11785_fsm
-rw------- 1 manu manu 8192 6月 3 21:31 11785_vm
如上图所示11785对应某relation,但磁盘空间中有三种,包括fsm和vm后缀的两个文件。我们看下ForkNumber的注释:
/*
* The physical storage of a relation consists of one or more forks. The
* main fork is always created, but in addition to that there can be
* additional forks for storing various metadata. ForkNumber is used when
* we need to refer to a specific fork in a relation.
*/
MAIN_FORKNUM type的总是存在,但是某些relation还存在FSM_FORKNUM和VISIBILITYMAP_FORKNUM两种文件,这两种我目前知之不详,我就不瞎说了。
我们慢慢来,先放下blockNum这个成员变量,步子太大容易扯蛋,我们先根据rnode+forkNum找到磁盘对应的文件?
这个寻找磁盘文件的事儿是relpath这个宏通过调用relpathbackend实现的:
char *
relpathbackend(RelFileNode rnode, BackendId backend, ForkNumber forknum)
{
if (rnode.spcNode == GLOBALTABLESPACE_OID)
{
...
}
else if (rnode.spcNode ==DEFAULTTABLESPACE_OID)
{
pathlen = 5 + OIDCHARS + 1 + OIDCHARS + 1 + FORKNAMECHARS + 1;
path = (char *) palloc(pathlen);
if (forknum != MAIN_FORKNUM)
snprintf(path, pathlen, "base/%u/%u_%s",
rnode.dbNode, rnode.relNode,
forkNames[forknum]);
else
snprintf(path, pathlen, "base/%u/%u",
rnode.dbNode, rnode.relNode);
}
else
{
...
}
}
因为我们是pg_default,所以我们走DEFAULTTABLESPACE_OID这个分支。决定了我们在base目录下,db的oid(即BufferTag->rnode->dbNode)是16384决定了base/16384/,BufferTag->rnode->relNode + BufferTag->forkNum 决定了是base/16384/16385还是 base/16384/16385_fsm or base/16384/16385_vm。
查找文件基本结束,不过,某些某些relation比较大,记录比较多,会导致磁盘文件超大,为了防止文件系统对磁盘文件大小的限制而导致的写入失败,PostgreSQL做了分段的机制。以我的friends为例,如果随着记录的不断插入,最后friends对应的磁盘文件16385越来越大,当超过1G的时候,PostgreSQL就会新建一个磁盘文件叫16385.1,超过2G的时候PostgreSQL再次分段,新建文件16385.2 。这个1G就是有Block size = 8KB和blockS per segment of large relation=128K(个)共同决定的。
源码中的定义上面有注释,解释了很多内容:
/* RELSEG_SIZE is the maximum number of blocks allowed in one disk file. Thus,
the maximum size of a single file is RELSEG_SIZE * BLCKSZ; relations bigger
than that are divided into multiple files. RELSEG_SIZE * BLCKSZ must be
less than your OS' limit on file size. This is often 2 GB or 4GB in a
32-bit operating system, unless you have large file support enabled. By
default, we make the limit 1 GB to avoid any possible integer-overflow
problems within the OS. A limit smaller than necessary only means we divide
a large relation into more chunks than necessary, so it seems best to err
in the direction of a small limit. A power-of-2 value is recommended to
save a few cycles in md.c, but is not absolutely required. Changing
RELSEG_SIZE requires an initdb. */
#define RELSEG_SIZE 131072
当然了这个128K的值是默认值,我们编译PostgreSQL的阶段 configure的时候,可以通过--with-segsize 指定其他的值,不过这个我没有try过。
考虑上segment,真正的磁盘文件名fullpath就呼之欲出了:
如果分段了,在relpath获取的名字后面加上段号segno,如果段号是0,那么fullpath就是前面讲的relpath。
static char *
_mdfd_segpath(SMgrRelation reln, ForkNumber forknum, BlockNumber segno)
{
char *path,
*fullpath;
path = relpath(reln->smgr_rnode, forknum);
if (segno > 0)
{
/* be sure we have enough space for the '.segno' */
fullpath = (char *) palloc(strlen(path) + 12);
sprintf(fullpath, "%s.%u", path, segno);
pfree(path);
}
else
fullpath = path;
return fullpath;
}
怎么判断segno是几?这个太easy了,(BufferTag->rnode->blockNum/RELSEG_SIZE)。
OK,讲过这个shared buffer中的8K块和relation 的磁盘文件的对应关系,我们就可以安心讲述 shared buffer的一些内容了。悲剧啊,文章写了好久。
参考文献:
1 PostgreSQL 性能调校
2 PostgreSQL 9.1.9 Source Code
3 Bruce Momjian的Insider PostgreSQL shared memory
上图中左下角是page的组织形式。PostgreSQL 8K为一个页面,从share buffer写入relation 对应的磁盘文件,或者从relation对应的磁盘文件读入8K到shared buffer。shared buffers是一组8K的页面,作为缓存。对于数据库的relation而言,一条记录(Item或者叫Tuple),大小不一,不会恰好占据8K的空间,可能只有几十个字节,所以,如何将多条记录存放进8K的shared buffer,这就是page的组织形式了,我会在另一篇博文介绍。
对于Linux 我们知道,读文件,会首先将磁盘上的内容读入内存,写文件会首先写入cache,将cache标记成dirty,在合适的时机写入磁盘。对于这个不太熟悉的,可以阅读我前面的一篇博文 file 和page cache的一些事,PostgreSQL中shared buffers 之于relation file in disk 就相当于Linux 中page cache之于file in disk。
查看/设置 shared buffers大小:
首当其冲的是,PostgreSQL中shared buffers有多大,多少个8KB的buffers,当然这是可以配置的,我们通过如下方法查看配置:
show shared_buffers
或者:
select name,unit,setting,current_setting(name) from pg_settings where name = 'shared_buffers' ;
上面讲述的是查看,如何修改呢?需要修改配置文件postgresql.conf :
root@manu:/usr/pgdata# cat postgresql.conf | grep ^shared_buffers
shared_buffers = 24MB # min 128kB
我们可以将shared_buffers改成一个其他的值,至于改成多大的值是合理的,则取决与你的硬件环境,比如你的硬件很强悍,16GB内存,那么这个值设置成24MB就太抠门了。至于shared buffers多大才合理,网上有很多的说法,有的说内存总量的10%~15%,有的说内存总量的25%,幸好PostgreSQL提供了一些performance measure的工具,让我们能够监测PostgreSQL运行的performance,我们实际情况可以根据PostgreSQL的性能统计信息,调大或者调小这个shared buffers的大小。
但是又有个问题,shared buffer是以共享内存的形式分配的,如果在配置文件中配置的值超过操作系统对share memory的最大限制,会导PostgreSQL初始化失败。如下图,我将postgresql.conf中shared_buffers = 64MB,就导致了启动失败如下图所示:
原因是kernel的SHMMAX最大只有32MB,下面我查看并且修改成512MB
改过之后,就可以启动PostgreSQL了,我们可以查看shared_buffers已经变成了64MB:
manu_db=# show shared_buffers ;
shared_buffers
----------------
64MB
(1 row)
简单的内容结束了,我们需要深入代码分析shared buffers的原理了,如何组织内存,如何分配,如何page replacement,都在源码之中查找答案。详细的内容,我打算在下一篇博文里面介绍,因为原理部分本身就会内容有很多,必然会导致我这篇文章比较长。我本文剩下的内容想介绍内存中的shared buffer 如何得知对应的磁盘的文件。因为shared buffer中的8K内容,最终会sync到磁盘文件。PostgreSQL是将内存中的shared buffer和磁盘上的某个文件对应起来的呢。
shared buffer与relation的磁盘文件的对应关系
本文的第一个图,上半部分讲述的是shared buffer的结构,分两部分
1 赤果果的buffer,N个8K块,每个块存放从relation对应磁盘文件读上来的某个8K的内容。
2 管理buffer的结构,也是N个,有几个buffer,就有几个管理结构。Of Course,管理结构占用的内存空间要远小于赤果果的buffer,否则内存利用率太低了。
这是初始化的时候,为这两个部分分配空间:
BufferDescriptors = (BufferDesc *)
ShmemInitStruct("Buffer Descriptors",
NBuffers * sizeof(BufferDesc), &foundDescs);
BufferBlocks = (char *)
ShmemInitStruct("Buffer Blocks",
NBuffers * (Size) BLCKSZ, &foundBufs);
这个管理buffer的结构体叫BufferDesc,我智商不高,也知道肯定也知道会记录对应的buffer有没有被使用,对应的是哪个磁盘文件的第几个8K block,为了应对并发,肯定会有锁。我们看下这个结构体的定义:
typedef struct sbufdesc
{
BufferTag tag; /* ID of page contained in buffer */
BufFlags flags; /* see bit definitions above */
uint16 usage_count; /* usage counter for clock sweep code */
unsigned refcount; /* # of backends holding pins on buffer */
int wait_backend_pid; /* backend PID of pin-count waiter */
slock_t buf_hdr_lock; /* protects the above fields */
int buf_id; /* buffer's index number (from 0) */
int freeNext; /* link in freelist chain */
LWLockId io_in_progress_lock; /* to wait for I/O to complete */
LWLockId content_lock; /* to lock access to buffer contents */
} BufferDesc;
OK,我们回到我们最初关系的问题,当前这个shared buffer和which db ,which table,which type(后面解释type),which file的which 8KB block对应。第一个 BUfferTag类型的tag字段就是确定这个对应关系的:
typedef enum ForkNumber
{
InvalidForkNumber = -1,
MAIN_FORKNUM = 0,
FSM_FORKNUM,
VISIBILITYMAP_FORKNUM,
INIT_FORKNUM
/*
* NOTE: if you add a new fork, change MAX_FORKNUM below and update the
* forkNames array in catalog.c
*/
} ForkNumber;
typedef struct RelFileNode
{
Oid spcNode; /* tablespace */
Oid dbNode; /* database */
Oid relNode; /* relation */
} RelFileNode;
/*
* Buffer tag identifies which disk block the buffer contains.
*
* Note: the BufferTag data must be sufficient to determine where to write the
* block, without reference to pg_class or pg_tablespace entries. It's
* possible that the backend flushing the buffer doesn't even believe the
* relation is visible yet (its xact may have started before the xact that
* created the rel). The storage manager must be able to cope anyway.
*
* Note: if there's any pad bytes in the struct, INIT_BUFFERTAG will have
* to be fixed to zero them, since this struct is used as a hash key.
*/
typedef struct buftag
{
RelFileNode rnode; /* physical relation identifier */
ForkNumber forkNum;
BlockNumber blockNum; /* blknum relative to begin of reln */
} BufferTag;
我们可以看到BufferTag中的rnode,表征的是which relation。这个rnode的类型是RelFileNode类型,包括数据库空间/database/relation,从上到下三级结构,唯一确定了PostgreSQL的一个relation。对于relation而言并不是只有一种类型的磁盘文件,
-rw------- 1 manu manu 270336 6月 3 21:31 11785
-rw------- 1 manu manu 24576 6月 3 21:31 11785_fsm
-rw------- 1 manu manu 8192 6月 3 21:31 11785_vm
如上图所示11785对应某relation,但磁盘空间中有三种,包括fsm和vm后缀的两个文件。我们看下ForkNumber的注释:
/*
* The physical storage of a relation consists of one or more forks. The
* main fork is always created, but in addition to that there can be
* additional forks for storing various metadata. ForkNumber is used when
* we need to refer to a specific fork in a relation.
*/
MAIN_FORKNUM type的总是存在,但是某些relation还存在FSM_FORKNUM和VISIBILITYMAP_FORKNUM两种文件,这两种我目前知之不详,我就不瞎说了。
我们慢慢来,先放下blockNum这个成员变量,步子太大容易扯蛋,我们先根据rnode+forkNum找到磁盘对应的文件?
这个寻找磁盘文件的事儿是relpath这个宏通过调用relpathbackend实现的:
char *
relpathbackend(RelFileNode rnode, BackendId backend, ForkNumber forknum)
{
if (rnode.spcNode == GLOBALTABLESPACE_OID)
{
...
}
else if (rnode.spcNode ==DEFAULTTABLESPACE_OID)
{
pathlen = 5 + OIDCHARS + 1 + OIDCHARS + 1 + FORKNAMECHARS + 1;
path = (char *) palloc(pathlen);
if (forknum != MAIN_FORKNUM)
snprintf(path, pathlen, "base/%u/%u_%s",
rnode.dbNode, rnode.relNode,
forkNames[forknum]);
else
snprintf(path, pathlen, "base/%u/%u",
rnode.dbNode, rnode.relNode);
}
else
{
...
}
}
因为我们是pg_default,所以我们走DEFAULTTABLESPACE_OID这个分支。决定了我们在base目录下,db的oid(即BufferTag->rnode->dbNode)是16384决定了base/16384/,BufferTag->rnode->relNode + BufferTag->forkNum 决定了是base/16384/16385还是 base/16384/16385_fsm or base/16384/16385_vm。
查找文件基本结束,不过,某些某些relation比较大,记录比较多,会导致磁盘文件超大,为了防止文件系统对磁盘文件大小的限制而导致的写入失败,PostgreSQL做了分段的机制。以我的friends为例,如果随着记录的不断插入,最后friends对应的磁盘文件16385越来越大,当超过1G的时候,PostgreSQL就会新建一个磁盘文件叫16385.1,超过2G的时候PostgreSQL再次分段,新建文件16385.2 。这个1G就是有Block size = 8KB和blockS per segment of large relation=128K(个)共同决定的。
源码中的定义上面有注释,解释了很多内容:
/* RELSEG_SIZE is the maximum number of blocks allowed in one disk file. Thus,
the maximum size of a single file is RELSEG_SIZE * BLCKSZ; relations bigger
than that are divided into multiple files. RELSEG_SIZE * BLCKSZ must be
less than your OS' limit on file size. This is often 2 GB or 4GB in a
32-bit operating system, unless you have large file support enabled. By
default, we make the limit 1 GB to avoid any possible integer-overflow
problems within the OS. A limit smaller than necessary only means we divide
a large relation into more chunks than necessary, so it seems best to err
in the direction of a small limit. A power-of-2 value is recommended to
save a few cycles in md.c, but is not absolutely required. Changing
RELSEG_SIZE requires an initdb. */
#define RELSEG_SIZE 131072
当然了这个128K的值是默认值,我们编译PostgreSQL的阶段 configure的时候,可以通过--with-segsize 指定其他的值,不过这个我没有try过。
考虑上segment,真正的磁盘文件名fullpath就呼之欲出了:
如果分段了,在relpath获取的名字后面加上段号segno,如果段号是0,那么fullpath就是前面讲的relpath。
static char *
_mdfd_segpath(SMgrRelation reln, ForkNumber forknum, BlockNumber segno)
{
char *path,
*fullpath;
path = relpath(reln->smgr_rnode, forknum);
if (segno > 0)
{
/* be sure we have enough space for the '.segno' */
fullpath = (char *) palloc(strlen(path) + 12);
sprintf(fullpath, "%s.%u", path, segno);
pfree(path);
}
else
fullpath = path;
return fullpath;
}
怎么判断segno是几?这个太easy了,(BufferTag->rnode->blockNum/RELSEG_SIZE)。
OK,讲过这个shared buffer中的8K块和relation 的磁盘文件的对应关系,我们就可以安心讲述 shared buffer的一些内容了。悲剧啊,文章写了好久。
参考文献:
1 PostgreSQL 性能调校
2 PostgreSQL 9.1.9 Source Code
3 Bruce Momjian的Insider PostgreSQL shared memory
相关文章推荐
- 06-js禁止回车提交表单
- Emberjs搜索
- C#Json数据反序列化为Dictionary并根据关键字获取指定的值
- css sprite vs Data URI
- Newtonsoft.Json 方法使用()
- js学习一:简单的鼠标移动和移出效果案例
- 深入理解javascript之防篡改对象
- jquery方法
- JS 返回上一步(退回上一步上一个网页)
- JavaScript语法
- HTML——window.document对象练习题
- 用JS做图片轮播
- js深入理解(二)
- IOS 如何更改导航栏样式及状态栏字体颜色(导航栏背景、前景、标题及返回字体颜色)
- JQuery的Ajax跨域请求的解决方案
- 网易云课堂前端微专业各部分作业解答
- 解决jQuery uploadify在非IE核心浏览器下无法上传
- 3.3节点类;3.3.1Node类的成员数据
- 利用Json记录玩家位置
- JS实现转动随机数抽奖的特效代码