checkpointer continuous flushing
Hello pg-devs,
This patch is a simplified and generalized version of Andres Freund's
August 2014 patch for flushing while writing during checkpoints, with some
documentation and configuration warnings added.
For the initial patch, see:
/messages/by-id/20140827091922.GD21544@awork2.anarazel.de
For the whole thread:
/messages/by-id/alpine.DEB.2.10.1408251900211.11151@sto
The objective is to help avoid PG stalling when fsyncing on checkpoints,
and in general to get better latency-bound performance.
Flushes are managed with pg throttled writes instead of waiting for the
checkpointer final "fsync" which induces occasional stalls. From
"pgbench -P 1 ...", such stalls look like this:
progress: 35.0 s, 615.9 tps, lat 1.344 ms stddev 4.043 # ok
progress: 36.0 s, 3.0 tps, lat 346.111 ms stddev 123.828 # stalled
progress: 37.0 s, 4.0 tps, lat 252.462 ms stddev 29.346 # ...
progress: 38.0 s, 161.0 tps, lat 6.968 ms stddev 32.964 # restart
progress: 39.0 s, 701.0 tps, lat 1.421 ms stddev 3.326 # ok
I've seen similar behavior on FreeBSD with its native FS, so it is not a
Linux-specific or ext4-specific issue, even if both factor may contribute.
There are two implementations, first one based on "sync_file_range" is Linux
specific, while the other relies on "posix_fadvise". Tests below ran on Linux.
If someone could test the posix_fadvise version on relevant platforms, that
would be great...
The Linux specific "sync_file_range" approach was suggested among other ideas
by Theodore Ts'o on Robert Haas blog in March 2014:
http://rhaas.blogspot.fr/2014/03/linuxs-fsync-woes-are-getting-some.html
Two guc variables control whether the feature is activated for writes of
dirty pages issued by checkpointer and bgwriter. Given that the settings
may improve or degrade performance, having GUC seems justified. In
particular the stalling issue disappears with SSD.
The effect is significant on a series of tests shown below with scale 10
pgbench on an (old) dedicated host (8 GB memory, 8 cores, ext4 over hw
RAID), with shared_buffers=1GB checkpoint_completion_target=0.8
completion_timeout=30s, unless stated otherwise.
Note: I know that this completion_timeout is too small for a normal
config, but the point is to test how checkpoints behave, so the test
triggers as many checkpoints as possible, hence the minimum timeout
setting. I have also done some tests with larger timeout.
(1) THROTTLED PGBENCH
The objective of the patch is to be able to reduce the latency of transactions
under a moderate load. These first serie of tests focuses on this point with
the help of pgbench -R (rate) and -L (skip/count late transactions).
The measure counts transactions which were skipped or beyond the expected
latency limit while targetting a transaction rate.
* "pgbench -M prepared -N -T 100 -P 1 -R 100 -L 100" (100 tps targeted during
100 seconds, and latency limit is 100 ms), over 256 runs, 7 hours per case:
flush | percent of skipped
cp | bgw | & out of latency limit transactions
off | off | 6.5 %
off | on | 6.1 %
on | off | 0.4 %
on | on | 0.4 %
* Same as above (100 tps target) over one run of 4000 seconds with
shared_buffers=256MB and checkpoint_timeout=10mn:
flush | percent of skipped
cp | bgw | & out of latency limit transactions
off | off | 1.3 %
off | on | 1.5 %
on | off | 0.6 %
on | on | 0.6 %
* Same as first one but with "-R 150", i.e. targetting 150 tps, 256 runs:
flush | percent of skipped
cp | bgw | & out of latency limit transactions
off | off | 8.0 %
off | on | 8.0 %
on | off | 0.4 %
on | on | 0.4 %
* Same as above (150 tps target) over one run of 4000 seconds with
shared_buffers=256MB and checkpoint_timeout=10mn:
flush | percent of skipped
cp | bgw | & out of latency limit transactions
off | off | 1.7 %
off | on | 1.9 %
on | off | 0.7 %
on | on | 0.6 %
Turning "checkpoint_flush_to_disk = on" reduces significantly the number
of late transactions. These late transactions are not uniformly distributed,
but are rather clustered around times when pg is stalled, i.e. more or less
unresponsive.
bgwriter_flush_to_disk does not seem to have a significant impact on these
tests, maybe because pg shared_buffers size is much larger than the
database, so the bgwriter is seldom active.
(2) FULL SPEED PGBENCH
This is not the target use case, but it seems necessary to assess the
impact of these options of tps figures and their variability.
* "pgbench -M prepared -N -T 100 -P 1" over 512 runs, 14 hours per case.
flush | performance on ...
cp | bgw | 512 100-seconds runs | 1s intervals (over 51200 seconds)
off | off | 691 +- 36 tps | 691 +- 236 tps
off | on | 677 +- 29 tps | 677 +- 230 tps
on | off | 655 +- 23 tps | 655 +- 130 tps
on | on | 657 +- 22 tps | 657 +- 130 tps
On this first test, setting checkpoint_flush_to_disk reduces the performance by
5%, but the per second standard deviation is nearly halved, that is the
performance is more stable over the runs, although lower.
Option bgwriter_flush_to_disk effect is inconclusive.
* "pgbench -M prepared -N -T 4000 -P 1" on only 1 (long) run, with
checkpoint_timeout=10mn and shared_buffers=256MB (at least 6 checkpoints
during the run, probably more because segments are filled more often than
every 10mn):
flush | performance ... (stddev over per second tps)
off | off | 877 +- 179 tps
off | on | 880 +- 183 tps
on | off | 896 +- 131 tps
on | on | 888 +- 132 tps
On this second short test, setting checkpoint_flush_to_disk seems to maybe
slightly improve performance (maybe 2% ?) and significantly reduces
variability, so it looks like a good move.
* "pgbench -M prepared -N -T 100 -j 2 -c 4 -P 1" over 32 runs (4 clients)
flush | performance on ...
cp | bgw | 32 100-seconds runs | 1s intervals (over 3200 seconds)
off | off | 1970 +- 60 tps | 1970 +- 783 tps
off | on | 1928 +- 61 tps | 1928 +- 813 tps
on | off | 1578 +- 45 tps | 1578 +- 631 tps
on | on | 1594 +- 47 tps | 1594 +- 618 tps
On this test both average and standard deviation are both reduced by 20%.
This does not look like a win.
CONCLUSION
This approach is simple and significantly improves pg fsync behavior under
moderate load, where the database stays mostly responsive. Under full load,
the situation may be improved or degraded, it depends.
OTHER OPTIONS
Another idea suggested by Theodore Ts'o seems impractical: playing with
Linux io-scheduler priority (ioprio_set) looks only relevant with the
"sfq" scheduler on actual hard disk, but does not work with other
schedulers, especially "deadline" which seems more advisable for Pg, nor
for hardware RAID, which is a common setting.
Also, Theodore Ts'o suggested to use "sync_file_range" to check whether
the writes have reached the disk, and possibly to delay the actual
fsync/checkpoint conclusion if not... I have not tried that, the
implementation is not as trivial, and I'm not sure what to do when the
completion target is coming, but possibly that could be an interesting
option to investigate. Preliminary tests by adding a sleep between the
writes and the final fsync did not yield very good results.
I've also played with numerous other options (changing checkpointer
throttling parameters, reducing checkpoint timeout to 1 second, playing
around with various kernel settings), but that did not seem to be very
effective for the problem at hand.
I also attached a test script I used, that can be adapted if someone wants
to collect some performance data. I also have some basic scripts to
extract and compute stats, ask if needed.
--
Fabien.
Attachments:
checkpoint-continuous-flush-1.patchtext/x-diff; name=checkpoint-continuous-flush-1.patchDownload
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 5549b7d..1c0a3a1 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -1818,6 +1818,24 @@ include_dir 'conf.d'
</listitem>
</varlistentry>
+ <variablelist>
+ <varlistentry id="guc-bgwriter-flush-to-disk" xreflabel="bgwriter_flush_to_disk">
+ <term><varname>bgwriter_flush_to_disk</varname> (<type>bool</type>)
+ <indexterm>
+ <primary><varname>bgwriter_flush_to_disk</> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ When the bgwriter writes data, hint the underlying OS that the data
+ must be sent to disk as soon as possible. This may help smoothing
+ disk IO writes and avoid a stall when an fsync is issued by a
+ checkpoint, but it may also reduce average performance.
+ This setting may have no effect on some platforms.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="guc-bgwriter-lru-maxpages" xreflabel="bgwriter_lru_maxpages">
<term><varname>bgwriter_lru_maxpages</varname> (<type>integer</type>)
<indexterm>
@@ -2495,6 +2513,23 @@ include_dir 'conf.d'
</listitem>
</varlistentry>
+ <varlistentry id="guc-checkpoint-flush-to-disk" xreflabel="checkpoint_flush_to_disk">
+ <term><varname>checkpoint_flush_to_disk</varname> (<type>bool</type>)
+ <indexterm>
+ <primary><varname>checkpoint_flush_to_disk</> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ When writing data for a checkpoint, hint the underlying OS that the
+ data must be sent to disk as soon as possible. This may help smoothing
+ disk IO writes and avoid a stall when fsync is issued at the end of
+ the checkpoint, but it may also reduce average performance.
+ This setting may have no effect on some platforms.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="guc-min-wal-size" xreflabel="min_wal_size">
<term><varname>min_wal_size</varname> (<type>integer</type>)
<indexterm>
diff --git a/doc/src/sgml/wal.sgml b/doc/src/sgml/wal.sgml
index f4083c3..cdbdca9 100644
--- a/doc/src/sgml/wal.sgml
+++ b/doc/src/sgml/wal.sgml
@@ -546,6 +546,15 @@
</para>
<para>
+ On Linux and POSIX platforms, <xref linkend="guc-checkpoint-flush-to-disk">
+ allows to hint the OS that pages written on checkpoints must be flushed
+ to disk quickly. Otherwise, these pages may be kept in cache for some time,
+ inducing a stall later when <literal>fsync</> is called to actually
+ complete the checkpoint. This setting helps to reduce transaction latency,
+ but it also has an adverse effect on the average transaction rate.
+ </para>
+
+ <para>
The number of WAL segment files in <filename>pg_xlog</> directory depends on
<varname>min_wal_size</>, <varname>max_wal_size</> and
the amount of WAL generated in previous checkpoint cycles. When old log
diff --git a/src/backend/access/heap/rewriteheap.c b/src/backend/access/heap/rewriteheap.c
index bcce3e3..2d5c873 100644
--- a/src/backend/access/heap/rewriteheap.c
+++ b/src/backend/access/heap/rewriteheap.c
@@ -918,7 +918,7 @@ logical_heap_rewrite_flush_mappings(RewriteState state)
* Note that we deviate from the usual WAL coding practices here,
* check the above "Logical rewrite support" comment for reasoning.
*/
- written = FileWrite(src->vfd, waldata_start, len);
+ written = FileWrite(src->vfd, waldata_start, len, false);
if (written != len)
ereport(ERROR,
(errcode_for_file_access(),
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index 9431ab5..3375032 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -203,7 +203,7 @@ btbuildempty(PG_FUNCTION_ARGS)
/* Write the page. If archiving/streaming, XLOG it. */
PageSetChecksumInplace(metapage, BTREE_METAPAGE);
smgrwrite(index->rd_smgr, INIT_FORKNUM, BTREE_METAPAGE,
- (char *) metapage, true);
+ (char *) metapage, true, false);
if (XLogIsNeeded())
log_newpage(&index->rd_smgr->smgr_rnode.node, INIT_FORKNUM,
BTREE_METAPAGE, metapage, false);
diff --git a/src/backend/access/nbtree/nbtsort.c b/src/backend/access/nbtree/nbtsort.c
index f95f67a..ae8c1ca 100644
--- a/src/backend/access/nbtree/nbtsort.c
+++ b/src/backend/access/nbtree/nbtsort.c
@@ -315,7 +315,7 @@ _bt_blwritepage(BTWriteState *wstate, Page page, BlockNumber blkno)
{
/* overwriting a block we zero-filled before */
smgrwrite(wstate->index->rd_smgr, MAIN_FORKNUM, blkno,
- (char *) page, true);
+ (char *) page, true, false);
}
pfree(page);
diff --git a/src/backend/access/spgist/spginsert.c b/src/backend/access/spgist/spginsert.c
index bceee8d..242af8f 100644
--- a/src/backend/access/spgist/spginsert.c
+++ b/src/backend/access/spgist/spginsert.c
@@ -170,7 +170,7 @@ spgbuildempty(PG_FUNCTION_ARGS)
/* Write the page. If archiving/streaming, XLOG it. */
PageSetChecksumInplace(page, SPGIST_METAPAGE_BLKNO);
smgrwrite(index->rd_smgr, INIT_FORKNUM, SPGIST_METAPAGE_BLKNO,
- (char *) page, true);
+ (char *) page, true, false);
if (XLogIsNeeded())
log_newpage(&index->rd_smgr->smgr_rnode.node, INIT_FORKNUM,
SPGIST_METAPAGE_BLKNO, page, false);
@@ -180,7 +180,7 @@ spgbuildempty(PG_FUNCTION_ARGS)
PageSetChecksumInplace(page, SPGIST_ROOT_BLKNO);
smgrwrite(index->rd_smgr, INIT_FORKNUM, SPGIST_ROOT_BLKNO,
- (char *) page, true);
+ (char *) page, true, false);
if (XLogIsNeeded())
log_newpage(&index->rd_smgr->smgr_rnode.node, INIT_FORKNUM,
SPGIST_ROOT_BLKNO, page, true);
@@ -190,7 +190,7 @@ spgbuildempty(PG_FUNCTION_ARGS)
PageSetChecksumInplace(page, SPGIST_NULL_BLKNO);
smgrwrite(index->rd_smgr, INIT_FORKNUM, SPGIST_NULL_BLKNO,
- (char *) page, true);
+ (char *) page, true, false);
if (XLogIsNeeded())
log_newpage(&index->rd_smgr->smgr_rnode.node, INIT_FORKNUM,
SPGIST_NULL_BLKNO, page, true);
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index cc973b5..3e19ebc 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -80,6 +80,9 @@ bool zero_damaged_pages = false;
int bgwriter_lru_maxpages = 100;
double bgwriter_lru_multiplier = 2.0;
bool track_io_timing = false;
+/* hint to move writes to high priority */
+bool checkpoint_flush_to_disk = false;
+bool bgwriter_flush_to_disk = false;
/*
* How many buffers PrefetchBuffer callers should try to stay ahead of their
@@ -396,7 +399,8 @@ static bool PinBuffer(volatile BufferDesc *buf, BufferAccessStrategy strategy);
static void PinBuffer_Locked(volatile BufferDesc *buf);
static void UnpinBuffer(volatile BufferDesc *buf, bool fixOwner);
static void BufferSync(int flags);
-static int SyncOneBuffer(int buf_id, bool skip_recently_used);
+static int SyncOneBuffer(int buf_id, bool skip_recently_used,
+ bool flush_to_disk);
static void WaitIO(volatile BufferDesc *buf);
static bool StartBufferIO(volatile BufferDesc *buf, bool forInput);
static void TerminateBufferIO(volatile BufferDesc *buf, bool clear_dirty,
@@ -409,7 +413,7 @@ static volatile BufferDesc *BufferAlloc(SMgrRelation smgr,
BlockNumber blockNum,
BufferAccessStrategy strategy,
bool *foundPtr);
-static void FlushBuffer(volatile BufferDesc *buf, SMgrRelation reln);
+static void FlushBuffer(volatile BufferDesc *buf, SMgrRelation reln, bool flush_to_disk);
static void AtProcExit_Buffers(int code, Datum arg);
static void CheckForBufferLeaks(void);
static int rnode_comparator(const void *p1, const void *p2);
@@ -1018,7 +1022,7 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
smgr->smgr_rnode.node.dbNode,
smgr->smgr_rnode.node.relNode);
- FlushBuffer(buf, NULL);
+ FlushBuffer(buf, NULL, false);
LWLockRelease(buf->content_lock);
TRACE_POSTGRESQL_BUFFER_WRITE_DIRTY_DONE(forkNum, blockNum,
@@ -1662,7 +1666,7 @@ BufferSync(int flags)
*/
if (bufHdr->flags & BM_CHECKPOINT_NEEDED)
{
- if (SyncOneBuffer(buf_id, false) & BUF_WRITTEN)
+ if (SyncOneBuffer(buf_id, false, checkpoint_flush_to_disk) & BUF_WRITTEN)
{
TRACE_POSTGRESQL_BUFFER_SYNC_WRITTEN(buf_id);
BgWriterStats.m_buf_written_checkpoints++;
@@ -1939,7 +1943,7 @@ BgBufferSync(void)
/* Execute the LRU scan */
while (num_to_scan > 0 && reusable_buffers < upcoming_alloc_est)
{
- int buffer_state = SyncOneBuffer(next_to_clean, true);
+ int buffer_state = SyncOneBuffer(next_to_clean, true, bgwriter_flush_to_disk);
if (++next_to_clean >= NBuffers)
{
@@ -2016,7 +2020,7 @@ BgBufferSync(void)
* Note: caller must have done ResourceOwnerEnlargeBuffers.
*/
static int
-SyncOneBuffer(int buf_id, bool skip_recently_used)
+SyncOneBuffer(int buf_id, bool skip_recently_used, bool flush_to_disk)
{
volatile BufferDesc *bufHdr = GetBufferDescriptor(buf_id);
int result = 0;
@@ -2057,7 +2061,7 @@ SyncOneBuffer(int buf_id, bool skip_recently_used)
PinBuffer_Locked(bufHdr);
LWLockAcquire(bufHdr->content_lock, LW_SHARED);
- FlushBuffer(bufHdr, NULL);
+ FlushBuffer(bufHdr, NULL, flush_to_disk);
LWLockRelease(bufHdr->content_lock);
UnpinBuffer(bufHdr, true);
@@ -2319,9 +2323,12 @@ BufferGetTag(Buffer buffer, RelFileNode *rnode, ForkNumber *forknum,
*
* If the caller has an smgr reference for the buffer's relation, pass it
* as the second parameter. If not, pass NULL.
+ *
+ * The third parameter tries to hint the OS that a high priority write is meant,
+ * possibly because io-throttling is already managed elsewhere.
*/
static void
-FlushBuffer(volatile BufferDesc *buf, SMgrRelation reln)
+FlushBuffer(volatile BufferDesc *buf, SMgrRelation reln, bool flush_to_disk)
{
XLogRecPtr recptr;
ErrorContextCallback errcallback;
@@ -2410,7 +2417,8 @@ FlushBuffer(volatile BufferDesc *buf, SMgrRelation reln)
buf->tag.forkNum,
buf->tag.blockNum,
bufToWrite,
- false);
+ false,
+ flush_to_disk);
if (track_io_timing)
{
@@ -2830,6 +2838,7 @@ FlushRelationBuffers(Relation rel)
bufHdr->tag.forkNum,
bufHdr->tag.blockNum,
localpage,
+ false,
false);
bufHdr->flags &= ~(BM_DIRTY | BM_JUST_DIRTIED);
@@ -2864,7 +2873,7 @@ FlushRelationBuffers(Relation rel)
{
PinBuffer_Locked(bufHdr);
LWLockAcquire(bufHdr->content_lock, LW_SHARED);
- FlushBuffer(bufHdr, rel->rd_smgr);
+ FlushBuffer(bufHdr, rel->rd_smgr, false);
LWLockRelease(bufHdr->content_lock);
UnpinBuffer(bufHdr, true);
}
@@ -2916,7 +2925,7 @@ FlushDatabaseBuffers(Oid dbid)
{
PinBuffer_Locked(bufHdr);
LWLockAcquire(bufHdr->content_lock, LW_SHARED);
- FlushBuffer(bufHdr, NULL);
+ FlushBuffer(bufHdr, NULL, false);
LWLockRelease(bufHdr->content_lock);
UnpinBuffer(bufHdr, true);
}
diff --git a/src/backend/storage/buffer/localbuf.c b/src/backend/storage/buffer/localbuf.c
index 3144afe..156539d 100644
--- a/src/backend/storage/buffer/localbuf.c
+++ b/src/backend/storage/buffer/localbuf.c
@@ -208,6 +208,7 @@ LocalBufferAlloc(SMgrRelation smgr, ForkNumber forkNum, BlockNumber blockNum,
bufHdr->tag.forkNum,
bufHdr->tag.blockNum,
localpage,
+ false,
false);
/* Mark not-dirty now in case we error out below */
diff --git a/src/backend/storage/file/buffile.c b/src/backend/storage/file/buffile.c
index ea4d689..132cc43 100644
--- a/src/backend/storage/file/buffile.c
+++ b/src/backend/storage/file/buffile.c
@@ -317,7 +317,7 @@ BufFileDumpBuffer(BufFile *file)
return; /* seek failed, give up */
file->offsets[file->curFile] = file->curOffset;
}
- bytestowrite = FileWrite(thisfile, file->buffer + wpos, bytestowrite);
+ bytestowrite = FileWrite(thisfile, file->buffer + wpos, bytestowrite, false);
if (bytestowrite <= 0)
return; /* failed to write */
file->offsets[file->curFile] += bytestowrite;
diff --git a/src/backend/storage/file/fd.c b/src/backend/storage/file/fd.c
index 1ba4946..717e772 100644
--- a/src/backend/storage/file/fd.c
+++ b/src/backend/storage/file/fd.c
@@ -1345,7 +1345,7 @@ retry:
}
int
-FileWrite(File file, char *buffer, int amount)
+FileWrite(File file, char *buffer, int amount, bool flush_to_disk)
{
int returnCode;
@@ -1395,6 +1395,55 @@ retry:
if (returnCode >= 0)
{
+
+#if defined(HAVE_SYNC_FILE_RANGE) || defined(HAVE_POSIX_FADVISE)
+
+ /*
+ * Calling "write" tells the OS that pg wants to write some page to disk,
+ * however when it is really done is chosen by the OS.
+ * Depending on other disk activities this may be delayed significantly,
+ * maybe up to an "fsync" call, which could induce an IO write surge.
+ * When checkpointing pg is doing its own throttling and the result
+ * should really be written to disk with high priority, so as to meet
+ * the completion target.
+ * This call hints that such write have a higher priority.
+ */
+ if (flush_to_disk && returnCode == amount && errno == 0)
+ {
+ int rc;
+
+#if defined(HAVE_SYNC_FILE_RANGE)
+
+ /* Linux: tell the memory manager to move these blocks to io so
+ * that they are considered for being actually written to disk.
+ */
+ rc = sync_file_range(VfdCache[file].fd, VfdCache[file].seekPos,
+ amount, SYNC_FILE_RANGE_WRITE);
+
+#elif defined(HAVE_POSIX_FADVISE)
+
+ /* Others: say that data should not be kept in memory...
+ * This is not exactly what we want to say, because we want to write
+ * the data for durability but we may need it later nevertheless.
+ * It seems that Linux would free the memory *if* the data has
+ * already been written do disk, else it is ignored.
+ * For FreeBSD this may have the desired effect of moving the
+ * data to the io layer.
+ */
+ rc = posix_fadvise(VfdCache[file].fd, VfdCache[file].seekPos,
+ amount, POSIX_FADV_DONTNEED);
+
+#endif
+
+ if (rc < 0)
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not flush block " INT64_FORMAT " in file \"%s\": %m",
+ (int64) VfdCache[file].seekPos / BLCKSZ,
+ VfdCache[file].fileName)));
+ }
+#endif /* HAVE_SYNC_FILE_RANGE || HAVE_POSIX_FADVISE */
+
VfdCache[file].seekPos += returnCode;
/* maintain fileSize and temporary_files_size if it's a temp file */
diff --git a/src/backend/storage/smgr/md.c b/src/backend/storage/smgr/md.c
index 42a43bb..5c50e19 100644
--- a/src/backend/storage/smgr/md.c
+++ b/src/backend/storage/smgr/md.c
@@ -531,7 +531,7 @@ mdextend(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
errmsg("could not seek to block %u in file \"%s\": %m",
blocknum, FilePathName(v->mdfd_vfd))));
- if ((nbytes = FileWrite(v->mdfd_vfd, buffer, BLCKSZ)) != BLCKSZ)
+ if ((nbytes = FileWrite(v->mdfd_vfd, buffer, BLCKSZ, false)) != BLCKSZ)
{
if (nbytes < 0)
ereport(ERROR,
@@ -738,7 +738,7 @@ mdread(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
*/
void
mdwrite(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
- char *buffer, bool skipFsync)
+ char *buffer, bool skipFsync, bool flush_to_disk)
{
off_t seekpos;
int nbytes;
@@ -767,7 +767,7 @@ mdwrite(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
errmsg("could not seek to block %u in file \"%s\": %m",
blocknum, FilePathName(v->mdfd_vfd))));
- nbytes = FileWrite(v->mdfd_vfd, buffer, BLCKSZ);
+ nbytes = FileWrite(v->mdfd_vfd, buffer, BLCKSZ, flush_to_disk);
TRACE_POSTGRESQL_SMGR_MD_WRITE_DONE(forknum, blocknum,
reln->smgr_rnode.node.spcNode,
diff --git a/src/backend/storage/smgr/smgr.c b/src/backend/storage/smgr/smgr.c
index 244b4ea..199695d 100644
--- a/src/backend/storage/smgr/smgr.c
+++ b/src/backend/storage/smgr/smgr.c
@@ -52,7 +52,8 @@ typedef struct f_smgr
void (*smgr_read) (SMgrRelation reln, ForkNumber forknum,
BlockNumber blocknum, char *buffer);
void (*smgr_write) (SMgrRelation reln, ForkNumber forknum,
- BlockNumber blocknum, char *buffer, bool skipFsync);
+ BlockNumber blocknum, char *buffer, bool skipFsync,
+ bool flush_to_disk);
BlockNumber (*smgr_nblocks) (SMgrRelation reln, ForkNumber forknum);
void (*smgr_truncate) (SMgrRelation reln, ForkNumber forknum,
BlockNumber nblocks);
@@ -643,10 +644,10 @@ smgrread(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
*/
void
smgrwrite(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
- char *buffer, bool skipFsync)
+ char *buffer, bool skipFsync, bool flush_to_disk)
{
(*(smgrsw[reln->smgr_which].smgr_write)) (reln, forknum, blocknum,
- buffer, skipFsync);
+ buffer, skipFsync, flush_to_disk);
}
/*
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index b3c9f14..0b5ca17 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -158,6 +158,7 @@ static bool check_bonjour(bool *newval, void **extra, GucSource source);
static bool check_ssl(bool *newval, void **extra, GucSource source);
static bool check_stage_log_stats(bool *newval, void **extra, GucSource source);
static bool check_log_stats(bool *newval, void **extra, GucSource source);
+static bool check_flush_to_disk(bool *newval, void **extra, GucSource source);
static bool check_canonical_path(char **newval, void **extra, GucSource source);
static bool check_timezone_abbreviations(char **newval, void **extra, GucSource source);
static void assign_timezone_abbreviations(const char *newval, void *extra);
@@ -569,6 +570,8 @@ const char *const config_group_names[] =
gettext_noop("Write-Ahead Log / Checkpoints"),
/* WAL_ARCHIVING */
gettext_noop("Write-Ahead Log / Archiving"),
+ /* BGWRITER */
+ gettext_noop("Background Writer"),
/* REPLICATION */
gettext_noop("Replication"),
/* REPLICATION_SENDING */
@@ -1009,6 +1012,27 @@ static struct config_bool ConfigureNamesBool[] =
false,
NULL, NULL, NULL
},
+
+ {
+ {"checkpoint_flush_to_disk", PGC_SIGHUP, WAL_CHECKPOINTS,
+ gettext_noop("Hint that checkpoint's writes are high priority."),
+ NULL
+ },
+ &checkpoint_flush_to_disk,
+ false,
+ check_flush_to_disk, NULL, NULL
+ },
+
+ {
+ {"bgwriter_flush_to_disk", PGC_SIGHUP, BGWRITER,
+ gettext_noop("Hint that bgwriter's writes are high priority."),
+ NULL
+ },
+ &bgwriter_flush_to_disk,
+ false,
+ check_flush_to_disk, NULL, NULL
+ },
+
{
{"log_connections", PGC_SU_BACKEND, LOGGING_WHAT,
gettext_noop("Logs each successful connection."),
@@ -9761,6 +9785,22 @@ check_log_stats(bool *newval, void **extra, GucSource source)
}
static bool
+check_flush_to_disk(bool *newval, void **extra, GucSource source)
+{
+/* This test must be consistent with the one in FileWrite (storage/file/fd.c)
+ */
+#if ! (defined(HAVE_SYNC_FILE_RANGE) || defined(HAVE_POSIX_FADVISE))
+ /* just warn if it has no effect */
+ ereport(WARNING,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("Setting \"checkpoint_flush_to_disk\" or "
+ "\"bgwriter_flush_to_disk\" has no effect "
+ "on this platform.")));
+#endif /* HAVE_SYNC_FILE_RANGE */
+ return true;
+}
+
+static bool
check_canonical_path(char **newval, void **extra, GucSource source)
{
/*
diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.h
index ec0a254..4fea196 100644
--- a/src/include/storage/bufmgr.h
+++ b/src/include/storage/bufmgr.h
@@ -54,6 +54,8 @@ extern int bgwriter_lru_maxpages;
extern double bgwriter_lru_multiplier;
extern bool track_io_timing;
extern int target_prefetch_pages;
+extern bool checkpoint_flush_to_disk;
+extern bool bgwriter_flush_to_disk;
/* in buf_init.c */
extern PGDLLIMPORT char *BufferBlocks;
diff --git a/src/include/storage/fd.h b/src/include/storage/fd.h
index 7eabe09..32ac80f 100644
--- a/src/include/storage/fd.h
+++ b/src/include/storage/fd.h
@@ -70,7 +70,7 @@ extern File OpenTemporaryFile(bool interXact);
extern void FileClose(File file);
extern int FilePrefetch(File file, off_t offset, int amount);
extern int FileRead(File file, char *buffer, int amount);
-extern int FileWrite(File file, char *buffer, int amount);
+extern int FileWrite(File file, char *buffer, int amount, bool flush_to_disk);
extern int FileSync(File file);
extern off_t FileSeek(File file, off_t offset, int whence);
extern int FileTruncate(File file, off_t offset);
diff --git a/src/include/storage/smgr.h b/src/include/storage/smgr.h
index 69a624f..0bf0886 100644
--- a/src/include/storage/smgr.h
+++ b/src/include/storage/smgr.h
@@ -95,7 +95,7 @@ extern void smgrprefetch(SMgrRelation reln, ForkNumber forknum,
extern void smgrread(SMgrRelation reln, ForkNumber forknum,
BlockNumber blocknum, char *buffer);
extern void smgrwrite(SMgrRelation reln, ForkNumber forknum,
- BlockNumber blocknum, char *buffer, bool skipFsync);
+ BlockNumber blocknum, char *buffer, bool skipFsync, bool flush_to_disk);
extern BlockNumber smgrnblocks(SMgrRelation reln, ForkNumber forknum);
extern void smgrtruncate(SMgrRelation reln, ForkNumber forknum,
BlockNumber nblocks);
@@ -121,7 +121,7 @@ extern void mdprefetch(SMgrRelation reln, ForkNumber forknum,
extern void mdread(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
char *buffer);
extern void mdwrite(SMgrRelation reln, ForkNumber forknum,
- BlockNumber blocknum, char *buffer, bool skipFsync);
+ BlockNumber blocknum, char *buffer, bool skipFsync, bool flush_to_disk);
extern BlockNumber mdnblocks(SMgrRelation reln, ForkNumber forknum);
extern void mdtruncate(SMgrRelation reln, ForkNumber forknum,
BlockNumber nblocks);
diff --git a/src/include/utils/guc_tables.h b/src/include/utils/guc_tables.h
index 7a58ddb..b69af2d 100644
--- a/src/include/utils/guc_tables.h
+++ b/src/include/utils/guc_tables.h
@@ -68,6 +68,7 @@ enum config_group
WAL_SETTINGS,
WAL_CHECKPOINTS,
WAL_ARCHIVING,
+ BGWRITER,
REPLICATION,
REPLICATION_SENDING,
REPLICATION_MASTER,
Hi Fabien,
On 2015-06-01 PM 08:40, Fabien COELHO wrote:
Turning "checkpoint_flush_to_disk = on" reduces significantly the number
of late transactions. These late transactions are not uniformly distributed,
but are rather clustered around times when pg is stalled, i.e. more or less
unresponsive.bgwriter_flush_to_disk does not seem to have a significant impact on these
tests, maybe because pg shared_buffers size is much larger than the database,
so the bgwriter is seldom active.
Not that the GUC naming is the most pressing issue here, but do you think
"*_flush_on_write" describes what the patch does?
Thanks,
Amit
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Hello Amit,
Not that the GUC naming is the most pressing issue here, but do you think
"*_flush_on_write" describes what the patch does?
It is currently "*_flush_to_disk". In Andres Freund version the name is
"sync_on_checkpoint_flush", but I did not found it very clear. Using
"*_flush_on_write" instead as your suggest, would be fine as well, it
emphasizes the "when/how" it occurs instead of the final "destination",
why not...
About words: checkpoint "write"s pages, but this really mean passing the
pages to the memory manager, which will think about it... "flush" seems to
suggest a more effective write, but really it may mean the same, the page
is just passed to the OS. So "write/flush" is really "to OS" and not "to
disk". I like the data to be on "disk" in the end, and as soon as
possible, hence the choice to emphasize that point.
Now I would really be okay with anything that people find simple to
understand, so any opinion is welcome!
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Hi,
It's nice to see the topic being picked up.
If I see correctly you picked up the version without sorting durch
checkpoints. I think that's not going to work - there'll be too many
situations where the new behaviour will be detrimental. Did you
consider combining both approaches?
Greetings,
Andres Freund
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Mon, Jun 1, 2015 at 5:10 PM, Fabien COELHO <coelho@cri.ensmp.fr> wrote:
Hello pg-devs,
This patch is a simplified and generalized version of Andres Freund's
August 2014 patch for flushing while writing during checkpoints, with some
documentation and configuration warnings added.For the initial patch, see:
/messages/by-id/20140827091922.GD21544@awork2.anarazel.de
For the whole thread:
/messages/by-id/alpine.DEB.2.10.1408251900211.11151@sto
The objective is to help avoid PG stalling when fsyncing on checkpoints,
and in general to get better latency-bound performance.
-FlushBuffer(volatile BufferDesc *buf, SMgrRelation reln)
+FlushBuffer(volatile BufferDesc *buf, SMgrRelation reln, bool
flush_to_disk)
{
XLogRecPtr recptr;
ErrorContextCallback errcallback;
@@ -2410,7 +2417,8 @@ FlushBuffer(volatile BufferDesc *buf, SMgrRelation
reln)
buf->tag.forkNum,
buf->tag.blockNum,
bufToWrite,
- false);
+ false,
+ flush_to_disk);
Won't this lead to more-unsorted writes (random I/O) as the
FlushBuffer requests (by checkpointer or bgwriter) are not sorted as
per files or order of blocks on disk?
I remember sometime back there was some discusion regarding
sorting writes during checkpoint, one idea could be try to
check this idea along with that patch. I just saw that Andres has
also given same suggestion which indicates that it is important
to see both the things together.
Also here another related point is that I think currently even fsync
requests are not in order of the files as they are stored on disk so
that also might cause random I/O?
Yet another idea could be to allow BGWriter to also fsync the dirty
buffers, that may have side impact of not able to clear the dirty pages
at speed required by system, but I think if that happens one can
think of having multiple BGwriter tasks.
With Regards,
Amit Kapila.
EnterpriseDB: http://www.enterprisedb.com
Hello Andres,
If I see correctly you picked up the version without sorting durch
checkpoints. I think that's not going to work - there'll be too many
situations where the new behaviour will be detrimental. Did you
consider combining both approaches?
Ja, I thought that it was a more complex patch with uncertain/less clear
benefits, and as this simpler version was already effective enough as it
was, so I decided to start with that and try to have reasonable proof of
benefits so that it could get through.
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Hello Amit,
[...]
The objective is to help avoid PG stalling when fsyncing on checkpoints,
and in general to get better latency-bound performance.Won't this lead to more-unsorted writes (random I/O) as the
FlushBuffer requests (by checkpointer or bgwriter) are not sorted as
per files or order of blocks on disk?
Yep, probably. Under "moderate load" this is not an issue. The
io-scheduler and other hd firmware will probably reorder writes anyway.
Also, if several data are updated together, probably they are likely to be
already neighbours in memory as well as on disk.
I remember sometime back there was some discusion regarding
sorting writes during checkpoint, one idea could be try to
check this idea along with that patch. I just saw that Andres has
also given same suggestion which indicates that it is important
to see both the things together.
I would rather separate them, unless this is a blocker. This version seems
already quite effective and very light. ISTM that adding a sort phase
would mean reworking significantly how the checkpointer processes pages.
Also here another related point is that I think currently even fsync
requests are not in order of the files as they are stored on disk so
that also might cause random I/O?
I think that currently the fsync is on the file handler, so what happens
depends on how fsync is implemented by the system.
Yet another idea could be to allow BGWriter to also fsync the dirty
buffers,
ISTM That it is done with this patch with "bgwriter_flush_to_disk=on".
that may have side impact of not able to clear the dirty pages at speed
required by system, but I think if that happens one can think of having
multiple BGwriter tasks.
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 2015-06-02 15:15:39 +0200, Fabien COELHO wrote:
Won't this lead to more-unsorted writes (random I/O) as the
FlushBuffer requests (by checkpointer or bgwriter) are not sorted as
per files or order of blocks on disk?Yep, probably. Under "moderate load" this is not an issue. The io-scheduler
and other hd firmware will probably reorder writes anyway.
They pretty much can't if you flush things frequently. That's why I
think this won't be acceptable without the sorting in the checkpointer.
Also, if several
data are updated together, probably they are likely to be already neighbours
in memory as well as on disk.
No, that's not how it'll happen outside of simplistic cases where you
start with an empty shared_buffers. Shared buffers are maintained by a
simplified LRU, so how often individual blocks are touched will define
the buffer replacement.
I remember sometime back there was some discusion regarding
sorting writes during checkpoint, one idea could be try to
check this idea along with that patch. I just saw that Andres has
also given same suggestion which indicates that it is important
to see both the things together.I would rather separate them, unless this is a blocker.
I think it is a blocker.
This version seems
already quite effective and very light. ISTM that adding a sort phase would
mean reworking significantly how the checkpointer processes pages.
Meh. The patch for that wasn't that big.
The problem with doing this separately is that without the sorting this
will be slower for throughput in a good number of cases. So we'll have
yet another GUC that's very hard to tune.
Greetings,
Andres Freund
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Hello Andres,
I would rather separate them, unless this is a blocker.
I think it is a blocker.
Hmmm. This is an argument...
This version seems already quite effective and very light. ISTM that
adding a sort phase would mean reworking significantly how the
checkpointer processes pages.Meh. The patch for that wasn't that big.
Hmmm. I think it should be implemented as Tom suggested, that is per
chunks of shared buffers, in order to avoid allocating a "large" memory.
The problem with doing this separately is that without the sorting this
will be slower for throughput in a good number of cases. So we'll have
yet another GUC that's very hard to tune.
ISTM that the two aspects are orthogonal, which would suggests two gucs
anyway.
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 2015-06-02 15:42:14 +0200, Fabien COELHO wrote:
This version seems already quite effective and very light. ISTM that
adding a sort phase would mean reworking significantly how the
checkpointer processes pages.Meh. The patch for that wasn't that big.
Hmmm. I think it should be implemented as Tom suggested, that is per chunks
of shared buffers, in order to avoid allocating a "large" memory.
I don't necessarily agree. But that's really just a minor implementation
detail. The actual problem is sorting & fsyncing in a way that deals
efficiently with tablespaces, i.e. doesn't write to tablespaces
one-by-one. Not impossible, but it requires some thought.
The problem with doing this separately is that without the sorting this
will be slower for throughput in a good number of cases. So we'll have
yet another GUC that's very hard to tune.ISTM that the two aspects are orthogonal, which would suggests two gucs
anyway.
They're pretty closely linked from their performance impact. IMO this
feature, if done correctly, should result in better performance in 95+%
of the workloads and be enabled by default. And that'll not be possible
without actually writing mostly sequentially.
It's also not just the sequential writes making this important, it's
also that it allows to do the final fsync() of the individual segments
as soon as their last buffer has been written out. That's important
because it means the file will get fewer writes done independently
(i.e. backends writing out dirty buffers) which will make the final
fsync more expensive.
It might be that we want to different gucs, but I don't think we can
release without both features.
Greetings,
Andres Freund
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Hmmm. I think it should be implemented as Tom suggested, that is per chunks
of shared buffers, in order to avoid allocating a "large" memory.I don't necessarily agree. But that's really just a minor implementation
detail.
Probably.
The actual problem is sorting & fsyncing in a way that deals efficiently
with tablespaces, i.e. doesn't write to tablespaces one-by-one.
Not impossible, but it requires some thought.
Hmmm... I would have neglected this point in a first approximation,
but I agree that not interleaving tablespaces could indeed loose some
performance.
ISTM that the two aspects are orthogonal, which would suggests two gucs
anyway.They're pretty closely linked from their performance impact.
Sure.
IMO this feature, if done correctly, should result in better performance
in 95+% of the workloads
To demonstrate that would require time...
and be enabled by default.
I did not had such an ambition with the submitted patch:-)
And that'll not be possible without actually writing mostly
sequentially.
It's also not just the sequential writes making this important, it's
also that it allows to do the final fsync() of the individual segments
as soon as their last buffer has been written out.
Hmmm... I'm not sure this would have a large impact. The writes are
throttled as much as possible, so fsync will catch plenty other writes
anyway, if there are some.
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 2015-06-02 17:01:50 +0200, Fabien COELHO wrote:
The actual problem is sorting & fsyncing in a way that deals efficiently
with tablespaces, i.e. doesn't write to tablespaces one-by-one.
Not impossible, but it requires some thought.Hmmm... I would have neglected this point in a first approximation,
but I agree that not interleaving tablespaces could indeed loose some
performance.
I think it'll be a hard to diagnose performance regression. So we'll
have to fix it. That argument actually was the blocker in previous
attempts...
IMO this feature, if done correctly, should result in better performance
in 95+% of the workloadsTo demonstrate that would require time...
Well, that's part of the contribution process. Obviously you can't test
100% of the problems, but you can work hard with coming up with very
adversarial scenarios and evaluate performance for those.
and be enabled by default.
I did not had such an ambition with the submitted patch:-)
I don't think we want yet another tuning knob that's hard to tune
because it's critical for one factor (latency) but bad for another
(throughput); especially when completely unnecessarily.
And that'll not be possible without actually writing mostly sequentially.
It's also not just the sequential writes making this important, it's also
that it allows to do the final fsync() of the individual segments as soon
as their last buffer has been written out.Hmmm... I'm not sure this would have a large impact. The writes are
throttled as much as possible, so fsync will catch plenty other writes
anyway, if there are some.
That might be the case in a database with a single small table;
i.e. where all the writes go to a single file. But as soon as you have
large tables (i.e. many segments) or multiple tables, a significant part
of the writes issued independently from checkpointing will be outside
the processing of the individual segment.
Greetings,
Andres Freund
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
IMO this feature, if done correctly, should result in better performance
in 95+% of the workloadsTo demonstrate that would require time...
Well, that's part of the contribution process. Obviously you can't test
100% of the problems, but you can work hard with coming up with very
adversarial scenarios and evaluate performance for those.
I did spent time (well, a machine spent time, really) to collect some
convincing data for the simple version without sorting to demonstrate that
it brings a clear value, which seems not to be enough...
I don't think we want yet another tuning knob that's hard to tune
because it's critical for one factor (latency) but bad for another
(throughput); especially when completely unnecessarily.
Hmmm.
My opinion is that throughput is given too much attention in general, but
if both can be kept/improved, this would be easier to sell, obviously.
It's also not just the sequential writes making this important, it's also
that it allows to do the final fsync() of the individual segments as soon
as their last buffer has been written out.Hmmm... I'm not sure this would have a large impact. The writes are
throttled as much as possible, so fsync will catch plenty other writes
anyway, if there are some.That might be the case in a database with a single small table;
i.e. where all the writes go to a single file. But as soon as you have
large tables (i.e. many segments) or multiple tables, a significant part
of the writes issued independently from checkpointing will be outside
the processing of the individual segment.
Statistically, I think that it would reduce the number of unrelated writes
taken in a fsync by about half: the last table to be written on a
tablespace, at the end of the checkpoint, will have accumulated
checkpoint-unrelated writes (bgwriter, whatever) from the whole checkpoint
time, while the first table will have avoided most of them.
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 2015-06-02 18:59:05 +0200, Fabien COELHO wrote:
IMO this feature, if done correctly, should result in better performance
in 95+% of the workloadsTo demonstrate that would require time...
Well, that's part of the contribution process. Obviously you can't test
100% of the problems, but you can work hard with coming up with very
adversarial scenarios and evaluate performance for those.I did spent time (well, a machine spent time, really) to collect some
convincing data for the simple version without sorting to demonstrate that
it brings a clear value, which seems not to be enough...
"which seems not to be enough" - man. It's trivial to make things
faster/better/whatever if you don't care about regressions in other
parts. And if we'd add a guc for each of these cases we'd end up with
thousands of them.
My opinion is that throughput is given too much attention in general, but if
both can be kept/improved, this would be easier to sell, obviously.
Your priorities are not everyone's. That's life.
That might be the case in a database with a single small table;
i.e. where all the writes go to a single file. But as soon as you have
large tables (i.e. many segments) or multiple tables, a significant part
of the writes issued independently from checkpointing will be outside
the processing of the individual segment.Statistically, I think that it would reduce the number of unrelated writes
taken in a fsync by about half: the last table to be written on a
tablespace, at the end of the checkpoint, will have accumulated
checkpoint-unrelated writes (bgwriter, whatever) from the whole checkpoint
time, while the first table will have avoided most of them.
That's disregarding that a buffer written out by a backend starts to get
written out by the kernel after ~5-30s, even without a fsync triggering
it.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Hi,
On 2015-06-02 PM 07:19, Fabien COELHO wrote:
Not that the GUC naming is the most pressing issue here, but do you think
"*_flush_on_write" describes what the patch does?It is currently "*_flush_to_disk". In Andres Freund version the name is
"sync_on_checkpoint_flush", but I did not found it very clear. Using
"*_flush_on_write" instead as your suggest, would be fine as well, it
emphasizes the "when/how" it occurs instead of the final "destination", why
not...About words: checkpoint "write"s pages, but this really mean passing the pages
to the memory manager, which will think about it... "flush" seems to suggest a
more effective write, but really it may mean the same, the page is just passed
to the OS. So "write/flush" is really "to OS" and not "to disk". I like the
data to be on "disk" in the end, and as soon as possible, hence the choice to
emphasize that point.Now I would really be okay with anything that people find simple to
understand, so any opinion is welcome!
It seems 'sync' gets closer to what I really wanted 'flush' to mean. If I
understand this and the previous discussion(s) correctly, the patch tries to
alleviate the problems caused by one-big-sync-at-the end-of-writes by doing
the sync in step with writes (which do abide by the
checkpoint_completion_target). Given that impression, it seems *_sync_on_write
may even do the job.
Again, this is a minor issue.
By the way, I tend to agree with others here that there needs to be found a
good balance such that this sync-blocks-one-at-time-in-random-order approach
does not hurt generalized workload too much although it seems to help with
solving the latency problem that you seem set out to solve.
Thanks,
Amit
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Tue, Jun 2, 2015 at 6:45 PM, Fabien COELHO <coelho@cri.ensmp.fr> wrote:
Hello Amit,
[...]
The objective is to help avoid PG stalling when fsyncing on checkpoints,
and in general to get better latency-bound performance.Won't this lead to more-unsorted writes (random I/O) as the
FlushBuffer requests (by checkpointer or bgwriter) are not sorted as
per files or order of blocks on disk?Yep, probably. Under "moderate load" this is not an issue. The
io-scheduler and other hd firmware will probably reorder writes anyway.
Also, if several data are updated together, probably they are likely to be
already neighbours in memory as well as on disk.
I remember sometime back there was some discusion regarding
sorting writes during checkpoint, one idea could be try to
check this idea along with that patch. I just saw that Andres has
also given same suggestion which indicates that it is important
to see both the things together.I would rather separate them, unless this is a blocker. This version
seems already quite effective and very light. ISTM that adding a sort phase
would mean reworking significantly how the checkpointer processes pages.
I agree with you that if we have to add a sort phase, there is additional
work and that work could be significant depending on the design we
choose, however without that, this patch can have impact on many kind
of workloads, even in your mail in one of the tests
("pgbench -M prepared -N -T 100 -j 2 -c 4 -P 1" over 32 runs (4 clients))
it has shown 20% degradation which is quite significant and test also
seems to be representative of the workload which many users in real-world
will use.
Now one can say that for such workloads turn the new knob to off, but
in reality it could be difficult to predict if the load is always moderate.
I think users might be able to predict that at table level, but inspite of
that
I don't think having any such knob can give us ticket to flush the buffers
in random order.
Also here another related point is that I think currently even fsync
requests are not in order of the files as they are stored on disk so
that also might cause random I/O?I think that currently the fsync is on the file handler, so what happens
depends on how fsync is implemented by the system.
That can also lead to random I/O if the fsync for different files is not in
order as they are actually stored on disk.
Yet another idea could be to allow BGWriter to also fsync the dirty
buffers,ISTM That it is done with this patch with "bgwriter_flush_to_disk=on".
I think patch just issues an async operation not the actual flush. Why
I have suggested so is that in your tests when the checkpoint_timeout
is small it seems there is a good gain in performance that means if
keep on flushing dirty buffers at regular intervals, the system's
performance
is good and BGWriter is the process where that can be done conveniently
apart from checkpoint, one might think that if same can be achieved by
using
shorter checkpoint_timeout interval, then why to do this incremental flushes
by bgwriter, but in reality I think checkpoint is responsible for other
things
as well other than dirty buffers, so we can't leave everything till
checkpoint
happens.
With Regards,
Amit Kapila.
EnterpriseDB: http://www.enterprisedb.com
That might be the case in a database with a single small table; i.e.
where all the writes go to a single file. But as soon as you have
large tables (i.e. many segments) or multiple tables, a significant
part of the writes issued independently from checkpointing will be
outside the processing of the individual segment.Statistically, I think that it would reduce the number of unrelated writes
taken in a fsync by about half: the last table to be written on a
tablespace, at the end of the checkpoint, will have accumulated
checkpoint-unrelated writes (bgwriter, whatever) from the whole checkpoint
time, while the first table will have avoided most of them.That's disregarding that a buffer written out by a backend starts to get
written out by the kernel after ~5-30s, even without a fsync triggering
it.
I meant my argument with "continuous flushing" activated, so there is no
up to 30 seconds delay induced my the memory manager. Hmmm, maybe I do not
understood your argument.
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Hello Amit,
It is currently "*_flush_to_disk". In Andres Freund version the name is
"sync_on_checkpoint_flush", but I did not found it very clear. Using
"*_flush_on_write" instead as your suggest, would be fine as well, it
emphasizes the "when/how" it occurs instead of the final "destination", why
not...[...]
It seems 'sync' gets closer to what I really wanted 'flush' to mean. If
I understand this and the previous discussion(s) correctly, the patch
tries to alleviate the problems caused by one-big-sync-at-the
end-of-writes by doing the sync in step with writes (which do abide by
the checkpoint_completion_target). Given that impression, it seems
*_sync_on_write may even do the job.
I desagree with this one, because the sync is only *initiated*, not done.
For this reason I think that "flush" seems a better word. I understand
"sync" as "committed to disk". For the data to be synced, it should call
with the "wait after" option, which is a partial "fsync", but that would
be terrible for performance as all checkpointed pages would be written one
by one, without any opportunity for reordering them.
For what it's worth and for the record, Linux sync_file_range
documentation says "This is an asynchronous flush-to-disk operation" to
describe the corresponding option. This is probably where I took it.
So two contenders:
*_flush_to_disk
*_flush_on_write
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
I agree with you that if we have to add a sort phase, there is additional
work and that work could be significant depending on the design we
choose, however without that, this patch can have impact on many kind
of workloads, even in your mail in one of the tests
("pgbench -M prepared -N -T 100 -j 2 -c 4 -P 1" over 32 runs (4 clients))
it has shown 20% degradation which is quite significant and test also
seems to be representative of the workload which many users in real-world
will use.
Yes, I do agree with the 4 clients, but I doubt that many user run their
application at maximum available throughput all the time (like always
driving foot to the floor). So for me throttled runs are more
representative of real life.
Now one can say that for such workloads turn the new knob to off, but
in reality it could be difficult to predict if the load is always moderate.
Hmmm. The switch says "I prefer stable (say latency bounded) performance",
if you run a web site probably you should want that.
Anyway, I'll look at sorting when I have some time.
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Fabien,
On 2015-06-03 PM 02:53, Fabien COELHO wrote:
It seems 'sync' gets closer to what I really wanted 'flush' to mean. If I
understand this and the previous discussion(s) correctly, the patch tries to
alleviate the problems caused by one-big-sync-at-the end-of-writes by doing
the sync in step with writes (which do abide by the
checkpoint_completion_target). Given that impression, it seems
*_sync_on_write may even do the job.I desagree with this one, because the sync is only *initiated*, not done. For
this reason I think that "flush" seems a better word. I understand "sync" as
"committed to disk". For the data to be synced, it should call with the "wait
after" option, which is a partial "fsync", but that would be terrible for
performance as all checkpointed pages would be written one by one, without any
opportunity for reordering them.For what it's worth and for the record, Linux sync_file_range documentation
says "This is an asynchronous flush-to-disk operation" to describe the
corresponding option. This is probably where I took it.
Ah, okay! I didn't quite think about the async aspect here. But, I sure do
hope that the added mechanism turns out to be *less* async than kernel's own
dirty cache handling to achieve the hoped for gain.
So two contenders:
*_flush_to_disk
*_flush_on_write
Yep!
Regards,
Amit
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Hello Andres,
They pretty much can't if you flush things frequently. That's why I
think this won't be acceptable without the sorting in the checkpointer.
* VERSION 2 "WORK IN PROGRESS".
The implementation is more a proof-of-concept for having feedback than
clean code. What it does:
- as version 1 : simplified asynchronous flush based on Andres Freund
patch, with sync_file_range/posix_fadvise used to hint the OS that
the buffer must be sent to disk "now".
- added: checkpoint buffer sorting based on a 2007 patch by Takahiro Itagaki
but with a smaller and static buffer allocated once. Also,
sorting is done by chunks in the current version.
- also added: sync/advise calls are now merged if possible,
so less calls are used, especially when buffers are sorted,
but also if there are few files.
* PERFORMANCE TESTS
Impacts on "pgbench -M prepared -N -P 1" scale 10 (simple update pgbench
with a mostly-write activity), with checkpoint_completion_target=0.8
and shared_buffers=1GB.
Contrary to v1, I have not tested bgwriter flushing as the impact
on the first round was close to nought. This does not mean that particular
loads may benefit or be harmed but flushing from bgwriter.
- 100 tps throttled max 100 ms latency over 6400 seconds
with checkpoint_timeout=30s
flush | sort | late transactions
off | off | 6.0 %
off | on | 6.1 %
on | off | 0.4 %
on | on | 0.4 % (93% improvement)
- 100 tps throttled max 100 ms latency over 4000 seconds
with checkpoint_timeout=10mn
flush | sort | late transactions
off | off | 1.5 %
off | on | 0.6 % (?!)
on | off | 0.8 %
on | on | 0.6 % (60% improvement)
- 150 tps throttled max 100 ms latency over 19600 seconds (5.5 hours)
with checkpoint_timeout=30s
flush | sort | late transactions
off | off | 8.5 %
off | on | 8.1 %
on | off | 0.5 %
on | on | 0.4 % (95% improvement)
- full speed bgbench over 6400 seconds with checkpoint_timeout=30s
flush | sort | tps performance over per second data
off | off | 676 +- 230
off | on | 683 +- 213
on | off | 712 +- 130
on | on | 725 +- 116 (7.2% avg/50% stddev improvements)
- full speed bgbench over 4000 seconds with checkpoint_timeout=10mn
flush | sort | tps performance over per second data
off | off | 885 +- 188
off | on | 940 +- 120 (6%/36%!)
on | off | 778 +- 245 (hmmm... not very consistent?)
on | on | 927 +- 108 (4.5% avg/43% sttdev improvements)
- full speed bgbench "-j2 -c4" over 6400 seconds with checkpoint_timeout=30s
flush | sort | tps performance over per second data
off | off | 2012 +- 747
off | on | 2086 +- 708
on | off | 2099 +- 459
on | on | 2114 +- 422 (5% avg/44% stddev improvements)
* CONCLUSION :
For all these HDD tests, when both options are activated the tps performance
is improved, the latency is reduced and the performance is more stable
(smaller standard deviation).
Overall the option effects, not surprisingly, are quite (with exceptions)
orthogonal:
- latency is essentially improved (60 to 95% reduction) by flushing
- throughput is improved (4 to 7% better) thanks to sorting
In detail, some loads may benefit more from only one option activated.
Also on SSD probably both options would have limited benefit.
Usual caveat: these are only benches on one host at a particular time and
location, which may or may not be reproducible nor be representative
as such of any other load. The good news is that all these tests tell
the same thing.
* LOOK FOR THOUGHTS
- The bgwriter flushing option seems ineffective, it could be removed
from the patch?
- Move fsync as early as possible, suggested by Andres Freund?
In these tests, when the flush option is activated, the fsync duration
at the end of the checkpoint is small: on more than 5525 checkpoint
fsyncs, 0.5% are above 1 second when flush is on, but the figure raises
to 24% when it is off.... This suggest that doing the fsync as soon as
possible would probably have no significant effect on these tests.
My opinion is that this should be left out for the nonce.
- Take into account tablespaces, as pointed out by Andres Freund?
The issue is that if writes are sorted, they are not be distributed
randomly over tablespaces, inducing lower performance on such systems.
How to do it: while scanning shared_buffers, count dirty buffers for each
tablespace. Then start as many threads as table spaces, each one doing
its own independent throttling for a tablespace? For some obscure reason
there are 2 tablespaces by default (pg_global and pg_default), that would
mean at least 2 threads.
Alternatively, maybe it can be done from one thread, but it would probably
involve some strange hocus-pocus to switch frequently between tablespaces.
--
Fabien.
Attachments:
checkpoint-continuous-flush-2-WIP.patchtext/x-diff; name=checkpoint-continuous-flush-2-WIP.patchDownload
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 1da7dfb..2e6bb10 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -1818,6 +1818,24 @@ include_dir 'conf.d'
</listitem>
</varlistentry>
+ <variablelist>
+ <varlistentry id="guc-bgwriter-flush-to-disk" xreflabel="bgwriter_flush_to_disk">
+ <term><varname>bgwriter_flush_to_disk</varname> (<type>bool</type>)
+ <indexterm>
+ <primary><varname>bgwriter_flush_to_disk</> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ When the bgwriter writes data, hint the underlying OS that the data
+ must be sent to disk as soon as possible. This may help smoothing
+ disk IO writes and avoid a stall when an fsync is issued by a
+ checkpoint, but it may also reduce average performance.
+ This setting may have no effect on some platforms.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="guc-bgwriter-lru-maxpages" xreflabel="bgwriter_lru_maxpages">
<term><varname>bgwriter_lru_maxpages</varname> (<type>integer</type>)
<indexterm>
@@ -2495,6 +2513,23 @@ include_dir 'conf.d'
</listitem>
</varlistentry>
+ <varlistentry id="guc-checkpoint-flush-to-disk" xreflabel="checkpoint_flush_to_disk">
+ <term><varname>checkpoint_flush_to_disk</varname> (<type>bool</type>)
+ <indexterm>
+ <primary><varname>checkpoint_flush_to_disk</> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ When writing data for a checkpoint, hint the underlying OS that the
+ data must be sent to disk as soon as possible. This may help smoothing
+ disk IO writes and avoid a stall when fsync is issued at the end of
+ the checkpoint, but it may also reduce average performance.
+ This setting may have no effect on some platforms.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="guc-min-wal-size" xreflabel="min_wal_size">
<term><varname>min_wal_size</varname> (<type>integer</type>)
<indexterm>
diff --git a/doc/src/sgml/wal.sgml b/doc/src/sgml/wal.sgml
index f4083c3..cdbdca9 100644
--- a/doc/src/sgml/wal.sgml
+++ b/doc/src/sgml/wal.sgml
@@ -546,6 +546,15 @@
</para>
<para>
+ On Linux and POSIX platforms, <xref linkend="guc-checkpoint-flush-to-disk">
+ allows to hint the OS that pages written on checkpoints must be flushed
+ to disk quickly. Otherwise, these pages may be kept in cache for some time,
+ inducing a stall later when <literal>fsync</> is called to actually
+ complete the checkpoint. This setting helps to reduce transaction latency,
+ but it also has an adverse effect on the average transaction rate.
+ </para>
+
+ <para>
The number of WAL segment files in <filename>pg_xlog</> directory depends on
<varname>min_wal_size</>, <varname>max_wal_size</> and
the amount of WAL generated in previous checkpoint cycles. When old log
diff --git a/src/backend/access/heap/rewriteheap.c b/src/backend/access/heap/rewriteheap.c
index bcce3e3..f565dc4 100644
--- a/src/backend/access/heap/rewriteheap.c
+++ b/src/backend/access/heap/rewriteheap.c
@@ -918,7 +918,7 @@ logical_heap_rewrite_flush_mappings(RewriteState state)
* Note that we deviate from the usual WAL coding practices here,
* check the above "Logical rewrite support" comment for reasoning.
*/
- written = FileWrite(src->vfd, waldata_start, len);
+ written = FileWrite(src->vfd, waldata_start, len, false, NULL);
if (written != len)
ereport(ERROR,
(errcode_for_file_access(),
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index 9431ab5..49ec258 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -203,7 +203,7 @@ btbuildempty(PG_FUNCTION_ARGS)
/* Write the page. If archiving/streaming, XLOG it. */
PageSetChecksumInplace(metapage, BTREE_METAPAGE);
smgrwrite(index->rd_smgr, INIT_FORKNUM, BTREE_METAPAGE,
- (char *) metapage, true);
+ (char *) metapage, true, false, NULL);
if (XLogIsNeeded())
log_newpage(&index->rd_smgr->smgr_rnode.node, INIT_FORKNUM,
BTREE_METAPAGE, metapage, false);
diff --git a/src/backend/access/nbtree/nbtsort.c b/src/backend/access/nbtree/nbtsort.c
index f95f67a..ea7a45d 100644
--- a/src/backend/access/nbtree/nbtsort.c
+++ b/src/backend/access/nbtree/nbtsort.c
@@ -315,7 +315,7 @@ _bt_blwritepage(BTWriteState *wstate, Page page, BlockNumber blkno)
{
/* overwriting a block we zero-filled before */
smgrwrite(wstate->index->rd_smgr, MAIN_FORKNUM, blkno,
- (char *) page, true);
+ (char *) page, true, false, NULL);
}
pfree(page);
diff --git a/src/backend/access/spgist/spginsert.c b/src/backend/access/spgist/spginsert.c
index bceee8d..b700efb 100644
--- a/src/backend/access/spgist/spginsert.c
+++ b/src/backend/access/spgist/spginsert.c
@@ -170,7 +170,7 @@ spgbuildempty(PG_FUNCTION_ARGS)
/* Write the page. If archiving/streaming, XLOG it. */
PageSetChecksumInplace(page, SPGIST_METAPAGE_BLKNO);
smgrwrite(index->rd_smgr, INIT_FORKNUM, SPGIST_METAPAGE_BLKNO,
- (char *) page, true);
+ (char *) page, true, false, NULL);
if (XLogIsNeeded())
log_newpage(&index->rd_smgr->smgr_rnode.node, INIT_FORKNUM,
SPGIST_METAPAGE_BLKNO, page, false);
@@ -180,7 +180,7 @@ spgbuildempty(PG_FUNCTION_ARGS)
PageSetChecksumInplace(page, SPGIST_ROOT_BLKNO);
smgrwrite(index->rd_smgr, INIT_FORKNUM, SPGIST_ROOT_BLKNO,
- (char *) page, true);
+ (char *) page, true, false, NULL);
if (XLogIsNeeded())
log_newpage(&index->rd_smgr->smgr_rnode.node, INIT_FORKNUM,
SPGIST_ROOT_BLKNO, page, true);
@@ -190,7 +190,7 @@ spgbuildempty(PG_FUNCTION_ARGS)
PageSetChecksumInplace(page, SPGIST_NULL_BLKNO);
smgrwrite(index->rd_smgr, INIT_FORKNUM, SPGIST_NULL_BLKNO,
- (char *) page, true);
+ (char *) page, true, false, NULL);
if (XLogIsNeeded())
log_newpage(&index->rd_smgr->smgr_rnode.node, INIT_FORKNUM,
SPGIST_NULL_BLKNO, page, true);
diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c
index 0dce6a8..d962c3a 100644
--- a/src/backend/postmaster/checkpointer.c
+++ b/src/backend/postmaster/checkpointer.c
@@ -663,7 +663,7 @@ ImmediateCheckpointRequested(void)
* fraction between 0.0 meaning none, and 1.0 meaning all done.
*/
void
-CheckpointWriteDelay(int flags, double progress)
+CheckpointWriteDelay(int flags, double progress, FileFlushContext * context)
{
static int absorb_counter = WRITES_PER_ABSORB;
@@ -693,6 +693,14 @@ CheckpointWriteDelay(int flags, double progress)
CheckArchiveTimeout();
+ /* Before sleeping, sync written blocks
+ */
+ if (checkpoint_flush_to_disk && context->ncalls != 0)
+ {
+ PerformFileFlush(context);
+ ResetFileFlushContext(context);
+ }
+
/*
* Report interim activity statistics to the stats collector.
*/
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index cc973b5..b341bf7 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -80,6 +80,10 @@ bool zero_damaged_pages = false;
int bgwriter_lru_maxpages = 100;
double bgwriter_lru_multiplier = 2.0;
bool track_io_timing = false;
+/* hint to move writes to high priority */
+bool checkpoint_flush_to_disk = false;
+bool bgwriter_flush_to_disk = false;
+int checkpoint_sort_size = 1024 * 1024;
/*
* How many buffers PrefetchBuffer callers should try to stay ahead of their
@@ -396,7 +400,8 @@ static bool PinBuffer(volatile BufferDesc *buf, BufferAccessStrategy strategy);
static void PinBuffer_Locked(volatile BufferDesc *buf);
static void UnpinBuffer(volatile BufferDesc *buf, bool fixOwner);
static void BufferSync(int flags);
-static int SyncOneBuffer(int buf_id, bool skip_recently_used);
+static int SyncOneBuffer(int buf_id, bool skip_recently_used,
+ bool flush_to_disk, FileFlushContext *context);
static void WaitIO(volatile BufferDesc *buf);
static bool StartBufferIO(volatile BufferDesc *buf, bool forInput);
static void TerminateBufferIO(volatile BufferDesc *buf, bool clear_dirty,
@@ -409,7 +414,8 @@ static volatile BufferDesc *BufferAlloc(SMgrRelation smgr,
BlockNumber blockNum,
BufferAccessStrategy strategy,
bool *foundPtr);
-static void FlushBuffer(volatile BufferDesc *buf, SMgrRelation reln);
+static void FlushBuffer(volatile BufferDesc *buf, SMgrRelation reln,
+ bool flush_to_disk, FileFlushContext *context);
static void AtProcExit_Buffers(int code, Datum arg);
static void CheckForBufferLeaks(void);
static int rnode_comparator(const void *p1, const void *p2);
@@ -1018,7 +1024,7 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
smgr->smgr_rnode.node.dbNode,
smgr->smgr_rnode.node.relNode);
- FlushBuffer(buf, NULL);
+ FlushBuffer(buf, NULL, false, NULL);
LWLockRelease(buf->content_lock);
TRACE_POSTGRESQL_BUFFER_WRITE_DIRTY_DONE(forkNum, blockNum,
@@ -1561,6 +1567,53 @@ UnpinBuffer(volatile BufferDesc *buf, bool fixOwner)
}
}
+/* Array of buffer ids of all buffers to checkpoint.
+ */
+static int * CheckpointBufferIds = NULL;
+
+/* Compare checkpoint buffers
+ */
+static int bufcmp(const int * pa, const int * pb)
+{
+ BufferDesc
+ *a = GetBufferDescriptor(*pa),
+ *b = GetBufferDescriptor(*pb);
+
+ /* tag: rnode, forkNum (different files), blockNum
+ * rnode: { spcNode, dbNode (ignore: this is a directory), relNode }
+ * spcNode: table space oid, not that there are at least two
+ * (pg_global and pg_default).
+ */
+ /* first, compare table space (hmmm...) */
+ if (a->tag.rnode.spcNode < b->tag.rnode.spcNode)
+ return -1;
+ else if (a->tag.rnode.spcNode > b->tag.rnode.spcNode)
+ return 1;
+ /* same table space, compare relation */
+ else if (a->tag.rnode.relNode < b->tag.rnode.relNode)
+ return -1;
+ else if (a->tag.rnode.relNode > b->tag.rnode.relNode)
+ return 1;
+ /* same relation, compare fork */
+ else if (a->tag.forkNum < b->tag.forkNum)
+ return -1;
+ else if (a->tag.forkNum > b->tag.forkNum)
+ return 1;
+ /* same relation/fork, so same file, try block number */
+ else if (a->tag.blockNum < b->tag.blockNum)
+ return -1;
+ else /* should not be the same block... */
+ return 1;
+}
+
+static void AllocateCheckpointBufferIds(void)
+{
+ /* safe worst case allocation, all buffers belong to the checkpoint...
+ * that is pretty unlikely.
+ */
+ CheckpointBufferIds = (int *) palloc(sizeof(int) * NBuffers);
+}
+
/*
* BufferSync -- Write out all dirty buffers in the pool.
*
@@ -1575,10 +1628,17 @@ static void
BufferSync(int flags)
{
int buf_id;
- int num_to_scan;
int num_to_write;
int num_written;
+ int i;
int mask = BM_DIRTY;
+ FileFlushContext context;
+
+ ResetFileFlushContext(&context);
+
+ // lazy, to be really called by CheckpointerMain
+ if (CheckpointBufferIds == NULL)
+ AllocateCheckpointBufferIds();
/* Make sure we can handle the pin inside SyncOneBuffer */
ResourceOwnerEnlargeBuffers(CurrentResourceOwner);
@@ -1622,6 +1682,7 @@ BufferSync(int flags)
if ((bufHdr->flags & mask) == mask)
{
bufHdr->flags |= BM_CHECKPOINT_NEEDED;
+ CheckpointBufferIds[num_to_write] = buf_id;
num_to_write++;
}
@@ -1633,19 +1694,47 @@ BufferSync(int flags)
TRACE_POSTGRESQL_BUFFER_SYNC_START(NBuffers, num_to_write);
+ /* Sort buffer ids by chunks to help find sequential writes.
+ * Note: buffers are not locked in anyway, but that does not matter,
+ * this sorting is really advisory, if some buffer changes status during
+ * this pass it will be filtered out later. The only necessary property
+ * is that marked buffers do not move elsewhere. Also, qsort implementation
+ * should be resilient to occasional contradictions (cmp(a,b) != -cmp(b,a))
+ * because of these possible concurrent changes.
+ */
+ if (checkpoint_sort_size > 1)
+ {
+ int i;
+
+ // debug...
+ ereport(WARNING,
+ (errcode(ERRCODE_WARNING),
+ errmsg("Checkpoint: sorting %d buffers (%d chunks of size %d)",
+ num_to_write,
+ (checkpoint_sort_size+num_to_write-1) /
+ checkpoint_sort_size,
+ checkpoint_sort_size)));
+
+ // hmmm... should it equalize on the number of chunks?
+ for (i = 0; i < num_to_write; i += checkpoint_sort_size)
+ qsort(CheckpointBufferIds + i,
+ (i + checkpoint_sort_size <= num_to_write ?
+ checkpoint_sort_size : num_to_write - i),
+ sizeof(int),
+ (int(*)(const void *, const void *)) bufcmp);
+ }
+
/*
- * Loop over all buffers again, and write the ones (still) marked with
- * BM_CHECKPOINT_NEEDED. In this loop, we start at the clock sweep point
- * since we might as well dump soon-to-be-recycled buffers first.
+ * Loop over buffers again, and write the ones (still) marked with
+ * BM_CHECKPOINT_NEEDED.
*
- * Note that we don't read the buffer alloc count here --- that should be
- * left untouched till the next BgBufferSync() call.
+ * TODO: do something clever about table spaces...
+ * scan them in parallel with multiple threads?
*/
- buf_id = StrategySyncStart(NULL, NULL);
- num_to_scan = NBuffers;
num_written = 0;
- while (num_to_scan-- > 0)
+ for (i = 0; i < num_to_write; i++)
{
+ int buf_id = CheckpointBufferIds[i];
volatile BufferDesc *bufHdr = GetBufferDescriptor(buf_id);
/*
@@ -1662,38 +1751,31 @@ BufferSync(int flags)
*/
if (bufHdr->flags & BM_CHECKPOINT_NEEDED)
{
- if (SyncOneBuffer(buf_id, false) & BUF_WRITTEN)
+ if (SyncOneBuffer(buf_id, false, checkpoint_flush_to_disk, &context) & BUF_WRITTEN)
{
TRACE_POSTGRESQL_BUFFER_SYNC_WRITTEN(buf_id);
BgWriterStats.m_buf_written_checkpoints++;
num_written++;
/*
- * We know there are at most num_to_write buffers with
- * BM_CHECKPOINT_NEEDED set; so we can stop scanning if
- * num_written reaches num_to_write.
- *
- * Note that num_written doesn't include buffers written by
- * other backends, or by the bgwriter cleaning scan. That
- * means that the estimate of how much progress we've made is
- * conservative, and also that this test will often fail to
- * trigger. But it seems worth making anyway.
- */
- if (num_written >= num_to_write)
- break;
-
- /*
* Sleep to throttle our I/O rate.
*/
- CheckpointWriteDelay(flags, (double) num_written / num_to_write);
+ CheckpointWriteDelay(flags, (double) num_written / num_to_write, &context);
}
}
-
- if (++buf_id >= NBuffers)
- buf_id = 0;
}
/*
+ * Loop over all buffers again, and write the ones (still) marked with
+ * BM_CHECKPOINT_NEEDED. In this loop, we start at the clock sweep point
+ * since we might as well dump soon-to-be-recycled buffers first.
+ *
+ * Note that we don't read the buffer alloc count here --- that should be
+ * left untouched till the next BgBufferSync() call.
+ */
+ /* OLD CODE REMOVED */
+
+ /*
* Update checkpoint statistics. As noted above, this doesn't include
* buffers written by other backends or bgwriter scan.
*/
@@ -1757,6 +1839,8 @@ BgBufferSync(void)
long new_strategy_delta;
uint32 new_recent_alloc;
+ FileFlushContext context;
+
/*
* Find out where the freelist clock sweep currently is, and how many
* buffer allocations have happened since our last call.
@@ -1935,11 +2019,13 @@ BgBufferSync(void)
num_to_scan = bufs_to_lap;
num_written = 0;
reusable_buffers = reusable_buffers_est;
+ ResetFileFlushContext(&context);
/* Execute the LRU scan */
while (num_to_scan > 0 && reusable_buffers < upcoming_alloc_est)
{
- int buffer_state = SyncOneBuffer(next_to_clean, true);
+ int buffer_state =
+ SyncOneBuffer(next_to_clean, true, bgwriter_flush_to_disk, &context);
if (++next_to_clean >= NBuffers)
{
@@ -1963,6 +2049,9 @@ BgBufferSync(void)
BgWriterStats.m_buf_written_clean += num_written;
+ PerformFileFlush(&context);
+ ResetFileFlushContext(&context);
+
#ifdef BGW_DEBUG
elog(DEBUG1, "bgwriter: recent_alloc=%u smoothed=%.2f delta=%ld ahead=%d density=%.2f reusable_est=%d upcoming_est=%d scanned=%d wrote=%d reusable=%d",
recent_alloc, smoothed_alloc, strategy_delta, bufs_ahead,
@@ -2016,7 +2105,8 @@ BgBufferSync(void)
* Note: caller must have done ResourceOwnerEnlargeBuffers.
*/
static int
-SyncOneBuffer(int buf_id, bool skip_recently_used)
+SyncOneBuffer(int buf_id, bool skip_recently_used, bool flush_to_disk,
+ FileFlushContext * context)
{
volatile BufferDesc *bufHdr = GetBufferDescriptor(buf_id);
int result = 0;
@@ -2057,7 +2147,7 @@ SyncOneBuffer(int buf_id, bool skip_recently_used)
PinBuffer_Locked(bufHdr);
LWLockAcquire(bufHdr->content_lock, LW_SHARED);
- FlushBuffer(bufHdr, NULL);
+ FlushBuffer(bufHdr, NULL, flush_to_disk, context);
LWLockRelease(bufHdr->content_lock);
UnpinBuffer(bufHdr, true);
@@ -2319,9 +2409,13 @@ BufferGetTag(Buffer buffer, RelFileNode *rnode, ForkNumber *forknum,
*
* If the caller has an smgr reference for the buffer's relation, pass it
* as the second parameter. If not, pass NULL.
+ *
+ * The third parameter tries to hint the OS that a high priority write is meant,
+ * possibly because io-throttling is already managed elsewhere.
*/
static void
-FlushBuffer(volatile BufferDesc *buf, SMgrRelation reln)
+FlushBuffer(volatile BufferDesc *buf, SMgrRelation reln, bool flush_to_disk,
+ FileFlushContext * context)
{
XLogRecPtr recptr;
ErrorContextCallback errcallback;
@@ -2410,7 +2504,9 @@ FlushBuffer(volatile BufferDesc *buf, SMgrRelation reln)
buf->tag.forkNum,
buf->tag.blockNum,
bufToWrite,
- false);
+ false,
+ flush_to_disk,
+ context);
if (track_io_timing)
{
@@ -2830,7 +2926,9 @@ FlushRelationBuffers(Relation rel)
bufHdr->tag.forkNum,
bufHdr->tag.blockNum,
localpage,
- false);
+ false,
+ false,
+ NULL);
bufHdr->flags &= ~(BM_DIRTY | BM_JUST_DIRTIED);
@@ -2864,7 +2962,7 @@ FlushRelationBuffers(Relation rel)
{
PinBuffer_Locked(bufHdr);
LWLockAcquire(bufHdr->content_lock, LW_SHARED);
- FlushBuffer(bufHdr, rel->rd_smgr);
+ FlushBuffer(bufHdr, rel->rd_smgr, false, NULL);
LWLockRelease(bufHdr->content_lock);
UnpinBuffer(bufHdr, true);
}
@@ -2916,7 +3014,7 @@ FlushDatabaseBuffers(Oid dbid)
{
PinBuffer_Locked(bufHdr);
LWLockAcquire(bufHdr->content_lock, LW_SHARED);
- FlushBuffer(bufHdr, NULL);
+ FlushBuffer(bufHdr, NULL, false, NULL);
LWLockRelease(bufHdr->content_lock);
UnpinBuffer(bufHdr, true);
}
diff --git a/src/backend/storage/buffer/localbuf.c b/src/backend/storage/buffer/localbuf.c
index 3144afe..114a0a6 100644
--- a/src/backend/storage/buffer/localbuf.c
+++ b/src/backend/storage/buffer/localbuf.c
@@ -208,7 +208,9 @@ LocalBufferAlloc(SMgrRelation smgr, ForkNumber forkNum, BlockNumber blockNum,
bufHdr->tag.forkNum,
bufHdr->tag.blockNum,
localpage,
- false);
+ false,
+ false,
+ NULL);
/* Mark not-dirty now in case we error out below */
bufHdr->flags &= ~BM_DIRTY;
diff --git a/src/backend/storage/file/buffile.c b/src/backend/storage/file/buffile.c
index ea4d689..fb3b383 100644
--- a/src/backend/storage/file/buffile.c
+++ b/src/backend/storage/file/buffile.c
@@ -317,7 +317,7 @@ BufFileDumpBuffer(BufFile *file)
return; /* seek failed, give up */
file->offsets[file->curFile] = file->curOffset;
}
- bytestowrite = FileWrite(thisfile, file->buffer + wpos, bytestowrite);
+ bytestowrite = FileWrite(thisfile, file->buffer + wpos, bytestowrite, false, NULL);
if (bytestowrite <= 0)
return; /* failed to write */
file->offsets[file->curFile] += bytestowrite;
diff --git a/src/backend/storage/file/fd.c b/src/backend/storage/file/fd.c
index 1ba4946..bb28aec 100644
--- a/src/backend/storage/file/fd.c
+++ b/src/backend/storage/file/fd.c
@@ -1344,8 +1344,95 @@ retry:
return returnCode;
}
+#if defined(HAVE_SYNC_FILE_RANGE) || defined(HAVE_POSIX_FADVISE)
+
+void
+ResetFileFlushContext(FileFlushContext * context)
+{
+ context->fd = 0;
+ context->ncalls = 0;
+ context->offset = 0;
+ context->nbytes = 0;
+ context->filename = NULL;
+}
+
+void
+PerformFileFlush(FileFlushContext * context)
+{
+ if (context->ncalls != 0)
+ {
+ int rc;
+
+#if defined(HAVE_SYNC_FILE_RANGE)
+
+ /* Linux: tell the memory manager to move these blocks to io so
+ * that they are considered for being actually written to disk.
+ */
+ rc = sync_file_range(context->fd, context->offset, context->nbytes,
+ SYNC_FILE_RANGE_WRITE);
+
+#elif defined(HAVE_POSIX_FADVISE)
+
+ /* Others: say that data should not be kept in memory...
+ * This is not exactly what we want to say, because we want to write
+ * the data for durability but we may need it later nevertheless.
+ * It seems that Linux would free the memory *if* the data has
+ * already been written do disk, else it is ignored.
+ * For FreeBSD this may have the desired effect of moving the
+ * data to the io layer.
+ */
+ rc = posix_fadvise(context->fd, context->offset, context->nbytes,
+ POSIX_FADV_DONTNEED);
+
+#endif
+
+ if (rc < 0)
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not flush block " INT64_FORMAT
+ " on " INT64_FORMAT " blocks in file \"%s\": %m",
+ context->offset / BLCKSZ,
+ context->nbytes / BLCKSZ,
+ context->filename)));
+ }
+}
+
+void
+FileAsynchronousFlush(FileFlushContext * context,
+ int fd, off_t offset, off_t nbytes, char * filename)
+{
+ if (context->ncalls != 0 && context->fd == fd)
+ {
+ /* Same file: merge current flush with previous ones */
+ off_t new_offset = offset < context->offset? offset: context->offset;
+
+ context->nbytes =
+ (context->offset + context->nbytes > offset + nbytes ?
+ context->offset + context->nbytes : offset + nbytes) -
+ new_offset;
+ context->offset = new_offset;
+ context->ncalls ++;
+ }
+ else
+ {
+ /* file has changed; actually flush previous file before restarting
+ * to accumulate flushes
+ */
+ PerformFileFlush(context);
+
+ context->fd = fd;
+ context->ncalls = 1;
+ context->offset = offset;
+ context->nbytes = nbytes;
+ context->filename = filename;
+ }
+}
+
+#endif /* HAVE_SYNC_FILE_RANGE || HAVE_POSIX_FADVISE */
+
int
-FileWrite(File file, char *buffer, int amount)
+FileWrite(File file, char *buffer, int amount, bool flush_to_disk,
+ FileFlushContext * context)
{
int returnCode;
@@ -1395,6 +1482,28 @@ retry:
if (returnCode >= 0)
{
+
+#if defined(HAVE_SYNC_FILE_RANGE) || defined(HAVE_POSIX_FADVISE)
+
+ /*
+ * Calling "write" tells the OS that pg wants to write some page to disk,
+ * however when it is really done is chosen by the OS.
+ * Depending on other disk activities this may be delayed significantly,
+ * maybe up to an "fsync" call, which could induce an IO write surge.
+ * When checkpointing pg is doing its own throttling and the result
+ * should really be written to disk with high priority, so as to meet
+ * the completion target.
+ * This call hints that such write have a higher priority.
+ */
+ if (flush_to_disk && returnCode == amount && errno == 0)
+ {
+ FileAsynchronousFlush(context,
+ VfdCache[file].fd, VfdCache[file].seekPos,
+ amount, VfdCache[file].fileName);
+ }
+
+#endif /* HAVE_SYNC_FILE_RANGE || HAVE_POSIX_FADVISE */
+
VfdCache[file].seekPos += returnCode;
/* maintain fileSize and temporary_files_size if it's a temp file */
diff --git a/src/backend/storage/smgr/md.c b/src/backend/storage/smgr/md.c
index 42a43bb..dbf057f 100644
--- a/src/backend/storage/smgr/md.c
+++ b/src/backend/storage/smgr/md.c
@@ -531,7 +531,7 @@ mdextend(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
errmsg("could not seek to block %u in file \"%s\": %m",
blocknum, FilePathName(v->mdfd_vfd))));
- if ((nbytes = FileWrite(v->mdfd_vfd, buffer, BLCKSZ)) != BLCKSZ)
+ if ((nbytes = FileWrite(v->mdfd_vfd, buffer, BLCKSZ, false, NULL)) != BLCKSZ)
{
if (nbytes < 0)
ereport(ERROR,
@@ -738,7 +738,8 @@ mdread(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
*/
void
mdwrite(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
- char *buffer, bool skipFsync)
+ char *buffer, bool skipFsync, bool flush_to_disk,
+ FileFlushContext * context)
{
off_t seekpos;
int nbytes;
@@ -767,7 +768,7 @@ mdwrite(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
errmsg("could not seek to block %u in file \"%s\": %m",
blocknum, FilePathName(v->mdfd_vfd))));
- nbytes = FileWrite(v->mdfd_vfd, buffer, BLCKSZ);
+ nbytes = FileWrite(v->mdfd_vfd, buffer, BLCKSZ, flush_to_disk, context);
TRACE_POSTGRESQL_SMGR_MD_WRITE_DONE(forknum, blocknum,
reln->smgr_rnode.node.spcNode,
diff --git a/src/backend/storage/smgr/smgr.c b/src/backend/storage/smgr/smgr.c
index 244b4ea..2db3cd3 100644
--- a/src/backend/storage/smgr/smgr.c
+++ b/src/backend/storage/smgr/smgr.c
@@ -52,7 +52,8 @@ typedef struct f_smgr
void (*smgr_read) (SMgrRelation reln, ForkNumber forknum,
BlockNumber blocknum, char *buffer);
void (*smgr_write) (SMgrRelation reln, ForkNumber forknum,
- BlockNumber blocknum, char *buffer, bool skipFsync);
+ BlockNumber blocknum, char *buffer, bool skipFsync,
+ bool flush_to_disk, FileFlushContext *context);
BlockNumber (*smgr_nblocks) (SMgrRelation reln, ForkNumber forknum);
void (*smgr_truncate) (SMgrRelation reln, ForkNumber forknum,
BlockNumber nblocks);
@@ -643,10 +644,11 @@ smgrread(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
*/
void
smgrwrite(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
- char *buffer, bool skipFsync)
+ char *buffer, bool skipFsync, bool flush_to_disk,
+ FileFlushContext * context)
{
(*(smgrsw[reln->smgr_which].smgr_write)) (reln, forknum, blocknum,
- buffer, skipFsync);
+ buffer, skipFsync, flush_to_disk, context);
}
/*
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index b3c9f14..c8706ba 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -158,6 +158,7 @@ static bool check_bonjour(bool *newval, void **extra, GucSource source);
static bool check_ssl(bool *newval, void **extra, GucSource source);
static bool check_stage_log_stats(bool *newval, void **extra, GucSource source);
static bool check_log_stats(bool *newval, void **extra, GucSource source);
+static bool check_flush_to_disk(bool *newval, void **extra, GucSource source);
static bool check_canonical_path(char **newval, void **extra, GucSource source);
static bool check_timezone_abbreviations(char **newval, void **extra, GucSource source);
static void assign_timezone_abbreviations(const char *newval, void *extra);
@@ -569,6 +570,8 @@ const char *const config_group_names[] =
gettext_noop("Write-Ahead Log / Checkpoints"),
/* WAL_ARCHIVING */
gettext_noop("Write-Ahead Log / Archiving"),
+ /* BGWRITER */
+ gettext_noop("Background Writer"),
/* REPLICATION */
gettext_noop("Replication"),
/* REPLICATION_SENDING */
@@ -1009,6 +1012,27 @@ static struct config_bool ConfigureNamesBool[] =
false,
NULL, NULL, NULL
},
+
+ {
+ {"checkpoint_flush_to_disk", PGC_SIGHUP, WAL_CHECKPOINTS,
+ gettext_noop("Hint that checkpoint's writes are high priority."),
+ NULL
+ },
+ &checkpoint_flush_to_disk,
+ false,
+ check_flush_to_disk, NULL, NULL
+ },
+
+ {
+ {"bgwriter_flush_to_disk", PGC_SIGHUP, BGWRITER,
+ gettext_noop("Hint that bgwriter's writes are high priority."),
+ NULL
+ },
+ &bgwriter_flush_to_disk,
+ false,
+ check_flush_to_disk, NULL, NULL
+ },
+
{
{"log_connections", PGC_SU_BACKEND, LOGGING_WHAT,
gettext_noop("Logs each successful connection."),
@@ -2205,6 +2229,16 @@ static struct config_int ConfigureNamesInt[] =
},
{
+ {"checkpoint_sort_size", PGC_SIGHUP, WAL_CHECKPOINTS,
+ gettext_noop("Sort chunks of pages before writing them, ...."),
+ NULL
+ },
+ &checkpoint_sort_size,
+ 1024*1024, 0, INT_MAX,
+ NULL, NULL, NULL
+ },
+
+ {
{"wal_buffers", PGC_POSTMASTER, WAL_SETTINGS,
gettext_noop("Sets the number of disk-page buffers in shared memory for WAL."),
NULL,
@@ -9761,6 +9795,22 @@ check_log_stats(bool *newval, void **extra, GucSource source)
}
static bool
+check_flush_to_disk(bool *newval, void **extra, GucSource source)
+{
+/* This test must be consistent with the one in FileWrite (storage/file/fd.c)
+ */
+#if ! (defined(HAVE_SYNC_FILE_RANGE) || defined(HAVE_POSIX_FADVISE))
+ /* just warn if it has no effect */
+ ereport(WARNING,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("Setting \"checkpoint_flush_to_disk\" or "
+ "\"bgwriter_flush_to_disk\" has no effect "
+ "on this platform.")));
+#endif /* HAVE_SYNC_FILE_RANGE */
+ return true;
+}
+
+static bool
check_canonical_path(char **newval, void **extra, GucSource source)
{
/*
diff --git a/src/include/postmaster/bgwriter.h b/src/include/postmaster/bgwriter.h
index a49c208..c483832 100644
--- a/src/include/postmaster/bgwriter.h
+++ b/src/include/postmaster/bgwriter.h
@@ -29,7 +29,10 @@ extern void BackgroundWriterMain(void) pg_attribute_noreturn();
extern void CheckpointerMain(void) pg_attribute_noreturn();
extern void RequestCheckpoint(int flags);
-extern void CheckpointWriteDelay(int flags, double progress);
+struct FileFlushContext;
+typedef struct FileFlushContext FileFlushContext;
+extern void CheckpointWriteDelay(int flags, double progress,
+ FileFlushContext * context);
extern bool ForwardFsyncRequest(RelFileNode rnode, ForkNumber forknum,
BlockNumber segno);
diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.h
index ec0a254..2bf0cf8 100644
--- a/src/include/storage/bufmgr.h
+++ b/src/include/storage/bufmgr.h
@@ -54,6 +54,9 @@ extern int bgwriter_lru_maxpages;
extern double bgwriter_lru_multiplier;
extern bool track_io_timing;
extern int target_prefetch_pages;
+extern bool checkpoint_flush_to_disk;
+extern bool bgwriter_flush_to_disk;
+extern int checkpoint_sort_size;
/* in buf_init.c */
extern PGDLLIMPORT char *BufferBlocks;
diff --git a/src/include/storage/fd.h b/src/include/storage/fd.h
index 7eabe09..150c283 100644
--- a/src/include/storage/fd.h
+++ b/src/include/storage/fd.h
@@ -59,6 +59,22 @@ extern int max_files_per_process;
*/
extern int max_safe_fds;
+/* FileFlushContext:
+ * This structure is used to accumulate several flush requests on a file
+ * into a larger flush request.
+ * - fd: file descriptor of the file
+ * - ncalls: number of flushes merged together
+ * - offset: starting offset (minimum of all offset)
+ * - nbytes: size (minimum extent to cover all flushed data
+ * - filename: filename of fd for error messages
+ */
+typedef struct FileFlushContext{
+ int fd;
+ int ncalls;
+ off_t offset;
+ off_t nbytes;
+ char * filename;
+} FileFlushContext;
/*
* prototypes for functions in fd.c
@@ -70,7 +86,12 @@ extern File OpenTemporaryFile(bool interXact);
extern void FileClose(File file);
extern int FilePrefetch(File file, off_t offset, int amount);
extern int FileRead(File file, char *buffer, int amount);
-extern int FileWrite(File file, char *buffer, int amount);
+extern void ResetFileFlushContext(FileFlushContext * context);
+extern void PerformFileFlush(FileFlushContext * context);
+extern void FileAsynchronousFlush(FileFlushContext * context,
+ int fd, off_t offset, off_t nbytes, char * filename);
+extern int FileWrite(File file, char *buffer, int amount, bool flush_to_disk,
+ FileFlushContext * context);
extern int FileSync(File file);
extern off_t FileSeek(File file, off_t offset, int whence);
extern int FileTruncate(File file, off_t offset);
diff --git a/src/include/storage/smgr.h b/src/include/storage/smgr.h
index 69a624f..da0e929 100644
--- a/src/include/storage/smgr.h
+++ b/src/include/storage/smgr.h
@@ -94,8 +94,11 @@ extern void smgrprefetch(SMgrRelation reln, ForkNumber forknum,
BlockNumber blocknum);
extern void smgrread(SMgrRelation reln, ForkNumber forknum,
BlockNumber blocknum, char *buffer);
+struct FileFlushContext;
+typedef struct FileFlushContext FileFlushContext;
extern void smgrwrite(SMgrRelation reln, ForkNumber forknum,
- BlockNumber blocknum, char *buffer, bool skipFsync);
+ BlockNumber blocknum, char *buffer, bool skipFsync,
+ bool flush_to_disk, FileFlushContext * context);
extern BlockNumber smgrnblocks(SMgrRelation reln, ForkNumber forknum);
extern void smgrtruncate(SMgrRelation reln, ForkNumber forknum,
BlockNumber nblocks);
@@ -120,8 +123,9 @@ extern void mdprefetch(SMgrRelation reln, ForkNumber forknum,
BlockNumber blocknum);
extern void mdread(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
char *buffer);
-extern void mdwrite(SMgrRelation reln, ForkNumber forknum,
- BlockNumber blocknum, char *buffer, bool skipFsync);
+extern void mdwrite(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
+ char *buffer, bool skipFsync, bool flush_to_disk,
+ FileFlushContext * context);
extern BlockNumber mdnblocks(SMgrRelation reln, ForkNumber forknum);
extern void mdtruncate(SMgrRelation reln, ForkNumber forknum,
BlockNumber nblocks);
diff --git a/src/include/utils/guc_tables.h b/src/include/utils/guc_tables.h
index 7a58ddb..b69af2d 100644
--- a/src/include/utils/guc_tables.h
+++ b/src/include/utils/guc_tables.h
@@ -68,6 +68,7 @@ enum config_group
WAL_SETTINGS,
WAL_CHECKPOINTS,
WAL_ARCHIVING,
+ BGWRITER,
REPLICATION,
REPLICATION_SENDING,
REPLICATION_MASTER,
Le 07/06/2015 16:53, Fabien COELHO a �crit :
+� � /*�Others:�say�that�data�should�not�be�kept�in�memory... +� � �*�This�is�not�exactly�what�we�want�to�say,�because�we�want�to�write +� � �*�the�data�for�durability�but�we�may�need�it�later�nevertheless. +� � �*�It�seems�that�Linux�would�free�the�memory�*if*�the�data�has +� � �*�already�been�written�do�disk,�else�it�is�ignored. +� � �*�For�FreeBSD�this�may�have�the�desired�effect�of�moving�the +� � �*�data�to�the�io�layer. +� � �*/ +� � rc�=�posix_fadvise(context->fd,�context->offset,�context->nbytes, +� � � � � � ���POSIX_FADV_DONTNEED); +
It looks a bit hazardous, do you have a benchmark for freeBSD ?
Sources says:
case POSIX_FADV_DONTNEED:
/*
* Flush any open FS buffers and then remove pages
* from the backing VM object. Using vinvalbuf() here
* is a bit heavy-handed as it flushes all buffers for
* the given vnode, not just the buffers covering the
* requested range.
--
C�dric Villemain +33 (0)6 20 30 22 52
http://2ndQuadrant.fr/
PostgreSQL: Support 24x7 - D�veloppement, Expertise et Formation
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Hello C�dric,
It looks a bit hazardous, do you have a benchmark for freeBSD ?
No, I just consulted the FreeBSD man page for posix_fadvise. I someone can
run tests on something which HDDs is not linux, that would be nice.
Sources says:
case POSIX_FADV_DONTNEED:
/*
* Flush any open FS buffers and then remove pages
* from the backing VM object. Using vinvalbuf() here
* is a bit heavy-handed as it flushes all buffers for
* the given vnode, not just the buffers covering the
* requested range.
It is indeed heavy-handed, but that would probably trigger the expected
behavior which is to start writing to disk, so I would expect to see
benefits similar to those of "sync_file_range" on Linux.
Buffer writes from bgwriter & checkpointer are throttled, which reduces
the potential impact of a "heavy-handed" approach in the kernel.
Now if on some platforms the behavior is absurd, obviously it would be
better to turn the feature off on those.
Note that this is already used by pg in "initdb", but the impact would
probably be very small anyway.
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Hello,
Here is version 3, including many performance tests with various settings,
representing about 100 hours of pgbench run. This patch aims at improving
checkpoint I/O behavior so that tps throughput is improved, late
transactions are less frequent, and overall performances are more stable.
* SOLILOQUIZING
- The bgwriter flushing option seems ineffective, it could be removed
from the patch?
I did that.
- Move fsync as early as possible, suggested by Andres Freund?
My opinion is that this should be left out for the nonce.
I did that.
- Take into account tablespaces, as pointed out by Andres Freund?
Alternatively, maybe it can be done from one thread, but it would probably
involve some strange hocus-pocus to switch frequently between tablespaces.
I did the hocus-pocus approach, including a quasi-proof (not sure what is
this mathematical object:-) in comments to show how/why it works.
* PATCH CONTENTS
- as version 1: simplified asynchronous flush based on Andres Freund
patch, with sync_file_range/posix_fadvise used to hint the OS that
the buffer must be sent to disk "now".
- as version 2: checkpoint buffer sorting based on a 2007 patch by
Takahiro Itagaki but with a smaller and static buffer allocated once.
Also, sorting is done by chunks of 131072 pages in the current version,
with a guc to change this value.
- as version 2: sync/advise calls are now merged if possible,
so less calls will be used, especially when buffers are sorted,
but also if there are few files written.
- new: the checkpointer balance its page writes per tablespace.
this is done by choosing to write pages for a tablespace for which
the progress ratio (written/to_write) is beyond the overall progress
ratio for all tablespace, and by doing that in a round robin manner
so that all tablespaces regularly get some attention. No threads.
- new: some more documentation is added.
- removed: "bgwriter_flush_to_write" is removed, as there was no clear
benefit on the (simple) tests. It could be considered for another patch.
- question: I'm not sure I understand the checkpointer memory management.
There is some exception handling in the checkpointer main. I wonder
whether the allocated memory would be lost in such event and should
be reallocated. The patch currently assumes that the memory is kept.
* PERFORMANCE TESTS
Impacts on "pgbench -M prepared -N -P 1 ..." (simple update test, mostly
random write activity on one table), checkpoint_completion_target=0.8, with
different settings on a 16GB 8-core host:
. tiny: scale=10 shared_buffers=1GB checkpoint_timeout=30s time=6400s
. small: scale=120 shared_buffers=2GB checkpoint_timeout=300s time=4000s
. medium: scale=250 shared_buffers=4GB checkpoint_timeout=15min time=4000s
. large: scale=1000 shared_buffers=4GB checkpoint_timeout=40min time=7500s
Note: figures noted with a star (*) had various issues during their run, so
pgbench progress figures were more or less incorrect, thus the standard
deviation computation is not to be trusted beyond "pretty bad".
Caveat: these are only benches on one host at a particular time and
location, which may or may not be reproducible nor be representative
as such of any other load. The good news is that all these tests tell
the same thing.
- full-speed 1-client
options | tps performance over per second data
flush | sort | tiny | small | medium | large
off | off | 687 +- 231 | 163 +- 280 * | 191 +- 626 * | 37.7 +- 25.6
off | on | 699 +- 223 | 457 +- 315 | 479 +- 319 | 48.4 +- 28.8
on | off | 740 +- 125 | 143 +- 387 * | 179 +- 501 * | 37.3 +- 13.3
on | on | 722 +- 119 | 550 +- 140 | 549 +- 180 | 47.2 +- 16.8
- full speed 4-clients
options | tps performance over per second data
flush | sort | tiny | small | medium
off | off | 2006 +- 748 | 193 +- 1898 * | 205 +- 2465 *
off | on | 2086 +- 673 | 819 +- 905 * | 807 +- 1029 *
on | off | 2212 +- 451 | 169 +- 1269 * | 160 +- 502 *
on | on | 2073 +- 437 | 743 +- 413 | 822 +- 467
- 100-tps 1-client max 100-ms latency
options | percent of late transactions
flush | sort | tiny | small | medium
off | off | 6.31 | 29.44 | 30.74
off | on | 6.23 | 8.93 | 7.12
on | off | 0.44 | 7.01 | 8.14
on | on | 0.59 | 0.83 | 1.84
- 200-tps 1-client max 100-ms latency
options | percent of late transactions
flush | sort | tiny | small | medium
off | off | 10.00 | 50.61 | 45.51
off | on | 8.82 | 12.75 | 12.89
on | off | 0.59 | 40.48 | 42.64
on | on | 0.53 | 1.76 | 2.59
- 400-tps 1-client (or 4 for medium) max 100-ms latency
options | percent of late transactions
flush | sort | tiny | small | medium
off | off | 12.0 | 64.28 | 68.6
off | on | 11.3 | 22.05 | 22.6
on | off | 1.1 | 67.93 | 67.9
on | on | 0.6 | 3.24 | 3.1
* CONCLUSION :
For most of these HDD tests, when both options are activated the tps
throughput is improved (+3 to +300%), late transactions are reduced (by
91% to 97%) and overall the performance is more stable (tps standard
deviation is typically halved).
The option effects are somehow orthogonal:
- latency is essentially limited by flushing, although sorting also
contributes.
- throughput is mostly improved thanks to sorting, with some occasional
small positive or negative effect from flushing.
In detail, some loads may benefit more from only one option activated. In
particular, flushing may have a small adverse effect on throughput in some
conditions, although not always. With SSD probably both options would
probably have limited benefit.
--
Fabien.
Attachments:
checkpoint-continuous-flush-3.patchtext/x-diff; name=checkpoint-continuous-flush-3.patchDownload
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 1da7dfb..7a3d274 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -2474,6 +2474,29 @@ include_dir 'conf.d'
</listitem>
</varlistentry>
+ <varlistentry id="guc-checkpoint-sort-size" xreflabel="checkpoint_sort_size">
+ <term><varname>checkpoint_sort_size</varname> (<type>integer</type>)
+ <indexterm>
+ <primary><varname>checkpoint_sort_size</> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ The number of pages in the chunks sorted together before being written
+ out to disk by a checkpoint.
+ For a HDD storage, this setting allows to group together
+ neighboring pages written to disk, thus improving performance by
+ reducing random write activity.
+ This sorting can be skipped for SSD backends as such storages have good
+ random write performance.
+ The default is <literal>131072</>.
+ This feature is turned off by setting the value to <literal>0</>.
+ This parameter can only be set in the <filename>postgresql.conf</>
+ file or on the server command line.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="guc-checkpoint-warning" xreflabel="checkpoint_warning">
<term><varname>checkpoint_warning</varname> (<type>integer</type>)
<indexterm>
@@ -2495,6 +2518,24 @@ include_dir 'conf.d'
</listitem>
</varlistentry>
+ <varlistentry id="guc-checkpoint-flush-to-disk" xreflabel="checkpoint_flush_to_disk">
+ <term><varname>checkpoint_flush_to_disk</varname> (<type>bool</type>)
+ <indexterm>
+ <primary><varname>checkpoint_flush_to_disk</> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ When writing data for a checkpoint, hint the underlying OS that the
+ data must be sent to disk as soon as possible. This may help smoothing
+ disk I/O writes and avoid a stall when fsync is issued at the end of
+ the checkpoint, but it may also reduce average performance.
+ This setting may have no effect on some platforms.
+ The default is <literal>off</>.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="guc-min-wal-size" xreflabel="min_wal_size">
<term><varname>min_wal_size</varname> (<type>integer</type>)
<indexterm>
diff --git a/doc/src/sgml/wal.sgml b/doc/src/sgml/wal.sgml
index f4083c3..2b6aab7 100644
--- a/doc/src/sgml/wal.sgml
+++ b/doc/src/sgml/wal.sgml
@@ -546,6 +546,27 @@
</para>
<para>
+ When hard-disk drives (HDD) are used for terminal data storage,
+ <xref linkend="guc-checkpoint-sort-size">, allows to sort chunks of pages
+ so that neighboring pages on disk will be flushed together by
+ chekpoints, reducing the random write load and improving performance.
+ If solid-state drives (SSD) are used, sorting pages induces no benefit
+ as their random write I/O performance is good: this feature should then
+ be disabled by setting <varname>checkpoint_sort_size</> to <value>0</>.
+ </para>
+
+ <para>
+ On Linux and POSIX platforms, <xref linkend="guc-checkpoint-flush-to-disk">
+ allows to hint the OS that pages written on checkpoints must be flushed
+ to disk quickly. Otherwise, these pages may be kept in cache for some time,
+ inducing a stall later when <literal>fsync</> is called to actually
+ complete the checkpoint. This setting helps to reduce transaction latency,
+ but it may also have a small adverse effect on the average transaction rate
+ at maximum throughput. This feature probably brings no benefit on SSD,
+ as the I/O write latency is small on such hardware, thus it may be disabled.
+ </para>
+
+ <para>
The number of WAL segment files in <filename>pg_xlog</> directory depends on
<varname>min_wal_size</>, <varname>max_wal_size</> and
the amount of WAL generated in previous checkpoint cycles. When old log
diff --git a/src/backend/access/heap/rewriteheap.c b/src/backend/access/heap/rewriteheap.c
index bcce3e3..f565dc4 100644
--- a/src/backend/access/heap/rewriteheap.c
+++ b/src/backend/access/heap/rewriteheap.c
@@ -918,7 +918,7 @@ logical_heap_rewrite_flush_mappings(RewriteState state)
* Note that we deviate from the usual WAL coding practices here,
* check the above "Logical rewrite support" comment for reasoning.
*/
- written = FileWrite(src->vfd, waldata_start, len);
+ written = FileWrite(src->vfd, waldata_start, len, false, NULL);
if (written != len)
ereport(ERROR,
(errcode_for_file_access(),
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index 9431ab5..49ec258 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -203,7 +203,7 @@ btbuildempty(PG_FUNCTION_ARGS)
/* Write the page. If archiving/streaming, XLOG it. */
PageSetChecksumInplace(metapage, BTREE_METAPAGE);
smgrwrite(index->rd_smgr, INIT_FORKNUM, BTREE_METAPAGE,
- (char *) metapage, true);
+ (char *) metapage, true, false, NULL);
if (XLogIsNeeded())
log_newpage(&index->rd_smgr->smgr_rnode.node, INIT_FORKNUM,
BTREE_METAPAGE, metapage, false);
diff --git a/src/backend/access/nbtree/nbtsort.c b/src/backend/access/nbtree/nbtsort.c
index f95f67a..ea7a45d 100644
--- a/src/backend/access/nbtree/nbtsort.c
+++ b/src/backend/access/nbtree/nbtsort.c
@@ -315,7 +315,7 @@ _bt_blwritepage(BTWriteState *wstate, Page page, BlockNumber blkno)
{
/* overwriting a block we zero-filled before */
smgrwrite(wstate->index->rd_smgr, MAIN_FORKNUM, blkno,
- (char *) page, true);
+ (char *) page, true, false, NULL);
}
pfree(page);
diff --git a/src/backend/access/spgist/spginsert.c b/src/backend/access/spgist/spginsert.c
index bceee8d..b700efb 100644
--- a/src/backend/access/spgist/spginsert.c
+++ b/src/backend/access/spgist/spginsert.c
@@ -170,7 +170,7 @@ spgbuildempty(PG_FUNCTION_ARGS)
/* Write the page. If archiving/streaming, XLOG it. */
PageSetChecksumInplace(page, SPGIST_METAPAGE_BLKNO);
smgrwrite(index->rd_smgr, INIT_FORKNUM, SPGIST_METAPAGE_BLKNO,
- (char *) page, true);
+ (char *) page, true, false, NULL);
if (XLogIsNeeded())
log_newpage(&index->rd_smgr->smgr_rnode.node, INIT_FORKNUM,
SPGIST_METAPAGE_BLKNO, page, false);
@@ -180,7 +180,7 @@ spgbuildempty(PG_FUNCTION_ARGS)
PageSetChecksumInplace(page, SPGIST_ROOT_BLKNO);
smgrwrite(index->rd_smgr, INIT_FORKNUM, SPGIST_ROOT_BLKNO,
- (char *) page, true);
+ (char *) page, true, false, NULL);
if (XLogIsNeeded())
log_newpage(&index->rd_smgr->smgr_rnode.node, INIT_FORKNUM,
SPGIST_ROOT_BLKNO, page, true);
@@ -190,7 +190,7 @@ spgbuildempty(PG_FUNCTION_ARGS)
PageSetChecksumInplace(page, SPGIST_NULL_BLKNO);
smgrwrite(index->rd_smgr, INIT_FORKNUM, SPGIST_NULL_BLKNO,
- (char *) page, true);
+ (char *) page, true, false, NULL);
if (XLogIsNeeded())
log_newpage(&index->rd_smgr->smgr_rnode.node, INIT_FORKNUM,
SPGIST_NULL_BLKNO, page, true);
diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c
index 0dce6a8..52dd7db 100644
--- a/src/backend/postmaster/checkpointer.c
+++ b/src/backend/postmaster/checkpointer.c
@@ -663,7 +663,8 @@ ImmediateCheckpointRequested(void)
* fraction between 0.0 meaning none, and 1.0 meaning all done.
*/
void
-CheckpointWriteDelay(int flags, double progress)
+CheckpointWriteDelay(int flags, double progress,
+ FileFlushContext * context, int ctx_size)
{
static int absorb_counter = WRITES_PER_ABSORB;
@@ -698,6 +699,26 @@ CheckpointWriteDelay(int flags, double progress)
*/
pgstat_send_bgwriter();
+#if defined(HAVE_SYNC_FILE_RANGE) || defined(HAVE_POSIX_FADVISE)
+
+ /*
+ * Before sleeping, flush written blocks for each tablespace.
+ */
+ if (checkpoint_flush_to_disk)
+ {
+ int i;
+
+ for (i = 0; i < ctx_size; i++)
+ {
+ if (context[i].ncalls != 0)
+ {
+ PerformFileFlush(&context[i]);
+ ResetFileFlushContext(&context[i]);
+ }
+ }
+ }
+#endif /* HAVE_SYNC_FILE_RANGE || HAVE_POSIX_FADVISE */
+
/*
* This sleep used to be connected to bgwriter_delay, typically 200ms.
* That resulted in more frequent wakeups if not much work to do.
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index cc973b5..3ea1028 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -80,6 +80,10 @@ bool zero_damaged_pages = false;
int bgwriter_lru_maxpages = 100;
double bgwriter_lru_multiplier = 2.0;
bool track_io_timing = false;
+/* hint to move writes to high priority */
+bool checkpoint_flush_to_disk = false;
+/* by default, sort by chunks of 1 GB worth of 8 kB buffers */
+int checkpoint_sort_size = 128 * 1024;
/*
* How many buffers PrefetchBuffer callers should try to stay ahead of their
@@ -396,7 +400,8 @@ static bool PinBuffer(volatile BufferDesc *buf, BufferAccessStrategy strategy);
static void PinBuffer_Locked(volatile BufferDesc *buf);
static void UnpinBuffer(volatile BufferDesc *buf, bool fixOwner);
static void BufferSync(int flags);
-static int SyncOneBuffer(int buf_id, bool skip_recently_used);
+static int SyncOneBuffer(int buf_id, bool skip_recently_used,
+ bool flush_to_disk, FileFlushContext *context);
static void WaitIO(volatile BufferDesc *buf);
static bool StartBufferIO(volatile BufferDesc *buf, bool forInput);
static void TerminateBufferIO(volatile BufferDesc *buf, bool clear_dirty,
@@ -409,7 +414,8 @@ static volatile BufferDesc *BufferAlloc(SMgrRelation smgr,
BlockNumber blockNum,
BufferAccessStrategy strategy,
bool *foundPtr);
-static void FlushBuffer(volatile BufferDesc *buf, SMgrRelation reln);
+static void FlushBuffer(volatile BufferDesc *buf, SMgrRelation reln,
+ bool flush_to_disk, FileFlushContext *context);
static void AtProcExit_Buffers(int code, Datum arg);
static void CheckForBufferLeaks(void);
static int rnode_comparator(const void *p1, const void *p2);
@@ -1018,7 +1024,7 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
smgr->smgr_rnode.node.dbNode,
smgr->smgr_rnode.node.relNode);
- FlushBuffer(buf, NULL);
+ FlushBuffer(buf, NULL, false, NULL);
LWLockRelease(buf->content_lock);
TRACE_POSTGRESQL_BUFFER_WRITE_DIRTY_DONE(forkNum, blockNum,
@@ -1561,6 +1567,75 @@ UnpinBuffer(volatile BufferDesc *buf, bool fixOwner)
}
}
+/* Array of buffer ids of all buffers to checkpoint.
+ */
+static int * CheckpointBufferIds = NULL;
+
+/* Compare checkpoint buffers
+ */
+static int bufcmp(const int * pa, const int * pb)
+{
+ BufferDesc
+ *a = GetBufferDescriptor(*pa),
+ *b = GetBufferDescriptor(*pb);
+
+ /* tag: rnode, forkNum (different files), blockNum
+ * rnode: { spcNode (ignore: not really needed),
+ * dbNode (ignore: this is a directory), relNode }
+ * spcNode: table space oid, not that there are at least two
+ * (pg_global and pg_default).
+ */
+ /* compare relation */
+ if (a->tag.rnode.relNode < b->tag.rnode.relNode)
+ return -1;
+ else if (a->tag.rnode.relNode > b->tag.rnode.relNode)
+ return 1;
+ /* same relation, compare fork */
+ else if (a->tag.forkNum < b->tag.forkNum)
+ return -1;
+ else if (a->tag.forkNum > b->tag.forkNum)
+ return 1;
+ /* same relation/fork, so same segmented "file", compare block number
+ * which are mapped on different segments depending on the number.
+ */
+ else if (a->tag.blockNum < b->tag.blockNum)
+ return -1;
+ else /* should not be the same block anyway... */
+ return 1;
+}
+
+static void AllocateCheckpointBufferIds(void)
+{
+ /* Safe worst case allocation, all buffers belong to the checkpoint...
+ * that is pretty unlikely.
+ */
+ CheckpointBufferIds = (int *) palloc(sizeof(int) * NBuffers);
+}
+
+/* Status of buffers to checkpoint for a particular tablespace,
+ * used internally in BufferSync.
+ * - spcNone: oid of the tablespace
+ * - num_to_write: number of pages counted for this tablespace
+ * - num_written: number of pages actually written out
+ * - index: scanning position in CheckpointBufferIds for this tablespace
+ * - done: whether it is done
+ */
+typedef struct TableSpaceCheckpointStatus {
+ Oid space;
+ int num_to_write;
+ int num_written;
+ int index;
+ bool done;
+} TableSpaceCheckpointStatus;
+
+/* entry structure for table space to count hashtable,
+ * used internally in BufferSync.
+ */
+typedef struct TableSpaceCountEntry {
+ Oid space;
+ int count;
+} TableSpaceCountEntry;
+
/*
* BufferSync -- Write out all dirty buffers in the pool.
*
@@ -1575,10 +1650,21 @@ static void
BufferSync(int flags)
{
int buf_id;
- int num_to_scan;
int num_to_write;
int num_written;
+ int i;
int mask = BM_DIRTY;
+ HTAB *spcBuffers;
+ TableSpaceCheckpointStatus *spcStatus = NULL;
+ int nb_spaces, active_spaces, space;
+ FileFlushContext * spcContext = NULL;
+
+ /*
+ * Lazy allocation: this function is called through the checkpointer,
+ * but also by initdb. Maybe the allocation could be moved to the callers.
+ */
+ if (CheckpointBufferIds == NULL)
+ AllocateCheckpointBufferIds();
/* Make sure we can handle the pin inside SyncOneBuffer */
ResourceOwnerEnlargeBuffers(CurrentResourceOwner);
@@ -1609,6 +1695,18 @@ BufferSync(int flags)
* certainly need to be written for the next checkpoint attempt, too.
*/
num_to_write = 0;
+
+ /* initialize oid -> int buffer count hash table */
+ {
+ HASHCTL ctl;
+
+ MemSet(&ctl, 0, sizeof(HASHCTL));
+ ctl.keysize = sizeof(Oid);
+ ctl.entrysize = sizeof(TableSpaceCountEntry);
+ spcBuffers = hash_create("Number of buffers to write per tablespace",
+ 16, &ctl, HASH_ELEM | HASH_BLOBS);
+ }
+
for (buf_id = 0; buf_id < NBuffers; buf_id++)
{
volatile BufferDesc *bufHdr = GetBufferDescriptor(buf_id);
@@ -1621,32 +1719,185 @@ BufferSync(int flags)
if ((bufHdr->flags & mask) == mask)
{
+ Oid spc;
+ TableSpaceCountEntry * entry;
+ bool found;
+
bufHdr->flags |= BM_CHECKPOINT_NEEDED;
+ CheckpointBufferIds[num_to_write] = buf_id;
num_to_write++;
+
+ /* keep track of per tablespace buffers */
+ spc = bufHdr->tag.rnode.spcNode;
+ entry = (TableSpaceCountEntry *)
+ hash_search(spcBuffers, (void *) &spc, HASH_ENTER, &found);
+
+ if (found) entry->count++;
+ else entry->count = 1;
}
UnlockBufHdr(bufHdr);
}
if (num_to_write == 0)
+ {
+ hash_destroy(spcBuffers);
return; /* nothing to do */
+ }
TRACE_POSTGRESQL_BUFFER_SYNC_START(NBuffers, num_to_write);
+ /* Build checkpoint tablespace buffer status & flush context arrays */
+ nb_spaces = hash_get_num_entries(spcBuffers);
+ spcStatus = (TableSpaceCheckpointStatus *)
+ palloc(sizeof(TableSpaceCheckpointStatus) * nb_spaces);
+ spcContext = (FileFlushContext *)
+ palloc(sizeof(FileFlushContext) * nb_spaces);
+
+ {
+ int index = 0;
+ HASH_SEQ_STATUS hseq;
+ TableSpaceCountEntry * entry;
+
+ hash_seq_init(&hseq, spcBuffers);
+ while ((entry = (TableSpaceCountEntry *) hash_seq_search(&hseq)))
+ {
+ Assert(index < nb_spaces);
+ spcStatus[index].space = entry->space;
+ spcStatus[index].num_to_write = entry->count;
+ spcStatus[index].num_written = 0;
+ /* should it be randomized? chosen with some criterion? */
+ spcStatus[index].index = 0;
+ spcStatus[index].done = false;
+
+#if defined(HAVE_SYNC_FILE_RANGE) || defined(HAVE_POSIX_FADVISE)
+
+ ResetFileFlushContext(&spcContext[index]);
+
+#endif /* HAVE_SYNC_FILE_RANGE || HAVE_POSIX_FADVISE */
+
+ index ++;
+ }
+ }
+
+ hash_destroy(spcBuffers);
+ spcBuffers = NULL;
+
/*
- * Loop over all buffers again, and write the ones (still) marked with
- * BM_CHECKPOINT_NEEDED. In this loop, we start at the clock sweep point
- * since we might as well dump soon-to-be-recycled buffers first.
+ * Sort buffer ids by chunks to help find sequential writes.
+ * Note: buffers are not locked in anyway, but that does not matter,
+ * this sorting is really advisory, if some buffer changes status during
+ * this pass it will be filtered out later. The only necessary property
+ * is that marked buffers do not move elsewhere. Also, qsort implementation
+ * should be resilient to occasional contradictions (cmp(a,b) != -cmp(b,a))
+ * because of these possible concurrent changes.
+ */
+ if (checkpoint_sort_size > 1)
+ {
+ /* debug...
+ ereport(WARNING,
+ (errcode(ERRCODE_WARNING),
+ errmsg("Checkpoint: sorting %d buffers (%d chunks, size=%d)",
+ num_to_write,
+ (checkpoint_sort_size+num_to_write-1) /
+ checkpoint_sort_size,
+ checkpoint_sort_size)));
+ */
+
+ for (i = 0; i < num_to_write; i += checkpoint_sort_size)
+ qsort(CheckpointBufferIds + i,
+ (i + checkpoint_sort_size <= num_to_write ?
+ checkpoint_sort_size : num_to_write - i),
+ sizeof(int),
+ (int(*)(const void *, const void *)) bufcmp);
+ }
+
+ /* debug
+ ereport(WARNING,
+ (errcode(ERRCODE_WARNING),
+ errmsg("Checkpoint: running on %d tablespaces",
+ nb_spaces)));
+ */
+
+ /*
+ * Loop over buffers to write through CheckpointBufferIds,
+ * and write the ones (still) marked with BM_CHECKPOINT_NEEDED,
+ * with some round robin over table spaces so as to balance writes,
+ * so that buffer writes move forward roughly proportionally for each
+ * tablespace.
*
- * Note that we don't read the buffer alloc count here --- that should be
- * left untouched till the next BgBufferSync() call.
+ * Termination: if a tablespace is selected by the inner while loop
+ * (see argument there), its index is incremented and will eventually
+ * reach num_to_write, mark this table space scanning as done and
+ * decrement the number of active spaces, which will thus reach 0.
*/
- buf_id = StrategySyncStart(NULL, NULL);
- num_to_scan = NBuffers;
+ active_spaces = nb_spaces;
+ space = 0;
num_written = 0;
- while (num_to_scan-- > 0)
+
+ while (active_spaces != 0)
{
- volatile BufferDesc *bufHdr = GetBufferDescriptor(buf_id);
+ volatile BufferDesc *bufHdr = NULL;
+ int index;
+
+ /*
+ * Select a tablespace depending on the current overall progress.
+ *
+ * The progress ratio of each unfinished tablespace is compared to
+ * the overall progress ratio to find one with is not in advance
+ * (i.e. tablespace ratio <= overall ratio).
+ *
+ * Existence: it is bound to exist otherwise the overall progress
+ * ratio would be inconsistent: with positive buffers to write (t1 & t2)
+ * and already written buffers (w1 & w2), we have:
+ *
+ * If w1/t1 > (w1+w2)/(t1+t2) # one table space is in advance
+ * => w1t1+w1t2 > w1t1+w2t1 => w1t2 > w2t1 => w1t2+w2t2 > w2t1+w2t2
+ * => (w1+w2) / (t1+t2) > w2 / t2 # the other one is late
+ *
+ * The round robin ensures that each space is given some attention
+ * till it is over the current ratio, before going to the next.
+ *
+ * Precision: using int32 computations for comparing fractions
+ * (w1 / t1 > w / t <=> w1 t > w t1) seems a bad idea as the values
+ * can overflow 32-bit integers: the limit would be sqrt(2**31) ~
+ * 46340 buffers, i.e. a 362 MB checkpoint. So ensure that 64-bit
+ * integers are used in the comparison.
+ */
+ while (spcStatus[space].done ||
+ /* compare tablespace vs overall progress ratio:
+ * tablespace written/to_write > overall written/to_write
+ */
+ (int64) spcStatus[space].num_written * num_to_write >
+ (int64) num_written * spcStatus[space].num_to_write)
+ space = (space + 1) % nb_spaces; /* round robin */
+
+ /*
+ * Find a valid buffer in the selected tablespace,
+ * by continuing the tablespace specific buffer scan
+ * where it was left.
+ */
+ index = spcStatus[space].index;
+
+ while (index < num_to_write && bufHdr == NULL)
+ {
+ buf_id = CheckpointBufferIds[index];
+ bufHdr = GetBufferDescriptor(buf_id);
+
+ /* Skip if in another tablespace or not in checkpoint anymore.
+ * No lock is acquired, see comments below.
+ */
+ if (spcStatus[space].space != bufHdr->tag.rnode.spcNode ||
+ ! (bufHdr->flags & BM_CHECKPOINT_NEEDED))
+ {
+ index ++;
+ buf_id = -1;
+ bufHdr = NULL;
+ }
+ }
+
+ /* Update tablespace writing status, will start over at next index */
+ spcStatus[space].index = index+1;
/*
* We don't need to acquire the lock here, because we're only looking
@@ -1660,39 +1911,49 @@ BufferSync(int flags)
* write the buffer though we didn't need to. It doesn't seem worth
* guarding against this, though.
*/
- if (bufHdr->flags & BM_CHECKPOINT_NEEDED)
+ if (bufHdr != NULL && bufHdr->flags & BM_CHECKPOINT_NEEDED)
{
- if (SyncOneBuffer(buf_id, false) & BUF_WRITTEN)
+ if (SyncOneBuffer(buf_id, false, checkpoint_flush_to_disk,
+ &spcContext[space]) & BUF_WRITTEN)
{
TRACE_POSTGRESQL_BUFFER_SYNC_WRITTEN(buf_id);
BgWriterStats.m_buf_written_checkpoints++;
+ spcStatus[space].num_written++;
num_written++;
/*
- * We know there are at most num_to_write buffers with
- * BM_CHECKPOINT_NEEDED set; so we can stop scanning if
- * num_written reaches num_to_write.
- *
- * Note that num_written doesn't include buffers written by
- * other backends, or by the bgwriter cleaning scan. That
- * means that the estimate of how much progress we've made is
- * conservative, and also that this test will often fail to
- * trigger. But it seems worth making anyway.
- */
- if (num_written >= num_to_write)
- break;
-
- /*
* Sleep to throttle our I/O rate.
*/
- CheckpointWriteDelay(flags, (double) num_written / num_to_write);
+ CheckpointWriteDelay(flags, (double) num_written / num_to_write,
+ spcContext, nb_spaces);
}
}
- if (++buf_id >= NBuffers)
- buf_id = 0;
+ /*
+ * Detect checkpoint end for a tablespace: either the scan is done
+ * or all tablespace buffers have been written out.
+ */
+ if (spcStatus[space].index >= num_to_write ||
+ spcStatus[space].num_written >= spcStatus[space].num_to_write)
+ {
+
+#if defined(HAVE_SYNC_FILE_RANGE) || defined(HAVE_POSIX_FADVISE)
+
+ PerformFileFlush(&spcContext[space]);
+ ResetFileFlushContext(&spcContext[space]);
+
+#endif /* HAVE_SYNC_FILE_RANGE || HAVE_POSIX_FADVISE */
+
+ spcStatus[space].done = true;
+ active_spaces--;
+ }
}
+ pfree(spcStatus);
+ spcStatus = NULL;
+ pfree(spcContext);
+ spcContext = NULL;
+
/*
* Update checkpoint statistics. As noted above, this doesn't include
* buffers written by other backends or bgwriter scan.
@@ -1939,7 +2200,8 @@ BgBufferSync(void)
/* Execute the LRU scan */
while (num_to_scan > 0 && reusable_buffers < upcoming_alloc_est)
{
- int buffer_state = SyncOneBuffer(next_to_clean, true);
+ int buffer_state =
+ SyncOneBuffer(next_to_clean, true, false, NULL);
if (++next_to_clean >= NBuffers)
{
@@ -2016,7 +2278,8 @@ BgBufferSync(void)
* Note: caller must have done ResourceOwnerEnlargeBuffers.
*/
static int
-SyncOneBuffer(int buf_id, bool skip_recently_used)
+SyncOneBuffer(int buf_id, bool skip_recently_used, bool flush_to_disk,
+ FileFlushContext * context)
{
volatile BufferDesc *bufHdr = GetBufferDescriptor(buf_id);
int result = 0;
@@ -2057,7 +2320,7 @@ SyncOneBuffer(int buf_id, bool skip_recently_used)
PinBuffer_Locked(bufHdr);
LWLockAcquire(bufHdr->content_lock, LW_SHARED);
- FlushBuffer(bufHdr, NULL);
+ FlushBuffer(bufHdr, NULL, flush_to_disk, context);
LWLockRelease(bufHdr->content_lock);
UnpinBuffer(bufHdr, true);
@@ -2319,9 +2582,16 @@ BufferGetTag(Buffer buffer, RelFileNode *rnode, ForkNumber *forknum,
*
* If the caller has an smgr reference for the buffer's relation, pass it
* as the second parameter. If not, pass NULL.
+ *
+ * The third parameter tries to hint the OS that a high priority write is meant,
+ * possibly because io-throttling is already managed elsewhere.
+ * The last parameter holds the current flush context that accumulates flush
+ * requests to be performed in one call, instead of being performed on a buffer
+ * per buffer basis.
*/
static void
-FlushBuffer(volatile BufferDesc *buf, SMgrRelation reln)
+FlushBuffer(volatile BufferDesc *buf, SMgrRelation reln, bool flush_to_disk,
+ FileFlushContext * context)
{
XLogRecPtr recptr;
ErrorContextCallback errcallback;
@@ -2410,7 +2680,9 @@ FlushBuffer(volatile BufferDesc *buf, SMgrRelation reln)
buf->tag.forkNum,
buf->tag.blockNum,
bufToWrite,
- false);
+ false,
+ flush_to_disk,
+ context);
if (track_io_timing)
{
@@ -2830,7 +3102,9 @@ FlushRelationBuffers(Relation rel)
bufHdr->tag.forkNum,
bufHdr->tag.blockNum,
localpage,
- false);
+ false,
+ false,
+ NULL);
bufHdr->flags &= ~(BM_DIRTY | BM_JUST_DIRTIED);
@@ -2864,7 +3138,7 @@ FlushRelationBuffers(Relation rel)
{
PinBuffer_Locked(bufHdr);
LWLockAcquire(bufHdr->content_lock, LW_SHARED);
- FlushBuffer(bufHdr, rel->rd_smgr);
+ FlushBuffer(bufHdr, rel->rd_smgr, false, NULL);
LWLockRelease(bufHdr->content_lock);
UnpinBuffer(bufHdr, true);
}
@@ -2916,7 +3190,7 @@ FlushDatabaseBuffers(Oid dbid)
{
PinBuffer_Locked(bufHdr);
LWLockAcquire(bufHdr->content_lock, LW_SHARED);
- FlushBuffer(bufHdr, NULL);
+ FlushBuffer(bufHdr, NULL, false, NULL);
LWLockRelease(bufHdr->content_lock);
UnpinBuffer(bufHdr, true);
}
diff --git a/src/backend/storage/buffer/localbuf.c b/src/backend/storage/buffer/localbuf.c
index 3144afe..114a0a6 100644
--- a/src/backend/storage/buffer/localbuf.c
+++ b/src/backend/storage/buffer/localbuf.c
@@ -208,7 +208,9 @@ LocalBufferAlloc(SMgrRelation smgr, ForkNumber forkNum, BlockNumber blockNum,
bufHdr->tag.forkNum,
bufHdr->tag.blockNum,
localpage,
- false);
+ false,
+ false,
+ NULL);
/* Mark not-dirty now in case we error out below */
bufHdr->flags &= ~BM_DIRTY;
diff --git a/src/backend/storage/file/buffile.c b/src/backend/storage/file/buffile.c
index ea4d689..fb3b383 100644
--- a/src/backend/storage/file/buffile.c
+++ b/src/backend/storage/file/buffile.c
@@ -317,7 +317,7 @@ BufFileDumpBuffer(BufFile *file)
return; /* seek failed, give up */
file->offsets[file->curFile] = file->curOffset;
}
- bytestowrite = FileWrite(thisfile, file->buffer + wpos, bytestowrite);
+ bytestowrite = FileWrite(thisfile, file->buffer + wpos, bytestowrite, false, NULL);
if (bytestowrite <= 0)
return; /* failed to write */
file->offsets[file->curFile] += bytestowrite;
diff --git a/src/backend/storage/file/fd.c b/src/backend/storage/file/fd.c
index 1ba4946..daf03e4 100644
--- a/src/backend/storage/file/fd.c
+++ b/src/backend/storage/file/fd.c
@@ -1344,8 +1344,97 @@ retry:
return returnCode;
}
+#if defined(HAVE_SYNC_FILE_RANGE) || defined(HAVE_POSIX_FADVISE)
+
+void
+ResetFileFlushContext(FileFlushContext * context)
+{
+ context->fd = 0;
+ context->ncalls = 0;
+ context->offset = 0;
+ context->nbytes = 0;
+ context->filename = NULL;
+}
+
+void
+PerformFileFlush(FileFlushContext * context)
+{
+ if (context->ncalls != 0)
+ {
+ int rc;
+
+#if defined(HAVE_SYNC_FILE_RANGE)
+
+ /* Linux: tell the memory manager to move these blocks to io so
+ * that they are considered for being actually written to disk.
+ */
+ rc = sync_file_range(context->fd, context->offset, context->nbytes,
+ SYNC_FILE_RANGE_WRITE);
+
+#elif defined(HAVE_POSIX_FADVISE)
+
+ /* Others: say that data should not be kept in memory...
+ * This is not exactly what we want to say, because we want to write
+ * the data for durability but we may need it later nevertheless.
+ * It seems that Linux would free the memory *if* the data has
+ * already been written do disk, else the "dontneed" call is ignored.
+ * For FreeBSD this may have the desired effect of moving the
+ * data to the io layer, although the system does not seem to
+ * take into account the provided offset & size, so it is rather
+ * rough...
+ */
+ rc = posix_fadvise(context->fd, context->offset, context->nbytes,
+ POSIX_FADV_DONTNEED);
+
+#endif
+
+ if (rc < 0)
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not flush block " INT64_FORMAT
+ " on " INT64_FORMAT " blocks in file \"%s\": %m",
+ context->offset / BLCKSZ,
+ context->nbytes / BLCKSZ,
+ context->filename)));
+ }
+}
+
+void
+FileAsynchronousFlush(FileFlushContext * context,
+ int fd, off_t offset, off_t nbytes, char * filename)
+{
+ if (context->ncalls != 0 && context->fd == fd)
+ {
+ /* Same file: merge current flush with previous ones */
+ off_t new_offset = offset < context->offset? offset: context->offset;
+
+ context->nbytes =
+ (context->offset + context->nbytes > offset + nbytes ?
+ context->offset + context->nbytes : offset + nbytes) -
+ new_offset;
+ context->offset = new_offset;
+ context->ncalls ++;
+ }
+ else
+ {
+ /* file has changed; actually flush previous file before restarting
+ * to accumulate flushes
+ */
+ PerformFileFlush(context);
+
+ context->fd = fd;
+ context->ncalls = 1;
+ context->offset = offset;
+ context->nbytes = nbytes;
+ context->filename = filename;
+ }
+}
+
+#endif /* HAVE_SYNC_FILE_RANGE || HAVE_POSIX_FADVISE */
+
int
-FileWrite(File file, char *buffer, int amount)
+FileWrite(File file, char *buffer, int amount, bool flush_to_disk,
+ FileFlushContext * context)
{
int returnCode;
@@ -1395,6 +1484,28 @@ retry:
if (returnCode >= 0)
{
+
+#if defined(HAVE_SYNC_FILE_RANGE) || defined(HAVE_POSIX_FADVISE)
+
+ /*
+ * Calling "write" tells the OS that pg wants to write some page to disk,
+ * however when it is really done is chosen by the OS.
+ * Depending on other disk activities this may be delayed significantly,
+ * maybe up to an "fsync" call, which could induce an IO write surge.
+ * When checkpointing pg is doing its own throttling and the result
+ * should really be written to disk with high priority, so as to meet
+ * the completion target.
+ * This call hints that such write have a higher priority.
+ */
+ if (flush_to_disk && returnCode == amount && errno == 0)
+ {
+ FileAsynchronousFlush(context,
+ VfdCache[file].fd, VfdCache[file].seekPos,
+ amount, VfdCache[file].fileName);
+ }
+
+#endif /* HAVE_SYNC_FILE_RANGE || HAVE_POSIX_FADVISE */
+
VfdCache[file].seekPos += returnCode;
/* maintain fileSize and temporary_files_size if it's a temp file */
diff --git a/src/backend/storage/smgr/md.c b/src/backend/storage/smgr/md.c
index 42a43bb..dbf057f 100644
--- a/src/backend/storage/smgr/md.c
+++ b/src/backend/storage/smgr/md.c
@@ -531,7 +531,7 @@ mdextend(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
errmsg("could not seek to block %u in file \"%s\": %m",
blocknum, FilePathName(v->mdfd_vfd))));
- if ((nbytes = FileWrite(v->mdfd_vfd, buffer, BLCKSZ)) != BLCKSZ)
+ if ((nbytes = FileWrite(v->mdfd_vfd, buffer, BLCKSZ, false, NULL)) != BLCKSZ)
{
if (nbytes < 0)
ereport(ERROR,
@@ -738,7 +738,8 @@ mdread(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
*/
void
mdwrite(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
- char *buffer, bool skipFsync)
+ char *buffer, bool skipFsync, bool flush_to_disk,
+ FileFlushContext * context)
{
off_t seekpos;
int nbytes;
@@ -767,7 +768,7 @@ mdwrite(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
errmsg("could not seek to block %u in file \"%s\": %m",
blocknum, FilePathName(v->mdfd_vfd))));
- nbytes = FileWrite(v->mdfd_vfd, buffer, BLCKSZ);
+ nbytes = FileWrite(v->mdfd_vfd, buffer, BLCKSZ, flush_to_disk, context);
TRACE_POSTGRESQL_SMGR_MD_WRITE_DONE(forknum, blocknum,
reln->smgr_rnode.node.spcNode,
diff --git a/src/backend/storage/smgr/smgr.c b/src/backend/storage/smgr/smgr.c
index 244b4ea..2db3cd3 100644
--- a/src/backend/storage/smgr/smgr.c
+++ b/src/backend/storage/smgr/smgr.c
@@ -52,7 +52,8 @@ typedef struct f_smgr
void (*smgr_read) (SMgrRelation reln, ForkNumber forknum,
BlockNumber blocknum, char *buffer);
void (*smgr_write) (SMgrRelation reln, ForkNumber forknum,
- BlockNumber blocknum, char *buffer, bool skipFsync);
+ BlockNumber blocknum, char *buffer, bool skipFsync,
+ bool flush_to_disk, FileFlushContext *context);
BlockNumber (*smgr_nblocks) (SMgrRelation reln, ForkNumber forknum);
void (*smgr_truncate) (SMgrRelation reln, ForkNumber forknum,
BlockNumber nblocks);
@@ -643,10 +644,11 @@ smgrread(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
*/
void
smgrwrite(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
- char *buffer, bool skipFsync)
+ char *buffer, bool skipFsync, bool flush_to_disk,
+ FileFlushContext * context)
{
(*(smgrsw[reln->smgr_which].smgr_write)) (reln, forknum, blocknum,
- buffer, skipFsync);
+ buffer, skipFsync, flush_to_disk, context);
}
/*
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index 230c5cc..d71cef7 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -158,6 +158,7 @@ static bool check_bonjour(bool *newval, void **extra, GucSource source);
static bool check_ssl(bool *newval, void **extra, GucSource source);
static bool check_stage_log_stats(bool *newval, void **extra, GucSource source);
static bool check_log_stats(bool *newval, void **extra, GucSource source);
+static bool check_flush_to_disk(bool *newval, void **extra, GucSource source);
static bool check_canonical_path(char **newval, void **extra, GucSource source);
static bool check_timezone_abbreviations(char **newval, void **extra, GucSource source);
static void assign_timezone_abbreviations(const char *newval, void *extra);
@@ -1009,6 +1010,17 @@ static struct config_bool ConfigureNamesBool[] =
false,
NULL, NULL, NULL
},
+
+ {
+ {"checkpoint_flush_to_disk", PGC_SIGHUP, WAL_CHECKPOINTS,
+ gettext_noop("Hint that checkpoint's writes are high priority."),
+ NULL
+ },
+ &checkpoint_flush_to_disk,
+ false,
+ check_flush_to_disk, NULL, NULL
+ },
+
{
{"log_connections", PGC_SU_BACKEND, LOGGING_WHAT,
gettext_noop("Logs each successful connection."),
@@ -2205,6 +2217,16 @@ static struct config_int ConfigureNamesInt[] =
},
{
+ {"checkpoint_sort_size", PGC_SIGHUP, WAL_CHECKPOINTS,
+ gettext_noop("Set the number of disk-page buffers sorted together on checkpoints."),
+ NULL
+ },
+ &checkpoint_sort_size,
+ 128*1024, 0, INT_MAX,
+ NULL, NULL, NULL
+ },
+
+ {
{"wal_buffers", PGC_POSTMASTER, WAL_SETTINGS,
gettext_noop("Sets the number of disk-page buffers in shared memory for WAL."),
NULL,
@@ -9760,6 +9782,21 @@ check_log_stats(bool *newval, void **extra, GucSource source)
}
static bool
+check_flush_to_disk(bool *newval, void **extra, GucSource source)
+{
+/* This test must be consistent with the one in FileWrite (storage/file/fd.c)
+ */
+#if ! (defined(HAVE_SYNC_FILE_RANGE) || defined(HAVE_POSIX_FADVISE))
+ /* just warn if it has no effect */
+ ereport(WARNING,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("Setting \"checkpoint_flush_to_disk\" has no effect "
+ "on this platform.")));
+#endif /* HAVE_SYNC_FILE_RANGE */
+ return true;
+}
+
+static bool
check_canonical_path(char **newval, void **extra, GucSource source)
{
/*
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 06dfc06..630100d 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -202,6 +202,8 @@
#max_wal_size = 1GB
#min_wal_size = 80MB
#checkpoint_completion_target = 0.5 # checkpoint target duration, 0.0 - 1.0
+#checkpoint_sort_size = 131072 # sort checkpoint buffers by chunks; 0 disables
+#checkpoint_flush_to_disk = off # send checkpoint buffers to disk
#checkpoint_warning = 30s # 0 disables
# - Archiving -
diff --git a/src/include/postmaster/bgwriter.h b/src/include/postmaster/bgwriter.h
index a49c208..f9c8ca1 100644
--- a/src/include/postmaster/bgwriter.h
+++ b/src/include/postmaster/bgwriter.h
@@ -16,6 +16,7 @@
#define _BGWRITER_H
#include "storage/block.h"
+#include "storage/fd.h"
#include "storage/relfilenode.h"
@@ -29,7 +30,8 @@ extern void BackgroundWriterMain(void) pg_attribute_noreturn();
extern void CheckpointerMain(void) pg_attribute_noreturn();
extern void RequestCheckpoint(int flags);
-extern void CheckpointWriteDelay(int flags, double progress);
+extern void CheckpointWriteDelay(int flags, double progress,
+ FileFlushContext * context, int ctx_size);
extern bool ForwardFsyncRequest(RelFileNode rnode, ForkNumber forknum,
BlockNumber segno);
diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.h
index ec0a254..0534155 100644
--- a/src/include/storage/bufmgr.h
+++ b/src/include/storage/bufmgr.h
@@ -54,6 +54,8 @@ extern int bgwriter_lru_maxpages;
extern double bgwriter_lru_multiplier;
extern bool track_io_timing;
extern int target_prefetch_pages;
+extern bool checkpoint_flush_to_disk;
+extern int checkpoint_sort_size;
/* in buf_init.c */
extern PGDLLIMPORT char *BufferBlocks;
diff --git a/src/include/storage/fd.h b/src/include/storage/fd.h
index 7eabe09..c740ee7 100644
--- a/src/include/storage/fd.h
+++ b/src/include/storage/fd.h
@@ -59,6 +59,22 @@ extern int max_files_per_process;
*/
extern int max_safe_fds;
+/* FileFlushContext:
+ * This structure is used to accumulate several flush requests on a file
+ * into a larger flush request.
+ * - fd: file descriptor of the file
+ * - ncalls: number of flushes merged together
+ * - offset: starting offset (minimum of all offset)
+ * - nbytes: size (minimum extent to cover all flushed data)
+ * - filename: filename of fd for error messages
+ */
+typedef struct FileFlushContext{
+ int fd;
+ int ncalls;
+ off_t offset;
+ off_t nbytes;
+ char * filename;
+} FileFlushContext;
/*
* prototypes for functions in fd.c
@@ -70,7 +86,12 @@ extern File OpenTemporaryFile(bool interXact);
extern void FileClose(File file);
extern int FilePrefetch(File file, off_t offset, int amount);
extern int FileRead(File file, char *buffer, int amount);
-extern int FileWrite(File file, char *buffer, int amount);
+extern void ResetFileFlushContext(FileFlushContext * context);
+extern void PerformFileFlush(FileFlushContext * context);
+extern void FileAsynchronousFlush(FileFlushContext * context,
+ int fd, off_t offset, off_t nbytes, char * filename);
+extern int FileWrite(File file, char *buffer, int amount, bool flush_to_disk,
+ FileFlushContext * context);
extern int FileSync(File file);
extern off_t FileSeek(File file, off_t offset, int whence);
extern int FileTruncate(File file, off_t offset);
diff --git a/src/include/storage/smgr.h b/src/include/storage/smgr.h
index 69a624f..a46a70c 100644
--- a/src/include/storage/smgr.h
+++ b/src/include/storage/smgr.h
@@ -16,6 +16,7 @@
#include "fmgr.h"
#include "storage/block.h"
+#include "storage/fd.h"
#include "storage/relfilenode.h"
@@ -95,7 +96,8 @@ extern void smgrprefetch(SMgrRelation reln, ForkNumber forknum,
extern void smgrread(SMgrRelation reln, ForkNumber forknum,
BlockNumber blocknum, char *buffer);
extern void smgrwrite(SMgrRelation reln, ForkNumber forknum,
- BlockNumber blocknum, char *buffer, bool skipFsync);
+ BlockNumber blocknum, char *buffer, bool skipFsync,
+ bool flush_to_disk, FileFlushContext * context);
extern BlockNumber smgrnblocks(SMgrRelation reln, ForkNumber forknum);
extern void smgrtruncate(SMgrRelation reln, ForkNumber forknum,
BlockNumber nblocks);
@@ -120,8 +122,9 @@ extern void mdprefetch(SMgrRelation reln, ForkNumber forknum,
BlockNumber blocknum);
extern void mdread(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
char *buffer);
-extern void mdwrite(SMgrRelation reln, ForkNumber forknum,
- BlockNumber blocknum, char *buffer, bool skipFsync);
+extern void mdwrite(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
+ char *buffer, bool skipFsync, bool flush_to_disk,
+ FileFlushContext * context);
extern BlockNumber mdnblocks(SMgrRelation reln, ForkNumber forknum);
extern void mdtruncate(SMgrRelation reln, ForkNumber forknum,
BlockNumber nblocks);
Hi,
On 2015-06-17 08:24:38 +0200, Fabien COELHO wrote:
Here is version 3, including many performance tests with various settings,
representing about 100 hours of pgbench run. This patch aims at improving
checkpoint I/O behavior so that tps throughput is improved, late
transactions are less frequent, and overall performances are more stable.
First off: This is pretty impressive stuff. Being at pgcon, I don't have
time to look into this in detail, but I do plan to comment more
extensively.
- Move fsync as early as possible, suggested by Andres Freund?
My opinion is that this should be left out for the nonce.
"for the nonce" - what does that mean?
I did that.
I'm doubtful that it's a good idea to separate this out, if you did.
- as version 2: checkpoint buffer sorting based on a 2007 patch by
Takahiro Itagaki but with a smaller and static buffer allocated once.
Also, sorting is done by chunks of 131072 pages in the current version,
with a guc to change this value.
I think it's a really bad idea to do this in chunks. That'll mean we'll
frequently uselessly cause repetitive random IO, often interleaved. That
pattern is horrible for SSDs too. We should always try to do this at
once, and only fail back to using less memory if we couldn't allocate
everything.
* PERFORMANCE TESTS
Impacts on "pgbench -M prepared -N -P 1 ..." (simple update test, mostly
random write activity on one table), checkpoint_completion_target=0.8, with
different settings on a 16GB 8-core host:. tiny: scale=10 shared_buffers=1GB checkpoint_timeout=30s time=6400s
. small: scale=120 shared_buffers=2GB checkpoint_timeout=300s time=4000s
. medium: scale=250 shared_buffers=4GB checkpoint_timeout=15min time=4000s
. large: scale=1000 shared_buffers=4GB checkpoint_timeout=40min time=7500s
It'd be interesting to see numbers for tiny, without the overly small
checkpoint timeout value. 30s is below the OS's writeback time.
Note: figures noted with a star (*) had various issues during their run, so
pgbench progress figures were more or less incorrect, thus the standard
deviation computation is not to be trusted beyond "pretty bad".Caveat: these are only benches on one host at a particular time and
location, which may or may not be reproducible nor be representative
as such of any other load. The good news is that all these tests tell
the same thing.- full-speed 1-client
options | tps performance over per second data
flush | sort | tiny | small | medium | large
off | off | 687 +- 231 | 163 +- 280 * | 191 +- 626 * | 37.7 +- 25.6
off | on | 699 +- 223 | 457 +- 315 | 479 +- 319 | 48.4 +- 28.8
on | off | 740 +- 125 | 143 +- 387 * | 179 +- 501 * | 37.3 +- 13.3
on | on | 722 +- 119 | 550 +- 140 | 549 +- 180 | 47.2 +- 16.8- full speed 4-clients
options | tps performance over per second data
flush | sort | tiny | small | medium
off | off | 2006 +- 748 | 193 +- 1898 * | 205 +- 2465 *
off | on | 2086 +- 673 | 819 +- 905 * | 807 +- 1029 *
on | off | 2212 +- 451 | 169 +- 1269 * | 160 +- 502 *
on | on | 2073 +- 437 | 743 +- 413 | 822 +- 467- 100-tps 1-client max 100-ms latency
options | percent of late transactions
flush | sort | tiny | small | medium
off | off | 6.31 | 29.44 | 30.74
off | on | 6.23 | 8.93 | 7.12
on | off | 0.44 | 7.01 | 8.14
on | on | 0.59 | 0.83 | 1.84- 200-tps 1-client max 100-ms latency
options | percent of late transactions
flush | sort | tiny | small | medium
off | off | 10.00 | 50.61 | 45.51
off | on | 8.82 | 12.75 | 12.89
on | off | 0.59 | 40.48 | 42.64
on | on | 0.53 | 1.76 | 2.59- 400-tps 1-client (or 4 for medium) max 100-ms latency
options | percent of late transactions
flush | sort | tiny | small | medium
off | off | 12.0 | 64.28 | 68.6
off | on | 11.3 | 22.05 | 22.6
on | off | 1.1 | 67.93 | 67.9
on | on | 0.6 | 3.24 | 3.1
So you've not run things at more serious concurrency, that'd be
interesting to see.
I'd also like to see concurrent workloads with synchronous_commit=off -
I've seen absolutely horrible latency behaviour for that, and I'm hoping
this will help. It's also a good way to simulate faster hardware than
you have.
It's also curious that sorting is detrimental for full speed 'tiny'.
* CONCLUSION :
For most of these HDD tests, when both options are activated the tps
throughput is improved (+3 to +300%), late transactions are reduced (by 91%
to 97%) and overall the performance is more stable (tps standard deviation
is typically halved).The option effects are somehow orthogonal:
- latency is essentially limited by flushing, although sorting also
contributes.- throughput is mostly improved thanks to sorting, with some occasional
small positive or negative effect from flushing.In detail, some loads may benefit more from only one option activated. In
particular, flushing may have a small adverse effect on throughput in some
conditions, although not always.
With SSD probably both options would probably have limited benefit.
I doubt that. Small random writes have bad consequences for wear
leveling. You might not notice that with a short tests - again, I doubt
it - but it'll definitely become visible over time.
Greetings,
Andres Freund
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Hello Andres,
- Move fsync as early as possible, suggested by Andres Freund?
My opinion is that this should be left out for the nonce.
"for the nonce" - what does that mean?
Nonce \Nonce\ (n[o^]ns), n. [For the nonce, OE. for the nones, ...
{for the nonce}, i. e. for the present time.
I'm doubtful that it's a good idea to separate this out, if you did.
Actually I did, because as explained in another mail the fsync time when
the other options are activated as reported in the logs is essentially
null, so it would not bring significant improvements on these runs,
and also the patch changes enough things as it is.
So this is an evidence-based decision.
I also agree that it seems interesting on principle and should be
beneficial in some case, but I would rather keep that on a TODO list
together with trying to do better things in the bgwriter and try to focus
on the current proposal which already changes significantly the
checkpointer throttling logic.
- as version 2: checkpoint buffer sorting based on a 2007 patch by
Takahiro Itagaki but with a smaller and static buffer allocated once.
Also, sorting is done by chunks of 131072 pages in the current version,
with a guc to change this value.I think it's a really bad idea to do this in chunks.
The small problem I see is that for a very large setting there could be
several seconds or even minutes of sorting, which may or may not be
desirable, so having some control on that seems a good idea.
Another argument is that Tom said he wanted that:-)
In practice the value can be set at a high value so that it is nearly
always sorted in one go. Maybe value "0" could be made special and used to
trigger this behavior systematically, and be the default.
That'll mean we'll frequently uselessly cause repetitive random IO,
This is not an issue if the chunks are large enough, and anyway the guc
allows to change the behavior as desired. As I said, keeping some control
seems a good idea, and the "full sorting" can be made the default
behavior.
often interleaved. That pattern is horrible for SSDs too. We should
always try to do this at once, and only fail back to using less memory
if we couldn't allocate everything.
The memory is needed anyway in order to avoid a double or significantly
more heavy implementation for the throttling loop. It is allocated once on
the first checkpoint. The allocation could be moved to the checkpointer
initialization if this is a concern. The memory needed is one int per
buffer, which is smaller than the 2007 patch.
. tiny: scale=10 shared_buffers=1GB checkpoint_timeout=30s time=6400s
It'd be interesting to see numbers for tiny, without the overly small
checkpoint timeout value. 30s is below the OS's writeback time.
The point of tiny was to trigger a lot of checkpoints. The size is pretty
ridiculous anyway, as "tiny" implies. I think I did some tests on other
versions of the patch and longer checkpoint_timeout on pretty small
database that showed smaller benefit from the options, as one would
expect. I'll try to re-run some.
So you've not run things at more serious concurrency, that'd be
interesting to see.
I do not have a box available for "serious concurrency".
I'd also like to see concurrent workloads with synchronous_commit=off -
I've seen absolutely horrible latency behaviour for that, and I'm hoping
this will help. It's also a good way to simulate faster hardware than
you have.
It's also curious that sorting is detrimental for full speed 'tiny'.
Yep.
With SSD probably both options would probably have limited benefit.
I doubt that. Small random writes have bad consequences for wear
leveling. You might not notice that with a short tests - again, I doubt
it - but it'll definitely become visible over time.
Possibly. Testing such effects does not seem easy, though. At least I have
not seen "write stalls" on SSD, which is my primary concern.
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Hi,
On 2015-06-20 08:57:57 +0200, Fabien COELHO wrote:
Actually I did, because as explained in another mail the fsync time when the
other options are activated as reported in the logs is essentially null, so
it would not bring significant improvements on these runs,
and also the patch changes enough things as it is.So this is an evidence-based decision.
Meh. You're testing on low concurrency.
- as version 2: checkpoint buffer sorting based on a 2007 patch by
Takahiro Itagaki but with a smaller and static buffer allocated once.
Also, sorting is done by chunks of 131072 pages in the current version,
with a guc to change this value.I think it's a really bad idea to do this in chunks.
The small problem I see is that for a very large setting there could be
several seconds or even minutes of sorting, which may or may not be
desirable, so having some control on that seems a good idea.
If the sorting of the dirty blocks alone takes minutes, it'll never
finish writing that many buffers out. That's a utterly bogus argument.
Another argument is that Tom said he wanted that:-)
I don't think he said that when we discussed this last.
In practice the value can be set at a high value so that it is nearly always
sorted in one go. Maybe value "0" could be made special and used to trigger
this behavior systematically, and be the default.
You're just making things too complicated.
That'll mean we'll frequently uselessly cause repetitive random IO,
This is not an issue if the chunks are large enough, and anyway the guc
allows to change the behavior as desired.
I don't think this is true. If two consecutive blocks are dirty, but you
sync them in two different chunks, you *always* will cause additional
random IO. Either the drive will have to skip the write for that block,
or the os will prefetch the data. More importantly with SSDs it voids
the wear leveling advantages.
often interleaved. That pattern is horrible for SSDs too. We should always
try to do this at once, and only fail back to using less memory if we
couldn't allocate everything.The memory is needed anyway in order to avoid a double or significantly more
heavy implementation for the throttling loop. It is allocated once on the
first checkpoint. The allocation could be moved to the checkpointer
initialization if this is a concern. The memory needed is one int per
buffer, which is smaller than the 2007 patch.
There's a reason the 2007 patch (and my revision of it last year) did
what it did. You can't just access buffer descriptors without
locking. Besides, causing additional cacheline bouncing during the
sorting process is a bad idea.
Greetings,
Andres Freund
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 6/20/15 2:57 AM, Fabien COELHO wrote:
- as version 2: checkpoint buffer sorting based on a 2007 patch by
Takahiro Itagaki but with a smaller and static buffer allocated once.
Also, sorting is done by chunks of 131072 pages in the current
version,
with a guc to change this value.I think it's a really bad idea to do this in chunks.
The small problem I see is that for a very large setting there could be
several seconds or even minutes of sorting, which may or may not be
desirable, so having some control on that seems a good idea.
ISTM a more elegant way to handle that would be to start off with a very
small number of buffers and sort larger and larger lists while the OS is
busy writing/syncing.
Another argument is that Tom said he wanted that:-)
Did he elaborate why? I don't see him on this thread (though I don't
have all of it).
In practice the value can be set at a high value so that it is nearly
always sorted in one go. Maybe value "0" could be made special and used
to trigger this behavior systematically, and be the default.
It'd be nice if it was just self-tuning, with no GUC.
It looks like it'd be much better to get this committed without more
than we have now than to do without it though...
--
Jim Nasby, Data Architect, Blue Treble Consulting, Austin TX
Data in Trouble? Get it in Treble! http://BlueTreble.com
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Hello Andres,
So this is an evidence-based decision.
Meh. You're testing on low concurrency.
Well, I'm just testing on the available box.
I do not see the link between high concurrency and whether moving fsync as
early as possible would have a large performance impact. I think it might
be interesting if bgwriter is doing a lot of writes, but I'm not sure
under which configuration & load that would be.
I think it's a really bad idea to do this in chunks.
The small problem I see is that for a very large setting there could be
several seconds or even minutes of sorting, which may or may not be
desirable, so having some control on that seems a good idea.If the sorting of the dirty blocks alone takes minutes, it'll never
finish writing that many buffers out. That's a utterly bogus argument.
Well, if in the future you have 8 TB of memory (I've seen a 512GB memory
server a few weeks ago), set shared_buffers=2TB, then if I'm not mistaken
in the worst case you may have 256 millions 8k-buffers to checkpoint. Then
it really depends on the I/O backend stuff used by the box, but if you
bought 8 TB of RAM probably you would have a nice I/O stuff attached.
Another argument is that Tom said he wanted that:-)
I don't think he said that when we discussed this last.
That is what I was recalling when I wrote this sentence:
/messages/by-id/6599.1409421040@sss.pgh.pa.us
But it had more to do with memory-allocation management.
In practice the value can be set at a high value so that it is nearly always
sorted in one go. Maybe value "0" could be made special and used to trigger
this behavior systematically, and be the default.You're just making things too complicated.
ISTM that it is not really complicated, but anyway it is easy to change
the checkpoint_sort stuff to a boolean.
In the reported performance tests, the is usually just one chunk anyway,
sometimes two, so this gives an idea of the overall performance effect.
This is not an issue if the chunks are large enough, and anyway the guc
allows to change the behavior as desired.I don't think this is true. If two consecutive blocks are dirty, but you
sync them in two different chunks, you *always* will cause additional
random IO.
I think that it could be a small number if the chunks are large, i.e. the
performance benefit of sorting larger and larger chunks is decreasing.
Either the drive will have to skip the write for that block,
or the os will prefetch the data. More importantly with SSDs it voids
the wear leveling advantages.
Possibly. I do not understand wear leveling done by SSD firmware.
often interleaved. That pattern is horrible for SSDs too. We should always
try to do this at once, and only fail back to using less memory if we
couldn't allocate everything.The memory is needed anyway in order to avoid a double or significantly more
heavy implementation for the throttling loop. It is allocated once on the
first checkpoint. The allocation could be moved to the checkpointer
initialization if this is a concern. The memory needed is one int per
buffer, which is smaller than the 2007 patch.There's a reason the 2007 patch (and my revision of it last year) did
what it did. You can't just access buffer descriptors without
locking.
I really think that you can because the sorting is really "advisory", i.e.
the checkpointer will work fine if the sorting is wrong or not done at
all, as it is now, when the checkpointer writes buffers. The only
condition is that the buffers must not be moved with their "to write in
this checkpoint" flag, but this is also necessary for the current
checkpointer stuff to work.
Moreover, this trick is alreay pre-existing from the patch I submitted:
some tests are done without locking, but the actual "buffer write" does
the locking and would skip it if the previous test was wrong, as described
in comments in the code.
Besides, causing additional cacheline bouncing during the
sorting process is a bad idea.
Hmmm. The impact would be to multiply the memory required by 3 or 4
(buf_id, relation, forknum, offset), instead of just buf_id, and I
understood that memory was a concern.
Moreover, once the sort process get the lines which contain the sorting
data from the buffer descriptor in its cache, I think that it should be
pretty much okay. Incidentally, they would probably have been brought to
cache by the scan to collect them. Also, I do not think that the sorting
time for 128000 buffers, and possible cache misses, was a big issue, but I
do not have a measure to defend that. I could try to collect some data
about that.
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Hello Jim,
The small problem I see is that for a very large setting there could be
several seconds or even minutes of sorting, which may or may not be
desirable, so having some control on that seems a good idea.ISTM a more elegant way to handle that would be to start off with a very
small number of buffers and sort larger and larger lists while the OS is busy
writing/syncing.
You really have to have done a significant part/most/all of sorting before
starting to write.
Another argument is that Tom said he wanted that:-)
Did he elaborate why? I don't see him on this thread (though I don't have all
of it).
/messages/by-id/6599.1409421040@sss.pgh.pa.us
But it has more to do with memory management.
In practice the value can be set at a high value so that it is nearly
always sorted in one go. Maybe value "0" could be made special and used
to trigger this behavior systematically, and be the default.It'd be nice if it was just self-tuning, with no GUC.
Hmmm. It can easilly be turned into a boolean, but otherwise I have no
clue about how to decide whether to sort and/or flush.
It looks like it'd be much better to get this committed without more than we
have now than to do without it though...
Yep, I think the figures are definitely encouraging.
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
It'd be interesting to see numbers for tiny, without the overly small
checkpoint timeout value. 30s is below the OS's writeback time.
Here are some tests with longer timeout:
tiny2: scale=10 shared_buffers=1GB checkpoint_timeout=5min
max_wal_size=1GB warmup=600 time=4000
flsh | full speed tps | percent of late tx, 4 clients, for tps:
/srt | 1 client | 4 clients | 100 | 200 | 400 | 800 | 1200 | 1600
N/N | 930 +- 124 | 2560 +- 394 | 0.70 | 1.03 | 1.27 | 1.56 | 2.02 | 2.38
N/Y | 924 +- 122 | 2612 +- 326 | 0.63 | 0.79 | 0.94 | 1.15 | 1.45 | 1.67
Y/N | 907 +- 112 | 2590 +- 315 | 0.58 | 0.83 | 0.68 | 0.71 | 0.81 | 1.26
Y/Y | 915 +- 114 | 2590 +- 317 | 0.60 | 0.68 | 0.70 | 0.78 | 0.88 | 1.13
There seems to be a small 1-2% performance benefit with 4 clients, this is
reversed for 1 client, there are significantly and consistently less late
transactions when options are activated, the performance is more stable
(standard deviation reduced by 10-18%).
The db is about 200 MB ~ 25000 pages, at 2500+ tps it is written 40 times
over in 5 minutes, so the checkpoint basically writes everything over 220
seconds, 0.9 MB/s. Given the preload phase the buffers may be more or less
in order in memory, so would be written out in order.
medium2: scale=300 shared_buffers=5GB checkpoint_timeout=30min
max_wal_size=4GB warmup=1200 time=7500
flsh | full speed tps | percent of late tx, 4 clients
/srt | 1 client | 4 clients | 100 | 200 | 400 |
N/N | 173 +- 289* | 198 +- 531* | 27.61 | 43.92 | 61.16 |
N/Y | 458 +- 327* | 743 +- 920* | 7.05 | 14.24 | 24.07 |
Y/N | 169 +- 166* | 187 +- 302* | 4.01 | 39.84 | 65.70 |
Y/Y | 546 +- 143 | 681 +- 459 | 1.55 | 3.51 | 2.84 |
The effect of sorting is very positive (+150% to 270% tps). On this run,
flushing has a positive (+20% with 1 client) or negative (-8 % with 4
clients) on throughput, and late transactions are reduced by 92-95% when
both options are activated.
At 550 tps checkpoints are xlog-triggered and write about 1/3 of the
database, (170000 buffers to write very 220-260 seconds, 4 MB/s).
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
<sorry, resent stalled post, wrong from>
It'd be interesting to see numbers for tiny, without the overly small
checkpoint timeout value. 30s is below the OS's writeback time.
Here are some tests with longer timeout:
tiny2: scale=10 shared_buffers=1GB checkpoint_timeout=5min
max_wal_size=1GB warmup=600 time=4000
flsh | full speed tps | percent of late tx, 4 clients, for tps:
/srt | 1 client | 4 clients | 100 | 200 | 400 | 800 | 1200 | 1600
N/N | 930 +- 124 | 2560 +- 394 | 0.70 | 1.03 | 1.27 | 1.56 | 2.02 | 2.38
N/Y | 924 +- 122 | 2612 +- 326 | 0.63 | 0.79 | 0.94 | 1.15 | 1.45 | 1.67
Y/N | 907 +- 112 | 2590 +- 315 | 0.58 | 0.83 | 0.68 | 0.71 | 0.81 | 1.26
Y/Y | 915 +- 114 | 2590 +- 317 | 0.60 | 0.68 | 0.70 | 0.78 | 0.88 | 1.13
There seems to be a small 1-2% performance benefit with 4 clients, this is
reversed for 1 client, there are significantly and consistently less late
transactions when options are activated, the performance is more stable
(standard deviation reduced by 10-18%).
The db is about 200 MB ~ 25000 pages, at 2500+ tps it is written 40 times
over in 5 minutes, so the checkpoint basically writes everything in 220
seconds, 0.9 MB/s. Given the preload phase the buffers may be more or less
in order in memory, so may be written out in order anyway.
medium2: scale=300 shared_buffers=5GB checkpoint_timeout=30min
max_wal_size=4GB warmup=1200 time=7500
flsh | full speed tps | percent of late tx, 4 clients
/srt | 1 client | 4 clients | 100 | 200 | 400 |
N/N | 173 +- 289* | 198 +- 531* | 27.61 | 43.92 | 61.16 |
N/Y | 458 +- 327* | 743 +- 920* | 7.05 | 14.24 | 24.07 |
Y/N | 169 +- 166* | 187 +- 302* | 4.01 | 39.84 | 65.70 |
Y/Y | 546 +- 143 | 681 +- 459 | 1.55 | 3.51 | 2.84 |
The effect of sorting is very positive (+150% to 270% tps). On this run,
flushing has a positive (+20% with 1 client) or negative (-8 % with 4
clients) on throughput, and late transactions are reduced by 92-95% when
both options are activated.
At 550 tps checkpoints are xlog-triggered and write about 1/3 of the
database, (170000 buffers to write very 220-260 seconds, 4 MB/s).
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Mon, Jun 22, 2015 at 1:41 PM, Fabien COELHO <coelho@cri.ensmp.fr> wrote:
<sorry, resent stalled post, wrong from>
It'd be interesting to see numbers for tiny, without the overly small
checkpoint timeout value. 30s is below the OS's writeback time.Here are some tests with longer timeout:
tiny2: scale=10 shared_buffers=1GB checkpoint_timeout=5min
max_wal_size=1GB warmup=600 time=4000flsh | full speed tps | percent of late tx, 4 clients, for
tps:
/srt | 1 client | 4 clients | 100 | 200 | 400 | 800 | 1200 |
1600
N/N | 930 +- 124 | 2560 +- 394 | 0.70 | 1.03 | 1.27 | 1.56 | 2.02 |
2.38
N/Y | 924 +- 122 | 2612 +- 326 | 0.63 | 0.79 | 0.94 | 1.15 | 1.45 |
1.67
Y/N | 907 +- 112 | 2590 +- 315 | 0.58 | 0.83 | 0.68 | 0.71 | 0.81 |
1.26
Y/Y | 915 +- 114 | 2590 +- 317 | 0.60 | 0.68 | 0.70 | 0.78 | 0.88 |
1.13
There seems to be a small 1-2% performance benefit with 4 clients, this
is reversed for 1 client, there are significantly and consistently less
late transactions when options are activated, the performance is more stable
(standard deviation reduced by 10-18%).
The db is about 200 MB ~ 25000 pages, at 2500+ tps it is written 40 times
over in 5 minutes, so the checkpoint basically writes everything in 220
seconds, 0.9 MB/s. Given the preload phase the buffers may be more or less
in order in memory, so may be written out in order anyway.
medium2: scale=300 shared_buffers=5GB checkpoint_timeout=30min
max_wal_size=4GB warmup=1200 time=7500flsh | full speed tps | percent of late tx, 4 clients
/srt | 1 client | 4 clients | 100 | 200 | 400 |
N/N | 173 +- 289* | 198 +- 531* | 27.61 | 43.92 | 61.16 |
N/Y | 458 +- 327* | 743 +- 920* | 7.05 | 14.24 | 24.07 |
Y/N | 169 +- 166* | 187 +- 302* | 4.01 | 39.84 | 65.70 |
Y/Y | 546 +- 143 | 681 +- 459 | 1.55 | 3.51 | 2.84 |The effect of sorting is very positive (+150% to 270% tps). On this run,
flushing has a positive (+20% with 1 client) or negative (-8 % with 4
clients) on throughput, and late transactions are reduced by 92-95% when
both options are activated.
Why there is dip in performance with multiple clients, can it be
due to reason that we started doing more stuff after holding bufhdr
lock in below code?
BufferSync()
{
..
for (buf_id = 0; buf_id < NBuffers; buf_id++)
{
volatile BufferDesc *bufHdr = GetBufferDescriptor(buf_id);
@@ -1621,32 +1719,185 @@ BufferSync(int flags)
if ((bufHdr->flags & mask) == mask)
{
+ Oid spc;
+ TableSpaceCountEntry * entry;
+ bool found;
+
bufHdr->flags |= BM_CHECKPOINT_NEEDED;
+ CheckpointBufferIds[num_to_write] = buf_id;
num_to_write++;
+
+ /* keep track of per tablespace buffers */
+ spc = bufHdr->tag.rnode.spcNode;
+ entry = (TableSpaceCountEntry *)
+ hash_search(spcBuffers, (void *) &spc, HASH_ENTER, &found);
+
+ if (found) entry->count++;
+ else entry->count = 1;
}
..
}
-
BufferSync()
{
..
- buf_id = StrategySyncStart(NULL, NULL);
- num_to_scan = NBuffers;
+ active_spaces = nb_spaces;
+ space = 0;
num_written = 0;
- while (num_to_scan-- > 0)
+
+ while (active_spaces != 0)
..
}
The changed code doesn't seems to give any consideration to
clock-sweep point which might not be helpful for cases when checkpoint
could have flushed soon-to-be-recycled buffers. I think flushing the
sorted buffers w.r.t tablespaces is a good idea, but not giving any
preference to clock-sweep point seems to me that we would loose in
some cases by this new change.
With Regards,
Amit Kapila.
EnterpriseDB: http://www.enterprisedb.com
Hello Amit,
medium2: scale=300 shared_buffers=5GB checkpoint_timeout=30min
max_wal_size=4GB warmup=1200 time=7500flsh | full speed tps | percent of late tx, 4 clients
/srt | 1 client | 4 clients | 100 | 200 | 400 |
N/N | 173 +- 289* | 198 +- 531* | 27.61 | 43.92 | 61.16 |
N/Y | 458 +- 327* | 743 +- 920* | 7.05 | 14.24 | 24.07 |
Y/N | 169 +- 166* | 187 +- 302* | 4.01 | 39.84 | 65.70 |
Y/Y | 546 +- 143 | 681 +- 459 | 1.55 | 3.51 | 2.84 |The effect of sorting is very positive (+150% to 270% tps). On this run,
flushing has a positive (+20% with 1 client) or negative (-8 % with 4
clients) on throughput, and late transactions are reduced by 92-95% when
both options are activated.Why there is dip in performance with multiple clients,
I'm not sure to see the "dip". The performances are better with 4 clients
compared to 1 client?
can it be due to reason that we started doing more stuff after holding
bufhdr lock in below code?
I think it is very unlikely that the buffer being locked would be
simultaneously requested by one of the 4 clients for an UPDATE, so I do
not think it should have a significant impact.
BufferSync() [...]
BufferSync() { .. - buf_id = StrategySyncStart(NULL, NULL); - num_to_scan = NBuffers; + active_spaces = nb_spaces; + space = 0; num_written = 0; - while (num_to_scan-- > 0) + + while (active_spaces != 0) .. }The changed code doesn't seems to give any consideration to
clock-sweep point
Indeed.
which might not be helpful for cases when checkpoint could have flushed
soon-to-be-recycled buffers. I think flushing the sorted buffers w.r.t
tablespaces is a good idea, but not giving any preference to clock-sweep
point seems to me that we would loose in some cases by this new change.
I do not see how to do both, as these two orders seem more or less
unrelated? The traditionnal assumption is that the I/O are very slow and
they are to be optimized first, so going for buffer ordering to be nice to
the disk looks like the priority.
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
I'd also like to see concurrent workloads with synchronous_commit=off -
I've seen absolutely horrible latency behaviour for that, and I'm hoping
this will help. It's also a good way to simulate faster hardware than
you have.
It helps. I've done a few runs, where the very-very-bad situation is
improved to... I would say very-bad:
medium3: scale=200 shared_buffers=4GB checkpoint_timeout=15min
max_wal_size=4GB warmup=1200 time=6000 clients=4
synchronous_commit=off
flush sort | tps | percent of seconds offline
off off | 296 | 83% offline
off on | 1496 | 33% offline
off on | 1641 | 59% offline
on on | 1515 | 31% offline
The offline figure is the percentage of seconds in the 6000 seconds run
where 0.0 tps are reported, or where nothing is reported because pgbench
is stuck.
It is somehow better... on an abysmal scale: sorting and flushing reduced
the offline time by a factor of 2.6. Too bad it is so high to begin with.
The tps is improved by a factor of 5 with either options.
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 6/22/15 11:59 PM, Fabien COELHO wrote:
which might not be helpful for cases when checkpoint could have
flushed soon-to-be-recycled buffers. I think flushing the sorted
buffers w.r.t tablespaces is a good idea, but not giving any
preference to clock-sweep point seems to me that we would loose in
some cases by this new change.I do not see how to do both, as these two orders seem more or less
unrelated? The traditionnal assumption is that the I/O are very slow
and they are to be optimized first, so going for buffer ordering to be
nice to the disk looks like the priority.
The point is that it's already expensive for backends to advance the
clock; if they then have to wait on IO as well it gets REALLY expensive.
So we want to avoid that.
Other than that though, it is pretty orthogonal, so perhaps another
indication that the clock should be handled separately from both
backends and bgwriter...
--
Jim Nasby, Data Architect, Blue Treble Consulting, Austin TX
Data in Trouble? Get it in Treble! http://BlueTreble.com
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Tue, Jun 23, 2015 at 10:29 AM, Fabien COELHO <coelho@cri.ensmp.fr> wrote:
Hello Amit,
medium2: scale=300 shared_buffers=5GB checkpoint_timeout=30min
max_wal_size=4GB warmup=1200 time=7500
flsh | full speed tps | percent of late tx, 4 clients
/srt | 1 client | 4 clients | 100 | 200 | 400 |
N/N | 173 +- 289* | 198 +- 531* | 27.61 | 43.92 | 61.16 |
N/Y | 458 +- 327* | 743 +- 920* | 7.05 | 14.24 | 24.07 |
Y/N | 169 +- 166* | 187 +- 302* | 4.01 | 39.84 | 65.70 |
Y/Y | 546 +- 143 | 681 +- 459 | 1.55 | 3.51 | 2.84 |The effect of sorting is very positive (+150% to 270% tps). On this run,
flushing has a positive (+20% with 1 client) or negative (-8 % with 4
clients) on throughput, and late transactions are reduced by 92-95% when
both options are activated.Why there is dip in performance with multiple clients,
I'm not sure to see the "dip". The performances are better with 4 clients
compared to 1 client?
What do you mean by "negative (-8 % with 4 clients) on throughput"
in above sentence? I thought by that you mean that there is dip
in TPS with patch as compare to HEAD at 4 clients.
Also I am not completely sure what's +- means in your data above?
can it be due to reason that we started doing more stuff after holding
bufhdr lock in below code?
I think it is very unlikely that the buffer being locked would be
simultaneously requested by one of the 4 clients for an UPDATE, so I do not
think it should have a significant impact.BufferSync() [...]
BufferSync()
{ .. - buf_id = StrategySyncStart(NULL, NULL); - num_to_scan = NBuffers; + active_spaces = nb_spaces; + space = 0; num_written = 0; - while (num_to_scan-- > 0) + + while (active_spaces != 0) .. }The changed code doesn't seems to give any consideration to
clock-sweep pointIndeed.
which might not be helpful for cases when checkpoint could have flushed
soon-to-be-recycled buffers. I think flushing the sorted buffers w.r.t
tablespaces is a good idea, but not giving any preference to clock-sweep
point seems to me that we would loose in some cases by this new change.I do not see how to do both, as these two orders seem more or less
unrelated?
I understand your point and I also don't have any specific answer
for it at this moment, the point of worry is that it should not lead
to degradation of certain cases as compare to current algorithm.
The workload where it could effect is when your data doesn't fit
in shared buffers, but can fit in RAM.
With Regards,
Amit Kapila.
EnterpriseDB: http://www.enterprisedb.com
flsh | full speed tps | percent of late tx, 4 clients
/srt | 1 client | 4 clients | 100 | 200 | 400 |
N/N | 173 +- 289* | 198 +- 531* | 27.61 | 43.92 | 61.16 |
N/Y | 458 +- 327* | 743 +- 920* | 7.05 | 14.24 | 24.07 |
Y/N | 169 +- 166* | 187 +- 302* | 4.01 | 39.84 | 65.70 |
Y/Y | 546 +- 143 | 681 +- 459 | 1.55 | 3.51 | 2.84 |The effect of sorting is very positive (+150% to 270% tps). On this run,
flushing has a positive (+20% with 1 client) or negative (-8 % with 4
clients) on throughput, and late transactions are reduced by 92-95% when
both options are activated.Why there is dip in performance with multiple clients,
I'm not sure to see the "dip". The performances are better with 4 clients
compared to 1 client?What do you mean by "negative (-8 % with 4 clients) on throughput" in
above sentence? I thought by that you mean that there is dip in TPS with
patch as compare to HEAD at 4 clients.
Ok, I misunderstood your question. I thought you meant a dip between 1
client and 4 clients. I meant that when flush is turned on tps goes down
by 8% (743 to 681 tps) on this particular run. Basically tps improvements
mostly come from "sort", and "flush" has uncertain effects on tps
(throuput), but much more on latency and performance stability (lower late
rate, lower standard deviation).
Note that I'm not comparing to HEAD in the above tests, but with the new
options desactivated, which should be more or less comparable to current
HEAD, i.e. there is no sorting nor flushing done, but this is not strictly
speaking HEAD behavior. Probably I should get some figures with HEAD as
well to check the "more or less" assumption.
Also I am not completely sure what's +- means in your data above?
The first figure before "+-" is the tps, the second after is its standard
deviation computed in per-second traces. Some runs are very bad, with
pgbench stuck at times, and result on stddev larger than the average, they
ere noted with "*".
I understand your point and I also don't have any specific answer
for it at this moment, the point of worry is that it should not lead
to degradation of certain cases as compare to current algorithm.
The workload where it could effect is when your data doesn't fit
in shared buffers, but can fit in RAM.
Hmmm. My point of view is still that the logical priority is to optimize
for disk IO first, then look for compatible RAM optimisations later.
I can run tests with a small shared_buffers, but probably it would just
trigger a lot of checkpoints, or worse rely on the bgwriter to find space,
which would generate random IOs.
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
I do not see how to do both, as these two orders seem more or less
unrelated? The traditionnal assumption is that the I/O are very slow
and they are to be optimized first, so going for buffer ordering to be
nice to the disk looks like the priority.The point is that it's already expensive for backends to advance the clock;
if they then have to wait on IO as well it gets REALLY expensive. So we want
to avoid that.
I do not know what this clock stuff does. Note that the checkpoint buffer
scan is done once at the beginning of the checkpoint and its time is
relatively small compared to everything else in the checkpoint.
If this scan is an issue, it can be done in reverse order, or in some
other order, but I think it is better to do it in order for better cache
behavior, although the effect should be marginal.
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Besides, causing additional cacheline bouncing during the
sorting process is a bad idea.Hmmm. The impact would be to multiply the memory required by 3 or 4 (buf_id,
relation, forknum, offset), instead of just buf_id, and I understood that
memory was a concern.Moreover, once the sort process get the lines which contain the sorting data
from the buffer descriptor in its cache, I think that it should be pretty
much okay. Incidentally, they would probably have been brought to cache by
the scan to collect them. Also, I do not think that the sorting time for
128000 buffers, and possible cache misses, was a big issue, but I do not have
a measure to defend that. I could try to collect some data about that.
I've collected some data by adding a "sort time" measure, with
checkpoint_sort_size=10000000 so that sorting is in one chunk, and done
some large checkpoints:
LOG: checkpoint complete: wrote 41091 buffers (6.3%);
0 transaction log file(s) added, 0 removed, 0 recycled;
sort=0.024 s, write=0.488 s, sync=8.790 s, total=9.837 s;
sync files=41, longest=8.717 s, average=0.214 s;
distance=404972 kB, estimate=404972 kB
LOG: checkpoint complete: wrote 212124 buffers (32.4%);
0 transaction log file(s) added, 0 removed, 0 recycled;
sort=0.078 s, write=128.885 s, sync=1.269 s, total=131.646 s;
sync files=43, longest=1.155 s, average=0.029 s;
distance=2102950 kB, estimate=2102950 kB
LOG: checkpoint complete: wrote 384427 buffers (36.7%);
0 transaction log file(s) added, 0 removed, 1 recycled;
sort=0.120 s, write=83.995 s, sync=13.944 s, total=98.035 s;
sync files=9, longest=13.724 s, average=1.549 s;
distance=3783305 kB, estimate=3783305 kB
LOG: checkpoint complete: wrote 809211 buffers (77.2%);
0 transaction log file(s) added, 0 removed, 1 recycled;
sort=0.358 s, write=138.146 s, sync=14.943 s, total=153.124 s;
sync files=13, longest=14.871 s, average=1.149 s;
distance=8075338 kB, estimate=8075338 kB
Summary of these checkpoints:
#buffers size sort
41091 328MB 0.024
212124 1.7GB 0.078
384427 2.9GB 0.120
809211 6.2GB 0.358
Sort times are pretty negligeable compared to the whole checkpoint time,
and under 0.1 s/GB of buffers sorted.
On a 512 GB server with shared_buffers=128GB (25%), this suggest a worst
case checkpoint sorting in a few seconds, and then you have a hundred GB
to write anyway. If we project on next decade 1 TB checkpoint that would
make sorting in under a minute... But then you have 1 TB of data to dump.
As a comparison point, I've done the large checkpoint with the default
sort size of 131072:
LOG: checkpoint complete: wrote 809211 buffers (77.2%);
0 transaction log file(s) added, 0 removed, 1 recycled;
sort=0.251 s, write=152.377 s, sync=15.062 s, total=167.453 s;
sync files=13, longest=14.974 s, average=1.158 s;
distance=8075338 kB, estimate=8075338 kB
The 0.251 sort time is to be compared to 0.358. Well, n.log(n) is not too
bad, as expected.
These figures suggest that sorting time and associated cache misses are
not a significant issue and thus are not worth bothering much about, and
also that probably a simple boolean option would be quite acceptable
instead of the chunk approach.
Attached is an updated version of the patch which turns the sort option
into a boolean, and also include the sort time in the checkpoint log.
There is still an open question about whether the sorting buffer
allocation is lost on some signals and should be reallocated in such
event.
--
Fabien.
Attachments:
checkpoint-continuous-flush-4.patchtext/x-diff; name=checkpoint-continuous-flush-4.patchDownload
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 1da7dfb..d7c1ff8 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -2474,6 +2474,28 @@ include_dir 'conf.d'
</listitem>
</varlistentry>
+ <varlistentry id="guc-checkpoint-sort" xreflabel="checkpoint_sort">
+ <term><varname>checkpoint_sort</varname> (<type>bool</type>)
+ <indexterm>
+ <primary><varname>checkpoint_sort</> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ Whether to sort buffers before writting them out to disk on checkpoint.
+ For a HDD storage, this setting allows to group together
+ neighboring pages written to disk, thus improving performance by
+ reducing random write activity.
+ This sorting should have limited performance effects on SSD backends
+ as such storages have good random write performance, but it may
+ help with wear-leveling so be worth keeping anyway.
+ The default is <literal>on</>.
+ This parameter can only be set in the <filename>postgresql.conf</>
+ file or on the server command line.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="guc-checkpoint-warning" xreflabel="checkpoint_warning">
<term><varname>checkpoint_warning</varname> (<type>integer</type>)
<indexterm>
@@ -2495,6 +2517,24 @@ include_dir 'conf.d'
</listitem>
</varlistentry>
+ <varlistentry id="guc-checkpoint-flush-to-disk" xreflabel="checkpoint_flush_to_disk">
+ <term><varname>checkpoint_flush_to_disk</varname> (<type>bool</type>)
+ <indexterm>
+ <primary><varname>checkpoint_flush_to_disk</> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ When writing data for a checkpoint, hint the underlying OS that the
+ data must be sent to disk as soon as possible. This may help smoothing
+ disk I/O writes and avoid a stall when fsync is issued at the end of
+ the checkpoint, but it may also reduce average performance.
+ This setting may have no effect on some platforms.
+ The default is <literal>off</>.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="guc-min-wal-size" xreflabel="min_wal_size">
<term><varname>min_wal_size</varname> (<type>integer</type>)
<indexterm>
diff --git a/doc/src/sgml/wal.sgml b/doc/src/sgml/wal.sgml
index f4083c3..172a779 100644
--- a/doc/src/sgml/wal.sgml
+++ b/doc/src/sgml/wal.sgml
@@ -546,6 +546,29 @@
</para>
<para>
+ When hard-disk drives (HDD) are used for terminal data storage
+ <xref linkend="guc-checkpoint-sort"> allows to sort pages
+ so that neighboring pages on disk will be flushed together by
+ chekpoints, reducing the random write load and improving performance.
+ If solid-state drives (SSD) are used, sorting pages induces no benefit
+ as their random write I/O performance is good: this feature could then
+ be disabled by setting <varname>checkpoint_sort</> to <value>off</>.
+ It is possible that sorting may help with SSD wear leveling, so it may
+ be kept on that account.
+ </para>
+
+ <para>
+ On Linux and POSIX platforms, <xref linkend="guc-checkpoint-flush-to-disk">
+ allows to hint the OS that pages written on checkpoints must be flushed
+ to disk quickly. Otherwise, these pages may be kept in cache for some time,
+ inducing a stall later when <literal>fsync</> is called to actually
+ complete the checkpoint. This setting helps to reduce transaction latency,
+ but it may also have a small adverse effect on the average transaction rate
+ at maximum throughput. This feature probably brings no benefit on SSD,
+ as the I/O write latency is small on such hardware, thus it may be disabled.
+ </para>
+
+ <para>
The number of WAL segment files in <filename>pg_xlog</> directory depends on
<varname>min_wal_size</>, <varname>max_wal_size</> and
the amount of WAL generated in previous checkpoint cycles. When old log
diff --git a/src/backend/access/heap/rewriteheap.c b/src/backend/access/heap/rewriteheap.c
index bcce3e3..f565dc4 100644
--- a/src/backend/access/heap/rewriteheap.c
+++ b/src/backend/access/heap/rewriteheap.c
@@ -918,7 +918,7 @@ logical_heap_rewrite_flush_mappings(RewriteState state)
* Note that we deviate from the usual WAL coding practices here,
* check the above "Logical rewrite support" comment for reasoning.
*/
- written = FileWrite(src->vfd, waldata_start, len);
+ written = FileWrite(src->vfd, waldata_start, len, false, NULL);
if (written != len)
ereport(ERROR,
(errcode_for_file_access(),
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index 9431ab5..49ec258 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -203,7 +203,7 @@ btbuildempty(PG_FUNCTION_ARGS)
/* Write the page. If archiving/streaming, XLOG it. */
PageSetChecksumInplace(metapage, BTREE_METAPAGE);
smgrwrite(index->rd_smgr, INIT_FORKNUM, BTREE_METAPAGE,
- (char *) metapage, true);
+ (char *) metapage, true, false, NULL);
if (XLogIsNeeded())
log_newpage(&index->rd_smgr->smgr_rnode.node, INIT_FORKNUM,
BTREE_METAPAGE, metapage, false);
diff --git a/src/backend/access/nbtree/nbtsort.c b/src/backend/access/nbtree/nbtsort.c
index f95f67a..ea7a45d 100644
--- a/src/backend/access/nbtree/nbtsort.c
+++ b/src/backend/access/nbtree/nbtsort.c
@@ -315,7 +315,7 @@ _bt_blwritepage(BTWriteState *wstate, Page page, BlockNumber blkno)
{
/* overwriting a block we zero-filled before */
smgrwrite(wstate->index->rd_smgr, MAIN_FORKNUM, blkno,
- (char *) page, true);
+ (char *) page, true, false, NULL);
}
pfree(page);
diff --git a/src/backend/access/spgist/spginsert.c b/src/backend/access/spgist/spginsert.c
index bceee8d..b700efb 100644
--- a/src/backend/access/spgist/spginsert.c
+++ b/src/backend/access/spgist/spginsert.c
@@ -170,7 +170,7 @@ spgbuildempty(PG_FUNCTION_ARGS)
/* Write the page. If archiving/streaming, XLOG it. */
PageSetChecksumInplace(page, SPGIST_METAPAGE_BLKNO);
smgrwrite(index->rd_smgr, INIT_FORKNUM, SPGIST_METAPAGE_BLKNO,
- (char *) page, true);
+ (char *) page, true, false, NULL);
if (XLogIsNeeded())
log_newpage(&index->rd_smgr->smgr_rnode.node, INIT_FORKNUM,
SPGIST_METAPAGE_BLKNO, page, false);
@@ -180,7 +180,7 @@ spgbuildempty(PG_FUNCTION_ARGS)
PageSetChecksumInplace(page, SPGIST_ROOT_BLKNO);
smgrwrite(index->rd_smgr, INIT_FORKNUM, SPGIST_ROOT_BLKNO,
- (char *) page, true);
+ (char *) page, true, false, NULL);
if (XLogIsNeeded())
log_newpage(&index->rd_smgr->smgr_rnode.node, INIT_FORKNUM,
SPGIST_ROOT_BLKNO, page, true);
@@ -190,7 +190,7 @@ spgbuildempty(PG_FUNCTION_ARGS)
PageSetChecksumInplace(page, SPGIST_NULL_BLKNO);
smgrwrite(index->rd_smgr, INIT_FORKNUM, SPGIST_NULL_BLKNO,
- (char *) page, true);
+ (char *) page, true, false, NULL);
if (XLogIsNeeded())
log_newpage(&index->rd_smgr->smgr_rnode.node, INIT_FORKNUM,
SPGIST_NULL_BLKNO, page, true);
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 4e37ad3..0ff48b3 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -7956,11 +7956,13 @@ LogCheckpointEnd(bool restartpoint)
sync_secs,
total_secs,
longest_secs,
+ sort_secs,
average_secs;
int write_usecs,
sync_usecs,
total_usecs,
longest_usecs,
+ sort_usecs,
average_usecs;
uint64 average_sync_time;
@@ -7991,6 +7993,10 @@ LogCheckpointEnd(bool restartpoint)
CheckpointStats.ckpt_end_t,
&total_secs, &total_usecs);
+ TimestampDifference(CheckpointStats.ckpt_sort_t,
+ CheckpointStats.ckpt_sort_end_t,
+ &sort_secs, &sort_usecs);
+
/*
* Timing values returned from CheckpointStats are in microseconds.
* Convert to the second plus microsecond form that TimestampDifference
@@ -8009,8 +8015,8 @@ LogCheckpointEnd(bool restartpoint)
elog(LOG, "%s complete: wrote %d buffers (%.1f%%); "
"%d transaction log file(s) added, %d removed, %d recycled; "
- "write=%ld.%03d s, sync=%ld.%03d s, total=%ld.%03d s; "
- "sync files=%d, longest=%ld.%03d s, average=%ld.%03d s; "
+ "sort=%ld.%03d s, write=%ld.%03d s, sync=%ld.%03d s, total=%ld.%03d s;"
+ " sync files=%d, longest=%ld.%03d s, average=%ld.%03d s; "
"distance=%d kB, estimate=%d kB",
restartpoint ? "restartpoint" : "checkpoint",
CheckpointStats.ckpt_bufs_written,
@@ -8018,6 +8024,7 @@ LogCheckpointEnd(bool restartpoint)
CheckpointStats.ckpt_segs_added,
CheckpointStats.ckpt_segs_removed,
CheckpointStats.ckpt_segs_recycled,
+ sort_secs, sort_usecs / 1000,
write_secs, write_usecs / 1000,
sync_secs, sync_usecs / 1000,
total_secs, total_usecs / 1000,
diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c
index 0dce6a8..52dd7db 100644
--- a/src/backend/postmaster/checkpointer.c
+++ b/src/backend/postmaster/checkpointer.c
@@ -663,7 +663,8 @@ ImmediateCheckpointRequested(void)
* fraction between 0.0 meaning none, and 1.0 meaning all done.
*/
void
-CheckpointWriteDelay(int flags, double progress)
+CheckpointWriteDelay(int flags, double progress,
+ FileFlushContext * context, int ctx_size)
{
static int absorb_counter = WRITES_PER_ABSORB;
@@ -698,6 +699,26 @@ CheckpointWriteDelay(int flags, double progress)
*/
pgstat_send_bgwriter();
+#if defined(HAVE_SYNC_FILE_RANGE) || defined(HAVE_POSIX_FADVISE)
+
+ /*
+ * Before sleeping, flush written blocks for each tablespace.
+ */
+ if (checkpoint_flush_to_disk)
+ {
+ int i;
+
+ for (i = 0; i < ctx_size; i++)
+ {
+ if (context[i].ncalls != 0)
+ {
+ PerformFileFlush(&context[i]);
+ ResetFileFlushContext(&context[i]);
+ }
+ }
+ }
+#endif /* HAVE_SYNC_FILE_RANGE || HAVE_POSIX_FADVISE */
+
/*
* This sleep used to be connected to bgwriter_delay, typically 200ms.
* That resulted in more frequent wakeups if not much work to do.
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index cc973b5..2bfb067 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -80,6 +80,10 @@ bool zero_damaged_pages = false;
int bgwriter_lru_maxpages = 100;
double bgwriter_lru_multiplier = 2.0;
bool track_io_timing = false;
+/* hint to move writes to high priority */
+bool checkpoint_flush_to_disk = false;
+/* by default, sort by chunks of 1 GB worth of 8 kB buffers */
+bool checkpoint_sort = true;
/*
* How many buffers PrefetchBuffer callers should try to stay ahead of their
@@ -396,7 +400,8 @@ static bool PinBuffer(volatile BufferDesc *buf, BufferAccessStrategy strategy);
static void PinBuffer_Locked(volatile BufferDesc *buf);
static void UnpinBuffer(volatile BufferDesc *buf, bool fixOwner);
static void BufferSync(int flags);
-static int SyncOneBuffer(int buf_id, bool skip_recently_used);
+static int SyncOneBuffer(int buf_id, bool skip_recently_used,
+ bool flush_to_disk, FileFlushContext *context);
static void WaitIO(volatile BufferDesc *buf);
static bool StartBufferIO(volatile BufferDesc *buf, bool forInput);
static void TerminateBufferIO(volatile BufferDesc *buf, bool clear_dirty,
@@ -409,7 +414,8 @@ static volatile BufferDesc *BufferAlloc(SMgrRelation smgr,
BlockNumber blockNum,
BufferAccessStrategy strategy,
bool *foundPtr);
-static void FlushBuffer(volatile BufferDesc *buf, SMgrRelation reln);
+static void FlushBuffer(volatile BufferDesc *buf, SMgrRelation reln,
+ bool flush_to_disk, FileFlushContext *context);
static void AtProcExit_Buffers(int code, Datum arg);
static void CheckForBufferLeaks(void);
static int rnode_comparator(const void *p1, const void *p2);
@@ -1018,7 +1024,7 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
smgr->smgr_rnode.node.dbNode,
smgr->smgr_rnode.node.relNode);
- FlushBuffer(buf, NULL);
+ FlushBuffer(buf, NULL, false, NULL);
LWLockRelease(buf->content_lock);
TRACE_POSTGRESQL_BUFFER_WRITE_DIRTY_DONE(forkNum, blockNum,
@@ -1561,6 +1567,75 @@ UnpinBuffer(volatile BufferDesc *buf, bool fixOwner)
}
}
+/* Array of buffer ids of all buffers to checkpoint.
+ */
+static int * CheckpointBufferIds = NULL;
+
+/* Compare checkpoint buffers
+ */
+static int bufcmp(const int * pa, const int * pb)
+{
+ BufferDesc
+ *a = GetBufferDescriptor(*pa),
+ *b = GetBufferDescriptor(*pb);
+
+ /* tag: rnode, forkNum (different files), blockNum
+ * rnode: { spcNode (ignore: not really needed),
+ * dbNode (ignore: this is a directory), relNode }
+ * spcNode: table space oid, not that there are at least two
+ * (pg_global and pg_default).
+ */
+ /* compare relation */
+ if (a->tag.rnode.relNode < b->tag.rnode.relNode)
+ return -1;
+ else if (a->tag.rnode.relNode > b->tag.rnode.relNode)
+ return 1;
+ /* same relation, compare fork */
+ else if (a->tag.forkNum < b->tag.forkNum)
+ return -1;
+ else if (a->tag.forkNum > b->tag.forkNum)
+ return 1;
+ /* same relation/fork, so same segmented "file", compare block number
+ * which are mapped on different segments depending on the number.
+ */
+ else if (a->tag.blockNum < b->tag.blockNum)
+ return -1;
+ else /* should not be the same block anyway... */
+ return 1;
+}
+
+static void AllocateCheckpointBufferIds(void)
+{
+ /* Safe worst case allocation, all buffers belong to the checkpoint...
+ * that is pretty unlikely.
+ */
+ CheckpointBufferIds = (int *) palloc(sizeof(int) * NBuffers);
+}
+
+/* Status of buffers to checkpoint for a particular tablespace,
+ * used internally in BufferSync.
+ * - spcNone: oid of the tablespace
+ * - num_to_write: number of pages counted for this tablespace
+ * - num_written: number of pages actually written out
+ * - index: scanning position in CheckpointBufferIds for this tablespace
+ * - done: whether it is done
+ */
+typedef struct TableSpaceCheckpointStatus {
+ Oid space;
+ int num_to_write;
+ int num_written;
+ int index;
+ bool done;
+} TableSpaceCheckpointStatus;
+
+/* entry structure for table space to count hashtable,
+ * used internally in BufferSync.
+ */
+typedef struct TableSpaceCountEntry {
+ Oid space;
+ int count;
+} TableSpaceCountEntry;
+
/*
* BufferSync -- Write out all dirty buffers in the pool.
*
@@ -1575,10 +1650,21 @@ static void
BufferSync(int flags)
{
int buf_id;
- int num_to_scan;
int num_to_write;
int num_written;
+ int i;
int mask = BM_DIRTY;
+ HTAB *spcBuffers;
+ TableSpaceCheckpointStatus *spcStatus = NULL;
+ int nb_spaces, active_spaces, space;
+ FileFlushContext * spcContext = NULL;
+
+ /*
+ * Lazy allocation: this function is called through the checkpointer,
+ * but also by initdb. Maybe the allocation could be moved to the callers.
+ */
+ if (CheckpointBufferIds == NULL)
+ AllocateCheckpointBufferIds();
/* Make sure we can handle the pin inside SyncOneBuffer */
ResourceOwnerEnlargeBuffers(CurrentResourceOwner);
@@ -1609,6 +1695,18 @@ BufferSync(int flags)
* certainly need to be written for the next checkpoint attempt, too.
*/
num_to_write = 0;
+
+ /* initialize oid -> int buffer count hash table */
+ {
+ HASHCTL ctl;
+
+ MemSet(&ctl, 0, sizeof(HASHCTL));
+ ctl.keysize = sizeof(Oid);
+ ctl.entrysize = sizeof(TableSpaceCountEntry);
+ spcBuffers = hash_create("Number of buffers to write per tablespace",
+ 16, &ctl, HASH_ELEM | HASH_BLOBS);
+ }
+
for (buf_id = 0; buf_id < NBuffers; buf_id++)
{
volatile BufferDesc *bufHdr = GetBufferDescriptor(buf_id);
@@ -1621,32 +1719,169 @@ BufferSync(int flags)
if ((bufHdr->flags & mask) == mask)
{
+ Oid spc;
+ TableSpaceCountEntry * entry;
+ bool found;
+
bufHdr->flags |= BM_CHECKPOINT_NEEDED;
+ CheckpointBufferIds[num_to_write] = buf_id;
num_to_write++;
+
+ /* keep track of per tablespace buffers */
+ spc = bufHdr->tag.rnode.spcNode;
+ entry = (TableSpaceCountEntry *)
+ hash_search(spcBuffers, (void *) &spc, HASH_ENTER, &found);
+
+ if (found) entry->count++;
+ else entry->count = 1;
}
UnlockBufHdr(bufHdr);
}
if (num_to_write == 0)
+ {
+ hash_destroy(spcBuffers);
return; /* nothing to do */
+ }
TRACE_POSTGRESQL_BUFFER_SYNC_START(NBuffers, num_to_write);
+ /* Build checkpoint tablespace buffer status & flush context arrays */
+ nb_spaces = hash_get_num_entries(spcBuffers);
+ spcStatus = (TableSpaceCheckpointStatus *)
+ palloc(sizeof(TableSpaceCheckpointStatus) * nb_spaces);
+ spcContext = (FileFlushContext *)
+ palloc(sizeof(FileFlushContext) * nb_spaces);
+
+ {
+ int index = 0;
+ HASH_SEQ_STATUS hseq;
+ TableSpaceCountEntry * entry;
+
+ hash_seq_init(&hseq, spcBuffers);
+ while ((entry = (TableSpaceCountEntry *) hash_seq_search(&hseq)))
+ {
+ Assert(index < nb_spaces);
+ spcStatus[index].space = entry->space;
+ spcStatus[index].num_to_write = entry->count;
+ spcStatus[index].num_written = 0;
+ /* should it be randomized? chosen with some criterion? */
+ spcStatus[index].index = 0;
+ spcStatus[index].done = false;
+
+#if defined(HAVE_SYNC_FILE_RANGE) || defined(HAVE_POSIX_FADVISE)
+
+ ResetFileFlushContext(&spcContext[index]);
+
+#endif /* HAVE_SYNC_FILE_RANGE || HAVE_POSIX_FADVISE */
+
+ index ++;
+ }
+ }
+
+ hash_destroy(spcBuffers);
+ spcBuffers = NULL;
+
+ /*
+ * Sort buffer ids to help find sequential writes.
+ *
+ * Note: buffers are not locked in anyway, but that does not matter,
+ * this sorting is really advisory, if some buffer changes status during
+ * this pass it will be filtered out later. The only necessary property
+ * is that marked buffers do not move elsewhere. Also, qsort implementation
+ * should be resilient to occasional contradictions (cmp(a,b) != -cmp(b,a))
+ * because of these possible concurrent changes.
+ */
+ CheckpointStats.ckpt_sort_t = GetCurrentTimestamp();
+
+ if (checkpoint_sort)
+ {
+ qsort(CheckpointBufferIds + i, num_to_write, sizeof(int),
+ (int(*)(const void *, const void *)) bufcmp);
+ }
+
+ CheckpointStats.ckpt_sort_end_t = GetCurrentTimestamp();
+
/*
- * Loop over all buffers again, and write the ones (still) marked with
- * BM_CHECKPOINT_NEEDED. In this loop, we start at the clock sweep point
- * since we might as well dump soon-to-be-recycled buffers first.
+ * Loop over buffers to write through CheckpointBufferIds,
+ * and write the ones (still) marked with BM_CHECKPOINT_NEEDED,
+ * with some round robin over table spaces so as to balance writes,
+ * so that buffer writes move forward roughly proportionally for each
+ * tablespace.
*
- * Note that we don't read the buffer alloc count here --- that should be
- * left untouched till the next BgBufferSync() call.
+ * Termination: if a tablespace is selected by the inner while loop
+ * (see argument there), its index is incremented and will eventually
+ * reach num_to_write, mark this table space scanning as done and
+ * decrement the number of active spaces, which will thus reach 0.
*/
- buf_id = StrategySyncStart(NULL, NULL);
- num_to_scan = NBuffers;
+ active_spaces = nb_spaces;
+ space = 0;
num_written = 0;
- while (num_to_scan-- > 0)
+
+ while (active_spaces != 0)
{
- volatile BufferDesc *bufHdr = GetBufferDescriptor(buf_id);
+ volatile BufferDesc *bufHdr = NULL;
+ int index;
+
+ /*
+ * Select a tablespace depending on the current overall progress.
+ *
+ * The progress ratio of each unfinished tablespace is compared to
+ * the overall progress ratio to find one with is not in advance
+ * (i.e. tablespace ratio <= overall ratio).
+ *
+ * Existence: it is bound to exist otherwise the overall progress
+ * ratio would be inconsistent: with positive buffers to write (t1 & t2)
+ * and already written buffers (w1 & w2), we have:
+ *
+ * If w1/t1 > (w1+w2)/(t1+t2) # one table space is in advance
+ * => w1t1+w1t2 > w1t1+w2t1 => w1t2 > w2t1 => w1t2+w2t2 > w2t1+w2t2
+ * => (w1+w2) / (t1+t2) > w2 / t2 # the other one is late
+ *
+ * The round robin ensures that each space is given some attention
+ * till it is over the current ratio, before going to the next.
+ *
+ * Precision: using int32 computations for comparing fractions
+ * (w1 / t1 > w / t <=> w1 t > w t1) seems a bad idea as the values
+ * can overflow 32-bit integers: the limit would be sqrt(2**31) ~
+ * 46340 buffers, i.e. a 362 MB checkpoint. So ensure that 64-bit
+ * integers are used in the comparison.
+ */
+ while (spcStatus[space].done ||
+ /* compare tablespace vs overall progress ratio:
+ * tablespace written/to_write > overall written/to_write
+ */
+ (int64) spcStatus[space].num_written * num_to_write >
+ (int64) num_written * spcStatus[space].num_to_write)
+ space = (space + 1) % nb_spaces; /* round robin */
+
+ /*
+ * Find a valid buffer in the selected tablespace,
+ * by continuing the tablespace specific buffer scan
+ * where it was left.
+ */
+ index = spcStatus[space].index;
+
+ while (index < num_to_write && bufHdr == NULL)
+ {
+ buf_id = CheckpointBufferIds[index];
+ bufHdr = GetBufferDescriptor(buf_id);
+
+ /* Skip if in another tablespace or not in checkpoint anymore.
+ * No lock is acquired, see comments below.
+ */
+ if (spcStatus[space].space != bufHdr->tag.rnode.spcNode ||
+ ! (bufHdr->flags & BM_CHECKPOINT_NEEDED))
+ {
+ index ++;
+ buf_id = -1;
+ bufHdr = NULL;
+ }
+ }
+
+ /* Update tablespace writing status, will start over at next index */
+ spcStatus[space].index = index+1;
/*
* We don't need to acquire the lock here, because we're only looking
@@ -1660,39 +1895,49 @@ BufferSync(int flags)
* write the buffer though we didn't need to. It doesn't seem worth
* guarding against this, though.
*/
- if (bufHdr->flags & BM_CHECKPOINT_NEEDED)
+ if (bufHdr != NULL && bufHdr->flags & BM_CHECKPOINT_NEEDED)
{
- if (SyncOneBuffer(buf_id, false) & BUF_WRITTEN)
+ if (SyncOneBuffer(buf_id, false, checkpoint_flush_to_disk,
+ &spcContext[space]) & BUF_WRITTEN)
{
TRACE_POSTGRESQL_BUFFER_SYNC_WRITTEN(buf_id);
BgWriterStats.m_buf_written_checkpoints++;
+ spcStatus[space].num_written++;
num_written++;
/*
- * We know there are at most num_to_write buffers with
- * BM_CHECKPOINT_NEEDED set; so we can stop scanning if
- * num_written reaches num_to_write.
- *
- * Note that num_written doesn't include buffers written by
- * other backends, or by the bgwriter cleaning scan. That
- * means that the estimate of how much progress we've made is
- * conservative, and also that this test will often fail to
- * trigger. But it seems worth making anyway.
- */
- if (num_written >= num_to_write)
- break;
-
- /*
* Sleep to throttle our I/O rate.
*/
- CheckpointWriteDelay(flags, (double) num_written / num_to_write);
+ CheckpointWriteDelay(flags, (double) num_written / num_to_write,
+ spcContext, nb_spaces);
}
}
- if (++buf_id >= NBuffers)
- buf_id = 0;
+ /*
+ * Detect checkpoint end for a tablespace: either the scan is done
+ * or all tablespace buffers have been written out.
+ */
+ if (spcStatus[space].index >= num_to_write ||
+ spcStatus[space].num_written >= spcStatus[space].num_to_write)
+ {
+
+#if defined(HAVE_SYNC_FILE_RANGE) || defined(HAVE_POSIX_FADVISE)
+
+ PerformFileFlush(&spcContext[space]);
+ ResetFileFlushContext(&spcContext[space]);
+
+#endif /* HAVE_SYNC_FILE_RANGE || HAVE_POSIX_FADVISE */
+
+ spcStatus[space].done = true;
+ active_spaces--;
+ }
}
+ pfree(spcStatus);
+ spcStatus = NULL;
+ pfree(spcContext);
+ spcContext = NULL;
+
/*
* Update checkpoint statistics. As noted above, this doesn't include
* buffers written by other backends or bgwriter scan.
@@ -1939,7 +2184,8 @@ BgBufferSync(void)
/* Execute the LRU scan */
while (num_to_scan > 0 && reusable_buffers < upcoming_alloc_est)
{
- int buffer_state = SyncOneBuffer(next_to_clean, true);
+ int buffer_state =
+ SyncOneBuffer(next_to_clean, true, false, NULL);
if (++next_to_clean >= NBuffers)
{
@@ -2016,7 +2262,8 @@ BgBufferSync(void)
* Note: caller must have done ResourceOwnerEnlargeBuffers.
*/
static int
-SyncOneBuffer(int buf_id, bool skip_recently_used)
+SyncOneBuffer(int buf_id, bool skip_recently_used, bool flush_to_disk,
+ FileFlushContext * context)
{
volatile BufferDesc *bufHdr = GetBufferDescriptor(buf_id);
int result = 0;
@@ -2057,7 +2304,7 @@ SyncOneBuffer(int buf_id, bool skip_recently_used)
PinBuffer_Locked(bufHdr);
LWLockAcquire(bufHdr->content_lock, LW_SHARED);
- FlushBuffer(bufHdr, NULL);
+ FlushBuffer(bufHdr, NULL, flush_to_disk, context);
LWLockRelease(bufHdr->content_lock);
UnpinBuffer(bufHdr, true);
@@ -2319,9 +2566,16 @@ BufferGetTag(Buffer buffer, RelFileNode *rnode, ForkNumber *forknum,
*
* If the caller has an smgr reference for the buffer's relation, pass it
* as the second parameter. If not, pass NULL.
+ *
+ * The third parameter tries to hint the OS that a high priority write is meant,
+ * possibly because io-throttling is already managed elsewhere.
+ * The last parameter holds the current flush context that accumulates flush
+ * requests to be performed in one call, instead of being performed on a buffer
+ * per buffer basis.
*/
static void
-FlushBuffer(volatile BufferDesc *buf, SMgrRelation reln)
+FlushBuffer(volatile BufferDesc *buf, SMgrRelation reln, bool flush_to_disk,
+ FileFlushContext * context)
{
XLogRecPtr recptr;
ErrorContextCallback errcallback;
@@ -2410,7 +2664,9 @@ FlushBuffer(volatile BufferDesc *buf, SMgrRelation reln)
buf->tag.forkNum,
buf->tag.blockNum,
bufToWrite,
- false);
+ false,
+ flush_to_disk,
+ context);
if (track_io_timing)
{
@@ -2830,7 +3086,9 @@ FlushRelationBuffers(Relation rel)
bufHdr->tag.forkNum,
bufHdr->tag.blockNum,
localpage,
- false);
+ false,
+ false,
+ NULL);
bufHdr->flags &= ~(BM_DIRTY | BM_JUST_DIRTIED);
@@ -2864,7 +3122,7 @@ FlushRelationBuffers(Relation rel)
{
PinBuffer_Locked(bufHdr);
LWLockAcquire(bufHdr->content_lock, LW_SHARED);
- FlushBuffer(bufHdr, rel->rd_smgr);
+ FlushBuffer(bufHdr, rel->rd_smgr, false, NULL);
LWLockRelease(bufHdr->content_lock);
UnpinBuffer(bufHdr, true);
}
@@ -2916,7 +3174,7 @@ FlushDatabaseBuffers(Oid dbid)
{
PinBuffer_Locked(bufHdr);
LWLockAcquire(bufHdr->content_lock, LW_SHARED);
- FlushBuffer(bufHdr, NULL);
+ FlushBuffer(bufHdr, NULL, false, NULL);
LWLockRelease(bufHdr->content_lock);
UnpinBuffer(bufHdr, true);
}
diff --git a/src/backend/storage/buffer/localbuf.c b/src/backend/storage/buffer/localbuf.c
index 3144afe..114a0a6 100644
--- a/src/backend/storage/buffer/localbuf.c
+++ b/src/backend/storage/buffer/localbuf.c
@@ -208,7 +208,9 @@ LocalBufferAlloc(SMgrRelation smgr, ForkNumber forkNum, BlockNumber blockNum,
bufHdr->tag.forkNum,
bufHdr->tag.blockNum,
localpage,
- false);
+ false,
+ false,
+ NULL);
/* Mark not-dirty now in case we error out below */
bufHdr->flags &= ~BM_DIRTY;
diff --git a/src/backend/storage/file/buffile.c b/src/backend/storage/file/buffile.c
index ea4d689..fb3b383 100644
--- a/src/backend/storage/file/buffile.c
+++ b/src/backend/storage/file/buffile.c
@@ -317,7 +317,7 @@ BufFileDumpBuffer(BufFile *file)
return; /* seek failed, give up */
file->offsets[file->curFile] = file->curOffset;
}
- bytestowrite = FileWrite(thisfile, file->buffer + wpos, bytestowrite);
+ bytestowrite = FileWrite(thisfile, file->buffer + wpos, bytestowrite, false, NULL);
if (bytestowrite <= 0)
return; /* failed to write */
file->offsets[file->curFile] += bytestowrite;
diff --git a/src/backend/storage/file/fd.c b/src/backend/storage/file/fd.c
index 1ba4946..daf03e4 100644
--- a/src/backend/storage/file/fd.c
+++ b/src/backend/storage/file/fd.c
@@ -1344,8 +1344,97 @@ retry:
return returnCode;
}
+#if defined(HAVE_SYNC_FILE_RANGE) || defined(HAVE_POSIX_FADVISE)
+
+void
+ResetFileFlushContext(FileFlushContext * context)
+{
+ context->fd = 0;
+ context->ncalls = 0;
+ context->offset = 0;
+ context->nbytes = 0;
+ context->filename = NULL;
+}
+
+void
+PerformFileFlush(FileFlushContext * context)
+{
+ if (context->ncalls != 0)
+ {
+ int rc;
+
+#if defined(HAVE_SYNC_FILE_RANGE)
+
+ /* Linux: tell the memory manager to move these blocks to io so
+ * that they are considered for being actually written to disk.
+ */
+ rc = sync_file_range(context->fd, context->offset, context->nbytes,
+ SYNC_FILE_RANGE_WRITE);
+
+#elif defined(HAVE_POSIX_FADVISE)
+
+ /* Others: say that data should not be kept in memory...
+ * This is not exactly what we want to say, because we want to write
+ * the data for durability but we may need it later nevertheless.
+ * It seems that Linux would free the memory *if* the data has
+ * already been written do disk, else the "dontneed" call is ignored.
+ * For FreeBSD this may have the desired effect of moving the
+ * data to the io layer, although the system does not seem to
+ * take into account the provided offset & size, so it is rather
+ * rough...
+ */
+ rc = posix_fadvise(context->fd, context->offset, context->nbytes,
+ POSIX_FADV_DONTNEED);
+
+#endif
+
+ if (rc < 0)
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not flush block " INT64_FORMAT
+ " on " INT64_FORMAT " blocks in file \"%s\": %m",
+ context->offset / BLCKSZ,
+ context->nbytes / BLCKSZ,
+ context->filename)));
+ }
+}
+
+void
+FileAsynchronousFlush(FileFlushContext * context,
+ int fd, off_t offset, off_t nbytes, char * filename)
+{
+ if (context->ncalls != 0 && context->fd == fd)
+ {
+ /* Same file: merge current flush with previous ones */
+ off_t new_offset = offset < context->offset? offset: context->offset;
+
+ context->nbytes =
+ (context->offset + context->nbytes > offset + nbytes ?
+ context->offset + context->nbytes : offset + nbytes) -
+ new_offset;
+ context->offset = new_offset;
+ context->ncalls ++;
+ }
+ else
+ {
+ /* file has changed; actually flush previous file before restarting
+ * to accumulate flushes
+ */
+ PerformFileFlush(context);
+
+ context->fd = fd;
+ context->ncalls = 1;
+ context->offset = offset;
+ context->nbytes = nbytes;
+ context->filename = filename;
+ }
+}
+
+#endif /* HAVE_SYNC_FILE_RANGE || HAVE_POSIX_FADVISE */
+
int
-FileWrite(File file, char *buffer, int amount)
+FileWrite(File file, char *buffer, int amount, bool flush_to_disk,
+ FileFlushContext * context)
{
int returnCode;
@@ -1395,6 +1484,28 @@ retry:
if (returnCode >= 0)
{
+
+#if defined(HAVE_SYNC_FILE_RANGE) || defined(HAVE_POSIX_FADVISE)
+
+ /*
+ * Calling "write" tells the OS that pg wants to write some page to disk,
+ * however when it is really done is chosen by the OS.
+ * Depending on other disk activities this may be delayed significantly,
+ * maybe up to an "fsync" call, which could induce an IO write surge.
+ * When checkpointing pg is doing its own throttling and the result
+ * should really be written to disk with high priority, so as to meet
+ * the completion target.
+ * This call hints that such write have a higher priority.
+ */
+ if (flush_to_disk && returnCode == amount && errno == 0)
+ {
+ FileAsynchronousFlush(context,
+ VfdCache[file].fd, VfdCache[file].seekPos,
+ amount, VfdCache[file].fileName);
+ }
+
+#endif /* HAVE_SYNC_FILE_RANGE || HAVE_POSIX_FADVISE */
+
VfdCache[file].seekPos += returnCode;
/* maintain fileSize and temporary_files_size if it's a temp file */
diff --git a/src/backend/storage/smgr/md.c b/src/backend/storage/smgr/md.c
index 42a43bb..dbf057f 100644
--- a/src/backend/storage/smgr/md.c
+++ b/src/backend/storage/smgr/md.c
@@ -531,7 +531,7 @@ mdextend(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
errmsg("could not seek to block %u in file \"%s\": %m",
blocknum, FilePathName(v->mdfd_vfd))));
- if ((nbytes = FileWrite(v->mdfd_vfd, buffer, BLCKSZ)) != BLCKSZ)
+ if ((nbytes = FileWrite(v->mdfd_vfd, buffer, BLCKSZ, false, NULL)) != BLCKSZ)
{
if (nbytes < 0)
ereport(ERROR,
@@ -738,7 +738,8 @@ mdread(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
*/
void
mdwrite(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
- char *buffer, bool skipFsync)
+ char *buffer, bool skipFsync, bool flush_to_disk,
+ FileFlushContext * context)
{
off_t seekpos;
int nbytes;
@@ -767,7 +768,7 @@ mdwrite(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
errmsg("could not seek to block %u in file \"%s\": %m",
blocknum, FilePathName(v->mdfd_vfd))));
- nbytes = FileWrite(v->mdfd_vfd, buffer, BLCKSZ);
+ nbytes = FileWrite(v->mdfd_vfd, buffer, BLCKSZ, flush_to_disk, context);
TRACE_POSTGRESQL_SMGR_MD_WRITE_DONE(forknum, blocknum,
reln->smgr_rnode.node.spcNode,
diff --git a/src/backend/storage/smgr/smgr.c b/src/backend/storage/smgr/smgr.c
index 244b4ea..2db3cd3 100644
--- a/src/backend/storage/smgr/smgr.c
+++ b/src/backend/storage/smgr/smgr.c
@@ -52,7 +52,8 @@ typedef struct f_smgr
void (*smgr_read) (SMgrRelation reln, ForkNumber forknum,
BlockNumber blocknum, char *buffer);
void (*smgr_write) (SMgrRelation reln, ForkNumber forknum,
- BlockNumber blocknum, char *buffer, bool skipFsync);
+ BlockNumber blocknum, char *buffer, bool skipFsync,
+ bool flush_to_disk, FileFlushContext *context);
BlockNumber (*smgr_nblocks) (SMgrRelation reln, ForkNumber forknum);
void (*smgr_truncate) (SMgrRelation reln, ForkNumber forknum,
BlockNumber nblocks);
@@ -643,10 +644,11 @@ smgrread(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
*/
void
smgrwrite(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
- char *buffer, bool skipFsync)
+ char *buffer, bool skipFsync, bool flush_to_disk,
+ FileFlushContext * context)
{
(*(smgrsw[reln->smgr_which].smgr_write)) (reln, forknum, blocknum,
- buffer, skipFsync);
+ buffer, skipFsync, flush_to_disk, context);
}
/*
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index 230c5cc..2549873 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -158,6 +158,7 @@ static bool check_bonjour(bool *newval, void **extra, GucSource source);
static bool check_ssl(bool *newval, void **extra, GucSource source);
static bool check_stage_log_stats(bool *newval, void **extra, GucSource source);
static bool check_log_stats(bool *newval, void **extra, GucSource source);
+static bool check_flush_to_disk(bool *newval, void **extra, GucSource source);
static bool check_canonical_path(char **newval, void **extra, GucSource source);
static bool check_timezone_abbreviations(char **newval, void **extra, GucSource source);
static void assign_timezone_abbreviations(const char *newval, void *extra);
@@ -1009,6 +1010,27 @@ static struct config_bool ConfigureNamesBool[] =
false,
NULL, NULL, NULL
},
+
+ {
+ {"checkpoint_sort", PGC_SIGHUP, WAL_CHECKPOINTS,
+ gettext_noop("Whether disk-page buffers are sorted on checkpoints."),
+ NULL
+ },
+ &checkpoint_sort,
+ true,
+ NULL, NULL, NULL
+ },
+
+ {
+ {"checkpoint_flush_to_disk", PGC_SIGHUP, WAL_CHECKPOINTS,
+ gettext_noop("Hint that checkpoint's writes are high priority."),
+ NULL
+ },
+ &checkpoint_flush_to_disk,
+ false,
+ check_flush_to_disk, NULL, NULL
+ },
+
{
{"log_connections", PGC_SU_BACKEND, LOGGING_WHAT,
gettext_noop("Logs each successful connection."),
@@ -9760,6 +9782,21 @@ check_log_stats(bool *newval, void **extra, GucSource source)
}
static bool
+check_flush_to_disk(bool *newval, void **extra, GucSource source)
+{
+/* This test must be consistent with the one in FileWrite (storage/file/fd.c)
+ */
+#if ! (defined(HAVE_SYNC_FILE_RANGE) || defined(HAVE_POSIX_FADVISE))
+ /* just warn if it has no effect */
+ ereport(WARNING,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("Setting \"checkpoint_flush_to_disk\" has no effect "
+ "on this platform.")));
+#endif /* HAVE_SYNC_FILE_RANGE */
+ return true;
+}
+
+static bool
check_canonical_path(char **newval, void **extra, GucSource source)
{
/*
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 06dfc06..ae7f7cb 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -202,6 +202,8 @@
#max_wal_size = 1GB
#min_wal_size = 80MB
#checkpoint_completion_target = 0.5 # checkpoint target duration, 0.0 - 1.0
+#checkpoint_sort = on # sort buffers on checkpoint
+#checkpoint_flush_to_disk = off # send buffers to disk on checkpoint
#checkpoint_warning = 30s # 0 disables
# - Archiving -
diff --git a/src/include/access/xlog.h b/src/include/access/xlog.h
index 6dacee2..dbd4757 100644
--- a/src/include/access/xlog.h
+++ b/src/include/access/xlog.h
@@ -186,6 +186,8 @@ extern bool XLOG_DEBUG;
typedef struct CheckpointStatsData
{
TimestampTz ckpt_start_t; /* start of checkpoint */
+ TimestampTz ckpt_sort_t; /* start buffer sorting */
+ TimestampTz ckpt_sort_end_t; /* end of sorting */
TimestampTz ckpt_write_t; /* start of flushing buffers */
TimestampTz ckpt_sync_t; /* start of fsyncs */
TimestampTz ckpt_sync_end_t; /* end of fsyncs */
diff --git a/src/include/postmaster/bgwriter.h b/src/include/postmaster/bgwriter.h
index a49c208..f9c8ca1 100644
--- a/src/include/postmaster/bgwriter.h
+++ b/src/include/postmaster/bgwriter.h
@@ -16,6 +16,7 @@
#define _BGWRITER_H
#include "storage/block.h"
+#include "storage/fd.h"
#include "storage/relfilenode.h"
@@ -29,7 +30,8 @@ extern void BackgroundWriterMain(void) pg_attribute_noreturn();
extern void CheckpointerMain(void) pg_attribute_noreturn();
extern void RequestCheckpoint(int flags);
-extern void CheckpointWriteDelay(int flags, double progress);
+extern void CheckpointWriteDelay(int flags, double progress,
+ FileFlushContext * context, int ctx_size);
extern bool ForwardFsyncRequest(RelFileNode rnode, ForkNumber forknum,
BlockNumber segno);
diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.h
index ec0a254..db0e2c3 100644
--- a/src/include/storage/bufmgr.h
+++ b/src/include/storage/bufmgr.h
@@ -54,6 +54,8 @@ extern int bgwriter_lru_maxpages;
extern double bgwriter_lru_multiplier;
extern bool track_io_timing;
extern int target_prefetch_pages;
+extern bool checkpoint_flush_to_disk;
+extern bool checkpoint_sort;
/* in buf_init.c */
extern PGDLLIMPORT char *BufferBlocks;
diff --git a/src/include/storage/fd.h b/src/include/storage/fd.h
index 7eabe09..c740ee7 100644
--- a/src/include/storage/fd.h
+++ b/src/include/storage/fd.h
@@ -59,6 +59,22 @@ extern int max_files_per_process;
*/
extern int max_safe_fds;
+/* FileFlushContext:
+ * This structure is used to accumulate several flush requests on a file
+ * into a larger flush request.
+ * - fd: file descriptor of the file
+ * - ncalls: number of flushes merged together
+ * - offset: starting offset (minimum of all offset)
+ * - nbytes: size (minimum extent to cover all flushed data)
+ * - filename: filename of fd for error messages
+ */
+typedef struct FileFlushContext{
+ int fd;
+ int ncalls;
+ off_t offset;
+ off_t nbytes;
+ char * filename;
+} FileFlushContext;
/*
* prototypes for functions in fd.c
@@ -70,7 +86,12 @@ extern File OpenTemporaryFile(bool interXact);
extern void FileClose(File file);
extern int FilePrefetch(File file, off_t offset, int amount);
extern int FileRead(File file, char *buffer, int amount);
-extern int FileWrite(File file, char *buffer, int amount);
+extern void ResetFileFlushContext(FileFlushContext * context);
+extern void PerformFileFlush(FileFlushContext * context);
+extern void FileAsynchronousFlush(FileFlushContext * context,
+ int fd, off_t offset, off_t nbytes, char * filename);
+extern int FileWrite(File file, char *buffer, int amount, bool flush_to_disk,
+ FileFlushContext * context);
extern int FileSync(File file);
extern off_t FileSeek(File file, off_t offset, int whence);
extern int FileTruncate(File file, off_t offset);
diff --git a/src/include/storage/smgr.h b/src/include/storage/smgr.h
index 69a624f..a46a70c 100644
--- a/src/include/storage/smgr.h
+++ b/src/include/storage/smgr.h
@@ -16,6 +16,7 @@
#include "fmgr.h"
#include "storage/block.h"
+#include "storage/fd.h"
#include "storage/relfilenode.h"
@@ -95,7 +96,8 @@ extern void smgrprefetch(SMgrRelation reln, ForkNumber forknum,
extern void smgrread(SMgrRelation reln, ForkNumber forknum,
BlockNumber blocknum, char *buffer);
extern void smgrwrite(SMgrRelation reln, ForkNumber forknum,
- BlockNumber blocknum, char *buffer, bool skipFsync);
+ BlockNumber blocknum, char *buffer, bool skipFsync,
+ bool flush_to_disk, FileFlushContext * context);
extern BlockNumber smgrnblocks(SMgrRelation reln, ForkNumber forknum);
extern void smgrtruncate(SMgrRelation reln, ForkNumber forknum,
BlockNumber nblocks);
@@ -120,8 +122,9 @@ extern void mdprefetch(SMgrRelation reln, ForkNumber forknum,
BlockNumber blocknum);
extern void mdread(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
char *buffer);
-extern void mdwrite(SMgrRelation reln, ForkNumber forknum,
- BlockNumber blocknum, char *buffer, bool skipFsync);
+extern void mdwrite(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
+ char *buffer, bool skipFsync, bool flush_to_disk,
+ FileFlushContext * context);
extern BlockNumber mdnblocks(SMgrRelation reln, ForkNumber forknum);
extern void mdtruncate(SMgrRelation reln, ForkNumber forknum,
BlockNumber nblocks);
On Wed, Jun 24, 2015 at 9:50 AM, Fabien COELHO <coelho@cri.ensmp.fr> wrote:
flsh | full speed tps | percent of late tx, 4 clients
/srt | 1 client | 4 clients | 100 | 200 | 400 |
N/N | 173 +- 289* | 198 +- 531* | 27.61 | 43.92 | 61.16 |
N/Y | 458 +- 327* | 743 +- 920* | 7.05 | 14.24 | 24.07 |
Y/N | 169 +- 166* | 187 +- 302* | 4.01 | 39.84 | 65.70 |
Y/Y | 546 +- 143 | 681 +- 459 | 1.55 | 3.51 | 2.84 |The effect of sorting is very positive (+150% to 270% tps). On this
run,flushing has a positive (+20% with 1 client) or negative (-8 % with 4
clients) on throughput, and late transactions are reduced by 92-95% when
both options are activated.Why there is dip in performance with multiple clients,
I'm not sure to see the "dip". The performances are better with 4 clients
compared to 1 client?What do you mean by "negative (-8 % with 4 clients) on throughput" in
above sentence? I thought by that you mean that there is dip in TPS with
patch as compare to HEAD at 4 clients.Ok, I misunderstood your question. I thought you meant a dip between 1
client and 4 clients. I meant that when flush is turned on tps goes down by
8% (743 to 681 tps) on this particular run.
This 8% might matter if the dip is bigger with more clients and
more aggressive workload. Do you know what could lead to this
dip, because if we know what is the reason than it will be more
predictable to know if this is the max dip that could happen or it
could lead to bigger dip in other cases.
Basically tps improvements mostly come from "sort", and "flush" has
uncertain effects on tps (throuput), but much more on latency and
performance stability (lower late rate, lower standard deviation).
I agree that performance stability is important, but not sure if it
is good idea to sacrifice the throuput for it. If sort + flush always
gives better results, then isn't it better to perform these actions
together under one option.
Note that I'm not comparing to HEAD in the above tests, but with the new
options desactivated, which should be more or less comparable to current
HEAD, i.e. there is no sorting nor flushing done, but this is not strictly
speaking HEAD behavior. Probably I should get some figures with HEAD as
well to check the "more or less" assumption.Also I am not completely sure what's +- means in your data above?
The first figure before "+-" is the tps, the second after is its standard
deviation computed in per-second traces. Some runs are very bad, with
pgbench stuck at times, and result on stddev larger than the average, they
ere noted with "*".I understand your point and I also don't have any specific answer
for it at this moment, the point of worry is that it should not lead
to degradation of certain cases as compare to current algorithm.
The workload where it could effect is when your data doesn't fit
in shared buffers, but can fit in RAM.Hmmm. My point of view is still that the logical priority is to optimize
for disk IO first, then look for compatible RAM optimisations later.
It is not only about RAM optimisation which we can do later, but also
about avoiding regression in existing use-cases.
With Regards,
Amit Kapila.
EnterpriseDB: http://www.enterprisedb.com
Hello Amit,
[...]
Ok, I misunderstood your question. I thought you meant a dip between 1
client and 4 clients. I meant that when flush is turned on tps goes down by
8% (743 to 681 tps) on this particular run.This 8% might matter if the dip is bigger with more clients and
more aggressive workload. Do you know what could lead to this
dip, because if we know what is the reason than it will be more
predictable to know if this is the max dip that could happen or it
could lead to bigger dip in other cases.
I do not know the cause of the dip, and whether it would increase with
more clients. I do not have a box for such tests. If someone can provided
the box, I can provide test scripts:-)
The first, although higher, measure is really very unstable, with pg
totaly unresponsive (offline, really) at time.
I think that the flush option may always have a risk of (small)
detrimental effects on tps, because there are two steady states: one with
pg only doing wal-logged transactions with great tps, and one with pg
doing the checkpoint at nought tps. If this is on the same disk, even at
best the combination means that probably each operation will amper the
other one a little bit, so the combined tps performance would/could be
lower than doing one after the other and having pg offline 50% of the
time...
Please also note that this 8% "dip" is on a 681 (with the dip) vs 198 (no
options at all) a X 3.4 improvement compared to pg current behavior.
Basically tps improvements mostly come from "sort", and "flush" has
uncertain effects on tps (throuput), but much more on latency and
performance stability (lower late rate, lower standard deviation).I agree that performance stability is important, but not sure if it
is good idea to sacrifice the throuput for it.
See discussion above. I think better stability may imply slightly lower
throughput on some load. That is why there are options and DBA to choose
them:-)
If sort + flush always gives better results, then isn't it better to
perform these actions together under one option.
Sure, but that is not currently the case. Also what is done is very
orthogonal, so I would tend to keep these separate. If one is always
beneficial and it is wished that it should be always activated, then the
option could be removed.
Hmmm. My point of view is still that the logical priority is to optimize
for disk IO first, then look for compatible RAM optimisations later.It is not only about RAM optimisation which we can do later, but also
about avoiding regression in existing use-cases.
Hmmm. Currently I have not seen really significant regressions. I have
seen some less good impact of some options on some loads.
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Note that I'm not comparing to HEAD in the above tests, but with the new
options desactivated, which should be more or less comparable to current
HEAD, i.e. there is no sorting nor flushing done, but this is not strictly
speaking HEAD behavior. Probably I should get some figures with HEAD as well
to check the "more or less" assumption.
Just for answering myself on this point, I tried current HEAD vs patch v4
with sort OFF + flush OFF: the figures are indeed quite comparable (see
below), so although the internal implementation is different, the
performance when both options are off is still a reasonable approximation
of the performance without the patch, as I was expecting. What patch v4
still does with OFF/OFF which is not done by HEAD is balancing writes
among tablespaces, but there is only one disk on these tests so it does
not matter.
tps & stddev full speed:
HEAD OFF/OFF
tiny 1 client 727 +- 227 221 +- 246
small 1 client 158 +- 316 158 +- 325
medium 1 client 148 +- 285 157 +- 326
tiny 4 clients 2088 +- 786 2074 +- 699
small 4 clients 192 +- 648 188 +- 560
medium 4 clients 220 +- 654 220 +- 648
percent of late transactions:
HEAD OFF/OFF
tiny 4 clients 100 tps 6.31 6.67
small 4c 100 tps 35.68 35.23
medium 4c 100 tps 37.38 38.00
tiny 4c 200 tps 9.06 9.10
small 4c 200 tps 51.65 51.16
medium 4c 200 tps 51.35 50.20
tiny 4 clients 400 tps 11.4 10.5
small 4 clients 400 tps 66.4 67.6
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 2015-06-26 21:47:30 +0200, Fabien COELHO wrote:
tps & stddev full speed:
HEAD OFF/OFF
tiny 1 client 727 +- 227 221 +- 246
Huh?
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Hello Andres,
HEAD OFF/OFF
tiny 1 client 727 +- 227 221 +- 246
Huh?
Indeed, just to check that someone was reading this magnificent mail:-)
Just a typo because I reformated the figures for simpler comparison. 221
is really 721, quite close to 727.
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Attached is an updated version of the patch which turns the sort option into
a boolean, and also include the sort time in the checkpoint log.There is still an open question about whether the sorting buffer allocation
is lost on some signals and should be reallocated in such event.
In such case, probably the allocation should be managed from
CheckpointerMain, and the lazy allocation could remain for other callers
(I guess just "initdb").
More open questions:
- best name for the flush option (checkpoint_flush_to_disk,
checkpoint_flush_on_write, checkpoint_flush, ...)
- best name for the sort option (checkpoint_sort,
checkpoint_sort_buffers, checkpoint_sort_ios, ...)
Other nice-to-have inputs:
- tests on a non-linux system with posix_fadvise
(FreeBSD? others?)
- tests on a large dedicated box
Attached are some scripts to help with testing, if someone's feels like
that:
- cp_test.sh: run some tests, to adapt to one's setup...
- cp_test_count.pl: show percent of late transactions
- avg.py: show stats about stuff
sh> grep 'progress: ' OUTPUT_FILE | cut -d' ' -f4 | avg.py
*BEWARE* that if pgbench got stuck some "0" data are missing,
look for the actual tps in the output file and for the line
count to check whether it is the case... some currently submitted
patch on pgbench helps, see https://commitfest.postgresql.org/5/199/
--
Fabien.
Hello,
Attached is very minor v5 update which does a rebase & completes the
cleanup of doing a full sort instead of a chuncked sort.
Attached is an updated version of the patch which turns the sort option
into a boolean, and also include the sort time in the checkpoint log.There is still an open question about whether the sorting buffer allocation
is lost on some signals and should be reallocated in such event.In such case, probably the allocation should be managed from
CheckpointerMain, and the lazy allocation could remain for other callers (I
guess just "initdb").More open questions:
- best name for the flush option (checkpoint_flush_to_disk,
checkpoint_flush_on_write, checkpoint_flush, ...)- best name for the sort option (checkpoint_sort,
checkpoint_sort_buffers, checkpoint_sort_ios, ...)Other nice-to-have inputs:
- tests on a non-linux system with posix_fadvise
(FreeBSD? others?)- tests on a large dedicated box
Attached are some scripts to help with testing, if someone's feels like that:
- cp_test.sh: run some tests, to adapt to one's setup...
- cp_test_count.pl: show percent of late transactions
- avg.py: show stats about stuff
sh> grep 'progress: ' OUTPUT_FILE | cut -d' ' -f4 | avg.py
*BEWARE* that if pgbench got stuck some "0" data are missing,
look for the actual tps in the output file and for the line
count to check whether it is the case... some currently submitted
patch on pgbench helps, see https://commitfest.postgresql.org/5/199/
As this pgbench patch is now in master, pgbench is less likely to get
stuck, but check nevertheless that the number of progress line matches the
expected number.
--
Fabien.
Attachments:
checkpoint-continuous-flush-5.patchtext/x-diff; name=checkpoint-continuous-flush-5.patchDownload
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index bbe1eb0..0257e34 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -2483,6 +2483,28 @@ include_dir 'conf.d'
</listitem>
</varlistentry>
+ <varlistentry id="guc-checkpoint-sort" xreflabel="checkpoint_sort">
+ <term><varname>checkpoint_sort</varname> (<type>bool</type>)
+ <indexterm>
+ <primary><varname>checkpoint_sort</> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ Whether to sort buffers before writting them out to disk on checkpoint.
+ For a HDD storage, this setting allows to group together
+ neighboring pages written to disk, thus improving performance by
+ reducing random write activity.
+ This sorting should have limited performance effects on SSD backends
+ as such storages have good random write performance, but it may
+ help with wear-leveling so be worth keeping anyway.
+ The default is <literal>on</>.
+ This parameter can only be set in the <filename>postgresql.conf</>
+ file or on the server command line.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="guc-checkpoint-warning" xreflabel="checkpoint_warning">
<term><varname>checkpoint_warning</varname> (<type>integer</type>)
<indexterm>
@@ -2504,6 +2526,24 @@ include_dir 'conf.d'
</listitem>
</varlistentry>
+ <varlistentry id="guc-checkpoint-flush-to-disk" xreflabel="checkpoint_flush_to_disk">
+ <term><varname>checkpoint_flush_to_disk</varname> (<type>bool</type>)
+ <indexterm>
+ <primary><varname>checkpoint_flush_to_disk</> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ When writing data for a checkpoint, hint the underlying OS that the
+ data must be sent to disk as soon as possible. This may help smoothing
+ disk I/O writes and avoid a stall when fsync is issued at the end of
+ the checkpoint, but it may also reduce average performance.
+ This setting may have no effect on some platforms.
+ The default is <literal>off</>.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="guc-min-wal-size" xreflabel="min_wal_size">
<term><varname>min_wal_size</varname> (<type>integer</type>)
<indexterm>
diff --git a/doc/src/sgml/wal.sgml b/doc/src/sgml/wal.sgml
index e3941c9..eea6668 100644
--- a/doc/src/sgml/wal.sgml
+++ b/doc/src/sgml/wal.sgml
@@ -546,6 +546,29 @@
</para>
<para>
+ When hard-disk drives (HDD) are used for terminal data storage
+ <xref linkend="guc-checkpoint-sort"> allows to sort pages
+ so that neighboring pages on disk will be flushed together by
+ chekpoints, reducing the random write load and improving performance.
+ If solid-state drives (SSD) are used, sorting pages induces no benefit
+ as their random write I/O performance is good: this feature could then
+ be disabled by setting <varname>checkpoint_sort</> to <value>off</>.
+ It is possible that sorting may help with SSD wear leveling, so it may
+ be kept on that account.
+ </para>
+
+ <para>
+ On Linux and POSIX platforms, <xref linkend="guc-checkpoint-flush-to-disk">
+ allows to hint the OS that pages written on checkpoints must be flushed
+ to disk quickly. Otherwise, these pages may be kept in cache for some time,
+ inducing a stall later when <literal>fsync</> is called to actually
+ complete the checkpoint. This setting helps to reduce transaction latency,
+ but it may also have a small adverse effect on the average transaction rate
+ at maximum throughput. This feature probably brings no benefit on SSD,
+ as the I/O write latency is small on such hardware, thus it may be disabled.
+ </para>
+
+ <para>
The number of WAL segment files in <filename>pg_xlog</> directory depends on
<varname>min_wal_size</>, <varname>max_wal_size</> and
the amount of WAL generated in previous checkpoint cycles. When old log
diff --git a/src/backend/access/heap/rewriteheap.c b/src/backend/access/heap/rewriteheap.c
index bcce3e3..f565dc4 100644
--- a/src/backend/access/heap/rewriteheap.c
+++ b/src/backend/access/heap/rewriteheap.c
@@ -918,7 +918,7 @@ logical_heap_rewrite_flush_mappings(RewriteState state)
* Note that we deviate from the usual WAL coding practices here,
* check the above "Logical rewrite support" comment for reasoning.
*/
- written = FileWrite(src->vfd, waldata_start, len);
+ written = FileWrite(src->vfd, waldata_start, len, false, NULL);
if (written != len)
ereport(ERROR,
(errcode_for_file_access(),
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index 9431ab5..49ec258 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -203,7 +203,7 @@ btbuildempty(PG_FUNCTION_ARGS)
/* Write the page. If archiving/streaming, XLOG it. */
PageSetChecksumInplace(metapage, BTREE_METAPAGE);
smgrwrite(index->rd_smgr, INIT_FORKNUM, BTREE_METAPAGE,
- (char *) metapage, true);
+ (char *) metapage, true, false, NULL);
if (XLogIsNeeded())
log_newpage(&index->rd_smgr->smgr_rnode.node, INIT_FORKNUM,
BTREE_METAPAGE, metapage, false);
diff --git a/src/backend/access/nbtree/nbtsort.c b/src/backend/access/nbtree/nbtsort.c
index f95f67a..ea7a45d 100644
--- a/src/backend/access/nbtree/nbtsort.c
+++ b/src/backend/access/nbtree/nbtsort.c
@@ -315,7 +315,7 @@ _bt_blwritepage(BTWriteState *wstate, Page page, BlockNumber blkno)
{
/* overwriting a block we zero-filled before */
smgrwrite(wstate->index->rd_smgr, MAIN_FORKNUM, blkno,
- (char *) page, true);
+ (char *) page, true, false, NULL);
}
pfree(page);
diff --git a/src/backend/access/spgist/spginsert.c b/src/backend/access/spgist/spginsert.c
index bceee8d..b700efb 100644
--- a/src/backend/access/spgist/spginsert.c
+++ b/src/backend/access/spgist/spginsert.c
@@ -170,7 +170,7 @@ spgbuildempty(PG_FUNCTION_ARGS)
/* Write the page. If archiving/streaming, XLOG it. */
PageSetChecksumInplace(page, SPGIST_METAPAGE_BLKNO);
smgrwrite(index->rd_smgr, INIT_FORKNUM, SPGIST_METAPAGE_BLKNO,
- (char *) page, true);
+ (char *) page, true, false, NULL);
if (XLogIsNeeded())
log_newpage(&index->rd_smgr->smgr_rnode.node, INIT_FORKNUM,
SPGIST_METAPAGE_BLKNO, page, false);
@@ -180,7 +180,7 @@ spgbuildempty(PG_FUNCTION_ARGS)
PageSetChecksumInplace(page, SPGIST_ROOT_BLKNO);
smgrwrite(index->rd_smgr, INIT_FORKNUM, SPGIST_ROOT_BLKNO,
- (char *) page, true);
+ (char *) page, true, false, NULL);
if (XLogIsNeeded())
log_newpage(&index->rd_smgr->smgr_rnode.node, INIT_FORKNUM,
SPGIST_ROOT_BLKNO, page, true);
@@ -190,7 +190,7 @@ spgbuildempty(PG_FUNCTION_ARGS)
PageSetChecksumInplace(page, SPGIST_NULL_BLKNO);
smgrwrite(index->rd_smgr, INIT_FORKNUM, SPGIST_NULL_BLKNO,
- (char *) page, true);
+ (char *) page, true, false, NULL);
if (XLogIsNeeded())
log_newpage(&index->rd_smgr->smgr_rnode.node, INIT_FORKNUM,
SPGIST_NULL_BLKNO, page, true);
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 1dd31b3..e0bfa66 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -7942,11 +7942,13 @@ LogCheckpointEnd(bool restartpoint)
sync_secs,
total_secs,
longest_secs,
+ sort_secs,
average_secs;
int write_usecs,
sync_usecs,
total_usecs,
longest_usecs,
+ sort_usecs,
average_usecs;
uint64 average_sync_time;
@@ -7977,6 +7979,10 @@ LogCheckpointEnd(bool restartpoint)
CheckpointStats.ckpt_end_t,
&total_secs, &total_usecs);
+ TimestampDifference(CheckpointStats.ckpt_sort_t,
+ CheckpointStats.ckpt_sort_end_t,
+ &sort_secs, &sort_usecs);
+
/*
* Timing values returned from CheckpointStats are in microseconds.
* Convert to the second plus microsecond form that TimestampDifference
@@ -7995,8 +8001,8 @@ LogCheckpointEnd(bool restartpoint)
elog(LOG, "%s complete: wrote %d buffers (%.1f%%); "
"%d transaction log file(s) added, %d removed, %d recycled; "
- "write=%ld.%03d s, sync=%ld.%03d s, total=%ld.%03d s; "
- "sync files=%d, longest=%ld.%03d s, average=%ld.%03d s; "
+ "sort=%ld.%03d s, write=%ld.%03d s, sync=%ld.%03d s, total=%ld.%03d s;"
+ " sync files=%d, longest=%ld.%03d s, average=%ld.%03d s; "
"distance=%d kB, estimate=%d kB",
restartpoint ? "restartpoint" : "checkpoint",
CheckpointStats.ckpt_bufs_written,
@@ -8004,6 +8010,7 @@ LogCheckpointEnd(bool restartpoint)
CheckpointStats.ckpt_segs_added,
CheckpointStats.ckpt_segs_removed,
CheckpointStats.ckpt_segs_recycled,
+ sort_secs, sort_usecs / 1000,
write_secs, write_usecs / 1000,
sync_secs, sync_usecs / 1000,
total_secs, total_usecs / 1000,
diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c
index 3b3a09e..e361907 100644
--- a/src/backend/postmaster/checkpointer.c
+++ b/src/backend/postmaster/checkpointer.c
@@ -665,7 +665,8 @@ ImmediateCheckpointRequested(void)
* fraction between 0.0 meaning none, and 1.0 meaning all done.
*/
void
-CheckpointWriteDelay(int flags, double progress)
+CheckpointWriteDelay(int flags, double progress,
+ FileFlushContext * context, int ctx_size)
{
static int absorb_counter = WRITES_PER_ABSORB;
@@ -700,6 +701,26 @@ CheckpointWriteDelay(int flags, double progress)
*/
pgstat_send_bgwriter();
+#if defined(HAVE_SYNC_FILE_RANGE) || defined(HAVE_POSIX_FADVISE)
+
+ /*
+ * Before sleeping, flush written blocks for each tablespace.
+ */
+ if (checkpoint_flush_to_disk)
+ {
+ int i;
+
+ for (i = 0; i < ctx_size; i++)
+ {
+ if (context[i].ncalls != 0)
+ {
+ PerformFileFlush(&context[i]);
+ ResetFileFlushContext(&context[i]);
+ }
+ }
+ }
+#endif /* HAVE_SYNC_FILE_RANGE || HAVE_POSIX_FADVISE */
+
/*
* This sleep used to be connected to bgwriter_delay, typically 200ms.
* That resulted in more frequent wakeups if not much work to do.
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index e4b25587..8c7b099 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -80,6 +80,10 @@ bool zero_damaged_pages = false;
int bgwriter_lru_maxpages = 100;
double bgwriter_lru_multiplier = 2.0;
bool track_io_timing = false;
+/* hint to move writes to high priority */
+bool checkpoint_flush_to_disk = false;
+/* by default, sort by chunks of 1 GB worth of 8 kB buffers */
+bool checkpoint_sort = true;
/*
* How many buffers PrefetchBuffer callers should try to stay ahead of their
@@ -396,7 +400,8 @@ static bool PinBuffer(volatile BufferDesc *buf, BufferAccessStrategy strategy);
static void PinBuffer_Locked(volatile BufferDesc *buf);
static void UnpinBuffer(volatile BufferDesc *buf, bool fixOwner);
static void BufferSync(int flags);
-static int SyncOneBuffer(int buf_id, bool skip_recently_used);
+static int SyncOneBuffer(int buf_id, bool skip_recently_used,
+ bool flush_to_disk, FileFlushContext *context);
static void WaitIO(volatile BufferDesc *buf);
static bool StartBufferIO(volatile BufferDesc *buf, bool forInput);
static void TerminateBufferIO(volatile BufferDesc *buf, bool clear_dirty,
@@ -409,7 +414,8 @@ static volatile BufferDesc *BufferAlloc(SMgrRelation smgr,
BlockNumber blockNum,
BufferAccessStrategy strategy,
bool *foundPtr);
-static void FlushBuffer(volatile BufferDesc *buf, SMgrRelation reln);
+static void FlushBuffer(volatile BufferDesc *buf, SMgrRelation reln,
+ bool flush_to_disk, FileFlushContext *context);
static void AtProcExit_Buffers(int code, Datum arg);
static void CheckForBufferLeaks(void);
static int rnode_comparator(const void *p1, const void *p2);
@@ -1018,7 +1024,7 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
smgr->smgr_rnode.node.dbNode,
smgr->smgr_rnode.node.relNode);
- FlushBuffer(buf, NULL);
+ FlushBuffer(buf, NULL, false, NULL);
LWLockRelease(buf->content_lock);
TRACE_POSTGRESQL_BUFFER_WRITE_DIRTY_DONE(forkNum, blockNum,
@@ -1561,6 +1567,75 @@ UnpinBuffer(volatile BufferDesc *buf, bool fixOwner)
}
}
+/* Array of buffer ids of all buffers to checkpoint.
+ */
+static int * CheckpointBufferIds = NULL;
+
+/* Compare checkpoint buffers
+ */
+static int bufcmp(const int * pa, const int * pb)
+{
+ BufferDesc
+ *a = GetBufferDescriptor(*pa),
+ *b = GetBufferDescriptor(*pb);
+
+ /* tag: rnode, forkNum (different files), blockNum
+ * rnode: { spcNode (ignore: not really needed),
+ * dbNode (ignore: this is a directory), relNode }
+ * spcNode: table space oid, not that there are at least two
+ * (pg_global and pg_default).
+ */
+ /* compare relation */
+ if (a->tag.rnode.relNode < b->tag.rnode.relNode)
+ return -1;
+ else if (a->tag.rnode.relNode > b->tag.rnode.relNode)
+ return 1;
+ /* same relation, compare fork */
+ else if (a->tag.forkNum < b->tag.forkNum)
+ return -1;
+ else if (a->tag.forkNum > b->tag.forkNum)
+ return 1;
+ /* same relation/fork, so same segmented "file", compare block number
+ * which are mapped on different segments depending on the number.
+ */
+ else if (a->tag.blockNum < b->tag.blockNum)
+ return -1;
+ else /* should not be the same block anyway... */
+ return 1;
+}
+
+static void AllocateCheckpointBufferIds(void)
+{
+ /* Safe worst case allocation, all buffers belong to the checkpoint...
+ * that is pretty unlikely.
+ */
+ CheckpointBufferIds = (int *) palloc(sizeof(int) * NBuffers);
+}
+
+/* Status of buffers to checkpoint for a particular tablespace,
+ * used internally in BufferSync.
+ * - spcNone: oid of the tablespace
+ * - num_to_write: number of pages counted for this tablespace
+ * - num_written: number of pages actually written out
+ * - index: scanning position in CheckpointBufferIds for this tablespace
+ * - done: whether it is done
+ */
+typedef struct TableSpaceCheckpointStatus {
+ Oid space;
+ int num_to_write;
+ int num_written;
+ int index;
+ bool done;
+} TableSpaceCheckpointStatus;
+
+/* entry structure for table space to count hashtable,
+ * used internally in BufferSync.
+ */
+typedef struct TableSpaceCountEntry {
+ Oid space;
+ int count;
+} TableSpaceCountEntry;
+
/*
* BufferSync -- Write out all dirty buffers in the pool.
*
@@ -1575,10 +1650,20 @@ static void
BufferSync(int flags)
{
int buf_id;
- int num_to_scan;
int num_to_write;
int num_written;
int mask = BM_DIRTY;
+ HTAB *spcBuffers;
+ TableSpaceCheckpointStatus *spcStatus = NULL;
+ int nb_spaces, active_spaces, space;
+ FileFlushContext * spcContext = NULL;
+
+ /*
+ * Lazy allocation: this function is called through the checkpointer,
+ * but also by initdb. Maybe the allocation could be moved to the callers.
+ */
+ if (CheckpointBufferIds == NULL)
+ AllocateCheckpointBufferIds();
/* Make sure we can handle the pin inside SyncOneBuffer */
ResourceOwnerEnlargeBuffers(CurrentResourceOwner);
@@ -1609,6 +1694,18 @@ BufferSync(int flags)
* certainly need to be written for the next checkpoint attempt, too.
*/
num_to_write = 0;
+
+ /* initialize oid -> int buffer count hash table */
+ {
+ HASHCTL ctl;
+
+ MemSet(&ctl, 0, sizeof(HASHCTL));
+ ctl.keysize = sizeof(Oid);
+ ctl.entrysize = sizeof(TableSpaceCountEntry);
+ spcBuffers = hash_create("Number of buffers to write per tablespace",
+ 16, &ctl, HASH_ELEM | HASH_BLOBS);
+ }
+
for (buf_id = 0; buf_id < NBuffers; buf_id++)
{
volatile BufferDesc *bufHdr = GetBufferDescriptor(buf_id);
@@ -1621,32 +1718,169 @@ BufferSync(int flags)
if ((bufHdr->flags & mask) == mask)
{
+ Oid spc;
+ TableSpaceCountEntry * entry;
+ bool found;
+
bufHdr->flags |= BM_CHECKPOINT_NEEDED;
+ CheckpointBufferIds[num_to_write] = buf_id;
num_to_write++;
+
+ /* keep track of per tablespace buffers */
+ spc = bufHdr->tag.rnode.spcNode;
+ entry = (TableSpaceCountEntry *)
+ hash_search(spcBuffers, (void *) &spc, HASH_ENTER, &found);
+
+ if (found) entry->count++;
+ else entry->count = 1;
}
UnlockBufHdr(bufHdr);
}
if (num_to_write == 0)
+ {
+ hash_destroy(spcBuffers);
return; /* nothing to do */
+ }
TRACE_POSTGRESQL_BUFFER_SYNC_START(NBuffers, num_to_write);
+ /* Build checkpoint tablespace buffer status & flush context arrays */
+ nb_spaces = hash_get_num_entries(spcBuffers);
+ spcStatus = (TableSpaceCheckpointStatus *)
+ palloc(sizeof(TableSpaceCheckpointStatus) * nb_spaces);
+ spcContext = (FileFlushContext *)
+ palloc(sizeof(FileFlushContext) * nb_spaces);
+
+ {
+ int index = 0;
+ HASH_SEQ_STATUS hseq;
+ TableSpaceCountEntry * entry;
+
+ hash_seq_init(&hseq, spcBuffers);
+ while ((entry = (TableSpaceCountEntry *) hash_seq_search(&hseq)))
+ {
+ Assert(index < nb_spaces);
+ spcStatus[index].space = entry->space;
+ spcStatus[index].num_to_write = entry->count;
+ spcStatus[index].num_written = 0;
+ /* should it be randomized? chosen with some criterion? */
+ spcStatus[index].index = 0;
+ spcStatus[index].done = false;
+
+#if defined(HAVE_SYNC_FILE_RANGE) || defined(HAVE_POSIX_FADVISE)
+
+ ResetFileFlushContext(&spcContext[index]);
+
+#endif /* HAVE_SYNC_FILE_RANGE || HAVE_POSIX_FADVISE */
+
+ index ++;
+ }
+ }
+
+ hash_destroy(spcBuffers);
+ spcBuffers = NULL;
+
+ /*
+ * Sort buffer ids to help find sequential writes.
+ *
+ * Note: buffers are not locked in anyway, but that does not matter,
+ * this sorting is really advisory, if some buffer changes status during
+ * this pass it will be filtered out later. The only necessary property
+ * is that marked buffers do not move elsewhere. Also, qsort implementation
+ * should be resilient to occasional contradictions (cmp(a,b) != -cmp(b,a))
+ * because of these possible concurrent changes.
+ */
+ CheckpointStats.ckpt_sort_t = GetCurrentTimestamp();
+
+ if (checkpoint_sort)
+ {
+ qsort(CheckpointBufferIds, num_to_write, sizeof(int),
+ (int(*)(const void *, const void *)) bufcmp);
+ }
+
+ CheckpointStats.ckpt_sort_end_t = GetCurrentTimestamp();
+
/*
- * Loop over all buffers again, and write the ones (still) marked with
- * BM_CHECKPOINT_NEEDED. In this loop, we start at the clock sweep point
- * since we might as well dump soon-to-be-recycled buffers first.
+ * Loop over buffers to write through CheckpointBufferIds,
+ * and write the ones (still) marked with BM_CHECKPOINT_NEEDED,
+ * with some round robin over table spaces so as to balance writes,
+ * so that buffer writes move forward roughly proportionally for each
+ * tablespace.
*
- * Note that we don't read the buffer alloc count here --- that should be
- * left untouched till the next BgBufferSync() call.
+ * Termination: if a tablespace is selected by the inner while loop
+ * (see argument there), its index is incremented and will eventually
+ * reach num_to_write, mark this table space scanning as done and
+ * decrement the number of active spaces, which will thus reach 0.
*/
- buf_id = StrategySyncStart(NULL, NULL);
- num_to_scan = NBuffers;
+ active_spaces = nb_spaces;
+ space = 0;
num_written = 0;
- while (num_to_scan-- > 0)
+
+ while (active_spaces != 0)
{
- volatile BufferDesc *bufHdr = GetBufferDescriptor(buf_id);
+ volatile BufferDesc *bufHdr = NULL;
+ int index;
+
+ /*
+ * Select a tablespace depending on the current overall progress.
+ *
+ * The progress ratio of each unfinished tablespace is compared to
+ * the overall progress ratio to find one with is not in advance
+ * (i.e. tablespace ratio <= overall ratio).
+ *
+ * Existence: it is bound to exist otherwise the overall progress
+ * ratio would be inconsistent: with positive buffers to write (t1 & t2)
+ * and already written buffers (w1 & w2), we have:
+ *
+ * If w1/t1 > (w1+w2)/(t1+t2) # one table space is in advance
+ * => w1t1+w1t2 > w1t1+w2t1 => w1t2 > w2t1 => w1t2+w2t2 > w2t1+w2t2
+ * => (w1+w2) / (t1+t2) > w2 / t2 # the other one is late
+ *
+ * The round robin ensures that each space is given some attention
+ * till it is over the current ratio, before going to the next.
+ *
+ * Precision: using int32 computations for comparing fractions
+ * (w1 / t1 > w / t <=> w1 t > w t1) seems a bad idea as the values
+ * can overflow 32-bit integers: the limit would be sqrt(2**31) ~
+ * 46340 buffers, i.e. a 362 MB checkpoint. So ensure that 64-bit
+ * integers are used in the comparison.
+ */
+ while (spcStatus[space].done ||
+ /* compare tablespace vs overall progress ratio:
+ * tablespace written/to_write > overall written/to_write
+ */
+ (int64) spcStatus[space].num_written * num_to_write >
+ (int64) num_written * spcStatus[space].num_to_write)
+ space = (space + 1) % nb_spaces; /* round robin */
+
+ /*
+ * Find a valid buffer in the selected tablespace,
+ * by continuing the tablespace specific buffer scan
+ * where it was left.
+ */
+ index = spcStatus[space].index;
+
+ while (index < num_to_write && bufHdr == NULL)
+ {
+ buf_id = CheckpointBufferIds[index];
+ bufHdr = GetBufferDescriptor(buf_id);
+
+ /* Skip if in another tablespace or not in checkpoint anymore.
+ * No lock is acquired, see comments below.
+ */
+ if (spcStatus[space].space != bufHdr->tag.rnode.spcNode ||
+ ! (bufHdr->flags & BM_CHECKPOINT_NEEDED))
+ {
+ index ++;
+ buf_id = -1;
+ bufHdr = NULL;
+ }
+ }
+
+ /* Update tablespace writing status, will start over at next index */
+ spcStatus[space].index = index+1;
/*
* We don't need to acquire the lock here, because we're only looking
@@ -1660,39 +1894,49 @@ BufferSync(int flags)
* write the buffer though we didn't need to. It doesn't seem worth
* guarding against this, though.
*/
- if (bufHdr->flags & BM_CHECKPOINT_NEEDED)
+ if (bufHdr != NULL && bufHdr->flags & BM_CHECKPOINT_NEEDED)
{
- if (SyncOneBuffer(buf_id, false) & BUF_WRITTEN)
+ if (SyncOneBuffer(buf_id, false, checkpoint_flush_to_disk,
+ &spcContext[space]) & BUF_WRITTEN)
{
TRACE_POSTGRESQL_BUFFER_SYNC_WRITTEN(buf_id);
BgWriterStats.m_buf_written_checkpoints++;
+ spcStatus[space].num_written++;
num_written++;
/*
- * We know there are at most num_to_write buffers with
- * BM_CHECKPOINT_NEEDED set; so we can stop scanning if
- * num_written reaches num_to_write.
- *
- * Note that num_written doesn't include buffers written by
- * other backends, or by the bgwriter cleaning scan. That
- * means that the estimate of how much progress we've made is
- * conservative, and also that this test will often fail to
- * trigger. But it seems worth making anyway.
- */
- if (num_written >= num_to_write)
- break;
-
- /*
* Sleep to throttle our I/O rate.
*/
- CheckpointWriteDelay(flags, (double) num_written / num_to_write);
+ CheckpointWriteDelay(flags, (double) num_written / num_to_write,
+ spcContext, nb_spaces);
}
}
- if (++buf_id >= NBuffers)
- buf_id = 0;
+ /*
+ * Detect checkpoint end for a tablespace: either the scan is done
+ * or all tablespace buffers have been written out.
+ */
+ if (spcStatus[space].index >= num_to_write ||
+ spcStatus[space].num_written >= spcStatus[space].num_to_write)
+ {
+
+#if defined(HAVE_SYNC_FILE_RANGE) || defined(HAVE_POSIX_FADVISE)
+
+ PerformFileFlush(&spcContext[space]);
+ ResetFileFlushContext(&spcContext[space]);
+
+#endif /* HAVE_SYNC_FILE_RANGE || HAVE_POSIX_FADVISE */
+
+ spcStatus[space].done = true;
+ active_spaces--;
+ }
}
+ pfree(spcStatus);
+ spcStatus = NULL;
+ pfree(spcContext);
+ spcContext = NULL;
+
/*
* Update checkpoint statistics. As noted above, this doesn't include
* buffers written by other backends or bgwriter scan.
@@ -1939,7 +2183,8 @@ BgBufferSync(void)
/* Execute the LRU scan */
while (num_to_scan > 0 && reusable_buffers < upcoming_alloc_est)
{
- int buffer_state = SyncOneBuffer(next_to_clean, true);
+ int buffer_state =
+ SyncOneBuffer(next_to_clean, true, false, NULL);
if (++next_to_clean >= NBuffers)
{
@@ -2016,7 +2261,8 @@ BgBufferSync(void)
* Note: caller must have done ResourceOwnerEnlargeBuffers.
*/
static int
-SyncOneBuffer(int buf_id, bool skip_recently_used)
+SyncOneBuffer(int buf_id, bool skip_recently_used, bool flush_to_disk,
+ FileFlushContext * context)
{
volatile BufferDesc *bufHdr = GetBufferDescriptor(buf_id);
int result = 0;
@@ -2057,7 +2303,7 @@ SyncOneBuffer(int buf_id, bool skip_recently_used)
PinBuffer_Locked(bufHdr);
LWLockAcquire(bufHdr->content_lock, LW_SHARED);
- FlushBuffer(bufHdr, NULL);
+ FlushBuffer(bufHdr, NULL, flush_to_disk, context);
LWLockRelease(bufHdr->content_lock);
UnpinBuffer(bufHdr, true);
@@ -2319,9 +2565,16 @@ BufferGetTag(Buffer buffer, RelFileNode *rnode, ForkNumber *forknum,
*
* If the caller has an smgr reference for the buffer's relation, pass it
* as the second parameter. If not, pass NULL.
+ *
+ * The third parameter tries to hint the OS that a high priority write is meant,
+ * possibly because io-throttling is already managed elsewhere.
+ * The last parameter holds the current flush context that accumulates flush
+ * requests to be performed in one call, instead of being performed on a buffer
+ * per buffer basis.
*/
static void
-FlushBuffer(volatile BufferDesc *buf, SMgrRelation reln)
+FlushBuffer(volatile BufferDesc *buf, SMgrRelation reln, bool flush_to_disk,
+ FileFlushContext * context)
{
XLogRecPtr recptr;
ErrorContextCallback errcallback;
@@ -2410,7 +2663,9 @@ FlushBuffer(volatile BufferDesc *buf, SMgrRelation reln)
buf->tag.forkNum,
buf->tag.blockNum,
bufToWrite,
- false);
+ false,
+ flush_to_disk,
+ context);
if (track_io_timing)
{
@@ -2830,7 +3085,9 @@ FlushRelationBuffers(Relation rel)
bufHdr->tag.forkNum,
bufHdr->tag.blockNum,
localpage,
- false);
+ false,
+ false,
+ NULL);
bufHdr->flags &= ~(BM_DIRTY | BM_JUST_DIRTIED);
@@ -2864,7 +3121,7 @@ FlushRelationBuffers(Relation rel)
{
PinBuffer_Locked(bufHdr);
LWLockAcquire(bufHdr->content_lock, LW_SHARED);
- FlushBuffer(bufHdr, rel->rd_smgr);
+ FlushBuffer(bufHdr, rel->rd_smgr, false, NULL);
LWLockRelease(bufHdr->content_lock);
UnpinBuffer(bufHdr, true);
}
@@ -2916,7 +3173,7 @@ FlushDatabaseBuffers(Oid dbid)
{
PinBuffer_Locked(bufHdr);
LWLockAcquire(bufHdr->content_lock, LW_SHARED);
- FlushBuffer(bufHdr, NULL);
+ FlushBuffer(bufHdr, NULL, false, NULL);
LWLockRelease(bufHdr->content_lock);
UnpinBuffer(bufHdr, true);
}
diff --git a/src/backend/storage/buffer/localbuf.c b/src/backend/storage/buffer/localbuf.c
index 3144afe..114a0a6 100644
--- a/src/backend/storage/buffer/localbuf.c
+++ b/src/backend/storage/buffer/localbuf.c
@@ -208,7 +208,9 @@ LocalBufferAlloc(SMgrRelation smgr, ForkNumber forkNum, BlockNumber blockNum,
bufHdr->tag.forkNum,
bufHdr->tag.blockNum,
localpage,
- false);
+ false,
+ false,
+ NULL);
/* Mark not-dirty now in case we error out below */
bufHdr->flags &= ~BM_DIRTY;
diff --git a/src/backend/storage/file/buffile.c b/src/backend/storage/file/buffile.c
index ea4d689..fb3b383 100644
--- a/src/backend/storage/file/buffile.c
+++ b/src/backend/storage/file/buffile.c
@@ -317,7 +317,7 @@ BufFileDumpBuffer(BufFile *file)
return; /* seek failed, give up */
file->offsets[file->curFile] = file->curOffset;
}
- bytestowrite = FileWrite(thisfile, file->buffer + wpos, bytestowrite);
+ bytestowrite = FileWrite(thisfile, file->buffer + wpos, bytestowrite, false, NULL);
if (bytestowrite <= 0)
return; /* failed to write */
file->offsets[file->curFile] += bytestowrite;
diff --git a/src/backend/storage/file/fd.c b/src/backend/storage/file/fd.c
index 1ba4946..daf03e4 100644
--- a/src/backend/storage/file/fd.c
+++ b/src/backend/storage/file/fd.c
@@ -1344,8 +1344,97 @@ retry:
return returnCode;
}
+#if defined(HAVE_SYNC_FILE_RANGE) || defined(HAVE_POSIX_FADVISE)
+
+void
+ResetFileFlushContext(FileFlushContext * context)
+{
+ context->fd = 0;
+ context->ncalls = 0;
+ context->offset = 0;
+ context->nbytes = 0;
+ context->filename = NULL;
+}
+
+void
+PerformFileFlush(FileFlushContext * context)
+{
+ if (context->ncalls != 0)
+ {
+ int rc;
+
+#if defined(HAVE_SYNC_FILE_RANGE)
+
+ /* Linux: tell the memory manager to move these blocks to io so
+ * that they are considered for being actually written to disk.
+ */
+ rc = sync_file_range(context->fd, context->offset, context->nbytes,
+ SYNC_FILE_RANGE_WRITE);
+
+#elif defined(HAVE_POSIX_FADVISE)
+
+ /* Others: say that data should not be kept in memory...
+ * This is not exactly what we want to say, because we want to write
+ * the data for durability but we may need it later nevertheless.
+ * It seems that Linux would free the memory *if* the data has
+ * already been written do disk, else the "dontneed" call is ignored.
+ * For FreeBSD this may have the desired effect of moving the
+ * data to the io layer, although the system does not seem to
+ * take into account the provided offset & size, so it is rather
+ * rough...
+ */
+ rc = posix_fadvise(context->fd, context->offset, context->nbytes,
+ POSIX_FADV_DONTNEED);
+
+#endif
+
+ if (rc < 0)
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not flush block " INT64_FORMAT
+ " on " INT64_FORMAT " blocks in file \"%s\": %m",
+ context->offset / BLCKSZ,
+ context->nbytes / BLCKSZ,
+ context->filename)));
+ }
+}
+
+void
+FileAsynchronousFlush(FileFlushContext * context,
+ int fd, off_t offset, off_t nbytes, char * filename)
+{
+ if (context->ncalls != 0 && context->fd == fd)
+ {
+ /* Same file: merge current flush with previous ones */
+ off_t new_offset = offset < context->offset? offset: context->offset;
+
+ context->nbytes =
+ (context->offset + context->nbytes > offset + nbytes ?
+ context->offset + context->nbytes : offset + nbytes) -
+ new_offset;
+ context->offset = new_offset;
+ context->ncalls ++;
+ }
+ else
+ {
+ /* file has changed; actually flush previous file before restarting
+ * to accumulate flushes
+ */
+ PerformFileFlush(context);
+
+ context->fd = fd;
+ context->ncalls = 1;
+ context->offset = offset;
+ context->nbytes = nbytes;
+ context->filename = filename;
+ }
+}
+
+#endif /* HAVE_SYNC_FILE_RANGE || HAVE_POSIX_FADVISE */
+
int
-FileWrite(File file, char *buffer, int amount)
+FileWrite(File file, char *buffer, int amount, bool flush_to_disk,
+ FileFlushContext * context)
{
int returnCode;
@@ -1395,6 +1484,28 @@ retry:
if (returnCode >= 0)
{
+
+#if defined(HAVE_SYNC_FILE_RANGE) || defined(HAVE_POSIX_FADVISE)
+
+ /*
+ * Calling "write" tells the OS that pg wants to write some page to disk,
+ * however when it is really done is chosen by the OS.
+ * Depending on other disk activities this may be delayed significantly,
+ * maybe up to an "fsync" call, which could induce an IO write surge.
+ * When checkpointing pg is doing its own throttling and the result
+ * should really be written to disk with high priority, so as to meet
+ * the completion target.
+ * This call hints that such write have a higher priority.
+ */
+ if (flush_to_disk && returnCode == amount && errno == 0)
+ {
+ FileAsynchronousFlush(context,
+ VfdCache[file].fd, VfdCache[file].seekPos,
+ amount, VfdCache[file].fileName);
+ }
+
+#endif /* HAVE_SYNC_FILE_RANGE || HAVE_POSIX_FADVISE */
+
VfdCache[file].seekPos += returnCode;
/* maintain fileSize and temporary_files_size if it's a temp file */
diff --git a/src/backend/storage/smgr/md.c b/src/backend/storage/smgr/md.c
index 42a43bb..dbf057f 100644
--- a/src/backend/storage/smgr/md.c
+++ b/src/backend/storage/smgr/md.c
@@ -531,7 +531,7 @@ mdextend(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
errmsg("could not seek to block %u in file \"%s\": %m",
blocknum, FilePathName(v->mdfd_vfd))));
- if ((nbytes = FileWrite(v->mdfd_vfd, buffer, BLCKSZ)) != BLCKSZ)
+ if ((nbytes = FileWrite(v->mdfd_vfd, buffer, BLCKSZ, false, NULL)) != BLCKSZ)
{
if (nbytes < 0)
ereport(ERROR,
@@ -738,7 +738,8 @@ mdread(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
*/
void
mdwrite(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
- char *buffer, bool skipFsync)
+ char *buffer, bool skipFsync, bool flush_to_disk,
+ FileFlushContext * context)
{
off_t seekpos;
int nbytes;
@@ -767,7 +768,7 @@ mdwrite(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
errmsg("could not seek to block %u in file \"%s\": %m",
blocknum, FilePathName(v->mdfd_vfd))));
- nbytes = FileWrite(v->mdfd_vfd, buffer, BLCKSZ);
+ nbytes = FileWrite(v->mdfd_vfd, buffer, BLCKSZ, flush_to_disk, context);
TRACE_POSTGRESQL_SMGR_MD_WRITE_DONE(forknum, blocknum,
reln->smgr_rnode.node.spcNode,
diff --git a/src/backend/storage/smgr/smgr.c b/src/backend/storage/smgr/smgr.c
index 244b4ea..2db3cd3 100644
--- a/src/backend/storage/smgr/smgr.c
+++ b/src/backend/storage/smgr/smgr.c
@@ -52,7 +52,8 @@ typedef struct f_smgr
void (*smgr_read) (SMgrRelation reln, ForkNumber forknum,
BlockNumber blocknum, char *buffer);
void (*smgr_write) (SMgrRelation reln, ForkNumber forknum,
- BlockNumber blocknum, char *buffer, bool skipFsync);
+ BlockNumber blocknum, char *buffer, bool skipFsync,
+ bool flush_to_disk, FileFlushContext *context);
BlockNumber (*smgr_nblocks) (SMgrRelation reln, ForkNumber forknum);
void (*smgr_truncate) (SMgrRelation reln, ForkNumber forknum,
BlockNumber nblocks);
@@ -643,10 +644,11 @@ smgrread(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
*/
void
smgrwrite(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
- char *buffer, bool skipFsync)
+ char *buffer, bool skipFsync, bool flush_to_disk,
+ FileFlushContext * context)
{
(*(smgrsw[reln->smgr_which].smgr_write)) (reln, forknum, blocknum,
- buffer, skipFsync);
+ buffer, skipFsync, flush_to_disk, context);
}
/*
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index 1bed525..80d9a3e 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -158,6 +158,7 @@ static bool check_bonjour(bool *newval, void **extra, GucSource source);
static bool check_ssl(bool *newval, void **extra, GucSource source);
static bool check_stage_log_stats(bool *newval, void **extra, GucSource source);
static bool check_log_stats(bool *newval, void **extra, GucSource source);
+static bool check_flush_to_disk(bool *newval, void **extra, GucSource source);
static bool check_canonical_path(char **newval, void **extra, GucSource source);
static bool check_timezone_abbreviations(char **newval, void **extra, GucSource source);
static void assign_timezone_abbreviations(const char *newval, void *extra);
@@ -1013,6 +1014,27 @@ static struct config_bool ConfigureNamesBool[] =
false,
NULL, NULL, NULL
},
+
+ {
+ {"checkpoint_sort", PGC_SIGHUP, WAL_CHECKPOINTS,
+ gettext_noop("Whether disk-page buffers are sorted on checkpoints."),
+ NULL
+ },
+ &checkpoint_sort,
+ true,
+ NULL, NULL, NULL
+ },
+
+ {
+ {"checkpoint_flush_to_disk", PGC_SIGHUP, WAL_CHECKPOINTS,
+ gettext_noop("Hint that checkpoint's writes are high priority."),
+ NULL
+ },
+ &checkpoint_flush_to_disk,
+ false,
+ check_flush_to_disk, NULL, NULL
+ },
+
{
{"log_connections", PGC_SU_BACKEND, LOGGING_WHAT,
gettext_noop("Logs each successful connection."),
@@ -9806,6 +9828,21 @@ check_log_stats(bool *newval, void **extra, GucSource source)
}
static bool
+check_flush_to_disk(bool *newval, void **extra, GucSource source)
+{
+/* This test must be consistent with the one in FileWrite (storage/file/fd.c)
+ */
+#if ! (defined(HAVE_SYNC_FILE_RANGE) || defined(HAVE_POSIX_FADVISE))
+ /* just warn if it has no effect */
+ ereport(WARNING,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("Setting \"checkpoint_flush_to_disk\" has no effect "
+ "on this platform.")));
+#endif /* HAVE_SYNC_FILE_RANGE */
+ return true;
+}
+
+static bool
check_canonical_path(char **newval, void **extra, GucSource source)
{
/*
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 06dfc06..ae7f7cb 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -202,6 +202,8 @@
#max_wal_size = 1GB
#min_wal_size = 80MB
#checkpoint_completion_target = 0.5 # checkpoint target duration, 0.0 - 1.0
+#checkpoint_sort = on # sort buffers on checkpoint
+#checkpoint_flush_to_disk = off # send buffers to disk on checkpoint
#checkpoint_warning = 30s # 0 disables
# - Archiving -
diff --git a/src/include/access/xlog.h b/src/include/access/xlog.h
index 6dacee2..dbd4757 100644
--- a/src/include/access/xlog.h
+++ b/src/include/access/xlog.h
@@ -186,6 +186,8 @@ extern bool XLOG_DEBUG;
typedef struct CheckpointStatsData
{
TimestampTz ckpt_start_t; /* start of checkpoint */
+ TimestampTz ckpt_sort_t; /* start buffer sorting */
+ TimestampTz ckpt_sort_end_t; /* end of sorting */
TimestampTz ckpt_write_t; /* start of flushing buffers */
TimestampTz ckpt_sync_t; /* start of fsyncs */
TimestampTz ckpt_sync_end_t; /* end of fsyncs */
diff --git a/src/include/postmaster/bgwriter.h b/src/include/postmaster/bgwriter.h
index a49c208..f9c8ca1 100644
--- a/src/include/postmaster/bgwriter.h
+++ b/src/include/postmaster/bgwriter.h
@@ -16,6 +16,7 @@
#define _BGWRITER_H
#include "storage/block.h"
+#include "storage/fd.h"
#include "storage/relfilenode.h"
@@ -29,7 +30,8 @@ extern void BackgroundWriterMain(void) pg_attribute_noreturn();
extern void CheckpointerMain(void) pg_attribute_noreturn();
extern void RequestCheckpoint(int flags);
-extern void CheckpointWriteDelay(int flags, double progress);
+extern void CheckpointWriteDelay(int flags, double progress,
+ FileFlushContext * context, int ctx_size);
extern bool ForwardFsyncRequest(RelFileNode rnode, ForkNumber forknum,
BlockNumber segno);
diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.h
index ec0a254..db0e2c3 100644
--- a/src/include/storage/bufmgr.h
+++ b/src/include/storage/bufmgr.h
@@ -54,6 +54,8 @@ extern int bgwriter_lru_maxpages;
extern double bgwriter_lru_multiplier;
extern bool track_io_timing;
extern int target_prefetch_pages;
+extern bool checkpoint_flush_to_disk;
+extern bool checkpoint_sort;
/* in buf_init.c */
extern PGDLLIMPORT char *BufferBlocks;
diff --git a/src/include/storage/fd.h b/src/include/storage/fd.h
index 7eabe09..c740ee7 100644
--- a/src/include/storage/fd.h
+++ b/src/include/storage/fd.h
@@ -59,6 +59,22 @@ extern int max_files_per_process;
*/
extern int max_safe_fds;
+/* FileFlushContext:
+ * This structure is used to accumulate several flush requests on a file
+ * into a larger flush request.
+ * - fd: file descriptor of the file
+ * - ncalls: number of flushes merged together
+ * - offset: starting offset (minimum of all offset)
+ * - nbytes: size (minimum extent to cover all flushed data)
+ * - filename: filename of fd for error messages
+ */
+typedef struct FileFlushContext{
+ int fd;
+ int ncalls;
+ off_t offset;
+ off_t nbytes;
+ char * filename;
+} FileFlushContext;
/*
* prototypes for functions in fd.c
@@ -70,7 +86,12 @@ extern File OpenTemporaryFile(bool interXact);
extern void FileClose(File file);
extern int FilePrefetch(File file, off_t offset, int amount);
extern int FileRead(File file, char *buffer, int amount);
-extern int FileWrite(File file, char *buffer, int amount);
+extern void ResetFileFlushContext(FileFlushContext * context);
+extern void PerformFileFlush(FileFlushContext * context);
+extern void FileAsynchronousFlush(FileFlushContext * context,
+ int fd, off_t offset, off_t nbytes, char * filename);
+extern int FileWrite(File file, char *buffer, int amount, bool flush_to_disk,
+ FileFlushContext * context);
extern int FileSync(File file);
extern off_t FileSeek(File file, off_t offset, int whence);
extern int FileTruncate(File file, off_t offset);
diff --git a/src/include/storage/smgr.h b/src/include/storage/smgr.h
index 69a624f..a46a70c 100644
--- a/src/include/storage/smgr.h
+++ b/src/include/storage/smgr.h
@@ -16,6 +16,7 @@
#include "fmgr.h"
#include "storage/block.h"
+#include "storage/fd.h"
#include "storage/relfilenode.h"
@@ -95,7 +96,8 @@ extern void smgrprefetch(SMgrRelation reln, ForkNumber forknum,
extern void smgrread(SMgrRelation reln, ForkNumber forknum,
BlockNumber blocknum, char *buffer);
extern void smgrwrite(SMgrRelation reln, ForkNumber forknum,
- BlockNumber blocknum, char *buffer, bool skipFsync);
+ BlockNumber blocknum, char *buffer, bool skipFsync,
+ bool flush_to_disk, FileFlushContext * context);
extern BlockNumber smgrnblocks(SMgrRelation reln, ForkNumber forknum);
extern void smgrtruncate(SMgrRelation reln, ForkNumber forknum,
BlockNumber nblocks);
@@ -120,8 +122,9 @@ extern void mdprefetch(SMgrRelation reln, ForkNumber forknum,
BlockNumber blocknum);
extern void mdread(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
char *buffer);
-extern void mdwrite(SMgrRelation reln, ForkNumber forknum,
- BlockNumber blocknum, char *buffer, bool skipFsync);
+extern void mdwrite(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
+ char *buffer, bool skipFsync, bool flush_to_disk,
+ FileFlushContext * context);
extern BlockNumber mdnblocks(SMgrRelation reln, ForkNumber forknum);
extern void mdtruncate(SMgrRelation reln, ForkNumber forknum,
BlockNumber nblocks);
On 07/26/2015 06:01 PM, Fabien COELHO wrote:
Attached is very minor v5 update which does a rebase & completes the
cleanup of doing a full sort instead of a chuncked sort.
Some thoughts on this:
* I think we should drop the "flush" part of this for now. It's not as
clearly beneficial as the sorting part, and adds a great deal more code
complexity. And it's orthogonal to the sorting patch, so we can deal
with it separately.
* Is it really necessary to parallelize the I/O among tablespaces? I can
see the point, but I wonder if it makes any difference in practice.
* Is there ever any harm in sorting the buffers? The GUC is useful for
benchmarking, but could we leave it out of the final patch?
* Do we need to worry about exceeding the 1 GB allocation limit in
AllocateCheckpointBufferIds? It's enough got 2 TB of shared_buffers.
That's a lot, but it's not totally crazy these days that someone might
do that. At the very least, we need to lower the maximum of
shared_buffers so that you can't hit that limit.
I ripped out the "flushing" part, keeping only the sorting. I refactored
the logic in BufferSync() a bit. There's now a separate function,
nextCheckpointBuffer(), that returns the next buffer ID from the sorted
list. The tablespace-parallelization behaviour in encapsulated there,
keeping the code in BufferSync() much simpler. See attached. Needs some
minor cleanup and commenting still before committing, and I haven't done
any testing besides a simple "make check".
- Heikki
Attachments:
checkpoint-sort-heikki-1.patchapplication/x-patch; name=checkpoint-sort-heikki-1.patchDownload
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index e900dcc..1cec243 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -2454,6 +2454,28 @@ include_dir 'conf.d'
</listitem>
</varlistentry>
+ <varlistentry id="guc-checkpoint-sort" xreflabel="checkpoint_sort">
+ <term><varname>checkpoint_sort</varname> (<type>bool</type>)
+ <indexterm>
+ <primary><varname>checkpoint_sort</> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ Whether to sort buffers before writting them out to disk on checkpoint.
+ For a HDD storage, this setting allows to group together
+ neighboring pages written to disk, thus improving performance by
+ reducing random write activity.
+ This sorting should have limited performance effects on SSD backends
+ as such storages have good random write performance, but it may
+ help with wear-leveling so be worth keeping anyway.
+ The default is <literal>on</>.
+ This parameter can only be set in the <filename>postgresql.conf</>
+ file or on the server command line.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="guc-checkpoint-warning" xreflabel="checkpoint_warning">
<term><varname>checkpoint_warning</varname> (<type>integer</type>)
<indexterm>
diff --git a/doc/src/sgml/wal.sgml b/doc/src/sgml/wal.sgml
index e3941c9..f538698 100644
--- a/doc/src/sgml/wal.sgml
+++ b/doc/src/sgml/wal.sgml
@@ -546,6 +546,18 @@
</para>
<para>
+ When hard-disk drives (HDD) are used for terminal data storage
+ <xref linkend="guc-checkpoint-sort"> allows to sort pages
+ so that neighboring pages on disk will be flushed together by
+ chekpoints, reducing the random write load and improving performance.
+ If solid-state drives (SSD) are used, sorting pages induces no benefit
+ as their random write I/O performance is good: this feature could then
+ be disabled by setting <varname>checkpoint_sort</> to <value>off</>.
+ It is possible that sorting may help with SSD wear leveling, so it may
+ be kept on that account.
+ </para>
+
+ <para>
The number of WAL segment files in <filename>pg_xlog</> directory depends on
<varname>min_wal_size</>, <varname>max_wal_size</> and
the amount of WAL generated in previous checkpoint cycles. When old log
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 68e33eb..bee38ab 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -7995,11 +7995,13 @@ LogCheckpointEnd(bool restartpoint)
sync_secs,
total_secs,
longest_secs,
+ sort_secs,
average_secs;
int write_usecs,
sync_usecs,
total_usecs,
longest_usecs,
+ sort_usecs,
average_usecs;
uint64 average_sync_time;
@@ -8030,6 +8032,10 @@ LogCheckpointEnd(bool restartpoint)
CheckpointStats.ckpt_end_t,
&total_secs, &total_usecs);
+ TimestampDifference(CheckpointStats.ckpt_sort_t,
+ CheckpointStats.ckpt_sort_end_t,
+ &sort_secs, &sort_usecs);
+
/*
* Timing values returned from CheckpointStats are in microseconds.
* Convert to the second plus microsecond form that TimestampDifference
@@ -8048,8 +8054,8 @@ LogCheckpointEnd(bool restartpoint)
elog(LOG, "%s complete: wrote %d buffers (%.1f%%); "
"%d transaction log file(s) added, %d removed, %d recycled; "
- "write=%ld.%03d s, sync=%ld.%03d s, total=%ld.%03d s; "
- "sync files=%d, longest=%ld.%03d s, average=%ld.%03d s; "
+ "sort=%ld.%03d s, write=%ld.%03d s, sync=%ld.%03d s, total=%ld.%03d s;"
+ " sync files=%d, longest=%ld.%03d s, average=%ld.%03d s; "
"distance=%d kB, estimate=%d kB",
restartpoint ? "restartpoint" : "checkpoint",
CheckpointStats.ckpt_bufs_written,
@@ -8057,6 +8063,7 @@ LogCheckpointEnd(bool restartpoint)
CheckpointStats.ckpt_segs_added,
CheckpointStats.ckpt_segs_removed,
CheckpointStats.ckpt_segs_recycled,
+ sort_secs, sort_usecs / 1000,
write_secs, write_usecs / 1000,
sync_secs, sync_usecs / 1000,
total_secs, total_usecs / 1000,
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index e4b25587..084bbfb 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -80,6 +80,7 @@ bool zero_damaged_pages = false;
int bgwriter_lru_maxpages = 100;
double bgwriter_lru_multiplier = 2.0;
bool track_io_timing = false;
+bool checkpoint_sort = true;
/*
* How many buffers PrefetchBuffer callers should try to stay ahead of their
@@ -1562,6 +1563,101 @@ UnpinBuffer(volatile BufferDesc *buf, bool fixOwner)
}
/*
+ * Array of buffer ids of all buffers to checkpoint.
+ */
+static int *CheckpointBufferIds = NULL;
+
+/* Compare checkpoint buffers
+ */
+static int bufcmp(const int * pa, const int * pb)
+{
+ BufferDesc
+ *a = GetBufferDescriptor(*pa),
+ *b = GetBufferDescriptor(*pb);
+
+ /* tag: rnode, forkNum (different files), blockNum
+ * rnode: { spcNode (ignore: not really needed),
+ * dbNode (ignore: this is a directory), relNode }
+ * spcNode: table space oid, not that there are at least two
+ * (pg_global and pg_default).
+ */
+ /* compare relation */
+ if (a->tag.rnode.spcNode < b->tag.rnode.spcNode)
+ return -1;
+ else if (a->tag.rnode.spcNode > b->tag.rnode.spcNode)
+ return 1;
+ if (a->tag.rnode.relNode < b->tag.rnode.relNode)
+ return -1;
+ else if (a->tag.rnode.relNode > b->tag.rnode.relNode)
+ return 1;
+ /* same relation, compare fork */
+ else if (a->tag.forkNum < b->tag.forkNum)
+ return -1;
+ else if (a->tag.forkNum > b->tag.forkNum)
+ return 1;
+ /* same relation/fork, so same segmented "file", compare block number
+ * which are mapped on different segments depending on the number.
+ */
+ else if (a->tag.blockNum < b->tag.blockNum)
+ return -1;
+ else /* should not be the same block anyway... */
+ return 1;
+}
+
+static void
+AllocateCheckpointBufferIds(void)
+{
+ /* Safe worst case allocation, all buffers belong to the checkpoint...
+ * that is pretty unlikely.
+ */
+ CheckpointBufferIds = (int *) palloc(sizeof(int) * NBuffers);
+}
+
+/* Status of buffers to checkpoint for a particular tablespace,
+ * used internally in BufferSync.
+ * - spcNone: oid of the tablespace
+ * - num_to_write: number of pages counted for this tablespace
+ * - num_written: number of pages actually written out
+ * - index: scanning position in CheckpointBufferIds for this tablespace
+ */
+typedef struct TableSpaceCheckpointStatus {
+ int index;
+ int index_end;
+} TableSpaceCheckpointStatus;
+
+static int allocatedSpc = 0;
+static TableSpaceCheckpointStatus *spcStatus = NULL;
+static int numSpc;
+static int currSpc;
+
+static int
+nextCheckpointBuffer(void)
+{
+ int result;
+
+ if (numSpc == 0)
+ return -1;
+
+ currSpc = (currSpc + 1) % numSpc;
+
+ result = CheckpointBufferIds[spcStatus[currSpc].index];
+ spcStatus[currSpc].index++;
+
+ if (spcStatus[currSpc].index == spcStatus[currSpc].index_end)
+ {
+ if (currSpc < numSpc - 1)
+ {
+ TableSpaceCheckpointStatus tmp = spcStatus[currSpc];
+ spcStatus[currSpc] = spcStatus[numSpc - 1];
+ spcStatus[numSpc - 1] = tmp;
+ }
+ numSpc--;
+ }
+
+ return result;
+}
+
+/*
* BufferSync -- Write out all dirty buffers in the pool.
*
* This is called at checkpoint time to write out all dirty shared buffers.
@@ -1575,11 +1671,17 @@ static void
BufferSync(int flags)
{
int buf_id;
- int num_to_scan;
int num_to_write;
int num_written;
int mask = BM_DIRTY;
+ /*
+ * Lazy allocation: this function is called through the checkpointer,
+ * but also by initdb. Maybe the allocation could be moved to the callers.
+ */
+ if (CheckpointBufferIds == NULL)
+ AllocateCheckpointBufferIds();
+
/* Make sure we can handle the pin inside SyncOneBuffer */
ResourceOwnerEnlargeBuffers(CurrentResourceOwner);
@@ -1622,6 +1724,7 @@ BufferSync(int flags)
if ((bufHdr->flags & mask) == mask)
{
bufHdr->flags |= BM_CHECKPOINT_NEEDED;
+ CheckpointBufferIds[num_to_write] = buf_id;
num_to_write++;
}
@@ -1633,18 +1736,97 @@ BufferSync(int flags)
TRACE_POSTGRESQL_BUFFER_SYNC_START(NBuffers, num_to_write);
+ /* Build checkpoint tablespace buffer status & flush context arrays */
+
+ /*
+ * Sort buffer ids to help find sequential writes.
+ *
+ * Note: buffers are not locked in anyway, but that does not matter,
+ * this sorting is really advisory, if some buffer changes status during
+ * this pass it will be filtered out later. The only necessary property
+ * is that marked buffers do not move elsewhere. Also, qsort implementation
+ * should be resilient to occasional contradictions (cmp(a,b) != -cmp(b,a))
+ * because of these possible concurrent changes.
+ */
+ CheckpointStats.ckpt_sort_t = GetCurrentTimestamp();
+
+ if (checkpoint_sort && num_to_write > 1 && false)
+ {
+ Oid lastspc;
+ Oid spc;
+ int i,
+ j;
+ volatile BufferDesc *bufHdr;
+
+ qsort(CheckpointBufferIds, num_to_write, sizeof(int),
+ (int(*)(const void *, const void *)) bufcmp);
+
+ if (allocatedSpc == 0)
+ {
+ allocatedSpc = 5;
+ spcStatus = (TableSpaceCheckpointStatus *)
+ palloc(sizeof(TableSpaceCheckpointStatus) * allocatedSpc);
+ }
+
+ bufHdr = GetBufferDescriptor(CheckpointBufferIds[0]);
+ spcStatus[0].index = 0;
+ lastspc = bufHdr->tag.rnode.spcNode;
+ j = 0;
+ for (i = 1; i < num_to_write; i++)
+ {
+ bufHdr = GetBufferDescriptor(CheckpointBufferIds[i]);
+
+ spc = bufHdr->tag.rnode.spcNode;
+ if (spc != lastspc && (bufHdr->flags & BM_CHECKPOINT_NEEDED) != 0)
+ {
+ if (allocatedSpc <= j)
+ {
+ allocatedSpc = j + 5;
+ spcStatus = (TableSpaceCheckpointStatus *)
+ repalloc(spcStatus, sizeof(TableSpaceCheckpointStatus) * allocatedSpc);
+ }
+
+ spcStatus[j].index_end = spcStatus[j + 1].index = i;
+ j++;
+ lastspc = spc;
+ }
+ }
+ spcStatus[j].index_end = num_to_write;
+ j++;
+ numSpc = j;
+ currSpc = 0;
+ }
+ else
+ {
+ if (allocatedSpc == 0)
+ {
+ allocatedSpc = 1;
+ spcStatus = (TableSpaceCheckpointStatus *)
+ palloc(sizeof(TableSpaceCheckpointStatus) * allocatedSpc);
+ }
+ spcStatus[0].index = 0;
+ spcStatus[0].index_end = num_to_write;
+ numSpc = 1;
+ currSpc = 0;
+ }
+
+ CheckpointStats.ckpt_sort_end_t = GetCurrentTimestamp();
+
/*
- * Loop over all buffers again, and write the ones (still) marked with
- * BM_CHECKPOINT_NEEDED. In this loop, we start at the clock sweep point
- * since we might as well dump soon-to-be-recycled buffers first.
+ * Loop over buffers to write through CheckpointBufferIds,
+ * and write the ones (still) marked with BM_CHECKPOINT_NEEDED,
+ * with some round robin over table spaces so as to balance writes,
+ * so that buffer writes move forward roughly proportionally for each
+ * tablespace.
*
- * Note that we don't read the buffer alloc count here --- that should be
- * left untouched till the next BgBufferSync() call.
+ * Termination: if a tablespace is selected by the inner while loop
+ * (see argument there), its index is incremented and will eventually
+ * reach num_to_write, mark this table space scanning as done and
+ * decrement the number of active spaces, which will thus reach 0.
*/
- buf_id = StrategySyncStart(NULL, NULL);
- num_to_scan = NBuffers;
num_written = 0;
- while (num_to_scan-- > 0)
+
+ while ((buf_id = nextCheckpointBuffer()) != -1)
{
volatile BufferDesc *bufHdr = GetBufferDescriptor(buf_id);
@@ -1669,28 +1851,11 @@ BufferSync(int flags)
num_written++;
/*
- * We know there are at most num_to_write buffers with
- * BM_CHECKPOINT_NEEDED set; so we can stop scanning if
- * num_written reaches num_to_write.
- *
- * Note that num_written doesn't include buffers written by
- * other backends, or by the bgwriter cleaning scan. That
- * means that the estimate of how much progress we've made is
- * conservative, and also that this test will often fail to
- * trigger. But it seems worth making anyway.
- */
- if (num_written >= num_to_write)
- break;
-
- /*
* Sleep to throttle our I/O rate.
*/
CheckpointWriteDelay(flags, (double) num_written / num_to_write);
}
}
-
- if (++buf_id >= NBuffers)
- buf_id = 0;
}
/*
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index 1b7b914..e07daca 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -1013,6 +1013,17 @@ static struct config_bool ConfigureNamesBool[] =
false,
NULL, NULL, NULL
},
+
+ {
+ {"checkpoint_sort", PGC_SIGHUP, WAL_CHECKPOINTS,
+ gettext_noop("Whether disk-page buffers are sorted on checkpoints."),
+ NULL
+ },
+ &checkpoint_sort,
+ true,
+ NULL, NULL, NULL
+ },
+
{
{"log_connections", PGC_SU_BACKEND, LOGGING_WHAT,
gettext_noop("Logs each successful connection."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index e5d275d..e84f380 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -201,6 +201,7 @@
#max_wal_size = 1GB
#min_wal_size = 80MB
#checkpoint_completion_target = 0.5 # checkpoint target duration, 0.0 - 1.0
+#checkpoint_sort = on # sort buffers on checkpoint
#checkpoint_warning = 30s # 0 disables
# - Archiving -
diff --git a/src/include/access/xlog.h b/src/include/access/xlog.h
index 6dacee2..dbd4757 100644
--- a/src/include/access/xlog.h
+++ b/src/include/access/xlog.h
@@ -186,6 +186,8 @@ extern bool XLOG_DEBUG;
typedef struct CheckpointStatsData
{
TimestampTz ckpt_start_t; /* start of checkpoint */
+ TimestampTz ckpt_sort_t; /* start buffer sorting */
+ TimestampTz ckpt_sort_end_t; /* end of sorting */
TimestampTz ckpt_write_t; /* start of flushing buffers */
TimestampTz ckpt_sync_t; /* start of fsyncs */
TimestampTz ckpt_sync_end_t; /* end of fsyncs */
diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.h
index ec0a254..c228f39 100644
--- a/src/include/storage/bufmgr.h
+++ b/src/include/storage/bufmgr.h
@@ -54,6 +54,7 @@ extern int bgwriter_lru_maxpages;
extern double bgwriter_lru_multiplier;
extern bool track_io_timing;
extern int target_prefetch_pages;
+extern bool checkpoint_sort;
/* in buf_init.c */
extern PGDLLIMPORT char *BufferBlocks;
Hello Heikki,
Thanks for having a look at the patch.
* I think we should drop the "flush" part of this for now. It's not as
clearly beneficial as the sorting part, and adds a great deal more code
complexity. And it's orthogonal to the sorting patch, so we can deal with it
separately.
I agree that it is orthogonal and that the two features could be in
distinct patches. The flush part is the first patch I really submitted
because it has significant effect on latency, and I was told to mix it
with sorting...
The flushing part really helps to keep "write stalls" under control in
many cases, for instance:
- 400-tps 1-client (or 4 for medium) max 100-ms latency
options | percent of late transactions
flush | sort | tiny | small | medium
off | off | 12.0 | 64.28 | 68.6
off | on | 11.3 | 22.05 | 22.6
on | off | 1.1 | 67.93 | 67.9
on | on | 0.6 | 3.24 | 3.1
The "percent of late transactions" is really the fraction of time the
database is unreachable because of write stalls... So sort without flush
is cleary not enough.
Another thing suggested by Andres is to fsync as early as possible, but
this is not a simple patch because is intermix things which are currently
in distinct parts of checkpoint processing, so I already decided that this
would be for another submission.
* Is it really necessary to parallelize the I/O among tablespaces? I can see
the point, but I wonder if it makes any difference in practice.
I think that if someone bothers with tablespace there is no reason to kill
them behind her. Without sorting you may hope that tablespaces will be
touched randomly enough, but once buffers are sorted you can probably find
cases where it would write on one table space and then on the other.
So I think that it really should be kept.
* Is there ever any harm in sorting the buffers? The GUC is useful for
benchmarking, but could we leave it out of the final patch?
I think that the performance show that it is basically always beneficial,
so the guc may be left out. However on SSD it is unclear to me whether it
is just a loss of time or whether it helps, say with wear-leveling. Maybe
best to keep it? Anyway it is definitely needed for testing.
* Do we need to worry about exceeding the 1 GB allocation limit in
AllocateCheckpointBufferIds? It's enough got 2 TB of shared_buffers. That's a
lot, but it's not totally crazy these days that someone might do that. At the
very least, we need to lower the maximum of shared_buffers so that you can't
hit that limit.
Yep.
I ripped out the "flushing" part, keeping only the sorting. I refactored
the logic in BufferSync() a bit. There's now a separate function,
nextCheckpointBuffer(), that returns the next buffer ID from the sorted
list. The tablespace-parallelization behaviour in encapsulated there,
I do not understand the new tablespace-parallelization logic: there is no
test about the tablespace of the buffer in the selection process... Note
that I did wrote a proof for the one I put, and also did some detailed
testing on the side because I'm always wary of proofs, especially mines:-)
I notice that you assume that table space numbers are always small and
contiguous. Is that a fact? I was feeling more at ease with relying on a
hash table to avoid such an assumption.
keeping the code in BufferSync() much simpler. See attached. Needs some
minor cleanup and commenting still before committing, and I haven't done
any testing besides a simple "make check".
Hmmm..., just another detail, the patch does not sort:
+ if (checkpoint_sort && num_to_write > 1 && false)
I'll resubmit a patch with only the sorting part, and do the kind of
restructuring you suggest which is a good thing.
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 2015-08-08 20:49:03 +0300, Heikki Linnakangas wrote:
* I think we should drop the "flush" part of this for now. It's not as
clearly beneficial as the sorting part, and adds a great deal more code
complexity. And it's orthogonal to the sorting patch, so we can deal with it
separately.
I don't agree. For one I've seen it cause rather big latency
improvements, and we're horrible at that. But more importantly I think
the requirements of the flush logic influences how exactly the sorting
is done. Splitting them will just make it harder to do the flushing in a
not too big patch.
* Is it really necessary to parallelize the I/O among tablespaces? I can see
the point, but I wonder if it makes any difference in practice.
Today it's somewhat common to have databases that are bottlenecked on
write IO and all those writes being done by the checkpointer. If we
suddenly do the writes to individual tablespaces separately and
sequentially we'll be bottlenecked on the peak IO of a single
tablespace.
* Is there ever any harm in sorting the buffers? The GUC is useful for
benchmarking, but could we leave it out of the final patch?
Agreed.
* Do we need to worry about exceeding the 1 GB allocation limit in
AllocateCheckpointBufferIds? It's enough got 2 TB of shared_buffers. That's
a lot, but it's not totally crazy these days that someone might do that. At
the very least, we need to lower the maximum of shared_buffers so that you
can't hit that limit.
We can just use the _huge variant?
Greetings,
Andres Freund
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Hi,
On 2015-08-08 20:49:03 +0300, Heikki Linnakangas wrote:
I ripped out the "flushing" part, keeping only the sorting. I refactored the
logic in BufferSync() a bit. There's now a separate function,
nextCheckpointBuffer(), that returns the next buffer ID from the sorted
list. The tablespace-parallelization behaviour in encapsulated there,
keeping the code in BufferSync() much simpler. See attached. Needs some
minor cleanup and commenting still before committing, and I haven't done any
testing besides a simple "make check".
Thought it'd be useful to review the current version as well. Some of
what I'm commenting on you'll probably already have though of under the
label of "minor cleanup".
/* + * Array of buffer ids of all buffers to checkpoint. + */ +static int *CheckpointBufferIds = NULL; + +/* Compare checkpoint buffers + */
Should be at the beginning of the file. There's a bunch more cases of that.
+/* Compare checkpoint buffers + */ +static int bufcmp(const int * pa, const int * pb) +{ + BufferDesc + *a = GetBufferDescriptor(*pa), + *b = GetBufferDescriptor(*pb); + + /* tag: rnode, forkNum (different files), blockNum + * rnode: { spcNode (ignore: not really needed), + * dbNode (ignore: this is a directory), relNode } + * spcNode: table space oid, not that there are at least two + * (pg_global and pg_default). + */ + /* compare relation */ + if (a->tag.rnode.spcNode < b->tag.rnode.spcNode) + return -1; + else if (a->tag.rnode.spcNode > b->tag.rnode.spcNode) + return 1; + if (a->tag.rnode.relNode < b->tag.rnode.relNode) + return -1; + else if (a->tag.rnode.relNode > b->tag.rnode.relNode) + return 1; + /* same relation, compare fork */ + else if (a->tag.forkNum < b->tag.forkNum) + return -1; + else if (a->tag.forkNum > b->tag.forkNum) + return 1; + /* same relation/fork, so same segmented "file", compare block number + * which are mapped on different segments depending on the number. + */ + else if (a->tag.blockNum < b->tag.blockNum) + return -1; + else /* should not be the same block anyway... */ + return 1; +}
This definitely needs comments about ignoring the normal buffer header
locking.
Why are we ignoring the database directory? I doubt it'll make a huge
difference, but grouping metadata affecting operations by directory
helps.
+ +static void +AllocateCheckpointBufferIds(void) +{ + /* Safe worst case allocation, all buffers belong to the checkpoint... + * that is pretty unlikely. + */ + CheckpointBufferIds = (int *) palloc(sizeof(int) * NBuffers); +}
(wrong comment style...)
Heikki, you were concerned about the size of the allocation of this,
right? I don't think it's relevant - we used to allocate an array of
that size for the backend's private buffer pin array until 9.5, so in
theory we should be safe agains that. NBuffers is limited to INT_MAX/2
in guc.ċ, which ought to be sufficient?
+ /* + * Lazy allocation: this function is called through the checkpointer, + * but also by initdb. Maybe the allocation could be moved to the callers. + */ + if (CheckpointBufferIds == NULL) + AllocateCheckpointBufferIds(); +
I don't think it's a good idea to allocate this on every round. That
just means a lot of page table entries have to be built and torn down
regularly. It's not like checkpoints only run for 1% of the time or
such.
FWIW, I still think it's a much better idea to allocate the memory once
in shared buffers. It's not like that makes us need more memory overall,
and it'll be huge page allocations if configured. I also think that
sooner rather than later we're going to need more than one process
flushing buffers, and then it'll need to be moved there.
+ /* + * Sort buffer ids to help find sequential writes. + * + * Note: buffers are not locked in anyway, but that does not matter, + * this sorting is really advisory, if some buffer changes status during + * this pass it will be filtered out later. The only necessary property + * is that marked buffers do not move elsewhere. + */
That reasoning makes it impossible to move the fsyncing of files into
the loop (whenever we move to a new file). That's not nice. The
formulation with "necessary property" doesn't seem very clear to me?
How about:
/*
* Note: Buffers are not locked in any way during sorting, but that's ok:
* A change in the buffer header is only relevant when it changes the
* buffer's identity. If the identity has changed it'll have been
* written out by BufferAlloc(), so there's no need for checkpointer to
* write it out anymore. The buffer might also get written out by a
* backend or bgwriter, but that's equally harmless.
*/
Also, qsort implementation + * should be resilient to occasional contradictions (cmp(a,b) != -cmp(b,a)) + * because of these possible concurrent changes.
Hm. Is that actually the case for our qsort implementation? If the pivot
element changes its identity won't the result be pretty much random?
+ + if (checkpoint_sort && num_to_write > 1 && false) + {
&& false - Huh?
+ qsort(CheckpointBufferIds, num_to_write, sizeof(int), + (int(*)(const void *, const void *)) bufcmp); +
Ick, I'd rather move the typecasts to the comparator.
+ for (i = 1; i < num_to_write; i++) + { + bufHdr = GetBufferDescriptor(CheckpointBufferIds[i]); + + spc = bufHdr->tag.rnode.spcNode; + if (spc != lastspc && (bufHdr->flags & BM_CHECKPOINT_NEEDED) != 0) + { + if (allocatedSpc <= j) + { + allocatedSpc = j + 5; + spcStatus = (TableSpaceCheckpointStatus *) + repalloc(spcStatus, sizeof(TableSpaceCheckpointStatus) * allocatedSpc); + } + + spcStatus[j].index_end = spcStatus[j + 1].index = i; + j++; + lastspc = spc; + } + } + spcStatus[j].index_end = num_to_write;
This really deserves some explanation.
Regards,
Andres Freund
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Hello Andres,
Thanks for your comments. Some answers and new patches included.
+ /* + * Array of buffer ids of all buffers to checkpoint. + */ +static int *CheckpointBufferIds = NULL;Should be at the beginning of the file. There's a bunch more cases of that.
done.
+/* Compare checkpoint buffers + */ +static int bufcmp(const int * pa, const int * pb) +{ + BufferDesc + *a = GetBufferDescriptor(*pa), + *b = GetBufferDescriptor(*pb);This definitely needs comments about ignoring the normal buffer header
locking.
Added.
Why are we ignoring the database directory? I doubt it'll make a huge
difference, but grouping metadata affecting operations by directory
helps.
I wanted to do the minimal comparisons to order buffers per file, so I
skipped everything else. My idea of a checkpoint is a lot of data in a few
files (at least compared to the data...), so I do not think that it is
worth it. I may be proven wrong!
+static void +AllocateCheckpointBufferIds(void) +{ + /* Safe worst case allocation, all buffers belong to the checkpoint... + * that is pretty unlikely. + */ + CheckpointBufferIds = (int *) palloc(sizeof(int) * NBuffers); +}(wrong comment style...)
Fixed.
Heikki, you were concerned about the size of the allocation of this,
right? I don't think it's relevant - we used to allocate an array of
that size for the backend's private buffer pin array until 9.5, so in
theory we should be safe agains that. NBuffers is limited to INT_MAX/2
in guc.ċ, which ought to be sufficient?
I think that there is no issue with the current shared_buffers limit. I
could allocate and use 4 GB on my laptop without problem. I added a cast
to ensure that unsigned int are used for the size computation.
+ /* + * Lazy allocation: this function is called through the
checkpointer, + * but also by initdb. Maybe the allocation could be
moved to the callers. + */ + if (CheckpointBufferIds == NULL) +
AllocateCheckpointBufferIds(); +I don't think it's a good idea to allocate this on every round.
That just means a lot of page table entries have to be built and torn
down regularly. It's not like checkpoints only run for 1% of the time or
such.
Sure. It is not allocated on every round, it is allocated once on the
first checkpoint, the variable tested is static. There is no free. Maybe
the allocation could be moved to the callers, though.
FWIW, I still think it's a much better idea to allocate the memory once
in shared buffers.
Hmmm. The memory does not need to be shared with other processes?
It's not like that makes us need more memory overall, and it'll be huge
page allocations if configured. I also think that sooner rather than
later we're going to need more than one process flushing buffers, and
then it'll need to be moved there.
That is an argument. I think that it could wait for the need to actually
arise.
+ /* + * Sort buffer ids to help find sequential writes. + * + * Note: buffers are not locked in anyway, but that does not matter, + * this sorting is really advisory, if some buffer changes status during + * this pass it will be filtered out later. The only necessary property + * is that marked buffers do not move elsewhere. + */That reasoning makes it impossible to move the fsyncing of files into
the loop (whenever we move to a new file). That's not nice.
I do not see why. Moving rsync ahead is definitely an idea that you
already pointed out, I have given it some thoughts, and it would require
a carefull implementation and some restructuring. For instance, you do not
want to issue fsync right after having done writes, you want to wait a
little bit so that the system had time to write the buffers to disk.
The formulation with "necessary property" doesn't seem very clear to me?
Removed.
How about: /* * Note: Buffers are not locked in any way during sorting,
but that's ok: * A change in the buffer header is only relevant when it
changes the * buffer's identity. If the identity has changed it'll have
been * written out by BufferAlloc(), so there's no need for checkpointer
to * write it out anymore. The buffer might also get written out by a *
backend or bgwriter, but that's equally harmless. */
This new version included.
Also, qsort implementation + * should be resilient to occasional contradictions (cmp(a,b) != -cmp(b,a)) + * because of these possible concurrent changes.Hm. Is that actually the case for our qsort implementation?
I think that it is hard to write a qsort which would fail that. That would
mean that it would compare the same items twice, which would be
inefficient.
If the pivot element changes its identity won't the result be pretty
much random?
That would be a very unlikely event, given the short time spent in qsort.
Anyway, this is not a problem, and is the beauty of the "advisory" sort:
if the sort is wrong because of any such rare event, it just mean that the
buffers would not be strictly in file order, which is currently the
case.... Well, too bad, but the correctness of the checkpoint does not
depend on it, that just mean that the checkpointer would come back twice
on one file, no big deal.
+ if (checkpoint_sort && num_to_write > 1 && false) + {&& false - Huh?
Probably Heikki tests.
+ qsort(CheckpointBufferIds, num_to_write, sizeof(int), + (int(*)(const void *, const void *)) bufcmp); +Ick, I'd rather move the typecasts to the comparator.
Done.
+ for (i = 1; i < num_to_write; i++)
+ { [...]This really deserves some explanation.
I think that this version does not work. I've reinstated my version and a
lot of comments in the attached patches.
Please find attached two combined patches which provide both features one
after the other.
(a) shared buffer sorting
- I took Heikki hint about restructuring the buffer selection in a
separate function, which makes the code much more readable.
- I also followed Heikki intention (I think) that only active
table spaces are considered in the switching loop.
(b) add asynchronous flushes on top of the previous sort patch
I think that the many performance results I reported show that the
improvements need both features, and one feature without the other is much
less effective at improving responsiveness, which is my primary concern.
The TPS improvements are just a side effect.
I did not remove the gucs: I think it could be kept so that people can
test around with it, and they may be removed in the future? I would be
also fine if they are removed.
There are a lot of comments in some places. I think that they should be
kept because the code is subtle.
--
Fabien.
Attachments:
checkpoint-continuous-flush-6-a.patchtext/x-diff; name=checkpoint-continuous-flush-6-a.patchDownload
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index e900dcc..1cec243 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -2454,6 +2454,28 @@ include_dir 'conf.d'
</listitem>
</varlistentry>
+ <varlistentry id="guc-checkpoint-sort" xreflabel="checkpoint_sort">
+ <term><varname>checkpoint_sort</varname> (<type>bool</type>)
+ <indexterm>
+ <primary><varname>checkpoint_sort</> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ Whether to sort buffers before writting them out to disk on checkpoint.
+ For a HDD storage, this setting allows to group together
+ neighboring pages written to disk, thus improving performance by
+ reducing random write activity.
+ This sorting should have limited performance effects on SSD backends
+ as such storages have good random write performance, but it may
+ help with wear-leveling so be worth keeping anyway.
+ The default is <literal>on</>.
+ This parameter can only be set in the <filename>postgresql.conf</>
+ file or on the server command line.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="guc-checkpoint-warning" xreflabel="checkpoint_warning">
<term><varname>checkpoint_warning</varname> (<type>integer</type>)
<indexterm>
diff --git a/doc/src/sgml/wal.sgml b/doc/src/sgml/wal.sgml
index e3941c9..f538698 100644
--- a/doc/src/sgml/wal.sgml
+++ b/doc/src/sgml/wal.sgml
@@ -546,6 +546,18 @@
</para>
<para>
+ When hard-disk drives (HDD) are used for terminal data storage
+ <xref linkend="guc-checkpoint-sort"> allows to sort pages
+ so that neighboring pages on disk will be flushed together by
+ chekpoints, reducing the random write load and improving performance.
+ If solid-state drives (SSD) are used, sorting pages induces no benefit
+ as their random write I/O performance is good: this feature could then
+ be disabled by setting <varname>checkpoint_sort</> to <value>off</>.
+ It is possible that sorting may help with SSD wear leveling, so it may
+ be kept on that account.
+ </para>
+
+ <para>
The number of WAL segment files in <filename>pg_xlog</> directory depends on
<varname>min_wal_size</>, <varname>max_wal_size</> and
the amount of WAL generated in previous checkpoint cycles. When old log
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 68e33eb..bee38ab 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -7995,11 +7995,13 @@ LogCheckpointEnd(bool restartpoint)
sync_secs,
total_secs,
longest_secs,
+ sort_secs,
average_secs;
int write_usecs,
sync_usecs,
total_usecs,
longest_usecs,
+ sort_usecs,
average_usecs;
uint64 average_sync_time;
@@ -8030,6 +8032,10 @@ LogCheckpointEnd(bool restartpoint)
CheckpointStats.ckpt_end_t,
&total_secs, &total_usecs);
+ TimestampDifference(CheckpointStats.ckpt_sort_t,
+ CheckpointStats.ckpt_sort_end_t,
+ &sort_secs, &sort_usecs);
+
/*
* Timing values returned from CheckpointStats are in microseconds.
* Convert to the second plus microsecond form that TimestampDifference
@@ -8048,8 +8054,8 @@ LogCheckpointEnd(bool restartpoint)
elog(LOG, "%s complete: wrote %d buffers (%.1f%%); "
"%d transaction log file(s) added, %d removed, %d recycled; "
- "write=%ld.%03d s, sync=%ld.%03d s, total=%ld.%03d s; "
- "sync files=%d, longest=%ld.%03d s, average=%ld.%03d s; "
+ "sort=%ld.%03d s, write=%ld.%03d s, sync=%ld.%03d s, total=%ld.%03d s;"
+ " sync files=%d, longest=%ld.%03d s, average=%ld.%03d s; "
"distance=%d kB, estimate=%d kB",
restartpoint ? "restartpoint" : "checkpoint",
CheckpointStats.ckpt_bufs_written,
@@ -8057,6 +8063,7 @@ LogCheckpointEnd(bool restartpoint)
CheckpointStats.ckpt_segs_added,
CheckpointStats.ckpt_segs_removed,
CheckpointStats.ckpt_segs_recycled,
+ sort_secs, sort_usecs / 1000,
write_secs, write_usecs / 1000,
sync_secs, sync_usecs / 1000,
total_secs, total_usecs / 1000,
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index e4b25587..c2bba56 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -80,6 +80,7 @@ bool zero_damaged_pages = false;
int bgwriter_lru_maxpages = 100;
double bgwriter_lru_multiplier = 2.0;
bool track_io_timing = false;
+bool checkpoint_sort = true;
/*
* How many buffers PrefetchBuffer callers should try to stay ahead of their
@@ -95,6 +96,9 @@ static bool IsForInput;
/* local state for LockBufferForCleanup */
static volatile BufferDesc *PinCountWaitBuf = NULL;
+/* Array of buffer ids of all buffers to checkpoint */
+static int * CheckpointBufferIds = NULL;
+
/*
* Backend-Private refcount management:
*
@@ -1561,6 +1565,146 @@ UnpinBuffer(volatile BufferDesc *buf, bool fixOwner)
}
}
+/* Compare checkpoint buffers.
+ * No lock is acquired, see comments below.
+ */
+static int bufcmp(const void * pa, const void * pb)
+{
+ BufferDesc
+ *a = GetBufferDescriptor(* (int *) pa),
+ *b = GetBufferDescriptor(* (int *) pb);
+
+ /* tag: rnode, forkNum (different files), blockNum
+ * rnode: { spcNode (ignore: not really needed),
+ * dbNode (ignore: this is a directory), relNode }
+ * spcNode: table space oid, not that there are at least two
+ * (pg_global and pg_default).
+ */
+ /* compare relation */
+ if (a->tag.rnode.relNode < b->tag.rnode.relNode)
+ return -1;
+ else if (a->tag.rnode.relNode > b->tag.rnode.relNode)
+ return 1;
+ /* same relation, compare fork */
+ else if (a->tag.forkNum < b->tag.forkNum)
+ return -1;
+ else if (a->tag.forkNum > b->tag.forkNum)
+ return 1;
+ /* same relation/fork, so same segmented "file", compare block number
+ * which are mapped on different segments depending on the number.
+ */
+ else if (a->tag.blockNum < b->tag.blockNum)
+ return -1;
+ else /* should not be the same block anyway... */
+ return 1;
+}
+
+static void AllocateCheckpointBufferIds(void)
+{
+ /*
+ * Safe worst case allocation, all buffers belong to the checkpoint...
+ * that is pretty unlikely. This allocation should be ok up to 4 GB
+ * for the current maximum possible NBuffers (8 TB of shared_buffers).
+ */
+ CheckpointBufferIds = (int *) palloc(sizeof(int) * (size_t) NBuffers);
+}
+
+/* Status of buffers to checkpoint for a particular tablespace,
+ * used internally in BufferSync.
+ * - space: oid of the tablespace
+ * - num_to_write: number of checkpoint pages counted for this tablespace
+ * - num_written: number of pages actually written out
+ * - index: scanning position in CheckpointBufferIds for this tablespace
+ */
+typedef struct TableSpaceCheckpointStatus {
+ Oid space;
+ int num_to_write;
+ int num_written;
+ int index;
+} TableSpaceCheckpointStatus;
+
+/* entry structure for table space to count hashtable,
+ * used internally in BufferSync.
+ */
+typedef struct TableSpaceCountEntry {
+ Oid space;
+ int count;
+} TableSpaceCountEntry;
+
+/* return the next buffer to write, or NULL if none.
+ * this function balances buffers over tablespaces.
+ */
+static int
+NextBufferToWrite(
+ TableSpaceCheckpointStatus *spcStatus, int nb_spaces,
+ int *pspace, int num_to_write, int num_written)
+{
+ int space = *pspace, buf_id = -1, index;
+
+ /*
+ * Select a tablespace depending on the current overall progress.
+ *
+ * The progress ratio of each unfinished tablespace is compared to
+ * the overall progress ratio to find one with is not in advance
+ * (i.e. tablespace ratio <= overall ratio).
+ *
+ * Existence: it is bound to exist otherwise the overall progress
+ * ratio would be inconsistent: with positive buffers to write (t1 & t2)
+ * and already written buffers (w1 & w2), we have:
+ *
+ * If w1/t1 > (w1+w2)/(t1+t2) # one table space is in advance
+ * => w1t1+w1t2 > w1t1+w2t1 => w1t2 > w2t1 => w1t2+w2t2 > w2t1+w2t2
+ * => (w1+w2) / (t1+t2) > w2 / t2 # the other one is late
+ *
+ * The round robin ensures that each space is given some attention
+ * till it is over the current ratio, before going to the next.
+ *
+ * Precision: using int32 computations for comparing fractions
+ * (w1 / t1 > w / t <=> w1 t > w t1) seems a bad idea as the values
+ * can overflow 32-bit integers: the limit would be sqrt(2**31) ~
+ * 46340 buffers, i.e. a 362 MB checkpoint. So ensure that 64-bit
+ * integers are used in the comparison.
+ */
+ while (/* compare tablespace vs overall progress ratio:
+ * tablespace written/to_write > overall written/to_write
+ */
+ (int64) spcStatus[space].num_written * num_to_write >
+ (int64) num_written * spcStatus[space].num_to_write)
+ space = (space + 1) % nb_spaces; /* round robin */
+
+ /*
+ * Find a valid buffer in the selected tablespace,
+ * by continuing the tablespace specific buffer scan
+ * where it was left.
+ */
+ index = spcStatus[space].index;
+
+ while (index < num_to_write && buf_id == -1)
+ {
+ volatile BufferDesc *bufHdr;
+
+ buf_id = CheckpointBufferIds[index];
+ bufHdr = GetBufferDescriptor(buf_id);
+
+ /* Skip if in another tablespace or not in checkpoint anymore.
+ * No lock is acquired, see comments below.
+ */
+ if (spcStatus[space].space != bufHdr->tag.rnode.spcNode ||
+ ! (bufHdr->flags & BM_CHECKPOINT_NEEDED))
+ {
+ index ++;
+ buf_id = -1;
+ }
+ }
+
+ /* Update tablespace writing status, will start over at next index */
+ spcStatus[space].index = index+1;
+
+ *pspace = space;
+
+ return buf_id;
+}
+
/*
* BufferSync -- Write out all dirty buffers in the pool.
*
@@ -1574,11 +1718,20 @@ UnpinBuffer(volatile BufferDesc *buf, bool fixOwner)
static void
BufferSync(int flags)
{
- int buf_id;
- int num_to_scan;
+ int buf_id = -1;
int num_to_write;
int num_written;
int mask = BM_DIRTY;
+ HTAB *spcBuffers;
+ TableSpaceCheckpointStatus *spcStatus = NULL;
+ int nb_spaces, space;
+
+ /*
+ * Lazy allocation: BufferSync is called through the checkpointer, but
+ * also by initdb. Maybe the allocation should be moved to these callers.
+ */
+ if (CheckpointBufferIds == NULL)
+ AllocateCheckpointBufferIds();
/* Make sure we can handle the pin inside SyncOneBuffer */
ResourceOwnerEnlargeBuffers(CurrentResourceOwner);
@@ -1609,6 +1762,18 @@ BufferSync(int flags)
* certainly need to be written for the next checkpoint attempt, too.
*/
num_to_write = 0;
+
+ /* initialize oid -> int buffer count hash table */
+ {
+ HASHCTL ctl;
+
+ MemSet(&ctl, 0, sizeof(HASHCTL));
+ ctl.keysize = sizeof(Oid);
+ ctl.entrysize = sizeof(TableSpaceCountEntry);
+ spcBuffers = hash_create("Number of buffers to write per tablespace",
+ 16, &ctl, HASH_ELEM | HASH_BLOBS);
+ }
+
for (buf_id = 0; buf_id < NBuffers; buf_id++)
{
volatile BufferDesc *bufHdr = GetBufferDescriptor(buf_id);
@@ -1621,32 +1786,107 @@ BufferSync(int flags)
if ((bufHdr->flags & mask) == mask)
{
+ Oid spc;
+ TableSpaceCountEntry * entry;
+ bool found;
+
bufHdr->flags |= BM_CHECKPOINT_NEEDED;
+ CheckpointBufferIds[num_to_write] = buf_id;
num_to_write++;
+
+ /* keep track of per tablespace buffers */
+ spc = bufHdr->tag.rnode.spcNode;
+ entry = (TableSpaceCountEntry *)
+ hash_search(spcBuffers, (void *) &spc, HASH_ENTER, &found);
+
+ if (found) entry->count++;
+ else entry->count = 1;
}
UnlockBufHdr(bufHdr);
}
if (num_to_write == 0)
+ {
+ hash_destroy(spcBuffers);
return; /* nothing to do */
+ }
TRACE_POSTGRESQL_BUFFER_SYNC_START(NBuffers, num_to_write);
+ /* Build checkpoint tablespace buffer status */
+ nb_spaces = hash_get_num_entries(spcBuffers);
+ spcStatus = (TableSpaceCheckpointStatus *)
+ palloc(sizeof(TableSpaceCheckpointStatus) * nb_spaces);
+
+ {
+ int index = 0;
+ HASH_SEQ_STATUS hseq;
+ TableSpaceCountEntry * entry;
+
+ hash_seq_init(&hseq, spcBuffers);
+ while ((entry = (TableSpaceCountEntry *) hash_seq_search(&hseq)))
+ {
+ Assert(index < nb_spaces);
+ spcStatus[index].space = entry->space;
+ spcStatus[index].num_to_write = entry->count;
+ spcStatus[index].num_written = 0;
+ /* should it be randomized? chosen with some criterion? */
+ spcStatus[index].index = 0;
+
+ index ++;
+ }
+ }
+
+ hash_destroy(spcBuffers);
+ spcBuffers = NULL;
+
+ /*
+ * Sort buffer ids to help find sequential writes.
+ *
+ * Note: Buffers are not locked in any way during sorting, but that's ok:
+ * A change in the buffer header is only relevant when it changes the
+ * buffer's identity. If the identity has changed it'll have been
+ * written out by BufferAlloc(), so there's no need for checkpointer to
+ * write it out anymore. The buffer might also get written out by a
+ * backend or bgwriter, but that's equally harmless.
+ *
+ * Marked buffers must not be move during the checkpoint.
+ * Also, qsort implementation should be resilient to occasional
+ * contradictions (cmp(a,b) != -cmp(b,a)) because of possible
+ * concurrent changes.
+ */
+ CheckpointStats.ckpt_sort_t = GetCurrentTimestamp();
+
+ if (checkpoint_sort)
+ {
+ qsort(CheckpointBufferIds, num_to_write, sizeof(int),
+ (int(*)(const void *, const void *)) bufcmp);
+ }
+
+ CheckpointStats.ckpt_sort_end_t = GetCurrentTimestamp();
+
/*
- * Loop over all buffers again, and write the ones (still) marked with
- * BM_CHECKPOINT_NEEDED. In this loop, we start at the clock sweep point
- * since we might as well dump soon-to-be-recycled buffers first.
+ * Loop over buffers to write through CheckpointBufferIds,
+ * and write the ones (still) marked with BM_CHECKPOINT_NEEDED,
+ * with some round robin over table spaces so as to balance writes,
+ * so that buffer writes move forward roughly proportionally for each
+ * tablespace.
*
- * Note that we don't read the buffer alloc count here --- that should be
- * left untouched till the next BgBufferSync() call.
+ * Termination: if a tablespace is selected by the inner while loop
+ * (see argument there), its index is incremented and will eventually
+ * reach num_to_write, mark this table space scanning as done and
+ * decrement the number of (active) spaces, which will thus reach 0.
*/
- buf_id = StrategySyncStart(NULL, NULL);
- num_to_scan = NBuffers;
+ space = 0;
num_written = 0;
- while (num_to_scan-- > 0)
+
+ while (nb_spaces != 0)
{
- volatile BufferDesc *bufHdr = GetBufferDescriptor(buf_id);
+ volatile BufferDesc *bufHdr;
+ buf_id = NextBufferToWrite(spcStatus, nb_spaces, &space,
+ num_to_write, num_written);
+ bufHdr = GetBufferDescriptor(buf_id);
/*
* We don't need to acquire the lock here, because we're only looking
@@ -1660,39 +1900,45 @@ BufferSync(int flags)
* write the buffer though we didn't need to. It doesn't seem worth
* guarding against this, though.
*/
- if (bufHdr->flags & BM_CHECKPOINT_NEEDED)
+ if (bufHdr != NULL && bufHdr->flags & BM_CHECKPOINT_NEEDED)
{
if (SyncOneBuffer(buf_id, false) & BUF_WRITTEN)
{
TRACE_POSTGRESQL_BUFFER_SYNC_WRITTEN(buf_id);
BgWriterStats.m_buf_written_checkpoints++;
+ spcStatus[space].num_written++;
num_written++;
/*
- * We know there are at most num_to_write buffers with
- * BM_CHECKPOINT_NEEDED set; so we can stop scanning if
- * num_written reaches num_to_write.
- *
- * Note that num_written doesn't include buffers written by
- * other backends, or by the bgwriter cleaning scan. That
- * means that the estimate of how much progress we've made is
- * conservative, and also that this test will often fail to
- * trigger. But it seems worth making anyway.
- */
- if (num_written >= num_to_write)
- break;
-
- /*
* Sleep to throttle our I/O rate.
*/
CheckpointWriteDelay(flags, (double) num_written / num_to_write);
}
}
- if (++buf_id >= NBuffers)
- buf_id = 0;
+ /*
+ * Detect checkpoint end for a tablespace: either the scan is done
+ * or all tablespace buffers have been written out. If so, the
+ * another active tablespace status is moved in place of the current
+ * one and the next round will start on this one, or maybe round about.
+ * Note: maybe an exchange could be made instead in order to keep
+ * informations about the closed table space, but this is currently
+ * not used afterwards.
+ */
+ if (spcStatus[space].index >= num_to_write ||
+ spcStatus[space].num_written >= spcStatus[space].num_to_write)
+ {
+ nb_spaces--;
+ if (space != nb_spaces)
+ spcStatus[space] = spcStatus[nb_spaces];
+ else
+ space = 0;
+ }
}
+ pfree(spcStatus);
+ spcStatus = NULL;
+
/*
* Update checkpoint statistics. As noted above, this doesn't include
* buffers written by other backends or bgwriter scan.
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index b3dac51..ff95e61 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -1013,6 +1013,17 @@ static struct config_bool ConfigureNamesBool[] =
false,
NULL, NULL, NULL
},
+
+ {
+ {"checkpoint_sort", PGC_SIGHUP, WAL_CHECKPOINTS,
+ gettext_noop("Whether disk-page buffers are sorted on checkpoints."),
+ NULL
+ },
+ &checkpoint_sort,
+ true,
+ NULL, NULL, NULL
+ },
+
{
{"log_connections", PGC_SU_BACKEND, LOGGING_WHAT,
gettext_noop("Logs each successful connection."),
@@ -1798,6 +1809,9 @@ static struct config_int ConfigureNamesInt[] =
/*
* We sometimes multiply the number of shared buffers by two without
* checking for overflow, so we mustn't allow more than INT_MAX / 2.
+ * Also, checkpoint uses a malloced int array to store index of shared
+ * buffers for sorting, which results in a SIZE_MAX / sizeof(int) limit,
+ * that is UINT_MAX / 4 == INT_MAX / 2 as well on a 32 bits system.
*/
{
{"shared_buffers", PGC_POSTMASTER, RESOURCES_MEM,
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index e5d275d..e84f380 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -201,6 +201,7 @@
#max_wal_size = 1GB
#min_wal_size = 80MB
#checkpoint_completion_target = 0.5 # checkpoint target duration, 0.0 - 1.0
+#checkpoint_sort = on # sort buffers on checkpoint
#checkpoint_warning = 30s # 0 disables
# - Archiving -
diff --git a/src/include/access/xlog.h b/src/include/access/xlog.h
index 6dacee2..dbd4757 100644
--- a/src/include/access/xlog.h
+++ b/src/include/access/xlog.h
@@ -186,6 +186,8 @@ extern bool XLOG_DEBUG;
typedef struct CheckpointStatsData
{
TimestampTz ckpt_start_t; /* start of checkpoint */
+ TimestampTz ckpt_sort_t; /* start buffer sorting */
+ TimestampTz ckpt_sort_end_t; /* end of sorting */
TimestampTz ckpt_write_t; /* start of flushing buffers */
TimestampTz ckpt_sync_t; /* start of fsyncs */
TimestampTz ckpt_sync_end_t; /* end of fsyncs */
diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.h
index ec0a254..c228f39 100644
--- a/src/include/storage/bufmgr.h
+++ b/src/include/storage/bufmgr.h
@@ -54,6 +54,7 @@ extern int bgwriter_lru_maxpages;
extern double bgwriter_lru_multiplier;
extern bool track_io_timing;
extern int target_prefetch_pages;
+extern bool checkpoint_sort;
/* in buf_init.c */
extern PGDLLIMPORT char *BufferBlocks;
checkpoint-continuous-flush-6-b.patchtext/x-diff; name=checkpoint-continuous-flush-6-b.patchDownload
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 1cec243..2551d95 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -2497,6 +2497,24 @@ include_dir 'conf.d'
</listitem>
</varlistentry>
+ <varlistentry id="guc-checkpoint-flush-to-disk" xreflabel="checkpoint_flush_to_disk">
+ <term><varname>checkpoint_flush_to_disk</varname> (<type>bool</type>)
+ <indexterm>
+ <primary><varname>checkpoint_flush_to_disk</> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ When writing data for a checkpoint, hint the underlying OS that the
+ data must be sent to disk as soon as possible. This may help smoothing
+ disk I/O writes and avoid a stall when fsync is issued at the end of
+ the checkpoint, but it may also reduce average performance.
+ This setting may have no effect on some platforms.
+ The default is <literal>off</>.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="guc-min-wal-size" xreflabel="min_wal_size">
<term><varname>min_wal_size</varname> (<type>integer</type>)
<indexterm>
diff --git a/doc/src/sgml/wal.sgml b/doc/src/sgml/wal.sgml
index f538698..eea6668 100644
--- a/doc/src/sgml/wal.sgml
+++ b/doc/src/sgml/wal.sgml
@@ -558,6 +558,17 @@
</para>
<para>
+ On Linux and POSIX platforms, <xref linkend="guc-checkpoint-flush-to-disk">
+ allows to hint the OS that pages written on checkpoints must be flushed
+ to disk quickly. Otherwise, these pages may be kept in cache for some time,
+ inducing a stall later when <literal>fsync</> is called to actually
+ complete the checkpoint. This setting helps to reduce transaction latency,
+ but it may also have a small adverse effect on the average transaction rate
+ at maximum throughput. This feature probably brings no benefit on SSD,
+ as the I/O write latency is small on such hardware, thus it may be disabled.
+ </para>
+
+ <para>
The number of WAL segment files in <filename>pg_xlog</> directory depends on
<varname>min_wal_size</>, <varname>max_wal_size</> and
the amount of WAL generated in previous checkpoint cycles. When old log
diff --git a/src/backend/access/heap/rewriteheap.c b/src/backend/access/heap/rewriteheap.c
index bcce3e3..f565dc4 100644
--- a/src/backend/access/heap/rewriteheap.c
+++ b/src/backend/access/heap/rewriteheap.c
@@ -918,7 +918,7 @@ logical_heap_rewrite_flush_mappings(RewriteState state)
* Note that we deviate from the usual WAL coding practices here,
* check the above "Logical rewrite support" comment for reasoning.
*/
- written = FileWrite(src->vfd, waldata_start, len);
+ written = FileWrite(src->vfd, waldata_start, len, false, NULL);
if (written != len)
ereport(ERROR,
(errcode_for_file_access(),
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index cf4a6dc..4b5e9cd 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -203,7 +203,7 @@ btbuildempty(PG_FUNCTION_ARGS)
/* Write the page. If archiving/streaming, XLOG it. */
PageSetChecksumInplace(metapage, BTREE_METAPAGE);
smgrwrite(index->rd_smgr, INIT_FORKNUM, BTREE_METAPAGE,
- (char *) metapage, true);
+ (char *) metapage, true, false, NULL);
if (XLogIsNeeded())
log_newpage(&index->rd_smgr->smgr_rnode.node, INIT_FORKNUM,
BTREE_METAPAGE, metapage, false);
diff --git a/src/backend/access/nbtree/nbtsort.c b/src/backend/access/nbtree/nbtsort.c
index f95f67a..ea7a45d 100644
--- a/src/backend/access/nbtree/nbtsort.c
+++ b/src/backend/access/nbtree/nbtsort.c
@@ -315,7 +315,7 @@ _bt_blwritepage(BTWriteState *wstate, Page page, BlockNumber blkno)
{
/* overwriting a block we zero-filled before */
smgrwrite(wstate->index->rd_smgr, MAIN_FORKNUM, blkno,
- (char *) page, true);
+ (char *) page, true, false, NULL);
}
pfree(page);
diff --git a/src/backend/access/spgist/spginsert.c b/src/backend/access/spgist/spginsert.c
index bceee8d..b700efb 100644
--- a/src/backend/access/spgist/spginsert.c
+++ b/src/backend/access/spgist/spginsert.c
@@ -170,7 +170,7 @@ spgbuildempty(PG_FUNCTION_ARGS)
/* Write the page. If archiving/streaming, XLOG it. */
PageSetChecksumInplace(page, SPGIST_METAPAGE_BLKNO);
smgrwrite(index->rd_smgr, INIT_FORKNUM, SPGIST_METAPAGE_BLKNO,
- (char *) page, true);
+ (char *) page, true, false, NULL);
if (XLogIsNeeded())
log_newpage(&index->rd_smgr->smgr_rnode.node, INIT_FORKNUM,
SPGIST_METAPAGE_BLKNO, page, false);
@@ -180,7 +180,7 @@ spgbuildempty(PG_FUNCTION_ARGS)
PageSetChecksumInplace(page, SPGIST_ROOT_BLKNO);
smgrwrite(index->rd_smgr, INIT_FORKNUM, SPGIST_ROOT_BLKNO,
- (char *) page, true);
+ (char *) page, true, false, NULL);
if (XLogIsNeeded())
log_newpage(&index->rd_smgr->smgr_rnode.node, INIT_FORKNUM,
SPGIST_ROOT_BLKNO, page, true);
@@ -190,7 +190,7 @@ spgbuildempty(PG_FUNCTION_ARGS)
PageSetChecksumInplace(page, SPGIST_NULL_BLKNO);
smgrwrite(index->rd_smgr, INIT_FORKNUM, SPGIST_NULL_BLKNO,
- (char *) page, true);
+ (char *) page, true, false, NULL);
if (XLogIsNeeded())
log_newpage(&index->rd_smgr->smgr_rnode.node, INIT_FORKNUM,
SPGIST_NULL_BLKNO, page, true);
diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c
index 3b3a09e..e361907 100644
--- a/src/backend/postmaster/checkpointer.c
+++ b/src/backend/postmaster/checkpointer.c
@@ -665,7 +665,8 @@ ImmediateCheckpointRequested(void)
* fraction between 0.0 meaning none, and 1.0 meaning all done.
*/
void
-CheckpointWriteDelay(int flags, double progress)
+CheckpointWriteDelay(int flags, double progress,
+ FileFlushContext * context, int ctx_size)
{
static int absorb_counter = WRITES_PER_ABSORB;
@@ -700,6 +701,26 @@ CheckpointWriteDelay(int flags, double progress)
*/
pgstat_send_bgwriter();
+#if defined(HAVE_SYNC_FILE_RANGE) || defined(HAVE_POSIX_FADVISE)
+
+ /*
+ * Before sleeping, flush written blocks for each tablespace.
+ */
+ if (checkpoint_flush_to_disk)
+ {
+ int i;
+
+ for (i = 0; i < ctx_size; i++)
+ {
+ if (context[i].ncalls != 0)
+ {
+ PerformFileFlush(&context[i]);
+ ResetFileFlushContext(&context[i]);
+ }
+ }
+ }
+#endif /* HAVE_SYNC_FILE_RANGE || HAVE_POSIX_FADVISE */
+
/*
* This sleep used to be connected to bgwriter_delay, typically 200ms.
* That resulted in more frequent wakeups if not much work to do.
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index c2bba56..63bb628 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -80,6 +80,8 @@ bool zero_damaged_pages = false;
int bgwriter_lru_maxpages = 100;
double bgwriter_lru_multiplier = 2.0;
bool track_io_timing = false;
+/* hint to move writes to high priority */
+bool checkpoint_flush_to_disk = false;
bool checkpoint_sort = true;
/*
@@ -400,7 +402,8 @@ static bool PinBuffer(volatile BufferDesc *buf, BufferAccessStrategy strategy);
static void PinBuffer_Locked(volatile BufferDesc *buf);
static void UnpinBuffer(volatile BufferDesc *buf, bool fixOwner);
static void BufferSync(int flags);
-static int SyncOneBuffer(int buf_id, bool skip_recently_used);
+static int SyncOneBuffer(int buf_id, bool skip_recently_used,
+ bool flush_to_disk, FileFlushContext *context);
static void WaitIO(volatile BufferDesc *buf);
static bool StartBufferIO(volatile BufferDesc *buf, bool forInput);
static void TerminateBufferIO(volatile BufferDesc *buf, bool clear_dirty,
@@ -413,7 +416,8 @@ static volatile BufferDesc *BufferAlloc(SMgrRelation smgr,
BlockNumber blockNum,
BufferAccessStrategy strategy,
bool *foundPtr);
-static void FlushBuffer(volatile BufferDesc *buf, SMgrRelation reln);
+static void FlushBuffer(volatile BufferDesc *buf, SMgrRelation reln,
+ bool flush_to_disk, FileFlushContext *context);
static void AtProcExit_Buffers(int code, Datum arg);
static void CheckForBufferLeaks(void);
static int rnode_comparator(const void *p1, const void *p2);
@@ -1022,7 +1026,7 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
smgr->smgr_rnode.node.dbNode,
smgr->smgr_rnode.node.relNode);
- FlushBuffer(buf, NULL);
+ FlushBuffer(buf, NULL, false, NULL);
LWLockRelease(buf->content_lock);
TRACE_POSTGRESQL_BUFFER_WRITE_DIRTY_DONE(forkNum, blockNum,
@@ -1725,6 +1729,7 @@ BufferSync(int flags)
HTAB *spcBuffers;
TableSpaceCheckpointStatus *spcStatus = NULL;
int nb_spaces, space;
+ FileFlushContext * spcContext = NULL;
/*
* Lazy allocation: BufferSync is called through the checkpointer, but
@@ -1814,10 +1819,12 @@ BufferSync(int flags)
TRACE_POSTGRESQL_BUFFER_SYNC_START(NBuffers, num_to_write);
- /* Build checkpoint tablespace buffer status */
+ /* Build checkpoint tablespace buffer status & flush context arrays */
nb_spaces = hash_get_num_entries(spcBuffers);
spcStatus = (TableSpaceCheckpointStatus *)
palloc(sizeof(TableSpaceCheckpointStatus) * nb_spaces);
+ spcContext = (FileFlushContext *)
+ palloc(sizeof(FileFlushContext) * nb_spaces);
{
int index = 0;
@@ -1834,6 +1841,12 @@ BufferSync(int flags)
/* should it be randomized? chosen with some criterion? */
spcStatus[index].index = 0;
+#if defined(HAVE_SYNC_FILE_RANGE) || defined(HAVE_POSIX_FADVISE)
+
+ ResetFileFlushContext(&spcContext[index]);
+
+#endif /* HAVE_SYNC_FILE_RANGE || HAVE_POSIX_FADVISE */
+
index ++;
}
}
@@ -1902,7 +1915,8 @@ BufferSync(int flags)
*/
if (bufHdr != NULL && bufHdr->flags & BM_CHECKPOINT_NEEDED)
{
- if (SyncOneBuffer(buf_id, false) & BUF_WRITTEN)
+ if (SyncOneBuffer(buf_id, false, checkpoint_flush_to_disk,
+ &spcContext[space]) & BUF_WRITTEN)
{
TRACE_POSTGRESQL_BUFFER_SYNC_WRITTEN(buf_id);
BgWriterStats.m_buf_written_checkpoints++;
@@ -1912,7 +1926,8 @@ BufferSync(int flags)
/*
* Sleep to throttle our I/O rate.
*/
- CheckpointWriteDelay(flags, (double) num_written / num_to_write);
+ CheckpointWriteDelay(flags, (double) num_written / num_to_write,
+ spcContext, nb_spaces);
}
}
@@ -1928,6 +1943,13 @@ BufferSync(int flags)
if (spcStatus[space].index >= num_to_write ||
spcStatus[space].num_written >= spcStatus[space].num_to_write)
{
+#if defined(HAVE_SYNC_FILE_RANGE) || defined(HAVE_POSIX_FADVISE)
+
+ PerformFileFlush(&spcContext[space]);
+ ResetFileFlushContext(&spcContext[space]);
+
+#endif /* HAVE_SYNC_FILE_RANGE || HAVE_POSIX_FADVISE */
+
nb_spaces--;
if (space != nb_spaces)
spcStatus[space] = spcStatus[nb_spaces];
@@ -1938,6 +1960,8 @@ BufferSync(int flags)
pfree(spcStatus);
spcStatus = NULL;
+ pfree(spcContext);
+ spcContext = NULL;
/*
* Update checkpoint statistics. As noted above, this doesn't include
@@ -2185,7 +2209,8 @@ BgBufferSync(void)
/* Execute the LRU scan */
while (num_to_scan > 0 && reusable_buffers < upcoming_alloc_est)
{
- int buffer_state = SyncOneBuffer(next_to_clean, true);
+ int buffer_state =
+ SyncOneBuffer(next_to_clean, true, false, NULL);
if (++next_to_clean >= NBuffers)
{
@@ -2262,7 +2287,8 @@ BgBufferSync(void)
* Note: caller must have done ResourceOwnerEnlargeBuffers.
*/
static int
-SyncOneBuffer(int buf_id, bool skip_recently_used)
+SyncOneBuffer(int buf_id, bool skip_recently_used, bool flush_to_disk,
+ FileFlushContext * context)
{
volatile BufferDesc *bufHdr = GetBufferDescriptor(buf_id);
int result = 0;
@@ -2303,7 +2329,7 @@ SyncOneBuffer(int buf_id, bool skip_recently_used)
PinBuffer_Locked(bufHdr);
LWLockAcquire(bufHdr->content_lock, LW_SHARED);
- FlushBuffer(bufHdr, NULL);
+ FlushBuffer(bufHdr, NULL, flush_to_disk, context);
LWLockRelease(bufHdr->content_lock);
UnpinBuffer(bufHdr, true);
@@ -2565,9 +2591,16 @@ BufferGetTag(Buffer buffer, RelFileNode *rnode, ForkNumber *forknum,
*
* If the caller has an smgr reference for the buffer's relation, pass it
* as the second parameter. If not, pass NULL.
+ *
+ * The third parameter tries to hint the OS that a high priority write is meant,
+ * possibly because io-throttling is already managed elsewhere.
+ * The last parameter holds the current flush context that accumulates flush
+ * requests to be performed in one call, instead of being performed on a buffer
+ * per buffer basis.
*/
static void
-FlushBuffer(volatile BufferDesc *buf, SMgrRelation reln)
+FlushBuffer(volatile BufferDesc *buf, SMgrRelation reln, bool flush_to_disk,
+ FileFlushContext * context)
{
XLogRecPtr recptr;
ErrorContextCallback errcallback;
@@ -2656,7 +2689,9 @@ FlushBuffer(volatile BufferDesc *buf, SMgrRelation reln)
buf->tag.forkNum,
buf->tag.blockNum,
bufToWrite,
- false);
+ false,
+ flush_to_disk,
+ context);
if (track_io_timing)
{
@@ -3076,7 +3111,9 @@ FlushRelationBuffers(Relation rel)
bufHdr->tag.forkNum,
bufHdr->tag.blockNum,
localpage,
- false);
+ false,
+ false,
+ NULL);
bufHdr->flags &= ~(BM_DIRTY | BM_JUST_DIRTIED);
@@ -3110,7 +3147,7 @@ FlushRelationBuffers(Relation rel)
{
PinBuffer_Locked(bufHdr);
LWLockAcquire(bufHdr->content_lock, LW_SHARED);
- FlushBuffer(bufHdr, rel->rd_smgr);
+ FlushBuffer(bufHdr, rel->rd_smgr, false, NULL);
LWLockRelease(bufHdr->content_lock);
UnpinBuffer(bufHdr, true);
}
@@ -3162,7 +3199,7 @@ FlushDatabaseBuffers(Oid dbid)
{
PinBuffer_Locked(bufHdr);
LWLockAcquire(bufHdr->content_lock, LW_SHARED);
- FlushBuffer(bufHdr, NULL);
+ FlushBuffer(bufHdr, NULL, false, NULL);
LWLockRelease(bufHdr->content_lock);
UnpinBuffer(bufHdr, true);
}
diff --git a/src/backend/storage/buffer/localbuf.c b/src/backend/storage/buffer/localbuf.c
index 3144afe..114a0a6 100644
--- a/src/backend/storage/buffer/localbuf.c
+++ b/src/backend/storage/buffer/localbuf.c
@@ -208,7 +208,9 @@ LocalBufferAlloc(SMgrRelation smgr, ForkNumber forkNum, BlockNumber blockNum,
bufHdr->tag.forkNum,
bufHdr->tag.blockNum,
localpage,
- false);
+ false,
+ false,
+ NULL);
/* Mark not-dirty now in case we error out below */
bufHdr->flags &= ~BM_DIRTY;
diff --git a/src/backend/storage/file/buffile.c b/src/backend/storage/file/buffile.c
index ea4d689..fb3b383 100644
--- a/src/backend/storage/file/buffile.c
+++ b/src/backend/storage/file/buffile.c
@@ -317,7 +317,7 @@ BufFileDumpBuffer(BufFile *file)
return; /* seek failed, give up */
file->offsets[file->curFile] = file->curOffset;
}
- bytestowrite = FileWrite(thisfile, file->buffer + wpos, bytestowrite);
+ bytestowrite = FileWrite(thisfile, file->buffer + wpos, bytestowrite, false, NULL);
if (bytestowrite <= 0)
return; /* failed to write */
file->offsets[file->curFile] += bytestowrite;
diff --git a/src/backend/storage/file/fd.c b/src/backend/storage/file/fd.c
index 1ba4946..daf03e4 100644
--- a/src/backend/storage/file/fd.c
+++ b/src/backend/storage/file/fd.c
@@ -1344,8 +1344,97 @@ retry:
return returnCode;
}
+#if defined(HAVE_SYNC_FILE_RANGE) || defined(HAVE_POSIX_FADVISE)
+
+void
+ResetFileFlushContext(FileFlushContext * context)
+{
+ context->fd = 0;
+ context->ncalls = 0;
+ context->offset = 0;
+ context->nbytes = 0;
+ context->filename = NULL;
+}
+
+void
+PerformFileFlush(FileFlushContext * context)
+{
+ if (context->ncalls != 0)
+ {
+ int rc;
+
+#if defined(HAVE_SYNC_FILE_RANGE)
+
+ /* Linux: tell the memory manager to move these blocks to io so
+ * that they are considered for being actually written to disk.
+ */
+ rc = sync_file_range(context->fd, context->offset, context->nbytes,
+ SYNC_FILE_RANGE_WRITE);
+
+#elif defined(HAVE_POSIX_FADVISE)
+
+ /* Others: say that data should not be kept in memory...
+ * This is not exactly what we want to say, because we want to write
+ * the data for durability but we may need it later nevertheless.
+ * It seems that Linux would free the memory *if* the data has
+ * already been written do disk, else the "dontneed" call is ignored.
+ * For FreeBSD this may have the desired effect of moving the
+ * data to the io layer, although the system does not seem to
+ * take into account the provided offset & size, so it is rather
+ * rough...
+ */
+ rc = posix_fadvise(context->fd, context->offset, context->nbytes,
+ POSIX_FADV_DONTNEED);
+
+#endif
+
+ if (rc < 0)
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not flush block " INT64_FORMAT
+ " on " INT64_FORMAT " blocks in file \"%s\": %m",
+ context->offset / BLCKSZ,
+ context->nbytes / BLCKSZ,
+ context->filename)));
+ }
+}
+
+void
+FileAsynchronousFlush(FileFlushContext * context,
+ int fd, off_t offset, off_t nbytes, char * filename)
+{
+ if (context->ncalls != 0 && context->fd == fd)
+ {
+ /* Same file: merge current flush with previous ones */
+ off_t new_offset = offset < context->offset? offset: context->offset;
+
+ context->nbytes =
+ (context->offset + context->nbytes > offset + nbytes ?
+ context->offset + context->nbytes : offset + nbytes) -
+ new_offset;
+ context->offset = new_offset;
+ context->ncalls ++;
+ }
+ else
+ {
+ /* file has changed; actually flush previous file before restarting
+ * to accumulate flushes
+ */
+ PerformFileFlush(context);
+
+ context->fd = fd;
+ context->ncalls = 1;
+ context->offset = offset;
+ context->nbytes = nbytes;
+ context->filename = filename;
+ }
+}
+
+#endif /* HAVE_SYNC_FILE_RANGE || HAVE_POSIX_FADVISE */
+
int
-FileWrite(File file, char *buffer, int amount)
+FileWrite(File file, char *buffer, int amount, bool flush_to_disk,
+ FileFlushContext * context)
{
int returnCode;
@@ -1395,6 +1484,28 @@ retry:
if (returnCode >= 0)
{
+
+#if defined(HAVE_SYNC_FILE_RANGE) || defined(HAVE_POSIX_FADVISE)
+
+ /*
+ * Calling "write" tells the OS that pg wants to write some page to disk,
+ * however when it is really done is chosen by the OS.
+ * Depending on other disk activities this may be delayed significantly,
+ * maybe up to an "fsync" call, which could induce an IO write surge.
+ * When checkpointing pg is doing its own throttling and the result
+ * should really be written to disk with high priority, so as to meet
+ * the completion target.
+ * This call hints that such write have a higher priority.
+ */
+ if (flush_to_disk && returnCode == amount && errno == 0)
+ {
+ FileAsynchronousFlush(context,
+ VfdCache[file].fd, VfdCache[file].seekPos,
+ amount, VfdCache[file].fileName);
+ }
+
+#endif /* HAVE_SYNC_FILE_RANGE || HAVE_POSIX_FADVISE */
+
VfdCache[file].seekPos += returnCode;
/* maintain fileSize and temporary_files_size if it's a temp file */
diff --git a/src/backend/storage/smgr/md.c b/src/backend/storage/smgr/md.c
index 42a43bb..dbf057f 100644
--- a/src/backend/storage/smgr/md.c
+++ b/src/backend/storage/smgr/md.c
@@ -531,7 +531,7 @@ mdextend(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
errmsg("could not seek to block %u in file \"%s\": %m",
blocknum, FilePathName(v->mdfd_vfd))));
- if ((nbytes = FileWrite(v->mdfd_vfd, buffer, BLCKSZ)) != BLCKSZ)
+ if ((nbytes = FileWrite(v->mdfd_vfd, buffer, BLCKSZ, false, NULL)) != BLCKSZ)
{
if (nbytes < 0)
ereport(ERROR,
@@ -738,7 +738,8 @@ mdread(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
*/
void
mdwrite(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
- char *buffer, bool skipFsync)
+ char *buffer, bool skipFsync, bool flush_to_disk,
+ FileFlushContext * context)
{
off_t seekpos;
int nbytes;
@@ -767,7 +768,7 @@ mdwrite(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
errmsg("could not seek to block %u in file \"%s\": %m",
blocknum, FilePathName(v->mdfd_vfd))));
- nbytes = FileWrite(v->mdfd_vfd, buffer, BLCKSZ);
+ nbytes = FileWrite(v->mdfd_vfd, buffer, BLCKSZ, flush_to_disk, context);
TRACE_POSTGRESQL_SMGR_MD_WRITE_DONE(forknum, blocknum,
reln->smgr_rnode.node.spcNode,
diff --git a/src/backend/storage/smgr/smgr.c b/src/backend/storage/smgr/smgr.c
index 244b4ea..2db3cd3 100644
--- a/src/backend/storage/smgr/smgr.c
+++ b/src/backend/storage/smgr/smgr.c
@@ -52,7 +52,8 @@ typedef struct f_smgr
void (*smgr_read) (SMgrRelation reln, ForkNumber forknum,
BlockNumber blocknum, char *buffer);
void (*smgr_write) (SMgrRelation reln, ForkNumber forknum,
- BlockNumber blocknum, char *buffer, bool skipFsync);
+ BlockNumber blocknum, char *buffer, bool skipFsync,
+ bool flush_to_disk, FileFlushContext *context);
BlockNumber (*smgr_nblocks) (SMgrRelation reln, ForkNumber forknum);
void (*smgr_truncate) (SMgrRelation reln, ForkNumber forknum,
BlockNumber nblocks);
@@ -643,10 +644,11 @@ smgrread(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
*/
void
smgrwrite(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
- char *buffer, bool skipFsync)
+ char *buffer, bool skipFsync, bool flush_to_disk,
+ FileFlushContext * context)
{
(*(smgrsw[reln->smgr_which].smgr_write)) (reln, forknum, blocknum,
- buffer, skipFsync);
+ buffer, skipFsync, flush_to_disk, context);
}
/*
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index ff95e61..c5c996c 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -158,6 +158,7 @@ static bool check_bonjour(bool *newval, void **extra, GucSource source);
static bool check_ssl(bool *newval, void **extra, GucSource source);
static bool check_stage_log_stats(bool *newval, void **extra, GucSource source);
static bool check_log_stats(bool *newval, void **extra, GucSource source);
+static bool check_flush_to_disk(bool *newval, void **extra, GucSource source);
static bool check_canonical_path(char **newval, void **extra, GucSource source);
static bool check_timezone_abbreviations(char **newval, void **extra, GucSource source);
static void assign_timezone_abbreviations(const char *newval, void *extra);
@@ -1025,6 +1026,16 @@ static struct config_bool ConfigureNamesBool[] =
},
{
+ {"checkpoint_flush_to_disk", PGC_SIGHUP, WAL_CHECKPOINTS,
+ gettext_noop("Hint that checkpoint's writes are high priority."),
+ NULL
+ },
+ &checkpoint_flush_to_disk,
+ false,
+ check_flush_to_disk, NULL, NULL
+ },
+
+ {
{"log_connections", PGC_SU_BACKEND, LOGGING_WHAT,
gettext_noop("Logs each successful connection."),
NULL
@@ -9809,6 +9820,21 @@ check_log_stats(bool *newval, void **extra, GucSource source)
}
static bool
+check_flush_to_disk(bool *newval, void **extra, GucSource source)
+{
+/* This test must be consistent with the one in FileWrite (storage/file/fd.c)
+ */
+#if ! (defined(HAVE_SYNC_FILE_RANGE) || defined(HAVE_POSIX_FADVISE))
+ /* just warn if it has no effect */
+ ereport(WARNING,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("Setting \"checkpoint_flush_to_disk\" has no effect "
+ "on this platform.")));
+#endif /* HAVE_SYNC_FILE_RANGE */
+ return true;
+}
+
+static bool
check_canonical_path(char **newval, void **extra, GucSource source)
{
/*
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index e84f380..66010b1 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -202,6 +202,7 @@
#min_wal_size = 80MB
#checkpoint_completion_target = 0.5 # checkpoint target duration, 0.0 - 1.0
#checkpoint_sort = on # sort buffers on checkpoint
+#checkpoint_flush_to_disk = off # send buffers to disk on checkpoint
#checkpoint_warning = 30s # 0 disables
# - Archiving -
diff --git a/src/include/postmaster/bgwriter.h b/src/include/postmaster/bgwriter.h
index a49c208..f9c8ca1 100644
--- a/src/include/postmaster/bgwriter.h
+++ b/src/include/postmaster/bgwriter.h
@@ -16,6 +16,7 @@
#define _BGWRITER_H
#include "storage/block.h"
+#include "storage/fd.h"
#include "storage/relfilenode.h"
@@ -29,7 +30,8 @@ extern void BackgroundWriterMain(void) pg_attribute_noreturn();
extern void CheckpointerMain(void) pg_attribute_noreturn();
extern void RequestCheckpoint(int flags);
-extern void CheckpointWriteDelay(int flags, double progress);
+extern void CheckpointWriteDelay(int flags, double progress,
+ FileFlushContext * context, int ctx_size);
extern bool ForwardFsyncRequest(RelFileNode rnode, ForkNumber forknum,
BlockNumber segno);
diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.h
index c228f39..db0e2c3 100644
--- a/src/include/storage/bufmgr.h
+++ b/src/include/storage/bufmgr.h
@@ -54,6 +54,7 @@ extern int bgwriter_lru_maxpages;
extern double bgwriter_lru_multiplier;
extern bool track_io_timing;
extern int target_prefetch_pages;
+extern bool checkpoint_flush_to_disk;
extern bool checkpoint_sort;
/* in buf_init.c */
diff --git a/src/include/storage/fd.h b/src/include/storage/fd.h
index 7eabe09..c740ee7 100644
--- a/src/include/storage/fd.h
+++ b/src/include/storage/fd.h
@@ -59,6 +59,22 @@ extern int max_files_per_process;
*/
extern int max_safe_fds;
+/* FileFlushContext:
+ * This structure is used to accumulate several flush requests on a file
+ * into a larger flush request.
+ * - fd: file descriptor of the file
+ * - ncalls: number of flushes merged together
+ * - offset: starting offset (minimum of all offset)
+ * - nbytes: size (minimum extent to cover all flushed data)
+ * - filename: filename of fd for error messages
+ */
+typedef struct FileFlushContext{
+ int fd;
+ int ncalls;
+ off_t offset;
+ off_t nbytes;
+ char * filename;
+} FileFlushContext;
/*
* prototypes for functions in fd.c
@@ -70,7 +86,12 @@ extern File OpenTemporaryFile(bool interXact);
extern void FileClose(File file);
extern int FilePrefetch(File file, off_t offset, int amount);
extern int FileRead(File file, char *buffer, int amount);
-extern int FileWrite(File file, char *buffer, int amount);
+extern void ResetFileFlushContext(FileFlushContext * context);
+extern void PerformFileFlush(FileFlushContext * context);
+extern void FileAsynchronousFlush(FileFlushContext * context,
+ int fd, off_t offset, off_t nbytes, char * filename);
+extern int FileWrite(File file, char *buffer, int amount, bool flush_to_disk,
+ FileFlushContext * context);
extern int FileSync(File file);
extern off_t FileSeek(File file, off_t offset, int whence);
extern int FileTruncate(File file, off_t offset);
diff --git a/src/include/storage/smgr.h b/src/include/storage/smgr.h
index 69a624f..a46a70c 100644
--- a/src/include/storage/smgr.h
+++ b/src/include/storage/smgr.h
@@ -16,6 +16,7 @@
#include "fmgr.h"
#include "storage/block.h"
+#include "storage/fd.h"
#include "storage/relfilenode.h"
@@ -95,7 +96,8 @@ extern void smgrprefetch(SMgrRelation reln, ForkNumber forknum,
extern void smgrread(SMgrRelation reln, ForkNumber forknum,
BlockNumber blocknum, char *buffer);
extern void smgrwrite(SMgrRelation reln, ForkNumber forknum,
- BlockNumber blocknum, char *buffer, bool skipFsync);
+ BlockNumber blocknum, char *buffer, bool skipFsync,
+ bool flush_to_disk, FileFlushContext * context);
extern BlockNumber smgrnblocks(SMgrRelation reln, ForkNumber forknum);
extern void smgrtruncate(SMgrRelation reln, ForkNumber forknum,
BlockNumber nblocks);
@@ -120,8 +122,9 @@ extern void mdprefetch(SMgrRelation reln, ForkNumber forknum,
BlockNumber blocknum);
extern void mdread(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
char *buffer);
-extern void mdwrite(SMgrRelation reln, ForkNumber forknum,
- BlockNumber blocknum, char *buffer, bool skipFsync);
+extern void mdwrite(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
+ char *buffer, bool skipFsync, bool flush_to_disk,
+ FileFlushContext * context);
extern BlockNumber mdnblocks(SMgrRelation reln, ForkNumber forknum);
extern void mdtruncate(SMgrRelation reln, ForkNumber forknum,
BlockNumber nblocks);
On 2015-08-10 19:07:12 +0200, Fabien COELHO wrote:
I think that there is no issue with the current shared_buffers limit. I
could allocate and use 4 GB on my laptop without problem. I added a cast to
ensure that unsigned int are used for the size computation.
You can't allocate 4GB with palloc(), it has a builtin limit against
allocating more than 1GB.
+ /* + * Lazy allocation: this function is called through the
checkpointer, + * but also by initdb. Maybe the allocation could be
moved to the callers. + */ + if (CheckpointBufferIds == NULL) +
AllocateCheckpointBufferIds(); +I don't think it's a good idea to allocate this on every round.
That just means a lot of page table entries have to be built and torn down
regularly. It's not like checkpoints only run for 1% of the time or such.Sure. It is not allocated on every round, it is allocated once on the first
checkpoint, the variable tested is static. There is no free. Maybe
the allocation could be moved to the callers, though.
Well, then everytime the checkpointer is restarted.
FWIW, I still think it's a much better idea to allocate the memory once
in shared buffers.Hmmm. The memory does not need to be shared with other processes?
The point is that it's done at postmaster startup, and we're pretty much
guaranteed that the memory will availabl.e.
It's not like that makes us need more memory overall, and it'll be huge
page allocations if configured. I also think that sooner rather than later
we're going to need more than one process flushing buffers, and then it'll
need to be moved there.That is an argument. I think that it could wait for the need to actually
arise.
Huge pages are used today.
+ /* + * Sort buffer ids to help find sequential writes. + * + * Note: buffers are not locked in anyway, but that does not matter, + * this sorting is really advisory, if some buffer changes status during + * this pass it will be filtered out later. The only necessary property + * is that marked buffers do not move elsewhere. + */That reasoning makes it impossible to move the fsyncing of files into the
loop (whenever we move to a new file). That's not nice.I do not see why.
Because it means that the sorting isn't necessarily correct. I.e. we
can't rely on it to determine whether a file has already been fsynced.
Also, qsort implementation + * should be resilient to occasional contradictions (cmp(a,b) != -cmp(b,a)) + * because of these possible concurrent changes.Hm. Is that actually the case for our qsort implementation?
I think that it is hard to write a qsort which would fail that. That would
mean that it would compare the same items twice, which would be inefficient.
What? The same two elements aren't frequently compared pairwise with
each other, but of course an individual element is frequently compared
with other elements. Consider what happens when the chosen pivot element
changes its identity after already dividing half. The two partitions
will not be divided in any meaning full way anymore. I don't see how
this will results in a meaningful sort.
If the pivot element changes its identity won't the result be pretty much
random?That would be a very unlikely event, given the short time spent in
qsort.
Meh, we don't want to rely on "likeliness" on such things.
Greetings,
Andres Freund
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Hello Andres,
You can't allocate 4GB with palloc(), it has a builtin limit against
allocating more than 1GB.
Argh, too bad, I assumed very naively that palloc was malloc in disguise.
[...]
Well, then everytime the checkpointer is restarted.
Hm...
The point is that it's done at postmaster startup, and we're pretty much
guaranteed that the memory will availabl.e.
Ok ok, I stop resisting... I'll have a look.
Would it also fix the 1 GB palloc limit on the same go? I guess so...
That reasoning makes it impossible to move the fsyncing of files into the
loop (whenever we move to a new file). That's not nice.I do not see why.
Because it means that the sorting isn't necessarily correct. I.e. we
can't rely on it to determine whether a file has already been fsynced.
Ok, I understand your point.
Then the file would be fsynced twice: if the fsync is done properly (data
have already been flushed to disk) then it would not cost much, and doing
it sometimes twice on some file would not be a big issue. The code could
also detect such event and log a warning, which would give a hint about
how often it occurs in practice.
Hm. Is that actually the case for our qsort implementation?
I think that it is hard to write a qsort which would fail that. That would
mean that it would compare the same items twice, which would be inefficient.What? The same two elements aren't frequently compared pairwise with
each other, but of course an individual element is frequently compared
with other elements.
Sure.
Consider what happens when the chosen pivot element changes its identity
after already dividing half. The two partitions will not be divided in
any meaning full way anymore. I don't see how this will results in a
meaningful sort.
It would be partly meaningful, which is enough for performance, and does
not matter for correctness: currently buffers are not sorted at all and it
works, even if it does not work well.
If the pivot element changes its identity won't the result be pretty much
random?That would be a very unlikely event, given the short time spent in
qsort.Meh, we don't want to rely on "likeliness" on such things.
My main argument is that even if it occurs, and the qsort result is partly
wrong, it does not change correctness, it just mean that the actual writes
will be less in order than wished. If it occurs, one pivot separation
would be quite strange, but then others would be right, so the buffers
would be "partly sorted".
Another issue I see is that even if buffers are locked within cmp, the
status may change between two cmp... I do not think that locking all
buffers for sorting them is an option. So on the whole, I think that
locking buffers for sorting is probably not possible with the simple (and
efficient) lightweight approach used in the patch.
The good news, as I argued before, is that the order is only advisory to
help with performance, but the correctness is really that all checkpoint
buffers are written and fsync is called in the end, and does not depend on
the buffer order. That is how it currently works anyway.
If you block on this then I'll put a heavy weight approach, but that would
be a waste of memory in my opinion, hence my argumentation for the
lightweight approach.
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Ok ok, I stop resisting... I'll have a look.
Here is a v7 a&b version which uses shared memory instead of palloc.
--
Fabien.
Attachments:
checkpoint-continuous-flush-7-a.patchtext/x-diff; name=checkpoint-continuous-flush-7-a.patchDownload
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index e900dcc..1cec243 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -2454,6 +2454,28 @@ include_dir 'conf.d'
</listitem>
</varlistentry>
+ <varlistentry id="guc-checkpoint-sort" xreflabel="checkpoint_sort">
+ <term><varname>checkpoint_sort</varname> (<type>bool</type>)
+ <indexterm>
+ <primary><varname>checkpoint_sort</> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ Whether to sort buffers before writting them out to disk on checkpoint.
+ For a HDD storage, this setting allows to group together
+ neighboring pages written to disk, thus improving performance by
+ reducing random write activity.
+ This sorting should have limited performance effects on SSD backends
+ as such storages have good random write performance, but it may
+ help with wear-leveling so be worth keeping anyway.
+ The default is <literal>on</>.
+ This parameter can only be set in the <filename>postgresql.conf</>
+ file or on the server command line.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="guc-checkpoint-warning" xreflabel="checkpoint_warning">
<term><varname>checkpoint_warning</varname> (<type>integer</type>)
<indexterm>
diff --git a/doc/src/sgml/wal.sgml b/doc/src/sgml/wal.sgml
index e3941c9..f538698 100644
--- a/doc/src/sgml/wal.sgml
+++ b/doc/src/sgml/wal.sgml
@@ -546,6 +546,18 @@
</para>
<para>
+ When hard-disk drives (HDD) are used for terminal data storage
+ <xref linkend="guc-checkpoint-sort"> allows to sort pages
+ so that neighboring pages on disk will be flushed together by
+ chekpoints, reducing the random write load and improving performance.
+ If solid-state drives (SSD) are used, sorting pages induces no benefit
+ as their random write I/O performance is good: this feature could then
+ be disabled by setting <varname>checkpoint_sort</> to <value>off</>.
+ It is possible that sorting may help with SSD wear leveling, so it may
+ be kept on that account.
+ </para>
+
+ <para>
The number of WAL segment files in <filename>pg_xlog</> directory depends on
<varname>min_wal_size</>, <varname>max_wal_size</> and
the amount of WAL generated in previous checkpoint cycles. When old log
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 68e33eb..bee38ab 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -7995,11 +7995,13 @@ LogCheckpointEnd(bool restartpoint)
sync_secs,
total_secs,
longest_secs,
+ sort_secs,
average_secs;
int write_usecs,
sync_usecs,
total_usecs,
longest_usecs,
+ sort_usecs,
average_usecs;
uint64 average_sync_time;
@@ -8030,6 +8032,10 @@ LogCheckpointEnd(bool restartpoint)
CheckpointStats.ckpt_end_t,
&total_secs, &total_usecs);
+ TimestampDifference(CheckpointStats.ckpt_sort_t,
+ CheckpointStats.ckpt_sort_end_t,
+ &sort_secs, &sort_usecs);
+
/*
* Timing values returned from CheckpointStats are in microseconds.
* Convert to the second plus microsecond form that TimestampDifference
@@ -8048,8 +8054,8 @@ LogCheckpointEnd(bool restartpoint)
elog(LOG, "%s complete: wrote %d buffers (%.1f%%); "
"%d transaction log file(s) added, %d removed, %d recycled; "
- "write=%ld.%03d s, sync=%ld.%03d s, total=%ld.%03d s; "
- "sync files=%d, longest=%ld.%03d s, average=%ld.%03d s; "
+ "sort=%ld.%03d s, write=%ld.%03d s, sync=%ld.%03d s, total=%ld.%03d s;"
+ " sync files=%d, longest=%ld.%03d s, average=%ld.%03d s; "
"distance=%d kB, estimate=%d kB",
restartpoint ? "restartpoint" : "checkpoint",
CheckpointStats.ckpt_bufs_written,
@@ -8057,6 +8063,7 @@ LogCheckpointEnd(bool restartpoint)
CheckpointStats.ckpt_segs_added,
CheckpointStats.ckpt_segs_removed,
CheckpointStats.ckpt_segs_recycled,
+ sort_secs, sort_usecs / 1000,
write_secs, write_usecs / 1000,
sync_secs, sync_usecs / 1000,
total_secs, total_usecs / 1000,
diff --git a/src/backend/storage/buffer/buf_init.c b/src/backend/storage/buffer/buf_init.c
index 3ae2848..ec2436f 100644
--- a/src/backend/storage/buffer/buf_init.c
+++ b/src/backend/storage/buffer/buf_init.c
@@ -65,7 +65,8 @@ void
InitBufferPool(void)
{
bool foundBufs,
- foundDescs;
+ foundDescs,
+ foundCpid;
/* Align descriptors to a cacheline boundary. */
BufferDescriptors = (BufferDescPadded *) CACHELINEALIGN(
@@ -77,10 +78,14 @@ InitBufferPool(void)
ShmemInitStruct("Buffer Blocks",
NBuffers * (Size) BLCKSZ, &foundBufs);
- if (foundDescs || foundBufs)
+ CheckpointBufferIds = (int *)
+ ShmemInitStruct("Checkpoint BufferIds",
+ NBuffers * sizeof(int), &foundCpid);
+
+ if (foundDescs || foundBufs || foundCpid)
{
- /* both should be present or neither */
- Assert(foundDescs && foundBufs);
+ /* all should be present or neither */
+ Assert(foundDescs && foundBufs && foundCpid);
/* note: this path is only taken in EXEC_BACKEND case */
}
else
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index e4b25587..ba5298d 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -80,6 +80,7 @@ bool zero_damaged_pages = false;
int bgwriter_lru_maxpages = 100;
double bgwriter_lru_multiplier = 2.0;
bool track_io_timing = false;
+bool checkpoint_sort = true;
/*
* How many buffers PrefetchBuffer callers should try to stay ahead of their
@@ -95,6 +96,9 @@ static bool IsForInput;
/* local state for LockBufferForCleanup */
static volatile BufferDesc *PinCountWaitBuf = NULL;
+/* Array of buffer ids of all buffers to checkpoint */
+int * CheckpointBufferIds = NULL;
+
/*
* Backend-Private refcount management:
*
@@ -1561,6 +1565,136 @@ UnpinBuffer(volatile BufferDesc *buf, bool fixOwner)
}
}
+/* Compare checkpoint buffers.
+ * No lock is acquired, see comments below.
+ */
+static int bufcmp(const void * pa, const void * pb)
+{
+ BufferDesc
+ *a = GetBufferDescriptor(* (int *) pa),
+ *b = GetBufferDescriptor(* (int *) pb);
+
+ /* tag: rnode, forkNum (different files), blockNum
+ * rnode: { spcNode (ignore: not really needed),
+ * dbNode (ignore: this is a directory), relNode }
+ * spcNode: table space oid, not that there are at least two
+ * (pg_global and pg_default).
+ */
+ /* compare relation */
+ if (a->tag.rnode.relNode < b->tag.rnode.relNode)
+ return -1;
+ else if (a->tag.rnode.relNode > b->tag.rnode.relNode)
+ return 1;
+ /* same relation, compare fork */
+ else if (a->tag.forkNum < b->tag.forkNum)
+ return -1;
+ else if (a->tag.forkNum > b->tag.forkNum)
+ return 1;
+ /* same relation/fork, so same segmented "file", compare block number
+ * which are mapped on different segments depending on the number.
+ */
+ else if (a->tag.blockNum < b->tag.blockNum)
+ return -1;
+ else /* should not be the same block anyway... */
+ return 1;
+}
+
+/* Status of buffers to checkpoint for a particular tablespace,
+ * used internally in BufferSync.
+ * - space: oid of the tablespace
+ * - num_to_write: number of checkpoint pages counted for this tablespace
+ * - num_written: number of pages actually written out
+ * - index: scanning position in CheckpointBufferIds for this tablespace
+ */
+typedef struct TableSpaceCheckpointStatus {
+ Oid space;
+ int num_to_write;
+ int num_written;
+ int index;
+} TableSpaceCheckpointStatus;
+
+/* entry structure for table space to count hashtable,
+ * used internally in BufferSync.
+ */
+typedef struct TableSpaceCountEntry {
+ Oid space;
+ int count;
+} TableSpaceCountEntry;
+
+/* return the next buffer to write, or NULL if none.
+ * this function balances buffers over tablespaces.
+ */
+static int
+NextBufferToWrite(
+ TableSpaceCheckpointStatus *spcStatus, int nb_spaces,
+ int *pspace, int num_to_write, int num_written)
+{
+ int space = *pspace, buf_id = -1, index;
+
+ /*
+ * Select a tablespace depending on the current overall progress.
+ *
+ * The progress ratio of each unfinished tablespace is compared to
+ * the overall progress ratio to find one with is not in advance
+ * (i.e. tablespace ratio <= overall ratio).
+ *
+ * Existence: it is bound to exist otherwise the overall progress
+ * ratio would be inconsistent: with positive buffers to write (t1 & t2)
+ * and already written buffers (w1 & w2), we have:
+ *
+ * If w1/t1 > (w1+w2)/(t1+t2) # one table space is in advance
+ * => w1t1+w1t2 > w1t1+w2t1 => w1t2 > w2t1 => w1t2+w2t2 > w2t1+w2t2
+ * => (w1+w2) / (t1+t2) > w2 / t2 # the other one is late
+ *
+ * The round robin ensures that each space is given some attention
+ * till it is over the current ratio, before going to the next.
+ *
+ * Precision: using int32 computations for comparing fractions
+ * (w1 / t1 > w / t <=> w1 t > w t1) seems a bad idea as the values
+ * can overflow 32-bit integers: the limit would be sqrt(2**31) ~
+ * 46340 buffers, i.e. a 362 MB checkpoint. So ensure that 64-bit
+ * integers are used in the comparison.
+ */
+ while (/* compare tablespace vs overall progress ratio:
+ * tablespace written/to_write > overall written/to_write
+ */
+ (int64) spcStatus[space].num_written * num_to_write >
+ (int64) num_written * spcStatus[space].num_to_write)
+ space = (space + 1) % nb_spaces; /* round robin */
+
+ /*
+ * Find a valid buffer in the selected tablespace,
+ * by continuing the tablespace specific buffer scan
+ * where it was left.
+ */
+ index = spcStatus[space].index;
+
+ while (index < num_to_write && buf_id == -1)
+ {
+ volatile BufferDesc *bufHdr;
+
+ buf_id = CheckpointBufferIds[index];
+ bufHdr = GetBufferDescriptor(buf_id);
+
+ /* Skip if in another tablespace or not in checkpoint anymore.
+ * No lock is acquired, see comments below.
+ */
+ if (spcStatus[space].space != bufHdr->tag.rnode.spcNode ||
+ ! (bufHdr->flags & BM_CHECKPOINT_NEEDED))
+ {
+ index ++;
+ buf_id = -1;
+ }
+ }
+
+ /* Update tablespace writing status, will start over at next index */
+ spcStatus[space].index = index+1;
+
+ *pspace = space;
+
+ return buf_id;
+}
+
/*
* BufferSync -- Write out all dirty buffers in the pool.
*
@@ -1574,11 +1708,13 @@ UnpinBuffer(volatile BufferDesc *buf, bool fixOwner)
static void
BufferSync(int flags)
{
- int buf_id;
- int num_to_scan;
+ int buf_id = -1;
int num_to_write;
int num_written;
int mask = BM_DIRTY;
+ HTAB *spcBuffers;
+ TableSpaceCheckpointStatus *spcStatus = NULL;
+ int nb_spaces, space;
/* Make sure we can handle the pin inside SyncOneBuffer */
ResourceOwnerEnlargeBuffers(CurrentResourceOwner);
@@ -1609,6 +1745,18 @@ BufferSync(int flags)
* certainly need to be written for the next checkpoint attempt, too.
*/
num_to_write = 0;
+
+ /* initialize oid -> int buffer count hash table */
+ {
+ HASHCTL ctl;
+
+ MemSet(&ctl, 0, sizeof(HASHCTL));
+ ctl.keysize = sizeof(Oid);
+ ctl.entrysize = sizeof(TableSpaceCountEntry);
+ spcBuffers = hash_create("Number of buffers to write per tablespace",
+ 16, &ctl, HASH_ELEM | HASH_BLOBS);
+ }
+
for (buf_id = 0; buf_id < NBuffers; buf_id++)
{
volatile BufferDesc *bufHdr = GetBufferDescriptor(buf_id);
@@ -1621,32 +1769,107 @@ BufferSync(int flags)
if ((bufHdr->flags & mask) == mask)
{
+ Oid spc;
+ TableSpaceCountEntry * entry;
+ bool found;
+
bufHdr->flags |= BM_CHECKPOINT_NEEDED;
+ CheckpointBufferIds[num_to_write] = buf_id;
num_to_write++;
+
+ /* keep track of per tablespace buffers */
+ spc = bufHdr->tag.rnode.spcNode;
+ entry = (TableSpaceCountEntry *)
+ hash_search(spcBuffers, (void *) &spc, HASH_ENTER, &found);
+
+ if (found) entry->count++;
+ else entry->count = 1;
}
UnlockBufHdr(bufHdr);
}
if (num_to_write == 0)
+ {
+ hash_destroy(spcBuffers);
return; /* nothing to do */
+ }
TRACE_POSTGRESQL_BUFFER_SYNC_START(NBuffers, num_to_write);
+ /* Build checkpoint tablespace buffer status */
+ nb_spaces = hash_get_num_entries(spcBuffers);
+ spcStatus = (TableSpaceCheckpointStatus *)
+ palloc(sizeof(TableSpaceCheckpointStatus) * nb_spaces);
+
+ {
+ int index = 0;
+ HASH_SEQ_STATUS hseq;
+ TableSpaceCountEntry * entry;
+
+ hash_seq_init(&hseq, spcBuffers);
+ while ((entry = (TableSpaceCountEntry *) hash_seq_search(&hseq)))
+ {
+ Assert(index < nb_spaces);
+ spcStatus[index].space = entry->space;
+ spcStatus[index].num_to_write = entry->count;
+ spcStatus[index].num_written = 0;
+ /* should it be randomized? chosen with some criterion? */
+ spcStatus[index].index = 0;
+
+ index ++;
+ }
+ }
+
+ hash_destroy(spcBuffers);
+ spcBuffers = NULL;
+
/*
- * Loop over all buffers again, and write the ones (still) marked with
- * BM_CHECKPOINT_NEEDED. In this loop, we start at the clock sweep point
- * since we might as well dump soon-to-be-recycled buffers first.
+ * Sort buffer ids to help find sequential writes.
*
- * Note that we don't read the buffer alloc count here --- that should be
- * left untouched till the next BgBufferSync() call.
+ * Note: Buffers are not locked in any way during sorting, but that's ok:
+ * A change in the buffer header is only relevant when it changes the
+ * buffer's identity. If the identity has changed it'll have been
+ * written out by BufferAlloc(), so there's no need for checkpointer to
+ * write it out anymore. The buffer might also get written out by a
+ * backend or bgwriter, but that's equally harmless.
+ *
+ * Marked buffers must not be move during the checkpoint.
+ * Also, qsort implementation should be resilient to occasional
+ * contradictions (cmp(a,b) != -cmp(b,a)) because of possible
+ * concurrent changes.
*/
- buf_id = StrategySyncStart(NULL, NULL);
- num_to_scan = NBuffers;
+ CheckpointStats.ckpt_sort_t = GetCurrentTimestamp();
+
+ if (checkpoint_sort)
+ {
+ qsort(CheckpointBufferIds, num_to_write, sizeof(int),
+ (int(*)(const void *, const void *)) bufcmp);
+ }
+
+ CheckpointStats.ckpt_sort_end_t = GetCurrentTimestamp();
+
+ /*
+ * Loop over buffers to write through CheckpointBufferIds,
+ * and write the ones (still) marked with BM_CHECKPOINT_NEEDED,
+ * with some round robin over table spaces so as to balance writes,
+ * so that buffer writes move forward roughly proportionally for each
+ * tablespace.
+ *
+ * Termination: if a tablespace is selected by the inner while loop
+ * (see argument there), its index is incremented and will eventually
+ * reach num_to_write, mark this table space scanning as done and
+ * decrement the number of (active) spaces, which will thus reach 0.
+ */
+ space = 0;
num_written = 0;
- while (num_to_scan-- > 0)
+
+ while (nb_spaces != 0)
{
- volatile BufferDesc *bufHdr = GetBufferDescriptor(buf_id);
+ volatile BufferDesc *bufHdr;
+ buf_id = NextBufferToWrite(spcStatus, nb_spaces, &space,
+ num_to_write, num_written);
+ bufHdr = GetBufferDescriptor(buf_id);
/*
* We don't need to acquire the lock here, because we're only looking
@@ -1660,39 +1883,45 @@ BufferSync(int flags)
* write the buffer though we didn't need to. It doesn't seem worth
* guarding against this, though.
*/
- if (bufHdr->flags & BM_CHECKPOINT_NEEDED)
+ if (bufHdr != NULL && bufHdr->flags & BM_CHECKPOINT_NEEDED)
{
if (SyncOneBuffer(buf_id, false) & BUF_WRITTEN)
{
TRACE_POSTGRESQL_BUFFER_SYNC_WRITTEN(buf_id);
BgWriterStats.m_buf_written_checkpoints++;
+ spcStatus[space].num_written++;
num_written++;
/*
- * We know there are at most num_to_write buffers with
- * BM_CHECKPOINT_NEEDED set; so we can stop scanning if
- * num_written reaches num_to_write.
- *
- * Note that num_written doesn't include buffers written by
- * other backends, or by the bgwriter cleaning scan. That
- * means that the estimate of how much progress we've made is
- * conservative, and also that this test will often fail to
- * trigger. But it seems worth making anyway.
- */
- if (num_written >= num_to_write)
- break;
-
- /*
* Sleep to throttle our I/O rate.
*/
CheckpointWriteDelay(flags, (double) num_written / num_to_write);
}
}
- if (++buf_id >= NBuffers)
- buf_id = 0;
+ /*
+ * Detect checkpoint end for a tablespace: either the scan is done
+ * or all tablespace buffers have been written out. If so, the
+ * another active tablespace status is moved in place of the current
+ * one and the next round will start on this one, or maybe round about.
+ * Note: maybe an exchange could be made instead in order to keep
+ * informations about the closed table space, but this is currently
+ * not used afterwards.
+ */
+ if (spcStatus[space].index >= num_to_write ||
+ spcStatus[space].num_written >= spcStatus[space].num_to_write)
+ {
+ nb_spaces--;
+ if (space != nb_spaces)
+ spcStatus[space] = spcStatus[nb_spaces];
+ else
+ space = 0;
+ }
}
+ pfree(spcStatus);
+ spcStatus = NULL;
+
/*
* Update checkpoint statistics. As noted above, this doesn't include
* buffers written by other backends or bgwriter scan.
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index b3dac51..ff95e61 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -1013,6 +1013,17 @@ static struct config_bool ConfigureNamesBool[] =
false,
NULL, NULL, NULL
},
+
+ {
+ {"checkpoint_sort", PGC_SIGHUP, WAL_CHECKPOINTS,
+ gettext_noop("Whether disk-page buffers are sorted on checkpoints."),
+ NULL
+ },
+ &checkpoint_sort,
+ true,
+ NULL, NULL, NULL
+ },
+
{
{"log_connections", PGC_SU_BACKEND, LOGGING_WHAT,
gettext_noop("Logs each successful connection."),
@@ -1798,6 +1809,9 @@ static struct config_int ConfigureNamesInt[] =
/*
* We sometimes multiply the number of shared buffers by two without
* checking for overflow, so we mustn't allow more than INT_MAX / 2.
+ * Also, checkpoint uses a malloced int array to store index of shared
+ * buffers for sorting, which results in a SIZE_MAX / sizeof(int) limit,
+ * that is UINT_MAX / 4 == INT_MAX / 2 as well on a 32 bits system.
*/
{
{"shared_buffers", PGC_POSTMASTER, RESOURCES_MEM,
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index e5d275d..e84f380 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -201,6 +201,7 @@
#max_wal_size = 1GB
#min_wal_size = 80MB
#checkpoint_completion_target = 0.5 # checkpoint target duration, 0.0 - 1.0
+#checkpoint_sort = on # sort buffers on checkpoint
#checkpoint_warning = 30s # 0 disables
# - Archiving -
diff --git a/src/include/access/xlog.h b/src/include/access/xlog.h
index 6dacee2..dbd4757 100644
--- a/src/include/access/xlog.h
+++ b/src/include/access/xlog.h
@@ -186,6 +186,8 @@ extern bool XLOG_DEBUG;
typedef struct CheckpointStatsData
{
TimestampTz ckpt_start_t; /* start of checkpoint */
+ TimestampTz ckpt_sort_t; /* start buffer sorting */
+ TimestampTz ckpt_sort_end_t; /* end of sorting */
TimestampTz ckpt_write_t; /* start of flushing buffers */
TimestampTz ckpt_sync_t; /* start of fsyncs */
TimestampTz ckpt_sync_end_t; /* end of fsyncs */
diff --git a/src/include/storage/buf_internals.h b/src/include/storage/buf_internals.h
index 521ee1c..4cb3a60 100644
--- a/src/include/storage/buf_internals.h
+++ b/src/include/storage/buf_internals.h
@@ -210,6 +210,8 @@ extern PGDLLIMPORT BufferDescPadded *BufferDescriptors;
/* in localbuf.c */
extern BufferDesc *LocalBufferDescriptors;
+/* in bufmgr.c */
+extern int *CheckpointBufferIds;
/*
* Internal routines: only called by bufmgr
diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.h
index ec0a254..c228f39 100644
--- a/src/include/storage/bufmgr.h
+++ b/src/include/storage/bufmgr.h
@@ -54,6 +54,7 @@ extern int bgwriter_lru_maxpages;
extern double bgwriter_lru_multiplier;
extern bool track_io_timing;
extern int target_prefetch_pages;
+extern bool checkpoint_sort;
/* in buf_init.c */
extern PGDLLIMPORT char *BufferBlocks;
checkpoint-continuous-flush-7-b.patchtext/x-diff; name=checkpoint-continuous-flush-7-b.patchDownload
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 1cec243..2551d95 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -2497,6 +2497,24 @@ include_dir 'conf.d'
</listitem>
</varlistentry>
+ <varlistentry id="guc-checkpoint-flush-to-disk" xreflabel="checkpoint_flush_to_disk">
+ <term><varname>checkpoint_flush_to_disk</varname> (<type>bool</type>)
+ <indexterm>
+ <primary><varname>checkpoint_flush_to_disk</> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ When writing data for a checkpoint, hint the underlying OS that the
+ data must be sent to disk as soon as possible. This may help smoothing
+ disk I/O writes and avoid a stall when fsync is issued at the end of
+ the checkpoint, but it may also reduce average performance.
+ This setting may have no effect on some platforms.
+ The default is <literal>off</>.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="guc-min-wal-size" xreflabel="min_wal_size">
<term><varname>min_wal_size</varname> (<type>integer</type>)
<indexterm>
diff --git a/doc/src/sgml/wal.sgml b/doc/src/sgml/wal.sgml
index f538698..eea6668 100644
--- a/doc/src/sgml/wal.sgml
+++ b/doc/src/sgml/wal.sgml
@@ -558,6 +558,17 @@
</para>
<para>
+ On Linux and POSIX platforms, <xref linkend="guc-checkpoint-flush-to-disk">
+ allows to hint the OS that pages written on checkpoints must be flushed
+ to disk quickly. Otherwise, these pages may be kept in cache for some time,
+ inducing a stall later when <literal>fsync</> is called to actually
+ complete the checkpoint. This setting helps to reduce transaction latency,
+ but it may also have a small adverse effect on the average transaction rate
+ at maximum throughput. This feature probably brings no benefit on SSD,
+ as the I/O write latency is small on such hardware, thus it may be disabled.
+ </para>
+
+ <para>
The number of WAL segment files in <filename>pg_xlog</> directory depends on
<varname>min_wal_size</>, <varname>max_wal_size</> and
the amount of WAL generated in previous checkpoint cycles. When old log
diff --git a/src/backend/access/heap/rewriteheap.c b/src/backend/access/heap/rewriteheap.c
index bcce3e3..f565dc4 100644
--- a/src/backend/access/heap/rewriteheap.c
+++ b/src/backend/access/heap/rewriteheap.c
@@ -918,7 +918,7 @@ logical_heap_rewrite_flush_mappings(RewriteState state)
* Note that we deviate from the usual WAL coding practices here,
* check the above "Logical rewrite support" comment for reasoning.
*/
- written = FileWrite(src->vfd, waldata_start, len);
+ written = FileWrite(src->vfd, waldata_start, len, false, NULL);
if (written != len)
ereport(ERROR,
(errcode_for_file_access(),
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index cf4a6dc..4b5e9cd 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -203,7 +203,7 @@ btbuildempty(PG_FUNCTION_ARGS)
/* Write the page. If archiving/streaming, XLOG it. */
PageSetChecksumInplace(metapage, BTREE_METAPAGE);
smgrwrite(index->rd_smgr, INIT_FORKNUM, BTREE_METAPAGE,
- (char *) metapage, true);
+ (char *) metapage, true, false, NULL);
if (XLogIsNeeded())
log_newpage(&index->rd_smgr->smgr_rnode.node, INIT_FORKNUM,
BTREE_METAPAGE, metapage, false);
diff --git a/src/backend/access/nbtree/nbtsort.c b/src/backend/access/nbtree/nbtsort.c
index f95f67a..ea7a45d 100644
--- a/src/backend/access/nbtree/nbtsort.c
+++ b/src/backend/access/nbtree/nbtsort.c
@@ -315,7 +315,7 @@ _bt_blwritepage(BTWriteState *wstate, Page page, BlockNumber blkno)
{
/* overwriting a block we zero-filled before */
smgrwrite(wstate->index->rd_smgr, MAIN_FORKNUM, blkno,
- (char *) page, true);
+ (char *) page, true, false, NULL);
}
pfree(page);
diff --git a/src/backend/access/spgist/spginsert.c b/src/backend/access/spgist/spginsert.c
index bceee8d..b700efb 100644
--- a/src/backend/access/spgist/spginsert.c
+++ b/src/backend/access/spgist/spginsert.c
@@ -170,7 +170,7 @@ spgbuildempty(PG_FUNCTION_ARGS)
/* Write the page. If archiving/streaming, XLOG it. */
PageSetChecksumInplace(page, SPGIST_METAPAGE_BLKNO);
smgrwrite(index->rd_smgr, INIT_FORKNUM, SPGIST_METAPAGE_BLKNO,
- (char *) page, true);
+ (char *) page, true, false, NULL);
if (XLogIsNeeded())
log_newpage(&index->rd_smgr->smgr_rnode.node, INIT_FORKNUM,
SPGIST_METAPAGE_BLKNO, page, false);
@@ -180,7 +180,7 @@ spgbuildempty(PG_FUNCTION_ARGS)
PageSetChecksumInplace(page, SPGIST_ROOT_BLKNO);
smgrwrite(index->rd_smgr, INIT_FORKNUM, SPGIST_ROOT_BLKNO,
- (char *) page, true);
+ (char *) page, true, false, NULL);
if (XLogIsNeeded())
log_newpage(&index->rd_smgr->smgr_rnode.node, INIT_FORKNUM,
SPGIST_ROOT_BLKNO, page, true);
@@ -190,7 +190,7 @@ spgbuildempty(PG_FUNCTION_ARGS)
PageSetChecksumInplace(page, SPGIST_NULL_BLKNO);
smgrwrite(index->rd_smgr, INIT_FORKNUM, SPGIST_NULL_BLKNO,
- (char *) page, true);
+ (char *) page, true, false, NULL);
if (XLogIsNeeded())
log_newpage(&index->rd_smgr->smgr_rnode.node, INIT_FORKNUM,
SPGIST_NULL_BLKNO, page, true);
diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c
index 3b3a09e..e361907 100644
--- a/src/backend/postmaster/checkpointer.c
+++ b/src/backend/postmaster/checkpointer.c
@@ -665,7 +665,8 @@ ImmediateCheckpointRequested(void)
* fraction between 0.0 meaning none, and 1.0 meaning all done.
*/
void
-CheckpointWriteDelay(int flags, double progress)
+CheckpointWriteDelay(int flags, double progress,
+ FileFlushContext * context, int ctx_size)
{
static int absorb_counter = WRITES_PER_ABSORB;
@@ -700,6 +701,26 @@ CheckpointWriteDelay(int flags, double progress)
*/
pgstat_send_bgwriter();
+#if defined(HAVE_SYNC_FILE_RANGE) || defined(HAVE_POSIX_FADVISE)
+
+ /*
+ * Before sleeping, flush written blocks for each tablespace.
+ */
+ if (checkpoint_flush_to_disk)
+ {
+ int i;
+
+ for (i = 0; i < ctx_size; i++)
+ {
+ if (context[i].ncalls != 0)
+ {
+ PerformFileFlush(&context[i]);
+ ResetFileFlushContext(&context[i]);
+ }
+ }
+ }
+#endif /* HAVE_SYNC_FILE_RANGE || HAVE_POSIX_FADVISE */
+
/*
* This sleep used to be connected to bgwriter_delay, typically 200ms.
* That resulted in more frequent wakeups if not much work to do.
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index ba5298d..aa7694c 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -80,6 +80,8 @@ bool zero_damaged_pages = false;
int bgwriter_lru_maxpages = 100;
double bgwriter_lru_multiplier = 2.0;
bool track_io_timing = false;
+/* hint to move writes to high priority */
+bool checkpoint_flush_to_disk = false;
bool checkpoint_sort = true;
/*
@@ -400,7 +402,8 @@ static bool PinBuffer(volatile BufferDesc *buf, BufferAccessStrategy strategy);
static void PinBuffer_Locked(volatile BufferDesc *buf);
static void UnpinBuffer(volatile BufferDesc *buf, bool fixOwner);
static void BufferSync(int flags);
-static int SyncOneBuffer(int buf_id, bool skip_recently_used);
+static int SyncOneBuffer(int buf_id, bool skip_recently_used,
+ bool flush_to_disk, FileFlushContext *context);
static void WaitIO(volatile BufferDesc *buf);
static bool StartBufferIO(volatile BufferDesc *buf, bool forInput);
static void TerminateBufferIO(volatile BufferDesc *buf, bool clear_dirty,
@@ -413,7 +416,8 @@ static volatile BufferDesc *BufferAlloc(SMgrRelation smgr,
BlockNumber blockNum,
BufferAccessStrategy strategy,
bool *foundPtr);
-static void FlushBuffer(volatile BufferDesc *buf, SMgrRelation reln);
+static void FlushBuffer(volatile BufferDesc *buf, SMgrRelation reln,
+ bool flush_to_disk, FileFlushContext *context);
static void AtProcExit_Buffers(int code, Datum arg);
static void CheckForBufferLeaks(void);
static int rnode_comparator(const void *p1, const void *p2);
@@ -1022,7 +1026,7 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
smgr->smgr_rnode.node.dbNode,
smgr->smgr_rnode.node.relNode);
- FlushBuffer(buf, NULL);
+ FlushBuffer(buf, NULL, false, NULL);
LWLockRelease(buf->content_lock);
TRACE_POSTGRESQL_BUFFER_WRITE_DIRTY_DONE(forkNum, blockNum,
@@ -1715,6 +1719,7 @@ BufferSync(int flags)
HTAB *spcBuffers;
TableSpaceCheckpointStatus *spcStatus = NULL;
int nb_spaces, space;
+ FileFlushContext * spcContext = NULL;
/* Make sure we can handle the pin inside SyncOneBuffer */
ResourceOwnerEnlargeBuffers(CurrentResourceOwner);
@@ -1797,10 +1802,12 @@ BufferSync(int flags)
TRACE_POSTGRESQL_BUFFER_SYNC_START(NBuffers, num_to_write);
- /* Build checkpoint tablespace buffer status */
+ /* Build checkpoint tablespace buffer status & flush context arrays */
nb_spaces = hash_get_num_entries(spcBuffers);
spcStatus = (TableSpaceCheckpointStatus *)
palloc(sizeof(TableSpaceCheckpointStatus) * nb_spaces);
+ spcContext = (FileFlushContext *)
+ palloc(sizeof(FileFlushContext) * nb_spaces);
{
int index = 0;
@@ -1817,6 +1824,12 @@ BufferSync(int flags)
/* should it be randomized? chosen with some criterion? */
spcStatus[index].index = 0;
+#if defined(HAVE_SYNC_FILE_RANGE) || defined(HAVE_POSIX_FADVISE)
+
+ ResetFileFlushContext(&spcContext[index]);
+
+#endif /* HAVE_SYNC_FILE_RANGE || HAVE_POSIX_FADVISE */
+
index ++;
}
}
@@ -1885,7 +1898,8 @@ BufferSync(int flags)
*/
if (bufHdr != NULL && bufHdr->flags & BM_CHECKPOINT_NEEDED)
{
- if (SyncOneBuffer(buf_id, false) & BUF_WRITTEN)
+ if (SyncOneBuffer(buf_id, false, checkpoint_flush_to_disk,
+ &spcContext[space]) & BUF_WRITTEN)
{
TRACE_POSTGRESQL_BUFFER_SYNC_WRITTEN(buf_id);
BgWriterStats.m_buf_written_checkpoints++;
@@ -1895,7 +1909,8 @@ BufferSync(int flags)
/*
* Sleep to throttle our I/O rate.
*/
- CheckpointWriteDelay(flags, (double) num_written / num_to_write);
+ CheckpointWriteDelay(flags, (double) num_written / num_to_write,
+ spcContext, nb_spaces);
}
}
@@ -1911,6 +1926,13 @@ BufferSync(int flags)
if (spcStatus[space].index >= num_to_write ||
spcStatus[space].num_written >= spcStatus[space].num_to_write)
{
+#if defined(HAVE_SYNC_FILE_RANGE) || defined(HAVE_POSIX_FADVISE)
+
+ PerformFileFlush(&spcContext[space]);
+ ResetFileFlushContext(&spcContext[space]);
+
+#endif /* HAVE_SYNC_FILE_RANGE || HAVE_POSIX_FADVISE */
+
nb_spaces--;
if (space != nb_spaces)
spcStatus[space] = spcStatus[nb_spaces];
@@ -1921,6 +1943,8 @@ BufferSync(int flags)
pfree(spcStatus);
spcStatus = NULL;
+ pfree(spcContext);
+ spcContext = NULL;
/*
* Update checkpoint statistics. As noted above, this doesn't include
@@ -2168,7 +2192,8 @@ BgBufferSync(void)
/* Execute the LRU scan */
while (num_to_scan > 0 && reusable_buffers < upcoming_alloc_est)
{
- int buffer_state = SyncOneBuffer(next_to_clean, true);
+ int buffer_state =
+ SyncOneBuffer(next_to_clean, true, false, NULL);
if (++next_to_clean >= NBuffers)
{
@@ -2245,7 +2270,8 @@ BgBufferSync(void)
* Note: caller must have done ResourceOwnerEnlargeBuffers.
*/
static int
-SyncOneBuffer(int buf_id, bool skip_recently_used)
+SyncOneBuffer(int buf_id, bool skip_recently_used, bool flush_to_disk,
+ FileFlushContext * context)
{
volatile BufferDesc *bufHdr = GetBufferDescriptor(buf_id);
int result = 0;
@@ -2286,7 +2312,7 @@ SyncOneBuffer(int buf_id, bool skip_recently_used)
PinBuffer_Locked(bufHdr);
LWLockAcquire(bufHdr->content_lock, LW_SHARED);
- FlushBuffer(bufHdr, NULL);
+ FlushBuffer(bufHdr, NULL, flush_to_disk, context);
LWLockRelease(bufHdr->content_lock);
UnpinBuffer(bufHdr, true);
@@ -2548,9 +2574,16 @@ BufferGetTag(Buffer buffer, RelFileNode *rnode, ForkNumber *forknum,
*
* If the caller has an smgr reference for the buffer's relation, pass it
* as the second parameter. If not, pass NULL.
+ *
+ * The third parameter tries to hint the OS that a high priority write is meant,
+ * possibly because io-throttling is already managed elsewhere.
+ * The last parameter holds the current flush context that accumulates flush
+ * requests to be performed in one call, instead of being performed on a buffer
+ * per buffer basis.
*/
static void
-FlushBuffer(volatile BufferDesc *buf, SMgrRelation reln)
+FlushBuffer(volatile BufferDesc *buf, SMgrRelation reln, bool flush_to_disk,
+ FileFlushContext * context)
{
XLogRecPtr recptr;
ErrorContextCallback errcallback;
@@ -2639,7 +2672,9 @@ FlushBuffer(volatile BufferDesc *buf, SMgrRelation reln)
buf->tag.forkNum,
buf->tag.blockNum,
bufToWrite,
- false);
+ false,
+ flush_to_disk,
+ context);
if (track_io_timing)
{
@@ -3059,7 +3094,9 @@ FlushRelationBuffers(Relation rel)
bufHdr->tag.forkNum,
bufHdr->tag.blockNum,
localpage,
- false);
+ false,
+ false,
+ NULL);
bufHdr->flags &= ~(BM_DIRTY | BM_JUST_DIRTIED);
@@ -3093,7 +3130,7 @@ FlushRelationBuffers(Relation rel)
{
PinBuffer_Locked(bufHdr);
LWLockAcquire(bufHdr->content_lock, LW_SHARED);
- FlushBuffer(bufHdr, rel->rd_smgr);
+ FlushBuffer(bufHdr, rel->rd_smgr, false, NULL);
LWLockRelease(bufHdr->content_lock);
UnpinBuffer(bufHdr, true);
}
@@ -3145,7 +3182,7 @@ FlushDatabaseBuffers(Oid dbid)
{
PinBuffer_Locked(bufHdr);
LWLockAcquire(bufHdr->content_lock, LW_SHARED);
- FlushBuffer(bufHdr, NULL);
+ FlushBuffer(bufHdr, NULL, false, NULL);
LWLockRelease(bufHdr->content_lock);
UnpinBuffer(bufHdr, true);
}
diff --git a/src/backend/storage/buffer/localbuf.c b/src/backend/storage/buffer/localbuf.c
index 3144afe..114a0a6 100644
--- a/src/backend/storage/buffer/localbuf.c
+++ b/src/backend/storage/buffer/localbuf.c
@@ -208,7 +208,9 @@ LocalBufferAlloc(SMgrRelation smgr, ForkNumber forkNum, BlockNumber blockNum,
bufHdr->tag.forkNum,
bufHdr->tag.blockNum,
localpage,
- false);
+ false,
+ false,
+ NULL);
/* Mark not-dirty now in case we error out below */
bufHdr->flags &= ~BM_DIRTY;
diff --git a/src/backend/storage/file/buffile.c b/src/backend/storage/file/buffile.c
index ea4d689..fb3b383 100644
--- a/src/backend/storage/file/buffile.c
+++ b/src/backend/storage/file/buffile.c
@@ -317,7 +317,7 @@ BufFileDumpBuffer(BufFile *file)
return; /* seek failed, give up */
file->offsets[file->curFile] = file->curOffset;
}
- bytestowrite = FileWrite(thisfile, file->buffer + wpos, bytestowrite);
+ bytestowrite = FileWrite(thisfile, file->buffer + wpos, bytestowrite, false, NULL);
if (bytestowrite <= 0)
return; /* failed to write */
file->offsets[file->curFile] += bytestowrite;
diff --git a/src/backend/storage/file/fd.c b/src/backend/storage/file/fd.c
index 1ba4946..daf03e4 100644
--- a/src/backend/storage/file/fd.c
+++ b/src/backend/storage/file/fd.c
@@ -1344,8 +1344,97 @@ retry:
return returnCode;
}
+#if defined(HAVE_SYNC_FILE_RANGE) || defined(HAVE_POSIX_FADVISE)
+
+void
+ResetFileFlushContext(FileFlushContext * context)
+{
+ context->fd = 0;
+ context->ncalls = 0;
+ context->offset = 0;
+ context->nbytes = 0;
+ context->filename = NULL;
+}
+
+void
+PerformFileFlush(FileFlushContext * context)
+{
+ if (context->ncalls != 0)
+ {
+ int rc;
+
+#if defined(HAVE_SYNC_FILE_RANGE)
+
+ /* Linux: tell the memory manager to move these blocks to io so
+ * that they are considered for being actually written to disk.
+ */
+ rc = sync_file_range(context->fd, context->offset, context->nbytes,
+ SYNC_FILE_RANGE_WRITE);
+
+#elif defined(HAVE_POSIX_FADVISE)
+
+ /* Others: say that data should not be kept in memory...
+ * This is not exactly what we want to say, because we want to write
+ * the data for durability but we may need it later nevertheless.
+ * It seems that Linux would free the memory *if* the data has
+ * already been written do disk, else the "dontneed" call is ignored.
+ * For FreeBSD this may have the desired effect of moving the
+ * data to the io layer, although the system does not seem to
+ * take into account the provided offset & size, so it is rather
+ * rough...
+ */
+ rc = posix_fadvise(context->fd, context->offset, context->nbytes,
+ POSIX_FADV_DONTNEED);
+
+#endif
+
+ if (rc < 0)
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not flush block " INT64_FORMAT
+ " on " INT64_FORMAT " blocks in file \"%s\": %m",
+ context->offset / BLCKSZ,
+ context->nbytes / BLCKSZ,
+ context->filename)));
+ }
+}
+
+void
+FileAsynchronousFlush(FileFlushContext * context,
+ int fd, off_t offset, off_t nbytes, char * filename)
+{
+ if (context->ncalls != 0 && context->fd == fd)
+ {
+ /* Same file: merge current flush with previous ones */
+ off_t new_offset = offset < context->offset? offset: context->offset;
+
+ context->nbytes =
+ (context->offset + context->nbytes > offset + nbytes ?
+ context->offset + context->nbytes : offset + nbytes) -
+ new_offset;
+ context->offset = new_offset;
+ context->ncalls ++;
+ }
+ else
+ {
+ /* file has changed; actually flush previous file before restarting
+ * to accumulate flushes
+ */
+ PerformFileFlush(context);
+
+ context->fd = fd;
+ context->ncalls = 1;
+ context->offset = offset;
+ context->nbytes = nbytes;
+ context->filename = filename;
+ }
+}
+
+#endif /* HAVE_SYNC_FILE_RANGE || HAVE_POSIX_FADVISE */
+
int
-FileWrite(File file, char *buffer, int amount)
+FileWrite(File file, char *buffer, int amount, bool flush_to_disk,
+ FileFlushContext * context)
{
int returnCode;
@@ -1395,6 +1484,28 @@ retry:
if (returnCode >= 0)
{
+
+#if defined(HAVE_SYNC_FILE_RANGE) || defined(HAVE_POSIX_FADVISE)
+
+ /*
+ * Calling "write" tells the OS that pg wants to write some page to disk,
+ * however when it is really done is chosen by the OS.
+ * Depending on other disk activities this may be delayed significantly,
+ * maybe up to an "fsync" call, which could induce an IO write surge.
+ * When checkpointing pg is doing its own throttling and the result
+ * should really be written to disk with high priority, so as to meet
+ * the completion target.
+ * This call hints that such write have a higher priority.
+ */
+ if (flush_to_disk && returnCode == amount && errno == 0)
+ {
+ FileAsynchronousFlush(context,
+ VfdCache[file].fd, VfdCache[file].seekPos,
+ amount, VfdCache[file].fileName);
+ }
+
+#endif /* HAVE_SYNC_FILE_RANGE || HAVE_POSIX_FADVISE */
+
VfdCache[file].seekPos += returnCode;
/* maintain fileSize and temporary_files_size if it's a temp file */
diff --git a/src/backend/storage/smgr/md.c b/src/backend/storage/smgr/md.c
index 42a43bb..dbf057f 100644
--- a/src/backend/storage/smgr/md.c
+++ b/src/backend/storage/smgr/md.c
@@ -531,7 +531,7 @@ mdextend(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
errmsg("could not seek to block %u in file \"%s\": %m",
blocknum, FilePathName(v->mdfd_vfd))));
- if ((nbytes = FileWrite(v->mdfd_vfd, buffer, BLCKSZ)) != BLCKSZ)
+ if ((nbytes = FileWrite(v->mdfd_vfd, buffer, BLCKSZ, false, NULL)) != BLCKSZ)
{
if (nbytes < 0)
ereport(ERROR,
@@ -738,7 +738,8 @@ mdread(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
*/
void
mdwrite(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
- char *buffer, bool skipFsync)
+ char *buffer, bool skipFsync, bool flush_to_disk,
+ FileFlushContext * context)
{
off_t seekpos;
int nbytes;
@@ -767,7 +768,7 @@ mdwrite(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
errmsg("could not seek to block %u in file \"%s\": %m",
blocknum, FilePathName(v->mdfd_vfd))));
- nbytes = FileWrite(v->mdfd_vfd, buffer, BLCKSZ);
+ nbytes = FileWrite(v->mdfd_vfd, buffer, BLCKSZ, flush_to_disk, context);
TRACE_POSTGRESQL_SMGR_MD_WRITE_DONE(forknum, blocknum,
reln->smgr_rnode.node.spcNode,
diff --git a/src/backend/storage/smgr/smgr.c b/src/backend/storage/smgr/smgr.c
index 244b4ea..2db3cd3 100644
--- a/src/backend/storage/smgr/smgr.c
+++ b/src/backend/storage/smgr/smgr.c
@@ -52,7 +52,8 @@ typedef struct f_smgr
void (*smgr_read) (SMgrRelation reln, ForkNumber forknum,
BlockNumber blocknum, char *buffer);
void (*smgr_write) (SMgrRelation reln, ForkNumber forknum,
- BlockNumber blocknum, char *buffer, bool skipFsync);
+ BlockNumber blocknum, char *buffer, bool skipFsync,
+ bool flush_to_disk, FileFlushContext *context);
BlockNumber (*smgr_nblocks) (SMgrRelation reln, ForkNumber forknum);
void (*smgr_truncate) (SMgrRelation reln, ForkNumber forknum,
BlockNumber nblocks);
@@ -643,10 +644,11 @@ smgrread(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
*/
void
smgrwrite(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
- char *buffer, bool skipFsync)
+ char *buffer, bool skipFsync, bool flush_to_disk,
+ FileFlushContext * context)
{
(*(smgrsw[reln->smgr_which].smgr_write)) (reln, forknum, blocknum,
- buffer, skipFsync);
+ buffer, skipFsync, flush_to_disk, context);
}
/*
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index ff95e61..c5c996c 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -158,6 +158,7 @@ static bool check_bonjour(bool *newval, void **extra, GucSource source);
static bool check_ssl(bool *newval, void **extra, GucSource source);
static bool check_stage_log_stats(bool *newval, void **extra, GucSource source);
static bool check_log_stats(bool *newval, void **extra, GucSource source);
+static bool check_flush_to_disk(bool *newval, void **extra, GucSource source);
static bool check_canonical_path(char **newval, void **extra, GucSource source);
static bool check_timezone_abbreviations(char **newval, void **extra, GucSource source);
static void assign_timezone_abbreviations(const char *newval, void *extra);
@@ -1025,6 +1026,16 @@ static struct config_bool ConfigureNamesBool[] =
},
{
+ {"checkpoint_flush_to_disk", PGC_SIGHUP, WAL_CHECKPOINTS,
+ gettext_noop("Hint that checkpoint's writes are high priority."),
+ NULL
+ },
+ &checkpoint_flush_to_disk,
+ false,
+ check_flush_to_disk, NULL, NULL
+ },
+
+ {
{"log_connections", PGC_SU_BACKEND, LOGGING_WHAT,
gettext_noop("Logs each successful connection."),
NULL
@@ -9809,6 +9820,21 @@ check_log_stats(bool *newval, void **extra, GucSource source)
}
static bool
+check_flush_to_disk(bool *newval, void **extra, GucSource source)
+{
+/* This test must be consistent with the one in FileWrite (storage/file/fd.c)
+ */
+#if ! (defined(HAVE_SYNC_FILE_RANGE) || defined(HAVE_POSIX_FADVISE))
+ /* just warn if it has no effect */
+ ereport(WARNING,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("Setting \"checkpoint_flush_to_disk\" has no effect "
+ "on this platform.")));
+#endif /* HAVE_SYNC_FILE_RANGE */
+ return true;
+}
+
+static bool
check_canonical_path(char **newval, void **extra, GucSource source)
{
/*
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index e84f380..66010b1 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -202,6 +202,7 @@
#min_wal_size = 80MB
#checkpoint_completion_target = 0.5 # checkpoint target duration, 0.0 - 1.0
#checkpoint_sort = on # sort buffers on checkpoint
+#checkpoint_flush_to_disk = off # send buffers to disk on checkpoint
#checkpoint_warning = 30s # 0 disables
# - Archiving -
diff --git a/src/include/postmaster/bgwriter.h b/src/include/postmaster/bgwriter.h
index a49c208..f9c8ca1 100644
--- a/src/include/postmaster/bgwriter.h
+++ b/src/include/postmaster/bgwriter.h
@@ -16,6 +16,7 @@
#define _BGWRITER_H
#include "storage/block.h"
+#include "storage/fd.h"
#include "storage/relfilenode.h"
@@ -29,7 +30,8 @@ extern void BackgroundWriterMain(void) pg_attribute_noreturn();
extern void CheckpointerMain(void) pg_attribute_noreturn();
extern void RequestCheckpoint(int flags);
-extern void CheckpointWriteDelay(int flags, double progress);
+extern void CheckpointWriteDelay(int flags, double progress,
+ FileFlushContext * context, int ctx_size);
extern bool ForwardFsyncRequest(RelFileNode rnode, ForkNumber forknum,
BlockNumber segno);
diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.h
index c228f39..db0e2c3 100644
--- a/src/include/storage/bufmgr.h
+++ b/src/include/storage/bufmgr.h
@@ -54,6 +54,7 @@ extern int bgwriter_lru_maxpages;
extern double bgwriter_lru_multiplier;
extern bool track_io_timing;
extern int target_prefetch_pages;
+extern bool checkpoint_flush_to_disk;
extern bool checkpoint_sort;
/* in buf_init.c */
diff --git a/src/include/storage/fd.h b/src/include/storage/fd.h
index 7eabe09..c740ee7 100644
--- a/src/include/storage/fd.h
+++ b/src/include/storage/fd.h
@@ -59,6 +59,22 @@ extern int max_files_per_process;
*/
extern int max_safe_fds;
+/* FileFlushContext:
+ * This structure is used to accumulate several flush requests on a file
+ * into a larger flush request.
+ * - fd: file descriptor of the file
+ * - ncalls: number of flushes merged together
+ * - offset: starting offset (minimum of all offset)
+ * - nbytes: size (minimum extent to cover all flushed data)
+ * - filename: filename of fd for error messages
+ */
+typedef struct FileFlushContext{
+ int fd;
+ int ncalls;
+ off_t offset;
+ off_t nbytes;
+ char * filename;
+} FileFlushContext;
/*
* prototypes for functions in fd.c
@@ -70,7 +86,12 @@ extern File OpenTemporaryFile(bool interXact);
extern void FileClose(File file);
extern int FilePrefetch(File file, off_t offset, int amount);
extern int FileRead(File file, char *buffer, int amount);
-extern int FileWrite(File file, char *buffer, int amount);
+extern void ResetFileFlushContext(FileFlushContext * context);
+extern void PerformFileFlush(FileFlushContext * context);
+extern void FileAsynchronousFlush(FileFlushContext * context,
+ int fd, off_t offset, off_t nbytes, char * filename);
+extern int FileWrite(File file, char *buffer, int amount, bool flush_to_disk,
+ FileFlushContext * context);
extern int FileSync(File file);
extern off_t FileSeek(File file, off_t offset, int whence);
extern int FileTruncate(File file, off_t offset);
diff --git a/src/include/storage/smgr.h b/src/include/storage/smgr.h
index 69a624f..a46a70c 100644
--- a/src/include/storage/smgr.h
+++ b/src/include/storage/smgr.h
@@ -16,6 +16,7 @@
#include "fmgr.h"
#include "storage/block.h"
+#include "storage/fd.h"
#include "storage/relfilenode.h"
@@ -95,7 +96,8 @@ extern void smgrprefetch(SMgrRelation reln, ForkNumber forknum,
extern void smgrread(SMgrRelation reln, ForkNumber forknum,
BlockNumber blocknum, char *buffer);
extern void smgrwrite(SMgrRelation reln, ForkNumber forknum,
- BlockNumber blocknum, char *buffer, bool skipFsync);
+ BlockNumber blocknum, char *buffer, bool skipFsync,
+ bool flush_to_disk, FileFlushContext * context);
extern BlockNumber smgrnblocks(SMgrRelation reln, ForkNumber forknum);
extern void smgrtruncate(SMgrRelation reln, ForkNumber forknum,
BlockNumber nblocks);
@@ -120,8 +122,9 @@ extern void mdprefetch(SMgrRelation reln, ForkNumber forknum,
BlockNumber blocknum);
extern void mdread(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
char *buffer);
-extern void mdwrite(SMgrRelation reln, ForkNumber forknum,
- BlockNumber blocknum, char *buffer, bool skipFsync);
+extern void mdwrite(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
+ char *buffer, bool skipFsync, bool flush_to_disk,
+ FileFlushContext * context);
extern BlockNumber mdnblocks(SMgrRelation reln, ForkNumber forknum);
extern void mdtruncate(SMgrRelation reln, ForkNumber forknum,
BlockNumber nblocks);
On August 10, 2015 8:24:21 PM GMT+02:00, Fabien COELHO <coelho@cri.ensmp.fr> wrote:
Hello Andres,
You can't allocate 4GB with palloc(), it has a builtin limit against
allocating more than 1GB.Argh, too bad, I assumed very naively that palloc was malloc in
disguise.
It is, but there's some layering (memory pools/contexts) on top. You can get huge allocations with polloc_huge.
Then the file would be fsynced twice: if the fsync is done properly
(data
have already been flushed to disk) then it would not cost much, and
doing
it sometimes twice on some file would not be a big issue. The code
could
also detect such event and log a warning, which would give a hint abouthow often it occurs in practice.
Right. At the cost of keeping track of all files...
If the pivot element changes its identity won't the result be
pretty much
random?
That would be a very unlikely event, given the short time spent in
qsort.Meh, we don't want to rely on "likeliness" on such things.
My main argument is that even if it occurs, and the qsort result is
partly
wrong, it does not change correctness, it just mean that the actual
writes
will be less in order than wished. If it occurs, one pivot separation
would be quite strange, but then others would be right, so the buffers
would be "partly sorted".
It doesn't matter for correctness today, correct. But it makes out impossible to rely on or too.
Another issue I see is that even if buffers are locked within cmp, the
status may change between two cmp...
Sure. That's not what in suggesting. Earlier versions of the patch kept an array of buffer headers exactly because of that.
I do not think that locking all
buffers for sorting them is an option. So on the whole, I think that
locking buffers for sorting is probably not possible with the simple
(and
efficient) lightweight approach used in the patch.
Yes, the other version has a higher space overhead. I'm not convinced that's meaningful in comparison to shared buffets in space.
And rather doubtful it a loss performance wise in a loaded server. All the buffer headers are touched on other cores and doing the sort with indirection will greatly increase bus traffic.
The good news, as I argued before, is that the order is only advisory
to
help with performance, but the correctness is really that all
checkpoint
buffers are written and fsync is called in the end, and does not depend
on
the buffer order. That is how it currently works anyway
It's not particularly desirable to have a performance feature that works less well if the server is heavily and concurrently loaded. The likelihood of bogus sort results will increase with the churn rate in shared buffers.
Andres
---
Please excuse brevity and formatting - I am writing this on my mobile phone.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Tue, Aug 11, 2015 at 4:28 AM, Andres Freund wrote:
On August 10, 2015 8:24:21 PM GMT+02:00, Fabien COELHO wrote:
You can't allocate 4GB with palloc(), it has a builtin limit against
allocating more than 1GB.Argh, too bad, I assumed very naively that palloc was malloc in
disguise.It is, but there's some layering (memory pools/contexts) on top. You can get huge allocations with polloc_huge.
palloc_huge does not exist yet ;)
There is either repalloc_huge or palloc_extended now, though
implementing one would be trivial.
--
Michael
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Hello Andres,
[...] Right. At the cost of keeping track of all files...
Sure. Pg already tracks all files, and probably some more tracking would
be necessary for an early fsync feature to know what are those already
fsync'ed and what are those not yet fsync'ed.
Yes, the other version has a higher space overhead.
Yep, this is my concern.
I'm not convinced that's meaningful in comparison to shared buffers in
space. And rather doubtful it a loss performance wise in a loaded
server. All the buffer headers are touched on other cores and doing the
sort with indirection will greatly increase bus traffic.
The measures I collected and reported showed that the sorting time is
basically insignificant, so bus traffic induced by sorting does not seem
to be an issue.
[...] It's not particularly desirable to have a performance feature that
works less well if the server is heavily and concurrently loaded. The
likelihood of bogus sort results will increase with the churn rate in
shared buffers.
Hm.
In conclusion I'm not convinced that it is worth the memory, but I'm also
tired of arguing, and hopefully nobody else cares about a few more bytes
per shared_buffers, so why should I care?
Here is a v8, I reduced the memory overhead of the "heavy weight" approach
from 24 to 16 bytes per buffer, so it is medium weight:-). It might be
compacted further down to 12 bytes by combining the 2 bits of forkNum
either with relNode or blockNum, and use a uint64_t comparison field with
all data so that the comparison code would be simpler and faster.
I also fixed the computation of the shmem size which I had not updated
when switching to shmem.
The patches still include the two guc, but it is easy to remove one or the
other. They are useful is someone wants to test. The default is on for
sort, and off for flush. Maybe it should be on for both.
--
Fabien.
Attachments:
checkpoint-continuous-flush-8-a.patchtext/x-diff; name=checkpoint-continuous-flush-8-a.patchDownload
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index e900dcc..1cec243 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -2454,6 +2454,28 @@ include_dir 'conf.d'
</listitem>
</varlistentry>
+ <varlistentry id="guc-checkpoint-sort" xreflabel="checkpoint_sort">
+ <term><varname>checkpoint_sort</varname> (<type>bool</type>)
+ <indexterm>
+ <primary><varname>checkpoint_sort</> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ Whether to sort buffers before writting them out to disk on checkpoint.
+ For a HDD storage, this setting allows to group together
+ neighboring pages written to disk, thus improving performance by
+ reducing random write activity.
+ This sorting should have limited performance effects on SSD backends
+ as such storages have good random write performance, but it may
+ help with wear-leveling so be worth keeping anyway.
+ The default is <literal>on</>.
+ This parameter can only be set in the <filename>postgresql.conf</>
+ file or on the server command line.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="guc-checkpoint-warning" xreflabel="checkpoint_warning">
<term><varname>checkpoint_warning</varname> (<type>integer</type>)
<indexterm>
diff --git a/doc/src/sgml/wal.sgml b/doc/src/sgml/wal.sgml
index e3941c9..f538698 100644
--- a/doc/src/sgml/wal.sgml
+++ b/doc/src/sgml/wal.sgml
@@ -546,6 +546,18 @@
</para>
<para>
+ When hard-disk drives (HDD) are used for terminal data storage
+ <xref linkend="guc-checkpoint-sort"> allows to sort pages
+ so that neighboring pages on disk will be flushed together by
+ chekpoints, reducing the random write load and improving performance.
+ If solid-state drives (SSD) are used, sorting pages induces no benefit
+ as their random write I/O performance is good: this feature could then
+ be disabled by setting <varname>checkpoint_sort</> to <value>off</>.
+ It is possible that sorting may help with SSD wear leveling, so it may
+ be kept on that account.
+ </para>
+
+ <para>
The number of WAL segment files in <filename>pg_xlog</> directory depends on
<varname>min_wal_size</>, <varname>max_wal_size</> and
the amount of WAL generated in previous checkpoint cycles. When old log
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 68e33eb..bee38ab 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -7995,11 +7995,13 @@ LogCheckpointEnd(bool restartpoint)
sync_secs,
total_secs,
longest_secs,
+ sort_secs,
average_secs;
int write_usecs,
sync_usecs,
total_usecs,
longest_usecs,
+ sort_usecs,
average_usecs;
uint64 average_sync_time;
@@ -8030,6 +8032,10 @@ LogCheckpointEnd(bool restartpoint)
CheckpointStats.ckpt_end_t,
&total_secs, &total_usecs);
+ TimestampDifference(CheckpointStats.ckpt_sort_t,
+ CheckpointStats.ckpt_sort_end_t,
+ &sort_secs, &sort_usecs);
+
/*
* Timing values returned from CheckpointStats are in microseconds.
* Convert to the second plus microsecond form that TimestampDifference
@@ -8048,8 +8054,8 @@ LogCheckpointEnd(bool restartpoint)
elog(LOG, "%s complete: wrote %d buffers (%.1f%%); "
"%d transaction log file(s) added, %d removed, %d recycled; "
- "write=%ld.%03d s, sync=%ld.%03d s, total=%ld.%03d s; "
- "sync files=%d, longest=%ld.%03d s, average=%ld.%03d s; "
+ "sort=%ld.%03d s, write=%ld.%03d s, sync=%ld.%03d s, total=%ld.%03d s;"
+ " sync files=%d, longest=%ld.%03d s, average=%ld.%03d s; "
"distance=%d kB, estimate=%d kB",
restartpoint ? "restartpoint" : "checkpoint",
CheckpointStats.ckpt_bufs_written,
@@ -8057,6 +8063,7 @@ LogCheckpointEnd(bool restartpoint)
CheckpointStats.ckpt_segs_added,
CheckpointStats.ckpt_segs_removed,
CheckpointStats.ckpt_segs_recycled,
+ sort_secs, sort_usecs / 1000,
write_secs, write_usecs / 1000,
sync_secs, sync_usecs / 1000,
total_secs, total_usecs / 1000,
diff --git a/src/backend/storage/buffer/buf_init.c b/src/backend/storage/buffer/buf_init.c
index 3ae2848..3bd5eab 100644
--- a/src/backend/storage/buffer/buf_init.c
+++ b/src/backend/storage/buffer/buf_init.c
@@ -65,7 +65,8 @@ void
InitBufferPool(void)
{
bool foundBufs,
- foundDescs;
+ foundDescs,
+ foundCpid;
/* Align descriptors to a cacheline boundary. */
BufferDescriptors = (BufferDescPadded *) CACHELINEALIGN(
@@ -77,10 +78,14 @@ InitBufferPool(void)
ShmemInitStruct("Buffer Blocks",
NBuffers * (Size) BLCKSZ, &foundBufs);
- if (foundDescs || foundBufs)
+ CheckpointBufferIds = (CheckpointSortItem *)
+ ShmemInitStruct("Checkpoint BufferIds",
+ NBuffers * sizeof(CheckpointSortItem), &foundCpid);
+
+ if (foundDescs || foundBufs || foundCpid)
{
- /* both should be present or neither */
- Assert(foundDescs && foundBufs);
+ /* all should be present or neither */
+ Assert(foundDescs && foundBufs && foundCpid);
/* note: this path is only taken in EXEC_BACKEND case */
}
else
@@ -144,5 +149,8 @@ BufferShmemSize(void)
/* size of stuff controlled by freelist.c */
size = add_size(size, StrategyShmemSize());
+ /* size of checkpoint sort array in bufmgr.c */
+ size = add_size(size, mul_size(NBuffers, sizeof(CheckpointSortItem)));
+
return size;
}
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index e4b25587..c5643ce 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -80,6 +80,7 @@ bool zero_damaged_pages = false;
int bgwriter_lru_maxpages = 100;
double bgwriter_lru_multiplier = 2.0;
bool track_io_timing = false;
+bool checkpoint_sort = true;
/*
* How many buffers PrefetchBuffer callers should try to stay ahead of their
@@ -95,6 +96,9 @@ static bool IsForInput;
/* local state for LockBufferForCleanup */
static volatile BufferDesc *PinCountWaitBuf = NULL;
+/* array of buffer ids & sort criterion of all buffers to checkpoint */
+CheckpointSortItem *CheckpointBufferIds = NULL;
+
/*
* Backend-Private refcount management:
*
@@ -1561,6 +1565,129 @@ UnpinBuffer(volatile BufferDesc *buf, bool fixOwner)
}
}
+/* Compare checkpoint buffers.
+ */
+static int bufcmp(const void * pa, const void * pb)
+{
+ CheckpointSortItem
+ *a = (CheckpointSortItem *) pa,
+ *b = (CheckpointSortItem *) pb;
+
+ /* compare relation */
+ if (a->relNode < b->relNode)
+ return -1;
+ else if (a->relNode > b->relNode)
+ return 1;
+ /* same relation, compare fork */
+ else if (a->forkNum < b->forkNum)
+ return -1;
+ else if (a->forkNum > b->forkNum)
+ return 1;
+ /* same relation/fork, so same segmented "file", compare block number
+ * which are mapped on different segments depending on the number.
+ */
+ else if (a->blockNum < b->blockNum)
+ return -1;
+ else /* should not be the same block anyway... */
+ return 1;
+}
+
+/* Status of buffers to checkpoint for a particular tablespace,
+ * used internally in BufferSync.
+ * - space: oid of the tablespace
+ * - num_to_write: number of checkpoint pages counted for this tablespace
+ * - num_written: number of pages actually written out
+ * - index: scanning position in CheckpointBufferIds for this tablespace
+ */
+typedef struct TableSpaceCheckpointStatus {
+ Oid space;
+ int num_to_write;
+ int num_written;
+ int index;
+} TableSpaceCheckpointStatus;
+
+/* entry structure for table space to count hashtable,
+ * used internally in BufferSync.
+ */
+typedef struct TableSpaceCountEntry {
+ Oid space;
+ int count;
+} TableSpaceCountEntry;
+
+/* return the next buffer to write, or -1.
+ * this function balances buffers over tablespaces.
+ */
+static int
+NextBufferToWrite(
+ TableSpaceCheckpointStatus *spcStatus, int nb_spaces,
+ int *pspace, int num_to_write, int num_written)
+{
+ int space = *pspace, buf_id = -1, index;
+
+ /*
+ * Select a tablespace depending on the current overall progress.
+ *
+ * The progress ratio of each unfinished tablespace is compared to
+ * the overall progress ratio to find one with is not in advance
+ * (i.e. tablespace ratio <= overall ratio).
+ *
+ * Existence: it is bound to exist otherwise the overall progress
+ * ratio would be inconsistent: with positive buffers to write (t1 & t2)
+ * and already written buffers (w1 & w2), we have:
+ *
+ * If w1/t1 > (w1+w2)/(t1+t2) # one table space is in advance
+ * => w1t1+w1t2 > w1t1+w2t1 => w1t2 > w2t1 => w1t2+w2t2 > w2t1+w2t2
+ * => (w1+w2) / (t1+t2) > w2 / t2 # the other one is late
+ *
+ * The round robin ensures that each space is given some attention
+ * till it is over the current ratio, before going to the next.
+ *
+ * Precision: using int32 computations for comparing fractions
+ * (w1 / t1 > w / t <=> w1 t > w t1) seems a bad idea as the values
+ * can overflow 32-bit integers: the limit would be sqrt(2**31) ~
+ * 46340 buffers, i.e. a 362 MB checkpoint. So ensure that 64-bit
+ * integers are used in the comparison.
+ */
+ while (/* compare tablespace vs overall progress ratio:
+ * tablespace written/to_write > overall written/to_write
+ */
+ (int64) spcStatus[space].num_written * num_to_write >
+ (int64) num_written * spcStatus[space].num_to_write)
+ space = (space + 1) % nb_spaces; /* round robin */
+
+ /*
+ * Find a valid buffer in the selected tablespace,
+ * by continuing the tablespace specific buffer scan
+ * where it was left.
+ */
+ index = spcStatus[space].index;
+
+ while (index < num_to_write && buf_id == -1)
+ {
+ volatile BufferDesc *bufHdr;
+
+ buf_id = CheckpointBufferIds[index].buf_id;
+ bufHdr = GetBufferDescriptor(buf_id);
+
+ /* Skip if in another tablespace or not in checkpoint anymore.
+ * No lock is acquired, see comments below.
+ */
+ if (spcStatus[space].space != bufHdr->tag.rnode.spcNode ||
+ ! (bufHdr->flags & BM_CHECKPOINT_NEEDED))
+ {
+ index ++;
+ buf_id = -1;
+ }
+ }
+
+ /* Update tablespace writing status, will start over at next index */
+ spcStatus[space].index = index+1;
+
+ *pspace = space;
+
+ return buf_id;
+}
+
/*
* BufferSync -- Write out all dirty buffers in the pool.
*
@@ -1574,11 +1701,13 @@ UnpinBuffer(volatile BufferDesc *buf, bool fixOwner)
static void
BufferSync(int flags)
{
- int buf_id;
- int num_to_scan;
+ int buf_id = -1;
int num_to_write;
int num_written;
int mask = BM_DIRTY;
+ HTAB *spcBuffers;
+ TableSpaceCheckpointStatus *spcStatus = NULL;
+ int nb_spaces, space;
/* Make sure we can handle the pin inside SyncOneBuffer */
ResourceOwnerEnlargeBuffers(CurrentResourceOwner);
@@ -1609,6 +1738,18 @@ BufferSync(int flags)
* certainly need to be written for the next checkpoint attempt, too.
*/
num_to_write = 0;
+
+ /* initialize oid -> int buffer count hash table */
+ {
+ HASHCTL ctl;
+
+ MemSet(&ctl, 0, sizeof(HASHCTL));
+ ctl.keysize = sizeof(Oid);
+ ctl.entrysize = sizeof(TableSpaceCountEntry);
+ spcBuffers = hash_create("Number of buffers to write per tablespace",
+ 16, &ctl, HASH_ELEM | HASH_BLOBS);
+ }
+
for (buf_id = 0; buf_id < NBuffers; buf_id++)
{
volatile BufferDesc *bufHdr = GetBufferDescriptor(buf_id);
@@ -1621,32 +1762,111 @@ BufferSync(int flags)
if ((bufHdr->flags & mask) == mask)
{
+ Oid spc;
+ TableSpaceCountEntry * entry;
+ bool found;
+
bufHdr->flags |= BM_CHECKPOINT_NEEDED;
+ CheckpointBufferIds[num_to_write].buf_id = buf_id;
+ CheckpointBufferIds[num_to_write].relNode = bufHdr->tag.rnode.relNode;
+ CheckpointBufferIds[num_to_write].forkNum = bufHdr->tag.forkNum;
+ CheckpointBufferIds[num_to_write].blockNum = bufHdr->tag.blockNum;
num_to_write++;
+
+ /* keep track of per tablespace buffers */
+ spc = bufHdr->tag.rnode.spcNode;
+ entry = (TableSpaceCountEntry *)
+ hash_search(spcBuffers, (void *) &spc, HASH_ENTER, &found);
+
+ if (found) entry->count++;
+ else entry->count = 1;
}
UnlockBufHdr(bufHdr);
}
if (num_to_write == 0)
+ {
+ hash_destroy(spcBuffers);
return; /* nothing to do */
+ }
TRACE_POSTGRESQL_BUFFER_SYNC_START(NBuffers, num_to_write);
+ /* Build checkpoint tablespace buffer status */
+ nb_spaces = hash_get_num_entries(spcBuffers);
+ spcStatus = (TableSpaceCheckpointStatus *)
+ palloc(sizeof(TableSpaceCheckpointStatus) * nb_spaces);
+
+ {
+ int index = 0;
+ HASH_SEQ_STATUS hseq;
+ TableSpaceCountEntry * entry;
+
+ hash_seq_init(&hseq, spcBuffers);
+ while ((entry = (TableSpaceCountEntry *) hash_seq_search(&hseq)))
+ {
+ Assert(index < nb_spaces);
+ spcStatus[index].space = entry->space;
+ spcStatus[index].num_to_write = entry->count;
+ spcStatus[index].num_written = 0;
+ /* should it be randomized? chosen with some criterion? */
+ spcStatus[index].index = 0;
+
+ index ++;
+ }
+ }
+
+ hash_destroy(spcBuffers);
+ spcBuffers = NULL;
+
/*
- * Loop over all buffers again, and write the ones (still) marked with
- * BM_CHECKPOINT_NEEDED. In this loop, we start at the clock sweep point
- * since we might as well dump soon-to-be-recycled buffers first.
+ * Sort buffer ids to help find sequential writes.
*
- * Note that we don't read the buffer alloc count here --- that should be
- * left untouched till the next BgBufferSync() call.
+ * Note: Buffers are not locked in any way during sorting, but that's ok:
+ * A change in the buffer header is only relevant when it changes the
+ * buffer's identity. If the identity has changed it'll have been
+ * written out by BufferAlloc(), so there's no need for checkpointer to
+ * write it out anymore. The buffer might also get written out by a
+ * backend or bgwriter, but that's equally harmless.
+ *
+ * Marked buffers must not be move during the checkpoint.
+ * Also, qsort implementation should be resilient to occasional
+ * contradictions (cmp(a,b) != -cmp(b,a)) because of possible
+ * concurrent changes.
*/
- buf_id = StrategySyncStart(NULL, NULL);
- num_to_scan = NBuffers;
+ CheckpointStats.ckpt_sort_t = GetCurrentTimestamp();
+
+ if (checkpoint_sort)
+ {
+ qsort(CheckpointBufferIds, num_to_write, sizeof(CheckpointSortItem),
+ bufcmp);
+ }
+
+ CheckpointStats.ckpt_sort_end_t = GetCurrentTimestamp();
+
+ /*
+ * Loop over buffers to write through CheckpointBufferIds,
+ * and write the ones (still) marked with BM_CHECKPOINT_NEEDED,
+ * with some round robin over table spaces so as to balance writes,
+ * so that buffer writes move forward roughly proportionally for each
+ * tablespace.
+ *
+ * Termination: if a tablespace is selected by the inner while loop
+ * (see argument there), its index is incremented and will eventually
+ * reach num_to_write, mark this table space scanning as done and
+ * decrement the number of (active) spaces, which will thus reach 0.
+ */
+ space = 0;
num_written = 0;
- while (num_to_scan-- > 0)
+
+ while (nb_spaces != 0)
{
- volatile BufferDesc *bufHdr = GetBufferDescriptor(buf_id);
+ volatile BufferDesc *bufHdr = NULL;
+ buf_id = NextBufferToWrite(spcStatus, nb_spaces, &space,
+ num_to_write, num_written);
+ if (buf_id != -1)
+ bufHdr = GetBufferDescriptor(buf_id);
/*
* We don't need to acquire the lock here, because we're only looking
@@ -1660,39 +1880,45 @@ BufferSync(int flags)
* write the buffer though we didn't need to. It doesn't seem worth
* guarding against this, though.
*/
- if (bufHdr->flags & BM_CHECKPOINT_NEEDED)
+ if (bufHdr != NULL && bufHdr->flags & BM_CHECKPOINT_NEEDED)
{
if (SyncOneBuffer(buf_id, false) & BUF_WRITTEN)
{
TRACE_POSTGRESQL_BUFFER_SYNC_WRITTEN(buf_id);
BgWriterStats.m_buf_written_checkpoints++;
+ spcStatus[space].num_written++;
num_written++;
/*
- * We know there are at most num_to_write buffers with
- * BM_CHECKPOINT_NEEDED set; so we can stop scanning if
- * num_written reaches num_to_write.
- *
- * Note that num_written doesn't include buffers written by
- * other backends, or by the bgwriter cleaning scan. That
- * means that the estimate of how much progress we've made is
- * conservative, and also that this test will often fail to
- * trigger. But it seems worth making anyway.
- */
- if (num_written >= num_to_write)
- break;
-
- /*
* Sleep to throttle our I/O rate.
*/
CheckpointWriteDelay(flags, (double) num_written / num_to_write);
}
}
- if (++buf_id >= NBuffers)
- buf_id = 0;
+ /*
+ * Detect checkpoint end for a tablespace: either the scan is done
+ * or all tablespace buffers have been written out. If so, the
+ * another active tablespace status is moved in place of the current
+ * one and the next round will start on this one, or maybe round about.
+ * Note: maybe an exchange could be made instead in order to keep
+ * informations about the closed table space, but this is currently
+ * not used afterwards.
+ */
+ if (spcStatus[space].index >= num_to_write ||
+ spcStatus[space].num_written >= spcStatus[space].num_to_write)
+ {
+ nb_spaces--;
+ if (space != nb_spaces)
+ spcStatus[space] = spcStatus[nb_spaces];
+ else
+ space = 0;
+ }
}
+ pfree(spcStatus);
+ spcStatus = NULL;
+
/*
* Update checkpoint statistics. As noted above, this doesn't include
* buffers written by other backends or bgwriter scan.
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index b3dac51..cf1e505 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -1013,6 +1013,17 @@ static struct config_bool ConfigureNamesBool[] =
false,
NULL, NULL, NULL
},
+
+ {
+ {"checkpoint_sort", PGC_SIGHUP, WAL_CHECKPOINTS,
+ gettext_noop("Whether disk-page buffers are sorted on checkpoints."),
+ NULL
+ },
+ &checkpoint_sort,
+ true,
+ NULL, NULL, NULL
+ },
+
{
{"log_connections", PGC_SU_BACKEND, LOGGING_WHAT,
gettext_noop("Logs each successful connection."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index e5d275d..e84f380 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -201,6 +201,7 @@
#max_wal_size = 1GB
#min_wal_size = 80MB
#checkpoint_completion_target = 0.5 # checkpoint target duration, 0.0 - 1.0
+#checkpoint_sort = on # sort buffers on checkpoint
#checkpoint_warning = 30s # 0 disables
# - Archiving -
diff --git a/src/include/access/xlog.h b/src/include/access/xlog.h
index 6dacee2..dbd4757 100644
--- a/src/include/access/xlog.h
+++ b/src/include/access/xlog.h
@@ -186,6 +186,8 @@ extern bool XLOG_DEBUG;
typedef struct CheckpointStatsData
{
TimestampTz ckpt_start_t; /* start of checkpoint */
+ TimestampTz ckpt_sort_t; /* start buffer sorting */
+ TimestampTz ckpt_sort_end_t; /* end of sorting */
TimestampTz ckpt_write_t; /* start of flushing buffers */
TimestampTz ckpt_sync_t; /* start of fsyncs */
TimestampTz ckpt_sync_end_t; /* end of fsyncs */
diff --git a/src/include/storage/buf_internals.h b/src/include/storage/buf_internals.h
index 521ee1c..7fde0dc 100644
--- a/src/include/storage/buf_internals.h
+++ b/src/include/storage/buf_internals.h
@@ -210,6 +210,22 @@ extern PGDLLIMPORT BufferDescPadded *BufferDescriptors;
/* in localbuf.c */
extern BufferDesc *LocalBufferDescriptors;
+/* in bufmgr.c */
+
+/*
+ * Structure to sort buffers per file on checkpoints.
+ *
+ * Maybe the sort criterion could be compacted to reduce memory requirement
+ * and for faster comparison?
+ */
+typedef struct CheckpointSortItem {
+ int buf_id;
+ Oid relNode;
+ ForkNumber forkNum; /* only 4 values */
+ BlockNumber blockNum;
+} CheckpointSortItem;
+
+extern CheckpointSortItem *CheckpointBufferIds;
/*
* Internal routines: only called by bufmgr
diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.h
index ec0a254..c228f39 100644
--- a/src/include/storage/bufmgr.h
+++ b/src/include/storage/bufmgr.h
@@ -54,6 +54,7 @@ extern int bgwriter_lru_maxpages;
extern double bgwriter_lru_multiplier;
extern bool track_io_timing;
extern int target_prefetch_pages;
+extern bool checkpoint_sort;
/* in buf_init.c */
extern PGDLLIMPORT char *BufferBlocks;
checkpoint-continuous-flush-8-b.patchtext/x-diff; name=checkpoint-continuous-flush-8-b.patchDownload
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 1cec243..2551d95 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -2497,6 +2497,24 @@ include_dir 'conf.d'
</listitem>
</varlistentry>
+ <varlistentry id="guc-checkpoint-flush-to-disk" xreflabel="checkpoint_flush_to_disk">
+ <term><varname>checkpoint_flush_to_disk</varname> (<type>bool</type>)
+ <indexterm>
+ <primary><varname>checkpoint_flush_to_disk</> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ When writing data for a checkpoint, hint the underlying OS that the
+ data must be sent to disk as soon as possible. This may help smoothing
+ disk I/O writes and avoid a stall when fsync is issued at the end of
+ the checkpoint, but it may also reduce average performance.
+ This setting may have no effect on some platforms.
+ The default is <literal>off</>.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="guc-min-wal-size" xreflabel="min_wal_size">
<term><varname>min_wal_size</varname> (<type>integer</type>)
<indexterm>
diff --git a/doc/src/sgml/wal.sgml b/doc/src/sgml/wal.sgml
index f538698..eea6668 100644
--- a/doc/src/sgml/wal.sgml
+++ b/doc/src/sgml/wal.sgml
@@ -558,6 +558,17 @@
</para>
<para>
+ On Linux and POSIX platforms, <xref linkend="guc-checkpoint-flush-to-disk">
+ allows to hint the OS that pages written on checkpoints must be flushed
+ to disk quickly. Otherwise, these pages may be kept in cache for some time,
+ inducing a stall later when <literal>fsync</> is called to actually
+ complete the checkpoint. This setting helps to reduce transaction latency,
+ but it may also have a small adverse effect on the average transaction rate
+ at maximum throughput. This feature probably brings no benefit on SSD,
+ as the I/O write latency is small on such hardware, thus it may be disabled.
+ </para>
+
+ <para>
The number of WAL segment files in <filename>pg_xlog</> directory depends on
<varname>min_wal_size</>, <varname>max_wal_size</> and
the amount of WAL generated in previous checkpoint cycles. When old log
diff --git a/src/backend/access/heap/rewriteheap.c b/src/backend/access/heap/rewriteheap.c
index bcce3e3..f565dc4 100644
--- a/src/backend/access/heap/rewriteheap.c
+++ b/src/backend/access/heap/rewriteheap.c
@@ -918,7 +918,7 @@ logical_heap_rewrite_flush_mappings(RewriteState state)
* Note that we deviate from the usual WAL coding practices here,
* check the above "Logical rewrite support" comment for reasoning.
*/
- written = FileWrite(src->vfd, waldata_start, len);
+ written = FileWrite(src->vfd, waldata_start, len, false, NULL);
if (written != len)
ereport(ERROR,
(errcode_for_file_access(),
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index cf4a6dc..4b5e9cd 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -203,7 +203,7 @@ btbuildempty(PG_FUNCTION_ARGS)
/* Write the page. If archiving/streaming, XLOG it. */
PageSetChecksumInplace(metapage, BTREE_METAPAGE);
smgrwrite(index->rd_smgr, INIT_FORKNUM, BTREE_METAPAGE,
- (char *) metapage, true);
+ (char *) metapage, true, false, NULL);
if (XLogIsNeeded())
log_newpage(&index->rd_smgr->smgr_rnode.node, INIT_FORKNUM,
BTREE_METAPAGE, metapage, false);
diff --git a/src/backend/access/nbtree/nbtsort.c b/src/backend/access/nbtree/nbtsort.c
index f95f67a..ea7a45d 100644
--- a/src/backend/access/nbtree/nbtsort.c
+++ b/src/backend/access/nbtree/nbtsort.c
@@ -315,7 +315,7 @@ _bt_blwritepage(BTWriteState *wstate, Page page, BlockNumber blkno)
{
/* overwriting a block we zero-filled before */
smgrwrite(wstate->index->rd_smgr, MAIN_FORKNUM, blkno,
- (char *) page, true);
+ (char *) page, true, false, NULL);
}
pfree(page);
diff --git a/src/backend/access/spgist/spginsert.c b/src/backend/access/spgist/spginsert.c
index bceee8d..b700efb 100644
--- a/src/backend/access/spgist/spginsert.c
+++ b/src/backend/access/spgist/spginsert.c
@@ -170,7 +170,7 @@ spgbuildempty(PG_FUNCTION_ARGS)
/* Write the page. If archiving/streaming, XLOG it. */
PageSetChecksumInplace(page, SPGIST_METAPAGE_BLKNO);
smgrwrite(index->rd_smgr, INIT_FORKNUM, SPGIST_METAPAGE_BLKNO,
- (char *) page, true);
+ (char *) page, true, false, NULL);
if (XLogIsNeeded())
log_newpage(&index->rd_smgr->smgr_rnode.node, INIT_FORKNUM,
SPGIST_METAPAGE_BLKNO, page, false);
@@ -180,7 +180,7 @@ spgbuildempty(PG_FUNCTION_ARGS)
PageSetChecksumInplace(page, SPGIST_ROOT_BLKNO);
smgrwrite(index->rd_smgr, INIT_FORKNUM, SPGIST_ROOT_BLKNO,
- (char *) page, true);
+ (char *) page, true, false, NULL);
if (XLogIsNeeded())
log_newpage(&index->rd_smgr->smgr_rnode.node, INIT_FORKNUM,
SPGIST_ROOT_BLKNO, page, true);
@@ -190,7 +190,7 @@ spgbuildempty(PG_FUNCTION_ARGS)
PageSetChecksumInplace(page, SPGIST_NULL_BLKNO);
smgrwrite(index->rd_smgr, INIT_FORKNUM, SPGIST_NULL_BLKNO,
- (char *) page, true);
+ (char *) page, true, false, NULL);
if (XLogIsNeeded())
log_newpage(&index->rd_smgr->smgr_rnode.node, INIT_FORKNUM,
SPGIST_NULL_BLKNO, page, true);
diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c
index 3b3a09e..e361907 100644
--- a/src/backend/postmaster/checkpointer.c
+++ b/src/backend/postmaster/checkpointer.c
@@ -665,7 +665,8 @@ ImmediateCheckpointRequested(void)
* fraction between 0.0 meaning none, and 1.0 meaning all done.
*/
void
-CheckpointWriteDelay(int flags, double progress)
+CheckpointWriteDelay(int flags, double progress,
+ FileFlushContext * context, int ctx_size)
{
static int absorb_counter = WRITES_PER_ABSORB;
@@ -700,6 +701,26 @@ CheckpointWriteDelay(int flags, double progress)
*/
pgstat_send_bgwriter();
+#if defined(HAVE_SYNC_FILE_RANGE) || defined(HAVE_POSIX_FADVISE)
+
+ /*
+ * Before sleeping, flush written blocks for each tablespace.
+ */
+ if (checkpoint_flush_to_disk)
+ {
+ int i;
+
+ for (i = 0; i < ctx_size; i++)
+ {
+ if (context[i].ncalls != 0)
+ {
+ PerformFileFlush(&context[i]);
+ ResetFileFlushContext(&context[i]);
+ }
+ }
+ }
+#endif /* HAVE_SYNC_FILE_RANGE || HAVE_POSIX_FADVISE */
+
/*
* This sleep used to be connected to bgwriter_delay, typically 200ms.
* That resulted in more frequent wakeups if not much work to do.
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index c5643ce..251bee2 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -80,6 +80,8 @@ bool zero_damaged_pages = false;
int bgwriter_lru_maxpages = 100;
double bgwriter_lru_multiplier = 2.0;
bool track_io_timing = false;
+/* hint to move writes to high priority */
+bool checkpoint_flush_to_disk = false;
bool checkpoint_sort = true;
/*
@@ -400,7 +402,8 @@ static bool PinBuffer(volatile BufferDesc *buf, BufferAccessStrategy strategy);
static void PinBuffer_Locked(volatile BufferDesc *buf);
static void UnpinBuffer(volatile BufferDesc *buf, bool fixOwner);
static void BufferSync(int flags);
-static int SyncOneBuffer(int buf_id, bool skip_recently_used);
+static int SyncOneBuffer(int buf_id, bool skip_recently_used,
+ bool flush_to_disk, FileFlushContext *context);
static void WaitIO(volatile BufferDesc *buf);
static bool StartBufferIO(volatile BufferDesc *buf, bool forInput);
static void TerminateBufferIO(volatile BufferDesc *buf, bool clear_dirty,
@@ -413,7 +416,8 @@ static volatile BufferDesc *BufferAlloc(SMgrRelation smgr,
BlockNumber blockNum,
BufferAccessStrategy strategy,
bool *foundPtr);
-static void FlushBuffer(volatile BufferDesc *buf, SMgrRelation reln);
+static void FlushBuffer(volatile BufferDesc *buf, SMgrRelation reln,
+ bool flush_to_disk, FileFlushContext *context);
static void AtProcExit_Buffers(int code, Datum arg);
static void CheckForBufferLeaks(void);
static int rnode_comparator(const void *p1, const void *p2);
@@ -1022,7 +1026,7 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
smgr->smgr_rnode.node.dbNode,
smgr->smgr_rnode.node.relNode);
- FlushBuffer(buf, NULL);
+ FlushBuffer(buf, NULL, false, NULL);
LWLockRelease(buf->content_lock);
TRACE_POSTGRESQL_BUFFER_WRITE_DIRTY_DONE(forkNum, blockNum,
@@ -1708,6 +1712,7 @@ BufferSync(int flags)
HTAB *spcBuffers;
TableSpaceCheckpointStatus *spcStatus = NULL;
int nb_spaces, space;
+ FileFlushContext * spcContext = NULL;
/* Make sure we can handle the pin inside SyncOneBuffer */
ResourceOwnerEnlargeBuffers(CurrentResourceOwner);
@@ -1793,10 +1798,12 @@ BufferSync(int flags)
TRACE_POSTGRESQL_BUFFER_SYNC_START(NBuffers, num_to_write);
- /* Build checkpoint tablespace buffer status */
+ /* Build checkpoint tablespace buffer status & flush context arrays */
nb_spaces = hash_get_num_entries(spcBuffers);
spcStatus = (TableSpaceCheckpointStatus *)
palloc(sizeof(TableSpaceCheckpointStatus) * nb_spaces);
+ spcContext = (FileFlushContext *)
+ palloc(sizeof(FileFlushContext) * nb_spaces);
{
int index = 0;
@@ -1813,6 +1820,12 @@ BufferSync(int flags)
/* should it be randomized? chosen with some criterion? */
spcStatus[index].index = 0;
+#if defined(HAVE_SYNC_FILE_RANGE) || defined(HAVE_POSIX_FADVISE)
+
+ ResetFileFlushContext(&spcContext[index]);
+
+#endif /* HAVE_SYNC_FILE_RANGE || HAVE_POSIX_FADVISE */
+
index ++;
}
}
@@ -1882,7 +1895,8 @@ BufferSync(int flags)
*/
if (bufHdr != NULL && bufHdr->flags & BM_CHECKPOINT_NEEDED)
{
- if (SyncOneBuffer(buf_id, false) & BUF_WRITTEN)
+ if (SyncOneBuffer(buf_id, false, checkpoint_flush_to_disk,
+ &spcContext[space]) & BUF_WRITTEN)
{
TRACE_POSTGRESQL_BUFFER_SYNC_WRITTEN(buf_id);
BgWriterStats.m_buf_written_checkpoints++;
@@ -1892,7 +1906,8 @@ BufferSync(int flags)
/*
* Sleep to throttle our I/O rate.
*/
- CheckpointWriteDelay(flags, (double) num_written / num_to_write);
+ CheckpointWriteDelay(flags, (double) num_written / num_to_write,
+ spcContext, nb_spaces);
}
}
@@ -1908,6 +1923,13 @@ BufferSync(int flags)
if (spcStatus[space].index >= num_to_write ||
spcStatus[space].num_written >= spcStatus[space].num_to_write)
{
+#if defined(HAVE_SYNC_FILE_RANGE) || defined(HAVE_POSIX_FADVISE)
+
+ PerformFileFlush(&spcContext[space]);
+ ResetFileFlushContext(&spcContext[space]);
+
+#endif /* HAVE_SYNC_FILE_RANGE || HAVE_POSIX_FADVISE */
+
nb_spaces--;
if (space != nb_spaces)
spcStatus[space] = spcStatus[nb_spaces];
@@ -1918,6 +1940,8 @@ BufferSync(int flags)
pfree(spcStatus);
spcStatus = NULL;
+ pfree(spcContext);
+ spcContext = NULL;
/*
* Update checkpoint statistics. As noted above, this doesn't include
@@ -2165,7 +2189,8 @@ BgBufferSync(void)
/* Execute the LRU scan */
while (num_to_scan > 0 && reusable_buffers < upcoming_alloc_est)
{
- int buffer_state = SyncOneBuffer(next_to_clean, true);
+ int buffer_state =
+ SyncOneBuffer(next_to_clean, true, false, NULL);
if (++next_to_clean >= NBuffers)
{
@@ -2242,7 +2267,8 @@ BgBufferSync(void)
* Note: caller must have done ResourceOwnerEnlargeBuffers.
*/
static int
-SyncOneBuffer(int buf_id, bool skip_recently_used)
+SyncOneBuffer(int buf_id, bool skip_recently_used, bool flush_to_disk,
+ FileFlushContext * context)
{
volatile BufferDesc *bufHdr = GetBufferDescriptor(buf_id);
int result = 0;
@@ -2283,7 +2309,7 @@ SyncOneBuffer(int buf_id, bool skip_recently_used)
PinBuffer_Locked(bufHdr);
LWLockAcquire(bufHdr->content_lock, LW_SHARED);
- FlushBuffer(bufHdr, NULL);
+ FlushBuffer(bufHdr, NULL, flush_to_disk, context);
LWLockRelease(bufHdr->content_lock);
UnpinBuffer(bufHdr, true);
@@ -2545,9 +2571,16 @@ BufferGetTag(Buffer buffer, RelFileNode *rnode, ForkNumber *forknum,
*
* If the caller has an smgr reference for the buffer's relation, pass it
* as the second parameter. If not, pass NULL.
+ *
+ * The third parameter tries to hint the OS that a high priority write is meant,
+ * possibly because io-throttling is already managed elsewhere.
+ * The last parameter holds the current flush context that accumulates flush
+ * requests to be performed in one call, instead of being performed on a buffer
+ * per buffer basis.
*/
static void
-FlushBuffer(volatile BufferDesc *buf, SMgrRelation reln)
+FlushBuffer(volatile BufferDesc *buf, SMgrRelation reln, bool flush_to_disk,
+ FileFlushContext * context)
{
XLogRecPtr recptr;
ErrorContextCallback errcallback;
@@ -2636,7 +2669,9 @@ FlushBuffer(volatile BufferDesc *buf, SMgrRelation reln)
buf->tag.forkNum,
buf->tag.blockNum,
bufToWrite,
- false);
+ false,
+ flush_to_disk,
+ context);
if (track_io_timing)
{
@@ -3056,7 +3091,9 @@ FlushRelationBuffers(Relation rel)
bufHdr->tag.forkNum,
bufHdr->tag.blockNum,
localpage,
- false);
+ false,
+ false,
+ NULL);
bufHdr->flags &= ~(BM_DIRTY | BM_JUST_DIRTIED);
@@ -3090,7 +3127,7 @@ FlushRelationBuffers(Relation rel)
{
PinBuffer_Locked(bufHdr);
LWLockAcquire(bufHdr->content_lock, LW_SHARED);
- FlushBuffer(bufHdr, rel->rd_smgr);
+ FlushBuffer(bufHdr, rel->rd_smgr, false, NULL);
LWLockRelease(bufHdr->content_lock);
UnpinBuffer(bufHdr, true);
}
@@ -3142,7 +3179,7 @@ FlushDatabaseBuffers(Oid dbid)
{
PinBuffer_Locked(bufHdr);
LWLockAcquire(bufHdr->content_lock, LW_SHARED);
- FlushBuffer(bufHdr, NULL);
+ FlushBuffer(bufHdr, NULL, false, NULL);
LWLockRelease(bufHdr->content_lock);
UnpinBuffer(bufHdr, true);
}
diff --git a/src/backend/storage/buffer/localbuf.c b/src/backend/storage/buffer/localbuf.c
index 3144afe..114a0a6 100644
--- a/src/backend/storage/buffer/localbuf.c
+++ b/src/backend/storage/buffer/localbuf.c
@@ -208,7 +208,9 @@ LocalBufferAlloc(SMgrRelation smgr, ForkNumber forkNum, BlockNumber blockNum,
bufHdr->tag.forkNum,
bufHdr->tag.blockNum,
localpage,
- false);
+ false,
+ false,
+ NULL);
/* Mark not-dirty now in case we error out below */
bufHdr->flags &= ~BM_DIRTY;
diff --git a/src/backend/storage/file/buffile.c b/src/backend/storage/file/buffile.c
index ea4d689..fb3b383 100644
--- a/src/backend/storage/file/buffile.c
+++ b/src/backend/storage/file/buffile.c
@@ -317,7 +317,7 @@ BufFileDumpBuffer(BufFile *file)
return; /* seek failed, give up */
file->offsets[file->curFile] = file->curOffset;
}
- bytestowrite = FileWrite(thisfile, file->buffer + wpos, bytestowrite);
+ bytestowrite = FileWrite(thisfile, file->buffer + wpos, bytestowrite, false, NULL);
if (bytestowrite <= 0)
return; /* failed to write */
file->offsets[file->curFile] += bytestowrite;
diff --git a/src/backend/storage/file/fd.c b/src/backend/storage/file/fd.c
index 1ba4946..daf03e4 100644
--- a/src/backend/storage/file/fd.c
+++ b/src/backend/storage/file/fd.c
@@ -1344,8 +1344,97 @@ retry:
return returnCode;
}
+#if defined(HAVE_SYNC_FILE_RANGE) || defined(HAVE_POSIX_FADVISE)
+
+void
+ResetFileFlushContext(FileFlushContext * context)
+{
+ context->fd = 0;
+ context->ncalls = 0;
+ context->offset = 0;
+ context->nbytes = 0;
+ context->filename = NULL;
+}
+
+void
+PerformFileFlush(FileFlushContext * context)
+{
+ if (context->ncalls != 0)
+ {
+ int rc;
+
+#if defined(HAVE_SYNC_FILE_RANGE)
+
+ /* Linux: tell the memory manager to move these blocks to io so
+ * that they are considered for being actually written to disk.
+ */
+ rc = sync_file_range(context->fd, context->offset, context->nbytes,
+ SYNC_FILE_RANGE_WRITE);
+
+#elif defined(HAVE_POSIX_FADVISE)
+
+ /* Others: say that data should not be kept in memory...
+ * This is not exactly what we want to say, because we want to write
+ * the data for durability but we may need it later nevertheless.
+ * It seems that Linux would free the memory *if* the data has
+ * already been written do disk, else the "dontneed" call is ignored.
+ * For FreeBSD this may have the desired effect of moving the
+ * data to the io layer, although the system does not seem to
+ * take into account the provided offset & size, so it is rather
+ * rough...
+ */
+ rc = posix_fadvise(context->fd, context->offset, context->nbytes,
+ POSIX_FADV_DONTNEED);
+
+#endif
+
+ if (rc < 0)
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not flush block " INT64_FORMAT
+ " on " INT64_FORMAT " blocks in file \"%s\": %m",
+ context->offset / BLCKSZ,
+ context->nbytes / BLCKSZ,
+ context->filename)));
+ }
+}
+
+void
+FileAsynchronousFlush(FileFlushContext * context,
+ int fd, off_t offset, off_t nbytes, char * filename)
+{
+ if (context->ncalls != 0 && context->fd == fd)
+ {
+ /* Same file: merge current flush with previous ones */
+ off_t new_offset = offset < context->offset? offset: context->offset;
+
+ context->nbytes =
+ (context->offset + context->nbytes > offset + nbytes ?
+ context->offset + context->nbytes : offset + nbytes) -
+ new_offset;
+ context->offset = new_offset;
+ context->ncalls ++;
+ }
+ else
+ {
+ /* file has changed; actually flush previous file before restarting
+ * to accumulate flushes
+ */
+ PerformFileFlush(context);
+
+ context->fd = fd;
+ context->ncalls = 1;
+ context->offset = offset;
+ context->nbytes = nbytes;
+ context->filename = filename;
+ }
+}
+
+#endif /* HAVE_SYNC_FILE_RANGE || HAVE_POSIX_FADVISE */
+
int
-FileWrite(File file, char *buffer, int amount)
+FileWrite(File file, char *buffer, int amount, bool flush_to_disk,
+ FileFlushContext * context)
{
int returnCode;
@@ -1395,6 +1484,28 @@ retry:
if (returnCode >= 0)
{
+
+#if defined(HAVE_SYNC_FILE_RANGE) || defined(HAVE_POSIX_FADVISE)
+
+ /*
+ * Calling "write" tells the OS that pg wants to write some page to disk,
+ * however when it is really done is chosen by the OS.
+ * Depending on other disk activities this may be delayed significantly,
+ * maybe up to an "fsync" call, which could induce an IO write surge.
+ * When checkpointing pg is doing its own throttling and the result
+ * should really be written to disk with high priority, so as to meet
+ * the completion target.
+ * This call hints that such write have a higher priority.
+ */
+ if (flush_to_disk && returnCode == amount && errno == 0)
+ {
+ FileAsynchronousFlush(context,
+ VfdCache[file].fd, VfdCache[file].seekPos,
+ amount, VfdCache[file].fileName);
+ }
+
+#endif /* HAVE_SYNC_FILE_RANGE || HAVE_POSIX_FADVISE */
+
VfdCache[file].seekPos += returnCode;
/* maintain fileSize and temporary_files_size if it's a temp file */
diff --git a/src/backend/storage/smgr/md.c b/src/backend/storage/smgr/md.c
index 42a43bb..dbf057f 100644
--- a/src/backend/storage/smgr/md.c
+++ b/src/backend/storage/smgr/md.c
@@ -531,7 +531,7 @@ mdextend(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
errmsg("could not seek to block %u in file \"%s\": %m",
blocknum, FilePathName(v->mdfd_vfd))));
- if ((nbytes = FileWrite(v->mdfd_vfd, buffer, BLCKSZ)) != BLCKSZ)
+ if ((nbytes = FileWrite(v->mdfd_vfd, buffer, BLCKSZ, false, NULL)) != BLCKSZ)
{
if (nbytes < 0)
ereport(ERROR,
@@ -738,7 +738,8 @@ mdread(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
*/
void
mdwrite(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
- char *buffer, bool skipFsync)
+ char *buffer, bool skipFsync, bool flush_to_disk,
+ FileFlushContext * context)
{
off_t seekpos;
int nbytes;
@@ -767,7 +768,7 @@ mdwrite(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
errmsg("could not seek to block %u in file \"%s\": %m",
blocknum, FilePathName(v->mdfd_vfd))));
- nbytes = FileWrite(v->mdfd_vfd, buffer, BLCKSZ);
+ nbytes = FileWrite(v->mdfd_vfd, buffer, BLCKSZ, flush_to_disk, context);
TRACE_POSTGRESQL_SMGR_MD_WRITE_DONE(forknum, blocknum,
reln->smgr_rnode.node.spcNode,
diff --git a/src/backend/storage/smgr/smgr.c b/src/backend/storage/smgr/smgr.c
index 244b4ea..2db3cd3 100644
--- a/src/backend/storage/smgr/smgr.c
+++ b/src/backend/storage/smgr/smgr.c
@@ -52,7 +52,8 @@ typedef struct f_smgr
void (*smgr_read) (SMgrRelation reln, ForkNumber forknum,
BlockNumber blocknum, char *buffer);
void (*smgr_write) (SMgrRelation reln, ForkNumber forknum,
- BlockNumber blocknum, char *buffer, bool skipFsync);
+ BlockNumber blocknum, char *buffer, bool skipFsync,
+ bool flush_to_disk, FileFlushContext *context);
BlockNumber (*smgr_nblocks) (SMgrRelation reln, ForkNumber forknum);
void (*smgr_truncate) (SMgrRelation reln, ForkNumber forknum,
BlockNumber nblocks);
@@ -643,10 +644,11 @@ smgrread(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
*/
void
smgrwrite(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
- char *buffer, bool skipFsync)
+ char *buffer, bool skipFsync, bool flush_to_disk,
+ FileFlushContext * context)
{
(*(smgrsw[reln->smgr_which].smgr_write)) (reln, forknum, blocknum,
- buffer, skipFsync);
+ buffer, skipFsync, flush_to_disk, context);
}
/*
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index cf1e505..617d511 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -158,6 +158,7 @@ static bool check_bonjour(bool *newval, void **extra, GucSource source);
static bool check_ssl(bool *newval, void **extra, GucSource source);
static bool check_stage_log_stats(bool *newval, void **extra, GucSource source);
static bool check_log_stats(bool *newval, void **extra, GucSource source);
+static bool check_flush_to_disk(bool *newval, void **extra, GucSource source);
static bool check_canonical_path(char **newval, void **extra, GucSource source);
static bool check_timezone_abbreviations(char **newval, void **extra, GucSource source);
static void assign_timezone_abbreviations(const char *newval, void *extra);
@@ -1025,6 +1026,16 @@ static struct config_bool ConfigureNamesBool[] =
},
{
+ {"checkpoint_flush_to_disk", PGC_SIGHUP, WAL_CHECKPOINTS,
+ gettext_noop("Hint that checkpoint's writes are high priority."),
+ NULL
+ },
+ &checkpoint_flush_to_disk,
+ false,
+ check_flush_to_disk, NULL, NULL
+ },
+
+ {
{"log_connections", PGC_SU_BACKEND, LOGGING_WHAT,
gettext_noop("Logs each successful connection."),
NULL
@@ -9806,6 +9817,21 @@ check_log_stats(bool *newval, void **extra, GucSource source)
}
static bool
+check_flush_to_disk(bool *newval, void **extra, GucSource source)
+{
+/* This test must be consistent with the one in FileWrite (storage/file/fd.c)
+ */
+#if ! (defined(HAVE_SYNC_FILE_RANGE) || defined(HAVE_POSIX_FADVISE))
+ /* just warn if it has no effect */
+ ereport(WARNING,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("Setting \"checkpoint_flush_to_disk\" has no effect "
+ "on this platform.")));
+#endif /* HAVE_SYNC_FILE_RANGE */
+ return true;
+}
+
+static bool
check_canonical_path(char **newval, void **extra, GucSource source)
{
/*
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index e84f380..66010b1 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -202,6 +202,7 @@
#min_wal_size = 80MB
#checkpoint_completion_target = 0.5 # checkpoint target duration, 0.0 - 1.0
#checkpoint_sort = on # sort buffers on checkpoint
+#checkpoint_flush_to_disk = off # send buffers to disk on checkpoint
#checkpoint_warning = 30s # 0 disables
# - Archiving -
diff --git a/src/include/postmaster/bgwriter.h b/src/include/postmaster/bgwriter.h
index a49c208..f9c8ca1 100644
--- a/src/include/postmaster/bgwriter.h
+++ b/src/include/postmaster/bgwriter.h
@@ -16,6 +16,7 @@
#define _BGWRITER_H
#include "storage/block.h"
+#include "storage/fd.h"
#include "storage/relfilenode.h"
@@ -29,7 +30,8 @@ extern void BackgroundWriterMain(void) pg_attribute_noreturn();
extern void CheckpointerMain(void) pg_attribute_noreturn();
extern void RequestCheckpoint(int flags);
-extern void CheckpointWriteDelay(int flags, double progress);
+extern void CheckpointWriteDelay(int flags, double progress,
+ FileFlushContext * context, int ctx_size);
extern bool ForwardFsyncRequest(RelFileNode rnode, ForkNumber forknum,
BlockNumber segno);
diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.h
index c228f39..db0e2c3 100644
--- a/src/include/storage/bufmgr.h
+++ b/src/include/storage/bufmgr.h
@@ -54,6 +54,7 @@ extern int bgwriter_lru_maxpages;
extern double bgwriter_lru_multiplier;
extern bool track_io_timing;
extern int target_prefetch_pages;
+extern bool checkpoint_flush_to_disk;
extern bool checkpoint_sort;
/* in buf_init.c */
diff --git a/src/include/storage/fd.h b/src/include/storage/fd.h
index 7eabe09..c740ee7 100644
--- a/src/include/storage/fd.h
+++ b/src/include/storage/fd.h
@@ -59,6 +59,22 @@ extern int max_files_per_process;
*/
extern int max_safe_fds;
+/* FileFlushContext:
+ * This structure is used to accumulate several flush requests on a file
+ * into a larger flush request.
+ * - fd: file descriptor of the file
+ * - ncalls: number of flushes merged together
+ * - offset: starting offset (minimum of all offset)
+ * - nbytes: size (minimum extent to cover all flushed data)
+ * - filename: filename of fd for error messages
+ */
+typedef struct FileFlushContext{
+ int fd;
+ int ncalls;
+ off_t offset;
+ off_t nbytes;
+ char * filename;
+} FileFlushContext;
/*
* prototypes for functions in fd.c
@@ -70,7 +86,12 @@ extern File OpenTemporaryFile(bool interXact);
extern void FileClose(File file);
extern int FilePrefetch(File file, off_t offset, int amount);
extern int FileRead(File file, char *buffer, int amount);
-extern int FileWrite(File file, char *buffer, int amount);
+extern void ResetFileFlushContext(FileFlushContext * context);
+extern void PerformFileFlush(FileFlushContext * context);
+extern void FileAsynchronousFlush(FileFlushContext * context,
+ int fd, off_t offset, off_t nbytes, char * filename);
+extern int FileWrite(File file, char *buffer, int amount, bool flush_to_disk,
+ FileFlushContext * context);
extern int FileSync(File file);
extern off_t FileSeek(File file, off_t offset, int whence);
extern int FileTruncate(File file, off_t offset);
diff --git a/src/include/storage/smgr.h b/src/include/storage/smgr.h
index 69a624f..a46a70c 100644
--- a/src/include/storage/smgr.h
+++ b/src/include/storage/smgr.h
@@ -16,6 +16,7 @@
#include "fmgr.h"
#include "storage/block.h"
+#include "storage/fd.h"
#include "storage/relfilenode.h"
@@ -95,7 +96,8 @@ extern void smgrprefetch(SMgrRelation reln, ForkNumber forknum,
extern void smgrread(SMgrRelation reln, ForkNumber forknum,
BlockNumber blocknum, char *buffer);
extern void smgrwrite(SMgrRelation reln, ForkNumber forknum,
- BlockNumber blocknum, char *buffer, bool skipFsync);
+ BlockNumber blocknum, char *buffer, bool skipFsync,
+ bool flush_to_disk, FileFlushContext * context);
extern BlockNumber smgrnblocks(SMgrRelation reln, ForkNumber forknum);
extern void smgrtruncate(SMgrRelation reln, ForkNumber forknum,
BlockNumber nblocks);
@@ -120,8 +122,9 @@ extern void mdprefetch(SMgrRelation reln, ForkNumber forknum,
BlockNumber blocknum);
extern void mdread(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
char *buffer);
-extern void mdwrite(SMgrRelation reln, ForkNumber forknum,
- BlockNumber blocknum, char *buffer, bool skipFsync);
+extern void mdwrite(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
+ char *buffer, bool skipFsync, bool flush_to_disk,
+ FileFlushContext * context);
extern BlockNumber mdnblocks(SMgrRelation reln, ForkNumber forknum);
extern void mdtruncate(SMgrRelation reln, ForkNumber forknum,
BlockNumber nblocks);
Here is a v8,
I collected a few performance figures with this patch on an old box with 8
cores, 16 GB, RAID 1 HDD, under Ubuntu precise.
postgresql.conf:
shared_buffers = 4GB
checkpoint_timeout = 15min
checkpoint_completion_target = 0.8
max_wal_size = 4GB
init> pgbench -i -s 250
warmup> pgbench -T 1200 -M prepared -S -j 2 -c 4
# 400 tps throttled "simple update" test
sh> pgbench -M prepared -N -P 1 -T 4000 -R 400 -L 100 -j 2 -c 4
sort/flush : percent of skipped/late transactions
on on : 2.7
on off : 16.2
off on : 68.4
off off : 68.7
# 200 tps
sh> pgbench -M prepared -N -P 1 -T 4000 -R 200 -L 100 -j 2 -c 4
sort/flush : percent of skipped/late transactions
on on : 2.7
on off : 9.5
off on : 47.4
off off : 48.8
The large "percent of skipped/late transactions" is to be understood as
"fraction of time with postgresql offline because of a write stall".
# full speed 1 client
sh> pgbench -M prepared -N -P 1 -T 4000
sort/flush : tps avg & stddev (percent of time beyond 10.0 tps)
on on : 631 +- 131 (0.1%)
on off : 564 +- 303 (12.0%)
off on : 167 +- 315 (76.8%) # stuck...
off off : 177 +- 305 (71.2%) # ~ current pg
# full speed 2 threads 4 clients
sh> pgbench -M prepared -N -P 1 -T 4000 -j 2 -c 4
sort/flush : tps avg & stddev (percent of time below 10.0 tps)
on on : 1058 +- 455 (0.1%)
on off : 1056 +- 942 (32.8%)
off on : 170 +- 500 (88.3%) # stuck...
off off : 209 +- 506 (82.0%) # ~ current pg
The combined features provide a tps speedup of 3-5 on these runs, and
allow to have some control on write stalls. Flushing is not effective on
unsorted buffers, at least on these example.
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Hi Fabien,
On 2015-08-12 22:34:59 +0200, Fabien COELHO wrote:
sort/flush : tps avg & stddev (percent of time beyond 10.0 tps)
on on : 631 +- 131 (0.1%)
on off : 564 +- 303 (12.0%)
off on : 167 +- 315 (76.8%) # stuck...
off off : 177 +- 305 (71.2%) # ~ current pg
What exactly do you mean with 'stuck'?
- Andres
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 2015-08-11 17:15:22 +0200, Fabien COELHO wrote:
+void +PerformFileFlush(FileFlushContext * context) +{ + if (context->ncalls != 0) + { + int rc; + +#if defined(HAVE_SYNC_FILE_RANGE) + + /* Linux: tell the memory manager to move these blocks to io so + * that they are considered for being actually written to disk. + */ + rc = sync_file_range(context->fd, context->offset, context->nbytes, + SYNC_FILE_RANGE_WRITE); + +#elif defined(HAVE_POSIX_FADVISE) + + /* Others: say that data should not be kept in memory... + * This is not exactly what we want to say, because we want to write + * the data for durability but we may need it later nevertheless. + * It seems that Linux would free the memory *if* the data has + * already been written do disk, else the "dontneed" call is ignored. + * For FreeBSD this may have the desired effect of moving the + * data to the io layer, although the system does not seem to + * take into account the provided offset & size, so it is rather + * rough... + */ + rc = posix_fadvise(context->fd, context->offset, context->nbytes, + POSIX_FADV_DONTNEED); + +#endif + + if (rc < 0) + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not flush block " INT64_FORMAT + " on " INT64_FORMAT " blocks in file \"%s\": %m", + context->offset / BLCKSZ, + context->nbytes / BLCKSZ, + context->filename))); + }
I'm a bit wary that this might cause significant regressions on
platforms not supporting sync_file_range, but support posix_fadvise()
for workloads that are bigger than shared_buffers. Consider what happens
if the workload does *not* fit into shared_buffers but *does* fit into
the OS's buffer cache. Suddenly reads will go to disk again, no?
Greetings,
Andres Freund
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Hello Andres,
On 2015-08-12 22:34:59 +0200, Fabien COELHO wrote:
sort/flush : tps avg & stddev (percent of time beyond 10.0 tps)
on on : 631 +- 131 (0.1%)
on off : 564 +- 303 (12.0%)
off on : 167 +- 315 (76.8%) # stuck...
off off : 177 +- 305 (71.2%) # ~ current pgWhat exactly do you mean with 'stuck'?
I mean that the during the I/O storms induced by the checkpoint pgbench
sometimes get stuck, i.e. does not report its progression every second (I
run with "-P 1"). This occurs when sort is off, either with or without
flush, for instance an extract from the off/off medium run:
progress: 573.0 s, 5.0 tps, lat 933.022 ms stddev 83.977
progress: 574.0 s, 777.1 tps, lat 7.161 ms stddev 37.059
progress: 575.0 s, 148.9 tps, lat 4.597 ms stddev 10.708
progress: 814.4 s, 0.0 tps, lat -nan ms stddev -nan
progress: 815.0 s, 0.0 tps, lat -nan ms stddev -nan
progress: 816.0 s, 0.0 tps, lat -nan ms stddev -nan
progress: 817.0 s, 0.0 tps, lat -nan ms stddev -nan
progress: 818.0 s, 0.0 tps, lat -nan ms stddev -nan
progress: 819.0 s, 0.0 tps, lat -nan ms stddev -nan
progress: 820.0 s, 0.0 tps, lat -nan ms stddev -nan
progress: 821.0 s, 0.0 tps, lat -nan ms stddev -nan
progress: 822.0 s, 0.0 tps, lat -nan ms stddev -nan
progress: 823.0 s, 0.0 tps, lat -nan ms stddev -nan
progress: 824.0 s, 0.0 tps, lat -nan ms stddev -nan
progress: 825.0 s, 0.0 tps, lat -nan ms stddev -nan
progress: 826.0 s, 0.0 tps, lat -nan ms stddev -nan
There is a 239.4 seconds gap in pgbench output. This occurs from time to
time and may represent a significant part of the run, and I count these
"stuck" times as 0 tps. Sometimes pgbench is stuck performance wise but
manages nevetheless to report a "0.0 tps" every second, as above after it
unstuck.
The actual origin of the issue with a stuck client (pgbench, libpq, OS,
postgres...) is unclear to me, but the whole system does not behave well
under an I/O storm anyway, and I have not succeeded in understanding where
pgbench is stuck when it does not report its progress. I tried some runs
with gdb but it did not get stuck and reported a lot of "0.0 tps" during
the storms.
Here are a few more figures with the v8 version of the patch, on a host
with 8 cores, 16 GB, RAID 1 HDD, under Ubuntu precise. I already reported
the medium case, and the small case turned afterwards.
small postgresql.conf:
shared_buffers = 2GB
checkpoint_timeout = 300s # this is the default
checkpoint_completion_target = 0.8
# initialization: pgbench -i -s 120
medium postgresql.conf: ## ALREADY REPORTED
shared_buffers = 4GB
checkpoint_timeout = 15min
checkpoint_completion_target = 0.8
max_wal_size = 4GB
# initialization: pgbench -i -s 250
warmup> pgbench -T 1200 -M prepared -S -j 2 -c 4
# 400 tps throttled test
sh> pgbench -M prepared -N -P 1 -T 4000 -R 400 -L 100 -j 2 -c 4
options / percent of skipped/late transactions
sort/flush / small medium
on on : 3.5 2.7
on off : 24.6 16.2
off on : 66.1 68.4
off off : 63.2 68.7
# 200 tps throttled test
sh> pgbench -M prepared -N -P 1 -T 4000 -R 200 -L 100 -j 2 -c 4
options / percent of skipped/late transactions
sort/flush / small medium
on on : 1.9 2.7
on off : 14.3 9.5
off on : 45.6 47.4
off off : 47.4 48.8
# 100 tps throttled test
sh> pgbench -M prepared -N -P 1 -T 4000 -R 100 -L 100 -j 2 -c 4
options / percent of skipped/late transactions
sort/flush / small medium
on on : 0.9 1.8
on off : 9.3 7.9
off on : 5.0 13.0
off off : 31.2 31.9
# full speed 1 client
sh> pgbench -M prepared -N -P 1 -T 4000
options / tps avg & stddev (percent of time below 10.0 tps)
sort/flush / small medium
on on : 564 +- 148 ( 0.1%) 631 +- 131 ( 0.1%)
on off : 470 +- 340 (21.7%) 564 +- 303 (12.0%)
off on : 157 +- 296 (66.2%) 167 +- 315 (76.8%)
off off : 154 +- 251 (61.5%) 177 +- 305 (71.2%)
# full speed 2 threads 4 clients
sh> pgbench -M prepared -N -P 1 -T 4000 -j 2 -c 4
options / tps avg & stddev (percent of time below 10.0 tps)
sort/flush / small medium
on on : 757 +- 417 ( 0.1%) 1058 +- 455 ( 0.1%)
on off : 752 +- 893 (48.4%) 1056 +- 942 (32.8%)
off on : 173 +- 521 (83.0%) 170 +- 500 (88.3%)
off off : 199 +- 512 (82.5%) 209 +- 506 (82.0%)
In all cases, the "sort on & flush on" provides the best results, with tps
speedup from 3-5, and overall high responsiveness (& lower latency).
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
<Oops, stalled post, sorry wrong "From", resent..>
Hello Andres,
+ rc = posix_fadvise(context->fd, context->offset, [...]
I'm a bit wary that this might cause significant regressions on
platforms not supporting sync_file_range, but support posix_fadvise()
for workloads that are bigger than shared_buffers. Consider what happens
if the workload does *not* fit into shared_buffers but *does* fit into
the OS's buffer cache. Suddenly reads will go to disk again, no?
That is an interesting question!
My current thinking is "maybe yes, maybe no":-), as it may depend on the OS
implementation of posix_fadvise, so it may differ between OS.
This is a reason why I think that flushing should be kept a guc, even if the
sort guc is removed and always on. The sync_file_range implementation is
clearly always very beneficial for Linux, and the posix_fadvise may or may
not induce a good behavior depending on the underlying system.
This is also a reason why the default value for the flush guc is currently
set to false in the patch. The documentation should advise to turn it on for
Linux and to test otherwise. Or if Linux is assumed to be often a host, then
maybe to set the default to on and to suggest that on some systems it may be
better to have it off. (Another reason to keep it "off" is that I'm not sure
about what happens with such HD flushing features on virtual servers).
Overall, I'm not pessimistic, because I've seen I/O storms on a FreeBSD host
and it was as bad as Linux (namely the database and even the box was offline
for long minutes...), and if you can avoid that having to read back some data
may be not that bad a down payment.
The issue is largely mitigated if the data is not removed from
shared_buffers, because the OS buffer is just a copy of already hold data.
What I would do on such systems is to increase shared_buffers and keep
flushing on, that is to count less on the system cache and more on postgres
own cache.
Overall, I'm not convince that the practice of relying on the OS cache is a
good one, given what it does with it, at least on Linux.
Now, if someone could provide a dedicated box with posix_fadvise (say
FreeBSD, maybe others...) for testing that would allow to provide data
instead of speculating... and then maybe to decide to change its default
value.
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Import Notes
Reply to msg id not found: alpine.DEB.2.10.1508171456480.28260@sto
On 2015-08-17 15:21:22 +0200, Fabien COELHO wrote:
My current thinking is "maybe yes, maybe no":-), as it may depend on the OS
implementation of posix_fadvise, so it may differ between OS.
As long as fadvise has no 'undirty' option, I don't see how that
problem goes away. You're telling the OS to throw the buffer away, so
unless it ignores it that'll have consequences when you read the page
back in.
This is a reason why I think that flushing should be kept a guc, even if the
sort guc is removed and always on. The sync_file_range implementation is
clearly always very beneficial for Linux, and the posix_fadvise may or may
not induce a good behavior depending on the underlying system.
That's certainly an argument.
This is also a reason why the default value for the flush guc is currently
set to false in the patch. The documentation should advise to turn it on for
Linux and to test otherwise. Or if Linux is assumed to be often a host, then
maybe to set the default to on and to suggest that on some systems it may be
better to have it off.
I'd say it should then be an os-specific default. No point in making
people work for it needlessly on linux and/or elsewhere.
(Another reason to keep it "off" is that I'm not sure about what
happens with such HD flushing features on virtual servers).
I don't see how that matters? Either the host will entirely ignore
flushing, and thus the sync_file_range and the fsync won't cost much, or
fsync will be honored, in which case the pre-flushing is helpful.
Overall, I'm not pessimistic, because I've seen I/O storms on a FreeBSD host
and it was as bad as Linux (namely the database and even the box was offline
for long minutes...), and if you can avoid that having to read back some
data may be not that bad a down payment.
I don't see how that'd alleviate my fear. Sure, the latency for many
workloads will be better, but I don't how that argument says anything
about the reads? And we'll not just use this in cases it'd be
beneficial...
The issue is largely mitigated if the data is not removed from
shared_buffers, because the OS buffer is just a copy of already hold data.
What I would do on such systems is to increase shared_buffers and keep
flushing on, that is to count less on the system cache and more on postgres
own cache.
That doesn't work that well for a bunch of reasons. For one it's
completely non-adaptive. With the OS's page cache you can rely on free
memory being used for caching *and* it be available should a query or
another program need lots of memory.
Overall, I'm not convince that the practice of relying on the OS cache is a
good one, given what it does with it, at least on Linux.
The alternatives aren't super realistic near-term though. Using direct
IO efficiently on the set of operating systems we support is
*hard*. It's more or less trivial to hack pg up to use direct IO for
relations/shared_buffers, but it'll perform utterly horribly in many
many cases.
To pick one thing out: Without the OS buffering writes any write will
have to wait for the disks, instead being asynchronous. That'll make
writes performed by backends a massive bottleneck.
Now, if someone could provide a dedicated box with posix_fadvise (say
FreeBSD, maybe others...) for testing that would allow to provide data
instead of speculating... and then maybe to decide to change its default
value.
Testing, as an approximation, how it turns out to work on linux would be
a good step.
Greetings,
Andres Freund
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Import Notes
Reply to msg id not found: alpine.DEB.2.10.1508171456480.28260@sto
Hello Andres,
[...] posix_fadvise().
My current thinking is "maybe yes, maybe no":-), as it may depend on the OS
implementation of posix_fadvise, so it may differ between OS.As long as fadvise has no 'undirty' option, I don't see how that
problem goes away. You're telling the OS to throw the buffer away, so
unless it ignores it that'll have consequences when you read the page
back in.
Yep, probably.
Note that we are talking about checkpoints, which "write" buffers out
*but* keep them nevertheless. As the buffer is kept, the OS page is a
duplicate, and freeing it should not harm, at least immediatly.
The situation is different if the memory is reused in between, which is
the work of the bgwriter I think, based on LRU/LFU heuristics, but such
writes are not flushed by the current patch.
Now, if a buffer was recently updated it should not be selected by the
bgwriter, if the LRU/LFU heuristics works as expected, which mitigate the
issue somehow...
To sum up, I agree that it is indeed possible that flushing with
posix_fadvise could reduce read OS-memory hits on some systems for some
workloads, although not on Linux, see below.
So the option is best kept as "off" for now, without further data, I'm
fine with that.
[...] I'd say it should then be an os-specific default. No point in
making people work for it needlessly on linux and/or elsewhere.
Ok. Version 9 attached does that, "on" for Linux, "off" for others because
of the potential issues you mentioned.
(Another reason to keep it "off" is that I'm not sure about what
happens with such HD flushing features on virtual servers).I don't see how that matters? Either the host will entirely ignore
flushing, and thus the sync_file_range and the fsync won't cost much, or
fsync will be honored, in which case the pre-flushing is helpful.
Possibly. I know that I do not know:-) The distance between the database
and real hardware is so great in VM, that I think that it may have any
effect, including good, bad or none:-)
Overall, I'm not pessimistic, because I've seen I/O storms on a FreeBSD host
and it was as bad as Linux (namely the database and even the box was offline
for long minutes...), and if you can avoid that having to read back some
data may be not that bad a down payment.I don't see how that'd alleviate my fear.
I'm trying to mitigate your fears, not to alleviate them:-)
Sure, the latency for many workloads will be better, but I don't how
that argument says anything about the reads?
It just says that there may be a compromise, better in some case, possibly
not so in others, because posix_fadvise does not really say what the
database would like to say to the OS, this is why I wrote such a large
comment about it in the source file in the first place.
And we'll not just use this in cases it'd be beneficial...
I'm fine if it is off by default for some systems. If people want to avoid
write stalls they can use the option, but it may have adverse effect on
the tps in some cases, that's life? Not using the option also has adverse
effects in some cases, because you have write stalls... and currently you
do not have the choice, so it would be a progress.
The issue is largely mitigated if the data is not removed from
shared_buffers, because the OS buffer is just a copy of already hold data.
What I would do on such systems is to increase shared_buffers and keep
flushing on, that is to count less on the system cache and more on postgres
own cache.That doesn't work that well for a bunch of reasons. For one it's
completely non-adaptive. With the OS's page cache you can rely on free
memory being used for caching *and* it be available should a query or
another program need lots of memory.
Yep. I was thinking about a dedicated database server, not a shared one.
Overall, I'm not convince that the practice of relying on the OS cache is a
good one, given what it does with it, at least on Linux.The alternatives aren't super realistic near-term though. Using direct
IO efficiently on the set of operating systems we support is
*hard*. [...]
Sure. This is not necessarily what I had in mind.
Currently pg "write"s stuff to the OS, and then suddenly calls "fsync" out
of the blue, hoping that in between the OS will actually have done a good
job with the underlying hardware. This is pretty naive, the fsync
generates write storms, and the database is offline: trying to improve
these things is the motivation for this patch.
Now if you think of the bgwriter, it does pretty much the same, and
probably may generate plenty of random I/Os, because the underlying
LRU/LFU heuristics used to select buffers does not care about the file
structures.
So I think that to get good performance the database must take some
control over the OS. That does not mean that direct I/O needs to be
involved, although maybe it could, but this patch shows that it is not
needed to improve things.
Now, if someone could provide a dedicated box with posix_fadvise (say
FreeBSD, maybe others...) for testing that would allow to provide data
instead of speculating... and then maybe to decide to change its default
value.Testing, as an approximation, how it turns out to work on linux would be
a good step.
Do you mean testing with posix_fadvise on Linux?
I did think about it, but the documented behavior of this call on Linux is
disappointing: if the buffer has been written to disk, it is freed by the
OS. If not, nothing is done. Given that the flush is called pretty close
after writes, mostly the buffer will not have been written to disk yet,
and the call would just be a no-op... So I concluded that there is no
point in trying that on Linux because it will have no effect other than
loosing some time, IMO.
Really, a useful test would be FreeBSD, when posix_fadvise does move
things to disk, although the actual offsets & length are ignored, but I do
not think that it would be a problem. I do not know about other systems
and what they do with posix_fadvise.
--
Fabien.
Attachments:
checkpoint-continuous-flush-9-a.patchtext/x-diff; name=checkpoint-continuous-flush-9-a.patchDownload
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index e900dcc..1cec243 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -2454,6 +2454,28 @@ include_dir 'conf.d'
</listitem>
</varlistentry>
+ <varlistentry id="guc-checkpoint-sort" xreflabel="checkpoint_sort">
+ <term><varname>checkpoint_sort</varname> (<type>bool</type>)
+ <indexterm>
+ <primary><varname>checkpoint_sort</> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ Whether to sort buffers before writting them out to disk on checkpoint.
+ For a HDD storage, this setting allows to group together
+ neighboring pages written to disk, thus improving performance by
+ reducing random write activity.
+ This sorting should have limited performance effects on SSD backends
+ as such storages have good random write performance, but it may
+ help with wear-leveling so be worth keeping anyway.
+ The default is <literal>on</>.
+ This parameter can only be set in the <filename>postgresql.conf</>
+ file or on the server command line.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="guc-checkpoint-warning" xreflabel="checkpoint_warning">
<term><varname>checkpoint_warning</varname> (<type>integer</type>)
<indexterm>
diff --git a/doc/src/sgml/wal.sgml b/doc/src/sgml/wal.sgml
index e3941c9..f538698 100644
--- a/doc/src/sgml/wal.sgml
+++ b/doc/src/sgml/wal.sgml
@@ -546,6 +546,18 @@
</para>
<para>
+ When hard-disk drives (HDD) are used for terminal data storage
+ <xref linkend="guc-checkpoint-sort"> allows to sort pages
+ so that neighboring pages on disk will be flushed together by
+ chekpoints, reducing the random write load and improving performance.
+ If solid-state drives (SSD) are used, sorting pages induces no benefit
+ as their random write I/O performance is good: this feature could then
+ be disabled by setting <varname>checkpoint_sort</> to <value>off</>.
+ It is possible that sorting may help with SSD wear leveling, so it may
+ be kept on that account.
+ </para>
+
+ <para>
The number of WAL segment files in <filename>pg_xlog</> directory depends on
<varname>min_wal_size</>, <varname>max_wal_size</> and
the amount of WAL generated in previous checkpoint cycles. When old log
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 68e33eb..bee38ab 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -7995,11 +7995,13 @@ LogCheckpointEnd(bool restartpoint)
sync_secs,
total_secs,
longest_secs,
+ sort_secs,
average_secs;
int write_usecs,
sync_usecs,
total_usecs,
longest_usecs,
+ sort_usecs,
average_usecs;
uint64 average_sync_time;
@@ -8030,6 +8032,10 @@ LogCheckpointEnd(bool restartpoint)
CheckpointStats.ckpt_end_t,
&total_secs, &total_usecs);
+ TimestampDifference(CheckpointStats.ckpt_sort_t,
+ CheckpointStats.ckpt_sort_end_t,
+ &sort_secs, &sort_usecs);
+
/*
* Timing values returned from CheckpointStats are in microseconds.
* Convert to the second plus microsecond form that TimestampDifference
@@ -8048,8 +8054,8 @@ LogCheckpointEnd(bool restartpoint)
elog(LOG, "%s complete: wrote %d buffers (%.1f%%); "
"%d transaction log file(s) added, %d removed, %d recycled; "
- "write=%ld.%03d s, sync=%ld.%03d s, total=%ld.%03d s; "
- "sync files=%d, longest=%ld.%03d s, average=%ld.%03d s; "
+ "sort=%ld.%03d s, write=%ld.%03d s, sync=%ld.%03d s, total=%ld.%03d s;"
+ " sync files=%d, longest=%ld.%03d s, average=%ld.%03d s; "
"distance=%d kB, estimate=%d kB",
restartpoint ? "restartpoint" : "checkpoint",
CheckpointStats.ckpt_bufs_written,
@@ -8057,6 +8063,7 @@ LogCheckpointEnd(bool restartpoint)
CheckpointStats.ckpt_segs_added,
CheckpointStats.ckpt_segs_removed,
CheckpointStats.ckpt_segs_recycled,
+ sort_secs, sort_usecs / 1000,
write_secs, write_usecs / 1000,
sync_secs, sync_usecs / 1000,
total_secs, total_usecs / 1000,
diff --git a/src/backend/storage/buffer/buf_init.c b/src/backend/storage/buffer/buf_init.c
index 3ae2848..3bd5eab 100644
--- a/src/backend/storage/buffer/buf_init.c
+++ b/src/backend/storage/buffer/buf_init.c
@@ -65,7 +65,8 @@ void
InitBufferPool(void)
{
bool foundBufs,
- foundDescs;
+ foundDescs,
+ foundCpid;
/* Align descriptors to a cacheline boundary. */
BufferDescriptors = (BufferDescPadded *) CACHELINEALIGN(
@@ -77,10 +78,14 @@ InitBufferPool(void)
ShmemInitStruct("Buffer Blocks",
NBuffers * (Size) BLCKSZ, &foundBufs);
- if (foundDescs || foundBufs)
+ CheckpointBufferIds = (CheckpointSortItem *)
+ ShmemInitStruct("Checkpoint BufferIds",
+ NBuffers * sizeof(CheckpointSortItem), &foundCpid);
+
+ if (foundDescs || foundBufs || foundCpid)
{
- /* both should be present or neither */
- Assert(foundDescs && foundBufs);
+ /* all should be present or neither */
+ Assert(foundDescs && foundBufs && foundCpid);
/* note: this path is only taken in EXEC_BACKEND case */
}
else
@@ -144,5 +149,8 @@ BufferShmemSize(void)
/* size of stuff controlled by freelist.c */
size = add_size(size, StrategyShmemSize());
+ /* size of checkpoint sort array in bufmgr.c */
+ size = add_size(size, mul_size(NBuffers, sizeof(CheckpointSortItem)));
+
return size;
}
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index cd3aaad..ca295f1 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -80,6 +80,7 @@ bool zero_damaged_pages = false;
int bgwriter_lru_maxpages = 100;
double bgwriter_lru_multiplier = 2.0;
bool track_io_timing = false;
+bool checkpoint_sort = true;
/*
* How many buffers PrefetchBuffer callers should try to stay ahead of their
@@ -95,6 +96,9 @@ static bool IsForInput;
/* local state for LockBufferForCleanup */
static volatile BufferDesc *PinCountWaitBuf = NULL;
+/* array of buffer ids & sort criterion of all buffers to checkpoint */
+CheckpointSortItem *CheckpointBufferIds = NULL;
+
/*
* Backend-Private refcount management:
*
@@ -1561,6 +1565,129 @@ UnpinBuffer(volatile BufferDesc *buf, bool fixOwner)
}
}
+/* Compare checkpoint buffers.
+ */
+static int bufcmp(const void * pa, const void * pb)
+{
+ CheckpointSortItem
+ *a = (CheckpointSortItem *) pa,
+ *b = (CheckpointSortItem *) pb;
+
+ /* compare relation */
+ if (a->relNode < b->relNode)
+ return -1;
+ else if (a->relNode > b->relNode)
+ return 1;
+ /* same relation, compare fork */
+ else if (a->forkNum < b->forkNum)
+ return -1;
+ else if (a->forkNum > b->forkNum)
+ return 1;
+ /* same relation/fork, so same segmented "file", compare block number
+ * which are mapped on different segments depending on the number.
+ */
+ else if (a->blockNum < b->blockNum)
+ return -1;
+ else /* should not be the same block anyway... */
+ return 1;
+}
+
+/* Status of buffers to checkpoint for a particular tablespace,
+ * used internally in BufferSync.
+ * - space: oid of the tablespace
+ * - num_to_write: number of checkpoint pages counted for this tablespace
+ * - num_written: number of pages actually written out
+ * - index: scanning position in CheckpointBufferIds for this tablespace
+ */
+typedef struct TableSpaceCheckpointStatus {
+ Oid space;
+ int num_to_write;
+ int num_written;
+ int index;
+} TableSpaceCheckpointStatus;
+
+/* entry structure for table space to count hashtable,
+ * used internally in BufferSync.
+ */
+typedef struct TableSpaceCountEntry {
+ Oid space;
+ int count;
+} TableSpaceCountEntry;
+
+/* return the next buffer to write, or -1.
+ * this function balances buffers over tablespaces.
+ */
+static int
+NextBufferToWrite(
+ TableSpaceCheckpointStatus *spcStatus, int nb_spaces,
+ int *pspace, int num_to_write, int num_written)
+{
+ int space = *pspace, buf_id = -1, index;
+
+ /*
+ * Select a tablespace depending on the current overall progress.
+ *
+ * The progress ratio of each unfinished tablespace is compared to
+ * the overall progress ratio to find one with is not in advance
+ * (i.e. tablespace ratio <= overall ratio).
+ *
+ * Existence: it is bound to exist otherwise the overall progress
+ * ratio would be inconsistent: with positive buffers to write (t1 & t2)
+ * and already written buffers (w1 & w2), we have:
+ *
+ * If w1/t1 > (w1+w2)/(t1+t2) # one table space is in advance
+ * => w1t1+w1t2 > w1t1+w2t1 => w1t2 > w2t1 => w1t2+w2t2 > w2t1+w2t2
+ * => (w1+w2) / (t1+t2) > w2 / t2 # the other one is late
+ *
+ * The round robin ensures that each space is given some attention
+ * till it is over the current ratio, before going to the next.
+ *
+ * Precision: using int32 computations for comparing fractions
+ * (w1 / t1 > w / t <=> w1 t > w t1) seems a bad idea as the values
+ * can overflow 32-bit integers: the limit would be sqrt(2**31) ~
+ * 46340 buffers, i.e. a 362 MB checkpoint. So ensure that 64-bit
+ * integers are used in the comparison.
+ */
+ while (/* compare tablespace vs overall progress ratio:
+ * tablespace written/to_write > overall written/to_write
+ */
+ (int64) spcStatus[space].num_written * num_to_write >
+ (int64) num_written * spcStatus[space].num_to_write)
+ space = (space + 1) % nb_spaces; /* round robin */
+
+ /*
+ * Find a valid buffer in the selected tablespace,
+ * by continuing the tablespace specific buffer scan
+ * where it was left.
+ */
+ index = spcStatus[space].index;
+
+ while (index < num_to_write && buf_id == -1)
+ {
+ volatile BufferDesc *bufHdr;
+
+ buf_id = CheckpointBufferIds[index].buf_id;
+ bufHdr = GetBufferDescriptor(buf_id);
+
+ /* Skip if in another tablespace or not in checkpoint anymore.
+ * No lock is acquired, see comments below.
+ */
+ if (spcStatus[space].space != bufHdr->tag.rnode.spcNode ||
+ ! (bufHdr->flags & BM_CHECKPOINT_NEEDED))
+ {
+ index ++;
+ buf_id = -1;
+ }
+ }
+
+ /* Update tablespace writing status, will start over at next index */
+ spcStatus[space].index = index+1;
+
+ *pspace = space;
+
+ return buf_id;
+}
+
/*
* BufferSync -- Write out all dirty buffers in the pool.
*
@@ -1574,11 +1701,13 @@ UnpinBuffer(volatile BufferDesc *buf, bool fixOwner)
static void
BufferSync(int flags)
{
- int buf_id;
- int num_to_scan;
+ int buf_id = -1;
int num_to_write;
int num_written;
int mask = BM_DIRTY;
+ HTAB *spcBuffers;
+ TableSpaceCheckpointStatus *spcStatus = NULL;
+ int nb_spaces, space;
/* Make sure we can handle the pin inside SyncOneBuffer */
ResourceOwnerEnlargeBuffers(CurrentResourceOwner);
@@ -1609,6 +1738,18 @@ BufferSync(int flags)
* certainly need to be written for the next checkpoint attempt, too.
*/
num_to_write = 0;
+
+ /* initialize oid -> int buffer count hash table */
+ {
+ HASHCTL ctl;
+
+ MemSet(&ctl, 0, sizeof(HASHCTL));
+ ctl.keysize = sizeof(Oid);
+ ctl.entrysize = sizeof(TableSpaceCountEntry);
+ spcBuffers = hash_create("Number of buffers to write per tablespace",
+ 16, &ctl, HASH_ELEM | HASH_BLOBS);
+ }
+
for (buf_id = 0; buf_id < NBuffers; buf_id++)
{
volatile BufferDesc *bufHdr = GetBufferDescriptor(buf_id);
@@ -1621,32 +1762,111 @@ BufferSync(int flags)
if ((bufHdr->flags & mask) == mask)
{
+ Oid spc;
+ TableSpaceCountEntry * entry;
+ bool found;
+
bufHdr->flags |= BM_CHECKPOINT_NEEDED;
+ CheckpointBufferIds[num_to_write].buf_id = buf_id;
+ CheckpointBufferIds[num_to_write].relNode = bufHdr->tag.rnode.relNode;
+ CheckpointBufferIds[num_to_write].forkNum = bufHdr->tag.forkNum;
+ CheckpointBufferIds[num_to_write].blockNum = bufHdr->tag.blockNum;
num_to_write++;
+
+ /* keep track of per tablespace buffers */
+ spc = bufHdr->tag.rnode.spcNode;
+ entry = (TableSpaceCountEntry *)
+ hash_search(spcBuffers, (void *) &spc, HASH_ENTER, &found);
+
+ if (found) entry->count++;
+ else entry->count = 1;
}
UnlockBufHdr(bufHdr);
}
if (num_to_write == 0)
+ {
+ hash_destroy(spcBuffers);
return; /* nothing to do */
+ }
TRACE_POSTGRESQL_BUFFER_SYNC_START(NBuffers, num_to_write);
+ /* Build checkpoint tablespace buffer status */
+ nb_spaces = hash_get_num_entries(spcBuffers);
+ spcStatus = (TableSpaceCheckpointStatus *)
+ palloc(sizeof(TableSpaceCheckpointStatus) * nb_spaces);
+
+ {
+ int index = 0;
+ HASH_SEQ_STATUS hseq;
+ TableSpaceCountEntry * entry;
+
+ hash_seq_init(&hseq, spcBuffers);
+ while ((entry = (TableSpaceCountEntry *) hash_seq_search(&hseq)))
+ {
+ Assert(index < nb_spaces);
+ spcStatus[index].space = entry->space;
+ spcStatus[index].num_to_write = entry->count;
+ spcStatus[index].num_written = 0;
+ /* should it be randomized? chosen with some criterion? */
+ spcStatus[index].index = 0;
+
+ index ++;
+ }
+ }
+
+ hash_destroy(spcBuffers);
+ spcBuffers = NULL;
+
/*
- * Loop over all buffers again, and write the ones (still) marked with
- * BM_CHECKPOINT_NEEDED. In this loop, we start at the clock sweep point
- * since we might as well dump soon-to-be-recycled buffers first.
+ * Sort buffer ids to help find sequential writes.
*
- * Note that we don't read the buffer alloc count here --- that should be
- * left untouched till the next BgBufferSync() call.
+ * Note: Buffers are not locked in any way during sorting, but that's ok:
+ * A change in the buffer header is only relevant when it changes the
+ * buffer's identity. If the identity has changed it'll have been
+ * written out by BufferAlloc(), so there's no need for checkpointer to
+ * write it out anymore. The buffer might also get written out by a
+ * backend or bgwriter, but that's equally harmless.
+ *
+ * Marked buffers must not be move during the checkpoint.
+ * Also, qsort implementation should be resilient to occasional
+ * contradictions (cmp(a,b) != -cmp(b,a)) because of possible
+ * concurrent changes.
*/
- buf_id = StrategySyncStart(NULL, NULL);
- num_to_scan = NBuffers;
+ CheckpointStats.ckpt_sort_t = GetCurrentTimestamp();
+
+ if (checkpoint_sort)
+ {
+ qsort(CheckpointBufferIds, num_to_write, sizeof(CheckpointSortItem),
+ bufcmp);
+ }
+
+ CheckpointStats.ckpt_sort_end_t = GetCurrentTimestamp();
+
+ /*
+ * Loop over buffers to write through CheckpointBufferIds,
+ * and write the ones (still) marked with BM_CHECKPOINT_NEEDED,
+ * with some round robin over table spaces so as to balance writes,
+ * so that buffer writes move forward roughly proportionally for each
+ * tablespace.
+ *
+ * Termination: if a tablespace is selected by the inner while loop
+ * (see argument there), its index is incremented and will eventually
+ * reach num_to_write, mark this table space scanning as done and
+ * decrement the number of (active) spaces, which will thus reach 0.
+ */
+ space = 0;
num_written = 0;
- while (num_to_scan-- > 0)
+
+ while (nb_spaces != 0)
{
- volatile BufferDesc *bufHdr = GetBufferDescriptor(buf_id);
+ volatile BufferDesc *bufHdr = NULL;
+ buf_id = NextBufferToWrite(spcStatus, nb_spaces, &space,
+ num_to_write, num_written);
+ if (buf_id != -1)
+ bufHdr = GetBufferDescriptor(buf_id);
/*
* We don't need to acquire the lock here, because we're only looking
@@ -1660,39 +1880,45 @@ BufferSync(int flags)
* write the buffer though we didn't need to. It doesn't seem worth
* guarding against this, though.
*/
- if (bufHdr->flags & BM_CHECKPOINT_NEEDED)
+ if (bufHdr != NULL && bufHdr->flags & BM_CHECKPOINT_NEEDED)
{
if (SyncOneBuffer(buf_id, false) & BUF_WRITTEN)
{
TRACE_POSTGRESQL_BUFFER_SYNC_WRITTEN(buf_id);
BgWriterStats.m_buf_written_checkpoints++;
+ spcStatus[space].num_written++;
num_written++;
/*
- * We know there are at most num_to_write buffers with
- * BM_CHECKPOINT_NEEDED set; so we can stop scanning if
- * num_written reaches num_to_write.
- *
- * Note that num_written doesn't include buffers written by
- * other backends, or by the bgwriter cleaning scan. That
- * means that the estimate of how much progress we've made is
- * conservative, and also that this test will often fail to
- * trigger. But it seems worth making anyway.
- */
- if (num_written >= num_to_write)
- break;
-
- /*
* Sleep to throttle our I/O rate.
*/
CheckpointWriteDelay(flags, (double) num_written / num_to_write);
}
}
- if (++buf_id >= NBuffers)
- buf_id = 0;
+ /*
+ * Detect checkpoint end for a tablespace: either the scan is done
+ * or all tablespace buffers have been written out. If so, the
+ * another active tablespace status is moved in place of the current
+ * one and the next round will start on this one, or maybe round about.
+ * Note: maybe an exchange could be made instead in order to keep
+ * informations about the closed table space, but this is currently
+ * not used afterwards.
+ */
+ if (spcStatus[space].index >= num_to_write ||
+ spcStatus[space].num_written >= spcStatus[space].num_to_write)
+ {
+ nb_spaces--;
+ if (space != nb_spaces)
+ spcStatus[space] = spcStatus[nb_spaces];
+ else
+ space = 0;
+ }
}
+ pfree(spcStatus);
+ spcStatus = NULL;
+
/*
* Update checkpoint statistics. As noted above, this doesn't include
* buffers written by other backends or bgwriter scan.
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index b3dac51..cf1e505 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -1013,6 +1013,17 @@ static struct config_bool ConfigureNamesBool[] =
false,
NULL, NULL, NULL
},
+
+ {
+ {"checkpoint_sort", PGC_SIGHUP, WAL_CHECKPOINTS,
+ gettext_noop("Whether disk-page buffers are sorted on checkpoints."),
+ NULL
+ },
+ &checkpoint_sort,
+ true,
+ NULL, NULL, NULL
+ },
+
{
{"log_connections", PGC_SU_BACKEND, LOGGING_WHAT,
gettext_noop("Logs each successful connection."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index e5d275d..e84f380 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -201,6 +201,7 @@
#max_wal_size = 1GB
#min_wal_size = 80MB
#checkpoint_completion_target = 0.5 # checkpoint target duration, 0.0 - 1.0
+#checkpoint_sort = on # sort buffers on checkpoint
#checkpoint_warning = 30s # 0 disables
# - Archiving -
diff --git a/src/include/access/xlog.h b/src/include/access/xlog.h
index 6dacee2..dbd4757 100644
--- a/src/include/access/xlog.h
+++ b/src/include/access/xlog.h
@@ -186,6 +186,8 @@ extern bool XLOG_DEBUG;
typedef struct CheckpointStatsData
{
TimestampTz ckpt_start_t; /* start of checkpoint */
+ TimestampTz ckpt_sort_t; /* start buffer sorting */
+ TimestampTz ckpt_sort_end_t; /* end of sorting */
TimestampTz ckpt_write_t; /* start of flushing buffers */
TimestampTz ckpt_sync_t; /* start of fsyncs */
TimestampTz ckpt_sync_end_t; /* end of fsyncs */
diff --git a/src/include/storage/buf_internals.h b/src/include/storage/buf_internals.h
index 521ee1c..7fde0dc 100644
--- a/src/include/storage/buf_internals.h
+++ b/src/include/storage/buf_internals.h
@@ -210,6 +210,22 @@ extern PGDLLIMPORT BufferDescPadded *BufferDescriptors;
/* in localbuf.c */
extern BufferDesc *LocalBufferDescriptors;
+/* in bufmgr.c */
+
+/*
+ * Structure to sort buffers per file on checkpoints.
+ *
+ * Maybe the sort criterion could be compacted to reduce memory requirement
+ * and for faster comparison?
+ */
+typedef struct CheckpointSortItem {
+ int buf_id;
+ Oid relNode;
+ ForkNumber forkNum; /* only 4 values */
+ BlockNumber blockNum;
+} CheckpointSortItem;
+
+extern CheckpointSortItem *CheckpointBufferIds;
/*
* Internal routines: only called by bufmgr
diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.h
index ec0a254..c228f39 100644
--- a/src/include/storage/bufmgr.h
+++ b/src/include/storage/bufmgr.h
@@ -54,6 +54,7 @@ extern int bgwriter_lru_maxpages;
extern double bgwriter_lru_multiplier;
extern bool track_io_timing;
extern int target_prefetch_pages;
+extern bool checkpoint_sort;
/* in buf_init.c */
extern PGDLLIMPORT char *BufferBlocks;
checkpoint-continuous-flush-9-b.patchtext/x-diff; name=checkpoint-continuous-flush-9-b.patchDownload
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 1cec243..917b2fb 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -2497,6 +2497,24 @@ include_dir 'conf.d'
</listitem>
</varlistentry>
+ <varlistentry id="guc-checkpoint-flush-to-disk" xreflabel="checkpoint_flush_to_disk">
+ <term><varname>checkpoint_flush_to_disk</varname> (<type>bool</type>)
+ <indexterm>
+ <primary><varname>checkpoint_flush_to_disk</> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ When writing data for a checkpoint, hint the underlying OS that the
+ data must be sent to disk as soon as possible. This may help smoothing
+ disk I/O writes and avoid a stall when fsync is issued at the end of
+ the checkpoint, but it may also reduce average performance.
+ This setting may have no effect on some platforms.
+ The default is <literal>on</> on Linux, <literal>off</> otherwise.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="guc-min-wal-size" xreflabel="min_wal_size">
<term><varname>min_wal_size</varname> (<type>integer</type>)
<indexterm>
diff --git a/doc/src/sgml/wal.sgml b/doc/src/sgml/wal.sgml
index f538698..eea6668 100644
--- a/doc/src/sgml/wal.sgml
+++ b/doc/src/sgml/wal.sgml
@@ -558,6 +558,17 @@
</para>
<para>
+ On Linux and POSIX platforms, <xref linkend="guc-checkpoint-flush-to-disk">
+ allows to hint the OS that pages written on checkpoints must be flushed
+ to disk quickly. Otherwise, these pages may be kept in cache for some time,
+ inducing a stall later when <literal>fsync</> is called to actually
+ complete the checkpoint. This setting helps to reduce transaction latency,
+ but it may also have a small adverse effect on the average transaction rate
+ at maximum throughput. This feature probably brings no benefit on SSD,
+ as the I/O write latency is small on such hardware, thus it may be disabled.
+ </para>
+
+ <para>
The number of WAL segment files in <filename>pg_xlog</> directory depends on
<varname>min_wal_size</>, <varname>max_wal_size</> and
the amount of WAL generated in previous checkpoint cycles. When old log
diff --git a/src/backend/access/heap/rewriteheap.c b/src/backend/access/heap/rewriteheap.c
index bcce3e3..f565dc4 100644
--- a/src/backend/access/heap/rewriteheap.c
+++ b/src/backend/access/heap/rewriteheap.c
@@ -918,7 +918,7 @@ logical_heap_rewrite_flush_mappings(RewriteState state)
* Note that we deviate from the usual WAL coding practices here,
* check the above "Logical rewrite support" comment for reasoning.
*/
- written = FileWrite(src->vfd, waldata_start, len);
+ written = FileWrite(src->vfd, waldata_start, len, false, NULL);
if (written != len)
ereport(ERROR,
(errcode_for_file_access(),
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index cf4a6dc..4b5e9cd 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -203,7 +203,7 @@ btbuildempty(PG_FUNCTION_ARGS)
/* Write the page. If archiving/streaming, XLOG it. */
PageSetChecksumInplace(metapage, BTREE_METAPAGE);
smgrwrite(index->rd_smgr, INIT_FORKNUM, BTREE_METAPAGE,
- (char *) metapage, true);
+ (char *) metapage, true, false, NULL);
if (XLogIsNeeded())
log_newpage(&index->rd_smgr->smgr_rnode.node, INIT_FORKNUM,
BTREE_METAPAGE, metapage, false);
diff --git a/src/backend/access/nbtree/nbtsort.c b/src/backend/access/nbtree/nbtsort.c
index f95f67a..ea7a45d 100644
--- a/src/backend/access/nbtree/nbtsort.c
+++ b/src/backend/access/nbtree/nbtsort.c
@@ -315,7 +315,7 @@ _bt_blwritepage(BTWriteState *wstate, Page page, BlockNumber blkno)
{
/* overwriting a block we zero-filled before */
smgrwrite(wstate->index->rd_smgr, MAIN_FORKNUM, blkno,
- (char *) page, true);
+ (char *) page, true, false, NULL);
}
pfree(page);
diff --git a/src/backend/access/spgist/spginsert.c b/src/backend/access/spgist/spginsert.c
index bceee8d..b700efb 100644
--- a/src/backend/access/spgist/spginsert.c
+++ b/src/backend/access/spgist/spginsert.c
@@ -170,7 +170,7 @@ spgbuildempty(PG_FUNCTION_ARGS)
/* Write the page. If archiving/streaming, XLOG it. */
PageSetChecksumInplace(page, SPGIST_METAPAGE_BLKNO);
smgrwrite(index->rd_smgr, INIT_FORKNUM, SPGIST_METAPAGE_BLKNO,
- (char *) page, true);
+ (char *) page, true, false, NULL);
if (XLogIsNeeded())
log_newpage(&index->rd_smgr->smgr_rnode.node, INIT_FORKNUM,
SPGIST_METAPAGE_BLKNO, page, false);
@@ -180,7 +180,7 @@ spgbuildempty(PG_FUNCTION_ARGS)
PageSetChecksumInplace(page, SPGIST_ROOT_BLKNO);
smgrwrite(index->rd_smgr, INIT_FORKNUM, SPGIST_ROOT_BLKNO,
- (char *) page, true);
+ (char *) page, true, false, NULL);
if (XLogIsNeeded())
log_newpage(&index->rd_smgr->smgr_rnode.node, INIT_FORKNUM,
SPGIST_ROOT_BLKNO, page, true);
@@ -190,7 +190,7 @@ spgbuildempty(PG_FUNCTION_ARGS)
PageSetChecksumInplace(page, SPGIST_NULL_BLKNO);
smgrwrite(index->rd_smgr, INIT_FORKNUM, SPGIST_NULL_BLKNO,
- (char *) page, true);
+ (char *) page, true, false, NULL);
if (XLogIsNeeded())
log_newpage(&index->rd_smgr->smgr_rnode.node, INIT_FORKNUM,
SPGIST_NULL_BLKNO, page, true);
diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c
index 3b3a09e..e361907 100644
--- a/src/backend/postmaster/checkpointer.c
+++ b/src/backend/postmaster/checkpointer.c
@@ -665,7 +665,8 @@ ImmediateCheckpointRequested(void)
* fraction between 0.0 meaning none, and 1.0 meaning all done.
*/
void
-CheckpointWriteDelay(int flags, double progress)
+CheckpointWriteDelay(int flags, double progress,
+ FileFlushContext * context, int ctx_size)
{
static int absorb_counter = WRITES_PER_ABSORB;
@@ -700,6 +701,26 @@ CheckpointWriteDelay(int flags, double progress)
*/
pgstat_send_bgwriter();
+#if defined(HAVE_SYNC_FILE_RANGE) || defined(HAVE_POSIX_FADVISE)
+
+ /*
+ * Before sleeping, flush written blocks for each tablespace.
+ */
+ if (checkpoint_flush_to_disk)
+ {
+ int i;
+
+ for (i = 0; i < ctx_size; i++)
+ {
+ if (context[i].ncalls != 0)
+ {
+ PerformFileFlush(&context[i]);
+ ResetFileFlushContext(&context[i]);
+ }
+ }
+ }
+#endif /* HAVE_SYNC_FILE_RANGE || HAVE_POSIX_FADVISE */
+
/*
* This sleep used to be connected to bgwriter_delay, typically 200ms.
* That resulted in more frequent wakeups if not much work to do.
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index ca295f1..3bd2043 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -80,6 +80,8 @@ bool zero_damaged_pages = false;
int bgwriter_lru_maxpages = 100;
double bgwriter_lru_multiplier = 2.0;
bool track_io_timing = false;
+/* hint to move writes to high priority */
+bool checkpoint_flush_to_disk = DEFAULT_CHECKPOINT_FLUSH_TO_DISK;
bool checkpoint_sort = true;
/*
@@ -400,7 +402,8 @@ static bool PinBuffer(volatile BufferDesc *buf, BufferAccessStrategy strategy);
static void PinBuffer_Locked(volatile BufferDesc *buf);
static void UnpinBuffer(volatile BufferDesc *buf, bool fixOwner);
static void BufferSync(int flags);
-static int SyncOneBuffer(int buf_id, bool skip_recently_used);
+static int SyncOneBuffer(int buf_id, bool skip_recently_used,
+ bool flush_to_disk, FileFlushContext *context);
static void WaitIO(volatile BufferDesc *buf);
static bool StartBufferIO(volatile BufferDesc *buf, bool forInput);
static void TerminateBufferIO(volatile BufferDesc *buf, bool clear_dirty,
@@ -413,7 +416,8 @@ static volatile BufferDesc *BufferAlloc(SMgrRelation smgr,
BlockNumber blockNum,
BufferAccessStrategy strategy,
bool *foundPtr);
-static void FlushBuffer(volatile BufferDesc *buf, SMgrRelation reln);
+static void FlushBuffer(volatile BufferDesc *buf, SMgrRelation reln,
+ bool flush_to_disk, FileFlushContext *context);
static void AtProcExit_Buffers(int code, Datum arg);
static void CheckForBufferLeaks(void);
static int rnode_comparator(const void *p1, const void *p2);
@@ -1022,7 +1026,7 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
smgr->smgr_rnode.node.dbNode,
smgr->smgr_rnode.node.relNode);
- FlushBuffer(buf, NULL);
+ FlushBuffer(buf, NULL, false, NULL);
LWLockRelease(buf->content_lock);
TRACE_POSTGRESQL_BUFFER_WRITE_DIRTY_DONE(forkNum, blockNum,
@@ -1708,6 +1712,7 @@ BufferSync(int flags)
HTAB *spcBuffers;
TableSpaceCheckpointStatus *spcStatus = NULL;
int nb_spaces, space;
+ FileFlushContext * spcContext = NULL;
/* Make sure we can handle the pin inside SyncOneBuffer */
ResourceOwnerEnlargeBuffers(CurrentResourceOwner);
@@ -1793,10 +1798,12 @@ BufferSync(int flags)
TRACE_POSTGRESQL_BUFFER_SYNC_START(NBuffers, num_to_write);
- /* Build checkpoint tablespace buffer status */
+ /* Build checkpoint tablespace buffer status & flush context arrays */
nb_spaces = hash_get_num_entries(spcBuffers);
spcStatus = (TableSpaceCheckpointStatus *)
palloc(sizeof(TableSpaceCheckpointStatus) * nb_spaces);
+ spcContext = (FileFlushContext *)
+ palloc(sizeof(FileFlushContext) * nb_spaces);
{
int index = 0;
@@ -1813,6 +1820,12 @@ BufferSync(int flags)
/* should it be randomized? chosen with some criterion? */
spcStatus[index].index = 0;
+#if defined(HAVE_SYNC_FILE_RANGE) || defined(HAVE_POSIX_FADVISE)
+
+ ResetFileFlushContext(&spcContext[index]);
+
+#endif /* HAVE_SYNC_FILE_RANGE || HAVE_POSIX_FADVISE */
+
index ++;
}
}
@@ -1882,7 +1895,8 @@ BufferSync(int flags)
*/
if (bufHdr != NULL && bufHdr->flags & BM_CHECKPOINT_NEEDED)
{
- if (SyncOneBuffer(buf_id, false) & BUF_WRITTEN)
+ if (SyncOneBuffer(buf_id, false, checkpoint_flush_to_disk,
+ &spcContext[space]) & BUF_WRITTEN)
{
TRACE_POSTGRESQL_BUFFER_SYNC_WRITTEN(buf_id);
BgWriterStats.m_buf_written_checkpoints++;
@@ -1892,7 +1906,8 @@ BufferSync(int flags)
/*
* Sleep to throttle our I/O rate.
*/
- CheckpointWriteDelay(flags, (double) num_written / num_to_write);
+ CheckpointWriteDelay(flags, (double) num_written / num_to_write,
+ spcContext, nb_spaces);
}
}
@@ -1908,6 +1923,13 @@ BufferSync(int flags)
if (spcStatus[space].index >= num_to_write ||
spcStatus[space].num_written >= spcStatus[space].num_to_write)
{
+#if defined(HAVE_SYNC_FILE_RANGE) || defined(HAVE_POSIX_FADVISE)
+
+ PerformFileFlush(&spcContext[space]);
+ ResetFileFlushContext(&spcContext[space]);
+
+#endif /* HAVE_SYNC_FILE_RANGE || HAVE_POSIX_FADVISE */
+
nb_spaces--;
if (space != nb_spaces)
spcStatus[space] = spcStatus[nb_spaces];
@@ -1918,6 +1940,8 @@ BufferSync(int flags)
pfree(spcStatus);
spcStatus = NULL;
+ pfree(spcContext);
+ spcContext = NULL;
/*
* Update checkpoint statistics. As noted above, this doesn't include
@@ -2165,7 +2189,8 @@ BgBufferSync(void)
/* Execute the LRU scan */
while (num_to_scan > 0 && reusable_buffers < upcoming_alloc_est)
{
- int buffer_state = SyncOneBuffer(next_to_clean, true);
+ int buffer_state =
+ SyncOneBuffer(next_to_clean, true, false, NULL);
if (++next_to_clean >= NBuffers)
{
@@ -2242,7 +2267,8 @@ BgBufferSync(void)
* Note: caller must have done ResourceOwnerEnlargeBuffers.
*/
static int
-SyncOneBuffer(int buf_id, bool skip_recently_used)
+SyncOneBuffer(int buf_id, bool skip_recently_used, bool flush_to_disk,
+ FileFlushContext * context)
{
volatile BufferDesc *bufHdr = GetBufferDescriptor(buf_id);
int result = 0;
@@ -2283,7 +2309,7 @@ SyncOneBuffer(int buf_id, bool skip_recently_used)
PinBuffer_Locked(bufHdr);
LWLockAcquire(bufHdr->content_lock, LW_SHARED);
- FlushBuffer(bufHdr, NULL);
+ FlushBuffer(bufHdr, NULL, flush_to_disk, context);
LWLockRelease(bufHdr->content_lock);
UnpinBuffer(bufHdr, true);
@@ -2545,9 +2571,16 @@ BufferGetTag(Buffer buffer, RelFileNode *rnode, ForkNumber *forknum,
*
* If the caller has an smgr reference for the buffer's relation, pass it
* as the second parameter. If not, pass NULL.
+ *
+ * The third parameter tries to hint the OS that a high priority write is meant,
+ * possibly because io-throttling is already managed elsewhere.
+ * The last parameter holds the current flush context that accumulates flush
+ * requests to be performed in one call, instead of being performed on a buffer
+ * per buffer basis.
*/
static void
-FlushBuffer(volatile BufferDesc *buf, SMgrRelation reln)
+FlushBuffer(volatile BufferDesc *buf, SMgrRelation reln, bool flush_to_disk,
+ FileFlushContext * context)
{
XLogRecPtr recptr;
ErrorContextCallback errcallback;
@@ -2636,7 +2669,9 @@ FlushBuffer(volatile BufferDesc *buf, SMgrRelation reln)
buf->tag.forkNum,
buf->tag.blockNum,
bufToWrite,
- false);
+ false,
+ flush_to_disk,
+ context);
if (track_io_timing)
{
@@ -3058,7 +3093,9 @@ FlushRelationBuffers(Relation rel)
bufHdr->tag.forkNum,
bufHdr->tag.blockNum,
localpage,
- false);
+ false,
+ false,
+ NULL);
bufHdr->flags &= ~(BM_DIRTY | BM_JUST_DIRTIED);
@@ -3092,7 +3129,7 @@ FlushRelationBuffers(Relation rel)
{
PinBuffer_Locked(bufHdr);
LWLockAcquire(bufHdr->content_lock, LW_SHARED);
- FlushBuffer(bufHdr, rel->rd_smgr);
+ FlushBuffer(bufHdr, rel->rd_smgr, false, NULL);
LWLockRelease(bufHdr->content_lock);
UnpinBuffer(bufHdr, true);
}
@@ -3144,7 +3181,7 @@ FlushDatabaseBuffers(Oid dbid)
{
PinBuffer_Locked(bufHdr);
LWLockAcquire(bufHdr->content_lock, LW_SHARED);
- FlushBuffer(bufHdr, NULL);
+ FlushBuffer(bufHdr, NULL, false, NULL);
LWLockRelease(bufHdr->content_lock);
UnpinBuffer(bufHdr, true);
}
diff --git a/src/backend/storage/buffer/localbuf.c b/src/backend/storage/buffer/localbuf.c
index 3144afe..114a0a6 100644
--- a/src/backend/storage/buffer/localbuf.c
+++ b/src/backend/storage/buffer/localbuf.c
@@ -208,7 +208,9 @@ LocalBufferAlloc(SMgrRelation smgr, ForkNumber forkNum, BlockNumber blockNum,
bufHdr->tag.forkNum,
bufHdr->tag.blockNum,
localpage,
- false);
+ false,
+ false,
+ NULL);
/* Mark not-dirty now in case we error out below */
bufHdr->flags &= ~BM_DIRTY;
diff --git a/src/backend/storage/file/buffile.c b/src/backend/storage/file/buffile.c
index ea4d689..fb3b383 100644
--- a/src/backend/storage/file/buffile.c
+++ b/src/backend/storage/file/buffile.c
@@ -317,7 +317,7 @@ BufFileDumpBuffer(BufFile *file)
return; /* seek failed, give up */
file->offsets[file->curFile] = file->curOffset;
}
- bytestowrite = FileWrite(thisfile, file->buffer + wpos, bytestowrite);
+ bytestowrite = FileWrite(thisfile, file->buffer + wpos, bytestowrite, false, NULL);
if (bytestowrite <= 0)
return; /* failed to write */
file->offsets[file->curFile] += bytestowrite;
diff --git a/src/backend/storage/file/fd.c b/src/backend/storage/file/fd.c
index 1ba4946..daf03e4 100644
--- a/src/backend/storage/file/fd.c
+++ b/src/backend/storage/file/fd.c
@@ -1344,8 +1344,97 @@ retry:
return returnCode;
}
+#if defined(HAVE_SYNC_FILE_RANGE) || defined(HAVE_POSIX_FADVISE)
+
+void
+ResetFileFlushContext(FileFlushContext * context)
+{
+ context->fd = 0;
+ context->ncalls = 0;
+ context->offset = 0;
+ context->nbytes = 0;
+ context->filename = NULL;
+}
+
+void
+PerformFileFlush(FileFlushContext * context)
+{
+ if (context->ncalls != 0)
+ {
+ int rc;
+
+#if defined(HAVE_SYNC_FILE_RANGE)
+
+ /* Linux: tell the memory manager to move these blocks to io so
+ * that they are considered for being actually written to disk.
+ */
+ rc = sync_file_range(context->fd, context->offset, context->nbytes,
+ SYNC_FILE_RANGE_WRITE);
+
+#elif defined(HAVE_POSIX_FADVISE)
+
+ /* Others: say that data should not be kept in memory...
+ * This is not exactly what we want to say, because we want to write
+ * the data for durability but we may need it later nevertheless.
+ * It seems that Linux would free the memory *if* the data has
+ * already been written do disk, else the "dontneed" call is ignored.
+ * For FreeBSD this may have the desired effect of moving the
+ * data to the io layer, although the system does not seem to
+ * take into account the provided offset & size, so it is rather
+ * rough...
+ */
+ rc = posix_fadvise(context->fd, context->offset, context->nbytes,
+ POSIX_FADV_DONTNEED);
+
+#endif
+
+ if (rc < 0)
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not flush block " INT64_FORMAT
+ " on " INT64_FORMAT " blocks in file \"%s\": %m",
+ context->offset / BLCKSZ,
+ context->nbytes / BLCKSZ,
+ context->filename)));
+ }
+}
+
+void
+FileAsynchronousFlush(FileFlushContext * context,
+ int fd, off_t offset, off_t nbytes, char * filename)
+{
+ if (context->ncalls != 0 && context->fd == fd)
+ {
+ /* Same file: merge current flush with previous ones */
+ off_t new_offset = offset < context->offset? offset: context->offset;
+
+ context->nbytes =
+ (context->offset + context->nbytes > offset + nbytes ?
+ context->offset + context->nbytes : offset + nbytes) -
+ new_offset;
+ context->offset = new_offset;
+ context->ncalls ++;
+ }
+ else
+ {
+ /* file has changed; actually flush previous file before restarting
+ * to accumulate flushes
+ */
+ PerformFileFlush(context);
+
+ context->fd = fd;
+ context->ncalls = 1;
+ context->offset = offset;
+ context->nbytes = nbytes;
+ context->filename = filename;
+ }
+}
+
+#endif /* HAVE_SYNC_FILE_RANGE || HAVE_POSIX_FADVISE */
+
int
-FileWrite(File file, char *buffer, int amount)
+FileWrite(File file, char *buffer, int amount, bool flush_to_disk,
+ FileFlushContext * context)
{
int returnCode;
@@ -1395,6 +1484,28 @@ retry:
if (returnCode >= 0)
{
+
+#if defined(HAVE_SYNC_FILE_RANGE) || defined(HAVE_POSIX_FADVISE)
+
+ /*
+ * Calling "write" tells the OS that pg wants to write some page to disk,
+ * however when it is really done is chosen by the OS.
+ * Depending on other disk activities this may be delayed significantly,
+ * maybe up to an "fsync" call, which could induce an IO write surge.
+ * When checkpointing pg is doing its own throttling and the result
+ * should really be written to disk with high priority, so as to meet
+ * the completion target.
+ * This call hints that such write have a higher priority.
+ */
+ if (flush_to_disk && returnCode == amount && errno == 0)
+ {
+ FileAsynchronousFlush(context,
+ VfdCache[file].fd, VfdCache[file].seekPos,
+ amount, VfdCache[file].fileName);
+ }
+
+#endif /* HAVE_SYNC_FILE_RANGE || HAVE_POSIX_FADVISE */
+
VfdCache[file].seekPos += returnCode;
/* maintain fileSize and temporary_files_size if it's a temp file */
diff --git a/src/backend/storage/smgr/md.c b/src/backend/storage/smgr/md.c
index 42a43bb..dbf057f 100644
--- a/src/backend/storage/smgr/md.c
+++ b/src/backend/storage/smgr/md.c
@@ -531,7 +531,7 @@ mdextend(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
errmsg("could not seek to block %u in file \"%s\": %m",
blocknum, FilePathName(v->mdfd_vfd))));
- if ((nbytes = FileWrite(v->mdfd_vfd, buffer, BLCKSZ)) != BLCKSZ)
+ if ((nbytes = FileWrite(v->mdfd_vfd, buffer, BLCKSZ, false, NULL)) != BLCKSZ)
{
if (nbytes < 0)
ereport(ERROR,
@@ -738,7 +738,8 @@ mdread(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
*/
void
mdwrite(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
- char *buffer, bool skipFsync)
+ char *buffer, bool skipFsync, bool flush_to_disk,
+ FileFlushContext * context)
{
off_t seekpos;
int nbytes;
@@ -767,7 +768,7 @@ mdwrite(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
errmsg("could not seek to block %u in file \"%s\": %m",
blocknum, FilePathName(v->mdfd_vfd))));
- nbytes = FileWrite(v->mdfd_vfd, buffer, BLCKSZ);
+ nbytes = FileWrite(v->mdfd_vfd, buffer, BLCKSZ, flush_to_disk, context);
TRACE_POSTGRESQL_SMGR_MD_WRITE_DONE(forknum, blocknum,
reln->smgr_rnode.node.spcNode,
diff --git a/src/backend/storage/smgr/smgr.c b/src/backend/storage/smgr/smgr.c
index 244b4ea..2db3cd3 100644
--- a/src/backend/storage/smgr/smgr.c
+++ b/src/backend/storage/smgr/smgr.c
@@ -52,7 +52,8 @@ typedef struct f_smgr
void (*smgr_read) (SMgrRelation reln, ForkNumber forknum,
BlockNumber blocknum, char *buffer);
void (*smgr_write) (SMgrRelation reln, ForkNumber forknum,
- BlockNumber blocknum, char *buffer, bool skipFsync);
+ BlockNumber blocknum, char *buffer, bool skipFsync,
+ bool flush_to_disk, FileFlushContext *context);
BlockNumber (*smgr_nblocks) (SMgrRelation reln, ForkNumber forknum);
void (*smgr_truncate) (SMgrRelation reln, ForkNumber forknum,
BlockNumber nblocks);
@@ -643,10 +644,11 @@ smgrread(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
*/
void
smgrwrite(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
- char *buffer, bool skipFsync)
+ char *buffer, bool skipFsync, bool flush_to_disk,
+ FileFlushContext * context)
{
(*(smgrsw[reln->smgr_which].smgr_write)) (reln, forknum, blocknum,
- buffer, skipFsync);
+ buffer, skipFsync, flush_to_disk, context);
}
/*
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index cf1e505..94b0d5b 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -158,6 +158,7 @@ static bool check_bonjour(bool *newval, void **extra, GucSource source);
static bool check_ssl(bool *newval, void **extra, GucSource source);
static bool check_stage_log_stats(bool *newval, void **extra, GucSource source);
static bool check_log_stats(bool *newval, void **extra, GucSource source);
+static bool check_flush_to_disk(bool *newval, void **extra, GucSource source);
static bool check_canonical_path(char **newval, void **extra, GucSource source);
static bool check_timezone_abbreviations(char **newval, void **extra, GucSource source);
static void assign_timezone_abbreviations(const char *newval, void *extra);
@@ -1025,6 +1026,17 @@ static struct config_bool ConfigureNamesBool[] =
},
{
+ {"checkpoint_flush_to_disk", PGC_SIGHUP, WAL_CHECKPOINTS,
+ gettext_noop("Hint that checkpoint's writes are high priority."),
+ NULL
+ },
+ &checkpoint_flush_to_disk,
+ /* see bufmgr.h: true on Linux, false otherwise */
+ DEFAULT_CHECKPOINT_FLUSH_TO_DISK,
+ check_flush_to_disk, NULL, NULL
+ },
+
+ {
{"log_connections", PGC_SU_BACKEND, LOGGING_WHAT,
gettext_noop("Logs each successful connection."),
NULL
@@ -9806,6 +9818,21 @@ check_log_stats(bool *newval, void **extra, GucSource source)
}
static bool
+check_flush_to_disk(bool *newval, void **extra, GucSource source)
+{
+/* This test must be consistent with the one in FileWrite (storage/file/fd.c)
+ */
+#if ! (defined(HAVE_SYNC_FILE_RANGE) || defined(HAVE_POSIX_FADVISE))
+ /* just warn if it has no effect */
+ ereport(WARNING,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("Setting \"checkpoint_flush_to_disk\" has no effect "
+ "on this platform.")));
+#endif /* HAVE_SYNC_FILE_RANGE || HAVE_POSIX_FADVISE */
+ return true;
+}
+
+static bool
check_canonical_path(char **newval, void **extra, GucSource source)
{
/*
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index e84f380..a5495da 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -202,6 +202,8 @@
#min_wal_size = 80MB
#checkpoint_completion_target = 0.5 # checkpoint target duration, 0.0 - 1.0
#checkpoint_sort = on # sort buffers on checkpoint
+#checkpoint_flush_to_disk = ? # send buffers to disk on checkpoint
+ # default is on if Linux, off otherwise
#checkpoint_warning = 30s # 0 disables
# - Archiving -
diff --git a/src/include/postmaster/bgwriter.h b/src/include/postmaster/bgwriter.h
index a49c208..f9c8ca1 100644
--- a/src/include/postmaster/bgwriter.h
+++ b/src/include/postmaster/bgwriter.h
@@ -16,6 +16,7 @@
#define _BGWRITER_H
#include "storage/block.h"
+#include "storage/fd.h"
#include "storage/relfilenode.h"
@@ -29,7 +30,8 @@ extern void BackgroundWriterMain(void) pg_attribute_noreturn();
extern void CheckpointerMain(void) pg_attribute_noreturn();
extern void RequestCheckpoint(int flags);
-extern void CheckpointWriteDelay(int flags, double progress);
+extern void CheckpointWriteDelay(int flags, double progress,
+ FileFlushContext * context, int ctx_size);
extern bool ForwardFsyncRequest(RelFileNode rnode, ForkNumber forknum,
BlockNumber segno);
diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.h
index c228f39..4fd3ff5 100644
--- a/src/include/storage/bufmgr.h
+++ b/src/include/storage/bufmgr.h
@@ -54,6 +54,14 @@ extern int bgwriter_lru_maxpages;
extern double bgwriter_lru_multiplier;
extern bool track_io_timing;
extern int target_prefetch_pages;
+
+#ifdef HAVE_SYNC_FILE_RANGE
+#define DEFAULT_CHECKPOINT_FLUSH_TO_DISK true
+#else
+#define DEFAULT_CHECKPOINT_FLUSH_TO_DISK false
+#endif /* HAVE_SYNC_FILE_RANGE */
+
+extern bool checkpoint_flush_to_disk;
extern bool checkpoint_sort;
/* in buf_init.c */
diff --git a/src/include/storage/fd.h b/src/include/storage/fd.h
index 7eabe09..c740ee7 100644
--- a/src/include/storage/fd.h
+++ b/src/include/storage/fd.h
@@ -59,6 +59,22 @@ extern int max_files_per_process;
*/
extern int max_safe_fds;
+/* FileFlushContext:
+ * This structure is used to accumulate several flush requests on a file
+ * into a larger flush request.
+ * - fd: file descriptor of the file
+ * - ncalls: number of flushes merged together
+ * - offset: starting offset (minimum of all offset)
+ * - nbytes: size (minimum extent to cover all flushed data)
+ * - filename: filename of fd for error messages
+ */
+typedef struct FileFlushContext{
+ int fd;
+ int ncalls;
+ off_t offset;
+ off_t nbytes;
+ char * filename;
+} FileFlushContext;
/*
* prototypes for functions in fd.c
@@ -70,7 +86,12 @@ extern File OpenTemporaryFile(bool interXact);
extern void FileClose(File file);
extern int FilePrefetch(File file, off_t offset, int amount);
extern int FileRead(File file, char *buffer, int amount);
-extern int FileWrite(File file, char *buffer, int amount);
+extern void ResetFileFlushContext(FileFlushContext * context);
+extern void PerformFileFlush(FileFlushContext * context);
+extern void FileAsynchronousFlush(FileFlushContext * context,
+ int fd, off_t offset, off_t nbytes, char * filename);
+extern int FileWrite(File file, char *buffer, int amount, bool flush_to_disk,
+ FileFlushContext * context);
extern int FileSync(File file);
extern off_t FileSeek(File file, off_t offset, int whence);
extern int FileTruncate(File file, off_t offset);
diff --git a/src/include/storage/smgr.h b/src/include/storage/smgr.h
index 69a624f..a46a70c 100644
--- a/src/include/storage/smgr.h
+++ b/src/include/storage/smgr.h
@@ -16,6 +16,7 @@
#include "fmgr.h"
#include "storage/block.h"
+#include "storage/fd.h"
#include "storage/relfilenode.h"
@@ -95,7 +96,8 @@ extern void smgrprefetch(SMgrRelation reln, ForkNumber forknum,
extern void smgrread(SMgrRelation reln, ForkNumber forknum,
BlockNumber blocknum, char *buffer);
extern void smgrwrite(SMgrRelation reln, ForkNumber forknum,
- BlockNumber blocknum, char *buffer, bool skipFsync);
+ BlockNumber blocknum, char *buffer, bool skipFsync,
+ bool flush_to_disk, FileFlushContext * context);
extern BlockNumber smgrnblocks(SMgrRelation reln, ForkNumber forknum);
extern void smgrtruncate(SMgrRelation reln, ForkNumber forknum,
BlockNumber nblocks);
@@ -120,8 +122,9 @@ extern void mdprefetch(SMgrRelation reln, ForkNumber forknum,
BlockNumber blocknum);
extern void mdread(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
char *buffer);
-extern void mdwrite(SMgrRelation reln, ForkNumber forknum,
- BlockNumber blocknum, char *buffer, bool skipFsync);
+extern void mdwrite(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
+ char *buffer, bool skipFsync, bool flush_to_disk,
+ FileFlushContext * context);
extern BlockNumber mdnblocks(SMgrRelation reln, ForkNumber forknum);
extern void mdtruncate(SMgrRelation reln, ForkNumber forknum,
BlockNumber nblocks);
On Tue, Aug 18, 2015 at 1:02 AM, Fabien COELHO <coelho@cri.ensmp.fr> wrote:
Hello Andres,
[...] posix_fadvise().
My current thinking is "maybe yes, maybe no":-), as it may depend on the
OS
implementation of posix_fadvise, so it may differ between OS.As long as fadvise has no 'undirty' option, I don't see how that
problem goes away. You're telling the OS to throw the buffer away, so
unless it ignores it that'll have consequences when you read the page
back in.Yep, probably.
Note that we are talking about checkpoints, which "write" buffers out
*but* keep them nevertheless. As the buffer is kept, the OS page is a
duplicate, and freeing it should not harm, at least immediatly.
This theory could makes sense if we can predict in some way that
the data we are flushing out of OS cache won't be needed soon.
After flush, we can only rely to an extent that data could be found in
shared_buffers if the usage_count is high, other wise it could be
replaced any moment by backend needing the buffer and there is no
free buffer. Now here one way to think is that if the usage_count is
low, then anyway it's okay to assume that this won't be needed in near
future, however I don't think relying only on usage_count for such a thing
is good idea.
To sum up, I agree that it is indeed possible that flushing with
posix_fadvise could reduce read OS-memory hits on some systems for some
workloads, although not on Linux, see below.So the option is best kept as "off" for now, without further data, I'm
fine with that.
One point to think here is on what basis user can decide make
this option on, is it predictable in any way?
I think one case could be when the data set fits in shared_buffers.
In general, providing an option is a good idea if user can decide with
ease when to use that option or we can give some clear recommendation
for the same otherwise one has to recommend that test your workload
with this option and if it works then great else don't use it which might
also
be okay in some cases, but it is better to be clear.
One minor point, while glancing through the patch, I noticed that couple
of multiline comments are not written in the way which is usually used
in code (Keep the first line as empty).
+/* Status of buffers to checkpoint for a particular tablespace,
+ * used internally in BufferSync.
+ * - space: oid of the tablespace
+ * - num_to_write: number of checkpoint pages counted for this tablespace
+ * - num_written: number of pages actually written out
+/* entry structure for table space to count hashtable,
+ * used internally in BufferSync.
+ */
With Regards,
Amit Kapila.
EnterpriseDB: http://www.enterprisedb.com
Hello Amit,
So the option is best kept as "off" for now, without further data, I'm
fine with that.One point to think here is on what basis user can decide make
this option on, is it predictable in any way?
I think one case could be when the data set fits in shared_buffers.
Yep.
In general, providing an option is a good idea if user can decide with
ease when to use that option or we can give some clear recommendation
for the same otherwise one has to recommend that test your workload with
this option and if it works then great else don't use it which might
also be okay in some cases, but it is better to be clear.
My opinion, which is not backed by any data (anyone can feel free to
provide a FreeBSD box for testing...) is that it would mostly be an
improvement if you have a significant write load to have the flush option
on when running on non-Linux systems which provide posix_fadvise.
If you have a lot of reads and few writes, then postgresql currently works
reasonably enough, which is why people do not complain too much about
write stalls, and I expect that the situation would not be significantly
degraded.
Now there are competing positive and negative effects induced by using
posix_fadvise, and moreover its implementation varries from OS to OS, so
without running some experiments it is hard to be definite.
One minor point, while glancing through the patch, I noticed that couple
of multiline comments are not written in the way which is usually used
in code (Keep the first line as empty).
Indeed.
Please find attached a v10, where I have reviewed comments for style &
contents, and also slightly extended the documentation about the flush
option to hint that it is essentially useful for high write loads. Without
further data, I think it is not obvious to give more definite advices.
--
Fabien.
Attachments:
checkpoint-continuous-flush-10-a.patchtext/x-diff; name=checkpoint-continuous-flush-10-a.patchDownload
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index e900dcc..1cec243 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -2454,6 +2454,28 @@ include_dir 'conf.d'
</listitem>
</varlistentry>
+ <varlistentry id="guc-checkpoint-sort" xreflabel="checkpoint_sort">
+ <term><varname>checkpoint_sort</varname> (<type>bool</type>)
+ <indexterm>
+ <primary><varname>checkpoint_sort</> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ Whether to sort buffers before writting them out to disk on checkpoint.
+ For a HDD storage, this setting allows to group together
+ neighboring pages written to disk, thus improving performance by
+ reducing random write activity.
+ This sorting should have limited performance effects on SSD backends
+ as such storages have good random write performance, but it may
+ help with wear-leveling so be worth keeping anyway.
+ The default is <literal>on</>.
+ This parameter can only be set in the <filename>postgresql.conf</>
+ file or on the server command line.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="guc-checkpoint-warning" xreflabel="checkpoint_warning">
<term><varname>checkpoint_warning</varname> (<type>integer</type>)
<indexterm>
diff --git a/doc/src/sgml/wal.sgml b/doc/src/sgml/wal.sgml
index e3941c9..f538698 100644
--- a/doc/src/sgml/wal.sgml
+++ b/doc/src/sgml/wal.sgml
@@ -546,6 +546,18 @@
</para>
<para>
+ When hard-disk drives (HDD) are used for terminal data storage
+ <xref linkend="guc-checkpoint-sort"> allows to sort pages
+ so that neighboring pages on disk will be flushed together by
+ chekpoints, reducing the random write load and improving performance.
+ If solid-state drives (SSD) are used, sorting pages induces no benefit
+ as their random write I/O performance is good: this feature could then
+ be disabled by setting <varname>checkpoint_sort</> to <value>off</>.
+ It is possible that sorting may help with SSD wear leveling, so it may
+ be kept on that account.
+ </para>
+
+ <para>
The number of WAL segment files in <filename>pg_xlog</> directory depends on
<varname>min_wal_size</>, <varname>max_wal_size</> and
the amount of WAL generated in previous checkpoint cycles. When old log
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 68e33eb..bee38ab 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -7995,11 +7995,13 @@ LogCheckpointEnd(bool restartpoint)
sync_secs,
total_secs,
longest_secs,
+ sort_secs,
average_secs;
int write_usecs,
sync_usecs,
total_usecs,
longest_usecs,
+ sort_usecs,
average_usecs;
uint64 average_sync_time;
@@ -8030,6 +8032,10 @@ LogCheckpointEnd(bool restartpoint)
CheckpointStats.ckpt_end_t,
&total_secs, &total_usecs);
+ TimestampDifference(CheckpointStats.ckpt_sort_t,
+ CheckpointStats.ckpt_sort_end_t,
+ &sort_secs, &sort_usecs);
+
/*
* Timing values returned from CheckpointStats are in microseconds.
* Convert to the second plus microsecond form that TimestampDifference
@@ -8048,8 +8054,8 @@ LogCheckpointEnd(bool restartpoint)
elog(LOG, "%s complete: wrote %d buffers (%.1f%%); "
"%d transaction log file(s) added, %d removed, %d recycled; "
- "write=%ld.%03d s, sync=%ld.%03d s, total=%ld.%03d s; "
- "sync files=%d, longest=%ld.%03d s, average=%ld.%03d s; "
+ "sort=%ld.%03d s, write=%ld.%03d s, sync=%ld.%03d s, total=%ld.%03d s;"
+ " sync files=%d, longest=%ld.%03d s, average=%ld.%03d s; "
"distance=%d kB, estimate=%d kB",
restartpoint ? "restartpoint" : "checkpoint",
CheckpointStats.ckpt_bufs_written,
@@ -8057,6 +8063,7 @@ LogCheckpointEnd(bool restartpoint)
CheckpointStats.ckpt_segs_added,
CheckpointStats.ckpt_segs_removed,
CheckpointStats.ckpt_segs_recycled,
+ sort_secs, sort_usecs / 1000,
write_secs, write_usecs / 1000,
sync_secs, sync_usecs / 1000,
total_secs, total_usecs / 1000,
diff --git a/src/backend/storage/buffer/buf_init.c b/src/backend/storage/buffer/buf_init.c
index 3ae2848..3bd5eab 100644
--- a/src/backend/storage/buffer/buf_init.c
+++ b/src/backend/storage/buffer/buf_init.c
@@ -65,7 +65,8 @@ void
InitBufferPool(void)
{
bool foundBufs,
- foundDescs;
+ foundDescs,
+ foundCpid;
/* Align descriptors to a cacheline boundary. */
BufferDescriptors = (BufferDescPadded *) CACHELINEALIGN(
@@ -77,10 +78,14 @@ InitBufferPool(void)
ShmemInitStruct("Buffer Blocks",
NBuffers * (Size) BLCKSZ, &foundBufs);
- if (foundDescs || foundBufs)
+ CheckpointBufferIds = (CheckpointSortItem *)
+ ShmemInitStruct("Checkpoint BufferIds",
+ NBuffers * sizeof(CheckpointSortItem), &foundCpid);
+
+ if (foundDescs || foundBufs || foundCpid)
{
- /* both should be present or neither */
- Assert(foundDescs && foundBufs);
+ /* all should be present or neither */
+ Assert(foundDescs && foundBufs && foundCpid);
/* note: this path is only taken in EXEC_BACKEND case */
}
else
@@ -144,5 +149,8 @@ BufferShmemSize(void)
/* size of stuff controlled by freelist.c */
size = add_size(size, StrategyShmemSize());
+ /* size of checkpoint sort array in bufmgr.c */
+ size = add_size(size, mul_size(NBuffers, sizeof(CheckpointSortItem)));
+
return size;
}
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index cd3aaad..8caf774 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -80,6 +80,7 @@ bool zero_damaged_pages = false;
int bgwriter_lru_maxpages = 100;
double bgwriter_lru_multiplier = 2.0;
bool track_io_timing = false;
+bool checkpoint_sort = true;
/*
* How many buffers PrefetchBuffer callers should try to stay ahead of their
@@ -95,6 +96,9 @@ static bool IsForInput;
/* local state for LockBufferForCleanup */
static volatile BufferDesc *PinCountWaitBuf = NULL;
+/* array of buffer ids & sort criterion of all buffers to checkpoint */
+CheckpointSortItem *CheckpointBufferIds = NULL;
+
/*
* Backend-Private refcount management:
*
@@ -1561,6 +1565,130 @@ UnpinBuffer(volatile BufferDesc *buf, bool fixOwner)
}
}
+/* checkpoint buffers comparison */
+static int bufcmp(const void * pa, const void * pb)
+{
+ CheckpointSortItem
+ *a = (CheckpointSortItem *) pa,
+ *b = (CheckpointSortItem *) pb;
+
+ /* compare relation */
+ if (a->relNode < b->relNode)
+ return -1;
+ else if (a->relNode > b->relNode)
+ return 1;
+ /* same relation, compare fork */
+ else if (a->forkNum < b->forkNum)
+ return -1;
+ else if (a->forkNum > b->forkNum)
+ return 1;
+ /* same relation/fork, so same segmented "file", compare block number
+ * which are mapped on different segments depending on the number.
+ */
+ else if (a->blockNum < b->blockNum)
+ return -1;
+ else /* should not be the same block anyway... */
+ return 1;
+}
+
+/*
+ * Status of buffers to checkpoint for a particular tablespace,
+ * used internally in BufferSync.
+ * - space: oid of the tablespace
+ * - num_to_write: number of checkpoint pages counted for this tablespace
+ * - num_written: number of pages actually written out
+ * - index: scanning position in CheckpointBufferIds for this tablespace
+ */
+typedef struct TableSpaceCheckpointStatus {
+ Oid space;
+ int num_to_write;
+ int num_written;
+ int index;
+} TableSpaceCheckpointStatus;
+
+/*
+ * Entry structure for table space to count hashtable,
+ * used internally in BufferSync.
+ */
+typedef struct TableSpaceCountEntry {
+ Oid space;
+ int count;
+} TableSpaceCountEntry;
+
+/*
+ * Return the next buffer to write, or -1.
+ * this function balances buffers over tablespaces, see comment inside.
+ */
+static int
+NextBufferToWrite(
+ TableSpaceCheckpointStatus *spcStatus, int nb_spaces,
+ int *pspace, int num_to_write, int num_written)
+{
+ int space = *pspace, buf_id = -1, index;
+
+ /*
+ * Select a tablespace depending on the current overall progress.
+ *
+ * The progress ratio of each unfinished tablespace is compared to
+ * the overall progress ratio to find one with is not in advance
+ * (i.e. overall ratio > tablespace ratio,
+ * i.e. tablespace written/to_write > overall written/to_write
+ *
+ * Existence: it is bound to exist otherwise the overall progress
+ * ratio would be inconsistent: with positive buffers to write (t1 & t2)
+ * and already written buffers (w1 & w2), we have:
+ *
+ * If w1/t1 > (w1+w2)/(t1+t2) # one table space is in advance
+ * => w1t1+w1t2 > w1t1+w2t1 => w1t2 > w2t1 => w1t2+w2t2 > w2t1+w2t2
+ * => (w1+w2) / (t1+t2) > w2 / t2 # the other one is late
+ *
+ * The round robin ensures that each space is given some attention
+ * till it is over the current ratio, before going to the next.
+ *
+ * Precision: using int32 computations for comparing fractions
+ * (w1 / t1 > w / t <=> w1 t > w t1) seems a bad idea as the values
+ * can overflow 32-bit integers: the limit would be sqrt(2**31) ~
+ * 46340 buffers, i.e. a 362 MB checkpoint. So ensure that 64-bit
+ * integers are used in the comparison.
+ */
+ while ((int64) spcStatus[space].num_written * num_to_write >
+ (int64) num_written * spcStatus[space].num_to_write)
+ space = (space + 1) % nb_spaces; /* round robin */
+
+ /*
+ * Find a valid buffer in the selected tablespace,
+ * by continuing the tablespace specific buffer scan
+ * where it was left.
+ */
+ index = spcStatus[space].index;
+
+ while (index < num_to_write && buf_id == -1)
+ {
+ volatile BufferDesc *bufHdr;
+
+ buf_id = CheckpointBufferIds[index].buf_id;
+ bufHdr = GetBufferDescriptor(buf_id);
+
+ /*
+ * Skip if in another tablespace or not in checkpoint anymore.
+ * No lock is acquired, see comments below.
+ */
+ if (spcStatus[space].space != bufHdr->tag.rnode.spcNode ||
+ ! (bufHdr->flags & BM_CHECKPOINT_NEEDED))
+ {
+ index ++;
+ buf_id = -1;
+ }
+ }
+
+ /* update tablespace writing status, will start over at next index */
+ spcStatus[space].index = index + 1;
+
+ *pspace = space;
+
+ return buf_id;
+}
+
/*
* BufferSync -- Write out all dirty buffers in the pool.
*
@@ -1574,11 +1702,13 @@ UnpinBuffer(volatile BufferDesc *buf, bool fixOwner)
static void
BufferSync(int flags)
{
- int buf_id;
- int num_to_scan;
+ int buf_id = -1;
int num_to_write;
int num_written;
int mask = BM_DIRTY;
+ HTAB *spcBuffers;
+ TableSpaceCheckpointStatus *spcStatus = NULL;
+ int nb_spaces, space;
/* Make sure we can handle the pin inside SyncOneBuffer */
ResourceOwnerEnlargeBuffers(CurrentResourceOwner);
@@ -1609,6 +1739,18 @@ BufferSync(int flags)
* certainly need to be written for the next checkpoint attempt, too.
*/
num_to_write = 0;
+
+ /* initialize oid -> int buffer count hash table */
+ {
+ HASHCTL ctl;
+
+ MemSet(&ctl, 0, sizeof(HASHCTL));
+ ctl.keysize = sizeof(Oid);
+ ctl.entrysize = sizeof(TableSpaceCountEntry);
+ spcBuffers = hash_create("Number of buffers to write per tablespace",
+ 16, &ctl, HASH_ELEM | HASH_BLOBS);
+ }
+
for (buf_id = 0; buf_id < NBuffers; buf_id++)
{
volatile BufferDesc *bufHdr = GetBufferDescriptor(buf_id);
@@ -1621,32 +1763,97 @@ BufferSync(int flags)
if ((bufHdr->flags & mask) == mask)
{
+ Oid spc;
+ TableSpaceCountEntry * entry;
+ bool found;
+
bufHdr->flags |= BM_CHECKPOINT_NEEDED;
+ CheckpointBufferIds[num_to_write].buf_id = buf_id;
+ CheckpointBufferIds[num_to_write].relNode = bufHdr->tag.rnode.relNode;
+ CheckpointBufferIds[num_to_write].forkNum = bufHdr->tag.forkNum;
+ CheckpointBufferIds[num_to_write].blockNum = bufHdr->tag.blockNum;
num_to_write++;
+
+ /* keep track of per tablespace buffers */
+ spc = bufHdr->tag.rnode.spcNode;
+ entry = (TableSpaceCountEntry *)
+ hash_search(spcBuffers, (void *) &spc, HASH_ENTER, &found);
+
+ if (found) entry->count++;
+ else entry->count = 1;
}
UnlockBufHdr(bufHdr);
}
if (num_to_write == 0)
+ {
+ hash_destroy(spcBuffers);
return; /* nothing to do */
+ }
TRACE_POSTGRESQL_BUFFER_SYNC_START(NBuffers, num_to_write);
+ /* Build checkpoint tablespace buffer status */
+ nb_spaces = hash_get_num_entries(spcBuffers);
+ spcStatus = (TableSpaceCheckpointStatus *)
+ palloc(sizeof(TableSpaceCheckpointStatus) * nb_spaces);
+
+ {
+ int index = 0;
+ HASH_SEQ_STATUS hseq;
+ TableSpaceCountEntry * entry;
+
+ hash_seq_init(&hseq, spcBuffers);
+ while ((entry = (TableSpaceCountEntry *) hash_seq_search(&hseq)))
+ {
+ Assert(index < nb_spaces);
+ spcStatus[index].space = entry->space;
+ spcStatus[index].num_to_write = entry->count;
+ spcStatus[index].num_written = 0;
+ /* should it be randomized? chosen with some criterion? */
+ spcStatus[index].index = 0;
+
+ index ++;
+ }
+ }
+
+ hash_destroy(spcBuffers);
+ spcBuffers = NULL;
+
+ /* sort buffer ids to help find sequential writes */
+ CheckpointStats.ckpt_sort_t = GetCurrentTimestamp();
+
+ if (checkpoint_sort)
+ {
+ qsort(CheckpointBufferIds, num_to_write, sizeof(CheckpointSortItem),
+ bufcmp);
+ }
+
+ CheckpointStats.ckpt_sort_end_t = GetCurrentTimestamp();
+
/*
- * Loop over all buffers again, and write the ones (still) marked with
- * BM_CHECKPOINT_NEEDED. In this loop, we start at the clock sweep point
- * since we might as well dump soon-to-be-recycled buffers first.
+ * Loop over buffers to write through CheckpointBufferIds,
+ * and write the ones (still) marked with BM_CHECKPOINT_NEEDED,
+ * with some round robin over table spaces so as to balance writes,
+ * so that buffer writes move forward roughly proportionally for each
+ * tablespace.
*
- * Note that we don't read the buffer alloc count here --- that should be
- * left untouched till the next BgBufferSync() call.
+ * Termination: if a tablespace is selected by the inner while loop
+ * (see argument there), its index is incremented and will eventually
+ * reach num_to_write, mark this table space scanning as done and
+ * decrement the number of (active) spaces, which will thus reach 0.
*/
- buf_id = StrategySyncStart(NULL, NULL);
- num_to_scan = NBuffers;
+ space = 0;
num_written = 0;
- while (num_to_scan-- > 0)
+
+ while (nb_spaces != 0)
{
- volatile BufferDesc *bufHdr = GetBufferDescriptor(buf_id);
+ volatile BufferDesc *bufHdr = NULL;
+ buf_id = NextBufferToWrite(spcStatus, nb_spaces, &space,
+ num_to_write, num_written);
+ if (buf_id != -1)
+ bufHdr = GetBufferDescriptor(buf_id);
/*
* We don't need to acquire the lock here, because we're only looking
@@ -1660,39 +1867,46 @@ BufferSync(int flags)
* write the buffer though we didn't need to. It doesn't seem worth
* guarding against this, though.
*/
- if (bufHdr->flags & BM_CHECKPOINT_NEEDED)
+ if (bufHdr != NULL && bufHdr->flags & BM_CHECKPOINT_NEEDED)
{
if (SyncOneBuffer(buf_id, false) & BUF_WRITTEN)
{
TRACE_POSTGRESQL_BUFFER_SYNC_WRITTEN(buf_id);
BgWriterStats.m_buf_written_checkpoints++;
+ spcStatus[space].num_written++;
num_written++;
/*
- * We know there are at most num_to_write buffers with
- * BM_CHECKPOINT_NEEDED set; so we can stop scanning if
- * num_written reaches num_to_write.
- *
- * Note that num_written doesn't include buffers written by
- * other backends, or by the bgwriter cleaning scan. That
- * means that the estimate of how much progress we've made is
- * conservative, and also that this test will often fail to
- * trigger. But it seems worth making anyway.
- */
- if (num_written >= num_to_write)
- break;
-
- /*
* Sleep to throttle our I/O rate.
*/
CheckpointWriteDelay(flags, (double) num_written / num_to_write);
}
}
- if (++buf_id >= NBuffers)
- buf_id = 0;
+ /*
+ * Detect checkpoint end for a tablespace: either the scan is done
+ * or all tablespace buffers have been written out. If so, the
+ * another active tablespace status is moved in place of the current
+ * one and the next round will start on this one, or maybe round about.
+ *
+ * Note: maybe an exchange could be made instead in order to keep
+ * informations about the closed table space, but this is currently
+ * not used afterwards.
+ */
+ if (spcStatus[space].index >= num_to_write ||
+ spcStatus[space].num_written >= spcStatus[space].num_to_write)
+ {
+ nb_spaces--;
+ if (space != nb_spaces)
+ spcStatus[space] = spcStatus[nb_spaces];
+ else
+ space = 0;
+ }
}
+ pfree(spcStatus);
+ spcStatus = NULL;
+
/*
* Update checkpoint statistics. As noted above, this doesn't include
* buffers written by other backends or bgwriter scan.
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index b3dac51..cf1e505 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -1013,6 +1013,17 @@ static struct config_bool ConfigureNamesBool[] =
false,
NULL, NULL, NULL
},
+
+ {
+ {"checkpoint_sort", PGC_SIGHUP, WAL_CHECKPOINTS,
+ gettext_noop("Whether disk-page buffers are sorted on checkpoints."),
+ NULL
+ },
+ &checkpoint_sort,
+ true,
+ NULL, NULL, NULL
+ },
+
{
{"log_connections", PGC_SU_BACKEND, LOGGING_WHAT,
gettext_noop("Logs each successful connection."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index e5d275d..e84f380 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -201,6 +201,7 @@
#max_wal_size = 1GB
#min_wal_size = 80MB
#checkpoint_completion_target = 0.5 # checkpoint target duration, 0.0 - 1.0
+#checkpoint_sort = on # sort buffers on checkpoint
#checkpoint_warning = 30s # 0 disables
# - Archiving -
diff --git a/src/include/access/xlog.h b/src/include/access/xlog.h
index 6dacee2..dbd4757 100644
--- a/src/include/access/xlog.h
+++ b/src/include/access/xlog.h
@@ -186,6 +186,8 @@ extern bool XLOG_DEBUG;
typedef struct CheckpointStatsData
{
TimestampTz ckpt_start_t; /* start of checkpoint */
+ TimestampTz ckpt_sort_t; /* start buffer sorting */
+ TimestampTz ckpt_sort_end_t; /* end of sorting */
TimestampTz ckpt_write_t; /* start of flushing buffers */
TimestampTz ckpt_sync_t; /* start of fsyncs */
TimestampTz ckpt_sync_end_t; /* end of fsyncs */
diff --git a/src/include/storage/buf_internals.h b/src/include/storage/buf_internals.h
index 521ee1c..32f2006 100644
--- a/src/include/storage/buf_internals.h
+++ b/src/include/storage/buf_internals.h
@@ -210,6 +210,23 @@ extern PGDLLIMPORT BufferDescPadded *BufferDescriptors;
/* in localbuf.c */
extern BufferDesc *LocalBufferDescriptors;
+/* in bufmgr.c */
+
+/*
+ * Structure to sort buffers per file on checkpoints.
+ *
+ * This structure is allocated per buffer in shared memory, so it should be
+ * kept as little as possible. Maybe the sort criterion could be compacted
+ * to reduce memory requirement and for faster comparison?
+ */
+typedef struct CheckpointSortItem {
+ int buf_id;
+ Oid relNode;
+ ForkNumber forkNum; /* hm... enum with only 4 values */
+ BlockNumber blockNum;
+} CheckpointSortItem;
+
+extern CheckpointSortItem *CheckpointBufferIds;
/*
* Internal routines: only called by bufmgr
diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.h
index ec0a254..c228f39 100644
--- a/src/include/storage/bufmgr.h
+++ b/src/include/storage/bufmgr.h
@@ -54,6 +54,7 @@ extern int bgwriter_lru_maxpages;
extern double bgwriter_lru_multiplier;
extern bool track_io_timing;
extern int target_prefetch_pages;
+extern bool checkpoint_sort;
/* in buf_init.c */
extern PGDLLIMPORT char *BufferBlocks;
checkpoint-continuous-flush-10-b.patchtext/x-diff; name=checkpoint-continuous-flush-10-b.patchDownload
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 1cec243..917b2fb 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -2497,6 +2497,24 @@ include_dir 'conf.d'
</listitem>
</varlistentry>
+ <varlistentry id="guc-checkpoint-flush-to-disk" xreflabel="checkpoint_flush_to_disk">
+ <term><varname>checkpoint_flush_to_disk</varname> (<type>bool</type>)
+ <indexterm>
+ <primary><varname>checkpoint_flush_to_disk</> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ When writing data for a checkpoint, hint the underlying OS that the
+ data must be sent to disk as soon as possible. This may help smoothing
+ disk I/O writes and avoid a stall when fsync is issued at the end of
+ the checkpoint, but it may also reduce average performance.
+ This setting may have no effect on some platforms.
+ The default is <literal>on</> on Linux, <literal>off</> otherwise.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="guc-min-wal-size" xreflabel="min_wal_size">
<term><varname>min_wal_size</varname> (<type>integer</type>)
<indexterm>
diff --git a/doc/src/sgml/wal.sgml b/doc/src/sgml/wal.sgml
index f538698..1b658f2 100644
--- a/doc/src/sgml/wal.sgml
+++ b/doc/src/sgml/wal.sgml
@@ -558,6 +558,18 @@
</para>
<para>
+ On Linux and POSIX platforms, <xref linkend="guc-checkpoint-flush-to-disk">
+ allows to hint the OS that pages written on checkpoints must be flushed
+ to disk quickly. Otherwise, these pages may be kept in cache for some time,
+ inducing a stall later when <literal>fsync</> is called to actually
+ complete the checkpoint. This setting helps to reduce transaction latency,
+ but it may also have a small adverse effect on the average transaction rate
+ at maximum throughput on some OS. It should be beneficial for high write
+ loads on HDD. This feature probably brings no benefit on SSD, as the I/O
+ write latency is small on such hardware, thus it may be disabled.
+ </para>
+
+ <para>
The number of WAL segment files in <filename>pg_xlog</> directory depends on
<varname>min_wal_size</>, <varname>max_wal_size</> and
the amount of WAL generated in previous checkpoint cycles. When old log
diff --git a/src/backend/access/heap/rewriteheap.c b/src/backend/access/heap/rewriteheap.c
index bcce3e3..f565dc4 100644
--- a/src/backend/access/heap/rewriteheap.c
+++ b/src/backend/access/heap/rewriteheap.c
@@ -918,7 +918,7 @@ logical_heap_rewrite_flush_mappings(RewriteState state)
* Note that we deviate from the usual WAL coding practices here,
* check the above "Logical rewrite support" comment for reasoning.
*/
- written = FileWrite(src->vfd, waldata_start, len);
+ written = FileWrite(src->vfd, waldata_start, len, false, NULL);
if (written != len)
ereport(ERROR,
(errcode_for_file_access(),
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index cf4a6dc..4b5e9cd 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -203,7 +203,7 @@ btbuildempty(PG_FUNCTION_ARGS)
/* Write the page. If archiving/streaming, XLOG it. */
PageSetChecksumInplace(metapage, BTREE_METAPAGE);
smgrwrite(index->rd_smgr, INIT_FORKNUM, BTREE_METAPAGE,
- (char *) metapage, true);
+ (char *) metapage, true, false, NULL);
if (XLogIsNeeded())
log_newpage(&index->rd_smgr->smgr_rnode.node, INIT_FORKNUM,
BTREE_METAPAGE, metapage, false);
diff --git a/src/backend/access/nbtree/nbtsort.c b/src/backend/access/nbtree/nbtsort.c
index f95f67a..ea7a45d 100644
--- a/src/backend/access/nbtree/nbtsort.c
+++ b/src/backend/access/nbtree/nbtsort.c
@@ -315,7 +315,7 @@ _bt_blwritepage(BTWriteState *wstate, Page page, BlockNumber blkno)
{
/* overwriting a block we zero-filled before */
smgrwrite(wstate->index->rd_smgr, MAIN_FORKNUM, blkno,
- (char *) page, true);
+ (char *) page, true, false, NULL);
}
pfree(page);
diff --git a/src/backend/access/spgist/spginsert.c b/src/backend/access/spgist/spginsert.c
index bceee8d..b700efb 100644
--- a/src/backend/access/spgist/spginsert.c
+++ b/src/backend/access/spgist/spginsert.c
@@ -170,7 +170,7 @@ spgbuildempty(PG_FUNCTION_ARGS)
/* Write the page. If archiving/streaming, XLOG it. */
PageSetChecksumInplace(page, SPGIST_METAPAGE_BLKNO);
smgrwrite(index->rd_smgr, INIT_FORKNUM, SPGIST_METAPAGE_BLKNO,
- (char *) page, true);
+ (char *) page, true, false, NULL);
if (XLogIsNeeded())
log_newpage(&index->rd_smgr->smgr_rnode.node, INIT_FORKNUM,
SPGIST_METAPAGE_BLKNO, page, false);
@@ -180,7 +180,7 @@ spgbuildempty(PG_FUNCTION_ARGS)
PageSetChecksumInplace(page, SPGIST_ROOT_BLKNO);
smgrwrite(index->rd_smgr, INIT_FORKNUM, SPGIST_ROOT_BLKNO,
- (char *) page, true);
+ (char *) page, true, false, NULL);
if (XLogIsNeeded())
log_newpage(&index->rd_smgr->smgr_rnode.node, INIT_FORKNUM,
SPGIST_ROOT_BLKNO, page, true);
@@ -190,7 +190,7 @@ spgbuildempty(PG_FUNCTION_ARGS)
PageSetChecksumInplace(page, SPGIST_NULL_BLKNO);
smgrwrite(index->rd_smgr, INIT_FORKNUM, SPGIST_NULL_BLKNO,
- (char *) page, true);
+ (char *) page, true, false, NULL);
if (XLogIsNeeded())
log_newpage(&index->rd_smgr->smgr_rnode.node, INIT_FORKNUM,
SPGIST_NULL_BLKNO, page, true);
diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c
index 3b3a09e..e361907 100644
--- a/src/backend/postmaster/checkpointer.c
+++ b/src/backend/postmaster/checkpointer.c
@@ -665,7 +665,8 @@ ImmediateCheckpointRequested(void)
* fraction between 0.0 meaning none, and 1.0 meaning all done.
*/
void
-CheckpointWriteDelay(int flags, double progress)
+CheckpointWriteDelay(int flags, double progress,
+ FileFlushContext * context, int ctx_size)
{
static int absorb_counter = WRITES_PER_ABSORB;
@@ -700,6 +701,26 @@ CheckpointWriteDelay(int flags, double progress)
*/
pgstat_send_bgwriter();
+#if defined(HAVE_SYNC_FILE_RANGE) || defined(HAVE_POSIX_FADVISE)
+
+ /*
+ * Before sleeping, flush written blocks for each tablespace.
+ */
+ if (checkpoint_flush_to_disk)
+ {
+ int i;
+
+ for (i = 0; i < ctx_size; i++)
+ {
+ if (context[i].ncalls != 0)
+ {
+ PerformFileFlush(&context[i]);
+ ResetFileFlushContext(&context[i]);
+ }
+ }
+ }
+#endif /* HAVE_SYNC_FILE_RANGE || HAVE_POSIX_FADVISE */
+
/*
* This sleep used to be connected to bgwriter_delay, typically 200ms.
* That resulted in more frequent wakeups if not much work to do.
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index 8caf774..436ead2 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -80,6 +80,8 @@ bool zero_damaged_pages = false;
int bgwriter_lru_maxpages = 100;
double bgwriter_lru_multiplier = 2.0;
bool track_io_timing = false;
+/* hint to move writes to high priority */
+bool checkpoint_flush_to_disk = DEFAULT_CHECKPOINT_FLUSH_TO_DISK;
bool checkpoint_sort = true;
/*
@@ -400,7 +402,8 @@ static bool PinBuffer(volatile BufferDesc *buf, BufferAccessStrategy strategy);
static void PinBuffer_Locked(volatile BufferDesc *buf);
static void UnpinBuffer(volatile BufferDesc *buf, bool fixOwner);
static void BufferSync(int flags);
-static int SyncOneBuffer(int buf_id, bool skip_recently_used);
+static int SyncOneBuffer(int buf_id, bool skip_recently_used,
+ bool flush_to_disk, FileFlushContext *context);
static void WaitIO(volatile BufferDesc *buf);
static bool StartBufferIO(volatile BufferDesc *buf, bool forInput);
static void TerminateBufferIO(volatile BufferDesc *buf, bool clear_dirty,
@@ -413,7 +416,8 @@ static volatile BufferDesc *BufferAlloc(SMgrRelation smgr,
BlockNumber blockNum,
BufferAccessStrategy strategy,
bool *foundPtr);
-static void FlushBuffer(volatile BufferDesc *buf, SMgrRelation reln);
+static void FlushBuffer(volatile BufferDesc *buf, SMgrRelation reln,
+ bool flush_to_disk, FileFlushContext *context);
static void AtProcExit_Buffers(int code, Datum arg);
static void CheckForBufferLeaks(void);
static int rnode_comparator(const void *p1, const void *p2);
@@ -1022,7 +1026,7 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
smgr->smgr_rnode.node.dbNode,
smgr->smgr_rnode.node.relNode);
- FlushBuffer(buf, NULL);
+ FlushBuffer(buf, NULL, false, NULL);
LWLockRelease(buf->content_lock);
TRACE_POSTGRESQL_BUFFER_WRITE_DIRTY_DONE(forkNum, blockNum,
@@ -1709,6 +1713,7 @@ BufferSync(int flags)
HTAB *spcBuffers;
TableSpaceCheckpointStatus *spcStatus = NULL;
int nb_spaces, space;
+ FileFlushContext * spcContext = NULL;
/* Make sure we can handle the pin inside SyncOneBuffer */
ResourceOwnerEnlargeBuffers(CurrentResourceOwner);
@@ -1794,10 +1799,12 @@ BufferSync(int flags)
TRACE_POSTGRESQL_BUFFER_SYNC_START(NBuffers, num_to_write);
- /* Build checkpoint tablespace buffer status */
+ /* Build checkpoint tablespace buffer status & flush context arrays */
nb_spaces = hash_get_num_entries(spcBuffers);
spcStatus = (TableSpaceCheckpointStatus *)
palloc(sizeof(TableSpaceCheckpointStatus) * nb_spaces);
+ spcContext = (FileFlushContext *)
+ palloc(sizeof(FileFlushContext) * nb_spaces);
{
int index = 0;
@@ -1814,6 +1821,12 @@ BufferSync(int flags)
/* should it be randomized? chosen with some criterion? */
spcStatus[index].index = 0;
+#if defined(HAVE_SYNC_FILE_RANGE) || defined(HAVE_POSIX_FADVISE)
+
+ ResetFileFlushContext(&spcContext[index]);
+
+#endif /* HAVE_SYNC_FILE_RANGE || HAVE_POSIX_FADVISE */
+
index ++;
}
}
@@ -1869,7 +1882,8 @@ BufferSync(int flags)
*/
if (bufHdr != NULL && bufHdr->flags & BM_CHECKPOINT_NEEDED)
{
- if (SyncOneBuffer(buf_id, false) & BUF_WRITTEN)
+ if (SyncOneBuffer(buf_id, false, checkpoint_flush_to_disk,
+ &spcContext[space]) & BUF_WRITTEN)
{
TRACE_POSTGRESQL_BUFFER_SYNC_WRITTEN(buf_id);
BgWriterStats.m_buf_written_checkpoints++;
@@ -1879,7 +1893,8 @@ BufferSync(int flags)
/*
* Sleep to throttle our I/O rate.
*/
- CheckpointWriteDelay(flags, (double) num_written / num_to_write);
+ CheckpointWriteDelay(flags, (double) num_written / num_to_write,
+ spcContext, nb_spaces);
}
}
@@ -1896,6 +1911,13 @@ BufferSync(int flags)
if (spcStatus[space].index >= num_to_write ||
spcStatus[space].num_written >= spcStatus[space].num_to_write)
{
+#if defined(HAVE_SYNC_FILE_RANGE) || defined(HAVE_POSIX_FADVISE)
+
+ PerformFileFlush(&spcContext[space]);
+ ResetFileFlushContext(&spcContext[space]);
+
+#endif /* HAVE_SYNC_FILE_RANGE || HAVE_POSIX_FADVISE */
+
nb_spaces--;
if (space != nb_spaces)
spcStatus[space] = spcStatus[nb_spaces];
@@ -1906,6 +1928,8 @@ BufferSync(int flags)
pfree(spcStatus);
spcStatus = NULL;
+ pfree(spcContext);
+ spcContext = NULL;
/*
* Update checkpoint statistics. As noted above, this doesn't include
@@ -2153,7 +2177,8 @@ BgBufferSync(void)
/* Execute the LRU scan */
while (num_to_scan > 0 && reusable_buffers < upcoming_alloc_est)
{
- int buffer_state = SyncOneBuffer(next_to_clean, true);
+ int buffer_state =
+ SyncOneBuffer(next_to_clean, true, false, NULL);
if (++next_to_clean >= NBuffers)
{
@@ -2230,7 +2255,8 @@ BgBufferSync(void)
* Note: caller must have done ResourceOwnerEnlargeBuffers.
*/
static int
-SyncOneBuffer(int buf_id, bool skip_recently_used)
+SyncOneBuffer(int buf_id, bool skip_recently_used, bool flush_to_disk,
+ FileFlushContext * context)
{
volatile BufferDesc *bufHdr = GetBufferDescriptor(buf_id);
int result = 0;
@@ -2271,7 +2297,7 @@ SyncOneBuffer(int buf_id, bool skip_recently_used)
PinBuffer_Locked(bufHdr);
LWLockAcquire(bufHdr->content_lock, LW_SHARED);
- FlushBuffer(bufHdr, NULL);
+ FlushBuffer(bufHdr, NULL, flush_to_disk, context);
LWLockRelease(bufHdr->content_lock);
UnpinBuffer(bufHdr, true);
@@ -2533,9 +2559,16 @@ BufferGetTag(Buffer buffer, RelFileNode *rnode, ForkNumber *forknum,
*
* If the caller has an smgr reference for the buffer's relation, pass it
* as the second parameter. If not, pass NULL.
+ *
+ * The third parameter tries to hint the OS that a high priority write is meant,
+ * possibly because io-throttling is already managed elsewhere.
+ * The last parameter holds the current flush context that accumulates flush
+ * requests to be performed in one call, instead of being performed on a buffer
+ * per buffer basis.
*/
static void
-FlushBuffer(volatile BufferDesc *buf, SMgrRelation reln)
+FlushBuffer(volatile BufferDesc *buf, SMgrRelation reln, bool flush_to_disk,
+ FileFlushContext * context)
{
XLogRecPtr recptr;
ErrorContextCallback errcallback;
@@ -2624,7 +2657,9 @@ FlushBuffer(volatile BufferDesc *buf, SMgrRelation reln)
buf->tag.forkNum,
buf->tag.blockNum,
bufToWrite,
- false);
+ false,
+ flush_to_disk,
+ context);
if (track_io_timing)
{
@@ -3046,7 +3081,9 @@ FlushRelationBuffers(Relation rel)
bufHdr->tag.forkNum,
bufHdr->tag.blockNum,
localpage,
- false);
+ false,
+ false,
+ NULL);
bufHdr->flags &= ~(BM_DIRTY | BM_JUST_DIRTIED);
@@ -3080,7 +3117,7 @@ FlushRelationBuffers(Relation rel)
{
PinBuffer_Locked(bufHdr);
LWLockAcquire(bufHdr->content_lock, LW_SHARED);
- FlushBuffer(bufHdr, rel->rd_smgr);
+ FlushBuffer(bufHdr, rel->rd_smgr, false, NULL);
LWLockRelease(bufHdr->content_lock);
UnpinBuffer(bufHdr, true);
}
@@ -3132,7 +3169,7 @@ FlushDatabaseBuffers(Oid dbid)
{
PinBuffer_Locked(bufHdr);
LWLockAcquire(bufHdr->content_lock, LW_SHARED);
- FlushBuffer(bufHdr, NULL);
+ FlushBuffer(bufHdr, NULL, false, NULL);
LWLockRelease(bufHdr->content_lock);
UnpinBuffer(bufHdr, true);
}
diff --git a/src/backend/storage/buffer/localbuf.c b/src/backend/storage/buffer/localbuf.c
index 3144afe..114a0a6 100644
--- a/src/backend/storage/buffer/localbuf.c
+++ b/src/backend/storage/buffer/localbuf.c
@@ -208,7 +208,9 @@ LocalBufferAlloc(SMgrRelation smgr, ForkNumber forkNum, BlockNumber blockNum,
bufHdr->tag.forkNum,
bufHdr->tag.blockNum,
localpage,
- false);
+ false,
+ false,
+ NULL);
/* Mark not-dirty now in case we error out below */
bufHdr->flags &= ~BM_DIRTY;
diff --git a/src/backend/storage/file/buffile.c b/src/backend/storage/file/buffile.c
index ea4d689..fb3b383 100644
--- a/src/backend/storage/file/buffile.c
+++ b/src/backend/storage/file/buffile.c
@@ -317,7 +317,7 @@ BufFileDumpBuffer(BufFile *file)
return; /* seek failed, give up */
file->offsets[file->curFile] = file->curOffset;
}
- bytestowrite = FileWrite(thisfile, file->buffer + wpos, bytestowrite);
+ bytestowrite = FileWrite(thisfile, file->buffer + wpos, bytestowrite, false, NULL);
if (bytestowrite <= 0)
return; /* failed to write */
file->offsets[file->curFile] += bytestowrite;
diff --git a/src/backend/storage/file/fd.c b/src/backend/storage/file/fd.c
index 1ba4946..e880a9e 100644
--- a/src/backend/storage/file/fd.c
+++ b/src/backend/storage/file/fd.c
@@ -1344,8 +1344,97 @@ retry:
return returnCode;
}
+#if defined(HAVE_SYNC_FILE_RANGE) || defined(HAVE_POSIX_FADVISE)
+
+void
+ResetFileFlushContext(FileFlushContext * context)
+{
+ context->fd = 0;
+ context->ncalls = 0;
+ context->offset = 0;
+ context->nbytes = 0;
+ context->filename = NULL;
+}
+
+void
+PerformFileFlush(FileFlushContext * context)
+{
+ if (context->ncalls != 0)
+ {
+ int rc;
+
+#if defined(HAVE_SYNC_FILE_RANGE)
+
+ /*
+ * Linux: tell the memory manager to move these blocks to io so
+ * that they are considered for being actually written to disk.
+ */
+ rc = sync_file_range(context->fd, context->offset, context->nbytes,
+ SYNC_FILE_RANGE_WRITE);
+
+#elif defined(HAVE_POSIX_FADVISE)
+
+ /*
+ * Others: say that data should not be kept in memory...
+ * This is not exactly what we want to say, because we want to write
+ * the data for durability but we may need it later nevertheless.
+ * It seems that Linux would free the memory *if* the data has
+ * already been written do disk, else the "dontneed" call is ignored.
+ * For FreeBSD this may have the desired effect of moving the
+ * data to the io layer, although the system does not seem to
+ * take into account the provided offset & size, so it is rather
+ * rough...
+ */
+ rc = posix_fadvise(context->fd, context->offset, context->nbytes,
+ POSIX_FADV_DONTNEED);
+
+#endif
+
+ if (rc < 0)
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not flush block " INT64_FORMAT
+ " on " INT64_FORMAT " blocks in file \"%s\": %m",
+ context->offset / BLCKSZ,
+ context->nbytes / BLCKSZ,
+ context->filename)));
+ }
+}
+
+void
+FileAsynchronousFlush(FileFlushContext * context,
+ int fd, off_t offset, off_t nbytes, char * filename)
+{
+ if (context->ncalls != 0 && context->fd == fd)
+ {
+ /* same file: merge current flush with previous ones */
+ off_t new_offset = offset < context->offset? offset: context->offset;
+
+ context->nbytes =
+ (context->offset + context->nbytes > offset + nbytes ?
+ context->offset + context->nbytes : offset + nbytes) -
+ new_offset;
+ context->offset = new_offset;
+ context->ncalls ++;
+ }
+ else
+ {
+ /* other file: do flush previous file & reset flush accumulator */
+ PerformFileFlush(context);
+
+ context->fd = fd;
+ context->ncalls = 1;
+ context->offset = offset;
+ context->nbytes = nbytes;
+ context->filename = filename;
+ }
+}
+
+#endif /* HAVE_SYNC_FILE_RANGE || HAVE_POSIX_FADVISE */
+
int
-FileWrite(File file, char *buffer, int amount)
+FileWrite(File file, char *buffer, int amount, bool flush_to_disk,
+ FileFlushContext * context)
{
int returnCode;
@@ -1395,6 +1484,28 @@ retry:
if (returnCode >= 0)
{
+
+#if defined(HAVE_SYNC_FILE_RANGE) || defined(HAVE_POSIX_FADVISE)
+
+ /*
+ * Calling "write" tells the OS that pg wants to write some page to disk,
+ * however when it is really done is chosen by the OS.
+ * Depending on other disk activities this may be delayed significantly,
+ * maybe up to an "fsync" call, which could induce an IO write surge.
+ * When checkpointing pg is doing its own throttling and the result
+ * should really be written to disk with high priority, so as to meet
+ * the completion target.
+ * This call hints that such write have a higher priority.
+ */
+ if (flush_to_disk && returnCode == amount && errno == 0)
+ {
+ FileAsynchronousFlush(context,
+ VfdCache[file].fd, VfdCache[file].seekPos,
+ amount, VfdCache[file].fileName);
+ }
+
+#endif /* HAVE_SYNC_FILE_RANGE || HAVE_POSIX_FADVISE */
+
VfdCache[file].seekPos += returnCode;
/* maintain fileSize and temporary_files_size if it's a temp file */
diff --git a/src/backend/storage/smgr/md.c b/src/backend/storage/smgr/md.c
index 42a43bb..dbf057f 100644
--- a/src/backend/storage/smgr/md.c
+++ b/src/backend/storage/smgr/md.c
@@ -531,7 +531,7 @@ mdextend(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
errmsg("could not seek to block %u in file \"%s\": %m",
blocknum, FilePathName(v->mdfd_vfd))));
- if ((nbytes = FileWrite(v->mdfd_vfd, buffer, BLCKSZ)) != BLCKSZ)
+ if ((nbytes = FileWrite(v->mdfd_vfd, buffer, BLCKSZ, false, NULL)) != BLCKSZ)
{
if (nbytes < 0)
ereport(ERROR,
@@ -738,7 +738,8 @@ mdread(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
*/
void
mdwrite(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
- char *buffer, bool skipFsync)
+ char *buffer, bool skipFsync, bool flush_to_disk,
+ FileFlushContext * context)
{
off_t seekpos;
int nbytes;
@@ -767,7 +768,7 @@ mdwrite(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
errmsg("could not seek to block %u in file \"%s\": %m",
blocknum, FilePathName(v->mdfd_vfd))));
- nbytes = FileWrite(v->mdfd_vfd, buffer, BLCKSZ);
+ nbytes = FileWrite(v->mdfd_vfd, buffer, BLCKSZ, flush_to_disk, context);
TRACE_POSTGRESQL_SMGR_MD_WRITE_DONE(forknum, blocknum,
reln->smgr_rnode.node.spcNode,
diff --git a/src/backend/storage/smgr/smgr.c b/src/backend/storage/smgr/smgr.c
index 244b4ea..2db3cd3 100644
--- a/src/backend/storage/smgr/smgr.c
+++ b/src/backend/storage/smgr/smgr.c
@@ -52,7 +52,8 @@ typedef struct f_smgr
void (*smgr_read) (SMgrRelation reln, ForkNumber forknum,
BlockNumber blocknum, char *buffer);
void (*smgr_write) (SMgrRelation reln, ForkNumber forknum,
- BlockNumber blocknum, char *buffer, bool skipFsync);
+ BlockNumber blocknum, char *buffer, bool skipFsync,
+ bool flush_to_disk, FileFlushContext *context);
BlockNumber (*smgr_nblocks) (SMgrRelation reln, ForkNumber forknum);
void (*smgr_truncate) (SMgrRelation reln, ForkNumber forknum,
BlockNumber nblocks);
@@ -643,10 +644,11 @@ smgrread(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
*/
void
smgrwrite(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
- char *buffer, bool skipFsync)
+ char *buffer, bool skipFsync, bool flush_to_disk,
+ FileFlushContext * context)
{
(*(smgrsw[reln->smgr_which].smgr_write)) (reln, forknum, blocknum,
- buffer, skipFsync);
+ buffer, skipFsync, flush_to_disk, context);
}
/*
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index cf1e505..9219330 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -158,6 +158,7 @@ static bool check_bonjour(bool *newval, void **extra, GucSource source);
static bool check_ssl(bool *newval, void **extra, GucSource source);
static bool check_stage_log_stats(bool *newval, void **extra, GucSource source);
static bool check_log_stats(bool *newval, void **extra, GucSource source);
+static bool check_flush_to_disk(bool *newval, void **extra, GucSource source);
static bool check_canonical_path(char **newval, void **extra, GucSource source);
static bool check_timezone_abbreviations(char **newval, void **extra, GucSource source);
static void assign_timezone_abbreviations(const char *newval, void *extra);
@@ -1025,6 +1026,17 @@ static struct config_bool ConfigureNamesBool[] =
},
{
+ {"checkpoint_flush_to_disk", PGC_SIGHUP, WAL_CHECKPOINTS,
+ gettext_noop("Hint that checkpoint's writes are high priority."),
+ NULL
+ },
+ &checkpoint_flush_to_disk,
+ /* see bufmgr.h: true on Linux, false otherwise */
+ DEFAULT_CHECKPOINT_FLUSH_TO_DISK,
+ check_flush_to_disk, NULL, NULL
+ },
+
+ {
{"log_connections", PGC_SU_BACKEND, LOGGING_WHAT,
gettext_noop("Logs each successful connection."),
NULL
@@ -9806,6 +9818,21 @@ check_log_stats(bool *newval, void **extra, GucSource source)
}
static bool
+check_flush_to_disk(bool *newval, void **extra, GucSource source)
+{
+/* This test must be consistent with the one in FileWrite (storage/file/fd.c)
+ */
+#if ! (defined(HAVE_SYNC_FILE_RANGE) || defined(HAVE_POSIX_FADVISE))
+ /* just warn if it has no effect */
+ ereport(WARNING,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("Setting \"checkpoint_flush_to_disk\" has no effect "
+ "on this platform.")));
+#endif /* ! (HAVE_SYNC_FILE_RANGE || HAVE_POSIX_FADVISE) */
+ return true;
+}
+
+static bool
check_canonical_path(char **newval, void **extra, GucSource source)
{
/*
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index e84f380..a5495da 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -202,6 +202,8 @@
#min_wal_size = 80MB
#checkpoint_completion_target = 0.5 # checkpoint target duration, 0.0 - 1.0
#checkpoint_sort = on # sort buffers on checkpoint
+#checkpoint_flush_to_disk = ? # send buffers to disk on checkpoint
+ # default is on if Linux, off otherwise
#checkpoint_warning = 30s # 0 disables
# - Archiving -
diff --git a/src/include/postmaster/bgwriter.h b/src/include/postmaster/bgwriter.h
index a49c208..f9c8ca1 100644
--- a/src/include/postmaster/bgwriter.h
+++ b/src/include/postmaster/bgwriter.h
@@ -16,6 +16,7 @@
#define _BGWRITER_H
#include "storage/block.h"
+#include "storage/fd.h"
#include "storage/relfilenode.h"
@@ -29,7 +30,8 @@ extern void BackgroundWriterMain(void) pg_attribute_noreturn();
extern void CheckpointerMain(void) pg_attribute_noreturn();
extern void RequestCheckpoint(int flags);
-extern void CheckpointWriteDelay(int flags, double progress);
+extern void CheckpointWriteDelay(int flags, double progress,
+ FileFlushContext * context, int ctx_size);
extern bool ForwardFsyncRequest(RelFileNode rnode, ForkNumber forknum,
BlockNumber segno);
diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.h
index c228f39..4fd3ff5 100644
--- a/src/include/storage/bufmgr.h
+++ b/src/include/storage/bufmgr.h
@@ -54,6 +54,14 @@ extern int bgwriter_lru_maxpages;
extern double bgwriter_lru_multiplier;
extern bool track_io_timing;
extern int target_prefetch_pages;
+
+#ifdef HAVE_SYNC_FILE_RANGE
+#define DEFAULT_CHECKPOINT_FLUSH_TO_DISK true
+#else
+#define DEFAULT_CHECKPOINT_FLUSH_TO_DISK false
+#endif /* HAVE_SYNC_FILE_RANGE */
+
+extern bool checkpoint_flush_to_disk;
extern bool checkpoint_sort;
/* in buf_init.c */
diff --git a/src/include/storage/fd.h b/src/include/storage/fd.h
index 7eabe09..c7b2a6d 100644
--- a/src/include/storage/fd.h
+++ b/src/include/storage/fd.h
@@ -59,6 +59,24 @@ extern int max_files_per_process;
*/
extern int max_safe_fds;
+/*
+ * FileFlushContext structure:
+ *
+ * This is used to accumulate several flush requests on a file
+ * into a larger flush request.
+ * - fd: file descriptor of the file
+ * - ncalls: number of flushes merged together
+ * - offset: starting offset (minimum of all offsets)
+ * - nbytes: size (minimum extent to cover all flushed data)
+ * - filename: filename of fd for error messages
+ */
+typedef struct FileFlushContext {
+ int fd;
+ int ncalls;
+ off_t offset;
+ off_t nbytes;
+ char * filename;
+} FileFlushContext;
/*
* prototypes for functions in fd.c
@@ -70,7 +88,12 @@ extern File OpenTemporaryFile(bool interXact);
extern void FileClose(File file);
extern int FilePrefetch(File file, off_t offset, int amount);
extern int FileRead(File file, char *buffer, int amount);
-extern int FileWrite(File file, char *buffer, int amount);
+extern void ResetFileFlushContext(FileFlushContext * context);
+extern void PerformFileFlush(FileFlushContext * context);
+extern void FileAsynchronousFlush(FileFlushContext * context,
+ int fd, off_t offset, off_t nbytes, char * filename);
+extern int FileWrite(File file, char *buffer, int amount, bool flush_to_disk,
+ FileFlushContext * context);
extern int FileSync(File file);
extern off_t FileSeek(File file, off_t offset, int whence);
extern int FileTruncate(File file, off_t offset);
diff --git a/src/include/storage/smgr.h b/src/include/storage/smgr.h
index 69a624f..a46a70c 100644
--- a/src/include/storage/smgr.h
+++ b/src/include/storage/smgr.h
@@ -16,6 +16,7 @@
#include "fmgr.h"
#include "storage/block.h"
+#include "storage/fd.h"
#include "storage/relfilenode.h"
@@ -95,7 +96,8 @@ extern void smgrprefetch(SMgrRelation reln, ForkNumber forknum,
extern void smgrread(SMgrRelation reln, ForkNumber forknum,
BlockNumber blocknum, char *buffer);
extern void smgrwrite(SMgrRelation reln, ForkNumber forknum,
- BlockNumber blocknum, char *buffer, bool skipFsync);
+ BlockNumber blocknum, char *buffer, bool skipFsync,
+ bool flush_to_disk, FileFlushContext * context);
extern BlockNumber smgrnblocks(SMgrRelation reln, ForkNumber forknum);
extern void smgrtruncate(SMgrRelation reln, ForkNumber forknum,
BlockNumber nblocks);
@@ -120,8 +122,9 @@ extern void mdprefetch(SMgrRelation reln, ForkNumber forknum,
BlockNumber blocknum);
extern void mdread(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
char *buffer);
-extern void mdwrite(SMgrRelation reln, ForkNumber forknum,
- BlockNumber blocknum, char *buffer, bool skipFsync);
+extern void mdwrite(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
+ char *buffer, bool skipFsync, bool flush_to_disk,
+ FileFlushContext * context);
extern BlockNumber mdnblocks(SMgrRelation reln, ForkNumber forknum);
extern void mdtruncate(SMgrRelation reln, ForkNumber forknum,
BlockNumber nblocks);
On Tue, Aug 18, 2015 at 12:38 PM, Fabien COELHO <coelho@cri.ensmp.fr> wrote:
Hello Amit,
So the option is best kept as "off" for now, without further data, I'm
fine with that.
One point to think here is on what basis user can decide make
this option on, is it predictable in any way?
I think one case could be when the data set fits in shared_buffers.Yep.
In general, providing an option is a good idea if user can decide with
ease when to use that option or we can give some clear recommendation for
the same otherwise one has to recommend that test your workload with this
option and if it works then great else don't use it which might also be
okay in some cases, but it is better to be clear.My opinion, which is not backed by any data (anyone can feel free to
provide a FreeBSD box for testing...) is that it would mostly be an
improvement if you have a significant write load to have the flush option
on when running on non-Linux systems which provide posix_fadvise.If you have a lot of reads and few writes, then postgresql currently works
reasonably enough, which is why people do not complain too much about write
stalls, and I expect that the situation would not be significantly degraded.Now there are competing positive and negative effects induced by using
posix_fadvise, and moreover its implementation varries from OS to OS, so
without running some experiments it is hard to be definite.
Sure, I think what can help here is a testcase/'s (in form of script file
or some other form, to test this behaviour of patch) which you can write
and post here, so that others can use that to get the data and share it.
Ofcourse, that is not mandatory to proceed with this patch, but still can
help you to prove your point as you might not have access to different
kind of systems to run the tests.
With Regards,
Amit Kapila.
EnterpriseDB: http://www.enterprisedb.com
Sure, I think what can help here is a testcase/'s (in form of script file
or some other form, to test this behaviour of patch) which you can write
and post here, so that others can use that to get the data and share it.
Sure... note that I already did that on this thread, without any echo...
but I can do it again...
Tests should be run on a dedicated host. If it has n cores, I suggest to
share them between postgres checkpointer & workers and pgbench threads so
as to avoid thread competition to use cores. With 8 cores I used up to 2
threads & 4 clients, so that there is 2 core left for the checkpointer and
other stuff (i.e. I also run iotop & htop in parallel...). Although it may
seem conservative to do so, I think that the point of the test is to
exercise checkpoints and not to test the process scheduler of the OS.
Here are the latest version of my test scripts:
(1) cp_test.sh <name> <test>
Run "test" with setup "name". Currently it runs 4000 seconds pgbench with
the 4 possible on/off combinations for sorting & flushing, after some
warmup. The 4000 second is chosen so that there are a few checkpoint
cycles. For larger checkpoint times, I suggest to extend the run time to
see at least 3 checkpoints during the run.
More test settings can be added to the 2 "case"s. Postgres settings,
especially shared_buffers, should be set to a pertinent value wrt the
memory of the test host.
The test run with postgres version found in the PATH, so ensure that the
right version is found!
(2) cp_test_count.py one-test-output.log
For rate limited runs, look at the final figures and compute the number of
late & skipped transactions. This can also be done by hand.
(3) avg.py
For full speed runs, compute stats about per second tps:
sh> grep 'progress:' one-test-output.log | cut -d' ' -f4 | \
./avg.py --limit=10 --length=4000
warning: 633 missing data, extending with zeros
avg over 4000: 199.290575 ± 512.114070 [0.000000, 0.000000, 4.000000, 5.000000, 2280.900000]
percent of values below 10.0: 82.5%
The figures I reported are the 199 (average tps), 512 (standard deviation
on per second figures), 82.5% (percent of time below 10 tps, aka postgres
is basically unresponsive). In brakets, the min q1 median q3 and max tps
seen in the run.
Ofcourse, that is not mandatory to proceed with this patch, but still can
help you to prove your point as you might not have access to different
kind of systems to run the tests.
I agree that more tests would be useful to decide which default value for
the flushing option is the better. For Linux, all tests so far suggest
"on" is the best choice, but for other systems that use posix_fadvise, it
is really an open question.
Another option would be to give me a temporary access for some available
host, I'm used to running these tests...
--
Fabien.
On Wed, Aug 19, 2015 at 12:13 PM, Fabien COELHO <coelho@cri.ensmp.fr> wrote:
Sure, I think what can help here is a testcase/'s (in form of script file
or some other form, to test this behaviour of patch) which you can write
and post here, so that others can use that to get the data and share it.Sure... note that I already did that on this thread, without any echo...
but I can do it again...
Thanks.
I have tried your scripts and found some problem while using avg.py
script.
grep 'progress:' test_medium4_FW_off.out | cut -d' ' -f4 | ./avg.py
--limit=10 --length=300
: No such file or directory
I didn't get chance to poke into avg.py script (the command without
avg.py works fine). Python version on the m/c, I planned to test is
Python 2.7.5.
Today while reading the first patch (checkpoint-continuous-flush-10-a),
I have given some thought to below part of patch which I would like
to share with you.
+static int
+NextBufferToWrite(
+ TableSpaceCheckpointStatus *spcStatus, int nb_spaces,
+ int *pspace, int num_to_write, int num_written)
+{
+ int space = *pspace, buf_id = -1, index;
+
+ /*
+ * Select a tablespace depending on the current overall progress.
+ *
+ * The progress ratio of each unfinished tablespace is compared to
+ * the overall progress ratio to find one with is not in advance
+ * (i.e. overall ratio > tablespace ratio,
+ * i.e. tablespace written/to_write > overall written/to_write
Here, I think above calculation can go for toss if backend or bgwriter
starts writing buffers when checkpoint is in progress. The tablespace
written parameter won't be able to consider the one's written by backends
or bgwriter. Now it may not big thing to worry but I find Heikki's version
worth considering, he has not changed the overall idea of this patch, but
the calculations are somewhat simpler and hence less chance of going
wrong.
With Regards,
Amit Kapila.
EnterpriseDB: http://www.enterprisedb.com
Hello Amit,
I have tried your scripts and found some problem while using avg.py
script.
grep 'progress:' test_medium4_FW_off.out | cut -d' ' -f4 | ./avg.py
--limit=10 --length=300
: No such file or directory
I didn't get chance to poke into avg.py script (the command without
avg.py works fine). Python version on the m/c, I planned to test is
Python 2.7.5.
Strange... What does "/usr/bin/env python" say? Can the script be started
on its own at all? I think that the script should work both with python2
and python3, at least it does on my laptop...
Today while reading the first patch (checkpoint-continuous-flush-10-a),
I have given some thought to below part of patch which I would like
to share with you.+ * Select a tablespace depending on the current overall progress. + * + * The progress ratio of each unfinished tablespace is compared to + * the overall progress ratio to find one with is not in advance + * (i.e. overall ratio > tablespace ratio, + * i.e. tablespace written/to_write > overall written/to_write
Here, I think above calculation can go for toss if backend or bgwriter
starts writing buffers when checkpoint is in progress. The tablespace
written parameter won't be able to consider the one's written by backends
or bgwriter.
Sure... This is *already* the case with the current checkpointer, the
schedule is performed with respect to the initial number of buffers it
think it will have to write, and if someone else writes these buffers then
the schedule is skewed a little bit, or more... I have not changed this
logic, but I extended it to handle several tablespaces.
If this (the checkpointer progress evaluation used for its schedule is
sometimes wrong because of other writes) is proven to be a major
performance issue, then the processes which writes the checkpointed
buffers behind its back should tell the checkpointer about it, probably
with some shared data structure, so that the checkpointer can adapt its
schedule.
This is an independent issue, that may be worth to address some day. My
opinion is that when the bgwriter or backends quick in to write buffers,
they are basically generating random I/Os on HDD and killing tps and
latency, so it is a very bad time anyway, thus I'm not sure that this is
the next problem to address to improve pg performance and responsiveness.
Now it may not big thing to worry but I find Heikki's version worth
considering, he has not changed the overall idea of this patch, but the
calculations are somewhat simpler and hence less chance of going wrong.
I do not think that Heikki version worked wrt to balancing writes over
tablespaces, and I'm not sure it worked at all. However I reused some of
his ideas to simplify and improve the code.
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Sun, Aug 23, 2015 at 12:33 PM, Fabien COELHO <coelho@cri.ensmp.fr> wrote:
Hello Amit,
I have tried your scripts and found some problem while using avg.py
script.
grep 'progress:' test_medium4_FW_off.out | cut -d' ' -f4 | ./avg.py
--limit=10 --length=300
: No such file or directoryI didn't get chance to poke into avg.py script (the command without
avg.py works fine). Python version on the m/c, I planned to test is
Python 2.7.5.Strange... What does "/usr/bin/env python" say?
Python 2.7.5 (default, Apr 9 2015, 11:07:29)
[GCC 4.8.3 20140911 (Red Hat 4.8.3-9)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
Can the script be started on its own at all?
I have tried like below which results in same error, also I tried few
other variations but could not succeed.
./avg.py
: No such file or directory
Here, I think above calculation can go for toss if backend or bgwriter
starts writing buffers when checkpoint is in progress. The tablespace
written parameter won't be able to consider the one's written by backends
or bgwriter.Sure... This is *already* the case with the current checkpointer, the
schedule is performed with respect to the initial number of buffers it
think it will have to write, and if someone else writes these buffers then
the schedule is skewed a little bit, or more... I have not changed this
logic, but I extended it to handle several tablespaces.
I don't know how good or bad it is to build further on somewhat skewed
logic, but the point is that unless it is required why to use it.
I do not think that Heikki version worked wrt to balancing writes over
tablespaces,
I also think that it doesn't balances over tablespaces, but the question
is why do we need to balance over tablespaces, can we reliably
predict in someway which indicates that performing balancing over
tablespace can help the workload. I think here we are doing more
engineering than required for this patch.
and I'm not sure it worked at all.
Okay, his version might have some bugs, but then those could be
fixed as well.
With Regards,
Amit Kapila.
EnterpriseDB: http://www.enterprisedb.com
Hello Amit,
Can the script be started on its own at all?
I have tried like below which results in same error, also I tried few
other variations but could not succeed.
./avg.py
Hmmm... Ensure that the script is readable and executable:
sh> chmod a+rx ./avg.py
Also check the file:
sh> file ./avg.py
./avg.py: Python script, UTF-8 Unicode text executable
Sure... This is *already* the case with the current checkpointer, the
schedule is performed with respect to the initial number of buffers it
think it will have to write, and if someone else writes these buffers then
the schedule is skewed a little bit, or more... I have not changed thisI don't know how good or bad it is to build further on somewhat skewed
logic,
The logic is no more skewed that it is with the current version: your
remark about the estimation which may be wrong in some cases is clearly
valid, but it is orthogonal (independent, unrelated, different) to what is
addressed by this patch.
I currently have no reason to believe that the issue you raise is a major
performance issue, but if so it may be addressed by another patch by
whoever want to do so.
What I have done is to demonstrate that generating a lot of random I/Os is
a major performance issue (well, sure), and this patch addresses this
point and provide major speedup (*3-5) and latency reductions (from +60%
unavailability to nearly full availability) for high OLTP write load, by
reordering and flushing checkpoint buffers in a sensible way.
but the point is that unless it is required why to use it.
This is really required to avoid predictable performance regressions, see
below.
I do not think that Heikki version worked wrt to balancing writes over
tablespaces,I also think that it doesn't balances over tablespaces, but the question
is why do we need to balance over tablespaces, can we reliably predict
in someway which indicates that performing balancing over tablespace can
help the workload.
The reason for the tablespace balancing is that in the current postgres
buffers are written more or less randomly, so it is (probably) implicitely
and statistically balanced over tablespaces because of this randomness,
and indeed, AFAIK, people with multi tablespace setup have not complained
that postgres was using the disks sequentially.
However, once the buffers are sorted per file, the order becomes
deterministic and there is no more implicit balancing, which means that if
someone has a pg setup with several disks it will write sequentially on
these instead of in parallel.
This regression was pointed out by Andres Freund, I agree that such a
regression for high end systems must be avoided, hence the tablespace
balancing.
I think here we are doing more engineering than required for this patch.
I do not think so, I think that Andres remark is justified to avoid a
performance regression on high end systems which use tablespaces, which is
really undesirable.
About the balancing code, it is not that difficult, even if it is not
trivial: the point is to select the tablespace for which the progress
ratio (written/to_write) is below the overall progress ratio, so that it
catches up, and do so in a round robin maner, so that all tablespaces get
to write things. I also have both written a proof and tested the logic (in
a separate script).
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Mon, Aug 24, 2015 at 4:15 PM, Fabien COELHO <coelho@cri.ensmp.fr> wrote:
[stuff]
Moved to next CF 2015-09.
--
Michael
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 2015-08-18 09:08:43 +0200, Fabien COELHO wrote:
Please find attached a v10, where I have reviewed comments for style &
contents, and also slightly extended the documentation about the flush
option to hint that it is essentially useful for high write loads. Without
further data, I think it is not obvious to give more definite advices.
v10b misses the checkpoint_sort part of the patch, and thus cannot be applied.
Andres
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Hello Andres,
Please find attached a v10, where I have reviewed comments for style &
contents, and also slightly extended the documentation about the flush
option to hint that it is essentially useful for high write loads.
Without further data, I think it is not obvious to give more definite
advices.v10b misses the checkpoint_sort part of the patch, and thus cannot be
applied.
Yes, indeed, the second part is expected to be applied on top of v10a.
Please find attached the cumulated version (v10a + v10b).
--
Fabien.
Attachments:
checkpoint-continuous-flush-10.patchtext/x-diff; name=checkpoint-continuous-flush-10.patchDownload
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index e3dc23b..927294b 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -2454,6 +2454,28 @@ include_dir 'conf.d'
</listitem>
</varlistentry>
+ <varlistentry id="guc-checkpoint-sort" xreflabel="checkpoint_sort">
+ <term><varname>checkpoint_sort</varname> (<type>bool</type>)
+ <indexterm>
+ <primary><varname>checkpoint_sort</> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ Whether to sort buffers before writting them out to disk on checkpoint.
+ For a HDD storage, this setting allows to group together
+ neighboring pages written to disk, thus improving performance by
+ reducing random write activity.
+ This sorting should have limited performance effects on SSD backends
+ as such storages have good random write performance, but it may
+ help with wear-leveling so be worth keeping anyway.
+ The default is <literal>on</>.
+ This parameter can only be set in the <filename>postgresql.conf</>
+ file or on the server command line.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="guc-checkpoint-warning" xreflabel="checkpoint_warning">
<term><varname>checkpoint_warning</varname> (<type>integer</type>)
<indexterm>
@@ -2475,6 +2497,24 @@ include_dir 'conf.d'
</listitem>
</varlistentry>
+ <varlistentry id="guc-checkpoint-flush-to-disk" xreflabel="checkpoint_flush_to_disk">
+ <term><varname>checkpoint_flush_to_disk</varname> (<type>bool</type>)
+ <indexterm>
+ <primary><varname>checkpoint_flush_to_disk</> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ When writing data for a checkpoint, hint the underlying OS that the
+ data must be sent to disk as soon as possible. This may help smoothing
+ disk I/O writes and avoid a stall when fsync is issued at the end of
+ the checkpoint, but it may also reduce average performance.
+ This setting may have no effect on some platforms.
+ The default is <literal>on</> on Linux, <literal>off</> otherwise.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="guc-min-wal-size" xreflabel="min_wal_size">
<term><varname>min_wal_size</varname> (<type>integer</type>)
<indexterm>
diff --git a/doc/src/sgml/wal.sgml b/doc/src/sgml/wal.sgml
index e3941c9..1b658f2 100644
--- a/doc/src/sgml/wal.sgml
+++ b/doc/src/sgml/wal.sgml
@@ -546,6 +546,30 @@
</para>
<para>
+ When hard-disk drives (HDD) are used for terminal data storage
+ <xref linkend="guc-checkpoint-sort"> allows to sort pages
+ so that neighboring pages on disk will be flushed together by
+ chekpoints, reducing the random write load and improving performance.
+ If solid-state drives (SSD) are used, sorting pages induces no benefit
+ as their random write I/O performance is good: this feature could then
+ be disabled by setting <varname>checkpoint_sort</> to <value>off</>.
+ It is possible that sorting may help with SSD wear leveling, so it may
+ be kept on that account.
+ </para>
+
+ <para>
+ On Linux and POSIX platforms, <xref linkend="guc-checkpoint-flush-to-disk">
+ allows to hint the OS that pages written on checkpoints must be flushed
+ to disk quickly. Otherwise, these pages may be kept in cache for some time,
+ inducing a stall later when <literal>fsync</> is called to actually
+ complete the checkpoint. This setting helps to reduce transaction latency,
+ but it may also have a small adverse effect on the average transaction rate
+ at maximum throughput on some OS. It should be beneficial for high write
+ loads on HDD. This feature probably brings no benefit on SSD, as the I/O
+ write latency is small on such hardware, thus it may be disabled.
+ </para>
+
+ <para>
The number of WAL segment files in <filename>pg_xlog</> directory depends on
<varname>min_wal_size</>, <varname>max_wal_size</> and
the amount of WAL generated in previous checkpoint cycles. When old log
diff --git a/src/backend/access/heap/rewriteheap.c b/src/backend/access/heap/rewriteheap.c
index bcce3e3..f565dc4 100644
--- a/src/backend/access/heap/rewriteheap.c
+++ b/src/backend/access/heap/rewriteheap.c
@@ -918,7 +918,7 @@ logical_heap_rewrite_flush_mappings(RewriteState state)
* Note that we deviate from the usual WAL coding practices here,
* check the above "Logical rewrite support" comment for reasoning.
*/
- written = FileWrite(src->vfd, waldata_start, len);
+ written = FileWrite(src->vfd, waldata_start, len, false, NULL);
if (written != len)
ereport(ERROR,
(errcode_for_file_access(),
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index cf4a6dc..4b5e9cd 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -203,7 +203,7 @@ btbuildempty(PG_FUNCTION_ARGS)
/* Write the page. If archiving/streaming, XLOG it. */
PageSetChecksumInplace(metapage, BTREE_METAPAGE);
smgrwrite(index->rd_smgr, INIT_FORKNUM, BTREE_METAPAGE,
- (char *) metapage, true);
+ (char *) metapage, true, false, NULL);
if (XLogIsNeeded())
log_newpage(&index->rd_smgr->smgr_rnode.node, INIT_FORKNUM,
BTREE_METAPAGE, metapage, false);
diff --git a/src/backend/access/nbtree/nbtsort.c b/src/backend/access/nbtree/nbtsort.c
index f95f67a..ea7a45d 100644
--- a/src/backend/access/nbtree/nbtsort.c
+++ b/src/backend/access/nbtree/nbtsort.c
@@ -315,7 +315,7 @@ _bt_blwritepage(BTWriteState *wstate, Page page, BlockNumber blkno)
{
/* overwriting a block we zero-filled before */
smgrwrite(wstate->index->rd_smgr, MAIN_FORKNUM, blkno,
- (char *) page, true);
+ (char *) page, true, false, NULL);
}
pfree(page);
diff --git a/src/backend/access/spgist/spginsert.c b/src/backend/access/spgist/spginsert.c
index bceee8d..b700efb 100644
--- a/src/backend/access/spgist/spginsert.c
+++ b/src/backend/access/spgist/spginsert.c
@@ -170,7 +170,7 @@ spgbuildempty(PG_FUNCTION_ARGS)
/* Write the page. If archiving/streaming, XLOG it. */
PageSetChecksumInplace(page, SPGIST_METAPAGE_BLKNO);
smgrwrite(index->rd_smgr, INIT_FORKNUM, SPGIST_METAPAGE_BLKNO,
- (char *) page, true);
+ (char *) page, true, false, NULL);
if (XLogIsNeeded())
log_newpage(&index->rd_smgr->smgr_rnode.node, INIT_FORKNUM,
SPGIST_METAPAGE_BLKNO, page, false);
@@ -180,7 +180,7 @@ spgbuildempty(PG_FUNCTION_ARGS)
PageSetChecksumInplace(page, SPGIST_ROOT_BLKNO);
smgrwrite(index->rd_smgr, INIT_FORKNUM, SPGIST_ROOT_BLKNO,
- (char *) page, true);
+ (char *) page, true, false, NULL);
if (XLogIsNeeded())
log_newpage(&index->rd_smgr->smgr_rnode.node, INIT_FORKNUM,
SPGIST_ROOT_BLKNO, page, true);
@@ -190,7 +190,7 @@ spgbuildempty(PG_FUNCTION_ARGS)
PageSetChecksumInplace(page, SPGIST_NULL_BLKNO);
smgrwrite(index->rd_smgr, INIT_FORKNUM, SPGIST_NULL_BLKNO,
- (char *) page, true);
+ (char *) page, true, false, NULL);
if (XLogIsNeeded())
log_newpage(&index->rd_smgr->smgr_rnode.node, INIT_FORKNUM,
SPGIST_NULL_BLKNO, page, true);
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 68e33eb..bee38ab 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -7995,11 +7995,13 @@ LogCheckpointEnd(bool restartpoint)
sync_secs,
total_secs,
longest_secs,
+ sort_secs,
average_secs;
int write_usecs,
sync_usecs,
total_usecs,
longest_usecs,
+ sort_usecs,
average_usecs;
uint64 average_sync_time;
@@ -8030,6 +8032,10 @@ LogCheckpointEnd(bool restartpoint)
CheckpointStats.ckpt_end_t,
&total_secs, &total_usecs);
+ TimestampDifference(CheckpointStats.ckpt_sort_t,
+ CheckpointStats.ckpt_sort_end_t,
+ &sort_secs, &sort_usecs);
+
/*
* Timing values returned from CheckpointStats are in microseconds.
* Convert to the second plus microsecond form that TimestampDifference
@@ -8048,8 +8054,8 @@ LogCheckpointEnd(bool restartpoint)
elog(LOG, "%s complete: wrote %d buffers (%.1f%%); "
"%d transaction log file(s) added, %d removed, %d recycled; "
- "write=%ld.%03d s, sync=%ld.%03d s, total=%ld.%03d s; "
- "sync files=%d, longest=%ld.%03d s, average=%ld.%03d s; "
+ "sort=%ld.%03d s, write=%ld.%03d s, sync=%ld.%03d s, total=%ld.%03d s;"
+ " sync files=%d, longest=%ld.%03d s, average=%ld.%03d s; "
"distance=%d kB, estimate=%d kB",
restartpoint ? "restartpoint" : "checkpoint",
CheckpointStats.ckpt_bufs_written,
@@ -8057,6 +8063,7 @@ LogCheckpointEnd(bool restartpoint)
CheckpointStats.ckpt_segs_added,
CheckpointStats.ckpt_segs_removed,
CheckpointStats.ckpt_segs_recycled,
+ sort_secs, sort_usecs / 1000,
write_secs, write_usecs / 1000,
sync_secs, sync_usecs / 1000,
total_secs, total_usecs / 1000,
diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c
index 3b3a09e..e361907 100644
--- a/src/backend/postmaster/checkpointer.c
+++ b/src/backend/postmaster/checkpointer.c
@@ -665,7 +665,8 @@ ImmediateCheckpointRequested(void)
* fraction between 0.0 meaning none, and 1.0 meaning all done.
*/
void
-CheckpointWriteDelay(int flags, double progress)
+CheckpointWriteDelay(int flags, double progress,
+ FileFlushContext * context, int ctx_size)
{
static int absorb_counter = WRITES_PER_ABSORB;
@@ -700,6 +701,26 @@ CheckpointWriteDelay(int flags, double progress)
*/
pgstat_send_bgwriter();
+#if defined(HAVE_SYNC_FILE_RANGE) || defined(HAVE_POSIX_FADVISE)
+
+ /*
+ * Before sleeping, flush written blocks for each tablespace.
+ */
+ if (checkpoint_flush_to_disk)
+ {
+ int i;
+
+ for (i = 0; i < ctx_size; i++)
+ {
+ if (context[i].ncalls != 0)
+ {
+ PerformFileFlush(&context[i]);
+ ResetFileFlushContext(&context[i]);
+ }
+ }
+ }
+#endif /* HAVE_SYNC_FILE_RANGE || HAVE_POSIX_FADVISE */
+
/*
* This sleep used to be connected to bgwriter_delay, typically 200ms.
* That resulted in more frequent wakeups if not much work to do.
diff --git a/src/backend/storage/buffer/buf_init.c b/src/backend/storage/buffer/buf_init.c
index 3ae2848..3bd5eab 100644
--- a/src/backend/storage/buffer/buf_init.c
+++ b/src/backend/storage/buffer/buf_init.c
@@ -65,7 +65,8 @@ void
InitBufferPool(void)
{
bool foundBufs,
- foundDescs;
+ foundDescs,
+ foundCpid;
/* Align descriptors to a cacheline boundary. */
BufferDescriptors = (BufferDescPadded *) CACHELINEALIGN(
@@ -77,10 +78,14 @@ InitBufferPool(void)
ShmemInitStruct("Buffer Blocks",
NBuffers * (Size) BLCKSZ, &foundBufs);
- if (foundDescs || foundBufs)
+ CheckpointBufferIds = (CheckpointSortItem *)
+ ShmemInitStruct("Checkpoint BufferIds",
+ NBuffers * sizeof(CheckpointSortItem), &foundCpid);
+
+ if (foundDescs || foundBufs || foundCpid)
{
- /* both should be present or neither */
- Assert(foundDescs && foundBufs);
+ /* all should be present or neither */
+ Assert(foundDescs && foundBufs && foundCpid);
/* note: this path is only taken in EXEC_BACKEND case */
}
else
@@ -144,5 +149,8 @@ BufferShmemSize(void)
/* size of stuff controlled by freelist.c */
size = add_size(size, StrategyShmemSize());
+ /* size of checkpoint sort array in bufmgr.c */
+ size = add_size(size, mul_size(NBuffers, sizeof(CheckpointSortItem)));
+
return size;
}
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index cd3aaad..436ead2 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -80,6 +80,9 @@ bool zero_damaged_pages = false;
int bgwriter_lru_maxpages = 100;
double bgwriter_lru_multiplier = 2.0;
bool track_io_timing = false;
+/* hint to move writes to high priority */
+bool checkpoint_flush_to_disk = DEFAULT_CHECKPOINT_FLUSH_TO_DISK;
+bool checkpoint_sort = true;
/*
* How many buffers PrefetchBuffer callers should try to stay ahead of their
@@ -95,6 +98,9 @@ static bool IsForInput;
/* local state for LockBufferForCleanup */
static volatile BufferDesc *PinCountWaitBuf = NULL;
+/* array of buffer ids & sort criterion of all buffers to checkpoint */
+CheckpointSortItem *CheckpointBufferIds = NULL;
+
/*
* Backend-Private refcount management:
*
@@ -396,7 +402,8 @@ static bool PinBuffer(volatile BufferDesc *buf, BufferAccessStrategy strategy);
static void PinBuffer_Locked(volatile BufferDesc *buf);
static void UnpinBuffer(volatile BufferDesc *buf, bool fixOwner);
static void BufferSync(int flags);
-static int SyncOneBuffer(int buf_id, bool skip_recently_used);
+static int SyncOneBuffer(int buf_id, bool skip_recently_used,
+ bool flush_to_disk, FileFlushContext *context);
static void WaitIO(volatile BufferDesc *buf);
static bool StartBufferIO(volatile BufferDesc *buf, bool forInput);
static void TerminateBufferIO(volatile BufferDesc *buf, bool clear_dirty,
@@ -409,7 +416,8 @@ static volatile BufferDesc *BufferAlloc(SMgrRelation smgr,
BlockNumber blockNum,
BufferAccessStrategy strategy,
bool *foundPtr);
-static void FlushBuffer(volatile BufferDesc *buf, SMgrRelation reln);
+static void FlushBuffer(volatile BufferDesc *buf, SMgrRelation reln,
+ bool flush_to_disk, FileFlushContext *context);
static void AtProcExit_Buffers(int code, Datum arg);
static void CheckForBufferLeaks(void);
static int rnode_comparator(const void *p1, const void *p2);
@@ -1018,7 +1026,7 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
smgr->smgr_rnode.node.dbNode,
smgr->smgr_rnode.node.relNode);
- FlushBuffer(buf, NULL);
+ FlushBuffer(buf, NULL, false, NULL);
LWLockRelease(buf->content_lock);
TRACE_POSTGRESQL_BUFFER_WRITE_DIRTY_DONE(forkNum, blockNum,
@@ -1561,6 +1569,130 @@ UnpinBuffer(volatile BufferDesc *buf, bool fixOwner)
}
}
+/* checkpoint buffers comparison */
+static int bufcmp(const void * pa, const void * pb)
+{
+ CheckpointSortItem
+ *a = (CheckpointSortItem *) pa,
+ *b = (CheckpointSortItem *) pb;
+
+ /* compare relation */
+ if (a->relNode < b->relNode)
+ return -1;
+ else if (a->relNode > b->relNode)
+ return 1;
+ /* same relation, compare fork */
+ else if (a->forkNum < b->forkNum)
+ return -1;
+ else if (a->forkNum > b->forkNum)
+ return 1;
+ /* same relation/fork, so same segmented "file", compare block number
+ * which are mapped on different segments depending on the number.
+ */
+ else if (a->blockNum < b->blockNum)
+ return -1;
+ else /* should not be the same block anyway... */
+ return 1;
+}
+
+/*
+ * Status of buffers to checkpoint for a particular tablespace,
+ * used internally in BufferSync.
+ * - space: oid of the tablespace
+ * - num_to_write: number of checkpoint pages counted for this tablespace
+ * - num_written: number of pages actually written out
+ * - index: scanning position in CheckpointBufferIds for this tablespace
+ */
+typedef struct TableSpaceCheckpointStatus {
+ Oid space;
+ int num_to_write;
+ int num_written;
+ int index;
+} TableSpaceCheckpointStatus;
+
+/*
+ * Entry structure for table space to count hashtable,
+ * used internally in BufferSync.
+ */
+typedef struct TableSpaceCountEntry {
+ Oid space;
+ int count;
+} TableSpaceCountEntry;
+
+/*
+ * Return the next buffer to write, or -1.
+ * this function balances buffers over tablespaces, see comment inside.
+ */
+static int
+NextBufferToWrite(
+ TableSpaceCheckpointStatus *spcStatus, int nb_spaces,
+ int *pspace, int num_to_write, int num_written)
+{
+ int space = *pspace, buf_id = -1, index;
+
+ /*
+ * Select a tablespace depending on the current overall progress.
+ *
+ * The progress ratio of each unfinished tablespace is compared to
+ * the overall progress ratio to find one with is not in advance
+ * (i.e. overall ratio > tablespace ratio,
+ * i.e. tablespace written/to_write > overall written/to_write
+ *
+ * Existence: it is bound to exist otherwise the overall progress
+ * ratio would be inconsistent: with positive buffers to write (t1 & t2)
+ * and already written buffers (w1 & w2), we have:
+ *
+ * If w1/t1 > (w1+w2)/(t1+t2) # one table space is in advance
+ * => w1t1+w1t2 > w1t1+w2t1 => w1t2 > w2t1 => w1t2+w2t2 > w2t1+w2t2
+ * => (w1+w2) / (t1+t2) > w2 / t2 # the other one is late
+ *
+ * The round robin ensures that each space is given some attention
+ * till it is over the current ratio, before going to the next.
+ *
+ * Precision: using int32 computations for comparing fractions
+ * (w1 / t1 > w / t <=> w1 t > w t1) seems a bad idea as the values
+ * can overflow 32-bit integers: the limit would be sqrt(2**31) ~
+ * 46340 buffers, i.e. a 362 MB checkpoint. So ensure that 64-bit
+ * integers are used in the comparison.
+ */
+ while ((int64) spcStatus[space].num_written * num_to_write >
+ (int64) num_written * spcStatus[space].num_to_write)
+ space = (space + 1) % nb_spaces; /* round robin */
+
+ /*
+ * Find a valid buffer in the selected tablespace,
+ * by continuing the tablespace specific buffer scan
+ * where it was left.
+ */
+ index = spcStatus[space].index;
+
+ while (index < num_to_write && buf_id == -1)
+ {
+ volatile BufferDesc *bufHdr;
+
+ buf_id = CheckpointBufferIds[index].buf_id;
+ bufHdr = GetBufferDescriptor(buf_id);
+
+ /*
+ * Skip if in another tablespace or not in checkpoint anymore.
+ * No lock is acquired, see comments below.
+ */
+ if (spcStatus[space].space != bufHdr->tag.rnode.spcNode ||
+ ! (bufHdr->flags & BM_CHECKPOINT_NEEDED))
+ {
+ index ++;
+ buf_id = -1;
+ }
+ }
+
+ /* update tablespace writing status, will start over at next index */
+ spcStatus[space].index = index + 1;
+
+ *pspace = space;
+
+ return buf_id;
+}
+
/*
* BufferSync -- Write out all dirty buffers in the pool.
*
@@ -1574,11 +1706,14 @@ UnpinBuffer(volatile BufferDesc *buf, bool fixOwner)
static void
BufferSync(int flags)
{
- int buf_id;
- int num_to_scan;
+ int buf_id = -1;
int num_to_write;
int num_written;
int mask = BM_DIRTY;
+ HTAB *spcBuffers;
+ TableSpaceCheckpointStatus *spcStatus = NULL;
+ int nb_spaces, space;
+ FileFlushContext * spcContext = NULL;
/* Make sure we can handle the pin inside SyncOneBuffer */
ResourceOwnerEnlargeBuffers(CurrentResourceOwner);
@@ -1609,6 +1744,18 @@ BufferSync(int flags)
* certainly need to be written for the next checkpoint attempt, too.
*/
num_to_write = 0;
+
+ /* initialize oid -> int buffer count hash table */
+ {
+ HASHCTL ctl;
+
+ MemSet(&ctl, 0, sizeof(HASHCTL));
+ ctl.keysize = sizeof(Oid);
+ ctl.entrysize = sizeof(TableSpaceCountEntry);
+ spcBuffers = hash_create("Number of buffers to write per tablespace",
+ 16, &ctl, HASH_ELEM | HASH_BLOBS);
+ }
+
for (buf_id = 0; buf_id < NBuffers; buf_id++)
{
volatile BufferDesc *bufHdr = GetBufferDescriptor(buf_id);
@@ -1621,32 +1768,105 @@ BufferSync(int flags)
if ((bufHdr->flags & mask) == mask)
{
+ Oid spc;
+ TableSpaceCountEntry * entry;
+ bool found;
+
bufHdr->flags |= BM_CHECKPOINT_NEEDED;
+ CheckpointBufferIds[num_to_write].buf_id = buf_id;
+ CheckpointBufferIds[num_to_write].relNode = bufHdr->tag.rnode.relNode;
+ CheckpointBufferIds[num_to_write].forkNum = bufHdr->tag.forkNum;
+ CheckpointBufferIds[num_to_write].blockNum = bufHdr->tag.blockNum;
num_to_write++;
+
+ /* keep track of per tablespace buffers */
+ spc = bufHdr->tag.rnode.spcNode;
+ entry = (TableSpaceCountEntry *)
+ hash_search(spcBuffers, (void *) &spc, HASH_ENTER, &found);
+
+ if (found) entry->count++;
+ else entry->count = 1;
}
UnlockBufHdr(bufHdr);
}
if (num_to_write == 0)
+ {
+ hash_destroy(spcBuffers);
return; /* nothing to do */
+ }
TRACE_POSTGRESQL_BUFFER_SYNC_START(NBuffers, num_to_write);
+ /* Build checkpoint tablespace buffer status & flush context arrays */
+ nb_spaces = hash_get_num_entries(spcBuffers);
+ spcStatus = (TableSpaceCheckpointStatus *)
+ palloc(sizeof(TableSpaceCheckpointStatus) * nb_spaces);
+ spcContext = (FileFlushContext *)
+ palloc(sizeof(FileFlushContext) * nb_spaces);
+
+ {
+ int index = 0;
+ HASH_SEQ_STATUS hseq;
+ TableSpaceCountEntry * entry;
+
+ hash_seq_init(&hseq, spcBuffers);
+ while ((entry = (TableSpaceCountEntry *) hash_seq_search(&hseq)))
+ {
+ Assert(index < nb_spaces);
+ spcStatus[index].space = entry->space;
+ spcStatus[index].num_to_write = entry->count;
+ spcStatus[index].num_written = 0;
+ /* should it be randomized? chosen with some criterion? */
+ spcStatus[index].index = 0;
+
+#if defined(HAVE_SYNC_FILE_RANGE) || defined(HAVE_POSIX_FADVISE)
+
+ ResetFileFlushContext(&spcContext[index]);
+
+#endif /* HAVE_SYNC_FILE_RANGE || HAVE_POSIX_FADVISE */
+
+ index ++;
+ }
+ }
+
+ hash_destroy(spcBuffers);
+ spcBuffers = NULL;
+
+ /* sort buffer ids to help find sequential writes */
+ CheckpointStats.ckpt_sort_t = GetCurrentTimestamp();
+
+ if (checkpoint_sort)
+ {
+ qsort(CheckpointBufferIds, num_to_write, sizeof(CheckpointSortItem),
+ bufcmp);
+ }
+
+ CheckpointStats.ckpt_sort_end_t = GetCurrentTimestamp();
+
/*
- * Loop over all buffers again, and write the ones (still) marked with
- * BM_CHECKPOINT_NEEDED. In this loop, we start at the clock sweep point
- * since we might as well dump soon-to-be-recycled buffers first.
+ * Loop over buffers to write through CheckpointBufferIds,
+ * and write the ones (still) marked with BM_CHECKPOINT_NEEDED,
+ * with some round robin over table spaces so as to balance writes,
+ * so that buffer writes move forward roughly proportionally for each
+ * tablespace.
*
- * Note that we don't read the buffer alloc count here --- that should be
- * left untouched till the next BgBufferSync() call.
+ * Termination: if a tablespace is selected by the inner while loop
+ * (see argument there), its index is incremented and will eventually
+ * reach num_to_write, mark this table space scanning as done and
+ * decrement the number of (active) spaces, which will thus reach 0.
*/
- buf_id = StrategySyncStart(NULL, NULL);
- num_to_scan = NBuffers;
+ space = 0;
num_written = 0;
- while (num_to_scan-- > 0)
+
+ while (nb_spaces != 0)
{
- volatile BufferDesc *bufHdr = GetBufferDescriptor(buf_id);
+ volatile BufferDesc *bufHdr = NULL;
+ buf_id = NextBufferToWrite(spcStatus, nb_spaces, &space,
+ num_to_write, num_written);
+ if (buf_id != -1)
+ bufHdr = GetBufferDescriptor(buf_id);
/*
* We don't need to acquire the lock here, because we're only looking
@@ -1660,39 +1880,57 @@ BufferSync(int flags)
* write the buffer though we didn't need to. It doesn't seem worth
* guarding against this, though.
*/
- if (bufHdr->flags & BM_CHECKPOINT_NEEDED)
+ if (bufHdr != NULL && bufHdr->flags & BM_CHECKPOINT_NEEDED)
{
- if (SyncOneBuffer(buf_id, false) & BUF_WRITTEN)
+ if (SyncOneBuffer(buf_id, false, checkpoint_flush_to_disk,
+ &spcContext[space]) & BUF_WRITTEN)
{
TRACE_POSTGRESQL_BUFFER_SYNC_WRITTEN(buf_id);
BgWriterStats.m_buf_written_checkpoints++;
+ spcStatus[space].num_written++;
num_written++;
/*
- * We know there are at most num_to_write buffers with
- * BM_CHECKPOINT_NEEDED set; so we can stop scanning if
- * num_written reaches num_to_write.
- *
- * Note that num_written doesn't include buffers written by
- * other backends, or by the bgwriter cleaning scan. That
- * means that the estimate of how much progress we've made is
- * conservative, and also that this test will often fail to
- * trigger. But it seems worth making anyway.
- */
- if (num_written >= num_to_write)
- break;
-
- /*
* Sleep to throttle our I/O rate.
*/
- CheckpointWriteDelay(flags, (double) num_written / num_to_write);
+ CheckpointWriteDelay(flags, (double) num_written / num_to_write,
+ spcContext, nb_spaces);
}
}
- if (++buf_id >= NBuffers)
- buf_id = 0;
+ /*
+ * Detect checkpoint end for a tablespace: either the scan is done
+ * or all tablespace buffers have been written out. If so, the
+ * another active tablespace status is moved in place of the current
+ * one and the next round will start on this one, or maybe round about.
+ *
+ * Note: maybe an exchange could be made instead in order to keep
+ * informations about the closed table space, but this is currently
+ * not used afterwards.
+ */
+ if (spcStatus[space].index >= num_to_write ||
+ spcStatus[space].num_written >= spcStatus[space].num_to_write)
+ {
+#if defined(HAVE_SYNC_FILE_RANGE) || defined(HAVE_POSIX_FADVISE)
+
+ PerformFileFlush(&spcContext[space]);
+ ResetFileFlushContext(&spcContext[space]);
+
+#endif /* HAVE_SYNC_FILE_RANGE || HAVE_POSIX_FADVISE */
+
+ nb_spaces--;
+ if (space != nb_spaces)
+ spcStatus[space] = spcStatus[nb_spaces];
+ else
+ space = 0;
+ }
}
+ pfree(spcStatus);
+ spcStatus = NULL;
+ pfree(spcContext);
+ spcContext = NULL;
+
/*
* Update checkpoint statistics. As noted above, this doesn't include
* buffers written by other backends or bgwriter scan.
@@ -1939,7 +2177,8 @@ BgBufferSync(void)
/* Execute the LRU scan */
while (num_to_scan > 0 && reusable_buffers < upcoming_alloc_est)
{
- int buffer_state = SyncOneBuffer(next_to_clean, true);
+ int buffer_state =
+ SyncOneBuffer(next_to_clean, true, false, NULL);
if (++next_to_clean >= NBuffers)
{
@@ -2016,7 +2255,8 @@ BgBufferSync(void)
* Note: caller must have done ResourceOwnerEnlargeBuffers.
*/
static int
-SyncOneBuffer(int buf_id, bool skip_recently_used)
+SyncOneBuffer(int buf_id, bool skip_recently_used, bool flush_to_disk,
+ FileFlushContext * context)
{
volatile BufferDesc *bufHdr = GetBufferDescriptor(buf_id);
int result = 0;
@@ -2057,7 +2297,7 @@ SyncOneBuffer(int buf_id, bool skip_recently_used)
PinBuffer_Locked(bufHdr);
LWLockAcquire(bufHdr->content_lock, LW_SHARED);
- FlushBuffer(bufHdr, NULL);
+ FlushBuffer(bufHdr, NULL, flush_to_disk, context);
LWLockRelease(bufHdr->content_lock);
UnpinBuffer(bufHdr, true);
@@ -2319,9 +2559,16 @@ BufferGetTag(Buffer buffer, RelFileNode *rnode, ForkNumber *forknum,
*
* If the caller has an smgr reference for the buffer's relation, pass it
* as the second parameter. If not, pass NULL.
+ *
+ * The third parameter tries to hint the OS that a high priority write is meant,
+ * possibly because io-throttling is already managed elsewhere.
+ * The last parameter holds the current flush context that accumulates flush
+ * requests to be performed in one call, instead of being performed on a buffer
+ * per buffer basis.
*/
static void
-FlushBuffer(volatile BufferDesc *buf, SMgrRelation reln)
+FlushBuffer(volatile BufferDesc *buf, SMgrRelation reln, bool flush_to_disk,
+ FileFlushContext * context)
{
XLogRecPtr recptr;
ErrorContextCallback errcallback;
@@ -2410,7 +2657,9 @@ FlushBuffer(volatile BufferDesc *buf, SMgrRelation reln)
buf->tag.forkNum,
buf->tag.blockNum,
bufToWrite,
- false);
+ false,
+ flush_to_disk,
+ context);
if (track_io_timing)
{
@@ -2832,7 +3081,9 @@ FlushRelationBuffers(Relation rel)
bufHdr->tag.forkNum,
bufHdr->tag.blockNum,
localpage,
- false);
+ false,
+ false,
+ NULL);
bufHdr->flags &= ~(BM_DIRTY | BM_JUST_DIRTIED);
@@ -2866,7 +3117,7 @@ FlushRelationBuffers(Relation rel)
{
PinBuffer_Locked(bufHdr);
LWLockAcquire(bufHdr->content_lock, LW_SHARED);
- FlushBuffer(bufHdr, rel->rd_smgr);
+ FlushBuffer(bufHdr, rel->rd_smgr, false, NULL);
LWLockRelease(bufHdr->content_lock);
UnpinBuffer(bufHdr, true);
}
@@ -2918,7 +3169,7 @@ FlushDatabaseBuffers(Oid dbid)
{
PinBuffer_Locked(bufHdr);
LWLockAcquire(bufHdr->content_lock, LW_SHARED);
- FlushBuffer(bufHdr, NULL);
+ FlushBuffer(bufHdr, NULL, false, NULL);
LWLockRelease(bufHdr->content_lock);
UnpinBuffer(bufHdr, true);
}
diff --git a/src/backend/storage/buffer/localbuf.c b/src/backend/storage/buffer/localbuf.c
index 3144afe..114a0a6 100644
--- a/src/backend/storage/buffer/localbuf.c
+++ b/src/backend/storage/buffer/localbuf.c
@@ -208,7 +208,9 @@ LocalBufferAlloc(SMgrRelation smgr, ForkNumber forkNum, BlockNumber blockNum,
bufHdr->tag.forkNum,
bufHdr->tag.blockNum,
localpage,
- false);
+ false,
+ false,
+ NULL);
/* Mark not-dirty now in case we error out below */
bufHdr->flags &= ~BM_DIRTY;
diff --git a/src/backend/storage/file/buffile.c b/src/backend/storage/file/buffile.c
index ea4d689..fb3b383 100644
--- a/src/backend/storage/file/buffile.c
+++ b/src/backend/storage/file/buffile.c
@@ -317,7 +317,7 @@ BufFileDumpBuffer(BufFile *file)
return; /* seek failed, give up */
file->offsets[file->curFile] = file->curOffset;
}
- bytestowrite = FileWrite(thisfile, file->buffer + wpos, bytestowrite);
+ bytestowrite = FileWrite(thisfile, file->buffer + wpos, bytestowrite, false, NULL);
if (bytestowrite <= 0)
return; /* failed to write */
file->offsets[file->curFile] += bytestowrite;
diff --git a/src/backend/storage/file/fd.c b/src/backend/storage/file/fd.c
index 1ba4946..e880a9e 100644
--- a/src/backend/storage/file/fd.c
+++ b/src/backend/storage/file/fd.c
@@ -1344,8 +1344,97 @@ retry:
return returnCode;
}
+#if defined(HAVE_SYNC_FILE_RANGE) || defined(HAVE_POSIX_FADVISE)
+
+void
+ResetFileFlushContext(FileFlushContext * context)
+{
+ context->fd = 0;
+ context->ncalls = 0;
+ context->offset = 0;
+ context->nbytes = 0;
+ context->filename = NULL;
+}
+
+void
+PerformFileFlush(FileFlushContext * context)
+{
+ if (context->ncalls != 0)
+ {
+ int rc;
+
+#if defined(HAVE_SYNC_FILE_RANGE)
+
+ /*
+ * Linux: tell the memory manager to move these blocks to io so
+ * that they are considered for being actually written to disk.
+ */
+ rc = sync_file_range(context->fd, context->offset, context->nbytes,
+ SYNC_FILE_RANGE_WRITE);
+
+#elif defined(HAVE_POSIX_FADVISE)
+
+ /*
+ * Others: say that data should not be kept in memory...
+ * This is not exactly what we want to say, because we want to write
+ * the data for durability but we may need it later nevertheless.
+ * It seems that Linux would free the memory *if* the data has
+ * already been written do disk, else the "dontneed" call is ignored.
+ * For FreeBSD this may have the desired effect of moving the
+ * data to the io layer, although the system does not seem to
+ * take into account the provided offset & size, so it is rather
+ * rough...
+ */
+ rc = posix_fadvise(context->fd, context->offset, context->nbytes,
+ POSIX_FADV_DONTNEED);
+
+#endif
+
+ if (rc < 0)
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not flush block " INT64_FORMAT
+ " on " INT64_FORMAT " blocks in file \"%s\": %m",
+ context->offset / BLCKSZ,
+ context->nbytes / BLCKSZ,
+ context->filename)));
+ }
+}
+
+void
+FileAsynchronousFlush(FileFlushContext * context,
+ int fd, off_t offset, off_t nbytes, char * filename)
+{
+ if (context->ncalls != 0 && context->fd == fd)
+ {
+ /* same file: merge current flush with previous ones */
+ off_t new_offset = offset < context->offset? offset: context->offset;
+
+ context->nbytes =
+ (context->offset + context->nbytes > offset + nbytes ?
+ context->offset + context->nbytes : offset + nbytes) -
+ new_offset;
+ context->offset = new_offset;
+ context->ncalls ++;
+ }
+ else
+ {
+ /* other file: do flush previous file & reset flush accumulator */
+ PerformFileFlush(context);
+
+ context->fd = fd;
+ context->ncalls = 1;
+ context->offset = offset;
+ context->nbytes = nbytes;
+ context->filename = filename;
+ }
+}
+
+#endif /* HAVE_SYNC_FILE_RANGE || HAVE_POSIX_FADVISE */
+
int
-FileWrite(File file, char *buffer, int amount)
+FileWrite(File file, char *buffer, int amount, bool flush_to_disk,
+ FileFlushContext * context)
{
int returnCode;
@@ -1395,6 +1484,28 @@ retry:
if (returnCode >= 0)
{
+
+#if defined(HAVE_SYNC_FILE_RANGE) || defined(HAVE_POSIX_FADVISE)
+
+ /*
+ * Calling "write" tells the OS that pg wants to write some page to disk,
+ * however when it is really done is chosen by the OS.
+ * Depending on other disk activities this may be delayed significantly,
+ * maybe up to an "fsync" call, which could induce an IO write surge.
+ * When checkpointing pg is doing its own throttling and the result
+ * should really be written to disk with high priority, so as to meet
+ * the completion target.
+ * This call hints that such write have a higher priority.
+ */
+ if (flush_to_disk && returnCode == amount && errno == 0)
+ {
+ FileAsynchronousFlush(context,
+ VfdCache[file].fd, VfdCache[file].seekPos,
+ amount, VfdCache[file].fileName);
+ }
+
+#endif /* HAVE_SYNC_FILE_RANGE || HAVE_POSIX_FADVISE */
+
VfdCache[file].seekPos += returnCode;
/* maintain fileSize and temporary_files_size if it's a temp file */
diff --git a/src/backend/storage/smgr/md.c b/src/backend/storage/smgr/md.c
index 42a43bb..dbf057f 100644
--- a/src/backend/storage/smgr/md.c
+++ b/src/backend/storage/smgr/md.c
@@ -531,7 +531,7 @@ mdextend(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
errmsg("could not seek to block %u in file \"%s\": %m",
blocknum, FilePathName(v->mdfd_vfd))));
- if ((nbytes = FileWrite(v->mdfd_vfd, buffer, BLCKSZ)) != BLCKSZ)
+ if ((nbytes = FileWrite(v->mdfd_vfd, buffer, BLCKSZ, false, NULL)) != BLCKSZ)
{
if (nbytes < 0)
ereport(ERROR,
@@ -738,7 +738,8 @@ mdread(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
*/
void
mdwrite(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
- char *buffer, bool skipFsync)
+ char *buffer, bool skipFsync, bool flush_to_disk,
+ FileFlushContext * context)
{
off_t seekpos;
int nbytes;
@@ -767,7 +768,7 @@ mdwrite(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
errmsg("could not seek to block %u in file \"%s\": %m",
blocknum, FilePathName(v->mdfd_vfd))));
- nbytes = FileWrite(v->mdfd_vfd, buffer, BLCKSZ);
+ nbytes = FileWrite(v->mdfd_vfd, buffer, BLCKSZ, flush_to_disk, context);
TRACE_POSTGRESQL_SMGR_MD_WRITE_DONE(forknum, blocknum,
reln->smgr_rnode.node.spcNode,
diff --git a/src/backend/storage/smgr/smgr.c b/src/backend/storage/smgr/smgr.c
index 244b4ea..2db3cd3 100644
--- a/src/backend/storage/smgr/smgr.c
+++ b/src/backend/storage/smgr/smgr.c
@@ -52,7 +52,8 @@ typedef struct f_smgr
void (*smgr_read) (SMgrRelation reln, ForkNumber forknum,
BlockNumber blocknum, char *buffer);
void (*smgr_write) (SMgrRelation reln, ForkNumber forknum,
- BlockNumber blocknum, char *buffer, bool skipFsync);
+ BlockNumber blocknum, char *buffer, bool skipFsync,
+ bool flush_to_disk, FileFlushContext *context);
BlockNumber (*smgr_nblocks) (SMgrRelation reln, ForkNumber forknum);
void (*smgr_truncate) (SMgrRelation reln, ForkNumber forknum,
BlockNumber nblocks);
@@ -643,10 +644,11 @@ smgrread(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
*/
void
smgrwrite(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
- char *buffer, bool skipFsync)
+ char *buffer, bool skipFsync, bool flush_to_disk,
+ FileFlushContext * context)
{
(*(smgrsw[reln->smgr_which].smgr_write)) (reln, forknum, blocknum,
- buffer, skipFsync);
+ buffer, skipFsync, flush_to_disk, context);
}
/*
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index b3dac51..9219330 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -158,6 +158,7 @@ static bool check_bonjour(bool *newval, void **extra, GucSource source);
static bool check_ssl(bool *newval, void **extra, GucSource source);
static bool check_stage_log_stats(bool *newval, void **extra, GucSource source);
static bool check_log_stats(bool *newval, void **extra, GucSource source);
+static bool check_flush_to_disk(bool *newval, void **extra, GucSource source);
static bool check_canonical_path(char **newval, void **extra, GucSource source);
static bool check_timezone_abbreviations(char **newval, void **extra, GucSource source);
static void assign_timezone_abbreviations(const char *newval, void *extra);
@@ -1013,6 +1014,28 @@ static struct config_bool ConfigureNamesBool[] =
false,
NULL, NULL, NULL
},
+
+ {
+ {"checkpoint_sort", PGC_SIGHUP, WAL_CHECKPOINTS,
+ gettext_noop("Whether disk-page buffers are sorted on checkpoints."),
+ NULL
+ },
+ &checkpoint_sort,
+ true,
+ NULL, NULL, NULL
+ },
+
+ {
+ {"checkpoint_flush_to_disk", PGC_SIGHUP, WAL_CHECKPOINTS,
+ gettext_noop("Hint that checkpoint's writes are high priority."),
+ NULL
+ },
+ &checkpoint_flush_to_disk,
+ /* see bufmgr.h: true on Linux, false otherwise */
+ DEFAULT_CHECKPOINT_FLUSH_TO_DISK,
+ check_flush_to_disk, NULL, NULL
+ },
+
{
{"log_connections", PGC_SU_BACKEND, LOGGING_WHAT,
gettext_noop("Logs each successful connection."),
@@ -9795,6 +9818,21 @@ check_log_stats(bool *newval, void **extra, GucSource source)
}
static bool
+check_flush_to_disk(bool *newval, void **extra, GucSource source)
+{
+/* This test must be consistent with the one in FileWrite (storage/file/fd.c)
+ */
+#if ! (defined(HAVE_SYNC_FILE_RANGE) || defined(HAVE_POSIX_FADVISE))
+ /* just warn if it has no effect */
+ ereport(WARNING,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("Setting \"checkpoint_flush_to_disk\" has no effect "
+ "on this platform.")));
+#endif /* ! (HAVE_SYNC_FILE_RANGE || HAVE_POSIX_FADVISE) */
+ return true;
+}
+
+static bool
check_canonical_path(char **newval, void **extra, GucSource source)
{
/*
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 695a88f..01b1c96 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -201,6 +201,9 @@
#max_wal_size = 1GB
#min_wal_size = 80MB
#checkpoint_completion_target = 0.5 # checkpoint target duration, 0.0 - 1.0
+#checkpoint_sort = on # sort buffers on checkpoint
+#checkpoint_flush_to_disk = ? # send buffers to disk on checkpoint
+ # default is on if Linux, off otherwise
#checkpoint_warning = 30s # 0 disables
# - Archiving -
diff --git a/src/include/access/xlog.h b/src/include/access/xlog.h
index 6dacee2..dbd4757 100644
--- a/src/include/access/xlog.h
+++ b/src/include/access/xlog.h
@@ -186,6 +186,8 @@ extern bool XLOG_DEBUG;
typedef struct CheckpointStatsData
{
TimestampTz ckpt_start_t; /* start of checkpoint */
+ TimestampTz ckpt_sort_t; /* start buffer sorting */
+ TimestampTz ckpt_sort_end_t; /* end of sorting */
TimestampTz ckpt_write_t; /* start of flushing buffers */
TimestampTz ckpt_sync_t; /* start of fsyncs */
TimestampTz ckpt_sync_end_t; /* end of fsyncs */
diff --git a/src/include/postmaster/bgwriter.h b/src/include/postmaster/bgwriter.h
index a49c208..f9c8ca1 100644
--- a/src/include/postmaster/bgwriter.h
+++ b/src/include/postmaster/bgwriter.h
@@ -16,6 +16,7 @@
#define _BGWRITER_H
#include "storage/block.h"
+#include "storage/fd.h"
#include "storage/relfilenode.h"
@@ -29,7 +30,8 @@ extern void BackgroundWriterMain(void) pg_attribute_noreturn();
extern void CheckpointerMain(void) pg_attribute_noreturn();
extern void RequestCheckpoint(int flags);
-extern void CheckpointWriteDelay(int flags, double progress);
+extern void CheckpointWriteDelay(int flags, double progress,
+ FileFlushContext * context, int ctx_size);
extern bool ForwardFsyncRequest(RelFileNode rnode, ForkNumber forknum,
BlockNumber segno);
diff --git a/src/include/storage/buf_internals.h b/src/include/storage/buf_internals.h
index 521ee1c..32f2006 100644
--- a/src/include/storage/buf_internals.h
+++ b/src/include/storage/buf_internals.h
@@ -210,6 +210,23 @@ extern PGDLLIMPORT BufferDescPadded *BufferDescriptors;
/* in localbuf.c */
extern BufferDesc *LocalBufferDescriptors;
+/* in bufmgr.c */
+
+/*
+ * Structure to sort buffers per file on checkpoints.
+ *
+ * This structure is allocated per buffer in shared memory, so it should be
+ * kept as little as possible. Maybe the sort criterion could be compacted
+ * to reduce memory requirement and for faster comparison?
+ */
+typedef struct CheckpointSortItem {
+ int buf_id;
+ Oid relNode;
+ ForkNumber forkNum; /* hm... enum with only 4 values */
+ BlockNumber blockNum;
+} CheckpointSortItem;
+
+extern CheckpointSortItem *CheckpointBufferIds;
/*
* Internal routines: only called by bufmgr
diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.h
index ec0a254..4fd3ff5 100644
--- a/src/include/storage/bufmgr.h
+++ b/src/include/storage/bufmgr.h
@@ -55,6 +55,15 @@ extern double bgwriter_lru_multiplier;
extern bool track_io_timing;
extern int target_prefetch_pages;
+#ifdef HAVE_SYNC_FILE_RANGE
+#define DEFAULT_CHECKPOINT_FLUSH_TO_DISK true
+#else
+#define DEFAULT_CHECKPOINT_FLUSH_TO_DISK false
+#endif /* HAVE_SYNC_FILE_RANGE */
+
+extern bool checkpoint_flush_to_disk;
+extern bool checkpoint_sort;
+
/* in buf_init.c */
extern PGDLLIMPORT char *BufferBlocks;
diff --git a/src/include/storage/fd.h b/src/include/storage/fd.h
index 7eabe09..c7b2a6d 100644
--- a/src/include/storage/fd.h
+++ b/src/include/storage/fd.h
@@ -59,6 +59,24 @@ extern int max_files_per_process;
*/
extern int max_safe_fds;
+/*
+ * FileFlushContext structure:
+ *
+ * This is used to accumulate several flush requests on a file
+ * into a larger flush request.
+ * - fd: file descriptor of the file
+ * - ncalls: number of flushes merged together
+ * - offset: starting offset (minimum of all offsets)
+ * - nbytes: size (minimum extent to cover all flushed data)
+ * - filename: filename of fd for error messages
+ */
+typedef struct FileFlushContext {
+ int fd;
+ int ncalls;
+ off_t offset;
+ off_t nbytes;
+ char * filename;
+} FileFlushContext;
/*
* prototypes for functions in fd.c
@@ -70,7 +88,12 @@ extern File OpenTemporaryFile(bool interXact);
extern void FileClose(File file);
extern int FilePrefetch(File file, off_t offset, int amount);
extern int FileRead(File file, char *buffer, int amount);
-extern int FileWrite(File file, char *buffer, int amount);
+extern void ResetFileFlushContext(FileFlushContext * context);
+extern void PerformFileFlush(FileFlushContext * context);
+extern void FileAsynchronousFlush(FileFlushContext * context,
+ int fd, off_t offset, off_t nbytes, char * filename);
+extern int FileWrite(File file, char *buffer, int amount, bool flush_to_disk,
+ FileFlushContext * context);
extern int FileSync(File file);
extern off_t FileSeek(File file, off_t offset, int whence);
extern int FileTruncate(File file, off_t offset);
diff --git a/src/include/storage/smgr.h b/src/include/storage/smgr.h
index 69a624f..a46a70c 100644
--- a/src/include/storage/smgr.h
+++ b/src/include/storage/smgr.h
@@ -16,6 +16,7 @@
#include "fmgr.h"
#include "storage/block.h"
+#include "storage/fd.h"
#include "storage/relfilenode.h"
@@ -95,7 +96,8 @@ extern void smgrprefetch(SMgrRelation reln, ForkNumber forknum,
extern void smgrread(SMgrRelation reln, ForkNumber forknum,
BlockNumber blocknum, char *buffer);
extern void smgrwrite(SMgrRelation reln, ForkNumber forknum,
- BlockNumber blocknum, char *buffer, bool skipFsync);
+ BlockNumber blocknum, char *buffer, bool skipFsync,
+ bool flush_to_disk, FileFlushContext * context);
extern BlockNumber smgrnblocks(SMgrRelation reln, ForkNumber forknum);
extern void smgrtruncate(SMgrRelation reln, ForkNumber forknum,
BlockNumber nblocks);
@@ -120,8 +122,9 @@ extern void mdprefetch(SMgrRelation reln, ForkNumber forknum,
BlockNumber blocknum);
extern void mdread(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
char *buffer);
-extern void mdwrite(SMgrRelation reln, ForkNumber forknum,
- BlockNumber blocknum, char *buffer, bool skipFsync);
+extern void mdwrite(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
+ char *buffer, bool skipFsync, bool flush_to_disk,
+ FileFlushContext * context);
extern BlockNumber mdnblocks(SMgrRelation reln, ForkNumber forknum);
extern void mdtruncate(SMgrRelation reln, ForkNumber forknum,
BlockNumber nblocks);
On 2015-08-27 14:32:39 +0200, Fabien COELHO wrote:
v10b misses the checkpoint_sort part of the patch, and thus cannot be
applied.Yes, indeed, the second part is expected to be applied on top of v10a.
Oh, sorry. I'd somehow assumed they were two variants of the same patch
(one with "slim" sorting and the other without).
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
v10b misses the checkpoint_sort part of the patch, and thus cannot be
applied.Yes, indeed, the second part is expected to be applied on top of v10a.
Oh, sorry. I'd somehow assumed they were two variants of the same patch
(one with "slim" sorting and the other without).
The idea is that as these two features could be committed separately.
However, experiments show that flushing is really efficient when sorting
is done first, and moreover the two features conflict, so I've made two
dependent patches.
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Mon, Aug 24, 2015 at 12:45 PM, Fabien COELHO <coelho@cri.ensmp.fr> wrote:
Also check the file:
sh> file ./avg.py
./avg.py: Python script, UTF-8 Unicode text executable
There were some CRLF line terminators, after removing those, it worked
fine and here are the results of some of the tests done for sorting patch
(checkpoint-continuous-flush-10-a) :
Config Used
----------------------
M/c details
--------------------
IBM POWER-8 24 cores, 192 hardware threads
RAM = 492GB
Test details
------------------
warmup=60
scale=300
max_connections=150
shared_buffers=8GB
checkpoint_timeout=2min
time=7200
synchronous_commit=on
max_wal_size=5GB
parallelism - 128 clients, 128 threads
Sort - off
avg over 7200: 8256.382528 ± 6218.769282 [0.000000, 76.050000,
10975.500000, 13105.950000, 21729.000000]
percent of values below 10.0: 19.5%
Sort - on
avg over 7200: 8375.930639 ± 6148.747366 [0.000000, 84.000000,
10946.000000, 13084.000000, 20289.900000]
percent of values below 10.0: 18.6%
Before going to conclusion, let me try to explain above data (I am
explaining again even though Fabien has explained, to make it clear
if someone has not read his mail)
Let's try to understand with data for sorting - off option
avg over 7200: 8256.382528 ± 6218.769282
8256.382528 - average tps for 7200s pgbench run
6218.769282 - standard deviation on per second figures
[0.000000, 84.000000, 10946.000000, 13084.000000, 20289.900000]
These 5 values can be read as minimum TPS, q1, median TPS, q3,
maximum TPS over 7200s pgbench run. As far as I understand q1
and q3 median of subset of values which I didn't focussed much.
percent of values below 10.0: 19.5%
Above means percent of time the result is below 10 tps.
Now about test results, these tests are done for pgbench full speed runs
and the above results indicate that there is approximately 1.5%
improvement in avg. TPS and ~1% improvement in tps values which are
below 10 with sorting on and there is almost no improvement in median or
maximum TPS values, instead they or slightly less when sorting is
on which could be due to run-to-run variation.
I have done more tests as well by varying time and number of clients
keeping other configuration same as above, but the results are quite
similar.
The results of sorting patch for the tests done indicate that the win is not
big enough with just doing sorting during checkpoints, we should consider
flush patch along with sorting. I would like to perform some tests with
both
the patches together (sort + flush) unless somebody else thinks that sorting
patch alone is beneficial and we should test some other kind of scenarios to
see it's benefit.
The reason for the tablespace balancing is that in the current postgres
buffers are written more or less randomly, so it is (probably) implicitely
and statistically balanced over tablespaces because of this randomness, and
indeed, AFAIK, people with multi tablespace setup have not complained that
postgres was using the disks sequentially.
However, once the buffers are sorted per file, the order becomes
deterministic and there is no more implicit balancing, which means that if
someone has a pg setup with several disks it will write sequentially on
these instead of in parallel.
What if tablespaces are not on separate disks or not enough hardware
support to make Writes parallel? I think for such cases it might be
better to do it sequentially.
With Regards,
Amit Kapila.
EnterpriseDB: http://www.enterprisedb.com
Hello Amit,
IBM POWER-8 24 cores, 192 hardware threads
RAM = 492GB
Wow! Thanks for trying the patch on such high-end hardware!
About the disks: what kind of HDD (RAID? speed?)? HDD write cache?
What is the OS? The FS?
warmup=60
Quite short, but probably ok.
scale=300
Means about 4-4.5 GB base.
time=7200
synchronous_commit=on
shared_buffers=8GB
This is small wrt hardware, but given the scale setup I think that it
should not matter much.
max_wal_size=5GB
Hmmm... Maybe quite small given the average performance?
checkpoint_timeout=2min
This seems rather small. Are the checkpoints xlog or time triggered?
You did not update checkpoint_completion_target, which means 0.5 so that
the checkpoint is scheduled to run in at most 1 minute, which suggest at
least 130 MB/s write performance for the checkpoint.
parallelism - 128 clients, 128 threads
Given 192 hw threads, I would have tried used 128 clients & 64 threads, so
that each pgbench client has its own dedicated postgres in a thread, and
that postgres processes are not competing with pgbench. Now as pgbench is
mostly sleeping, probably that does not matter much... I may also be
totally wrong:-)
Sort - off
avg over 7200: 8256.382528 ± 6218.769282 [0.000000, 76.050000,
10975.500000, 13105.950000, 21729.000000]
percent of values below 10.0: 19.5%
The max performance is consistent with 128 threads * 200 (random) writes
per second.
Sort - on
avg over 7200: 8375.930639 ± 6148.747366 [0.000000, 84.000000,
10946.000000, 13084.000000, 20289.900000]
percent of values below 10.0: 18.6%
This is really a small improvement, probably in the error interval of the
measure. I would not trust much 1.5% tps or 0.9% availability
improvements.
I think that we could conclude that on your (great) setup, with these
configuration parameter, this patch does not harm performance. This is a
good thing, even if I would have hoped to see better performance.
Before going to conclusion, let me try to explain above data (I am
explaining again even though Fabien has explained, to make it clear
if someone has not read his mail)Let's try to understand with data for sorting - off option
avg over 7200: 8256.382528 ± 6218.769282
8256.382528 - average tps for 7200s pgbench run
6218.769282 - standard deviation on per second figures[0.000000, 84.000000, 10946.000000, 13084.000000, 20289.900000]
These 5 values can be read as minimum TPS, q1, median TPS, q3,
maximum TPS over 7200s pgbench run. As far as I understand q1
and q3 median of subset of values which I didn't focussed much.
q1 = 84 means that 25% of the time the performance was below 84 tps, about
1% of the average performance, which I would translate as "pg is pretty
unresponsive 25% of the time".
This is the kind of issue I really want to address, the eventual tps
improvements are just a side effect.
percent of values below 10.0: 19.5%
Above means percent of time the result is below 10 tps.
Which means "postgres is really unresponsive 19.5% of the time".
If you count zeros, you will get "postgres was totally unresponsive X% of
the time".
Now about test results, these tests are done for pgbench full speed runs
and the above results indicate that there is approximately 1.5%
improvement in avg. TPS and ~1% improvement in tps values which are
below 10 with sorting on and there is almost no improvement in median or
maximum TPS values, instead they or slightly less when sorting is
on which could be due to run-to-run variation.
Yes, I agree.
I have done more tests as well by varying time and number of clients
keeping other configuration same as above, but the results are quite
similar.
Given the hardware, I would suggest to raise checkpoint_timeout,
shared_buffers and max_wal_size, and use checkpoint_completion_target=0.8.
I would expect that it should improve performance both with and without
sorting.
It would be interesting to have informations from checkpoint logs
(especially how many buffers written in how long, whether checkpoints are
time or xlog triggered, ...).
The results of sorting patch for the tests done indicate that the win is
not big enough with just doing sorting during checkpoints,
ISTM that you do too much generalization: The win is not big "under this
configuration and harware".
I think that the patch may have very small influence under some
conditions, but should not degrade performance significantly, and on the
other hand it should provide great improvements under some (other)
conditions.
So having no performance degradation is a good result, even if I would
hope to get better results. It would be interesting to understand why
random disk writes do not perform too poorly on this box: size of I/O
queue, kind of (expensive:-) disks, write caches, file system, raid
level...
we should consider flush patch along with sorting.
I also think that it would be interesting.
I would like to perform some tests with both the patches together (sort
+ flush) unless somebody else thinks that sorting patch alone is
beneficial and we should test some other kind of scenarios to see it's
benefit.
Yep. Is it a Linux box? If not, does it support posix_fadvise()?
The reason for the tablespace balancing is [...]
What if tablespaces are not on separate disks
I would expect that it might very slightly degrade performance, but only
marginally.
or not enough hardware support to make Writes parallel?
I'm not sure that balancing or not writes over tablespaces would change
anything to an I/O bottleneck which is not the disk write performance, so
I would say "no impact" in that case.
I think for such cases it might be better to do it sequentially.
Writing sequentially to different disks would be a bug, and degrade
performance significantly on a setup with several disks, up to dividing
the performance by the number of disks... so I do think that a patch which
predictability and significantly degrades performance on high-end harware
is a reasonable option.
If you want to be able to disactivate balancing, it could be done with a
guc, but I cannot see good reasons to want to do that: it would complicate
the code and it does not make much sense to use many tablespaces on one
disk, while anyone who uses several tablespaces on several disks is
probably expecting to see her expensive disks actually used in parallel.
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Mon, Aug 31, 2015 at 12:40 PM, Fabien COELHO <coelho@cri.ensmp.fr> wrote:
Hello Amit,
IBM POWER-8 24 cores, 192 hardware threads
RAM = 492GBWow! Thanks for trying the patch on such high-end hardware!
About the disks: what kind of HDD (RAID? speed?)? HDD write cache?
Speed of Reads -
Timing cached reads: 27790 MB in 1.98 seconds = 14001.86 MB/sec
Timing buffered disk reads: 3830 MB in 3.00 seconds = 1276.55 MB/sec
Copy speed -
dd if=/dev/zero of=/tmp/output.img bs=8k count=256k
262144+0 records in
262144+0 records out
2147483648 bytes (2.1 GB) copied, 1.30993 s, 1.6 GB/s
What is the OS? The FS?
OS info -
Linux <m/c addr> 3.10.0-123.1.2.el7.ppc64 #1 SMP Wed Jun 4 15:23:17 EDT
2014 ppc64 ppc64 ppc64 GNU/Linux
FS - ext4
shared_buffers=8GB
This is small wrt hardware, but given the scale setup I think that it
should not matter much.
Yes, I was testing the case for Read-Write transactions when all the data
fits in shared_buffers, so this is okay.
max_wal_size=5GB
Hmmm... Maybe quite small given the average performance?
We can check with larger value, but do you expect some different
results and why?
checkpoint_timeout=2min
This seems rather small. Are the checkpoints xlog or time triggered?
I wanted to test by triggering more checkpoints, but I can test with
larger checkpoint interval as wel like 5 or 10 mins. Any suggestions?
You did not update checkpoint_completion_target, which means 0.5 so that
the checkpoint is scheduled to run in at most 1 minute, which suggest at
least 130 MB/s write performance for the checkpoint.
The value used in your script was 0.8 for checkpoint_completion_target
which I have not changed during tests.
parallelism - 128 clients, 128 threads
Given 192 hw threads, I would have tried used 128 clients & 64 threads,
so that each pgbench client has its own dedicated postgres in a thread, and
that postgres processes are not competing with pgbench. Now as pgbench is
mostly sleeping, probably that does not matter much... I may also be
totally wrong:-)
In next run, I can use it with 64 threads, lets settle on other parameters
first for which you expect there could be a clear win with the first patch.
Given the hardware, I would suggest to raise checkpoint_timeout,
shared_buffers and max_wal_size, and use checkpoint_completion_target=0.8.
I would expect that it should improve performance both with and without
sorting.
I don't think increasing shared_buffers would have any impact, because
8GB is sufficient for 300 scale factor data, checkpoint_completion_target is
already 0.8 in my previous tests. Lets try with checkpoint_timeout = 10 min
and max_wal_size = 15GB, do you have any other suggestion?
It would be interesting to have informations from checkpoint logs
(especially how many buffers written in how long, whether checkpoints are
time or xlog triggered, ...).
The results of sorting patch for the tests done indicate that the win is
not big enough with just doing sorting during checkpoints,
ISTM that you do too much generalization: The win is not big "under this
configuration and harware".
Hmm.. nothing like that, this was based on couple of tests done by
me and I am open to do some more if you or anybody feels that the
first patch (checkpoint-continuous-flush-10-a) can alone gives benefit,
in-fact I have started these tests with the intention to see if first
patch gives benefit, then that could be evaluated and eventually
committed separately.
I think that the patch may have very small influence under some
conditions, but should not degrade performance significantly, and on the
other hand it should provide great improvements under some (other)
conditions.
True, let us try to find conditions/scenarios where you think it can give
big boost, suggestions are welcome.
What if tablespaces are not on separate disks
I would expect that it might very slightly degrade performance, but only
marginally.
If you want to be able to disactivate balancing, it could be done with a
guc, but I cannot see good reasons to want to do that: it would complicate
the code and it does not make much sense to use many tablespaces on one
disk, while anyone who uses several tablespaces on several disks is
probably expecting to see her expensive disks actually used in parallel.
I think we can leave this for committer to take a call or if anybody
else has any opinion, because there is nothing wrong in what you
have done, but I am not clear if there is a clear need for the same.
With Regards,
Amit Kapila.
EnterpriseDB: http://www.enterprisedb.com
Hello Amit,
About the disks: what kind of HDD (RAID? speed?)? HDD write cache?
Speed of Reads -
Timing cached reads: 27790 MB in 1.98 seconds = 14001.86 MB/sec
Timing buffered disk reads: 3830 MB in 3.00 seconds = 1276.55 MB/sec
Woops.... 14 GB/s and 1.2 GB/s?! Is this a *hard* disk??
Copy speed -
dd if=/dev/zero of=/tmp/output.img bs=8k count=256k
262144+0 records in
262144+0 records out
2147483648 bytes (2.1 GB) copied, 1.30993 s, 1.6 GB/s
Woops, 1.6 GB/s write... same questions, "rotating plates"?? Looks more
like several SSD... Or the file is kept in memory and not committed to
disk yet? Try a "sync" afterwards??
If these are SSD, or if there is some SSD cache on top of the HDD, I would
not expect the patch to do much, because the SSD random I/O writes are
pretty comparable to sequential I/O writes.
I would be curious whether flushing helps, though.
max_wal_size=5GB
Hmmm... Maybe quite small given the average performance?
We can check with larger value, but do you expect some different
results and why?
Because checkpoints are xlog triggered (which depends on max_wal_size) or
time triggered (which depends on checkpoint_timeout). Given the large tps,
I expect that the WAL is filled very quickly hence may trigger checkpoints
every ... that is the question.
checkpoint_timeout=2min
This seems rather small. Are the checkpoints xlog or time triggered?
I wanted to test by triggering more checkpoints, but I can test with
larger checkpoint interval as wel like 5 or 10 mins. Any suggestions?
For a +2 hours test, I would suggest 10 or 15 minutes.
It would be useful to know about checkpoint stats before suggesting values
for max_wal_size and checkpoint_timeout.
[...] The value used in your script was 0.8 for
checkpoint_completion_target which I have not changed during tests.
Ok.
parallelism - 128 clients, 128 threads [...]
In next run, I can use it with 64 threads, lets settle on other parameters
first for which you expect there could be a clear win with the first patch.
Ok.
Given the hardware, I would suggest to raise checkpoint_timeout,
shared_buffers and max_wal_size, [...]. I would expect that it should
improve performance both with and without sorting.I don't think increasing shared_buffers would have any impact, because
8GB is sufficient for 300 scale factor data,
It fits at the beginning, but when updates and inserts are performed
postgres adds new pages (update = delete + insert), and the deleted space
is eventually reclaimed by vacuum later on.
Now if space is available in the page it is reused, so what really happens
is not that simple...
At 8500 tps the disk space extension for tables may be up to 3 MB/s at the
beginning, and would evolve but should be at least about 0.6 MB/s (insert
in history, assuming updates are performed in page), on average.
So whether the database fits in 8 GB shared buffer during the 2 hours of
the pgbench run is an open question.
checkpoint_completion_target is already 0.8 in my previous tests. Lets
try with checkpoint_timeout = 10 min and max_wal_size = 15GB, do you
have any other suggestion?
Maybe shared_buffers = 32GB to ensure that it is a "in buffer" run ?
It would be interesting to have informations from checkpoint logs
(especially how many buffers written in how long, whether checkpoints
are time or xlog triggered, ...).
Information still welcome.
Hmm.. nothing like that, this was based on couple of tests done by
me and I am open to do some more if you or anybody feels that the
first patch (checkpoint-continuous-flush-10-a) can alone gives benefit,
in-fact I have started these tests with the intention to see if first
patch gives benefit, then that could be evaluated and eventually
committed separately.
Ok.
My initial question remains: is the setup using HDDs? For SSD there should
be probably no significant benefit with sorting, although it should not
harm, and I'm not sure about flushing.
True, let us try to find conditions/scenarios where you think it can give
big boost, suggestions are welcome.
HDDs?
I think we can leave this for committer to take a call or if anybody
else has any opinion, because there is nothing wrong in what you
have done, but I am not clear if there is a clear need for the same.
I may have an old box available with two disks, so that I can run some
tests with table spaces, but with very few cores.
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Tue, Sep 1, 2015 at 5:30 PM, Fabien COELHO <coelho@cri.ensmp.fr> wrote:
Hello Amit,
About the disks: what kind of HDD (RAID? speed?)? HDD write cache?
Speed of Reads -
Timing cached reads: 27790 MB in 1.98 seconds = 14001.86 MB/sec
Timing buffered disk reads: 3830 MB in 3.00 seconds = 1276.55 MB/secWoops.... 14 GB/s and 1.2 GB/s?! Is this a *hard* disk??
Yes, there is no SSD in system. I have confirmed the same. There are RAID
spinning drives.
Copy speed -
dd if=/dev/zero of=/tmp/output.img bs=8k count=256k
262144+0 records in
262144+0 records out
2147483648 bytes (2.1 GB) copied, 1.30993 s, 1.6 GB/sWoops, 1.6 GB/s write... same questions, "rotating plates"??
One thing to notice is that if I don't remove the output file (output.img)
the
speed is much slower, see the below output. I think this means in our case
we will get ~320 MB/s
dd if=/dev/zero of=/data/akapila/output.img bs=8k count=256k
262144+0 records in
262144+0 records out
2147483648 bytes (2.1 GB) copied, 1.28086 s, 1.7 GB/s
dd if=/dev/zero of=/data/akapila/output.img bs=8k count=256k
262144+0 records in
262144+0 records out
2147483648 bytes (2.1 GB) copied, 6.72301 s, 319 MB/s
dd if=/dev/zero of=/data/akapila/output.img bs=8k count=256k
262144+0 records in
262144+0 records out
2147483648 bytes (2.1 GB) copied, 6.73963 s, 319 MB/s
If I remove the file each time:
dd if=/dev/zero of=/data/akapila/output.img bs=8k count=256k
262144+0 records in
262144+0 records out
2147483648 bytes (2.1 GB) copied, 1.2855 s, 1.7 GB/s
rm /data/akapila/output.img
dd if=/dev/zero of=/data/akapila/output.img bs=8k count=256k
262144+0 records in
262144+0 records out
2147483648 bytes (2.1 GB) copied, 1.27725 s, 1.7 GB/s
rm /data/akapila/output.img
dd if=/dev/zero of=/data/akapila/output.img bs=8k count=256k
262144+0 records in
262144+0 records out
2147483648 bytes (2.1 GB) copied, 1.27417 s, 1.7 GB/s
rm /data/akapila/output.img
Looks more like several SSD... Or the file is kept in memory and not
committed to disk yet? Try a "sync" afterwards??
If these are SSD, or if there is some SSD cache on top of the HDD, I would
not expect the patch to do much, because the SSD random I/O writes are
pretty comparable to sequential I/O writes.I would be curious whether flushing helps, though.
Yes, me too. I think we should try to reach on consensus for exact scenarios
and configuration where this patch('es) can give benefit or we want to
verify
if there is any regression as I have access to this m/c for a very-very
limited
time. This m/c might get formatted soon for some other purpose.
max_wal_size=5GB
Hmmm... Maybe quite small given the average performance?
We can check with larger value, but do you expect some different
results and why?Because checkpoints are xlog triggered (which depends on max_wal_size) or
time triggered (which depends on checkpoint_timeout). Given the large tps,
I expect that the WAL is filled very quickly hence may trigger checkpoints
every ... that is the question.checkpoint_timeout=2min
This seems rather small. Are the checkpoints xlog or time triggered?
I wanted to test by triggering more checkpoints, but I can test with
larger checkpoint interval as wel like 5 or 10 mins. Any suggestions?For a +2 hours test, I would suggest 10 or 15 minutes.
Okay, lets keep it as 10 minutes.
I don't think increasing shared_buffers would have any impact, because
8GB is sufficient for 300 scale factor data,
It fits at the beginning, but when updates and inserts are performed
postgres adds new pages (update = delete + insert), and the deleted space
is eventually reclaimed by vacuum later on.Now if space is available in the page it is reused, so what really happens
is not that simple...At 8500 tps the disk space extension for tables may be up to 3 MB/s at the
beginning, and would evolve but should be at least about 0.6 MB/s (insert
in history, assuming updates are performed in page), on average.So whether the database fits in 8 GB shared buffer during the 2 hours of
the pgbench run is an open question.
With this kind of configuration, I have noticed that more than 80%
of updates are HOT updates, not much bloat, so I think it won't
cross 8GB limit, but still I can keep it to 32GB if you have any doubts.
With Regards,
Amit Kapila.
EnterpriseDB: http://www.enterprisedb.com
Hello Amit,
Woops.... 14 GB/s and 1.2 GB/s?! Is this a *hard* disk??
Yes, there is no SSD in system. I have confirmed the same. There are RAID
spinning drives.
Ok...
I guess that there is some kind of cache to explain these great tps
figures, probably on the RAID controller. What does "lspci" says? Does
"hdparm" suggests that the write cache is enabled? It would be fine if the
I/O system has a BBU, but that could also hide some of the patch
benefits...
A tentative explanation for the similar figures with and without sorting
could be that depending on the controller cache size (may be 1GB or more)
and firmware, the I/O system reorders disk writes so that they are
basically sequential and the fact that pg sorts them beforehand has little
or no impact. This may also be help by the fact that buffers are not
really in random order to begin with as the warmup phase does an initial
"select stuff from table".
There could be other possible factors such as the file system details,
"WAFL" hacks... the tricks are endless:-)
Checking for the right explanation would involve removing the
unconditional select warmup to use only a long and random warmup, and
probably trying a much larger than cache database, and/or disabling the
write cache, reading the hardware documentation in detail... But this is
also a lot of bother and time.
Maybe the simplest approach would be to disable the write cache for the
test. Is that possible?
Woops, 1.6 GB/s write... same questions, "rotating plates"??
One thing to notice is that if I don't remove the output file
(output.img) the speed is much slower, see the below output. I think
this means in our case we will get ~320 MB/s
I would say that the OS was doing something here, and 320 MB/s looks more
like an actual HDD RAID system sequential write performance.
If these are SSD, or if there is some SSD cache on top of the HDD, I would
not expect the patch to do much, because the SSD random I/O writes are
pretty comparable to sequential I/O writes.I would be curious whether flushing helps, though.
Yes, me too. I think we should try to reach on consensus for exact
scenarios and configuration where this patch('es) can give benefit or we
want to verify if there is any regression as I have access to this m/c
for a very-very limited time. This m/c might get formatted soon for
some other purpose.
Yep, it would be great if you have time for a flush test before it
disappears... I think it is advisable to disable the write cache as it may
also hide the impact of flushing.
So whether the database fits in 8 GB shared buffer during the 2 hours of
the pgbench run is an open question.With this kind of configuration, I have noticed that more than 80%
of updates are HOT updates, not much bloat, so I think it won't
cross 8GB limit, but still I can keep it to 32GB if you have any doubts.
The problem with performance tests is that you want to test one thing, but
there are many factors that intervene and you may end up testing something
else, such as lock contention or process scheduler or whatever, rather
than what you were trying to put in evidence. So I would suggest to be on
the safe side and use the larger value.
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
I would be curious whether flushing helps, though.
Yes, me too. I think we should try to reach on consensus for exact
scenarios and configuration where this patch('es) can give benefit or we
want to verify if there is any regression as I have access to this m/c for
a very-very limited time. This m/c might get formatted soon for some other
purpose.Yep, it would be great if you have time for a flush test before it
disappears... I think it is advisable to disable the write cache as it may
also hide the impact of flushing.
Still thinking... Depending on the results, it might be interesting to
have these tests run with the write cache enabled as well, to check how
much it interferes positively with performance.
I would guess "quite a lot".
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Hi,
Here's a bunch of comments on this (hopefully the latest?) version of
the patch:
* I'm not sure I like the FileWrite & FlushBuffer API changes. Do you
forsee other callsites needing similar logic? Wouldn't it be just as
easy to put this logic into the checkpointing code?
* We don't do one-line ifs; function parameters are always in the same
line as the function name
* Wouldn't a binary heap over the tablespaces + progress be nicer? If
you make the sorting criterion include the tablespace id you wouldn't
need the lookahead loop in NextBufferToWrite(). Isn't the current
approach O(NBuffers^2) in the worst case?
Greetings,
Andres Freund
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Hello Andres,
Here's a bunch of comments on this (hopefully the latest?)
Who knows?! :-)
version of the patch:
* I'm not sure I like the FileWrite & FlushBuffer API changes. Do you
forsee other callsites needing similar logic?
I foresee that the bgwriter should also do something more sensible than
generating random I/Os over HDDs, and this is also true for workers... But
this is for another time, maybe.
Wouldn't it be just as easy to put this logic into the checkpointing
code?
Not sure it would simplify anything, because the checkpointer currently
knows about buffers but flushing is about files, which are hidden from
view.
Doing it with this API change means that the code does not have to compute
twice in which file is a buffer: The buffer/file boundary has to be broken
somewhere anyway so that flushing can be done when needed, and the
solution I took seems the simplest way to do it, without having to make
the checkpointer too much file concious.
* We don't do one-line ifs;
Ok, I'll return them.
function parameters are always in the same line as the function name
Ok, I'll try to improve.
* Wouldn't a binary heap over the tablespaces + progress be nicer?
I'm not sure where it would fit exactly.
Anyway, I think it would complicate the code significantly (compared to
the straightforward array), so I would not do anything like that without a
strong intensive, such as an actual failing case.
Moreover such a data structure would probably require some kind of pointer
(probably 8 bytes added per node, maybe more), and the amount of memory is
already a concern, at least to me, and moreover it has to reside in shared
memory which does not simplify allocation of tree data structures.
If you make the sorting criterion include the tablespace id you wouldn't
need the lookahead loop in NextBufferToWrite().
Yep, I thought of it. It would mean 4 more bytes per buffer, and bsearch
to find the boundaries, so significantly less simple code. I think that
the current approach is ok as the number of tablespace should be small.
It may be improved upon later if there is a motivation to do so.
Isn't the current approach O(NBuffers^2) in the worst case?
ISTM that the overall lookahead complexity is Nbuffers * Ntablespace:
buffers are scanned once for each tablespace. I assume that the number of
tablespace is kept low, and having a simpler code which use less memory
seems a good idea.
ISTM that using a tablespace in the sorting would reduce the complexity
to ln(NBuffers) * Ntablespace for finding the boundaries, and then
Nbuffers * (Ntablespace/Ntablespace) = NBuffers for scanning, at the
expense of more memory and code complexity.
So this is a voluntary design decision.
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Here is a rebased two-part v11.
* We don't do one-line ifs;
I've found one instance.
function parameters are always in the same line as the function name
ISTM that I did that, or maybe I did not understand what I've done wrong.
--
Fabien.
Attachments:
checkpoint-continuous-flush-11a.patchtext/x-diff; name=checkpoint-continuous-flush-11a.patchDownload
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index e3dc23b..96c9a2f 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -2454,6 +2454,28 @@ include_dir 'conf.d'
</listitem>
</varlistentry>
+ <varlistentry id="guc-checkpoint-sort" xreflabel="checkpoint_sort">
+ <term><varname>checkpoint_sort</varname> (<type>bool</type>)
+ <indexterm>
+ <primary><varname>checkpoint_sort</> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ Whether to sort buffers before writting them out to disk on checkpoint.
+ For a HDD storage, this setting allows to group together
+ neighboring pages written to disk, thus improving performance by
+ reducing random write activity.
+ This sorting should have limited performance effects on SSD backends
+ as such storages have good random write performance, but it may
+ help with wear-leveling so be worth keeping anyway.
+ The default is <literal>on</>.
+ This parameter can only be set in the <filename>postgresql.conf</>
+ file or on the server command line.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="guc-checkpoint-warning" xreflabel="checkpoint_warning">
<term><varname>checkpoint_warning</varname> (<type>integer</type>)
<indexterm>
diff --git a/doc/src/sgml/wal.sgml b/doc/src/sgml/wal.sgml
index e3941c9..f538698 100644
--- a/doc/src/sgml/wal.sgml
+++ b/doc/src/sgml/wal.sgml
@@ -546,6 +546,18 @@
</para>
<para>
+ When hard-disk drives (HDD) are used for terminal data storage
+ <xref linkend="guc-checkpoint-sort"> allows to sort pages
+ so that neighboring pages on disk will be flushed together by
+ chekpoints, reducing the random write load and improving performance.
+ If solid-state drives (SSD) are used, sorting pages induces no benefit
+ as their random write I/O performance is good: this feature could then
+ be disabled by setting <varname>checkpoint_sort</> to <value>off</>.
+ It is possible that sorting may help with SSD wear leveling, so it may
+ be kept on that account.
+ </para>
+
+ <para>
The number of WAL segment files in <filename>pg_xlog</> directory depends on
<varname>min_wal_size</>, <varname>max_wal_size</> and
the amount of WAL generated in previous checkpoint cycles. When old log
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 127bc58..74412a6 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -7999,11 +7999,13 @@ LogCheckpointEnd(bool restartpoint)
sync_secs,
total_secs,
longest_secs,
+ sort_secs,
average_secs;
int write_usecs,
sync_usecs,
total_usecs,
longest_usecs,
+ sort_usecs,
average_usecs;
uint64 average_sync_time;
@@ -8034,6 +8036,10 @@ LogCheckpointEnd(bool restartpoint)
CheckpointStats.ckpt_end_t,
&total_secs, &total_usecs);
+ TimestampDifference(CheckpointStats.ckpt_sort_t,
+ CheckpointStats.ckpt_sort_end_t,
+ &sort_secs, &sort_usecs);
+
/*
* Timing values returned from CheckpointStats are in microseconds.
* Convert to the second plus microsecond form that TimestampDifference
@@ -8052,8 +8058,8 @@ LogCheckpointEnd(bool restartpoint)
elog(LOG, "%s complete: wrote %d buffers (%.1f%%); "
"%d transaction log file(s) added, %d removed, %d recycled; "
- "write=%ld.%03d s, sync=%ld.%03d s, total=%ld.%03d s; "
- "sync files=%d, longest=%ld.%03d s, average=%ld.%03d s; "
+ "sort=%ld.%03d s, write=%ld.%03d s, sync=%ld.%03d s, total=%ld.%03d s;"
+ " sync files=%d, longest=%ld.%03d s, average=%ld.%03d s; "
"distance=%d kB, estimate=%d kB",
restartpoint ? "restartpoint" : "checkpoint",
CheckpointStats.ckpt_bufs_written,
@@ -8061,6 +8067,7 @@ LogCheckpointEnd(bool restartpoint)
CheckpointStats.ckpt_segs_added,
CheckpointStats.ckpt_segs_removed,
CheckpointStats.ckpt_segs_recycled,
+ sort_secs, sort_usecs / 1000,
write_secs, write_usecs / 1000,
sync_secs, sync_usecs / 1000,
total_secs, total_usecs / 1000,
diff --git a/src/backend/storage/buffer/buf_init.c b/src/backend/storage/buffer/buf_init.c
index 3ae2848..3bd5eab 100644
--- a/src/backend/storage/buffer/buf_init.c
+++ b/src/backend/storage/buffer/buf_init.c
@@ -65,7 +65,8 @@ void
InitBufferPool(void)
{
bool foundBufs,
- foundDescs;
+ foundDescs,
+ foundCpid;
/* Align descriptors to a cacheline boundary. */
BufferDescriptors = (BufferDescPadded *) CACHELINEALIGN(
@@ -77,10 +78,14 @@ InitBufferPool(void)
ShmemInitStruct("Buffer Blocks",
NBuffers * (Size) BLCKSZ, &foundBufs);
- if (foundDescs || foundBufs)
+ CheckpointBufferIds = (CheckpointSortItem *)
+ ShmemInitStruct("Checkpoint BufferIds",
+ NBuffers * sizeof(CheckpointSortItem), &foundCpid);
+
+ if (foundDescs || foundBufs || foundCpid)
{
- /* both should be present or neither */
- Assert(foundDescs && foundBufs);
+ /* all should be present or neither */
+ Assert(foundDescs && foundBufs && foundCpid);
/* note: this path is only taken in EXEC_BACKEND case */
}
else
@@ -144,5 +149,8 @@ BufferShmemSize(void)
/* size of stuff controlled by freelist.c */
size = add_size(size, StrategyShmemSize());
+ /* size of checkpoint sort array in bufmgr.c */
+ size = add_size(size, mul_size(NBuffers, sizeof(CheckpointSortItem)));
+
return size;
}
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index cd3aaad..cc951e1 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -80,6 +80,7 @@ bool zero_damaged_pages = false;
int bgwriter_lru_maxpages = 100;
double bgwriter_lru_multiplier = 2.0;
bool track_io_timing = false;
+bool checkpoint_sort = true;
/*
* How many buffers PrefetchBuffer callers should try to stay ahead of their
@@ -95,6 +96,9 @@ static bool IsForInput;
/* local state for LockBufferForCleanup */
static volatile BufferDesc *PinCountWaitBuf = NULL;
+/* array of buffer ids & sort criterion of all buffers to checkpoint */
+CheckpointSortItem *CheckpointBufferIds = NULL;
+
/*
* Backend-Private refcount management:
*
@@ -1561,6 +1565,130 @@ UnpinBuffer(volatile BufferDesc *buf, bool fixOwner)
}
}
+/* checkpoint buffers comparison */
+static int bufcmp(const void * pa, const void * pb)
+{
+ CheckpointSortItem
+ *a = (CheckpointSortItem *) pa,
+ *b = (CheckpointSortItem *) pb;
+
+ /* compare relation */
+ if (a->relNode < b->relNode)
+ return -1;
+ else if (a->relNode > b->relNode)
+ return 1;
+ /* same relation, compare fork */
+ else if (a->forkNum < b->forkNum)
+ return -1;
+ else if (a->forkNum > b->forkNum)
+ return 1;
+ /* same relation/fork, so same segmented "file", compare block number
+ * which are mapped on different segments depending on the number.
+ */
+ else if (a->blockNum < b->blockNum)
+ return -1;
+ else /* should not be the same block anyway... */
+ return 1;
+}
+
+/*
+ * Status of buffers to checkpoint for a particular tablespace,
+ * used internally in BufferSync.
+ * - space: oid of the tablespace
+ * - num_to_write: number of checkpoint pages counted for this tablespace
+ * - num_written: number of pages actually written out
+ * - index: scanning position in CheckpointBufferIds for this tablespace
+ */
+typedef struct TableSpaceCheckpointStatus {
+ Oid space;
+ int num_to_write;
+ int num_written;
+ int index;
+} TableSpaceCheckpointStatus;
+
+/*
+ * Entry structure for table space to count hashtable,
+ * used internally in BufferSync.
+ */
+typedef struct TableSpaceCountEntry {
+ Oid space;
+ int count;
+} TableSpaceCountEntry;
+
+/*
+ * Return the next buffer to write, or -1.
+ * this function balances buffers over tablespaces, see comment inside.
+ */
+static int
+NextBufferToWrite(
+ TableSpaceCheckpointStatus *spcStatus, int nb_spaces,
+ int *pspace, int num_to_write, int num_written)
+{
+ int space = *pspace, buf_id = -1, index;
+
+ /*
+ * Select a tablespace depending on the current overall progress.
+ *
+ * The progress ratio of each unfinished tablespace is compared to
+ * the overall progress ratio to find one with is not in advance
+ * (i.e. overall ratio > tablespace ratio,
+ * i.e. tablespace written/to_write > overall written/to_write
+ *
+ * Existence: it is bound to exist otherwise the overall progress
+ * ratio would be inconsistent: with positive buffers to write (t1 & t2)
+ * and already written buffers (w1 & w2), we have:
+ *
+ * If w1/t1 > (w1+w2)/(t1+t2) # one table space is in advance
+ * => w1t1+w1t2 > w1t1+w2t1 => w1t2 > w2t1 => w1t2+w2t2 > w2t1+w2t2
+ * => (w1+w2) / (t1+t2) > w2 / t2 # the other one is late
+ *
+ * The round robin ensures that each space is given some attention
+ * till it is over the current ratio, before going to the next.
+ *
+ * Precision: using int32 computations for comparing fractions
+ * (w1 / t1 > w / t <=> w1 t > w t1) seems a bad idea as the values
+ * can overflow 32-bit integers: the limit would be sqrt(2**31) ~
+ * 46340 buffers, i.e. a 362 MB checkpoint. So ensure that 64-bit
+ * integers are used in the comparison.
+ */
+ while ((int64) spcStatus[space].num_written * num_to_write >
+ (int64) num_written * spcStatus[space].num_to_write)
+ space = (space + 1) % nb_spaces; /* round robin */
+
+ /*
+ * Find a valid buffer in the selected tablespace,
+ * by continuing the tablespace specific buffer scan
+ * where it was left.
+ */
+ index = spcStatus[space].index;
+
+ while (index < num_to_write && buf_id == -1)
+ {
+ volatile BufferDesc *bufHdr;
+
+ buf_id = CheckpointBufferIds[index].buf_id;
+ bufHdr = GetBufferDescriptor(buf_id);
+
+ /*
+ * Skip if in another tablespace or not in checkpoint anymore.
+ * No lock is acquired, see comments below.
+ */
+ if (spcStatus[space].space != bufHdr->tag.rnode.spcNode ||
+ ! (bufHdr->flags & BM_CHECKPOINT_NEEDED))
+ {
+ index ++;
+ buf_id = -1;
+ }
+ }
+
+ /* update tablespace writing status, will start over at next index */
+ spcStatus[space].index = index + 1;
+
+ *pspace = space;
+
+ return buf_id;
+}
+
/*
* BufferSync -- Write out all dirty buffers in the pool.
*
@@ -1574,11 +1702,13 @@ UnpinBuffer(volatile BufferDesc *buf, bool fixOwner)
static void
BufferSync(int flags)
{
- int buf_id;
- int num_to_scan;
+ int buf_id = -1;
int num_to_write;
int num_written;
int mask = BM_DIRTY;
+ HTAB *spcBuffers;
+ TableSpaceCheckpointStatus *spcStatus = NULL;
+ int nb_spaces, space;
/* Make sure we can handle the pin inside SyncOneBuffer */
ResourceOwnerEnlargeBuffers(CurrentResourceOwner);
@@ -1609,6 +1739,18 @@ BufferSync(int flags)
* certainly need to be written for the next checkpoint attempt, too.
*/
num_to_write = 0;
+
+ /* initialize oid -> int buffer count hash table */
+ {
+ HASHCTL ctl;
+
+ MemSet(&ctl, 0, sizeof(HASHCTL));
+ ctl.keysize = sizeof(Oid);
+ ctl.entrysize = sizeof(TableSpaceCountEntry);
+ spcBuffers = hash_create("Number of buffers to write per tablespace",
+ 16, &ctl, HASH_ELEM | HASH_BLOBS);
+ }
+
for (buf_id = 0; buf_id < NBuffers; buf_id++)
{
volatile BufferDesc *bufHdr = GetBufferDescriptor(buf_id);
@@ -1621,32 +1763,99 @@ BufferSync(int flags)
if ((bufHdr->flags & mask) == mask)
{
+ Oid spc;
+ TableSpaceCountEntry * entry;
+ bool found;
+
bufHdr->flags |= BM_CHECKPOINT_NEEDED;
+ CheckpointBufferIds[num_to_write].buf_id = buf_id;
+ CheckpointBufferIds[num_to_write].relNode = bufHdr->tag.rnode.relNode;
+ CheckpointBufferIds[num_to_write].forkNum = bufHdr->tag.forkNum;
+ CheckpointBufferIds[num_to_write].blockNum = bufHdr->tag.blockNum;
num_to_write++;
+
+ /* keep track of per tablespace buffers */
+ spc = bufHdr->tag.rnode.spcNode;
+ entry = (TableSpaceCountEntry *)
+ hash_search(spcBuffers, (void *) &spc, HASH_ENTER, &found);
+
+ if (found)
+ entry->count++;
+ else
+ entry->count = 1;
}
UnlockBufHdr(bufHdr);
}
if (num_to_write == 0)
+ {
+ hash_destroy(spcBuffers);
return; /* nothing to do */
+ }
TRACE_POSTGRESQL_BUFFER_SYNC_START(NBuffers, num_to_write);
+ /* Build checkpoint tablespace buffer status */
+ nb_spaces = hash_get_num_entries(spcBuffers);
+ spcStatus = (TableSpaceCheckpointStatus *)
+ palloc(sizeof(TableSpaceCheckpointStatus) * nb_spaces);
+
+ {
+ int index = 0;
+ HASH_SEQ_STATUS hseq;
+ TableSpaceCountEntry * entry;
+
+ hash_seq_init(&hseq, spcBuffers);
+ while ((entry = (TableSpaceCountEntry *) hash_seq_search(&hseq)))
+ {
+ Assert(index < nb_spaces);
+ spcStatus[index].space = entry->space;
+ spcStatus[index].num_to_write = entry->count;
+ spcStatus[index].num_written = 0;
+ /* should it be randomized? chosen with some criterion? */
+ spcStatus[index].index = 0;
+
+ index ++;
+ }
+ }
+
+ hash_destroy(spcBuffers);
+ spcBuffers = NULL;
+
+ /* sort buffer ids to help find sequential writes */
+ CheckpointStats.ckpt_sort_t = GetCurrentTimestamp();
+
+ if (checkpoint_sort)
+ {
+ qsort(CheckpointBufferIds, num_to_write, sizeof(CheckpointSortItem),
+ bufcmp);
+ }
+
+ CheckpointStats.ckpt_sort_end_t = GetCurrentTimestamp();
+
/*
- * Loop over all buffers again, and write the ones (still) marked with
- * BM_CHECKPOINT_NEEDED. In this loop, we start at the clock sweep point
- * since we might as well dump soon-to-be-recycled buffers first.
+ * Loop over buffers to write through CheckpointBufferIds,
+ * and write the ones (still) marked with BM_CHECKPOINT_NEEDED,
+ * with some round robin over table spaces so as to balance writes,
+ * so that buffer writes move forward roughly proportionally for each
+ * tablespace.
*
- * Note that we don't read the buffer alloc count here --- that should be
- * left untouched till the next BgBufferSync() call.
+ * Termination: if a tablespace is selected by the inner while loop
+ * (see argument there), its index is incremented and will eventually
+ * reach num_to_write, mark this table space scanning as done and
+ * decrement the number of (active) spaces, which will thus reach 0.
*/
- buf_id = StrategySyncStart(NULL, NULL);
- num_to_scan = NBuffers;
+ space = 0;
num_written = 0;
- while (num_to_scan-- > 0)
+
+ while (nb_spaces != 0)
{
- volatile BufferDesc *bufHdr = GetBufferDescriptor(buf_id);
+ volatile BufferDesc *bufHdr = NULL;
+ buf_id = NextBufferToWrite(spcStatus, nb_spaces, &space,
+ num_to_write, num_written);
+ if (buf_id != -1)
+ bufHdr = GetBufferDescriptor(buf_id);
/*
* We don't need to acquire the lock here, because we're only looking
@@ -1660,39 +1869,46 @@ BufferSync(int flags)
* write the buffer though we didn't need to. It doesn't seem worth
* guarding against this, though.
*/
- if (bufHdr->flags & BM_CHECKPOINT_NEEDED)
+ if (bufHdr != NULL && bufHdr->flags & BM_CHECKPOINT_NEEDED)
{
if (SyncOneBuffer(buf_id, false) & BUF_WRITTEN)
{
TRACE_POSTGRESQL_BUFFER_SYNC_WRITTEN(buf_id);
BgWriterStats.m_buf_written_checkpoints++;
+ spcStatus[space].num_written++;
num_written++;
/*
- * We know there are at most num_to_write buffers with
- * BM_CHECKPOINT_NEEDED set; so we can stop scanning if
- * num_written reaches num_to_write.
- *
- * Note that num_written doesn't include buffers written by
- * other backends, or by the bgwriter cleaning scan. That
- * means that the estimate of how much progress we've made is
- * conservative, and also that this test will often fail to
- * trigger. But it seems worth making anyway.
- */
- if (num_written >= num_to_write)
- break;
-
- /*
* Sleep to throttle our I/O rate.
*/
CheckpointWriteDelay(flags, (double) num_written / num_to_write);
}
}
- if (++buf_id >= NBuffers)
- buf_id = 0;
+ /*
+ * Detect checkpoint end for a tablespace: either the scan is done
+ * or all tablespace buffers have been written out. If so, the
+ * another active tablespace status is moved in place of the current
+ * one and the next round will start on this one, or maybe round about.
+ *
+ * Note: maybe an exchange could be made instead in order to keep
+ * informations about the closed table space, but this is currently
+ * not used afterwards.
+ */
+ if (spcStatus[space].index >= num_to_write ||
+ spcStatus[space].num_written >= spcStatus[space].num_to_write)
+ {
+ nb_spaces--;
+ if (space != nb_spaces)
+ spcStatus[space] = spcStatus[nb_spaces];
+ else
+ space = 0;
+ }
}
+ pfree(spcStatus);
+ spcStatus = NULL;
+
/*
* Update checkpoint statistics. As noted above, this doesn't include
* buffers written by other backends or bgwriter scan.
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index b3dac51..cf1e505 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -1013,6 +1013,17 @@ static struct config_bool ConfigureNamesBool[] =
false,
NULL, NULL, NULL
},
+
+ {
+ {"checkpoint_sort", PGC_SIGHUP, WAL_CHECKPOINTS,
+ gettext_noop("Whether disk-page buffers are sorted on checkpoints."),
+ NULL
+ },
+ &checkpoint_sort,
+ true,
+ NULL, NULL, NULL
+ },
+
{
{"log_connections", PGC_SU_BACKEND, LOGGING_WHAT,
gettext_noop("Logs each successful connection."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 695a88f..d4dfc25 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -201,6 +201,7 @@
#max_wal_size = 1GB
#min_wal_size = 80MB
#checkpoint_completion_target = 0.5 # checkpoint target duration, 0.0 - 1.0
+#checkpoint_sort = on # sort buffers on checkpoint
#checkpoint_warning = 30s # 0 disables
# - Archiving -
diff --git a/src/include/access/xlog.h b/src/include/access/xlog.h
index 6dacee2..dbd4757 100644
--- a/src/include/access/xlog.h
+++ b/src/include/access/xlog.h
@@ -186,6 +186,8 @@ extern bool XLOG_DEBUG;
typedef struct CheckpointStatsData
{
TimestampTz ckpt_start_t; /* start of checkpoint */
+ TimestampTz ckpt_sort_t; /* start buffer sorting */
+ TimestampTz ckpt_sort_end_t; /* end of sorting */
TimestampTz ckpt_write_t; /* start of flushing buffers */
TimestampTz ckpt_sync_t; /* start of fsyncs */
TimestampTz ckpt_sync_end_t; /* end of fsyncs */
diff --git a/src/include/storage/buf_internals.h b/src/include/storage/buf_internals.h
index 521ee1c..32f2006 100644
--- a/src/include/storage/buf_internals.h
+++ b/src/include/storage/buf_internals.h
@@ -210,6 +210,23 @@ extern PGDLLIMPORT BufferDescPadded *BufferDescriptors;
/* in localbuf.c */
extern BufferDesc *LocalBufferDescriptors;
+/* in bufmgr.c */
+
+/*
+ * Structure to sort buffers per file on checkpoints.
+ *
+ * This structure is allocated per buffer in shared memory, so it should be
+ * kept as little as possible. Maybe the sort criterion could be compacted
+ * to reduce memory requirement and for faster comparison?
+ */
+typedef struct CheckpointSortItem {
+ int buf_id;
+ Oid relNode;
+ ForkNumber forkNum; /* hm... enum with only 4 values */
+ BlockNumber blockNum;
+} CheckpointSortItem;
+
+extern CheckpointSortItem *CheckpointBufferIds;
/*
* Internal routines: only called by bufmgr
diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.h
index ec0a254..c228f39 100644
--- a/src/include/storage/bufmgr.h
+++ b/src/include/storage/bufmgr.h
@@ -54,6 +54,7 @@ extern int bgwriter_lru_maxpages;
extern double bgwriter_lru_multiplier;
extern bool track_io_timing;
extern int target_prefetch_pages;
+extern bool checkpoint_sort;
/* in buf_init.c */
extern PGDLLIMPORT char *BufferBlocks;
checkpoint-continuous-flush-11b.patchtext/x-diff; name=checkpoint-continuous-flush-11b.patchDownload
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 96c9a2f..927294b 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -2497,6 +2497,24 @@ include_dir 'conf.d'
</listitem>
</varlistentry>
+ <varlistentry id="guc-checkpoint-flush-to-disk" xreflabel="checkpoint_flush_to_disk">
+ <term><varname>checkpoint_flush_to_disk</varname> (<type>bool</type>)
+ <indexterm>
+ <primary><varname>checkpoint_flush_to_disk</> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ When writing data for a checkpoint, hint the underlying OS that the
+ data must be sent to disk as soon as possible. This may help smoothing
+ disk I/O writes and avoid a stall when fsync is issued at the end of
+ the checkpoint, but it may also reduce average performance.
+ This setting may have no effect on some platforms.
+ The default is <literal>on</> on Linux, <literal>off</> otherwise.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="guc-min-wal-size" xreflabel="min_wal_size">
<term><varname>min_wal_size</varname> (<type>integer</type>)
<indexterm>
diff --git a/doc/src/sgml/wal.sgml b/doc/src/sgml/wal.sgml
index f538698..1b658f2 100644
--- a/doc/src/sgml/wal.sgml
+++ b/doc/src/sgml/wal.sgml
@@ -558,6 +558,18 @@
</para>
<para>
+ On Linux and POSIX platforms, <xref linkend="guc-checkpoint-flush-to-disk">
+ allows to hint the OS that pages written on checkpoints must be flushed
+ to disk quickly. Otherwise, these pages may be kept in cache for some time,
+ inducing a stall later when <literal>fsync</> is called to actually
+ complete the checkpoint. This setting helps to reduce transaction latency,
+ but it may also have a small adverse effect on the average transaction rate
+ at maximum throughput on some OS. It should be beneficial for high write
+ loads on HDD. This feature probably brings no benefit on SSD, as the I/O
+ write latency is small on such hardware, thus it may be disabled.
+ </para>
+
+ <para>
The number of WAL segment files in <filename>pg_xlog</> directory depends on
<varname>min_wal_size</>, <varname>max_wal_size</> and
the amount of WAL generated in previous checkpoint cycles. When old log
diff --git a/src/backend/access/heap/rewriteheap.c b/src/backend/access/heap/rewriteheap.c
index 6a6fc3b..2a8f645 100644
--- a/src/backend/access/heap/rewriteheap.c
+++ b/src/backend/access/heap/rewriteheap.c
@@ -918,7 +918,7 @@ logical_heap_rewrite_flush_mappings(RewriteState state)
* Note that we deviate from the usual WAL coding practices here,
* check the above "Logical rewrite support" comment for reasoning.
*/
- written = FileWrite(src->vfd, waldata_start, len);
+ written = FileWrite(src->vfd, waldata_start, len, false, NULL);
if (written != len)
ereport(ERROR,
(errcode_for_file_access(),
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index cf4a6dc..4b5e9cd 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -203,7 +203,7 @@ btbuildempty(PG_FUNCTION_ARGS)
/* Write the page. If archiving/streaming, XLOG it. */
PageSetChecksumInplace(metapage, BTREE_METAPAGE);
smgrwrite(index->rd_smgr, INIT_FORKNUM, BTREE_METAPAGE,
- (char *) metapage, true);
+ (char *) metapage, true, false, NULL);
if (XLogIsNeeded())
log_newpage(&index->rd_smgr->smgr_rnode.node, INIT_FORKNUM,
BTREE_METAPAGE, metapage, false);
diff --git a/src/backend/access/nbtree/nbtsort.c b/src/backend/access/nbtree/nbtsort.c
index f95f67a..ea7a45d 100644
--- a/src/backend/access/nbtree/nbtsort.c
+++ b/src/backend/access/nbtree/nbtsort.c
@@ -315,7 +315,7 @@ _bt_blwritepage(BTWriteState *wstate, Page page, BlockNumber blkno)
{
/* overwriting a block we zero-filled before */
smgrwrite(wstate->index->rd_smgr, MAIN_FORKNUM, blkno,
- (char *) page, true);
+ (char *) page, true, false, NULL);
}
pfree(page);
diff --git a/src/backend/access/spgist/spginsert.c b/src/backend/access/spgist/spginsert.c
index bceee8d..b700efb 100644
--- a/src/backend/access/spgist/spginsert.c
+++ b/src/backend/access/spgist/spginsert.c
@@ -170,7 +170,7 @@ spgbuildempty(PG_FUNCTION_ARGS)
/* Write the page. If archiving/streaming, XLOG it. */
PageSetChecksumInplace(page, SPGIST_METAPAGE_BLKNO);
smgrwrite(index->rd_smgr, INIT_FORKNUM, SPGIST_METAPAGE_BLKNO,
- (char *) page, true);
+ (char *) page, true, false, NULL);
if (XLogIsNeeded())
log_newpage(&index->rd_smgr->smgr_rnode.node, INIT_FORKNUM,
SPGIST_METAPAGE_BLKNO, page, false);
@@ -180,7 +180,7 @@ spgbuildempty(PG_FUNCTION_ARGS)
PageSetChecksumInplace(page, SPGIST_ROOT_BLKNO);
smgrwrite(index->rd_smgr, INIT_FORKNUM, SPGIST_ROOT_BLKNO,
- (char *) page, true);
+ (char *) page, true, false, NULL);
if (XLogIsNeeded())
log_newpage(&index->rd_smgr->smgr_rnode.node, INIT_FORKNUM,
SPGIST_ROOT_BLKNO, page, true);
@@ -190,7 +190,7 @@ spgbuildempty(PG_FUNCTION_ARGS)
PageSetChecksumInplace(page, SPGIST_NULL_BLKNO);
smgrwrite(index->rd_smgr, INIT_FORKNUM, SPGIST_NULL_BLKNO,
- (char *) page, true);
+ (char *) page, true, false, NULL);
if (XLogIsNeeded())
log_newpage(&index->rd_smgr->smgr_rnode.node, INIT_FORKNUM,
SPGIST_NULL_BLKNO, page, true);
diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c
index 3b3a09e..e361907 100644
--- a/src/backend/postmaster/checkpointer.c
+++ b/src/backend/postmaster/checkpointer.c
@@ -665,7 +665,8 @@ ImmediateCheckpointRequested(void)
* fraction between 0.0 meaning none, and 1.0 meaning all done.
*/
void
-CheckpointWriteDelay(int flags, double progress)
+CheckpointWriteDelay(int flags, double progress,
+ FileFlushContext * context, int ctx_size)
{
static int absorb_counter = WRITES_PER_ABSORB;
@@ -700,6 +701,26 @@ CheckpointWriteDelay(int flags, double progress)
*/
pgstat_send_bgwriter();
+#if defined(HAVE_SYNC_FILE_RANGE) || defined(HAVE_POSIX_FADVISE)
+
+ /*
+ * Before sleeping, flush written blocks for each tablespace.
+ */
+ if (checkpoint_flush_to_disk)
+ {
+ int i;
+
+ for (i = 0; i < ctx_size; i++)
+ {
+ if (context[i].ncalls != 0)
+ {
+ PerformFileFlush(&context[i]);
+ ResetFileFlushContext(&context[i]);
+ }
+ }
+ }
+#endif /* HAVE_SYNC_FILE_RANGE || HAVE_POSIX_FADVISE */
+
/*
* This sleep used to be connected to bgwriter_delay, typically 200ms.
* That resulted in more frequent wakeups if not much work to do.
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index cc951e1..9da996e 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -80,6 +80,8 @@ bool zero_damaged_pages = false;
int bgwriter_lru_maxpages = 100;
double bgwriter_lru_multiplier = 2.0;
bool track_io_timing = false;
+/* hint to move writes to high priority */
+bool checkpoint_flush_to_disk = DEFAULT_CHECKPOINT_FLUSH_TO_DISK;
bool checkpoint_sort = true;
/*
@@ -400,7 +402,8 @@ static bool PinBuffer(volatile BufferDesc *buf, BufferAccessStrategy strategy);
static void PinBuffer_Locked(volatile BufferDesc *buf);
static void UnpinBuffer(volatile BufferDesc *buf, bool fixOwner);
static void BufferSync(int flags);
-static int SyncOneBuffer(int buf_id, bool skip_recently_used);
+static int SyncOneBuffer(int buf_id, bool skip_recently_used,
+ bool flush_to_disk, FileFlushContext *context);
static void WaitIO(volatile BufferDesc *buf);
static bool StartBufferIO(volatile BufferDesc *buf, bool forInput);
static void TerminateBufferIO(volatile BufferDesc *buf, bool clear_dirty,
@@ -413,7 +416,8 @@ static volatile BufferDesc *BufferAlloc(SMgrRelation smgr,
BlockNumber blockNum,
BufferAccessStrategy strategy,
bool *foundPtr);
-static void FlushBuffer(volatile BufferDesc *buf, SMgrRelation reln);
+static void FlushBuffer(volatile BufferDesc *buf, SMgrRelation reln,
+ bool flush_to_disk, FileFlushContext *context);
static void AtProcExit_Buffers(int code, Datum arg);
static void CheckForBufferLeaks(void);
static int rnode_comparator(const void *p1, const void *p2);
@@ -1022,7 +1026,7 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
smgr->smgr_rnode.node.dbNode,
smgr->smgr_rnode.node.relNode);
- FlushBuffer(buf, NULL);
+ FlushBuffer(buf, NULL, false, NULL);
LWLockRelease(buf->content_lock);
TRACE_POSTGRESQL_BUFFER_WRITE_DIRTY_DONE(forkNum, blockNum,
@@ -1709,6 +1713,7 @@ BufferSync(int flags)
HTAB *spcBuffers;
TableSpaceCheckpointStatus *spcStatus = NULL;
int nb_spaces, space;
+ FileFlushContext * spcContext = NULL;
/* Make sure we can handle the pin inside SyncOneBuffer */
ResourceOwnerEnlargeBuffers(CurrentResourceOwner);
@@ -1796,10 +1801,12 @@ BufferSync(int flags)
TRACE_POSTGRESQL_BUFFER_SYNC_START(NBuffers, num_to_write);
- /* Build checkpoint tablespace buffer status */
+ /* Build checkpoint tablespace buffer status & flush context arrays */
nb_spaces = hash_get_num_entries(spcBuffers);
spcStatus = (TableSpaceCheckpointStatus *)
palloc(sizeof(TableSpaceCheckpointStatus) * nb_spaces);
+ spcContext = (FileFlushContext *)
+ palloc(sizeof(FileFlushContext) * nb_spaces);
{
int index = 0;
@@ -1816,6 +1823,12 @@ BufferSync(int flags)
/* should it be randomized? chosen with some criterion? */
spcStatus[index].index = 0;
+#if defined(HAVE_SYNC_FILE_RANGE) || defined(HAVE_POSIX_FADVISE)
+
+ ResetFileFlushContext(&spcContext[index]);
+
+#endif /* HAVE_SYNC_FILE_RANGE || HAVE_POSIX_FADVISE */
+
index ++;
}
}
@@ -1871,7 +1884,8 @@ BufferSync(int flags)
*/
if (bufHdr != NULL && bufHdr->flags & BM_CHECKPOINT_NEEDED)
{
- if (SyncOneBuffer(buf_id, false) & BUF_WRITTEN)
+ if (SyncOneBuffer(buf_id, false, checkpoint_flush_to_disk,
+ &spcContext[space]) & BUF_WRITTEN)
{
TRACE_POSTGRESQL_BUFFER_SYNC_WRITTEN(buf_id);
BgWriterStats.m_buf_written_checkpoints++;
@@ -1881,7 +1895,8 @@ BufferSync(int flags)
/*
* Sleep to throttle our I/O rate.
*/
- CheckpointWriteDelay(flags, (double) num_written / num_to_write);
+ CheckpointWriteDelay(flags, (double) num_written / num_to_write,
+ spcContext, nb_spaces);
}
}
@@ -1898,6 +1913,13 @@ BufferSync(int flags)
if (spcStatus[space].index >= num_to_write ||
spcStatus[space].num_written >= spcStatus[space].num_to_write)
{
+#if defined(HAVE_SYNC_FILE_RANGE) || defined(HAVE_POSIX_FADVISE)
+
+ PerformFileFlush(&spcContext[space]);
+ ResetFileFlushContext(&spcContext[space]);
+
+#endif /* HAVE_SYNC_FILE_RANGE || HAVE_POSIX_FADVISE */
+
nb_spaces--;
if (space != nb_spaces)
spcStatus[space] = spcStatus[nb_spaces];
@@ -1908,6 +1930,8 @@ BufferSync(int flags)
pfree(spcStatus);
spcStatus = NULL;
+ pfree(spcContext);
+ spcContext = NULL;
/*
* Update checkpoint statistics. As noted above, this doesn't include
@@ -2155,7 +2179,7 @@ BgBufferSync(void)
/* Execute the LRU scan */
while (num_to_scan > 0 && reusable_buffers < upcoming_alloc_est)
{
- int buffer_state = SyncOneBuffer(next_to_clean, true);
+ int buffer_state = SyncOneBuffer(next_to_clean, true, false, NULL);
if (++next_to_clean >= NBuffers)
{
@@ -2232,7 +2256,8 @@ BgBufferSync(void)
* Note: caller must have done ResourceOwnerEnlargeBuffers.
*/
static int
-SyncOneBuffer(int buf_id, bool skip_recently_used)
+SyncOneBuffer(int buf_id, bool skip_recently_used, bool flush_to_disk,
+ FileFlushContext * context)
{
volatile BufferDesc *bufHdr = GetBufferDescriptor(buf_id);
int result = 0;
@@ -2273,7 +2298,7 @@ SyncOneBuffer(int buf_id, bool skip_recently_used)
PinBuffer_Locked(bufHdr);
LWLockAcquire(bufHdr->content_lock, LW_SHARED);
- FlushBuffer(bufHdr, NULL);
+ FlushBuffer(bufHdr, NULL, flush_to_disk, context);
LWLockRelease(bufHdr->content_lock);
UnpinBuffer(bufHdr, true);
@@ -2535,9 +2560,16 @@ BufferGetTag(Buffer buffer, RelFileNode *rnode, ForkNumber *forknum,
*
* If the caller has an smgr reference for the buffer's relation, pass it
* as the second parameter. If not, pass NULL.
+ *
+ * The third parameter tries to hint the OS that a high priority write is meant,
+ * possibly because io-throttling is already managed elsewhere.
+ * The last parameter holds the current flush context that accumulates flush
+ * requests to be performed in one call, instead of being performed on a buffer
+ * per buffer basis.
*/
static void
-FlushBuffer(volatile BufferDesc *buf, SMgrRelation reln)
+FlushBuffer(volatile BufferDesc *buf, SMgrRelation reln, bool flush_to_disk,
+ FileFlushContext * context)
{
XLogRecPtr recptr;
ErrorContextCallback errcallback;
@@ -2626,7 +2658,9 @@ FlushBuffer(volatile BufferDesc *buf, SMgrRelation reln)
buf->tag.forkNum,
buf->tag.blockNum,
bufToWrite,
- false);
+ false,
+ flush_to_disk,
+ context);
if (track_io_timing)
{
@@ -3048,7 +3082,9 @@ FlushRelationBuffers(Relation rel)
bufHdr->tag.forkNum,
bufHdr->tag.blockNum,
localpage,
- false);
+ false,
+ false,
+ NULL);
bufHdr->flags &= ~(BM_DIRTY | BM_JUST_DIRTIED);
@@ -3082,7 +3118,7 @@ FlushRelationBuffers(Relation rel)
{
PinBuffer_Locked(bufHdr);
LWLockAcquire(bufHdr->content_lock, LW_SHARED);
- FlushBuffer(bufHdr, rel->rd_smgr);
+ FlushBuffer(bufHdr, rel->rd_smgr, false, NULL);
LWLockRelease(bufHdr->content_lock);
UnpinBuffer(bufHdr, true);
}
@@ -3134,7 +3170,7 @@ FlushDatabaseBuffers(Oid dbid)
{
PinBuffer_Locked(bufHdr);
LWLockAcquire(bufHdr->content_lock, LW_SHARED);
- FlushBuffer(bufHdr, NULL);
+ FlushBuffer(bufHdr, NULL, false, NULL);
LWLockRelease(bufHdr->content_lock);
UnpinBuffer(bufHdr, true);
}
diff --git a/src/backend/storage/buffer/localbuf.c b/src/backend/storage/buffer/localbuf.c
index 3144afe..114a0a6 100644
--- a/src/backend/storage/buffer/localbuf.c
+++ b/src/backend/storage/buffer/localbuf.c
@@ -208,7 +208,9 @@ LocalBufferAlloc(SMgrRelation smgr, ForkNumber forkNum, BlockNumber blockNum,
bufHdr->tag.forkNum,
bufHdr->tag.blockNum,
localpage,
- false);
+ false,
+ false,
+ NULL);
/* Mark not-dirty now in case we error out below */
bufHdr->flags &= ~BM_DIRTY;
diff --git a/src/backend/storage/file/buffile.c b/src/backend/storage/file/buffile.c
index ea4d689..fb3b383 100644
--- a/src/backend/storage/file/buffile.c
+++ b/src/backend/storage/file/buffile.c
@@ -317,7 +317,7 @@ BufFileDumpBuffer(BufFile *file)
return; /* seek failed, give up */
file->offsets[file->curFile] = file->curOffset;
}
- bytestowrite = FileWrite(thisfile, file->buffer + wpos, bytestowrite);
+ bytestowrite = FileWrite(thisfile, file->buffer + wpos, bytestowrite, false, NULL);
if (bytestowrite <= 0)
return; /* failed to write */
file->offsets[file->curFile] += bytestowrite;
diff --git a/src/backend/storage/file/fd.c b/src/backend/storage/file/fd.c
index 1ba4946..e880a9e 100644
--- a/src/backend/storage/file/fd.c
+++ b/src/backend/storage/file/fd.c
@@ -1344,8 +1344,97 @@ retry:
return returnCode;
}
+#if defined(HAVE_SYNC_FILE_RANGE) || defined(HAVE_POSIX_FADVISE)
+
+void
+ResetFileFlushContext(FileFlushContext * context)
+{
+ context->fd = 0;
+ context->ncalls = 0;
+ context->offset = 0;
+ context->nbytes = 0;
+ context->filename = NULL;
+}
+
+void
+PerformFileFlush(FileFlushContext * context)
+{
+ if (context->ncalls != 0)
+ {
+ int rc;
+
+#if defined(HAVE_SYNC_FILE_RANGE)
+
+ /*
+ * Linux: tell the memory manager to move these blocks to io so
+ * that they are considered for being actually written to disk.
+ */
+ rc = sync_file_range(context->fd, context->offset, context->nbytes,
+ SYNC_FILE_RANGE_WRITE);
+
+#elif defined(HAVE_POSIX_FADVISE)
+
+ /*
+ * Others: say that data should not be kept in memory...
+ * This is not exactly what we want to say, because we want to write
+ * the data for durability but we may need it later nevertheless.
+ * It seems that Linux would free the memory *if* the data has
+ * already been written do disk, else the "dontneed" call is ignored.
+ * For FreeBSD this may have the desired effect of moving the
+ * data to the io layer, although the system does not seem to
+ * take into account the provided offset & size, so it is rather
+ * rough...
+ */
+ rc = posix_fadvise(context->fd, context->offset, context->nbytes,
+ POSIX_FADV_DONTNEED);
+
+#endif
+
+ if (rc < 0)
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not flush block " INT64_FORMAT
+ " on " INT64_FORMAT " blocks in file \"%s\": %m",
+ context->offset / BLCKSZ,
+ context->nbytes / BLCKSZ,
+ context->filename)));
+ }
+}
+
+void
+FileAsynchronousFlush(FileFlushContext * context,
+ int fd, off_t offset, off_t nbytes, char * filename)
+{
+ if (context->ncalls != 0 && context->fd == fd)
+ {
+ /* same file: merge current flush with previous ones */
+ off_t new_offset = offset < context->offset? offset: context->offset;
+
+ context->nbytes =
+ (context->offset + context->nbytes > offset + nbytes ?
+ context->offset + context->nbytes : offset + nbytes) -
+ new_offset;
+ context->offset = new_offset;
+ context->ncalls ++;
+ }
+ else
+ {
+ /* other file: do flush previous file & reset flush accumulator */
+ PerformFileFlush(context);
+
+ context->fd = fd;
+ context->ncalls = 1;
+ context->offset = offset;
+ context->nbytes = nbytes;
+ context->filename = filename;
+ }
+}
+
+#endif /* HAVE_SYNC_FILE_RANGE || HAVE_POSIX_FADVISE */
+
int
-FileWrite(File file, char *buffer, int amount)
+FileWrite(File file, char *buffer, int amount, bool flush_to_disk,
+ FileFlushContext * context)
{
int returnCode;
@@ -1395,6 +1484,28 @@ retry:
if (returnCode >= 0)
{
+
+#if defined(HAVE_SYNC_FILE_RANGE) || defined(HAVE_POSIX_FADVISE)
+
+ /*
+ * Calling "write" tells the OS that pg wants to write some page to disk,
+ * however when it is really done is chosen by the OS.
+ * Depending on other disk activities this may be delayed significantly,
+ * maybe up to an "fsync" call, which could induce an IO write surge.
+ * When checkpointing pg is doing its own throttling and the result
+ * should really be written to disk with high priority, so as to meet
+ * the completion target.
+ * This call hints that such write have a higher priority.
+ */
+ if (flush_to_disk && returnCode == amount && errno == 0)
+ {
+ FileAsynchronousFlush(context,
+ VfdCache[file].fd, VfdCache[file].seekPos,
+ amount, VfdCache[file].fileName);
+ }
+
+#endif /* HAVE_SYNC_FILE_RANGE || HAVE_POSIX_FADVISE */
+
VfdCache[file].seekPos += returnCode;
/* maintain fileSize and temporary_files_size if it's a temp file */
diff --git a/src/backend/storage/smgr/md.c b/src/backend/storage/smgr/md.c
index 42a43bb..dbf057f 100644
--- a/src/backend/storage/smgr/md.c
+++ b/src/backend/storage/smgr/md.c
@@ -531,7 +531,7 @@ mdextend(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
errmsg("could not seek to block %u in file \"%s\": %m",
blocknum, FilePathName(v->mdfd_vfd))));
- if ((nbytes = FileWrite(v->mdfd_vfd, buffer, BLCKSZ)) != BLCKSZ)
+ if ((nbytes = FileWrite(v->mdfd_vfd, buffer, BLCKSZ, false, NULL)) != BLCKSZ)
{
if (nbytes < 0)
ereport(ERROR,
@@ -738,7 +738,8 @@ mdread(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
*/
void
mdwrite(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
- char *buffer, bool skipFsync)
+ char *buffer, bool skipFsync, bool flush_to_disk,
+ FileFlushContext * context)
{
off_t seekpos;
int nbytes;
@@ -767,7 +768,7 @@ mdwrite(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
errmsg("could not seek to block %u in file \"%s\": %m",
blocknum, FilePathName(v->mdfd_vfd))));
- nbytes = FileWrite(v->mdfd_vfd, buffer, BLCKSZ);
+ nbytes = FileWrite(v->mdfd_vfd, buffer, BLCKSZ, flush_to_disk, context);
TRACE_POSTGRESQL_SMGR_MD_WRITE_DONE(forknum, blocknum,
reln->smgr_rnode.node.spcNode,
diff --git a/src/backend/storage/smgr/smgr.c b/src/backend/storage/smgr/smgr.c
index 244b4ea..2db3cd3 100644
--- a/src/backend/storage/smgr/smgr.c
+++ b/src/backend/storage/smgr/smgr.c
@@ -52,7 +52,8 @@ typedef struct f_smgr
void (*smgr_read) (SMgrRelation reln, ForkNumber forknum,
BlockNumber blocknum, char *buffer);
void (*smgr_write) (SMgrRelation reln, ForkNumber forknum,
- BlockNumber blocknum, char *buffer, bool skipFsync);
+ BlockNumber blocknum, char *buffer, bool skipFsync,
+ bool flush_to_disk, FileFlushContext *context);
BlockNumber (*smgr_nblocks) (SMgrRelation reln, ForkNumber forknum);
void (*smgr_truncate) (SMgrRelation reln, ForkNumber forknum,
BlockNumber nblocks);
@@ -643,10 +644,11 @@ smgrread(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
*/
void
smgrwrite(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
- char *buffer, bool skipFsync)
+ char *buffer, bool skipFsync, bool flush_to_disk,
+ FileFlushContext * context)
{
(*(smgrsw[reln->smgr_which].smgr_write)) (reln, forknum, blocknum,
- buffer, skipFsync);
+ buffer, skipFsync, flush_to_disk, context);
}
/*
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index cf1e505..9219330 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -158,6 +158,7 @@ static bool check_bonjour(bool *newval, void **extra, GucSource source);
static bool check_ssl(bool *newval, void **extra, GucSource source);
static bool check_stage_log_stats(bool *newval, void **extra, GucSource source);
static bool check_log_stats(bool *newval, void **extra, GucSource source);
+static bool check_flush_to_disk(bool *newval, void **extra, GucSource source);
static bool check_canonical_path(char **newval, void **extra, GucSource source);
static bool check_timezone_abbreviations(char **newval, void **extra, GucSource source);
static void assign_timezone_abbreviations(const char *newval, void *extra);
@@ -1025,6 +1026,17 @@ static struct config_bool ConfigureNamesBool[] =
},
{
+ {"checkpoint_flush_to_disk", PGC_SIGHUP, WAL_CHECKPOINTS,
+ gettext_noop("Hint that checkpoint's writes are high priority."),
+ NULL
+ },
+ &checkpoint_flush_to_disk,
+ /* see bufmgr.h: true on Linux, false otherwise */
+ DEFAULT_CHECKPOINT_FLUSH_TO_DISK,
+ check_flush_to_disk, NULL, NULL
+ },
+
+ {
{"log_connections", PGC_SU_BACKEND, LOGGING_WHAT,
gettext_noop("Logs each successful connection."),
NULL
@@ -9806,6 +9818,21 @@ check_log_stats(bool *newval, void **extra, GucSource source)
}
static bool
+check_flush_to_disk(bool *newval, void **extra, GucSource source)
+{
+/* This test must be consistent with the one in FileWrite (storage/file/fd.c)
+ */
+#if ! (defined(HAVE_SYNC_FILE_RANGE) || defined(HAVE_POSIX_FADVISE))
+ /* just warn if it has no effect */
+ ereport(WARNING,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("Setting \"checkpoint_flush_to_disk\" has no effect "
+ "on this platform.")));
+#endif /* ! (HAVE_SYNC_FILE_RANGE || HAVE_POSIX_FADVISE) */
+ return true;
+}
+
+static bool
check_canonical_path(char **newval, void **extra, GucSource source)
{
/*
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index d4dfc25..01b1c96 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -202,6 +202,8 @@
#min_wal_size = 80MB
#checkpoint_completion_target = 0.5 # checkpoint target duration, 0.0 - 1.0
#checkpoint_sort = on # sort buffers on checkpoint
+#checkpoint_flush_to_disk = ? # send buffers to disk on checkpoint
+ # default is on if Linux, off otherwise
#checkpoint_warning = 30s # 0 disables
# - Archiving -
diff --git a/src/include/postmaster/bgwriter.h b/src/include/postmaster/bgwriter.h
index a49c208..f9c8ca1 100644
--- a/src/include/postmaster/bgwriter.h
+++ b/src/include/postmaster/bgwriter.h
@@ -16,6 +16,7 @@
#define _BGWRITER_H
#include "storage/block.h"
+#include "storage/fd.h"
#include "storage/relfilenode.h"
@@ -29,7 +30,8 @@ extern void BackgroundWriterMain(void) pg_attribute_noreturn();
extern void CheckpointerMain(void) pg_attribute_noreturn();
extern void RequestCheckpoint(int flags);
-extern void CheckpointWriteDelay(int flags, double progress);
+extern void CheckpointWriteDelay(int flags, double progress,
+ FileFlushContext * context, int ctx_size);
extern bool ForwardFsyncRequest(RelFileNode rnode, ForkNumber forknum,
BlockNumber segno);
diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.h
index c228f39..4fd3ff5 100644
--- a/src/include/storage/bufmgr.h
+++ b/src/include/storage/bufmgr.h
@@ -54,6 +54,14 @@ extern int bgwriter_lru_maxpages;
extern double bgwriter_lru_multiplier;
extern bool track_io_timing;
extern int target_prefetch_pages;
+
+#ifdef HAVE_SYNC_FILE_RANGE
+#define DEFAULT_CHECKPOINT_FLUSH_TO_DISK true
+#else
+#define DEFAULT_CHECKPOINT_FLUSH_TO_DISK false
+#endif /* HAVE_SYNC_FILE_RANGE */
+
+extern bool checkpoint_flush_to_disk;
extern bool checkpoint_sort;
/* in buf_init.c */
diff --git a/src/include/storage/fd.h b/src/include/storage/fd.h
index 7eabe09..c7b2a6d 100644
--- a/src/include/storage/fd.h
+++ b/src/include/storage/fd.h
@@ -59,6 +59,24 @@ extern int max_files_per_process;
*/
extern int max_safe_fds;
+/*
+ * FileFlushContext structure:
+ *
+ * This is used to accumulate several flush requests on a file
+ * into a larger flush request.
+ * - fd: file descriptor of the file
+ * - ncalls: number of flushes merged together
+ * - offset: starting offset (minimum of all offsets)
+ * - nbytes: size (minimum extent to cover all flushed data)
+ * - filename: filename of fd for error messages
+ */
+typedef struct FileFlushContext {
+ int fd;
+ int ncalls;
+ off_t offset;
+ off_t nbytes;
+ char * filename;
+} FileFlushContext;
/*
* prototypes for functions in fd.c
@@ -70,7 +88,12 @@ extern File OpenTemporaryFile(bool interXact);
extern void FileClose(File file);
extern int FilePrefetch(File file, off_t offset, int amount);
extern int FileRead(File file, char *buffer, int amount);
-extern int FileWrite(File file, char *buffer, int amount);
+extern void ResetFileFlushContext(FileFlushContext * context);
+extern void PerformFileFlush(FileFlushContext * context);
+extern void FileAsynchronousFlush(FileFlushContext * context,
+ int fd, off_t offset, off_t nbytes, char * filename);
+extern int FileWrite(File file, char *buffer, int amount, bool flush_to_disk,
+ FileFlushContext * context);
extern int FileSync(File file);
extern off_t FileSeek(File file, off_t offset, int whence);
extern int FileTruncate(File file, off_t offset);
diff --git a/src/include/storage/smgr.h b/src/include/storage/smgr.h
index 69a624f..a46a70c 100644
--- a/src/include/storage/smgr.h
+++ b/src/include/storage/smgr.h
@@ -16,6 +16,7 @@
#include "fmgr.h"
#include "storage/block.h"
+#include "storage/fd.h"
#include "storage/relfilenode.h"
@@ -95,7 +96,8 @@ extern void smgrprefetch(SMgrRelation reln, ForkNumber forknum,
extern void smgrread(SMgrRelation reln, ForkNumber forknum,
BlockNumber blocknum, char *buffer);
extern void smgrwrite(SMgrRelation reln, ForkNumber forknum,
- BlockNumber blocknum, char *buffer, bool skipFsync);
+ BlockNumber blocknum, char *buffer, bool skipFsync,
+ bool flush_to_disk, FileFlushContext * context);
extern BlockNumber smgrnblocks(SMgrRelation reln, ForkNumber forknum);
extern void smgrtruncate(SMgrRelation reln, ForkNumber forknum,
BlockNumber nblocks);
@@ -120,8 +122,9 @@ extern void mdprefetch(SMgrRelation reln, ForkNumber forknum,
BlockNumber blocknum);
extern void mdread(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
char *buffer);
-extern void mdwrite(SMgrRelation reln, ForkNumber forknum,
- BlockNumber blocknum, char *buffer, bool skipFsync);
+extern void mdwrite(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
+ char *buffer, bool skipFsync, bool flush_to_disk,
+ FileFlushContext * context);
extern BlockNumber mdnblocks(SMgrRelation reln, ForkNumber forknum);
extern void mdtruncate(SMgrRelation reln, ForkNumber forknum,
BlockNumber nblocks);
On 2015-09-06 19:05, Fabien COELHO wrote:
Here is a rebased two-part v11.
function parameters are always in the same line as the function name
ISTM that I did that, or maybe I did not understand what I've done wrong.
I see one instance of this issue
+static int
+NextBufferToWrite(
+ TableSpaceCheckpointStatus *spcStatus, int nb_spaces,
+ int *pspace, int num_to_write, int num_written)
Also
+static int bufcmp(const void * pa, const void * pb)
+{
should IMHO be formatted as
+static int
+bufcmp(const void * pa, const void * pb)
+{
And I think we generally put the struct typedefs at the top of the C
file and don't mix them with function definitions (I am talking about
the TableSpaceCheckpointStatus and TableSpaceCountEntry).
--
Petr Jelinek http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Hello Petr,
function parameters are always in the same line as the function name
ISTM that I did that, or maybe I did not understand what I've done wrong.
I see one instance of this issue +static int +NextBufferToWrite( + TableSpaceCheckpointStatus *spcStatus, int nb_spaces, + int *pspace, int num_to_write, int num_written)
Ok, I was looking for function calls.
should IMHO be formatted as +static int +bufcmp(const void * pa, const void * pb) +{
Indeed.
And I think we generally put the struct typedefs at the top of the C file and
don't mix them with function definitions (I am talking about the
TableSpaceCheckpointStatus and TableSpaceCountEntry).
Ok, moved up.
Thanks for the hints! Two-part v12 attached fixes these.
--
Fabien.
Attachments:
checkpoint-continuous-flush-12a.patchtext/x-diff; name=checkpoint-continuous-flush-12a.patchDownload
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index e3dc23b..96c9a2f 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -2454,6 +2454,28 @@ include_dir 'conf.d'
</listitem>
</varlistentry>
+ <varlistentry id="guc-checkpoint-sort" xreflabel="checkpoint_sort">
+ <term><varname>checkpoint_sort</varname> (<type>bool</type>)
+ <indexterm>
+ <primary><varname>checkpoint_sort</> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ Whether to sort buffers before writting them out to disk on checkpoint.
+ For a HDD storage, this setting allows to group together
+ neighboring pages written to disk, thus improving performance by
+ reducing random write activity.
+ This sorting should have limited performance effects on SSD backends
+ as such storages have good random write performance, but it may
+ help with wear-leveling so be worth keeping anyway.
+ The default is <literal>on</>.
+ This parameter can only be set in the <filename>postgresql.conf</>
+ file or on the server command line.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="guc-checkpoint-warning" xreflabel="checkpoint_warning">
<term><varname>checkpoint_warning</varname> (<type>integer</type>)
<indexterm>
diff --git a/doc/src/sgml/wal.sgml b/doc/src/sgml/wal.sgml
index e3941c9..f538698 100644
--- a/doc/src/sgml/wal.sgml
+++ b/doc/src/sgml/wal.sgml
@@ -546,6 +546,18 @@
</para>
<para>
+ When hard-disk drives (HDD) are used for terminal data storage
+ <xref linkend="guc-checkpoint-sort"> allows to sort pages
+ so that neighboring pages on disk will be flushed together by
+ chekpoints, reducing the random write load and improving performance.
+ If solid-state drives (SSD) are used, sorting pages induces no benefit
+ as their random write I/O performance is good: this feature could then
+ be disabled by setting <varname>checkpoint_sort</> to <value>off</>.
+ It is possible that sorting may help with SSD wear leveling, so it may
+ be kept on that account.
+ </para>
+
+ <para>
The number of WAL segment files in <filename>pg_xlog</> directory depends on
<varname>min_wal_size</>, <varname>max_wal_size</> and
the amount of WAL generated in previous checkpoint cycles. When old log
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 127bc58..74412a6 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -7999,11 +7999,13 @@ LogCheckpointEnd(bool restartpoint)
sync_secs,
total_secs,
longest_secs,
+ sort_secs,
average_secs;
int write_usecs,
sync_usecs,
total_usecs,
longest_usecs,
+ sort_usecs,
average_usecs;
uint64 average_sync_time;
@@ -8034,6 +8036,10 @@ LogCheckpointEnd(bool restartpoint)
CheckpointStats.ckpt_end_t,
&total_secs, &total_usecs);
+ TimestampDifference(CheckpointStats.ckpt_sort_t,
+ CheckpointStats.ckpt_sort_end_t,
+ &sort_secs, &sort_usecs);
+
/*
* Timing values returned from CheckpointStats are in microseconds.
* Convert to the second plus microsecond form that TimestampDifference
@@ -8052,8 +8058,8 @@ LogCheckpointEnd(bool restartpoint)
elog(LOG, "%s complete: wrote %d buffers (%.1f%%); "
"%d transaction log file(s) added, %d removed, %d recycled; "
- "write=%ld.%03d s, sync=%ld.%03d s, total=%ld.%03d s; "
- "sync files=%d, longest=%ld.%03d s, average=%ld.%03d s; "
+ "sort=%ld.%03d s, write=%ld.%03d s, sync=%ld.%03d s, total=%ld.%03d s;"
+ " sync files=%d, longest=%ld.%03d s, average=%ld.%03d s; "
"distance=%d kB, estimate=%d kB",
restartpoint ? "restartpoint" : "checkpoint",
CheckpointStats.ckpt_bufs_written,
@@ -8061,6 +8067,7 @@ LogCheckpointEnd(bool restartpoint)
CheckpointStats.ckpt_segs_added,
CheckpointStats.ckpt_segs_removed,
CheckpointStats.ckpt_segs_recycled,
+ sort_secs, sort_usecs / 1000,
write_secs, write_usecs / 1000,
sync_secs, sync_usecs / 1000,
total_secs, total_usecs / 1000,
diff --git a/src/backend/storage/buffer/buf_init.c b/src/backend/storage/buffer/buf_init.c
index 3ae2848..3bd5eab 100644
--- a/src/backend/storage/buffer/buf_init.c
+++ b/src/backend/storage/buffer/buf_init.c
@@ -65,7 +65,8 @@ void
InitBufferPool(void)
{
bool foundBufs,
- foundDescs;
+ foundDescs,
+ foundCpid;
/* Align descriptors to a cacheline boundary. */
BufferDescriptors = (BufferDescPadded *) CACHELINEALIGN(
@@ -77,10 +78,14 @@ InitBufferPool(void)
ShmemInitStruct("Buffer Blocks",
NBuffers * (Size) BLCKSZ, &foundBufs);
- if (foundDescs || foundBufs)
+ CheckpointBufferIds = (CheckpointSortItem *)
+ ShmemInitStruct("Checkpoint BufferIds",
+ NBuffers * sizeof(CheckpointSortItem), &foundCpid);
+
+ if (foundDescs || foundBufs || foundCpid)
{
- /* both should be present or neither */
- Assert(foundDescs && foundBufs);
+ /* all should be present or neither */
+ Assert(foundDescs && foundBufs && foundCpid);
/* note: this path is only taken in EXEC_BACKEND case */
}
else
@@ -144,5 +149,8 @@ BufferShmemSize(void)
/* size of stuff controlled by freelist.c */
size = add_size(size, StrategyShmemSize());
+ /* size of checkpoint sort array in bufmgr.c */
+ size = add_size(size, mul_size(NBuffers, sizeof(CheckpointSortItem)));
+
return size;
}
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index cd3aaad..dae5954 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -75,11 +75,36 @@ typedef struct PrivateRefCountEntry
/* 64 bytes, about the size of a cache line on common systems */
#define REFCOUNT_ARRAY_ENTRIES 8
+/*
+ * Status of buffers to checkpoint for a particular tablespace,
+ * used internally in BufferSync.
+ * - space: oid of the tablespace
+ * - num_to_write: number of checkpoint pages counted for this tablespace
+ * - num_written: number of pages actually written out
+ * - index: scanning position in CheckpointBufferIds for this tablespace
+ */
+typedef struct TableSpaceCheckpointStatus {
+ Oid space;
+ int num_to_write;
+ int num_written;
+ int index;
+} TableSpaceCheckpointStatus;
+
+/*
+ * Entry structure for table space to count hashtable,
+ * used internally in BufferSync.
+ */
+typedef struct TableSpaceCountEntry {
+ Oid space;
+ int count;
+} TableSpaceCountEntry;
+
/* GUC variables */
bool zero_damaged_pages = false;
int bgwriter_lru_maxpages = 100;
double bgwriter_lru_multiplier = 2.0;
bool track_io_timing = false;
+bool checkpoint_sort = true;
/*
* How many buffers PrefetchBuffer callers should try to stay ahead of their
@@ -95,6 +120,9 @@ static bool IsForInput;
/* local state for LockBufferForCleanup */
static volatile BufferDesc *PinCountWaitBuf = NULL;
+/* array of buffer ids & sort criterion of all buffers to checkpoint */
+CheckpointSortItem *CheckpointBufferIds = NULL;
+
/*
* Backend-Private refcount management:
*
@@ -1561,6 +1589,106 @@ UnpinBuffer(volatile BufferDesc *buf, bool fixOwner)
}
}
+/* checkpoint buffers comparison */
+static int
+bufcmp(const void * pa, const void * pb)
+{
+ CheckpointSortItem
+ *a = (CheckpointSortItem *) pa,
+ *b = (CheckpointSortItem *) pb;
+
+ /* compare relation */
+ if (a->relNode < b->relNode)
+ return -1;
+ else if (a->relNode > b->relNode)
+ return 1;
+ /* same relation, compare fork */
+ else if (a->forkNum < b->forkNum)
+ return -1;
+ else if (a->forkNum > b->forkNum)
+ return 1;
+ /* same relation/fork, so same segmented "file", compare block number
+ * which are mapped on different segments depending on the number.
+ */
+ else if (a->blockNum < b->blockNum)
+ return -1;
+ else /* should not be the same block anyway... */
+ return 1;
+}
+
+/*
+ * Return the next buffer to write, or -1.
+ * this function balances buffers over tablespaces, see comment inside.
+ */
+static int
+NextBufferToWrite(TableSpaceCheckpointStatus *spcStatus, int nb_spaces,
+ int *pspace, int num_to_write, int num_written)
+{
+ int space = *pspace, buf_id = -1, index;
+
+ /*
+ * Select a tablespace depending on the current overall progress.
+ *
+ * The progress ratio of each unfinished tablespace is compared to
+ * the overall progress ratio to find one with is not in advance
+ * (i.e. overall ratio > tablespace ratio,
+ * i.e. tablespace written/to_write > overall written/to_write
+ *
+ * Existence: it is bound to exist otherwise the overall progress
+ * ratio would be inconsistent: with positive buffers to write (t1 & t2)
+ * and already written buffers (w1 & w2), we have:
+ *
+ * If w1/t1 > (w1+w2)/(t1+t2) # one table space is in advance
+ * => w1t1+w1t2 > w1t1+w2t1 => w1t2 > w2t1 => w1t2+w2t2 > w2t1+w2t2
+ * => (w1+w2) / (t1+t2) > w2 / t2 # the other one is late
+ *
+ * The round robin ensures that each space is given some attention
+ * till it is over the current ratio, before going to the next.
+ *
+ * Precision: using int32 computations for comparing fractions
+ * (w1 / t1 > w / t <=> w1 t > w t1) seems a bad idea as the values
+ * can overflow 32-bit integers: the limit would be sqrt(2**31) ~
+ * 46340 buffers, i.e. a 362 MB checkpoint. So ensure that 64-bit
+ * integers are used in the comparison.
+ */
+ while ((int64) spcStatus[space].num_written * num_to_write >
+ (int64) num_written * spcStatus[space].num_to_write)
+ space = (space + 1) % nb_spaces; /* round robin */
+
+ /*
+ * Find a valid buffer in the selected tablespace,
+ * by continuing the tablespace specific buffer scan
+ * where it was left.
+ */
+ index = spcStatus[space].index;
+
+ while (index < num_to_write && buf_id == -1)
+ {
+ volatile BufferDesc *bufHdr;
+
+ buf_id = CheckpointBufferIds[index].buf_id;
+ bufHdr = GetBufferDescriptor(buf_id);
+
+ /*
+ * Skip if in another tablespace or not in checkpoint anymore.
+ * No lock is acquired, see comments below.
+ */
+ if (spcStatus[space].space != bufHdr->tag.rnode.spcNode ||
+ ! (bufHdr->flags & BM_CHECKPOINT_NEEDED))
+ {
+ index ++;
+ buf_id = -1;
+ }
+ }
+
+ /* update tablespace writing status, will start over at next index */
+ spcStatus[space].index = index + 1;
+
+ *pspace = space;
+
+ return buf_id;
+}
+
/*
* BufferSync -- Write out all dirty buffers in the pool.
*
@@ -1574,11 +1702,13 @@ UnpinBuffer(volatile BufferDesc *buf, bool fixOwner)
static void
BufferSync(int flags)
{
- int buf_id;
- int num_to_scan;
+ int buf_id = -1;
int num_to_write;
int num_written;
int mask = BM_DIRTY;
+ HTAB *spcBuffers;
+ TableSpaceCheckpointStatus *spcStatus = NULL;
+ int nb_spaces, space;
/* Make sure we can handle the pin inside SyncOneBuffer */
ResourceOwnerEnlargeBuffers(CurrentResourceOwner);
@@ -1609,6 +1739,18 @@ BufferSync(int flags)
* certainly need to be written for the next checkpoint attempt, too.
*/
num_to_write = 0;
+
+ /* initialize oid -> int buffer count hash table */
+ {
+ HASHCTL ctl;
+
+ MemSet(&ctl, 0, sizeof(HASHCTL));
+ ctl.keysize = sizeof(Oid);
+ ctl.entrysize = sizeof(TableSpaceCountEntry);
+ spcBuffers = hash_create("Number of buffers to write per tablespace",
+ 16, &ctl, HASH_ELEM | HASH_BLOBS);
+ }
+
for (buf_id = 0; buf_id < NBuffers; buf_id++)
{
volatile BufferDesc *bufHdr = GetBufferDescriptor(buf_id);
@@ -1621,32 +1763,99 @@ BufferSync(int flags)
if ((bufHdr->flags & mask) == mask)
{
+ Oid spc;
+ TableSpaceCountEntry * entry;
+ bool found;
+
bufHdr->flags |= BM_CHECKPOINT_NEEDED;
+ CheckpointBufferIds[num_to_write].buf_id = buf_id;
+ CheckpointBufferIds[num_to_write].relNode = bufHdr->tag.rnode.relNode;
+ CheckpointBufferIds[num_to_write].forkNum = bufHdr->tag.forkNum;
+ CheckpointBufferIds[num_to_write].blockNum = bufHdr->tag.blockNum;
num_to_write++;
+
+ /* keep track of per tablespace buffers */
+ spc = bufHdr->tag.rnode.spcNode;
+ entry = (TableSpaceCountEntry *)
+ hash_search(spcBuffers, (void *) &spc, HASH_ENTER, &found);
+
+ if (found)
+ entry->count++;
+ else
+ entry->count = 1;
}
UnlockBufHdr(bufHdr);
}
if (num_to_write == 0)
+ {
+ hash_destroy(spcBuffers);
return; /* nothing to do */
+ }
TRACE_POSTGRESQL_BUFFER_SYNC_START(NBuffers, num_to_write);
+ /* Build checkpoint tablespace buffer status */
+ nb_spaces = hash_get_num_entries(spcBuffers);
+ spcStatus = (TableSpaceCheckpointStatus *)
+ palloc(sizeof(TableSpaceCheckpointStatus) * nb_spaces);
+
+ {
+ int index = 0;
+ HASH_SEQ_STATUS hseq;
+ TableSpaceCountEntry * entry;
+
+ hash_seq_init(&hseq, spcBuffers);
+ while ((entry = (TableSpaceCountEntry *) hash_seq_search(&hseq)))
+ {
+ Assert(index < nb_spaces);
+ spcStatus[index].space = entry->space;
+ spcStatus[index].num_to_write = entry->count;
+ spcStatus[index].num_written = 0;
+ /* should it be randomized? chosen with some criterion? */
+ spcStatus[index].index = 0;
+
+ index ++;
+ }
+ }
+
+ hash_destroy(spcBuffers);
+ spcBuffers = NULL;
+
+ /* sort buffer ids to help find sequential writes */
+ CheckpointStats.ckpt_sort_t = GetCurrentTimestamp();
+
+ if (checkpoint_sort)
+ {
+ qsort(CheckpointBufferIds, num_to_write, sizeof(CheckpointSortItem),
+ bufcmp);
+ }
+
+ CheckpointStats.ckpt_sort_end_t = GetCurrentTimestamp();
+
/*
- * Loop over all buffers again, and write the ones (still) marked with
- * BM_CHECKPOINT_NEEDED. In this loop, we start at the clock sweep point
- * since we might as well dump soon-to-be-recycled buffers first.
+ * Loop over buffers to write through CheckpointBufferIds,
+ * and write the ones (still) marked with BM_CHECKPOINT_NEEDED,
+ * with some round robin over table spaces so as to balance writes,
+ * so that buffer writes move forward roughly proportionally for each
+ * tablespace.
*
- * Note that we don't read the buffer alloc count here --- that should be
- * left untouched till the next BgBufferSync() call.
+ * Termination: if a tablespace is selected by the inner while loop
+ * (see argument there), its index is incremented and will eventually
+ * reach num_to_write, mark this table space scanning as done and
+ * decrement the number of (active) spaces, which will thus reach 0.
*/
- buf_id = StrategySyncStart(NULL, NULL);
- num_to_scan = NBuffers;
+ space = 0;
num_written = 0;
- while (num_to_scan-- > 0)
+
+ while (nb_spaces != 0)
{
- volatile BufferDesc *bufHdr = GetBufferDescriptor(buf_id);
+ volatile BufferDesc *bufHdr = NULL;
+ buf_id = NextBufferToWrite(spcStatus, nb_spaces, &space,
+ num_to_write, num_written);
+ if (buf_id != -1)
+ bufHdr = GetBufferDescriptor(buf_id);
/*
* We don't need to acquire the lock here, because we're only looking
@@ -1660,39 +1869,46 @@ BufferSync(int flags)
* write the buffer though we didn't need to. It doesn't seem worth
* guarding against this, though.
*/
- if (bufHdr->flags & BM_CHECKPOINT_NEEDED)
+ if (bufHdr != NULL && bufHdr->flags & BM_CHECKPOINT_NEEDED)
{
if (SyncOneBuffer(buf_id, false) & BUF_WRITTEN)
{
TRACE_POSTGRESQL_BUFFER_SYNC_WRITTEN(buf_id);
BgWriterStats.m_buf_written_checkpoints++;
+ spcStatus[space].num_written++;
num_written++;
/*
- * We know there are at most num_to_write buffers with
- * BM_CHECKPOINT_NEEDED set; so we can stop scanning if
- * num_written reaches num_to_write.
- *
- * Note that num_written doesn't include buffers written by
- * other backends, or by the bgwriter cleaning scan. That
- * means that the estimate of how much progress we've made is
- * conservative, and also that this test will often fail to
- * trigger. But it seems worth making anyway.
- */
- if (num_written >= num_to_write)
- break;
-
- /*
* Sleep to throttle our I/O rate.
*/
CheckpointWriteDelay(flags, (double) num_written / num_to_write);
}
}
- if (++buf_id >= NBuffers)
- buf_id = 0;
+ /*
+ * Detect checkpoint end for a tablespace: either the scan is done
+ * or all tablespace buffers have been written out. If so, the
+ * another active tablespace status is moved in place of the current
+ * one and the next round will start on this one, or maybe round about.
+ *
+ * Note: maybe an exchange could be made instead in order to keep
+ * informations about the closed table space, but this is currently
+ * not used afterwards.
+ */
+ if (spcStatus[space].index >= num_to_write ||
+ spcStatus[space].num_written >= spcStatus[space].num_to_write)
+ {
+ nb_spaces--;
+ if (space != nb_spaces)
+ spcStatus[space] = spcStatus[nb_spaces];
+ else
+ space = 0;
+ }
}
+ pfree(spcStatus);
+ spcStatus = NULL;
+
/*
* Update checkpoint statistics. As noted above, this doesn't include
* buffers written by other backends or bgwriter scan.
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index b3dac51..cf1e505 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -1013,6 +1013,17 @@ static struct config_bool ConfigureNamesBool[] =
false,
NULL, NULL, NULL
},
+
+ {
+ {"checkpoint_sort", PGC_SIGHUP, WAL_CHECKPOINTS,
+ gettext_noop("Whether disk-page buffers are sorted on checkpoints."),
+ NULL
+ },
+ &checkpoint_sort,
+ true,
+ NULL, NULL, NULL
+ },
+
{
{"log_connections", PGC_SU_BACKEND, LOGGING_WHAT,
gettext_noop("Logs each successful connection."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 695a88f..d4dfc25 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -201,6 +201,7 @@
#max_wal_size = 1GB
#min_wal_size = 80MB
#checkpoint_completion_target = 0.5 # checkpoint target duration, 0.0 - 1.0
+#checkpoint_sort = on # sort buffers on checkpoint
#checkpoint_warning = 30s # 0 disables
# - Archiving -
diff --git a/src/include/access/xlog.h b/src/include/access/xlog.h
index 6dacee2..dbd4757 100644
--- a/src/include/access/xlog.h
+++ b/src/include/access/xlog.h
@@ -186,6 +186,8 @@ extern bool XLOG_DEBUG;
typedef struct CheckpointStatsData
{
TimestampTz ckpt_start_t; /* start of checkpoint */
+ TimestampTz ckpt_sort_t; /* start buffer sorting */
+ TimestampTz ckpt_sort_end_t; /* end of sorting */
TimestampTz ckpt_write_t; /* start of flushing buffers */
TimestampTz ckpt_sync_t; /* start of fsyncs */
TimestampTz ckpt_sync_end_t; /* end of fsyncs */
diff --git a/src/include/storage/buf_internals.h b/src/include/storage/buf_internals.h
index 521ee1c..32f2006 100644
--- a/src/include/storage/buf_internals.h
+++ b/src/include/storage/buf_internals.h
@@ -210,6 +210,23 @@ extern PGDLLIMPORT BufferDescPadded *BufferDescriptors;
/* in localbuf.c */
extern BufferDesc *LocalBufferDescriptors;
+/* in bufmgr.c */
+
+/*
+ * Structure to sort buffers per file on checkpoints.
+ *
+ * This structure is allocated per buffer in shared memory, so it should be
+ * kept as little as possible. Maybe the sort criterion could be compacted
+ * to reduce memory requirement and for faster comparison?
+ */
+typedef struct CheckpointSortItem {
+ int buf_id;
+ Oid relNode;
+ ForkNumber forkNum; /* hm... enum with only 4 values */
+ BlockNumber blockNum;
+} CheckpointSortItem;
+
+extern CheckpointSortItem *CheckpointBufferIds;
/*
* Internal routines: only called by bufmgr
diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.h
index ec0a254..c228f39 100644
--- a/src/include/storage/bufmgr.h
+++ b/src/include/storage/bufmgr.h
@@ -54,6 +54,7 @@ extern int bgwriter_lru_maxpages;
extern double bgwriter_lru_multiplier;
extern bool track_io_timing;
extern int target_prefetch_pages;
+extern bool checkpoint_sort;
/* in buf_init.c */
extern PGDLLIMPORT char *BufferBlocks;
checkpoint-continuous-flush-12b.patchtext/x-diff; name=checkpoint-continuous-flush-12b.patchDownload
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 96c9a2f..927294b 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -2497,6 +2497,24 @@ include_dir 'conf.d'
</listitem>
</varlistentry>
+ <varlistentry id="guc-checkpoint-flush-to-disk" xreflabel="checkpoint_flush_to_disk">
+ <term><varname>checkpoint_flush_to_disk</varname> (<type>bool</type>)
+ <indexterm>
+ <primary><varname>checkpoint_flush_to_disk</> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ When writing data for a checkpoint, hint the underlying OS that the
+ data must be sent to disk as soon as possible. This may help smoothing
+ disk I/O writes and avoid a stall when fsync is issued at the end of
+ the checkpoint, but it may also reduce average performance.
+ This setting may have no effect on some platforms.
+ The default is <literal>on</> on Linux, <literal>off</> otherwise.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="guc-min-wal-size" xreflabel="min_wal_size">
<term><varname>min_wal_size</varname> (<type>integer</type>)
<indexterm>
diff --git a/doc/src/sgml/wal.sgml b/doc/src/sgml/wal.sgml
index f538698..1b658f2 100644
--- a/doc/src/sgml/wal.sgml
+++ b/doc/src/sgml/wal.sgml
@@ -558,6 +558,18 @@
</para>
<para>
+ On Linux and POSIX platforms, <xref linkend="guc-checkpoint-flush-to-disk">
+ allows to hint the OS that pages written on checkpoints must be flushed
+ to disk quickly. Otherwise, these pages may be kept in cache for some time,
+ inducing a stall later when <literal>fsync</> is called to actually
+ complete the checkpoint. This setting helps to reduce transaction latency,
+ but it may also have a small adverse effect on the average transaction rate
+ at maximum throughput on some OS. It should be beneficial for high write
+ loads on HDD. This feature probably brings no benefit on SSD, as the I/O
+ write latency is small on such hardware, thus it may be disabled.
+ </para>
+
+ <para>
The number of WAL segment files in <filename>pg_xlog</> directory depends on
<varname>min_wal_size</>, <varname>max_wal_size</> and
the amount of WAL generated in previous checkpoint cycles. When old log
diff --git a/src/backend/access/heap/rewriteheap.c b/src/backend/access/heap/rewriteheap.c
index 6a6fc3b..2a8f645 100644
--- a/src/backend/access/heap/rewriteheap.c
+++ b/src/backend/access/heap/rewriteheap.c
@@ -918,7 +918,7 @@ logical_heap_rewrite_flush_mappings(RewriteState state)
* Note that we deviate from the usual WAL coding practices here,
* check the above "Logical rewrite support" comment for reasoning.
*/
- written = FileWrite(src->vfd, waldata_start, len);
+ written = FileWrite(src->vfd, waldata_start, len, false, NULL);
if (written != len)
ereport(ERROR,
(errcode_for_file_access(),
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index cf4a6dc..4b5e9cd 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -203,7 +203,7 @@ btbuildempty(PG_FUNCTION_ARGS)
/* Write the page. If archiving/streaming, XLOG it. */
PageSetChecksumInplace(metapage, BTREE_METAPAGE);
smgrwrite(index->rd_smgr, INIT_FORKNUM, BTREE_METAPAGE,
- (char *) metapage, true);
+ (char *) metapage, true, false, NULL);
if (XLogIsNeeded())
log_newpage(&index->rd_smgr->smgr_rnode.node, INIT_FORKNUM,
BTREE_METAPAGE, metapage, false);
diff --git a/src/backend/access/nbtree/nbtsort.c b/src/backend/access/nbtree/nbtsort.c
index f95f67a..ea7a45d 100644
--- a/src/backend/access/nbtree/nbtsort.c
+++ b/src/backend/access/nbtree/nbtsort.c
@@ -315,7 +315,7 @@ _bt_blwritepage(BTWriteState *wstate, Page page, BlockNumber blkno)
{
/* overwriting a block we zero-filled before */
smgrwrite(wstate->index->rd_smgr, MAIN_FORKNUM, blkno,
- (char *) page, true);
+ (char *) page, true, false, NULL);
}
pfree(page);
diff --git a/src/backend/access/spgist/spginsert.c b/src/backend/access/spgist/spginsert.c
index bceee8d..b700efb 100644
--- a/src/backend/access/spgist/spginsert.c
+++ b/src/backend/access/spgist/spginsert.c
@@ -170,7 +170,7 @@ spgbuildempty(PG_FUNCTION_ARGS)
/* Write the page. If archiving/streaming, XLOG it. */
PageSetChecksumInplace(page, SPGIST_METAPAGE_BLKNO);
smgrwrite(index->rd_smgr, INIT_FORKNUM, SPGIST_METAPAGE_BLKNO,
- (char *) page, true);
+ (char *) page, true, false, NULL);
if (XLogIsNeeded())
log_newpage(&index->rd_smgr->smgr_rnode.node, INIT_FORKNUM,
SPGIST_METAPAGE_BLKNO, page, false);
@@ -180,7 +180,7 @@ spgbuildempty(PG_FUNCTION_ARGS)
PageSetChecksumInplace(page, SPGIST_ROOT_BLKNO);
smgrwrite(index->rd_smgr, INIT_FORKNUM, SPGIST_ROOT_BLKNO,
- (char *) page, true);
+ (char *) page, true, false, NULL);
if (XLogIsNeeded())
log_newpage(&index->rd_smgr->smgr_rnode.node, INIT_FORKNUM,
SPGIST_ROOT_BLKNO, page, true);
@@ -190,7 +190,7 @@ spgbuildempty(PG_FUNCTION_ARGS)
PageSetChecksumInplace(page, SPGIST_NULL_BLKNO);
smgrwrite(index->rd_smgr, INIT_FORKNUM, SPGIST_NULL_BLKNO,
- (char *) page, true);
+ (char *) page, true, false, NULL);
if (XLogIsNeeded())
log_newpage(&index->rd_smgr->smgr_rnode.node, INIT_FORKNUM,
SPGIST_NULL_BLKNO, page, true);
diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c
index 3b3a09e..e361907 100644
--- a/src/backend/postmaster/checkpointer.c
+++ b/src/backend/postmaster/checkpointer.c
@@ -665,7 +665,8 @@ ImmediateCheckpointRequested(void)
* fraction between 0.0 meaning none, and 1.0 meaning all done.
*/
void
-CheckpointWriteDelay(int flags, double progress)
+CheckpointWriteDelay(int flags, double progress,
+ FileFlushContext * context, int ctx_size)
{
static int absorb_counter = WRITES_PER_ABSORB;
@@ -700,6 +701,26 @@ CheckpointWriteDelay(int flags, double progress)
*/
pgstat_send_bgwriter();
+#if defined(HAVE_SYNC_FILE_RANGE) || defined(HAVE_POSIX_FADVISE)
+
+ /*
+ * Before sleeping, flush written blocks for each tablespace.
+ */
+ if (checkpoint_flush_to_disk)
+ {
+ int i;
+
+ for (i = 0; i < ctx_size; i++)
+ {
+ if (context[i].ncalls != 0)
+ {
+ PerformFileFlush(&context[i]);
+ ResetFileFlushContext(&context[i]);
+ }
+ }
+ }
+#endif /* HAVE_SYNC_FILE_RANGE || HAVE_POSIX_FADVISE */
+
/*
* This sleep used to be connected to bgwriter_delay, typically 200ms.
* That resulted in more frequent wakeups if not much work to do.
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index dae5954..74f3914 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -104,6 +104,8 @@ bool zero_damaged_pages = false;
int bgwriter_lru_maxpages = 100;
double bgwriter_lru_multiplier = 2.0;
bool track_io_timing = false;
+/* hint to move writes to high priority */
+bool checkpoint_flush_to_disk = DEFAULT_CHECKPOINT_FLUSH_TO_DISK;
bool checkpoint_sort = true;
/*
@@ -424,7 +426,8 @@ static bool PinBuffer(volatile BufferDesc *buf, BufferAccessStrategy strategy);
static void PinBuffer_Locked(volatile BufferDesc *buf);
static void UnpinBuffer(volatile BufferDesc *buf, bool fixOwner);
static void BufferSync(int flags);
-static int SyncOneBuffer(int buf_id, bool skip_recently_used);
+static int SyncOneBuffer(int buf_id, bool skip_recently_used,
+ bool flush_to_disk, FileFlushContext *context);
static void WaitIO(volatile BufferDesc *buf);
static bool StartBufferIO(volatile BufferDesc *buf, bool forInput);
static void TerminateBufferIO(volatile BufferDesc *buf, bool clear_dirty,
@@ -437,7 +440,8 @@ static volatile BufferDesc *BufferAlloc(SMgrRelation smgr,
BlockNumber blockNum,
BufferAccessStrategy strategy,
bool *foundPtr);
-static void FlushBuffer(volatile BufferDesc *buf, SMgrRelation reln);
+static void FlushBuffer(volatile BufferDesc *buf, SMgrRelation reln,
+ bool flush_to_disk, FileFlushContext *context);
static void AtProcExit_Buffers(int code, Datum arg);
static void CheckForBufferLeaks(void);
static int rnode_comparator(const void *p1, const void *p2);
@@ -1046,7 +1050,7 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
smgr->smgr_rnode.node.dbNode,
smgr->smgr_rnode.node.relNode);
- FlushBuffer(buf, NULL);
+ FlushBuffer(buf, NULL, false, NULL);
LWLockRelease(buf->content_lock);
TRACE_POSTGRESQL_BUFFER_WRITE_DIRTY_DONE(forkNum, blockNum,
@@ -1709,6 +1713,7 @@ BufferSync(int flags)
HTAB *spcBuffers;
TableSpaceCheckpointStatus *spcStatus = NULL;
int nb_spaces, space;
+ FileFlushContext * spcContext = NULL;
/* Make sure we can handle the pin inside SyncOneBuffer */
ResourceOwnerEnlargeBuffers(CurrentResourceOwner);
@@ -1796,10 +1801,12 @@ BufferSync(int flags)
TRACE_POSTGRESQL_BUFFER_SYNC_START(NBuffers, num_to_write);
- /* Build checkpoint tablespace buffer status */
+ /* Build checkpoint tablespace buffer status & flush context arrays */
nb_spaces = hash_get_num_entries(spcBuffers);
spcStatus = (TableSpaceCheckpointStatus *)
palloc(sizeof(TableSpaceCheckpointStatus) * nb_spaces);
+ spcContext = (FileFlushContext *)
+ palloc(sizeof(FileFlushContext) * nb_spaces);
{
int index = 0;
@@ -1816,6 +1823,12 @@ BufferSync(int flags)
/* should it be randomized? chosen with some criterion? */
spcStatus[index].index = 0;
+#if defined(HAVE_SYNC_FILE_RANGE) || defined(HAVE_POSIX_FADVISE)
+
+ ResetFileFlushContext(&spcContext[index]);
+
+#endif /* HAVE_SYNC_FILE_RANGE || HAVE_POSIX_FADVISE */
+
index ++;
}
}
@@ -1871,7 +1884,8 @@ BufferSync(int flags)
*/
if (bufHdr != NULL && bufHdr->flags & BM_CHECKPOINT_NEEDED)
{
- if (SyncOneBuffer(buf_id, false) & BUF_WRITTEN)
+ if (SyncOneBuffer(buf_id, false, checkpoint_flush_to_disk,
+ &spcContext[space]) & BUF_WRITTEN)
{
TRACE_POSTGRESQL_BUFFER_SYNC_WRITTEN(buf_id);
BgWriterStats.m_buf_written_checkpoints++;
@@ -1881,7 +1895,8 @@ BufferSync(int flags)
/*
* Sleep to throttle our I/O rate.
*/
- CheckpointWriteDelay(flags, (double) num_written / num_to_write);
+ CheckpointWriteDelay(flags, (double) num_written / num_to_write,
+ spcContext, nb_spaces);
}
}
@@ -1898,6 +1913,13 @@ BufferSync(int flags)
if (spcStatus[space].index >= num_to_write ||
spcStatus[space].num_written >= spcStatus[space].num_to_write)
{
+#if defined(HAVE_SYNC_FILE_RANGE) || defined(HAVE_POSIX_FADVISE)
+
+ PerformFileFlush(&spcContext[space]);
+ ResetFileFlushContext(&spcContext[space]);
+
+#endif /* HAVE_SYNC_FILE_RANGE || HAVE_POSIX_FADVISE */
+
nb_spaces--;
if (space != nb_spaces)
spcStatus[space] = spcStatus[nb_spaces];
@@ -1908,6 +1930,8 @@ BufferSync(int flags)
pfree(spcStatus);
spcStatus = NULL;
+ pfree(spcContext);
+ spcContext = NULL;
/*
* Update checkpoint statistics. As noted above, this doesn't include
@@ -2155,7 +2179,7 @@ BgBufferSync(void)
/* Execute the LRU scan */
while (num_to_scan > 0 && reusable_buffers < upcoming_alloc_est)
{
- int buffer_state = SyncOneBuffer(next_to_clean, true);
+ int buffer_state = SyncOneBuffer(next_to_clean, true, false, NULL);
if (++next_to_clean >= NBuffers)
{
@@ -2232,7 +2256,8 @@ BgBufferSync(void)
* Note: caller must have done ResourceOwnerEnlargeBuffers.
*/
static int
-SyncOneBuffer(int buf_id, bool skip_recently_used)
+SyncOneBuffer(int buf_id, bool skip_recently_used, bool flush_to_disk,
+ FileFlushContext * context)
{
volatile BufferDesc *bufHdr = GetBufferDescriptor(buf_id);
int result = 0;
@@ -2273,7 +2298,7 @@ SyncOneBuffer(int buf_id, bool skip_recently_used)
PinBuffer_Locked(bufHdr);
LWLockAcquire(bufHdr->content_lock, LW_SHARED);
- FlushBuffer(bufHdr, NULL);
+ FlushBuffer(bufHdr, NULL, flush_to_disk, context);
LWLockRelease(bufHdr->content_lock);
UnpinBuffer(bufHdr, true);
@@ -2535,9 +2560,16 @@ BufferGetTag(Buffer buffer, RelFileNode *rnode, ForkNumber *forknum,
*
* If the caller has an smgr reference for the buffer's relation, pass it
* as the second parameter. If not, pass NULL.
+ *
+ * The third parameter tries to hint the OS that a high priority write is meant,
+ * possibly because io-throttling is already managed elsewhere.
+ * The last parameter holds the current flush context that accumulates flush
+ * requests to be performed in one call, instead of being performed on a buffer
+ * per buffer basis.
*/
static void
-FlushBuffer(volatile BufferDesc *buf, SMgrRelation reln)
+FlushBuffer(volatile BufferDesc *buf, SMgrRelation reln, bool flush_to_disk,
+ FileFlushContext * context)
{
XLogRecPtr recptr;
ErrorContextCallback errcallback;
@@ -2626,7 +2658,9 @@ FlushBuffer(volatile BufferDesc *buf, SMgrRelation reln)
buf->tag.forkNum,
buf->tag.blockNum,
bufToWrite,
- false);
+ false,
+ flush_to_disk,
+ context);
if (track_io_timing)
{
@@ -3048,7 +3082,9 @@ FlushRelationBuffers(Relation rel)
bufHdr->tag.forkNum,
bufHdr->tag.blockNum,
localpage,
- false);
+ false,
+ false,
+ NULL);
bufHdr->flags &= ~(BM_DIRTY | BM_JUST_DIRTIED);
@@ -3082,7 +3118,7 @@ FlushRelationBuffers(Relation rel)
{
PinBuffer_Locked(bufHdr);
LWLockAcquire(bufHdr->content_lock, LW_SHARED);
- FlushBuffer(bufHdr, rel->rd_smgr);
+ FlushBuffer(bufHdr, rel->rd_smgr, false, NULL);
LWLockRelease(bufHdr->content_lock);
UnpinBuffer(bufHdr, true);
}
@@ -3134,7 +3170,7 @@ FlushDatabaseBuffers(Oid dbid)
{
PinBuffer_Locked(bufHdr);
LWLockAcquire(bufHdr->content_lock, LW_SHARED);
- FlushBuffer(bufHdr, NULL);
+ FlushBuffer(bufHdr, NULL, false, NULL);
LWLockRelease(bufHdr->content_lock);
UnpinBuffer(bufHdr, true);
}
diff --git a/src/backend/storage/buffer/localbuf.c b/src/backend/storage/buffer/localbuf.c
index 3144afe..114a0a6 100644
--- a/src/backend/storage/buffer/localbuf.c
+++ b/src/backend/storage/buffer/localbuf.c
@@ -208,7 +208,9 @@ LocalBufferAlloc(SMgrRelation smgr, ForkNumber forkNum, BlockNumber blockNum,
bufHdr->tag.forkNum,
bufHdr->tag.blockNum,
localpage,
- false);
+ false,
+ false,
+ NULL);
/* Mark not-dirty now in case we error out below */
bufHdr->flags &= ~BM_DIRTY;
diff --git a/src/backend/storage/file/buffile.c b/src/backend/storage/file/buffile.c
index ea4d689..fb3b383 100644
--- a/src/backend/storage/file/buffile.c
+++ b/src/backend/storage/file/buffile.c
@@ -317,7 +317,7 @@ BufFileDumpBuffer(BufFile *file)
return; /* seek failed, give up */
file->offsets[file->curFile] = file->curOffset;
}
- bytestowrite = FileWrite(thisfile, file->buffer + wpos, bytestowrite);
+ bytestowrite = FileWrite(thisfile, file->buffer + wpos, bytestowrite, false, NULL);
if (bytestowrite <= 0)
return; /* failed to write */
file->offsets[file->curFile] += bytestowrite;
diff --git a/src/backend/storage/file/fd.c b/src/backend/storage/file/fd.c
index 1ba4946..e880a9e 100644
--- a/src/backend/storage/file/fd.c
+++ b/src/backend/storage/file/fd.c
@@ -1344,8 +1344,97 @@ retry:
return returnCode;
}
+#if defined(HAVE_SYNC_FILE_RANGE) || defined(HAVE_POSIX_FADVISE)
+
+void
+ResetFileFlushContext(FileFlushContext * context)
+{
+ context->fd = 0;
+ context->ncalls = 0;
+ context->offset = 0;
+ context->nbytes = 0;
+ context->filename = NULL;
+}
+
+void
+PerformFileFlush(FileFlushContext * context)
+{
+ if (context->ncalls != 0)
+ {
+ int rc;
+
+#if defined(HAVE_SYNC_FILE_RANGE)
+
+ /*
+ * Linux: tell the memory manager to move these blocks to io so
+ * that they are considered for being actually written to disk.
+ */
+ rc = sync_file_range(context->fd, context->offset, context->nbytes,
+ SYNC_FILE_RANGE_WRITE);
+
+#elif defined(HAVE_POSIX_FADVISE)
+
+ /*
+ * Others: say that data should not be kept in memory...
+ * This is not exactly what we want to say, because we want to write
+ * the data for durability but we may need it later nevertheless.
+ * It seems that Linux would free the memory *if* the data has
+ * already been written do disk, else the "dontneed" call is ignored.
+ * For FreeBSD this may have the desired effect of moving the
+ * data to the io layer, although the system does not seem to
+ * take into account the provided offset & size, so it is rather
+ * rough...
+ */
+ rc = posix_fadvise(context->fd, context->offset, context->nbytes,
+ POSIX_FADV_DONTNEED);
+
+#endif
+
+ if (rc < 0)
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not flush block " INT64_FORMAT
+ " on " INT64_FORMAT " blocks in file \"%s\": %m",
+ context->offset / BLCKSZ,
+ context->nbytes / BLCKSZ,
+ context->filename)));
+ }
+}
+
+void
+FileAsynchronousFlush(FileFlushContext * context,
+ int fd, off_t offset, off_t nbytes, char * filename)
+{
+ if (context->ncalls != 0 && context->fd == fd)
+ {
+ /* same file: merge current flush with previous ones */
+ off_t new_offset = offset < context->offset? offset: context->offset;
+
+ context->nbytes =
+ (context->offset + context->nbytes > offset + nbytes ?
+ context->offset + context->nbytes : offset + nbytes) -
+ new_offset;
+ context->offset = new_offset;
+ context->ncalls ++;
+ }
+ else
+ {
+ /* other file: do flush previous file & reset flush accumulator */
+ PerformFileFlush(context);
+
+ context->fd = fd;
+ context->ncalls = 1;
+ context->offset = offset;
+ context->nbytes = nbytes;
+ context->filename = filename;
+ }
+}
+
+#endif /* HAVE_SYNC_FILE_RANGE || HAVE_POSIX_FADVISE */
+
int
-FileWrite(File file, char *buffer, int amount)
+FileWrite(File file, char *buffer, int amount, bool flush_to_disk,
+ FileFlushContext * context)
{
int returnCode;
@@ -1395,6 +1484,28 @@ retry:
if (returnCode >= 0)
{
+
+#if defined(HAVE_SYNC_FILE_RANGE) || defined(HAVE_POSIX_FADVISE)
+
+ /*
+ * Calling "write" tells the OS that pg wants to write some page to disk,
+ * however when it is really done is chosen by the OS.
+ * Depending on other disk activities this may be delayed significantly,
+ * maybe up to an "fsync" call, which could induce an IO write surge.
+ * When checkpointing pg is doing its own throttling and the result
+ * should really be written to disk with high priority, so as to meet
+ * the completion target.
+ * This call hints that such write have a higher priority.
+ */
+ if (flush_to_disk && returnCode == amount && errno == 0)
+ {
+ FileAsynchronousFlush(context,
+ VfdCache[file].fd, VfdCache[file].seekPos,
+ amount, VfdCache[file].fileName);
+ }
+
+#endif /* HAVE_SYNC_FILE_RANGE || HAVE_POSIX_FADVISE */
+
VfdCache[file].seekPos += returnCode;
/* maintain fileSize and temporary_files_size if it's a temp file */
diff --git a/src/backend/storage/smgr/md.c b/src/backend/storage/smgr/md.c
index 42a43bb..dbf057f 100644
--- a/src/backend/storage/smgr/md.c
+++ b/src/backend/storage/smgr/md.c
@@ -531,7 +531,7 @@ mdextend(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
errmsg("could not seek to block %u in file \"%s\": %m",
blocknum, FilePathName(v->mdfd_vfd))));
- if ((nbytes = FileWrite(v->mdfd_vfd, buffer, BLCKSZ)) != BLCKSZ)
+ if ((nbytes = FileWrite(v->mdfd_vfd, buffer, BLCKSZ, false, NULL)) != BLCKSZ)
{
if (nbytes < 0)
ereport(ERROR,
@@ -738,7 +738,8 @@ mdread(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
*/
void
mdwrite(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
- char *buffer, bool skipFsync)
+ char *buffer, bool skipFsync, bool flush_to_disk,
+ FileFlushContext * context)
{
off_t seekpos;
int nbytes;
@@ -767,7 +768,7 @@ mdwrite(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
errmsg("could not seek to block %u in file \"%s\": %m",
blocknum, FilePathName(v->mdfd_vfd))));
- nbytes = FileWrite(v->mdfd_vfd, buffer, BLCKSZ);
+ nbytes = FileWrite(v->mdfd_vfd, buffer, BLCKSZ, flush_to_disk, context);
TRACE_POSTGRESQL_SMGR_MD_WRITE_DONE(forknum, blocknum,
reln->smgr_rnode.node.spcNode,
diff --git a/src/backend/storage/smgr/smgr.c b/src/backend/storage/smgr/smgr.c
index 244b4ea..2db3cd3 100644
--- a/src/backend/storage/smgr/smgr.c
+++ b/src/backend/storage/smgr/smgr.c
@@ -52,7 +52,8 @@ typedef struct f_smgr
void (*smgr_read) (SMgrRelation reln, ForkNumber forknum,
BlockNumber blocknum, char *buffer);
void (*smgr_write) (SMgrRelation reln, ForkNumber forknum,
- BlockNumber blocknum, char *buffer, bool skipFsync);
+ BlockNumber blocknum, char *buffer, bool skipFsync,
+ bool flush_to_disk, FileFlushContext *context);
BlockNumber (*smgr_nblocks) (SMgrRelation reln, ForkNumber forknum);
void (*smgr_truncate) (SMgrRelation reln, ForkNumber forknum,
BlockNumber nblocks);
@@ -643,10 +644,11 @@ smgrread(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
*/
void
smgrwrite(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
- char *buffer, bool skipFsync)
+ char *buffer, bool skipFsync, bool flush_to_disk,
+ FileFlushContext * context)
{
(*(smgrsw[reln->smgr_which].smgr_write)) (reln, forknum, blocknum,
- buffer, skipFsync);
+ buffer, skipFsync, flush_to_disk, context);
}
/*
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index cf1e505..9219330 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -158,6 +158,7 @@ static bool check_bonjour(bool *newval, void **extra, GucSource source);
static bool check_ssl(bool *newval, void **extra, GucSource source);
static bool check_stage_log_stats(bool *newval, void **extra, GucSource source);
static bool check_log_stats(bool *newval, void **extra, GucSource source);
+static bool check_flush_to_disk(bool *newval, void **extra, GucSource source);
static bool check_canonical_path(char **newval, void **extra, GucSource source);
static bool check_timezone_abbreviations(char **newval, void **extra, GucSource source);
static void assign_timezone_abbreviations(const char *newval, void *extra);
@@ -1025,6 +1026,17 @@ static struct config_bool ConfigureNamesBool[] =
},
{
+ {"checkpoint_flush_to_disk", PGC_SIGHUP, WAL_CHECKPOINTS,
+ gettext_noop("Hint that checkpoint's writes are high priority."),
+ NULL
+ },
+ &checkpoint_flush_to_disk,
+ /* see bufmgr.h: true on Linux, false otherwise */
+ DEFAULT_CHECKPOINT_FLUSH_TO_DISK,
+ check_flush_to_disk, NULL, NULL
+ },
+
+ {
{"log_connections", PGC_SU_BACKEND, LOGGING_WHAT,
gettext_noop("Logs each successful connection."),
NULL
@@ -9806,6 +9818,21 @@ check_log_stats(bool *newval, void **extra, GucSource source)
}
static bool
+check_flush_to_disk(bool *newval, void **extra, GucSource source)
+{
+/* This test must be consistent with the one in FileWrite (storage/file/fd.c)
+ */
+#if ! (defined(HAVE_SYNC_FILE_RANGE) || defined(HAVE_POSIX_FADVISE))
+ /* just warn if it has no effect */
+ ereport(WARNING,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("Setting \"checkpoint_flush_to_disk\" has no effect "
+ "on this platform.")));
+#endif /* ! (HAVE_SYNC_FILE_RANGE || HAVE_POSIX_FADVISE) */
+ return true;
+}
+
+static bool
check_canonical_path(char **newval, void **extra, GucSource source)
{
/*
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index d4dfc25..01b1c96 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -202,6 +202,8 @@
#min_wal_size = 80MB
#checkpoint_completion_target = 0.5 # checkpoint target duration, 0.0 - 1.0
#checkpoint_sort = on # sort buffers on checkpoint
+#checkpoint_flush_to_disk = ? # send buffers to disk on checkpoint
+ # default is on if Linux, off otherwise
#checkpoint_warning = 30s # 0 disables
# - Archiving -
diff --git a/src/include/postmaster/bgwriter.h b/src/include/postmaster/bgwriter.h
index a49c208..f9c8ca1 100644
--- a/src/include/postmaster/bgwriter.h
+++ b/src/include/postmaster/bgwriter.h
@@ -16,6 +16,7 @@
#define _BGWRITER_H
#include "storage/block.h"
+#include "storage/fd.h"
#include "storage/relfilenode.h"
@@ -29,7 +30,8 @@ extern void BackgroundWriterMain(void) pg_attribute_noreturn();
extern void CheckpointerMain(void) pg_attribute_noreturn();
extern void RequestCheckpoint(int flags);
-extern void CheckpointWriteDelay(int flags, double progress);
+extern void CheckpointWriteDelay(int flags, double progress,
+ FileFlushContext * context, int ctx_size);
extern bool ForwardFsyncRequest(RelFileNode rnode, ForkNumber forknum,
BlockNumber segno);
diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.h
index c228f39..4fd3ff5 100644
--- a/src/include/storage/bufmgr.h
+++ b/src/include/storage/bufmgr.h
@@ -54,6 +54,14 @@ extern int bgwriter_lru_maxpages;
extern double bgwriter_lru_multiplier;
extern bool track_io_timing;
extern int target_prefetch_pages;
+
+#ifdef HAVE_SYNC_FILE_RANGE
+#define DEFAULT_CHECKPOINT_FLUSH_TO_DISK true
+#else
+#define DEFAULT_CHECKPOINT_FLUSH_TO_DISK false
+#endif /* HAVE_SYNC_FILE_RANGE */
+
+extern bool checkpoint_flush_to_disk;
extern bool checkpoint_sort;
/* in buf_init.c */
diff --git a/src/include/storage/fd.h b/src/include/storage/fd.h
index 7eabe09..c7b2a6d 100644
--- a/src/include/storage/fd.h
+++ b/src/include/storage/fd.h
@@ -59,6 +59,24 @@ extern int max_files_per_process;
*/
extern int max_safe_fds;
+/*
+ * FileFlushContext structure:
+ *
+ * This is used to accumulate several flush requests on a file
+ * into a larger flush request.
+ * - fd: file descriptor of the file
+ * - ncalls: number of flushes merged together
+ * - offset: starting offset (minimum of all offsets)
+ * - nbytes: size (minimum extent to cover all flushed data)
+ * - filename: filename of fd for error messages
+ */
+typedef struct FileFlushContext {
+ int fd;
+ int ncalls;
+ off_t offset;
+ off_t nbytes;
+ char * filename;
+} FileFlushContext;
/*
* prototypes for functions in fd.c
@@ -70,7 +88,12 @@ extern File OpenTemporaryFile(bool interXact);
extern void FileClose(File file);
extern int FilePrefetch(File file, off_t offset, int amount);
extern int FileRead(File file, char *buffer, int amount);
-extern int FileWrite(File file, char *buffer, int amount);
+extern void ResetFileFlushContext(FileFlushContext * context);
+extern void PerformFileFlush(FileFlushContext * context);
+extern void FileAsynchronousFlush(FileFlushContext * context,
+ int fd, off_t offset, off_t nbytes, char * filename);
+extern int FileWrite(File file, char *buffer, int amount, bool flush_to_disk,
+ FileFlushContext * context);
extern int FileSync(File file);
extern off_t FileSeek(File file, off_t offset, int whence);
extern int FileTruncate(File file, off_t offset);
diff --git a/src/include/storage/smgr.h b/src/include/storage/smgr.h
index 69a624f..a46a70c 100644
--- a/src/include/storage/smgr.h
+++ b/src/include/storage/smgr.h
@@ -16,6 +16,7 @@
#include "fmgr.h"
#include "storage/block.h"
+#include "storage/fd.h"
#include "storage/relfilenode.h"
@@ -95,7 +96,8 @@ extern void smgrprefetch(SMgrRelation reln, ForkNumber forknum,
extern void smgrread(SMgrRelation reln, ForkNumber forknum,
BlockNumber blocknum, char *buffer);
extern void smgrwrite(SMgrRelation reln, ForkNumber forknum,
- BlockNumber blocknum, char *buffer, bool skipFsync);
+ BlockNumber blocknum, char *buffer, bool skipFsync,
+ bool flush_to_disk, FileFlushContext * context);
extern BlockNumber smgrnblocks(SMgrRelation reln, ForkNumber forknum);
extern void smgrtruncate(SMgrRelation reln, ForkNumber forknum,
BlockNumber nblocks);
@@ -120,8 +122,9 @@ extern void mdprefetch(SMgrRelation reln, ForkNumber forknum,
BlockNumber blocknum);
extern void mdread(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
char *buffer);
-extern void mdwrite(SMgrRelation reln, ForkNumber forknum,
- BlockNumber blocknum, char *buffer, bool skipFsync);
+extern void mdwrite(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
+ char *buffer, bool skipFsync, bool flush_to_disk,
+ FileFlushContext * context);
extern BlockNumber mdnblocks(SMgrRelation reln, ForkNumber forknum);
extern void mdtruncate(SMgrRelation reln, ForkNumber forknum,
BlockNumber nblocks);
On 2015-09-06 16:05:01 +0200, Fabien COELHO wrote:
Wouldn't it be just as easy to put this logic into the checkpointing code?
Not sure it would simplify anything, because the checkpointer currently
knows about buffers but flushing is about files, which are hidden from
view.
It'd not really simplify things, but it'd keep it local.
* Wouldn't a binary heap over the tablespaces + progress be nicer?
I'm not sure where it would fit exactly.
Imagine a binaryheap.h style heap over a structure like (tablespaceid,
progress, progress_inc, nextbuf) where the comparator compares the progress.
Anyway, I think it would complicate the code significantly (compared to the
straightforward array)
I doubt it. I mean instead of your GetNext you'd just do:
next_tblspc = DatumGetPointer(binaryheap_first(heap));
if (next_tblspc == 0)
return 0;
next_tblspc.progress += next_tblspc.progress_slice;
binaryheap_replace_first(PointerGetDatum(next_tblspc));
return next_tblspc.nextbuf++;
progress_slice is the number of buffers in the tablespace divided by the
number of total buffers, to avoid doing any sort of expensive math in
the more frequently executed path.
Moreover such a data structure would probably require some kind of pointer
(probably 8 bytes added per node, maybe more), and the amount of memory is
already a concern, at least to me, and moreover it has to reside in shared
memory which does not simplify allocation of tree data structures.
I'm not seing where you'd need an extra pointer? Maybe the
misunderstanding is that I'm proposing to do a heap over the
*tablespaces* not the actual buffers.
If you make the sorting criterion include the tablespace id you wouldn't
need the lookahead loop in NextBufferToWrite().Yep, I thought of it. It would mean 4 more bytes per buffer, and bsearch to
find the boundaries, so significantly less simple code.
What for would you need to bsearch?
I think that the current approach is ok as the number of tablespace
should be small.
Right that's often the case.
Isn't the current approach O(NBuffers^2) in the worst case?
ISTM that the overall lookahead complexity is Nbuffers * Ntablespace:
buffers are scanned once for each tablespace.
Which in the worst case is NBuffers * 2...
ISTM that using a tablespace in the sorting would reduce the complexity to
ln(NBuffers) * Ntablespace for finding the boundaries, and then Nbuffers *
(Ntablespace/Ntablespace) = NBuffers for scanning, at the expense of more
memory and code complexity.
Afaics finding the boundaries can be done as part of the enumeration of
tablespaces in BufferSync(). That code needs to be moved, but that's not
too bad. I don't see the code be that much more complicated?
Greetings,
Andres Freund
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Mon, Sep 7, 2015 at 3:09 AM, Andres Freund <andres@anarazel.de> wrote:
On 2015-09-06 16:05:01 +0200, Fabien COELHO wrote:
Wouldn't it be just as easy to put this logic into the checkpointing
code?
Not sure it would simplify anything, because the checkpointer currently
knows about buffers but flushing is about files, which are hidden from
view.It'd not really simplify things, but it'd keep it local.
How about using the value of guc (checkpoint_flush_to_disk) and
AmCheckpointerProcess to identify whether to do async flush in FileWrite?
With Regards,
Amit Kapila.
EnterpriseDB: http://www.enterprisedb.com
Hello Amit,
It'd not really simplify things, but it'd keep it local.
How about using the value of guc (checkpoint_flush_to_disk) and
AmCheckpointerProcess to identify whether to do async flush in
FileWrite?
ISTM that what you suggest would just replace the added function arguments
with global variables to communicate and keep the necessary data for
managing the asynchronous flushing, which is called per tablespace
(1) on file changes (2) when the checkpointer is going to sleep.
Although it can be done obviously, I prefer to have functions arguments
rather than global variables, on principle.
Also, because of (2) and of the dependency on the number of tablespaces
being flushed, the flushing stuff cannot be fully hidden from the
checkpointer anyway.
Also I think that probably the bgwriter should do something similar, so
function parameters would be useful to drive flushing from it, rather than
adding yet another set of global variables, or share the same variables
for somehow different purposes.
So having these added parameters look reasonable to me.
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Sat, Sep 5, 2015 at 12:26 PM, Fabien COELHO <coelho@cri.ensmp.fr> wrote:
I would be curious whether flushing helps, though.
Yes, me too. I think we should try to reach on consensus for exact
scenarios and configuration where this patch('es) can give benefit or we
want to verify if there is any regression as I have access to this m/c for
a very-very limited time. This m/c might get formatted soon for some other
purpose.Yep, it would be great if you have time for a flush test before it
disappears... I think it is advisable to disable the write cache as it may
also hide the impact of flushing.Still thinking... Depending on the results, it might be interesting to
have these tests run with the write cache enabled as well, to check how
much it interferes positively with performance.
I have done some tests with both the patches(sort+flush) and below
are results:
M/c details
--------------------
IBM POWER-8 24 cores, 192 hardware threads
RAM = 492GB
Test - 1 (Data Fits in shared_buffers)
--------------------------------------------------------
non-default settings used in script provided by Fabien upthread
used below options for pgbench and the same is used for rest
of tests as well.
fw) ## full speed parallel write pgbench
run="FW"
opts="-M prepared -P 1 -T $time $para"
;;
warmup=1000
scale=300
max_connections=300
shared_buffers=32GB
checkpoint_timeout=10min
time=7200
synchronous_commit=on
max_wal_size=15GB
para="-j 64 -c 128"
checkpoint_completion_target=0.8
checkpoint_flush_to_disk="on off"
checkpoint_sort="on off"
Flush - off and Sort - off
avg over 7203: 27480.350104 ± 12791.098857 [0.000000, 16009.400000,
32109.200000, 37629.000000, 51671.400000]
percent of values below 10.0: 2.8%
Flush - off and Sort - on
avg over 7200: 27482.501264 ± 12552.036065 [0.000000, 16587.250000,
31225.950000, 37516.450000, 51296.900000]
percent of values below 10.0: 2.8%
Flush - on and Sort - off
avg over 7200: 25214.757292 ± 11059.709509 [5268.000000, 14188.400000,
26472.450000, 35626.100000, 51479.000000]
percent of values below 10.0: 0.0%
Flush - on and Sort - on
avg over 7200: 26819.631722 ± 10589.745016 [5191.700000, 16825.450000,
29429.750000, 35707.950000, 51475.100000]
percent of values below 10.0: 0.0%
For this test run, the best results are when both the sort and flush options
are enabled, the value of lowest TPS is increased substantially without
sacrificing much on average or median TPS values (though there is ~9%
dip in median TPS value). When only sorting is enabled, there is neither
significant gain nor any loss. When only flush is enabled, there is
significant degradation in both average and median value of TPS ~8%
and ~21% respectively.
Test - 2 (Data doesn't fit in shared_buffers, but fits in RAM)
----------------------------------------------------------------------------------------
warmup=1000
scale=3000
max_connections=300
shared_buffers=32GB
checkpoint_timeout=10min
time=7200
synchronous_commit=on
max_wal_size=25GB
para="-j 64 -c 128"
checkpoint_completion_target=0.8
checkpoint_flush_to_disk="on off"
checkpoint_sort="on off"
Flush - off and Sort - off
avg over 7200: 5050.059444 ± 4884.528702 [0.000000, 98.100000, 4699.100000,
10125.950000, 13631.000000]
percent of values below 10.0: 7.7%
Flush - off and Sort - on
avg over 7200: 6194.150264 ± 4913.525651 [0.000000, 98.100000, 8982.000000,
10558.000000, 14035.200000]
percent of values below 10.0: 11.0%
Flush - on and Sort - off
avg over 7200: 2771.327472 ± 1860.963043 [287.900000, 2038.850000,
2375.500000, 2679.000000, 12862.000000]
percent of values below 10.0: 0.0%
Flush - on and Sort - on
avg over 7200: 6110.617722 ± 1939.381029 [1652.200000, 5215.100000,
5724.000000, 6196.550000, 13828.000000]
percent of values below 10.0: 0.0%
For this test run, again the best results are when both the sort and flush
options are enabled, the value of lowest TPS is increased substantially
and the average and median value of TPS has also increased to
~21% and ~22% respectively. When only sorting is enabled, there is a
significant gain in average and median TPS values, but then there is also
an increase in number of times when TPS is below 10 which is bad.
When only flush is enabled, there is significant degradation in both average
and median value of TPS to ~82% and ~97% respectively, now I am not
sure if such a big degradation could be expected for this case or it's just
a problem in this run, I have not repeated this test.
Test - 3 (Data doesn't fit in shared_buffers, but fits in RAM)
----------------------------------------------------------------------------------------
Same configuration and settings as above, but this time, I have enforced
Flush to use posix_fadvise() rather than sync_file_range() (basically
changed
code to comment out sync_file_range() and enable posix_fadvise()).
Flush - on and Sort - on
avg over 7200: 3400.915069 ± 739.626478 [1642.100000, 2965.550000,
3271.900000, 3558.800000, 6763.000000]
percent of values below 10.0: 0.0%
On using posix_fadvise(), the results for best case (both flush and sort as
on) shows significant degradation in average and median TPS values
by ~48% and ~43% which indicates that probably using posix_fadvise()
with the current options might not be the best way to achieve Flush.
Overall, I think this patch (sort+flush) brings a lot of value on table in
terms of stablizing the TPS during checkpoint, however some of the
cases like use of posix_fadvise() and the case (all data fits in
shared_buffers)
where the value of median TPS is regressed could be investigated
to see what can be done to improve them. I think more tests can be done
to ensure the benefit or regression of this patch, but for now this is what
best I can do.
With Regards,
Amit Kapila.
EnterpriseDB: http://www.enterprisedb.com
Hello Amit,
I have done some tests with both the patches(sort+flush) and below
are results:
Thanks a lot for these runs on this great harware!
Test - 1 (Data Fits in shared_buffers)
Rounded for easier comparison:
flush/sort
off off: 27480.4 ᅵ 12791.1 [ 0, 16009, 32109, 37629, 51671] (2.8%)
off on : 27482.5 ᅵ 12552.0 [ 0, 16587, 31226, 37516, 51297] (2.8%)
The two above case are pretty indistinguishable, sorting has no impact.
The 2.8% means more than 1 minute offline per hour (not necessarily a
whole minute, it may be distributed over the whole hour).
on off: 25214.8 ᅵ 11059.7 [5268, 14188, 26472, 35626, 51479] (0.0%)
on on : 26819.6 ᅵ 10589.7 [5192, 16825, 29430, 35708, 51475] (0.0%)
For this test run, the best results are when both the sort and flush
options are enabled, the value of lowest TPS is increased substantially
without sacrificing much on average or median TPS values (though there
is ~9% dip in median TPS value). When only sorting is enabled, there is
neither significant gain nor any loss. When only flush is enabled,
there is significant degradation in both average and median value of TPS
~8% and ~21% respectively.
I interpret the five numbers in bracket as an indicator of performance
stability: they should be equal for perfect stability. Once they show some
stability, the next point for me is to focus at the average performance. I
do not see a median decrease as a big issue if the average is reasonably
good.
Thus I essentially note the -2.5% dip on average of on-on vs off-on. I
would say that it is probably significant, although it might be in the
error margin of the measure. Not sure whether the little stddev reduction
is really significant. Anyway the benefit is clear: 100% availability.
Flushing without sorting is a bad idea (tm), not a surprise.
Test - 2 (Data doesn't fit in shared_buffers, but fits in RAM)
flush/sort
off off: 5050.1 ᅵ 4884.5 [ 0, 98, 4699, 10126, 13631] ( 7.7%)
off on : 6194.2 ᅵ 4913.5 [ 0, 98, 8982, 10558, 14035] (11.0%)
on off: 2771.3 ᅵ 1861.0 [ 288, 2039, 2375, 2679, 12862] ( 0.0%)
on on : 6110.6 ᅵ 1939.3 [1652, 5215, 5724, 6196, 13828] ( 0.0%)
I'm not sure that the off-on vs on-on -1.3% avg tps dip is significant,
but it may be. With both flushing and sorting pg becomes fully available,
and the standard deviation is devided by more than 2, so the benefit is
clear.
For this test run, again the best results are when both the sort and flush
options are enabled, the value of lowest TPS is increased substantially
and the average and median value of TPS has also increased to
~21% and ~22% respectively. When only sorting is enabled, there is a
significant gain in average and median TPS values, but then there is also
an increase in number of times when TPS is below 10 which is bad.
When only flush is enabled, there is significant degradation in both average
and median value of TPS to ~82% and ~97% respectively, now I am not
sure if such a big degradation could be expected for this case or it's just
a problem in this run, I have not repeated this test.
Yes, I agree that it is strange that sorting without flushing on its own
both improves performance (+20% tps) but seems to degrade availability at
the same time. A rerun would have helped to check whether it is a fluke or
it is reproducible.
Test - 3 (Data doesn't fit in shared_buffers, but fits in RAM)
----------------------------------------------------------------------------------------
Same configuration and settings as above, but this time, I have enforced
Flush to use posix_fadvise() rather than sync_file_range() (basically
changed code to comment out sync_file_range() and enable posix_fadvise()).On using posix_fadvise(), the results for best case (both flush and sort as
on) shows significant degradation in average and median TPS values
by ~48% and ~43% which indicates that probably using posix_fadvise()
with the current options might not be the best way to achieve Flush.
Yes, indeed.
The way posix_fadvise is implemented on Linux is between no effect and bad
effect (the buffer is erased). You hit the later quite strongly... As you
are doing a "not fit in shared buffer" test, it is essential that buffers
are kept in ram, but posix_fadvise on Linux just instructs to erase the
buffer from memory if it was already passed to the I/O subsystem, which
given the probably large I/O device cache on your host should be done
pretty quickly, so that later read must be fetch back from the device
(either cache or disk), which means a drop in performance.
Note that FreeBSD implementation seems more convincing, although less good
than Linux sync_file_range function. I've no idea about other systems.
Overall, I think this patch (sort+flush) brings a lot of value on table
in terms of stablizing the TPS during checkpoint, however some of the
cases like use of posix_fadvise() and the case (all data fits in
shared_buffers) where the value of median TPS is regressed could be
investigated to see what can be done to improve them. I think more
tests can be done to ensure the benefit or regression of this patch, but
for now this is what best I can do.
Thanks a lot, again, for these tests!
I think that we may conclude, on these run:
(1) sorting seems not to harm performance, and may help a lot.
(2) Linux flushing with sync_file_range may degrade a little raw tps
average in some case, but definitely improves performance stability
(always 100% availability when on !).
(3) posix_fadvise on Linux is a bad idea... the good news is that it
is not needed there:-) How good or bad an idea it is on other system
is an open question...
These results are consistent with the current default values in the patch:
sorting is on by default, flushing is on with Linux and off otherwise
(posix_fadvise).
Also, as the effect on other systems is unclear, I think it is best to
keep both settings as GUCs for now.
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Tue, Sep 8, 2015 at 8:09 PM, Fabien COELHO <coelho@cri.ensmp.fr> wrote:
Thanks a lot, again, for these tests!
I think that we may conclude, on these run:
(1) sorting seems not to harm performance, and may help a lot.
I agree with first part, but about helping a lot, I am not sure based on
the tests conducted by me, among all the runs, it has shown improvement
in average TPS is one case and that too with a dip in number of times the
TPS is below 10.
(2) Linux flushing with sync_file_range may degrade a little raw tps
average in some case, but definitely improves performance stability
(always 100% availability when on !).
Agreed, I think the benefit is quite clear, but it would be better if we try
to do some more test for the cases (data fits in shared_buffers) where
we saw small regression just to make sure that regression is small.
(3) posix_fadvise on Linux is a bad idea... the good news is that it
is not needed there:-) How good or bad an idea it is on other system
is an open question...
I don't know what is the best way to verify that, if some body else has
access to such a m/c, please help to get that verified.
With Regards,
Amit Kapila.
EnterpriseDB: http://www.enterprisedb.com
Hello Amit,
I think that we may conclude, on these run:
(1) sorting seems not to harm performance, and may help a lot.
I agree with first part, but about helping a lot, I am not sure
I'm focussing on the "sort" dimension alone, that is I'm comparing the
average tps performance with sorting with the same test without sorting, :
There are 4 cases from your tests, if I'm not mistaken:
- T1 flush=off 27480 -> 27482 : +0.0%
- T1 flush=on 25214 -> 26819 : +6.3%
- T2 flush=off 5050 -> 6194 : +22.6%
- T2 flush=on 2771 -> 6110 : +120.4%
The average improvement induced by sort=on is +50%, if you do not agree on
"a lot", maybe we can agree on "significantly":-)
based on the tests conducted by me, among all the runs, it has shown
improvement in average TPS is one case and that too with a dip in number
of times the TPS is below 10.
(2) Linux flushing with sync_file_range may degrade a little raw tps
average in some case, but definitely improves performance stability
(always 100% availability when on !).Agreed, I think the benefit is quite clear, but it would be better if we try
to do some more test for the cases (data fits in shared_buffers) where
we saw small regression just to make sure that regression is small.
I've already reported a lot of tests (several hundred of hours on two
different hosts), and I did not have such a dip, but the hardware was more
"usual" or "casual", really different from your runs.
If you can run more tests, great!
I think that the main safeguard to handle the (small) uncertainty is to
keep gucs to control these features.
(3) posix_fadvise on Linux is a bad idea... the good news is that it
is not needed there:-) How good or bad an idea it is on other system
is an open question...I don't know what is the best way to verify that, if some body else has
access to such a m/c, please help to get that verified.
Yep. There has been such calls on this thread which were not very
effective, up to now.
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Tue, Sep 8, 2015 at 11:31 PM, Amit Kapila <amit.kapila16@gmail.com> wrote:
(3) posix_fadvise on Linux is a bad idea... the good news is that it
is not needed there:-) How good or bad an idea it is on other system
is an open question...I don't know what is the best way to verify that, if some body else has
access to such a m/c, please help to get that verified.
Why wouldn't we just leave it out then? Putting it in when the one
platform we've tried it on shows a regression makes no sense. We
shouldn't include it and then remove it if someone can prove it's bad;
we should only include it in the first place if we have good
benchmarks showing that it is good.
Does anyone have a big Windows box they can try this on?
--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
(3) posix_fadvise on Linux is a bad idea... the good news is that it
is not needed there:-) How good or bad an idea it is on other system
is an open question...I don't know what is the best way to verify that, if some body else has
access to such a m/c, please help to get that verified.Why wouldn't we just leave it out then? Putting it in when the one
platform we've tried it on shows a regression makes no sense. We
shouldn't include it and then remove it if someone can prove it's bad;
we should only include it in the first place if we have good benchmarks
showing that it is good.Does anyone have a big Windows box they can try this on?
Just a box with a disk would be enough, it does not need to be big!
As I wrote before, FreeBSD would be a good candidate because the
posix_fadvise seems much more reasonable than on Linux, and should be
profitable, so it would be a pity to remove it.
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 2015-09-09 20:56:15 +0200, Fabien COELHO wrote:
As I wrote before, FreeBSD would be a good candidate because the
posix_fadvise seems much more reasonable than on Linux, and should be
profitable, so it would be a pity to remove it.
Why do you think it's different on fbsd? Also, why is it unreasonable
that DONNEED removes stuff from the cache?
Greetings,
Andres Freund
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Hello Andres,
Wouldn't it be just as easy to put this logic into the checkpointing
code?Not sure it would simplify anything, because the checkpointer currently
knows about buffers but flushing is about files, which are hidden from
view.It'd not really simplify things, but it'd keep it local.
Ok, it would be local, but it would also mean that the checkpointer would
have to deal explicitely with files, whereas it currently does not have
to.
I think that the current buffer/file boundary is on engineering principle
a good one, so I tried to break it as little as possible to enable the
feature, and I wanted to avoid to have to do a buffer to file translation
twice, once in the checkpointer and once when writing the buffer.
* Wouldn't a binary heap over the tablespaces + progress be nicer?
I'm not sure where it would fit exactly.
Imagine a binaryheap.h style heap over a structure like (tablespaceid,
progress, progress_inc, nextbuf) where the comparator compares the progress.
It would replace what is currently an array. The balancing code needs to
enumerate all tablespaces in a round-robin way so as to ensure that all
tablespaces are given some attention, otherwise you could have a balance
on two tablespaces but others could be left out. The array makes this
property straightforward.
Anyway, I think it would complicate the code significantly (compared to the
straightforward array)I doubt it. I mean instead of your GetNext you'd just do:
next_tblspc = DatumGetPointer(binaryheap_first(heap));
if (next_tblspc == 0)
return 0;
next_tblspc.progress += next_tblspc.progress_slice;
binaryheap_replace_first(PointerGetDatum(next_tblspc));return next_tblspc.nextbuf++;
Compare to the array, this tree approach would required ln(Ntablespace)
work to extract and reinsert the tablespace under progress, so there is no
complexity advantage.
Moreover, given that in most cases there are 1 or 2 tablespaces, a tree
structure is really on the heavy side.
progress_slice is the number of buffers in the tablespace divided by the
number of total buffers, to avoid doing any sort of expensive math in
the more frequently executed path.
If there are many buffers, I'm not too sure about rounding issues and the
like, so the current approach with a rational seems more secure.
[...] I'm not seing where you'd need an extra pointer?
Indeed, I misunderstood.
[...] What for would you need to bsearch?
To find the tablespace boundaries in the sorted buffer array in
log(NBuffers) * Ntablespace, instead of NBuffers.
I think that the current approach is ok as the number of tablespace
should be small.Right that's often the case.
Yep.
ISTM that using a tablespace in the sorting would reduce the complexity to
ln(NBuffers) * Ntablespace for finding the boundaries, and then Nbuffers *
(Ntablespace/Ntablespace) = NBuffers for scanning, at the expense of more
memory and code complexity.Afaics finding the boundaries can be done as part of the enumeration of
tablespaces in BufferSync(). That code needs to be moved, but that's not
too bad. I don't see the code be that much more complicated?
Hmmm. you are proposing to replace prooved and heavilly tested code by a
more complex tree data structures distributed quite differently around the
source, and no very clear benefit.
So I would prefer to keep the code as is, that is pretty straightforward,
and wait for a strong incentive before doing anything fancier. ISTM that
there are other places in pg need attention more that further tweaking
this patch.
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 2015-09-09 21:29:12 +0200, Fabien COELHO wrote:
Imagine a binaryheap.h style heap over a structure like (tablespaceid,
progress, progress_inc, nextbuf) where the comparator compares the progress.It would replace what is currently an array.
It'd still be one afterwards.
The balancing code needs to enumerate all tablespaces in a round-robin
way so as to ensure that all tablespaces are given some attention,
otherwise you could have a balance on two tablespaces but others could
be left out. The array makes this property straightforward.
Why would a heap as I've described it require that?
Anyway, I think it would complicate the code significantly (compared to the
straightforward array)I doubt it. I mean instead of your GetNext you'd just do:
next_tblspc = DatumGetPointer(binaryheap_first(heap));
if (next_tblspc == 0)
return 0;
next_tblspc.progress += next_tblspc.progress_slice;
binaryheap_replace_first(PointerGetDatum(next_tblspc));return next_tblspc.nextbuf++;
Compare to the array, this tree approach would required ln(Ntablespace) work
to extract and reinsert the tablespace under progress, so there is no
complexity advantage.
extract/reinsert is actually O(1).
progress_slice is the number of buffers in the tablespace divided by the
number of total buffers, to avoid doing any sort of expensive math in
the more frequently executed path.If there are many buffers, I'm not too sure about rounding issues and the
like, so the current approach with a rational seems more secure.
Meh. The amount of imbalance introduced by rounding won't matter.
ISTM that using a tablespace in the sorting would reduce the complexity to
ln(NBuffers) * Ntablespace for finding the boundaries, and then Nbuffers *
(Ntablespace/Ntablespace) = NBuffers for scanning, at the expense of more
memory and code complexity.Afaics finding the boundaries can be done as part of the enumeration of
tablespaces in BufferSync(). That code needs to be moved, but that's not
too bad. I don't see the code be that much more complicated?Hmmm. you are proposing to replace prooved and heavilly tested code by a
more complex tree data structures distributed quite differently around the
source, and no very clear benefit.
There's no "proved and heavily tested code" touched here.
So I would prefer to keep the code as is, that is pretty straightforward,
and wait for a strong incentive before doing anything fancier.
I find the proposed code not particularly pretty, so I don't really buy
the straightforwardness argument.
Greetings,
Andres Freund
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
As I wrote before, FreeBSD would be a good candidate because the
posix_fadvise seems much more reasonable than on Linux, and should be
profitable, so it would be a pity to remove it.Why do you think it's different on fbsd? Also, why is it unreasonable
that DONNEED removes stuff from the cache?
Yep, I agree that this part is a bad point, obviously, but at least there
is also some advantage: I understood that buffers are actually pushed
towards the disk when calling posix_fadvise with DONTNEED on FreeBSD, so
in-buffer tests shoud see better performance, and out-of-buffer in-memory
tests would probably be degraded as Amit test shown on Linux. As an admin
I can choose if I know whether I run in buffer or not.
On Linux either the call is ignored (if the page is not written yet) or
the page is coldly removed, so it has either no effect or a bad effect,
basically.
So I think that the current off default when running with posix_fadvise is
reasonable, and in some case turning it on can probably provide a better
performance stability, esp for in-buffer runs.
Now, franckly I do not care much about FreeBSD or Windows, so I'm fine
with dropping posix_fadvise if this is a blocker.
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
It would replace what is currently an array.
It'd still be one afterwards.
[...]
extract/reinsert is actually O(1).
Hm, strange. I probably did not understood at all the heap structure
you're suggesting. No big deal.
[...] Why would a heap as I've described it require that?
Hmmm... The heap does *not* require anything, the *balancing* requires
this property.
[...] There's no "proved and heavily tested code" touched here.
I've prooved and tested heavily the submitted patch based on an array,
that you want to replace with some heap, so I think that my point stands.
Moreover, I do not see a clear benefit in changing the data structure.
So I would prefer to keep the code as is, that is pretty straightforward,
and wait for a strong incentive before doing anything fancier.I find the proposed code not particularly pretty, so I don't really buy
the straightforwardness argument.
No big deal. From my point of view, the data structure change you're
suggesting does not bring significant value, so there is no good reason to
do it.
If you want to submit another patch, this is free software, please
proceed.
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Wed, Sep 9, 2015 at 2:31 PM, Fabien COELHO <coelho@cri.ensmp.fr> wrote:
Hello Amit,
I think that we may conclude, on these run:
(1) sorting seems not to harm performance, and may help a lot.
I agree with first part, but about helping a lot, I am not sure
I'm focussing on the "sort" dimension alone, that is I'm comparing the
average tps performance with sorting with the same test without sorting, :
There are 4 cases from your tests, if I'm not mistaken:
- T1 flush=off 27480 -> 27482 : +0.0%
- T1 flush=on 25214 -> 26819 : +6.3%
- T2 flush=off 5050 -> 6194 : +22.6%
- T2 flush=on 2771 -> 6110 : +120.4%
There is a clear win only in cases when sort is used with flush, apart
from that using sort alone doesn't have any clear advantage.
With Regards,
Amit Kapila.
EnterpriseDB: http://www.enterprisedb.com
On Wed, Sep 9, 2015 at 12:12 PM, Andres Freund <andres@anarazel.de> wrote:
On 2015-09-09 20:56:15 +0200, Fabien COELHO wrote:
As I wrote before, FreeBSD would be a good candidate because the
posix_fadvise seems much more reasonable than on Linux, and should be
profitable, so it would be a pity to remove it.Why do you think it's different on fbsd? Also, why is it unreasonable
that DONNEED removes stuff from the cache?
It seems kind of silly that it means "No one, even people I am not aware of
and have no right to speak for, needs this" as opposed to "I don't need
this, don't keep it around on my behalf."
Cheers,
Jeff
Hello Amit,
- T1 flush=off 27480 -> 27482 : +0.0%
- T1 flush=on 25214 -> 26819 : +6.3%
- T2 flush=off 5050 -> 6194 : +22.6%
- T2 flush=on 2771 -> 6110 : +120.4%There is a clear win only in cases when sort is used with flush, apart
from that using sort alone doesn't have any clear advantage.
Indeed, I agree that the improvement is much smaller without flushing,
although it is there somehow (+0.0 & +22.6 => +11.3% on average).
Well, at least we may agree that it is "somehow significantly better" ?:-)
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Thanks for the hints! Two-part v12 attached fixes these.
Here is a v13, which is just a rebase after 1aba62ec.
--
Fabien.
Attachments:
checkpoint-continuous-flush-13a.patchtext/x-diff; name=checkpoint-continuous-flush-13a.patchDownload
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 9e7bcf5..2ef21fb 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -2457,6 +2457,28 @@ include_dir 'conf.d'
</listitem>
</varlistentry>
+ <varlistentry id="guc-checkpoint-sort" xreflabel="checkpoint_sort">
+ <term><varname>checkpoint_sort</varname> (<type>bool</type>)
+ <indexterm>
+ <primary><varname>checkpoint_sort</> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ Whether to sort buffers before writting them out to disk on checkpoint.
+ For a HDD storage, this setting allows to group together
+ neighboring pages written to disk, thus improving performance by
+ reducing random write activity.
+ This sorting should have limited performance effects on SSD backends
+ as such storages have good random write performance, but it may
+ help with wear-leveling so be worth keeping anyway.
+ The default is <literal>on</>.
+ This parameter can only be set in the <filename>postgresql.conf</>
+ file or on the server command line.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="guc-checkpoint-warning" xreflabel="checkpoint_warning">
<term><varname>checkpoint_warning</varname> (<type>integer</type>)
<indexterm>
diff --git a/doc/src/sgml/wal.sgml b/doc/src/sgml/wal.sgml
index e3941c9..f538698 100644
--- a/doc/src/sgml/wal.sgml
+++ b/doc/src/sgml/wal.sgml
@@ -546,6 +546,18 @@
</para>
<para>
+ When hard-disk drives (HDD) are used for terminal data storage
+ <xref linkend="guc-checkpoint-sort"> allows to sort pages
+ so that neighboring pages on disk will be flushed together by
+ chekpoints, reducing the random write load and improving performance.
+ If solid-state drives (SSD) are used, sorting pages induces no benefit
+ as their random write I/O performance is good: this feature could then
+ be disabled by setting <varname>checkpoint_sort</> to <value>off</>.
+ It is possible that sorting may help with SSD wear leveling, so it may
+ be kept on that account.
+ </para>
+
+ <para>
The number of WAL segment files in <filename>pg_xlog</> directory depends on
<varname>min_wal_size</>, <varname>max_wal_size</> and
the amount of WAL generated in previous checkpoint cycles. When old log
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 152d4ed..7291447 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -7999,11 +7999,13 @@ LogCheckpointEnd(bool restartpoint)
sync_secs,
total_secs,
longest_secs,
+ sort_secs,
average_secs;
int write_usecs,
sync_usecs,
total_usecs,
longest_usecs,
+ sort_usecs,
average_usecs;
uint64 average_sync_time;
@@ -8034,6 +8036,10 @@ LogCheckpointEnd(bool restartpoint)
CheckpointStats.ckpt_end_t,
&total_secs, &total_usecs);
+ TimestampDifference(CheckpointStats.ckpt_sort_t,
+ CheckpointStats.ckpt_sort_end_t,
+ &sort_secs, &sort_usecs);
+
/*
* Timing values returned from CheckpointStats are in microseconds.
* Convert to the second plus microsecond form that TimestampDifference
@@ -8052,8 +8058,8 @@ LogCheckpointEnd(bool restartpoint)
elog(LOG, "%s complete: wrote %d buffers (%.1f%%); "
"%d transaction log file(s) added, %d removed, %d recycled; "
- "write=%ld.%03d s, sync=%ld.%03d s, total=%ld.%03d s; "
- "sync files=%d, longest=%ld.%03d s, average=%ld.%03d s; "
+ "sort=%ld.%03d s, write=%ld.%03d s, sync=%ld.%03d s, total=%ld.%03d s;"
+ " sync files=%d, longest=%ld.%03d s, average=%ld.%03d s; "
"distance=%d kB, estimate=%d kB",
restartpoint ? "restartpoint" : "checkpoint",
CheckpointStats.ckpt_bufs_written,
@@ -8061,6 +8067,7 @@ LogCheckpointEnd(bool restartpoint)
CheckpointStats.ckpt_segs_added,
CheckpointStats.ckpt_segs_removed,
CheckpointStats.ckpt_segs_recycled,
+ sort_secs, sort_usecs / 1000,
write_secs, write_usecs / 1000,
sync_secs, sync_usecs / 1000,
total_secs, total_usecs / 1000,
diff --git a/src/backend/storage/buffer/buf_init.c b/src/backend/storage/buffer/buf_init.c
index 3ae2848..3bd5eab 100644
--- a/src/backend/storage/buffer/buf_init.c
+++ b/src/backend/storage/buffer/buf_init.c
@@ -65,7 +65,8 @@ void
InitBufferPool(void)
{
bool foundBufs,
- foundDescs;
+ foundDescs,
+ foundCpid;
/* Align descriptors to a cacheline boundary. */
BufferDescriptors = (BufferDescPadded *) CACHELINEALIGN(
@@ -77,10 +78,14 @@ InitBufferPool(void)
ShmemInitStruct("Buffer Blocks",
NBuffers * (Size) BLCKSZ, &foundBufs);
- if (foundDescs || foundBufs)
+ CheckpointBufferIds = (CheckpointSortItem *)
+ ShmemInitStruct("Checkpoint BufferIds",
+ NBuffers * sizeof(CheckpointSortItem), &foundCpid);
+
+ if (foundDescs || foundBufs || foundCpid)
{
- /* both should be present or neither */
- Assert(foundDescs && foundBufs);
+ /* all should be present or neither */
+ Assert(foundDescs && foundBufs && foundCpid);
/* note: this path is only taken in EXEC_BACKEND case */
}
else
@@ -144,5 +149,8 @@ BufferShmemSize(void)
/* size of stuff controlled by freelist.c */
size = add_size(size, StrategyShmemSize());
+ /* size of checkpoint sort array in bufmgr.c */
+ size = add_size(size, mul_size(NBuffers, sizeof(CheckpointSortItem)));
+
return size;
}
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index 8c0358e..09af13b 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -75,12 +75,37 @@ typedef struct PrivateRefCountEntry
/* 64 bytes, about the size of a cache line on common systems */
#define REFCOUNT_ARRAY_ENTRIES 8
+/*
+ * Status of buffers to checkpoint for a particular tablespace,
+ * used internally in BufferSync.
+ * - space: oid of the tablespace
+ * - num_to_write: number of checkpoint pages counted for this tablespace
+ * - num_written: number of pages actually written out
+ * - index: scanning position in CheckpointBufferIds for this tablespace
+ */
+typedef struct TableSpaceCheckpointStatus {
+ Oid space;
+ int num_to_write;
+ int num_written;
+ int index;
+} TableSpaceCheckpointStatus;
+
+/*
+ * Entry structure for table space to count hashtable,
+ * used internally in BufferSync.
+ */
+typedef struct TableSpaceCountEntry {
+ Oid space;
+ int count;
+} TableSpaceCountEntry;
+
/* GUC variables */
bool zero_damaged_pages = false;
int bgwriter_lru_maxpages = 100;
double bgwriter_lru_multiplier = 2.0;
bool track_io_timing = false;
int effective_io_concurrency = 0;
+bool checkpoint_sort = true;
/*
* How many buffers PrefetchBuffer callers should try to stay ahead of their
@@ -98,6 +123,9 @@ static bool IsForInput;
/* local state for LockBufferForCleanup */
static volatile BufferDesc *PinCountWaitBuf = NULL;
+/* array of buffer ids & sort criterion of all buffers to checkpoint */
+CheckpointSortItem *CheckpointBufferIds = NULL;
+
/*
* Backend-Private refcount management:
*
@@ -1622,6 +1650,106 @@ UnpinBuffer(volatile BufferDesc *buf, bool fixOwner)
}
}
+/* checkpoint buffers comparison */
+static int
+bufcmp(const void * pa, const void * pb)
+{
+ CheckpointSortItem
+ *a = (CheckpointSortItem *) pa,
+ *b = (CheckpointSortItem *) pb;
+
+ /* compare relation */
+ if (a->relNode < b->relNode)
+ return -1;
+ else if (a->relNode > b->relNode)
+ return 1;
+ /* same relation, compare fork */
+ else if (a->forkNum < b->forkNum)
+ return -1;
+ else if (a->forkNum > b->forkNum)
+ return 1;
+ /* same relation/fork, so same segmented "file", compare block number
+ * which are mapped on different segments depending on the number.
+ */
+ else if (a->blockNum < b->blockNum)
+ return -1;
+ else /* should not be the same block anyway... */
+ return 1;
+}
+
+/*
+ * Return the next buffer to write, or -1.
+ * this function balances buffers over tablespaces, see comment inside.
+ */
+static int
+NextBufferToWrite(TableSpaceCheckpointStatus *spcStatus, int nb_spaces,
+ int *pspace, int num_to_write, int num_written)
+{
+ int space = *pspace, buf_id = -1, index;
+
+ /*
+ * Select a tablespace depending on the current overall progress.
+ *
+ * The progress ratio of each unfinished tablespace is compared to
+ * the overall progress ratio to find one with is not in advance
+ * (i.e. overall ratio > tablespace ratio,
+ * i.e. tablespace written/to_write > overall written/to_write
+ *
+ * Existence: it is bound to exist otherwise the overall progress
+ * ratio would be inconsistent: with positive buffers to write (t1 & t2)
+ * and already written buffers (w1 & w2), we have:
+ *
+ * If w1/t1 > (w1+w2)/(t1+t2) # one table space is in advance
+ * => w1t1+w1t2 > w1t1+w2t1 => w1t2 > w2t1 => w1t2+w2t2 > w2t1+w2t2
+ * => (w1+w2) / (t1+t2) > w2 / t2 # the other one is late
+ *
+ * The round robin ensures that each space is given some attention
+ * till it is over the current ratio, before going to the next.
+ *
+ * Precision: using int32 computations for comparing fractions
+ * (w1 / t1 > w / t <=> w1 t > w t1) seems a bad idea as the values
+ * can overflow 32-bit integers: the limit would be sqrt(2**31) ~
+ * 46340 buffers, i.e. a 362 MB checkpoint. So ensure that 64-bit
+ * integers are used in the comparison.
+ */
+ while ((int64) spcStatus[space].num_written * num_to_write >
+ (int64) num_written * spcStatus[space].num_to_write)
+ space = (space + 1) % nb_spaces; /* round robin */
+
+ /*
+ * Find a valid buffer in the selected tablespace,
+ * by continuing the tablespace specific buffer scan
+ * where it was left.
+ */
+ index = spcStatus[space].index;
+
+ while (index < num_to_write && buf_id == -1)
+ {
+ volatile BufferDesc *bufHdr;
+
+ buf_id = CheckpointBufferIds[index].buf_id;
+ bufHdr = GetBufferDescriptor(buf_id);
+
+ /*
+ * Skip if in another tablespace or not in checkpoint anymore.
+ * No lock is acquired, see comments below.
+ */
+ if (spcStatus[space].space != bufHdr->tag.rnode.spcNode ||
+ ! (bufHdr->flags & BM_CHECKPOINT_NEEDED))
+ {
+ index ++;
+ buf_id = -1;
+ }
+ }
+
+ /* update tablespace writing status, will start over at next index */
+ spcStatus[space].index = index + 1;
+
+ *pspace = space;
+
+ return buf_id;
+}
+
/*
* BufferSync -- Write out all dirty buffers in the pool.
*
@@ -1635,11 +1763,13 @@ UnpinBuffer(volatile BufferDesc *buf, bool fixOwner)
static void
BufferSync(int flags)
{
- int buf_id;
- int num_to_scan;
+ int buf_id = -1;
int num_to_write;
int num_written;
int mask = BM_DIRTY;
+ HTAB *spcBuffers;
+ TableSpaceCheckpointStatus *spcStatus = NULL;
+ int nb_spaces, space;
/* Make sure we can handle the pin inside SyncOneBuffer */
ResourceOwnerEnlargeBuffers(CurrentResourceOwner);
@@ -1670,6 +1800,18 @@ BufferSync(int flags)
* certainly need to be written for the next checkpoint attempt, too.
*/
num_to_write = 0;
+
+ /* initialize oid -> int buffer count hash table */
+ {
+ HASHCTL ctl;
+
+ MemSet(&ctl, 0, sizeof(HASHCTL));
+ ctl.keysize = sizeof(Oid);
+ ctl.entrysize = sizeof(TableSpaceCountEntry);
+ spcBuffers = hash_create("Number of buffers to write per tablespace",
+ 16, &ctl, HASH_ELEM | HASH_BLOBS);
+ }
+
for (buf_id = 0; buf_id < NBuffers; buf_id++)
{
volatile BufferDesc *bufHdr = GetBufferDescriptor(buf_id);
@@ -1682,32 +1824,99 @@ BufferSync(int flags)
if ((bufHdr->flags & mask) == mask)
{
+ Oid spc;
+ TableSpaceCountEntry * entry;
+ bool found;
+
bufHdr->flags |= BM_CHECKPOINT_NEEDED;
+ CheckpointBufferIds[num_to_write].buf_id = buf_id;
+ CheckpointBufferIds[num_to_write].relNode = bufHdr->tag.rnode.relNode;
+ CheckpointBufferIds[num_to_write].forkNum = bufHdr->tag.forkNum;
+ CheckpointBufferIds[num_to_write].blockNum = bufHdr->tag.blockNum;
num_to_write++;
+
+ /* keep track of per tablespace buffers */
+ spc = bufHdr->tag.rnode.spcNode;
+ entry = (TableSpaceCountEntry *)
+ hash_search(spcBuffers, (void *) &spc, HASH_ENTER, &found);
+
+ if (found)
+ entry->count++;
+ else
+ entry->count = 1;
}
UnlockBufHdr(bufHdr);
}
if (num_to_write == 0)
+ {
+ hash_destroy(spcBuffers);
return; /* nothing to do */
+ }
TRACE_POSTGRESQL_BUFFER_SYNC_START(NBuffers, num_to_write);
+ /* Build checkpoint tablespace buffer status */
+ nb_spaces = hash_get_num_entries(spcBuffers);
+ spcStatus = (TableSpaceCheckpointStatus *)
+ palloc(sizeof(TableSpaceCheckpointStatus) * nb_spaces);
+
+ {
+ int index = 0;
+ HASH_SEQ_STATUS hseq;
+ TableSpaceCountEntry * entry;
+
+ hash_seq_init(&hseq, spcBuffers);
+ while ((entry = (TableSpaceCountEntry *) hash_seq_search(&hseq)))
+ {
+ Assert(index < nb_spaces);
+ spcStatus[index].space = entry->space;
+ spcStatus[index].num_to_write = entry->count;
+ spcStatus[index].num_written = 0;
+ /* should it be randomized? chosen with some criterion? */
+ spcStatus[index].index = 0;
+
+ index ++;
+ }
+ }
+
+ hash_destroy(spcBuffers);
+ spcBuffers = NULL;
+
+ /* sort buffer ids to help find sequential writes */
+ CheckpointStats.ckpt_sort_t = GetCurrentTimestamp();
+
+ if (checkpoint_sort)
+ {
+ qsort(CheckpointBufferIds, num_to_write, sizeof(CheckpointSortItem),
+ bufcmp);
+ }
+
+ CheckpointStats.ckpt_sort_end_t = GetCurrentTimestamp();
+
/*
- * Loop over all buffers again, and write the ones (still) marked with
- * BM_CHECKPOINT_NEEDED. In this loop, we start at the clock sweep point
- * since we might as well dump soon-to-be-recycled buffers first.
+ * Loop over buffers to write through CheckpointBufferIds,
+ * and write the ones (still) marked with BM_CHECKPOINT_NEEDED,
+ * with some round robin over table spaces so as to balance writes,
+ * so that buffer writes move forward roughly proportionally for each
+ * tablespace.
*
- * Note that we don't read the buffer alloc count here --- that should be
- * left untouched till the next BgBufferSync() call.
+ * Termination: if a tablespace is selected by the inner while loop
+ * (see argument there), its index is incremented and will eventually
+ * reach num_to_write, mark this table space scanning as done and
+ * decrement the number of (active) spaces, which will thus reach 0.
*/
- buf_id = StrategySyncStart(NULL, NULL);
- num_to_scan = NBuffers;
+ space = 0;
num_written = 0;
- while (num_to_scan-- > 0)
+
+ while (nb_spaces != 0)
{
- volatile BufferDesc *bufHdr = GetBufferDescriptor(buf_id);
+ volatile BufferDesc *bufHdr = NULL;
+ buf_id = NextBufferToWrite(spcStatus, nb_spaces, &space,
+ num_to_write, num_written);
+ if (buf_id != -1)
+ bufHdr = GetBufferDescriptor(buf_id);
/*
* We don't need to acquire the lock here, because we're only looking
@@ -1721,39 +1930,46 @@ BufferSync(int flags)
* write the buffer though we didn't need to. It doesn't seem worth
* guarding against this, though.
*/
- if (bufHdr->flags & BM_CHECKPOINT_NEEDED)
+ if (bufHdr != NULL && bufHdr->flags & BM_CHECKPOINT_NEEDED)
{
if (SyncOneBuffer(buf_id, false) & BUF_WRITTEN)
{
TRACE_POSTGRESQL_BUFFER_SYNC_WRITTEN(buf_id);
BgWriterStats.m_buf_written_checkpoints++;
+ spcStatus[space].num_written++;
num_written++;
/*
- * We know there are at most num_to_write buffers with
- * BM_CHECKPOINT_NEEDED set; so we can stop scanning if
- * num_written reaches num_to_write.
- *
- * Note that num_written doesn't include buffers written by
- * other backends, or by the bgwriter cleaning scan. That
- * means that the estimate of how much progress we've made is
- * conservative, and also that this test will often fail to
- * trigger. But it seems worth making anyway.
- */
- if (num_written >= num_to_write)
- break;
-
- /*
* Sleep to throttle our I/O rate.
*/
CheckpointWriteDelay(flags, (double) num_written / num_to_write);
}
}
- if (++buf_id >= NBuffers)
- buf_id = 0;
+ /*
+ * Detect checkpoint end for a tablespace: either the scan is done
+ * or all tablespace buffers have been written out. If so, the
+ * another active tablespace status is moved in place of the current
+ * one and the next round will start on this one, or maybe round about.
+ *
+ * Note: maybe an exchange could be made instead in order to keep
+ * informations about the closed table space, but this is currently
+ * not used afterwards.
+ */
+ if (spcStatus[space].index >= num_to_write ||
+ spcStatus[space].num_written >= spcStatus[space].num_to_write)
+ {
+ nb_spaces--;
+ if (space != nb_spaces)
+ spcStatus[space] = spcStatus[nb_spaces];
+ else
+ space = 0;
+ }
}
+ pfree(spcStatus);
+ spcStatus = NULL;
+
/*
* Update checkpoint statistics. As noted above, this doesn't include
* buffers written by other backends or bgwriter scan.
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index 8ebf424..1cd2aa0 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -1012,6 +1012,17 @@ static struct config_bool ConfigureNamesBool[] =
false,
NULL, NULL, NULL
},
+
+ {
+ {"checkpoint_sort", PGC_SIGHUP, WAL_CHECKPOINTS,
+ gettext_noop("Whether disk-page buffers are sorted on checkpoints."),
+ NULL
+ },
+ &checkpoint_sort,
+ true,
+ NULL, NULL, NULL
+ },
+
{
{"log_connections", PGC_SU_BACKEND, LOGGING_WHAT,
gettext_noop("Logs each successful connection."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 8c65287..8020c1c 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -201,6 +201,7 @@
#max_wal_size = 1GB
#min_wal_size = 80MB
#checkpoint_completion_target = 0.5 # checkpoint target duration, 0.0 - 1.0
+#checkpoint_sort = on # sort buffers on checkpoint
#checkpoint_warning = 30s # 0 disables
# - Archiving -
diff --git a/src/include/access/xlog.h b/src/include/access/xlog.h
index 790ca66..11815a8 100644
--- a/src/include/access/xlog.h
+++ b/src/include/access/xlog.h
@@ -186,6 +186,8 @@ extern bool XLOG_DEBUG;
typedef struct CheckpointStatsData
{
TimestampTz ckpt_start_t; /* start of checkpoint */
+ TimestampTz ckpt_sort_t; /* start buffer sorting */
+ TimestampTz ckpt_sort_end_t; /* end of sorting */
TimestampTz ckpt_write_t; /* start of flushing buffers */
TimestampTz ckpt_sync_t; /* start of fsyncs */
TimestampTz ckpt_sync_end_t; /* end of fsyncs */
diff --git a/src/include/storage/buf_internals.h b/src/include/storage/buf_internals.h
index 521ee1c..32f2006 100644
--- a/src/include/storage/buf_internals.h
+++ b/src/include/storage/buf_internals.h
@@ -210,6 +210,23 @@ extern PGDLLIMPORT BufferDescPadded *BufferDescriptors;
/* in localbuf.c */
extern BufferDesc *LocalBufferDescriptors;
+/* in bufmgr.c */
+
+/*
+ * Structure to sort buffers per file on checkpoints.
+ *
+ * This structure is allocated per buffer in shared memory, so it should be
+ * kept as little as possible. Maybe the sort criterion could be compacted
+ * to reduce memory requirement and for faster comparison?
+ */
+typedef struct CheckpointSortItem {
+ int buf_id;
+ Oid relNode;
+ ForkNumber forkNum; /* hm... enum with only 4 values */
+ BlockNumber blockNum;
+} CheckpointSortItem;
+
+extern CheckpointSortItem *CheckpointBufferIds;
/*
* Internal routines: only called by bufmgr
diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.h
index 0f59201..b56802b 100644
--- a/src/include/storage/bufmgr.h
+++ b/src/include/storage/bufmgr.h
@@ -54,6 +54,7 @@ extern int bgwriter_lru_maxpages;
extern double bgwriter_lru_multiplier;
extern bool track_io_timing;
extern int target_prefetch_pages;
+extern bool checkpoint_sort;
/* in buf_init.c */
extern PGDLLIMPORT char *BufferBlocks;
checkpoint-continuous-flush-13b.patchtext/x-diff; name=checkpoint-continuous-flush-13b.patchDownload
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 2ef21fb..356aed4 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -2500,6 +2500,24 @@ include_dir 'conf.d'
</listitem>
</varlistentry>
+ <varlistentry id="guc-checkpoint-flush-to-disk" xreflabel="checkpoint_flush_to_disk">
+ <term><varname>checkpoint_flush_to_disk</varname> (<type>bool</type>)
+ <indexterm>
+ <primary><varname>checkpoint_flush_to_disk</> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ When writing data for a checkpoint, hint the underlying OS that the
+ data must be sent to disk as soon as possible. This may help smoothing
+ disk I/O writes and avoid a stall when fsync is issued at the end of
+ the checkpoint, but it may also reduce average performance.
+ This setting may have no effect on some platforms.
+ The default is <literal>on</> on Linux, <literal>off</> otherwise.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="guc-min-wal-size" xreflabel="min_wal_size">
<term><varname>min_wal_size</varname> (<type>integer</type>)
<indexterm>
diff --git a/doc/src/sgml/wal.sgml b/doc/src/sgml/wal.sgml
index f538698..1b658f2 100644
--- a/doc/src/sgml/wal.sgml
+++ b/doc/src/sgml/wal.sgml
@@ -558,6 +558,18 @@
</para>
<para>
+ On Linux and POSIX platforms, <xref linkend="guc-checkpoint-flush-to-disk">
+ allows to hint the OS that pages written on checkpoints must be flushed
+ to disk quickly. Otherwise, these pages may be kept in cache for some time,
+ inducing a stall later when <literal>fsync</> is called to actually
+ complete the checkpoint. This setting helps to reduce transaction latency,
+ but it may also have a small adverse effect on the average transaction rate
+ at maximum throughput on some OS. It should be beneficial for high write
+ loads on HDD. This feature probably brings no benefit on SSD, as the I/O
+ write latency is small on such hardware, thus it may be disabled.
+ </para>
+
+ <para>
The number of WAL segment files in <filename>pg_xlog</> directory depends on
<varname>min_wal_size</>, <varname>max_wal_size</> and
the amount of WAL generated in previous checkpoint cycles. When old log
diff --git a/src/backend/access/heap/rewriteheap.c b/src/backend/access/heap/rewriteheap.c
index 6a6fc3b..2a8f645 100644
--- a/src/backend/access/heap/rewriteheap.c
+++ b/src/backend/access/heap/rewriteheap.c
@@ -918,7 +918,7 @@ logical_heap_rewrite_flush_mappings(RewriteState state)
* Note that we deviate from the usual WAL coding practices here,
* check the above "Logical rewrite support" comment for reasoning.
*/
- written = FileWrite(src->vfd, waldata_start, len);
+ written = FileWrite(src->vfd, waldata_start, len, false, NULL);
if (written != len)
ereport(ERROR,
(errcode_for_file_access(),
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index cf4a6dc..4b5e9cd 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -203,7 +203,7 @@ btbuildempty(PG_FUNCTION_ARGS)
/* Write the page. If archiving/streaming, XLOG it. */
PageSetChecksumInplace(metapage, BTREE_METAPAGE);
smgrwrite(index->rd_smgr, INIT_FORKNUM, BTREE_METAPAGE,
- (char *) metapage, true);
+ (char *) metapage, true, false, NULL);
if (XLogIsNeeded())
log_newpage(&index->rd_smgr->smgr_rnode.node, INIT_FORKNUM,
BTREE_METAPAGE, metapage, false);
diff --git a/src/backend/access/nbtree/nbtsort.c b/src/backend/access/nbtree/nbtsort.c
index f95f67a..ea7a45d 100644
--- a/src/backend/access/nbtree/nbtsort.c
+++ b/src/backend/access/nbtree/nbtsort.c
@@ -315,7 +315,7 @@ _bt_blwritepage(BTWriteState *wstate, Page page, BlockNumber blkno)
{
/* overwriting a block we zero-filled before */
smgrwrite(wstate->index->rd_smgr, MAIN_FORKNUM, blkno,
- (char *) page, true);
+ (char *) page, true, false, NULL);
}
pfree(page);
diff --git a/src/backend/access/spgist/spginsert.c b/src/backend/access/spgist/spginsert.c
index bceee8d..b700efb 100644
--- a/src/backend/access/spgist/spginsert.c
+++ b/src/backend/access/spgist/spginsert.c
@@ -170,7 +170,7 @@ spgbuildempty(PG_FUNCTION_ARGS)
/* Write the page. If archiving/streaming, XLOG it. */
PageSetChecksumInplace(page, SPGIST_METAPAGE_BLKNO);
smgrwrite(index->rd_smgr, INIT_FORKNUM, SPGIST_METAPAGE_BLKNO,
- (char *) page, true);
+ (char *) page, true, false, NULL);
if (XLogIsNeeded())
log_newpage(&index->rd_smgr->smgr_rnode.node, INIT_FORKNUM,
SPGIST_METAPAGE_BLKNO, page, false);
@@ -180,7 +180,7 @@ spgbuildempty(PG_FUNCTION_ARGS)
PageSetChecksumInplace(page, SPGIST_ROOT_BLKNO);
smgrwrite(index->rd_smgr, INIT_FORKNUM, SPGIST_ROOT_BLKNO,
- (char *) page, true);
+ (char *) page, true, false, NULL);
if (XLogIsNeeded())
log_newpage(&index->rd_smgr->smgr_rnode.node, INIT_FORKNUM,
SPGIST_ROOT_BLKNO, page, true);
@@ -190,7 +190,7 @@ spgbuildempty(PG_FUNCTION_ARGS)
PageSetChecksumInplace(page, SPGIST_NULL_BLKNO);
smgrwrite(index->rd_smgr, INIT_FORKNUM, SPGIST_NULL_BLKNO,
- (char *) page, true);
+ (char *) page, true, false, NULL);
if (XLogIsNeeded())
log_newpage(&index->rd_smgr->smgr_rnode.node, INIT_FORKNUM,
SPGIST_NULL_BLKNO, page, true);
diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c
index 3b3a09e..e361907 100644
--- a/src/backend/postmaster/checkpointer.c
+++ b/src/backend/postmaster/checkpointer.c
@@ -665,7 +665,8 @@ ImmediateCheckpointRequested(void)
* fraction between 0.0 meaning none, and 1.0 meaning all done.
*/
void
-CheckpointWriteDelay(int flags, double progress)
+CheckpointWriteDelay(int flags, double progress,
+ FileFlushContext * context, int ctx_size)
{
static int absorb_counter = WRITES_PER_ABSORB;
@@ -700,6 +701,26 @@ CheckpointWriteDelay(int flags, double progress)
*/
pgstat_send_bgwriter();
+#if defined(HAVE_SYNC_FILE_RANGE) || defined(HAVE_POSIX_FADVISE)
+
+ /*
+ * Before sleeping, flush written blocks for each tablespace.
+ */
+ if (checkpoint_flush_to_disk)
+ {
+ int i;
+
+ for (i = 0; i < ctx_size; i++)
+ {
+ if (context[i].ncalls != 0)
+ {
+ PerformFileFlush(&context[i]);
+ ResetFileFlushContext(&context[i]);
+ }
+ }
+ }
+#endif /* HAVE_SYNC_FILE_RANGE || HAVE_POSIX_FADVISE */
+
/*
* This sleep used to be connected to bgwriter_delay, typically 200ms.
* That resulted in more frequent wakeups if not much work to do.
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index 09af13b..deacec1 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -105,6 +105,8 @@ int bgwriter_lru_maxpages = 100;
double bgwriter_lru_multiplier = 2.0;
bool track_io_timing = false;
int effective_io_concurrency = 0;
+/* hint to move writes to high priority */
+bool checkpoint_flush_to_disk = DEFAULT_CHECKPOINT_FLUSH_TO_DISK;
bool checkpoint_sort = true;
/*
@@ -427,7 +429,8 @@ static bool PinBuffer(volatile BufferDesc *buf, BufferAccessStrategy strategy);
static void PinBuffer_Locked(volatile BufferDesc *buf);
static void UnpinBuffer(volatile BufferDesc *buf, bool fixOwner);
static void BufferSync(int flags);
-static int SyncOneBuffer(int buf_id, bool skip_recently_used);
+static int SyncOneBuffer(int buf_id, bool skip_recently_used,
+ bool flush_to_disk, FileFlushContext *context);
static void WaitIO(volatile BufferDesc *buf);
static bool StartBufferIO(volatile BufferDesc *buf, bool forInput);
static void TerminateBufferIO(volatile BufferDesc *buf, bool clear_dirty,
@@ -440,7 +443,8 @@ static volatile BufferDesc *BufferAlloc(SMgrRelation smgr,
BlockNumber blockNum,
BufferAccessStrategy strategy,
bool *foundPtr);
-static void FlushBuffer(volatile BufferDesc *buf, SMgrRelation reln);
+static void FlushBuffer(volatile BufferDesc *buf, SMgrRelation reln,
+ bool flush_to_disk, FileFlushContext *context);
static void AtProcExit_Buffers(int code, Datum arg);
static void CheckForBufferLeaks(void);
static int rnode_comparator(const void *p1, const void *p2);
@@ -1107,7 +1111,7 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
smgr->smgr_rnode.node.dbNode,
smgr->smgr_rnode.node.relNode);
- FlushBuffer(buf, NULL);
+ FlushBuffer(buf, NULL, false, NULL);
LWLockRelease(buf->content_lock);
TRACE_POSTGRESQL_BUFFER_WRITE_DIRTY_DONE(forkNum, blockNum,
@@ -1770,6 +1774,7 @@ BufferSync(int flags)
HTAB *spcBuffers;
TableSpaceCheckpointStatus *spcStatus = NULL;
int nb_spaces, space;
+ FileFlushContext * spcContext = NULL;
/* Make sure we can handle the pin inside SyncOneBuffer */
ResourceOwnerEnlargeBuffers(CurrentResourceOwner);
@@ -1857,10 +1862,12 @@ BufferSync(int flags)
TRACE_POSTGRESQL_BUFFER_SYNC_START(NBuffers, num_to_write);
- /* Build checkpoint tablespace buffer status */
+ /* Build checkpoint tablespace buffer status & flush context arrays */
nb_spaces = hash_get_num_entries(spcBuffers);
spcStatus = (TableSpaceCheckpointStatus *)
palloc(sizeof(TableSpaceCheckpointStatus) * nb_spaces);
+ spcContext = (FileFlushContext *)
+ palloc(sizeof(FileFlushContext) * nb_spaces);
{
int index = 0;
@@ -1877,6 +1884,12 @@ BufferSync(int flags)
/* should it be randomized? chosen with some criterion? */
spcStatus[index].index = 0;
+#if defined(HAVE_SYNC_FILE_RANGE) || defined(HAVE_POSIX_FADVISE)
+
+ ResetFileFlushContext(&spcContext[index]);
+
+#endif /* HAVE_SYNC_FILE_RANGE || HAVE_POSIX_FADVISE */
+
index ++;
}
}
@@ -1932,7 +1945,8 @@ BufferSync(int flags)
*/
if (bufHdr != NULL && bufHdr->flags & BM_CHECKPOINT_NEEDED)
{
- if (SyncOneBuffer(buf_id, false) & BUF_WRITTEN)
+ if (SyncOneBuffer(buf_id, false, checkpoint_flush_to_disk,
+ &spcContext[space]) & BUF_WRITTEN)
{
TRACE_POSTGRESQL_BUFFER_SYNC_WRITTEN(buf_id);
BgWriterStats.m_buf_written_checkpoints++;
@@ -1942,7 +1956,8 @@ BufferSync(int flags)
/*
* Sleep to throttle our I/O rate.
*/
- CheckpointWriteDelay(flags, (double) num_written / num_to_write);
+ CheckpointWriteDelay(flags, (double) num_written / num_to_write,
+ spcContext, nb_spaces);
}
}
@@ -1959,6 +1974,13 @@ BufferSync(int flags)
if (spcStatus[space].index >= num_to_write ||
spcStatus[space].num_written >= spcStatus[space].num_to_write)
{
+#if defined(HAVE_SYNC_FILE_RANGE) || defined(HAVE_POSIX_FADVISE)
+
+ PerformFileFlush(&spcContext[space]);
+ ResetFileFlushContext(&spcContext[space]);
+
+#endif /* HAVE_SYNC_FILE_RANGE || HAVE_POSIX_FADVISE */
+
nb_spaces--;
if (space != nb_spaces)
spcStatus[space] = spcStatus[nb_spaces];
@@ -1969,6 +1991,8 @@ BufferSync(int flags)
pfree(spcStatus);
spcStatus = NULL;
+ pfree(spcContext);
+ spcContext = NULL;
/*
* Update checkpoint statistics. As noted above, this doesn't include
@@ -2216,7 +2240,7 @@ BgBufferSync(void)
/* Execute the LRU scan */
while (num_to_scan > 0 && reusable_buffers < upcoming_alloc_est)
{
- int buffer_state = SyncOneBuffer(next_to_clean, true);
+ int buffer_state = SyncOneBuffer(next_to_clean, true, false, NULL);
if (++next_to_clean >= NBuffers)
{
@@ -2293,7 +2317,8 @@ BgBufferSync(void)
* Note: caller must have done ResourceOwnerEnlargeBuffers.
*/
static int
-SyncOneBuffer(int buf_id, bool skip_recently_used)
+SyncOneBuffer(int buf_id, bool skip_recently_used, bool flush_to_disk,
+ FileFlushContext * context)
{
volatile BufferDesc *bufHdr = GetBufferDescriptor(buf_id);
int result = 0;
@@ -2334,7 +2359,7 @@ SyncOneBuffer(int buf_id, bool skip_recently_used)
PinBuffer_Locked(bufHdr);
LWLockAcquire(bufHdr->content_lock, LW_SHARED);
- FlushBuffer(bufHdr, NULL);
+ FlushBuffer(bufHdr, NULL, flush_to_disk, context);
LWLockRelease(bufHdr->content_lock);
UnpinBuffer(bufHdr, true);
@@ -2596,9 +2621,16 @@ BufferGetTag(Buffer buffer, RelFileNode *rnode, ForkNumber *forknum,
*
* If the caller has an smgr reference for the buffer's relation, pass it
* as the second parameter. If not, pass NULL.
+ *
+ * The third parameter tries to hint the OS that a high priority write is meant,
+ * possibly because io-throttling is already managed elsewhere.
+ * The last parameter holds the current flush context that accumulates flush
+ * requests to be performed in one call, instead of being performed on a buffer
+ * per buffer basis.
*/
static void
-FlushBuffer(volatile BufferDesc *buf, SMgrRelation reln)
+FlushBuffer(volatile BufferDesc *buf, SMgrRelation reln, bool flush_to_disk,
+ FileFlushContext * context)
{
XLogRecPtr recptr;
ErrorContextCallback errcallback;
@@ -2687,7 +2719,9 @@ FlushBuffer(volatile BufferDesc *buf, SMgrRelation reln)
buf->tag.forkNum,
buf->tag.blockNum,
bufToWrite,
- false);
+ false,
+ flush_to_disk,
+ context);
if (track_io_timing)
{
@@ -3109,7 +3143,9 @@ FlushRelationBuffers(Relation rel)
bufHdr->tag.forkNum,
bufHdr->tag.blockNum,
localpage,
- false);
+ false,
+ false,
+ NULL);
bufHdr->flags &= ~(BM_DIRTY | BM_JUST_DIRTIED);
@@ -3143,7 +3179,7 @@ FlushRelationBuffers(Relation rel)
{
PinBuffer_Locked(bufHdr);
LWLockAcquire(bufHdr->content_lock, LW_SHARED);
- FlushBuffer(bufHdr, rel->rd_smgr);
+ FlushBuffer(bufHdr, rel->rd_smgr, false, NULL);
LWLockRelease(bufHdr->content_lock);
UnpinBuffer(bufHdr, true);
}
@@ -3195,7 +3231,7 @@ FlushDatabaseBuffers(Oid dbid)
{
PinBuffer_Locked(bufHdr);
LWLockAcquire(bufHdr->content_lock, LW_SHARED);
- FlushBuffer(bufHdr, NULL);
+ FlushBuffer(bufHdr, NULL, false, NULL);
LWLockRelease(bufHdr->content_lock);
UnpinBuffer(bufHdr, true);
}
diff --git a/src/backend/storage/buffer/localbuf.c b/src/backend/storage/buffer/localbuf.c
index 3144afe..114a0a6 100644
--- a/src/backend/storage/buffer/localbuf.c
+++ b/src/backend/storage/buffer/localbuf.c
@@ -208,7 +208,9 @@ LocalBufferAlloc(SMgrRelation smgr, ForkNumber forkNum, BlockNumber blockNum,
bufHdr->tag.forkNum,
bufHdr->tag.blockNum,
localpage,
- false);
+ false,
+ false,
+ NULL);
/* Mark not-dirty now in case we error out below */
bufHdr->flags &= ~BM_DIRTY;
diff --git a/src/backend/storage/file/buffile.c b/src/backend/storage/file/buffile.c
index ea4d689..fb3b383 100644
--- a/src/backend/storage/file/buffile.c
+++ b/src/backend/storage/file/buffile.c
@@ -317,7 +317,7 @@ BufFileDumpBuffer(BufFile *file)
return; /* seek failed, give up */
file->offsets[file->curFile] = file->curOffset;
}
- bytestowrite = FileWrite(thisfile, file->buffer + wpos, bytestowrite);
+ bytestowrite = FileWrite(thisfile, file->buffer + wpos, bytestowrite, false, NULL);
if (bytestowrite <= 0)
return; /* failed to write */
file->offsets[file->curFile] += bytestowrite;
diff --git a/src/backend/storage/file/fd.c b/src/backend/storage/file/fd.c
index 1ba4946..e880a9e 100644
--- a/src/backend/storage/file/fd.c
+++ b/src/backend/storage/file/fd.c
@@ -1344,8 +1344,97 @@ retry:
return returnCode;
}
+#if defined(HAVE_SYNC_FILE_RANGE) || defined(HAVE_POSIX_FADVISE)
+
+void
+ResetFileFlushContext(FileFlushContext * context)
+{
+ context->fd = 0;
+ context->ncalls = 0;
+ context->offset = 0;
+ context->nbytes = 0;
+ context->filename = NULL;
+}
+
+void
+PerformFileFlush(FileFlushContext * context)
+{
+ if (context->ncalls != 0)
+ {
+ int rc;
+
+#if defined(HAVE_SYNC_FILE_RANGE)
+
+ /*
+ * Linux: tell the memory manager to move these blocks to io so
+ * that they are considered for being actually written to disk.
+ */
+ rc = sync_file_range(context->fd, context->offset, context->nbytes,
+ SYNC_FILE_RANGE_WRITE);
+
+#elif defined(HAVE_POSIX_FADVISE)
+
+ /*
+ * Others: say that data should not be kept in memory...
+ * This is not exactly what we want to say, because we want to write
+ * the data for durability but we may need it later nevertheless.
+ * It seems that Linux would free the memory *if* the data has
+ * already been written do disk, else the "dontneed" call is ignored.
+ * For FreeBSD this may have the desired effect of moving the
+ * data to the io layer, although the system does not seem to
+ * take into account the provided offset & size, so it is rather
+ * rough...
+ */
+ rc = posix_fadvise(context->fd, context->offset, context->nbytes,
+ POSIX_FADV_DONTNEED);
+
+#endif
+
+ if (rc < 0)
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not flush block " INT64_FORMAT
+ " on " INT64_FORMAT " blocks in file \"%s\": %m",
+ context->offset / BLCKSZ,
+ context->nbytes / BLCKSZ,
+ context->filename)));
+ }
+}
+
+void
+FileAsynchronousFlush(FileFlushContext * context,
+ int fd, off_t offset, off_t nbytes, char * filename)
+{
+ if (context->ncalls != 0 && context->fd == fd)
+ {
+ /* same file: merge current flush with previous ones */
+ off_t new_offset = offset < context->offset? offset: context->offset;
+
+ context->nbytes =
+ (context->offset + context->nbytes > offset + nbytes ?
+ context->offset + context->nbytes : offset + nbytes) -
+ new_offset;
+ context->offset = new_offset;
+ context->ncalls ++;
+ }
+ else
+ {
+ /* other file: do flush previous file & reset flush accumulator */
+ PerformFileFlush(context);
+
+ context->fd = fd;
+ context->ncalls = 1;
+ context->offset = offset;
+ context->nbytes = nbytes;
+ context->filename = filename;
+ }
+}
+
+#endif /* HAVE_SYNC_FILE_RANGE || HAVE_POSIX_FADVISE */
+
int
-FileWrite(File file, char *buffer, int amount)
+FileWrite(File file, char *buffer, int amount, bool flush_to_disk,
+ FileFlushContext * context)
{
int returnCode;
@@ -1395,6 +1484,28 @@ retry:
if (returnCode >= 0)
{
+
+#if defined(HAVE_SYNC_FILE_RANGE) || defined(HAVE_POSIX_FADVISE)
+
+ /*
+ * Calling "write" tells the OS that pg wants to write some page to disk,
+ * however when it is really done is chosen by the OS.
+ * Depending on other disk activities this may be delayed significantly,
+ * maybe up to an "fsync" call, which could induce an IO write surge.
+ * When checkpointing pg is doing its own throttling and the result
+ * should really be written to disk with high priority, so as to meet
+ * the completion target.
+ * This call hints that such write have a higher priority.
+ */
+ if (flush_to_disk && returnCode == amount && errno == 0)
+ {
+ FileAsynchronousFlush(context,
+ VfdCache[file].fd, VfdCache[file].seekPos,
+ amount, VfdCache[file].fileName);
+ }
+
+#endif /* HAVE_SYNC_FILE_RANGE || HAVE_POSIX_FADVISE */
+
VfdCache[file].seekPos += returnCode;
/* maintain fileSize and temporary_files_size if it's a temp file */
diff --git a/src/backend/storage/smgr/md.c b/src/backend/storage/smgr/md.c
index 42a43bb..dbf057f 100644
--- a/src/backend/storage/smgr/md.c
+++ b/src/backend/storage/smgr/md.c
@@ -531,7 +531,7 @@ mdextend(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
errmsg("could not seek to block %u in file \"%s\": %m",
blocknum, FilePathName(v->mdfd_vfd))));
- if ((nbytes = FileWrite(v->mdfd_vfd, buffer, BLCKSZ)) != BLCKSZ)
+ if ((nbytes = FileWrite(v->mdfd_vfd, buffer, BLCKSZ, false, NULL)) != BLCKSZ)
{
if (nbytes < 0)
ereport(ERROR,
@@ -738,7 +738,8 @@ mdread(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
*/
void
mdwrite(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
- char *buffer, bool skipFsync)
+ char *buffer, bool skipFsync, bool flush_to_disk,
+ FileFlushContext * context)
{
off_t seekpos;
int nbytes;
@@ -767,7 +768,7 @@ mdwrite(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
errmsg("could not seek to block %u in file \"%s\": %m",
blocknum, FilePathName(v->mdfd_vfd))));
- nbytes = FileWrite(v->mdfd_vfd, buffer, BLCKSZ);
+ nbytes = FileWrite(v->mdfd_vfd, buffer, BLCKSZ, flush_to_disk, context);
TRACE_POSTGRESQL_SMGR_MD_WRITE_DONE(forknum, blocknum,
reln->smgr_rnode.node.spcNode,
diff --git a/src/backend/storage/smgr/smgr.c b/src/backend/storage/smgr/smgr.c
index 244b4ea..2db3cd3 100644
--- a/src/backend/storage/smgr/smgr.c
+++ b/src/backend/storage/smgr/smgr.c
@@ -52,7 +52,8 @@ typedef struct f_smgr
void (*smgr_read) (SMgrRelation reln, ForkNumber forknum,
BlockNumber blocknum, char *buffer);
void (*smgr_write) (SMgrRelation reln, ForkNumber forknum,
- BlockNumber blocknum, char *buffer, bool skipFsync);
+ BlockNumber blocknum, char *buffer, bool skipFsync,
+ bool flush_to_disk, FileFlushContext *context);
BlockNumber (*smgr_nblocks) (SMgrRelation reln, ForkNumber forknum);
void (*smgr_truncate) (SMgrRelation reln, ForkNumber forknum,
BlockNumber nblocks);
@@ -643,10 +644,11 @@ smgrread(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
*/
void
smgrwrite(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
- char *buffer, bool skipFsync)
+ char *buffer, bool skipFsync, bool flush_to_disk,
+ FileFlushContext * context)
{
(*(smgrsw[reln->smgr_which].smgr_write)) (reln, forknum, blocknum,
- buffer, skipFsync);
+ buffer, skipFsync, flush_to_disk, context);
}
/*
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index 1cd2aa0..95deb71 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -158,6 +158,7 @@ static bool check_bonjour(bool *newval, void **extra, GucSource source);
static bool check_ssl(bool *newval, void **extra, GucSource source);
static bool check_stage_log_stats(bool *newval, void **extra, GucSource source);
static bool check_log_stats(bool *newval, void **extra, GucSource source);
+static bool check_flush_to_disk(bool *newval, void **extra, GucSource source);
static bool check_canonical_path(char **newval, void **extra, GucSource source);
static bool check_timezone_abbreviations(char **newval, void **extra, GucSource source);
static void assign_timezone_abbreviations(const char *newval, void *extra);
@@ -1024,6 +1025,17 @@ static struct config_bool ConfigureNamesBool[] =
},
{
+ {"checkpoint_flush_to_disk", PGC_SIGHUP, WAL_CHECKPOINTS,
+ gettext_noop("Hint that checkpoint's writes are high priority."),
+ NULL
+ },
+ &checkpoint_flush_to_disk,
+ /* see bufmgr.h: true on Linux, false otherwise */
+ DEFAULT_CHECKPOINT_FLUSH_TO_DISK,
+ check_flush_to_disk, NULL, NULL
+ },
+
+ {
{"log_connections", PGC_SU_BACKEND, LOGGING_WHAT,
gettext_noop("Logs each successful connection."),
NULL
@@ -9805,6 +9817,21 @@ check_log_stats(bool *newval, void **extra, GucSource source)
}
static bool
+check_flush_to_disk(bool *newval, void **extra, GucSource source)
+{
+/* This test must be consistent with the one in FileWrite (storage/file/fd.c)
+ */
+#if ! (defined(HAVE_SYNC_FILE_RANGE) || defined(HAVE_POSIX_FADVISE))
+ /* just warn if it has no effect */
+ ereport(WARNING,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("Setting \"checkpoint_flush_to_disk\" has no effect "
+ "on this platform.")));
+#endif /* ! (HAVE_SYNC_FILE_RANGE || HAVE_POSIX_FADVISE) */
+ return true;
+}
+
+static bool
check_canonical_path(char **newval, void **extra, GucSource source)
{
/*
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 8020c1c..e4cf2a1 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -202,6 +202,8 @@
#min_wal_size = 80MB
#checkpoint_completion_target = 0.5 # checkpoint target duration, 0.0 - 1.0
#checkpoint_sort = on # sort buffers on checkpoint
+#checkpoint_flush_to_disk = ? # send buffers to disk on checkpoint
+ # default is on if Linux, off otherwise
#checkpoint_warning = 30s # 0 disables
# - Archiving -
diff --git a/src/include/postmaster/bgwriter.h b/src/include/postmaster/bgwriter.h
index a49c208..f9c8ca1 100644
--- a/src/include/postmaster/bgwriter.h
+++ b/src/include/postmaster/bgwriter.h
@@ -16,6 +16,7 @@
#define _BGWRITER_H
#include "storage/block.h"
+#include "storage/fd.h"
#include "storage/relfilenode.h"
@@ -29,7 +30,8 @@ extern void BackgroundWriterMain(void) pg_attribute_noreturn();
extern void CheckpointerMain(void) pg_attribute_noreturn();
extern void RequestCheckpoint(int flags);
-extern void CheckpointWriteDelay(int flags, double progress);
+extern void CheckpointWriteDelay(int flags, double progress,
+ FileFlushContext * context, int ctx_size);
extern bool ForwardFsyncRequest(RelFileNode rnode, ForkNumber forknum,
BlockNumber segno);
diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.h
index b56802b..cd9d130 100644
--- a/src/include/storage/bufmgr.h
+++ b/src/include/storage/bufmgr.h
@@ -54,6 +54,14 @@ extern int bgwriter_lru_maxpages;
extern double bgwriter_lru_multiplier;
extern bool track_io_timing;
extern int target_prefetch_pages;
+
+#ifdef HAVE_SYNC_FILE_RANGE
+#define DEFAULT_CHECKPOINT_FLUSH_TO_DISK true
+#else
+#define DEFAULT_CHECKPOINT_FLUSH_TO_DISK false
+#endif /* HAVE_SYNC_FILE_RANGE */
+
+extern bool checkpoint_flush_to_disk;
extern bool checkpoint_sort;
/* in buf_init.c */
diff --git a/src/include/storage/fd.h b/src/include/storage/fd.h
index 7eabe09..c7b2a6d 100644
--- a/src/include/storage/fd.h
+++ b/src/include/storage/fd.h
@@ -59,6 +59,24 @@ extern int max_files_per_process;
*/
extern int max_safe_fds;
+/*
+ * FileFlushContext structure:
+ *
+ * This is used to accumulate several flush requests on a file
+ * into a larger flush request.
+ * - fd: file descriptor of the file
+ * - ncalls: number of flushes merged together
+ * - offset: starting offset (minimum of all offsets)
+ * - nbytes: size (minimum extent to cover all flushed data)
+ * - filename: filename of fd for error messages
+ */
+typedef struct FileFlushContext {
+ int fd;
+ int ncalls;
+ off_t offset;
+ off_t nbytes;
+ char * filename;
+} FileFlushContext;
/*
* prototypes for functions in fd.c
@@ -70,7 +88,12 @@ extern File OpenTemporaryFile(bool interXact);
extern void FileClose(File file);
extern int FilePrefetch(File file, off_t offset, int amount);
extern int FileRead(File file, char *buffer, int amount);
-extern int FileWrite(File file, char *buffer, int amount);
+extern void ResetFileFlushContext(FileFlushContext * context);
+extern void PerformFileFlush(FileFlushContext * context);
+extern void FileAsynchronousFlush(FileFlushContext * context,
+ int fd, off_t offset, off_t nbytes, char * filename);
+extern int FileWrite(File file, char *buffer, int amount, bool flush_to_disk,
+ FileFlushContext * context);
extern int FileSync(File file);
extern off_t FileSeek(File file, off_t offset, int whence);
extern int FileTruncate(File file, off_t offset);
diff --git a/src/include/storage/smgr.h b/src/include/storage/smgr.h
index 69a624f..a46a70c 100644
--- a/src/include/storage/smgr.h
+++ b/src/include/storage/smgr.h
@@ -16,6 +16,7 @@
#include "fmgr.h"
#include "storage/block.h"
+#include "storage/fd.h"
#include "storage/relfilenode.h"
@@ -95,7 +96,8 @@ extern void smgrprefetch(SMgrRelation reln, ForkNumber forknum,
extern void smgrread(SMgrRelation reln, ForkNumber forknum,
BlockNumber blocknum, char *buffer);
extern void smgrwrite(SMgrRelation reln, ForkNumber forknum,
- BlockNumber blocknum, char *buffer, bool skipFsync);
+ BlockNumber blocknum, char *buffer, bool skipFsync,
+ bool flush_to_disk, FileFlushContext * context);
extern BlockNumber smgrnblocks(SMgrRelation reln, ForkNumber forknum);
extern void smgrtruncate(SMgrRelation reln, ForkNumber forknum,
BlockNumber nblocks);
@@ -120,8 +122,9 @@ extern void mdprefetch(SMgrRelation reln, ForkNumber forknum,
BlockNumber blocknum);
extern void mdread(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
char *buffer);
-extern void mdwrite(SMgrRelation reln, ForkNumber forknum,
- BlockNumber blocknum, char *buffer, bool skipFsync);
+extern void mdwrite(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
+ char *buffer, bool skipFsync, bool flush_to_disk,
+ FileFlushContext * context);
extern BlockNumber mdnblocks(SMgrRelation reln, ForkNumber forknum);
extern void mdtruncate(SMgrRelation reln, ForkNumber forknum,
BlockNumber nblocks);
Hello,
[...] If you make the sorting criterion include the tablespace id you
wouldn't need the lookahead loop in NextBufferToWrite().
I'm considering this precise point, i.e. including the tablespace as
a sorting criterion.
Currently the array used for sorting is 16 bytes per buffer (although I
wrote 12 in another mail, I was wrong...). The data include the bufid (4
bytes) the relation & fork num (8 bytes, but really 4 bytes + 2 bits are
used), and the block number (4 bytes) which is the offset within the
relation. These 3 combined data allow to find the file and the offset
within that file, for the given buffer id.
I'm concerned that these 16 bytes are already significant and I do not
want to extend them any more. I was already pretty happy with the previous
version with 4 bytes per buffer.
Now as the number of tablespace is expected to be very small (1, 2, maybe
3), there is no problem to pack it within the unused 30 bits in forknum.
That would mean some masking and casts here and there, so it would not be
very beautiful, but it would make it easy to find the buffers for a given
tablespace, and indeed remove the lookahead stuff in the next buffer
function, as you suggest.
My question is: would that be acceptable, or would someone object to the
use of masks and things like that? The benefit would be a simpler/more
direct next buffer function, but some more tinkering around the sorting
criterion to use a packed representation.
Note that I do not think that it would have any actual impact on
performance... it would only make a difference if there were really many
tablespaces (the scanning complexity would be Nbuffer instead of
Nbuffer*Ntablespace, but as Ntablespace is small...). My motivation is
rather to help the patch get through, so I'm fine if this is not needed.
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Hi,
On 2015-09-10 17:15:26 +0200, Fabien COELHO wrote:
Thanks for the hints! Two-part v12 attached fixes these.
Here is a v13, which is just a rebase after 1aba62ec.
I'm working on this patch, to get it into a state I think it'd be
commitable.
In my performance testing it showed that calling PerformFileFlush() only
at segment boundaries and in CheckpointWriteDelay() can lead to rather
spikey IO - not that surprisingly. The sync in CheckpointWriteDelay() is
problematic because it only is triggered while on schedule, and not when
behind. My testing seems to show that just adding a limit of 32 buffers to
FileAsynchronousFlush() leads to markedly better results.
I wonder if mmap() && msync(MS_ASYNC) isn't a better replacement for
sync_file_range(SYNC_FILE_RANGE_WRITE) than posix_fadvise(DONTNEED). It
might even be possible to later approximate that on windows using
FlushViewOfFile().
As far as I can see the while (nb_spaces != 0)/NextBufferToWrite() logic
doesn't work correctly if tablespaces aren't actually sorted. I'm
actually inclined to fix this by simply removing the flag to
enable/disable sorting.
Having defined(HAVE_SYNC_FILE_RANGE) || defined(HAVE_POSIX_FADVISE) in
so many places looks ugly, I want to push that to the underlying
functions. If we add a different flushing approach we shouldn't have to
touch several places that don't actually really care.
I've replaced the NextBufferToWrite() logic with a binaryheap.h heap -
seems to work well, with a bit less code actually.
I'll post this after some more cleanup & testing.
I've also noticed that sleeping logic in CheckpointWriteDelay() isn't
particularly good. In high throughput workloads the 100ms sleep is too
long, leading to bursty IO behaviour. If 1k+ buffers a written out a
second 100ms is a rather long sleep. For another that we only sleep
100ms when the write rate is low makes the checkpoint finish rather
quickly - on a slow disk (say microsd) that can cause unneccesary
slowdowns for concurrent activity. ISTM we should calculate the sleep
time in a better way. The SIGHUP behaviour is also weird. Anyway, this
probably belongs on a new thread.
Greetings,
Andres Freund
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Hello Andres,
Here is a v13, which is just a rebase after 1aba62ec.
I'm working on this patch, to get it into a state I think it'd be
commitable.
I'll review it carefully. Also, if you can include some performance
feature it would help, even if I'll do some more runs.
In my performance testing it showed that calling PerformFileFlush() only
at segment boundaries and in CheckpointWriteDelay() can lead to rather
spikey IO - not that surprisingly. The sync in CheckpointWriteDelay() is
problematic because it only is triggered while on schedule, and not when
behind.
When behind, the PerformFileFlush should be called on segment boundaries.
The idea was not to go to sleep without flushing, and to do it as little
as possible.
My testing seems to show that just adding a limit of 32 buffers to
FileAsynchronousFlush() leads to markedly better results.
Hmmm. 32 buffers means 256 KB, which is quite small. Not sure what a good
"limit" would be. It could depend whether pages are close or not.
I wonder if mmap() && msync(MS_ASYNC) isn't a better replacement for
sync_file_range(SYNC_FILE_RANGE_WRITE) than posix_fadvise(DONTNEED). It
might even be possible to later approximate that on windows using
FlushViewOfFile().
I'm not sure that mmap/msync can be used for this purpose, because there
is no real control it seems about where the file is mmapped.
As far as I can see the while (nb_spaces != 0)/NextBufferToWrite() logic
doesn't work correctly if tablespaces aren't actually sorted. I'm
actually inclined to fix this by simply removing the flag to
enable/disable sorting.
I do no think that there is a significant downside to having sort always
on, but showing it requires to be able to test, so to have a guc. The
point of the guc is to demonstrate that the feature is harmless:-)
Having defined(HAVE_SYNC_FILE_RANGE) || defined(HAVE_POSIX_FADVISE) in
so many places looks ugly, I want to push that to the underlying
functions. If we add a different flushing approach we shouldn't have to
touch several places that don't actually really care.
I agree that it is pretty ugly, but I do not think that you can remove
them all. You need at least one for checking the guc and one for enabling
the feature. Maybe their number could be reduced if the functions are
switched to do-nothing stubs which are called nevertheless, but I was not
keen on letting unused code when there is no sync_file_range nor
posix_fadvise.
I've replaced the NextBufferToWrite() logic with a binaryheap.h heap -
seems to work well, with a bit less code actually.
Hmmm. I'll check. I'm still unconvinced that using a tree for a 2-3
element set in most case is an improvement.
I'll post this after some more cleanup & testing.
I'll have a look when it is ready.
I've also noticed that sleeping logic in CheckpointWriteDelay() isn't
particularly good. In high throughput workloads the 100ms sleep is too
long, leading to bursty IO behaviour. If 1k+ buffers a written out a
second 100ms is a rather long sleep. For another that we only sleep
100ms when the write rate is low makes the checkpoint finish rather
quickly - on a slow disk (say microsd) that can cause unneccesary
slowdowns for concurrent activity. ISTM we should calculate the sleep
time in a better way.
I also noted this point, but I'm not sure how to have a better approach,
so I let it as it is. I tried 50 ms & 200 ms on some runs, without
significant effect on performance for the test I ran then. The point of
having not too small a value is that it provide some significant work to
the IO subsystem without overflowing it. On average it does not matter.
I'm unsure how it would interact with flushing. So I decided not to do
anything about it. Maybe it should be a guc, but I would not know how to
choose it.
The SIGHUP behaviour is also weird. Anyway, this probably belongs on a
new thread.
Probably. I did not try to look at that.
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Mon, Oct 19, 2015 at 4:06 AM, Andres Freund <andres@anarazel.de> wrote:
I wonder if mmap() && msync(MS_ASYNC) isn't a better replacement for
sync_file_range(SYNC_FILE_RANGE_WRITE) than posix_fadvise(DONTNEED). It
might even be possible to later approximate that on windows using
FlushViewOfFile().
I think this idea is worth exploring especially because we can have
Windows equivalent for this optimisation. Will this option by any
chance can lead to increase in memory usage as mmap has to
map the file/'s?
With Regards,
Amit Kapila.
EnterpriseDB: http://www.enterprisedb.com
On 2015-10-19 21:14:55 +0200, Fabien COELHO wrote:
In my performance testing it showed that calling PerformFileFlush() only
at segment boundaries and in CheckpointWriteDelay() can lead to rather
spikey IO - not that surprisingly. The sync in CheckpointWriteDelay() is
problematic because it only is triggered while on schedule, and not when
behind.When behind, the PerformFileFlush should be called on segment
boundaries.
That means it's flushing up to a gigabyte of data at once. Far too
much. The implementation pretty always will go behind schedule for some
time. Since sync_file_range() doesn't flush in the foreground I don't
think it's important to do the flushing in concert with sleeping.
My testing seems to show that just adding a limit of 32 buffers to
FileAsynchronousFlush() leads to markedly better results.Hmmm. 32 buffers means 256 KB, which is quite small.
Why? The aim is to not overwhelm the request queue - which is where the
coalescing is done. And usually that's rather small. If you flush much more
sync_file_range starts to do work in the foreground.
I wonder if mmap() && msync(MS_ASYNC) isn't a better replacement for
sync_file_range(SYNC_FILE_RANGE_WRITE) than posix_fadvise(DONTNEED). It
might even be possible to later approximate that on windows using
FlushViewOfFile().I'm not sure that mmap/msync can be used for this purpose, because there is
no real control it seems about where the file is mmapped.
I'm not following? Why does it matter where a file is mapped?
I have had a friend (Christian Kruse, thanks!) confirm that at least on
OSX msync(MS_ASYNC) triggers writeback. A freebsd dev confirmed that
that should be the case on freebsd too.
Having defined(HAVE_SYNC_FILE_RANGE) || defined(HAVE_POSIX_FADVISE) in
so many places looks ugly, I want to push that to the underlying
functions. If we add a different flushing approach we shouldn't have to
touch several places that don't actually really care.I agree that it is pretty ugly, but I do not think that you can remove them
all.
Sure, never said all. But most.
I've replaced the NextBufferToWrite() logic with a binaryheap.h heap -
seems to work well, with a bit less code actually.Hmmm. I'll check. I'm still unconvinced that using a tree for a 2-3 element
set in most case is an improvement.
Yes, it'll not matter that much in many cases. But I rather disliked the
NextBufferToWrite() implementation, especially that it walkes the array
multiple times. And I did see setups with ~15 tablespaces.
I've also noticed that sleeping logic in CheckpointWriteDelay() isn't
particularly good. In high throughput workloads the 100ms sleep is too
long, leading to bursty IO behaviour. If 1k+ buffers a written out a
second 100ms is a rather long sleep. For another that we only sleep
100ms when the write rate is low makes the checkpoint finish rather
quickly - on a slow disk (say microsd) that can cause unneccesary
slowdowns for concurrent activity. ISTM we should calculate the sleep
time in a better way.I also noted this point, but I'm not sure how to have a better approach, so
I let it as it is. I tried 50 ms & 200 ms on some runs, without significant
effect on performance for the test I ran then. The point of having not too
small a value is that it provide some significant work to the IO subsystem
without overflowing it.
I don't think that makes much sense. All a longer sleep achieves is
creating a larger burst of writes afterwards. We should really sleep
adaptively.
Greetings,
Andres Freund
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Hello Andres,
In my performance testing it showed that calling PerformFileFlush() only
at segment boundaries and in CheckpointWriteDelay() can lead to rather
spikey IO - not that surprisingly. The sync in CheckpointWriteDelay() is
problematic because it only is triggered while on schedule, and not when
behind.When behind, the PerformFileFlush should be called on segment
boundaries.That means it's flushing up to a gigabyte of data at once. Far too
much.
Hmmm. I do not get it. There would not be gigabytes, there would be as
much as was written since the last sleep, about 100 ms ago, which is not
likely to be gigabytes?
The implementation pretty always will go behind schedule for some
time. Since sync_file_range() doesn't flush in the foreground I don't
think it's important to do the flushing in concert with sleeping.
For me it is important to avoid accumulating too large flushes, and that
is the point of the call before sleeping.
My testing seems to show that just adding a limit of 32 buffers to
FileAsynchronousFlush() leads to markedly better results.Hmmm. 32 buffers means 256 KB, which is quite small.
Why?
Because the point of sorting is to generate sequential writes so that the
HDD has a lot of aligned stuff to write without moving the head, and 32 is
rather small for that.
The aim is to not overwhelm the request queue - which is where the
coalescing is done. And usually that's rather small.
That is an argument. How small, though? It seems to be 128 by default, so
I'd rather have 128? Also, it can be changed, so maybe it should really be
a guc?
If you flush much more sync_file_range starts to do work in the
foreground.
Argh, too bad. I would have hoped that the would just deal with in an
asynchronous way, this is not a "fsync" call, just a flush advise.
I wonder if mmap() && msync(MS_ASYNC) isn't a better replacement for
sync_file_range(SYNC_FILE_RANGE_WRITE) than posix_fadvise(DONTNEED). It
might even be possible to later approximate that on windows using
FlushViewOfFile().I'm not sure that mmap/msync can be used for this purpose, because there is
no real control it seems about where the file is mmapped.I'm not following? Why does it matter where a file is mapped?
Because it should be in shared buffers where pg needs it? You probably
should not want to mmap all pg data files in user space for a large
database? Or if so, currently the OS keeps the data in memory if it has
enough space, but if you got to mmap this cache management would be pg
responsability, if I understand correctly mmap and your intentions.
I have had a friend (Christian Kruse, thanks!) confirm that at least on
OSX msync(MS_ASYNC) triggers writeback. A freebsd dev confirmed that
that should be the case on freebsd too.
Good. My concern is how mmap could be used, though, not the flushing part.
Hmmm. I'll check. I'm still unconvinced that using a tree for a 2-3 element
set in most case is an improvement.Yes, it'll not matter that much in many cases. But I rather disliked the
NextBufferToWrite() implementation, especially that it walkes the array
multiple times. And I did see setups with ~15 tablespaces.
ISTM that it is rather an argument for taking the tablespace into the
sorting, not necessarily for a binary heap.
I also noted this point, but I'm not sure how to have a better approach, so
I let it as it is. I tried 50 ms & 200 ms on some runs, without significant
effect on performance for the test I ran then. The point of having not too
small a value is that it provide some significant work to the IO subsystem
without overflowing it.I don't think that makes much sense. All a longer sleep achieves is
creating a larger burst of writes afterwards. We should really sleep
adaptively.
It sounds reasonable, but what would be the criterion?
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 2015-10-21 07:49:23 +0200, Fabien COELHO wrote:
Hello Andres,
In my performance testing it showed that calling PerformFileFlush() only
at segment boundaries and in CheckpointWriteDelay() can lead to rather
spikey IO - not that surprisingly. The sync in CheckpointWriteDelay() is
problematic because it only is triggered while on schedule, and not when
behind.When behind, the PerformFileFlush should be called on segment
boundaries.That means it's flushing up to a gigabyte of data at once. Far too
much.Hmmm. I do not get it. There would not be gigabytes,
I said 'up to a gigabyte' not gigabytes. But it actually can be more
than one if you're unluckly.
there would be as much as was written since the last sleep, about 100
ms ago, which is not likely to be gigabytes?
In many cases we don't sleep all that frequently - after one 100ms sleep
we're already behind a lot. And even so, it's pretty easy to get into
checkpoint scenarios with ~500 mbyte/s as a writeout rate. Only issuing
a sync_file_range() 10 times for that is obviously problematic.
The implementation pretty always will go behind schedule for some
time. Since sync_file_range() doesn't flush in the foreground I don't
think it's important to do the flushing in concert with sleeping.For me it is important to avoid accumulating too large flushes, and that is
the point of the call before sleeping.
I don't follow this argument. It's important to avoid large flushes,
therefore we potentially allow large flushes to accumulate?
My testing seems to show that just adding a limit of 32 buffers to
FileAsynchronousFlush() leads to markedly better results.Hmmm. 32 buffers means 256 KB, which is quite small.
Why?
Because the point of sorting is to generate sequential writes so that the
HDD has a lot of aligned stuff to write without moving the head, and 32 is
rather small for that.
A sync_file_range(SYNC_FILE_RANGE_WRITE) doesn't synchronously write
data back. It just puts it into the write queue. You can have merging
between IOs from either side. But more importantly you can't merge that
many requests together anyway.
The aim is to not overwhelm the request queue - which is where the
coalescing is done. And usually that's rather small.That is an argument. How small, though? It seems to be 128 by default, so
I'd rather have 128? Also, it can be changed, so maybe it should really be a
guc?
I couldn't see any benefits above (and below) 32 on a 20 drive system,
so I doubt it's worthwhile. It's actually good for interactivity to
allow other requests into the queue concurrently - otherwise other
reads/writes will obviously have a higher latency...
If you flush much more sync_file_range starts to do work in the
foreground.Argh, too bad. I would have hoped that the would just deal with in an
asynchronous way,
It's even in the man page:
"Note that even this may block if you attempt to write more than
request queue size."
this is not a "fsync" call, just a flush advise.
sync_file_range isn't fadvise().
Because it should be in shared buffers where pg needs it?
Huh? I'm just suggesting p = mmap(fd, offset, bytes);msync(p, bytes);munmap(p);
instead of sync_file_range().
Hmmm. I'll check. I'm still unconvinced that using a tree for a 2-3 element
set in most case is an improvement.Yes, it'll not matter that much in many cases. But I rather disliked the
NextBufferToWrite() implementation, especially that it walkes the array
multiple times. And I did see setups with ~15 tablespaces.ISTM that it is rather an argument for taking the tablespace into the
sorting, not necessarily for a binary heap.
I don't understand your problem with that. The heap specific code is
small, smaller than your NextBufferToWrite() implementation?
ts_heap = binaryheap_allocate(nb_spaces,
ts_progress_cmp,
NULL);
spcContext = (FileFlushContext *)
palloc(sizeof(FileFlushContext) * nb_spaces);
for (i = 0; i < nb_spaces; i++)
{
TableSpaceCheckpointStatus *spc = &spcStatus[i];
spc->progress_slice = ((float8) num_to_write) / (float8) spc->num_to_write;
ResetFileFlushContext(&spcContext[i]);
spc->flushContext = &spcContext[i];
binaryheap_add_unordered(ts_heap, PointerGetDatum(&spcStatus[i]));
}
binaryheap_build(ts_heap);
and then
while (!binaryheap_empty(ts_heap))
{
TableSpaceCheckpointStatus *ts = (TableSpaceCheckpointStatus *)
DatumGetPointer(binaryheap_first(ts_heap));
...
ts->progress += ts->progress_slice;
ts->num_written++;
...
if (ts->num_written == ts->num_to_write)
{
...
binaryheap_remove_first(ts_heap);
}
else
{
/* update heap with the new progress */
binaryheap_replace_first(ts_heap, PointerGetDatum(ts));
}
I also noted this point, but I'm not sure how to have a better approach, so
I let it as it is. I tried 50 ms & 200 ms on some runs, without significant
effect on performance for the test I ran then. The point of having not too
small a value is that it provide some significant work to the IO subsystem
without overflowing it.I don't think that makes much sense. All a longer sleep achieves is
creating a larger burst of writes afterwards. We should really sleep
adaptively.It sounds reasonable, but what would be the criterion?
What IsCheckpointOnSchedule() does is essentially to calculate progress
for two things:
1) Are we on schedule based on WAL segments until CheckPointSegments
(computed via max_wal_size these days). I.e. is the percentage of
used up WAL bigger than the percentage of written out buffers.
2) Are we on schedule based on checkpoint_timeout. I.e. is the
percentage of checkpoint_timeout already passed bigger than the
percentage of buffers written out.
So the trick is just to compute the number of work items (e.g. buffers
to write out) and divide the remaining time by it. That's how long you
can sleep.
It's slightly trickier for WAL and I'm not sure it's equally
important. But even there it shouldn't be too hard to calculate the
amount of time till we're behind on schedule and only sleep that long.
I'm running benchmarks right now, they'll take a bit to run to
completion.
Greetings,
Andres Freund
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Hello Andres,
there would be as much as was written since the last sleep, about 100
ms ago, which is not likely to be gigabytes?In many cases we don't sleep all that frequently - after one 100ms sleep
we're already behind a lot.
I think that "being behind" is not a problem as such, it is really the way
the scheduler has been designed and works, by keeping pace with time &
wall progress by little bursts of writes. If you reduce the sleep time a
lot then it would end up having writes interleaved with small sleeps, but
then this would be bad for performance has the OS would loose the ability
to write much data sequentially on the disk.
It does not mean that the default 100 ms is a good figure, but the "being
behind" is a feature, not an issue as such.
And even so, it's pretty easy to get into checkpoint scenarios with ~500
mbyte/s as a writeout rate.
Hmmmm. Not with my hardware:-)
Only issuing a sync_file_range() 10 times for that is obviously
problematic.
Hmmm. Then it should depend on the expected write capacity of the
underlying disks...
The implementation pretty always will go behind schedule for some
time. Since sync_file_range() doesn't flush in the foreground I don't
think it's important to do the flushing in concert with sleeping.For me it is important to avoid accumulating too large flushes, and that is
the point of the call before sleeping.I don't follow this argument. It's important to avoid large flushes,
therefore we potentially allow large flushes to accumulate?
On my simple test hardware the flushes are not large, I think, so the
problem does not arise. Maybe I should check.
My testing seems to show that just adding a limit of 32 buffers to
FileAsynchronousFlush() leads to markedly better results.Hmmm. 32 buffers means 256 KB, which is quite small.
Why?
Because the point of sorting is to generate sequential writes so that the
HDD has a lot of aligned stuff to write without moving the head, and 32 is
rather small for that.A sync_file_range(SYNC_FILE_RANGE_WRITE) doesn't synchronously write
data back. It just puts it into the write queue.
Yes.
You can have merging between IOs from either side. But more importantly
you can't merge that many requests together anyway.
Probably.
The aim is to not overwhelm the request queue - which is where the
coalescing is done. And usually that's rather small.That is an argument. How small, though? It seems to be 128 by default, so
I'd rather have 128? Also, it can be changed, so maybe it should really be a
guc?I couldn't see any benefits above (and below) 32 on a 20 drive system,
So it is one kind of (big) hardware. Assuming that pages are contiguous,
how much is written on each disk depends on the RAID type, the stripe
size, and when it is really written depends on the various cache (in the
RAID HW card if any, on the disk, ...), so whether 32 at the OS level is
the right size is pretty unclear to me. I would have said the larger the
better, but indeed you should avoid blocking.
so I doubt it's worthwhile. It's actually good for interactivity to
allow other requests into the queue concurrently - otherwise other
reads/writes will obviously have a higher latency...
Sure. Now on my tests, with my (old & little) hardware it seemed quite
smooth. What I'm driving at is that what is good may be relative and
depend on the underlying hardware, which makes it not obvious to choose
the right parameter.
If you flush much more sync_file_range starts to do work in the
foreground.Argh, too bad. I would have hoped that the would just deal with in an
asynchronous way,It's even in the man page:
"Note that even this may block if you attempt to write more than
request queue size."
Hmmm. What about choosing "request queue size * 0.5", then ?
Because it should be in shared buffers where pg needs it?
Huh? I'm just suggesting p = mmap(fd, offset, bytes);msync(p, bytes);munmap(p);
instead of sync_file_range().
I think that I do not really understand how it may work, but possible it
could.
ISTM that it is rather an argument for taking the tablespace into the
sorting, not necessarily for a binary heap.I don't understand your problem with that. The heap specific code is
small, smaller than your NextBufferToWrite() implementation?
You have not yet posted the updated version of the patch.
Thee complexity of the round robin scan on the array is O(1) and very few
instructions, plus some stop condition which is mostly true I think if the
writes are balanced between table spaces, there is no dynamic allocation
in the data structure (it is an array). The binary heap is O(log(n)),
probably there are dynamic allocations and frees when extracting/inserting
something, there are functions calls to rebalance the tree, and so on. Ok,
"n" is expected to be small.
So basically, for me it is not obviously superior to the previous version.
Now I'm also tired, so if it works reasonably I'll be fine with it.
[... code extract ...]
I don't think that makes much sense. All a longer sleep achieves is
creating a larger burst of writes afterwards. We should really sleep
adaptively.It sounds reasonable, but what would be the criterion?
What IsCheckpointOnSchedule() does is essentially to calculate progress
for two things:
1) Are we on schedule based on WAL segments until CheckPointSegments
(computed via max_wal_size these days). I.e. is the percentage of
used up WAL bigger than the percentage of written out buffers.2) Are we on schedule based on checkpoint_timeout. I.e. is the
percentage of checkpoint_timeout already passed bigger than the
percentage of buffers written out.
So the trick is just to compute the number of work items (e.g. buffers
to write out) and divide the remaining time by it. That's how long you
can sleep.
See discussion above. ISTM that the "bursts" is a useful feature of the
checkpoint scheduler, especially with sorted buffers & flushes. You want
to provide grouped writes that will be easilly written to disk together.
You do not want to have page writes issued one by one and interleaved with
small sleeps.
It's slightly trickier for WAL and I'm not sure it's equally
important. But even there it shouldn't be too hard to calculate the
amount of time till we're behind on schedule and only sleep that long.
The scheduler stops writing as soon as it has overtaken the progress, so
it should be a very small time, but if you do that you would end up
writing pages one by one, which is not desirable at all.
I'm running benchmarks right now, they'll take a bit to run to
completion.
Good.
I'm looking forward to have a look at the updated version of the patch.
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 2015-09-10 17:15:26 +0200, Fabien COELHO wrote:
Here is a v13, which is just a rebase after 1aba62ec.
And here's v14. It's not something entirely ready. A lot of details have
changed, I unfortunately don't remember them all. But there are more
important things than the details of the patch.
I've played *a lot* with this patch. I found a bunch of issues:
1) The FileFlushContext context infrastructure isn't actually
correct. There's two problems: First, using the actual 'fd' number to
reference a to-be-flushed file isn't meaningful. If there are lots
of files open, fds get reused within fd.c. That part is enough fixed
by referencing File instead the fd. The bigger problem is that the
infrastructure doesn't deal with files being closed. There can, which
isn't that hard to trigger, be smgr invalidations causing smgr handle
and thus the file to be closed.
I think this means that the entire flushing infrastructure actually
needs to be hoisted up, onto the smgr/md level.
2) I noticed that sync_file_range() blocked far more often than I'd
expected. Reading the kernel code that turned out to be caused by a
pessimization in the kernel introduced years ago - in many situation
SFR_WRITE waited for the writes. A fix for this will be in the 4.4
kernel.
3) I found that latency wasn't improved much for workloads that are
significantly bigger than shared buffers. The problem here is that
neither bgwriter nor the backends have, so far, done
sync_file_range() calls. That meant that the old problem of having
gigabytes of dirty data that periodically get flushed out, still
exists. Having these do flushes mostly attacks that problem.
Benchmarking revealed that for workloads where the hot data set mostly
fits into shared buffers flushing and sorting is anywhere from a small
to a massive improvement, both in throughput and latency. Even without
the patch from 2), although fixing that improves things furhter.
What I did not expect, and what confounded me for a long while, is that
for workloads where the hot data set does *NOT* fit into shared buffers,
sorting often led to be a noticeable reduction in throughput. Up to
30%. The performance was still much more regular than before, i.e. no
more multi-second periods without any transactions happening.
By now I think I know what's going on: Before the sorting portion of the
patch the write-loop in BufferSync() starts at the current clock hand,
by using StrategySyncStart(). But after the sorting that obviously
doesn't happen anymore - buffers are accessed in their sort order. By
starting at the current clock hand and moving on from there the
checkpointer basically makes it more less likely that victim buffers
need to be written either by the backends themselves or by
bgwriter. That means that the sorted checkpoint writes can, indirectly,
increase the number of unsorted writes by other processes :(
My benchmarking suggest that that effect is the larger, the shorter the
checkpoint timeout is. That seems to intuitively make sense, give the
above explanation attempt. If the checkpoint takes longer the clock hand
will almost certainly soon overtake checkpoints 'implicit' hand.
I'm not sure if we can really do anything about this problem. While I'm
pretty jet lagged, I still spent a fair amount of time thinking about
it. Seems to suggest that we need to bring back the setting to
enable/disable sorting :(
What I think needs to happen next with the patch is:
1) Hoist up the FileFlushContext stuff into the smgr layer. Carefully
handling the issue of smgr invalidations.
2) Replace the boolean checkpoint_flush_to_disk GUC with a list guc that
later can contain multiple elements like checkpoint, bgwriter,
backends, ddl, bulk-writes. That seems better than adding GUCs for
these separately. Then make the flush locations in the patch
configurable using that.
3) I think we should remove the sort timing from the checkpoint logging
before commit. It'll always be pretty short.
Greetings,
Andres Freund
Attachments:
0001-ckpt-14-andres.patchtext/x-patch; charset=us-asciiDownload
>From dd0868d2c714bf18d34f82db40669b435d4b2ba2 Mon Sep 17 00:00:00 2001
From: Andres Freund <andres@anarazel.de>
Date: Fri, 23 Oct 2015 15:22:04 +0200
Subject: [PATCH] ckpt-14-andres
---
doc/src/sgml/config.sgml | 18 ++
doc/src/sgml/wal.sgml | 12 +
src/backend/access/heap/rewriteheap.c | 2 +-
src/backend/access/nbtree/nbtree.c | 2 +-
src/backend/access/nbtree/nbtsort.c | 2 +-
src/backend/access/spgist/spginsert.c | 6 +-
src/backend/access/transam/xlog.c | 11 +-
src/backend/storage/buffer/README | 5 -
src/backend/storage/buffer/buf_init.c | 24 +-
src/backend/storage/buffer/bufmgr.c | 365 ++++++++++++++++++++++----
src/backend/storage/buffer/freelist.c | 6 +-
src/backend/storage/buffer/localbuf.c | 3 +-
src/backend/storage/file/buffile.c | 3 +-
src/backend/storage/file/copydir.c | 4 +-
src/backend/storage/file/fd.c | 242 +++++++++++++++--
src/backend/storage/smgr/md.c | 6 +-
src/backend/storage/smgr/smgr.c | 8 +-
src/backend/utils/misc/guc.c | 12 +
src/backend/utils/misc/postgresql.conf.sample | 2 +
src/include/access/xlog.h | 2 +
src/include/storage/buf_internals.h | 18 ++
src/include/storage/bufmgr.h | 8 +
src/include/storage/fd.h | 37 ++-
src/include/storage/smgr.h | 7 +-
src/tools/pgindent/typedefs.list | 1 +
25 files changed, 705 insertions(+), 101 deletions(-)
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 5549de7..7db7ae7 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -2452,6 +2452,24 @@ include_dir 'conf.d'
</listitem>
</varlistentry>
+ <varlistentry id="guc-checkpoint-flush-to-disk" xreflabel="checkpoint_flush_to_disk">
+ <term><varname>checkpoint_flush_to_disk</varname> (<type>bool</type>)
+ <indexterm>
+ <primary><varname>checkpoint_flush_to_disk</> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ When writing data for a checkpoint, hint the underlying OS that the
+ data must be sent to disk as soon as possible. This may help smoothing
+ disk I/O writes and avoid a stall when fsync is issued at the end of
+ the checkpoint, but it may also reduce average performance.
+ This setting may have no effect on some platforms.
+ The default is <literal>on</> on Linux, <literal>off</> otherwise.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="guc-checkpoint-warning" xreflabel="checkpoint_warning">
<term><varname>checkpoint_warning</varname> (<type>integer</type>)
<indexterm>
diff --git a/doc/src/sgml/wal.sgml b/doc/src/sgml/wal.sgml
index e3941c9..a4b8d91 100644
--- a/doc/src/sgml/wal.sgml
+++ b/doc/src/sgml/wal.sgml
@@ -546,6 +546,18 @@
</para>
<para>
+ On Linux and POSIX platforms, <xref linkend="guc-checkpoint-flush-to-disk">
+ allows to hint the OS that pages written on checkpoints must be flushed
+ to disk quickly. Otherwise, these pages may be kept in cache for some time,
+ inducing a stall later when <literal>fsync</> is called to actually
+ complete the checkpoint. This setting helps to reduce transaction latency,
+ but it may also have a small adverse effect on the average transaction rate
+ at maximum throughput on some OS. It should be beneficial for high write
+ loads on HDD. This feature probably brings no benefit on SSD, as the I/O
+ write latency is small on such hardware, thus it may be disabled.
+ </para>
+
+ <para>
The number of WAL segment files in <filename>pg_xlog</> directory depends on
<varname>min_wal_size</>, <varname>max_wal_size</> and
the amount of WAL generated in previous checkpoint cycles. When old log
diff --git a/src/backend/access/heap/rewriteheap.c b/src/backend/access/heap/rewriteheap.c
index 6a6fc3b..95f086d 100644
--- a/src/backend/access/heap/rewriteheap.c
+++ b/src/backend/access/heap/rewriteheap.c
@@ -918,7 +918,7 @@ logical_heap_rewrite_flush_mappings(RewriteState state)
* Note that we deviate from the usual WAL coding practices here,
* check the above "Logical rewrite support" comment for reasoning.
*/
- written = FileWrite(src->vfd, waldata_start, len);
+ written = FileWrite(src->vfd, waldata_start, len, NULL);
if (written != len)
ereport(ERROR,
(errcode_for_file_access(),
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index cf4a6dc..efb3338 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -203,7 +203,7 @@ btbuildempty(PG_FUNCTION_ARGS)
/* Write the page. If archiving/streaming, XLOG it. */
PageSetChecksumInplace(metapage, BTREE_METAPAGE);
smgrwrite(index->rd_smgr, INIT_FORKNUM, BTREE_METAPAGE,
- (char *) metapage, true);
+ (char *) metapage, true, NULL);
if (XLogIsNeeded())
log_newpage(&index->rd_smgr->smgr_rnode.node, INIT_FORKNUM,
BTREE_METAPAGE, metapage, false);
diff --git a/src/backend/access/nbtree/nbtsort.c b/src/backend/access/nbtree/nbtsort.c
index f95f67a..f8976f1 100644
--- a/src/backend/access/nbtree/nbtsort.c
+++ b/src/backend/access/nbtree/nbtsort.c
@@ -315,7 +315,7 @@ _bt_blwritepage(BTWriteState *wstate, Page page, BlockNumber blkno)
{
/* overwriting a block we zero-filled before */
smgrwrite(wstate->index->rd_smgr, MAIN_FORKNUM, blkno,
- (char *) page, true);
+ (char *) page, true, NULL);
}
pfree(page);
diff --git a/src/backend/access/spgist/spginsert.c b/src/backend/access/spgist/spginsert.c
index bceee8d..149c1c4 100644
--- a/src/backend/access/spgist/spginsert.c
+++ b/src/backend/access/spgist/spginsert.c
@@ -170,7 +170,7 @@ spgbuildempty(PG_FUNCTION_ARGS)
/* Write the page. If archiving/streaming, XLOG it. */
PageSetChecksumInplace(page, SPGIST_METAPAGE_BLKNO);
smgrwrite(index->rd_smgr, INIT_FORKNUM, SPGIST_METAPAGE_BLKNO,
- (char *) page, true);
+ (char *) page, true, NULL);
if (XLogIsNeeded())
log_newpage(&index->rd_smgr->smgr_rnode.node, INIT_FORKNUM,
SPGIST_METAPAGE_BLKNO, page, false);
@@ -180,7 +180,7 @@ spgbuildempty(PG_FUNCTION_ARGS)
PageSetChecksumInplace(page, SPGIST_ROOT_BLKNO);
smgrwrite(index->rd_smgr, INIT_FORKNUM, SPGIST_ROOT_BLKNO,
- (char *) page, true);
+ (char *) page, true, NULL);
if (XLogIsNeeded())
log_newpage(&index->rd_smgr->smgr_rnode.node, INIT_FORKNUM,
SPGIST_ROOT_BLKNO, page, true);
@@ -190,7 +190,7 @@ spgbuildempty(PG_FUNCTION_ARGS)
PageSetChecksumInplace(page, SPGIST_NULL_BLKNO);
smgrwrite(index->rd_smgr, INIT_FORKNUM, SPGIST_NULL_BLKNO,
- (char *) page, true);
+ (char *) page, true, NULL);
if (XLogIsNeeded())
log_newpage(&index->rd_smgr->smgr_rnode.node, INIT_FORKNUM,
SPGIST_NULL_BLKNO, page, true);
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 08d1682..a40f7d5 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -7980,11 +7980,13 @@ LogCheckpointEnd(bool restartpoint)
sync_secs,
total_secs,
longest_secs,
+ sort_secs,
average_secs;
int write_usecs,
sync_usecs,
total_usecs,
longest_usecs,
+ sort_usecs,
average_usecs;
uint64 average_sync_time;
@@ -8015,6 +8017,10 @@ LogCheckpointEnd(bool restartpoint)
CheckpointStats.ckpt_end_t,
&total_secs, &total_usecs);
+ TimestampDifference(CheckpointStats.ckpt_sort_t,
+ CheckpointStats.ckpt_sort_end_t,
+ &sort_secs, &sort_usecs);
+
/*
* Timing values returned from CheckpointStats are in microseconds.
* Convert to the second plus microsecond form that TimestampDifference
@@ -8033,8 +8039,8 @@ LogCheckpointEnd(bool restartpoint)
elog(LOG, "%s complete: wrote %d buffers (%.1f%%); "
"%d transaction log file(s) added, %d removed, %d recycled; "
- "write=%ld.%03d s, sync=%ld.%03d s, total=%ld.%03d s; "
- "sync files=%d, longest=%ld.%03d s, average=%ld.%03d s; "
+ "sort=%ld.%03d s, write=%ld.%03d s, sync=%ld.%03d s, total=%ld.%03d s;"
+ " sync files=%d, longest=%ld.%03d s, average=%ld.%03d s; "
"distance=%d kB, estimate=%d kB",
restartpoint ? "restartpoint" : "checkpoint",
CheckpointStats.ckpt_bufs_written,
@@ -8042,6 +8048,7 @@ LogCheckpointEnd(bool restartpoint)
CheckpointStats.ckpt_segs_added,
CheckpointStats.ckpt_segs_removed,
CheckpointStats.ckpt_segs_recycled,
+ sort_secs, sort_usecs / 1000,
write_secs, write_usecs / 1000,
sync_secs, sync_usecs / 1000,
total_secs, total_usecs / 1000,
diff --git a/src/backend/storage/buffer/README b/src/backend/storage/buffer/README
index 45c5c83..e33e2ba 100644
--- a/src/backend/storage/buffer/README
+++ b/src/backend/storage/buffer/README
@@ -265,11 +265,6 @@ only needs to take the lock long enough to read the variable value, not
while scanning the buffers. (This is a very substantial improvement in
the contention cost of the writer compared to PG 8.0.)
-During a checkpoint, the writer's strategy must be to write every dirty
-buffer (pinned or not!). We may as well make it start this scan from
-nextVictimBuffer, however, so that the first-to-be-written pages are the
-ones that backends might otherwise have to write for themselves soon.
-
The background writer takes shared content lock on a buffer while writing it
out (and anyone else who flushes buffer contents to disk must do so too).
This ensures that the page image transferred to disk is reasonably consistent.
diff --git a/src/backend/storage/buffer/buf_init.c b/src/backend/storage/buffer/buf_init.c
index 3ae2848..c6a3be8 100644
--- a/src/backend/storage/buffer/buf_init.c
+++ b/src/backend/storage/buffer/buf_init.c
@@ -20,6 +20,7 @@
BufferDescPadded *BufferDescriptors;
char *BufferBlocks;
+CkptSortItem *CkptBufferIds;
/*
@@ -65,7 +66,8 @@ void
InitBufferPool(void)
{
bool foundBufs,
- foundDescs;
+ foundDescs,
+ foundBufCkpt;
/* Align descriptors to a cacheline boundary. */
BufferDescriptors = (BufferDescPadded *) CACHELINEALIGN(
@@ -77,10 +79,21 @@ InitBufferPool(void)
ShmemInitStruct("Buffer Blocks",
NBuffers * (Size) BLCKSZ, &foundBufs);
- if (foundDescs || foundBufs)
+ /*
+ * The array used to sort to-be-checkpointed buffer ids is located in
+ * shared memory, to avoid having to allocate significant amounts of
+ * memory at runtime. As that'd be in the middle of a checkpoint, or when
+ * the checkpointer is restarted, memory allocation failures would be
+ * painful.
+ */
+ CkptBufferIds = (CkptSortItem *)
+ ShmemInitStruct("Checkpoint BufferIds",
+ NBuffers * sizeof(CkptSortItem), &foundBufCkpt);
+
+ if (foundDescs || foundBufs || foundBufCkpt)
{
- /* both should be present or neither */
- Assert(foundDescs && foundBufs);
+ /* all should be present or neither */
+ Assert(foundDescs && foundBufs && foundBufCkpt);
/* note: this path is only taken in EXEC_BACKEND case */
}
else
@@ -144,5 +157,8 @@ BufferShmemSize(void)
/* size of stuff controlled by freelist.c */
size = add_size(size, StrategyShmemSize());
+ /* size of checkpoint sort array in bufmgr.c */
+ size = add_size(size, mul_size(NBuffers, sizeof(CkptSortItem)));
+
return size;
}
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index 8c0358e..5fb09c8 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -37,6 +37,7 @@
#include "catalog/catalog.h"
#include "catalog/storage.h"
#include "executor/instrument.h"
+#include "lib/binaryheap.h"
#include "miscadmin.h"
#include "pg_trace.h"
#include "pgstat.h"
@@ -47,6 +48,7 @@
#include "storage/proc.h"
#include "storage/smgr.h"
#include "storage/standby.h"
+#include "utils/memutils.h"
#include "utils/rel.h"
#include "utils/resowner_private.h"
#include "utils/timestamp.h"
@@ -75,6 +77,36 @@ typedef struct PrivateRefCountEntry
/* 64 bytes, about the size of a cache line on common systems */
#define REFCOUNT_ARRAY_ENTRIES 8
+/*
+ * Status of buffers to checkpoint for a particular tablespace, used
+ * internally in BufferSync.
+ */
+typedef struct CkptTsStatus
+{
+ /* oid of the tablespace */
+ Oid tsId;
+
+ /*
+ * Checkpoint progress for this tablespace. To make progress comparable
+ * between tablespaces the progress is, for each tablespace, measured as a
+ * number between 0 and the total number of to-be-checkpointed pages. Each
+ * page checkpointed in this tablespace increments this space's progress
+ * by progress_slice.
+ */
+ float8 progress;
+ float8 progress_slice;
+
+ /* number of to-be checkpointed pages in this tablespace */
+ int num_to_scan;
+ /* already processed pages in this tablespace */
+ int num_scanned;
+
+ /* current offset in CkptBufferIds for this tablespace */
+ int index;
+
+ FileFlushContext flushContext;
+} CkptTsStatus;
+
/* GUC variables */
bool zero_damaged_pages = false;
int bgwriter_lru_maxpages = 100;
@@ -82,6 +114,9 @@ double bgwriter_lru_multiplier = 2.0;
bool track_io_timing = false;
int effective_io_concurrency = 0;
+/* hint to move writes to high priority */
+bool checkpoint_flush_to_disk = DEFAULT_CHECKPOINT_FLUSH_TO_DISK;
+
/*
* How many buffers PrefetchBuffer callers should try to stay ahead of their
* ReadBuffer calls by. This is maintained by the assign hook for
@@ -399,7 +434,8 @@ static bool PinBuffer(volatile BufferDesc *buf, BufferAccessStrategy strategy);
static void PinBuffer_Locked(volatile BufferDesc *buf);
static void UnpinBuffer(volatile BufferDesc *buf, bool fixOwner);
static void BufferSync(int flags);
-static int SyncOneBuffer(int buf_id, bool skip_recently_used);
+static int SyncOneBuffer(int buf_id, bool skip_recently_used,
+ FileFlushContext *flush_context);
static void WaitIO(volatile BufferDesc *buf);
static bool StartBufferIO(volatile BufferDesc *buf, bool forInput);
static void TerminateBufferIO(volatile BufferDesc *buf, bool clear_dirty,
@@ -412,10 +448,13 @@ static volatile BufferDesc *BufferAlloc(SMgrRelation smgr,
BlockNumber blockNum,
BufferAccessStrategy strategy,
bool *foundPtr);
-static void FlushBuffer(volatile BufferDesc *buf, SMgrRelation reln);
+static void FlushBuffer(volatile BufferDesc *buf, SMgrRelation reln,
+ FileFlushContext *flush_context);
static void AtProcExit_Buffers(int code, Datum arg);
static void CheckForBufferLeaks(void);
static int rnode_comparator(const void *p1, const void *p2);
+static int ckpt_buforder_comparator(const void *pa, const void *pb);
+static int ts_ckpt_progress_comparator(Datum a, Datum b, void *arg);
/*
@@ -943,6 +982,14 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
int buf_id;
volatile BufferDesc *buf;
bool valid;
+ static FileFlushContext *context = NULL;
+
+ /* XXX: Should probably rather be in buf_init() */
+ if (context == NULL)
+ {
+ context = MemoryContextAlloc(TopMemoryContext, sizeof(*context));
+ FlushContextInit(context, FLUSH_CONTEXT_DEFAULT_MAX_COALESCE);
+ }
/* create a tag so we can lookup the buffer */
INIT_BUFFERTAG(newTag, smgr->smgr_rnode.node, forkNum, blockNum);
@@ -1078,8 +1125,8 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
smgr->smgr_rnode.node.spcNode,
smgr->smgr_rnode.node.dbNode,
smgr->smgr_rnode.node.relNode);
-
- FlushBuffer(buf, NULL);
+ /* FIXME: configurable */
+ FlushBuffer(buf, NULL, context);
LWLockRelease(buf->content_lock);
TRACE_POSTGRESQL_BUFFER_WRITE_DIRTY_DONE(forkNum, blockNum,
@@ -1637,10 +1684,16 @@ BufferSync(int flags)
{
int buf_id;
int num_to_scan;
- int num_to_write;
+ int num_spaces;
+ int num_processed;
int num_written;
+ CkptTsStatus *per_ts_stat = NULL;
+ Oid last_tsid;
+ binaryheap *ts_heap;
+ int i;
int mask = BM_DIRTY;
+
/* Make sure we can handle the pin inside SyncOneBuffer */
ResourceOwnerEnlargeBuffers(CurrentResourceOwner);
@@ -1655,7 +1708,7 @@ BufferSync(int flags)
/*
* Loop over all buffers, and mark the ones that need to be written with
- * BM_CHECKPOINT_NEEDED. Count them as we go (num_to_write), so that we
+ * BM_CHECKPOINT_NEEDED. Count them as we go (num_to_scan), so that we
* can estimate how much work needs to be done.
*
* This allows us to write only those pages that were dirty when the
@@ -1669,7 +1722,7 @@ BufferSync(int flags)
* BM_CHECKPOINT_NEEDED still set. This is OK since any such buffer would
* certainly need to be written for the next checkpoint attempt, too.
*/
- num_to_write = 0;
+ num_to_scan = 0;
for (buf_id = 0; buf_id < NBuffers; buf_id++)
{
volatile BufferDesc *bufHdr = GetBufferDescriptor(buf_id);
@@ -1682,32 +1735,144 @@ BufferSync(int flags)
if ((bufHdr->flags & mask) == mask)
{
+ CkptSortItem *item;
+
bufHdr->flags |= BM_CHECKPOINT_NEEDED;
- num_to_write++;
+
+ item = &CkptBufferIds[num_to_scan++];
+ item->buf_id = buf_id;
+ item->tsId = bufHdr->tag.rnode.spcNode;
+ item->relNode = bufHdr->tag.rnode.relNode;
+ item->forkNum = bufHdr->tag.forkNum;
+ item->blockNum = bufHdr->tag.blockNum;
}
UnlockBufHdr(bufHdr);
}
- if (num_to_write == 0)
+ if (num_to_scan == 0)
return; /* nothing to do */
- TRACE_POSTGRESQL_BUFFER_SYNC_START(NBuffers, num_to_write);
+ TRACE_POSTGRESQL_BUFFER_SYNC_START(NBuffers, num_to_scan);
/*
- * Loop over all buffers again, and write the ones (still) marked with
- * BM_CHECKPOINT_NEEDED. In this loop, we start at the clock sweep point
- * since we might as well dump soon-to-be-recycled buffers first.
- *
- * Note that we don't read the buffer alloc count here --- that should be
- * left untouched till the next BgBufferSync() call.
+ * Sort buffers that need to be written to reduce the likelihood of random
+ * IO. The sorting is also important for the implementation of balancing
+ * writes between tablespaces. Without balancing writes we'd potentially
+ * end up writing to the tablespaces one-by-one; possibly overloading the
+ * underlying system.
+ */
+ CheckpointStats.ckpt_sort_t = GetCurrentTimestamp();
+ qsort(CkptBufferIds, num_to_scan, sizeof(CkptSortItem),
+ ckpt_buforder_comparator);
+ CheckpointStats.ckpt_sort_end_t = GetCurrentTimestamp();
+
+ num_spaces = 0;
+
+ /*
+ * Allocate progress status for each tablespace with buffers that need to
+ * be flushed. This requires the to-be-flushed array to be sorted.
+ */
+ last_tsid = InvalidOid;
+ for (i = 0; i < num_to_scan; i++)
+ {
+ CkptTsStatus *s;
+ Oid cur_tsid;
+
+ cur_tsid = CkptBufferIds[i].tsId;
+
+ /*
+ * Grow array of per-tablespace status structs, everytime a new
+ * tablespace is found.
+ */
+ if (last_tsid == InvalidOid || last_tsid != cur_tsid)
+ {
+ Size sz;
+
+ num_spaces++;
+
+ /*
+ * Not worth adding grow-by-power-of-2 logic here - even with a
+ * few hundred tablespaces this will be fine.
+ */
+ sz = sizeof(CkptTsStatus) * num_spaces;
+
+ if (per_ts_stat == NULL)
+ per_ts_stat = (CkptTsStatus *) palloc(sz);
+ else
+ per_ts_stat = (CkptTsStatus *) repalloc(per_ts_stat, sz);
+
+ s = &per_ts_stat[num_spaces - 1];
+ memset(s, 0, sizeof(*s));
+ s->tsId = cur_tsid;
+
+ /*
+ * The first buffer in this tablespace. As CkptBufferIds is sorted
+ * by tablespace all (s->num_to_scan) buffers in this tablespace
+ * will follow afterwards.
+ */
+ s->index = i;
+
+ /*
+ * The progress_slice will be computed once we know how many
+ * buffers are in this tablespace, i.e. after this loop.
+ */
+
+ last_tsid = cur_tsid;
+ }
+ else
+ {
+ s = &per_ts_stat[num_spaces - 1];
+ }
+
+ s->num_to_scan++;
+ }
+
+ Assert(num_spaces > 0);
+
+ /*
+ * Build a min-heap over the write-progress in the individual tablespaces,
+ * and compute how large a portion of the total progress a single
+ * processed buffer is.
*/
- buf_id = StrategySyncStart(NULL, NULL);
- num_to_scan = NBuffers;
+ ts_heap = binaryheap_allocate(num_spaces,
+ ts_ckpt_progress_comparator,
+ NULL);
+
+ for (i = 0; i < num_spaces; i++)
+ {
+ CkptTsStatus *ts_stat = &per_ts_stat[i];
+
+ ts_stat->progress_slice = (float8) num_to_scan / ts_stat->num_to_scan;
+
+ FlushContextInit(&ts_stat->flushContext,
+ FLUSH_CONTEXT_DEFAULT_MAX_COALESCE);
+
+ binaryheap_add_unordered(ts_heap, PointerGetDatum(ts_stat));
+ }
+
+ binaryheap_build(ts_heap);
+
+ /*
+ * Iterate through to-be-checkpointed buffers and write the ones (still)
+ * marked with BM_CHECKPOINT_NEEDED. The writes are balanced between
+ * tablespaces.
+ */
+ num_processed = 0;
num_written = 0;
- while (num_to_scan-- > 0)
+ while (!binaryheap_empty(ts_heap))
{
- volatile BufferDesc *bufHdr = GetBufferDescriptor(buf_id);
+ volatile BufferDesc *bufHdr = NULL;
+ CkptTsStatus *ts_stat = (CkptTsStatus *)
+ DatumGetPointer(binaryheap_first(ts_heap));
+
+ buf_id = CkptBufferIds[ts_stat->index].buf_id;
+ Assert(buf_id != -1);
+
+ bufHdr = GetBufferDescriptor(buf_id);
+ Assert(bufHdr->tag.rnode.spcNode == ts_stat->tsId);
+
+ num_processed++;
/*
* We don't need to acquire the lock here, because we're only looking
@@ -1723,44 +1888,69 @@ BufferSync(int flags)
*/
if (bufHdr->flags & BM_CHECKPOINT_NEEDED)
{
- if (SyncOneBuffer(buf_id, false) & BUF_WRITTEN)
+ FileFlushContext *context;
+
+ if (checkpoint_flush_to_disk)
+ context = &ts_stat->flushContext;
+ else
+ context = NULL;
+
+ if (SyncOneBuffer(buf_id, false, context) & BUF_WRITTEN)
{
TRACE_POSTGRESQL_BUFFER_SYNC_WRITTEN(buf_id);
BgWriterStats.m_buf_written_checkpoints++;
num_written++;
+ }
+ }
- /*
- * We know there are at most num_to_write buffers with
- * BM_CHECKPOINT_NEEDED set; so we can stop scanning if
- * num_written reaches num_to_write.
- *
- * Note that num_written doesn't include buffers written by
- * other backends, or by the bgwriter cleaning scan. That
- * means that the estimate of how much progress we've made is
- * conservative, and also that this test will often fail to
- * trigger. But it seems worth making anyway.
- */
- if (num_written >= num_to_write)
- break;
+ /*
+ * Measure progress independent of actualy having to flush the buffer
+ * - otherwise writing become unbalanced.
+ */
+ ts_stat->progress += ts_stat->progress_slice;
+ ts_stat->num_scanned++;
+ ts_stat->index++;
- /*
- * Sleep to throttle our I/O rate.
- */
- CheckpointWriteDelay(flags, (double) num_written / num_to_write);
- }
+ /* Have all the buffers from the tablespace been processed? */
+ if (ts_stat->num_scanned == ts_stat->num_to_scan)
+ {
+ /*
+ * If there's a pending flush, perform that now, we're finished
+ * with the tablespace.
+ */
+ FlushContextIssuePending(&ts_stat->flushContext);
+
+ binaryheap_remove_first(ts_heap);
+ }
+ else
+ {
+ /* update heap with the new progress */
+ binaryheap_replace_first(ts_heap, PointerGetDatum(ts_stat));
}
- if (++buf_id >= NBuffers)
- buf_id = 0;
+ /*
+ * Sleep to throttle our I/O rate.
+ */
+ CheckpointWriteDelay(flags, (double) num_processed / num_to_scan);
+#ifdef CHECKPOINTER_DEBUG
+ /* delete current content of the line, print progress */
+ fprintf(stderr, "\33[2K\rto_scan: %d, scanned: %d, %%processed: %.2f, %%writeouts: %.2f",
+ num_to_scan, num_processed,
+ (((double) num_processed) / num_to_scan) * 100,
+ ((double) num_written / num_processed) * 100);
+#endif
}
+ pfree(per_ts_stat);
+ per_ts_stat = NULL;
+
/*
* Update checkpoint statistics. As noted above, this doesn't include
* buffers written by other backends or bgwriter scan.
*/
CheckpointStats.ckpt_bufs_written += num_written;
- TRACE_POSTGRESQL_BUFFER_SYNC_DONE(NBuffers, num_written, num_to_write);
+ TRACE_POSTGRESQL_BUFFER_SYNC_DONE(NBuffers, num_written, num_to_scan);
}
/*
@@ -1818,6 +2008,10 @@ BgBufferSync(void)
long new_strategy_delta;
uint32 new_recent_alloc;
+ FileFlushContext context;
+
+ FlushContextInit(&context, FLUSH_CONTEXT_DEFAULT_MAX_COALESCE);
+
/*
* Find out where the freelist clock sweep currently is, and how many
* buffer allocations have happened since our last call.
@@ -2000,7 +2194,15 @@ BgBufferSync(void)
/* Execute the LRU scan */
while (num_to_scan > 0 && reusable_buffers < upcoming_alloc_est)
{
- int buffer_state = SyncOneBuffer(next_to_clean, true);
+ int buffer_state;
+
+ /*
+ * FIXME: flushing should be configurable.
+ *
+ * Flushing here is important for latency, but also not unproblematic,
+ * because the buffers are written out entirely unsorted.
+ */
+ buffer_state = SyncOneBuffer(next_to_clean, true, &context);
if (++next_to_clean >= NBuffers)
{
@@ -2077,7 +2279,8 @@ BgBufferSync(void)
* Note: caller must have done ResourceOwnerEnlargeBuffers.
*/
static int
-SyncOneBuffer(int buf_id, bool skip_recently_used)
+SyncOneBuffer(int buf_id, bool skip_recently_used,
+ FileFlushContext *flush_context)
{
volatile BufferDesc *bufHdr = GetBufferDescriptor(buf_id);
int result = 0;
@@ -2118,7 +2321,7 @@ SyncOneBuffer(int buf_id, bool skip_recently_used)
PinBuffer_Locked(bufHdr);
LWLockAcquire(bufHdr->content_lock, LW_SHARED);
- FlushBuffer(bufHdr, NULL);
+ FlushBuffer(bufHdr, NULL, flush_context);
LWLockRelease(bufHdr->content_lock);
UnpinBuffer(bufHdr, true);
@@ -2380,9 +2583,16 @@ BufferGetTag(Buffer buffer, RelFileNode *rnode, ForkNumber *forknum,
*
* If the caller has an smgr reference for the buffer's relation, pass it
* as the second parameter. If not, pass NULL.
+ *
+ * The third parameter tries to hint the OS that a high priority write is meant,
+ * possibly because io-throttling is already managed elsewhere.
+ * The last parameter holds the current flush context that accumulates flush
+ * requests to be performed in one call, instead of being performed on a buffer
+ * per buffer basis.
*/
static void
-FlushBuffer(volatile BufferDesc *buf, SMgrRelation reln)
+FlushBuffer(volatile BufferDesc *buf, SMgrRelation reln,
+ FileFlushContext *flush_context)
{
XLogRecPtr recptr;
ErrorContextCallback errcallback;
@@ -2471,7 +2681,8 @@ FlushBuffer(volatile BufferDesc *buf, SMgrRelation reln)
buf->tag.forkNum,
buf->tag.blockNum,
bufToWrite,
- false);
+ false,
+ flush_context);
if (track_io_timing)
{
@@ -2893,7 +3104,8 @@ FlushRelationBuffers(Relation rel)
bufHdr->tag.forkNum,
bufHdr->tag.blockNum,
localpage,
- false);
+ false,
+ NULL);
bufHdr->flags &= ~(BM_DIRTY | BM_JUST_DIRTIED);
@@ -2927,7 +3139,7 @@ FlushRelationBuffers(Relation rel)
{
PinBuffer_Locked(bufHdr);
LWLockAcquire(bufHdr->content_lock, LW_SHARED);
- FlushBuffer(bufHdr, rel->rd_smgr);
+ FlushBuffer(bufHdr, rel->rd_smgr, NULL);
LWLockRelease(bufHdr->content_lock);
UnpinBuffer(bufHdr, true);
}
@@ -2979,7 +3191,7 @@ FlushDatabaseBuffers(Oid dbid)
{
PinBuffer_Locked(bufHdr);
LWLockAcquire(bufHdr->content_lock, LW_SHARED);
- FlushBuffer(bufHdr, NULL);
+ FlushBuffer(bufHdr, NULL, NULL);
LWLockRelease(bufHdr->content_lock);
UnpinBuffer(bufHdr, true);
}
@@ -3701,3 +3913,56 @@ rnode_comparator(const void *p1, const void *p2)
else
return 0;
}
+
+/*
+ * Comparator determining the writeout order in a checkpoint.
+ *
+ * It is important that tablespaces are compared first as the logic balancing
+ * writes between tablespaces relies on it.
+ */
+static int
+ckpt_buforder_comparator(const void *pa, const void *pb)
+{
+ const CkptSortItem *a = (CkptSortItem *) pa;
+ const CkptSortItem *b = (CkptSortItem *) pb;
+
+ /* compare tablespace */
+ if (a->tsId < b->tsId)
+ return -1;
+ else if (a->tsId > b->tsId)
+ return 1;
+ /* compare relation */
+ if (a->relNode < b->relNode)
+ return -1;
+ else if (a->relNode > b->relNode)
+ return 1;
+ /* compare fork */
+ else if (a->forkNum < b->forkNum)
+ return -1;
+ else if (a->forkNum > b->forkNum)
+ return 1;
+ /* compare block number */
+ else if (a->blockNum < b->blockNum)
+ return -1;
+ else /* should not be the same block anyway... */
+ return 1;
+}
+
+/*
+ * Comparator for a Min-Heap over the, per-tablespace, checkpoint completion
+ * progress.
+ */
+static int
+ts_ckpt_progress_comparator(Datum a, Datum b, void *arg)
+{
+ CkptTsStatus *sa = (CkptTsStatus *) a;
+ CkptTsStatus *sb = (CkptTsStatus *) b;
+
+ /* we want a min-heap, so return 1 for the a < b */
+ if (sa->progress < sb->progress)
+ return 1;
+ else if (sa->progress == sb->progress)
+ return 0;
+ else
+ return -1;
+}
diff --git a/src/backend/storage/buffer/freelist.c b/src/backend/storage/buffer/freelist.c
index bc2c773..18e4397 100644
--- a/src/backend/storage/buffer/freelist.c
+++ b/src/backend/storage/buffer/freelist.c
@@ -358,10 +358,10 @@ StrategyFreeBuffer(volatile BufferDesc *buf)
}
/*
- * StrategySyncStart -- tell BufferSync where to start syncing
+ * StrategySyncStart -- tell BgBufferSync where to start syncing
*
- * The result is the buffer index of the best buffer to sync first.
- * BufferSync() will proceed circularly around the buffer array from there.
+ * The result is the buffer index below the current clock-hand. BgBufferSync()
+ * will proceed circularly around the buffer array from there.
*
* In addition, we return the completed-pass count (which is effectively
* the higher-order bits of nextVictimBuffer) and the count of recent buffer
diff --git a/src/backend/storage/buffer/localbuf.c b/src/backend/storage/buffer/localbuf.c
index 3144afe..c508fc6 100644
--- a/src/backend/storage/buffer/localbuf.c
+++ b/src/backend/storage/buffer/localbuf.c
@@ -208,7 +208,8 @@ LocalBufferAlloc(SMgrRelation smgr, ForkNumber forkNum, BlockNumber blockNum,
bufHdr->tag.forkNum,
bufHdr->tag.blockNum,
localpage,
- false);
+ false,
+ NULL);
/* Mark not-dirty now in case we error out below */
bufHdr->flags &= ~BM_DIRTY;
diff --git a/src/backend/storage/file/buffile.c b/src/backend/storage/file/buffile.c
index ea4d689..f2913df 100644
--- a/src/backend/storage/file/buffile.c
+++ b/src/backend/storage/file/buffile.c
@@ -317,7 +317,8 @@ BufFileDumpBuffer(BufFile *file)
return; /* seek failed, give up */
file->offsets[file->curFile] = file->curOffset;
}
- bytestowrite = FileWrite(thisfile, file->buffer + wpos, bytestowrite);
+ bytestowrite = FileWrite(thisfile, file->buffer + wpos, bytestowrite,
+ NULL);
if (bytestowrite <= 0)
return; /* failed to write */
file->offsets[file->curFile] += bytestowrite;
diff --git a/src/backend/storage/file/copydir.c b/src/backend/storage/file/copydir.c
index 41b2c62..81c9754 100644
--- a/src/backend/storage/file/copydir.c
+++ b/src/backend/storage/file/copydir.c
@@ -190,9 +190,9 @@ copy_file(char *fromfile, char *tofile)
/*
* We fsync the files later but first flush them to avoid spamming the
* cache and hopefully get the kernel to start writing them out before
- * the fsync comes. Ignore any error, since it's only a hint.
+ * the fsync comes.
*/
- (void) pg_flush_data(dstfd, offset, nbytes);
+ pg_flush_data(dstfd, offset, nbytes);
}
if (CloseTransientFile(dstfd))
diff --git a/src/backend/storage/file/fd.c b/src/backend/storage/file/fd.c
index 1ba4946..2974c2b 100644
--- a/src/backend/storage/file/fd.c
+++ b/src/backend/storage/file/fd.c
@@ -61,6 +61,9 @@
#include <sys/file.h>
#include <sys/param.h>
#include <sys/stat.h>
+#ifndef WIN32
+#include <sys/mman.h>
+#endif
#include <unistd.h>
#include <fcntl.h>
#ifdef HAVE_SYS_RESOURCE_H
@@ -82,6 +85,8 @@
/* Define PG_FLUSH_DATA_WORKS if we have an implementation for pg_flush_data */
#if defined(HAVE_SYNC_FILE_RANGE)
#define PG_FLUSH_DATA_WORKS 1
+#elif !defined(WIN32) && defined(MS_ASYNC)
+#define PG_FLUSH_DATA_WORKS 1
#elif defined(USE_POSIX_FADVISE) && defined(POSIX_FADV_DONTNEED)
#define PG_FLUSH_DATA_WORKS 1
#endif
@@ -380,29 +385,128 @@ pg_fdatasync(int fd)
}
/*
- * pg_flush_data --- advise OS that the data described won't be needed soon
+ * pg_flush_data --- advise OS that the described dirty data should be flushed
*
- * Not all platforms have sync_file_range or posix_fadvise; treat as no-op
- * if not available. Also, treat as no-op if enableFsync is off; this is
- * because the call isn't free, and some platforms such as Linux will actually
- * block the requestor until the write is scheduled.
+ * An offset of 0 with an amount of 0 means that the entire file should be
+ * flushed.
*/
-int
-pg_flush_data(int fd, off_t offset, off_t amount)
+void
+pg_flush_data(int fd, off_t offset, off_t nbytes)
{
#ifdef PG_FLUSH_DATA_WORKS
- if (enableFsync)
- {
+
+ /*
+ * Right now file flushing is primarily used to avoid making later
+ * fsync()/fdatasync() calls have a significant impact. Thus don't trigger
+ * flushes if fsyncs are disabled - that's a decision we might want to
+ * make configurable at some point.
+ */
+ if (!enableFsync)
+ return;
+
#if defined(HAVE_SYNC_FILE_RANGE)
- return sync_file_range(fd, offset, amount, SYNC_FILE_RANGE_WRITE);
+ {
+ int rc = 0;
+
+ /*
+ * sync_file_range(2), currently linux specific, with
+ * SYNC_FILE_RANGE_WRITE as a parameter tells the OS that writeback
+ * for the passed in blocks should be started, but that we don't want
+ * to wait for completion. Note that this call might block if too
+ * much dirty data exists in the range. This is the preferrable
+ * method on OSs supporting it, as it works reliably when available
+ * (contrast to msync()) and doesn't flush out clean data (like
+ * FADV_DONTNEED).
+ */
+ rc = sync_file_range(fd, offset, nbytes,
+ SYNC_FILE_RANGE_WRITE);
+
+ /* don't error out, this is just a performance optimization */
+ if (rc != 0)
+ {
+ ereport(WARNING,
+ (errcode_for_file_access(),
+ errmsg("could not flush dirty data: %m")));
+ }
+ }
+#elif !defined(WIN32) && defined(MS_ASYNC)
+ {
+ int rc = 0;
+ void *p;
+
+ /*
+ * On many OSs msync() on a mmap'ed file triggers writeback. On linux
+ * it only does so when MS_SYNC is specified, but then it does the
+ * writeback in the foreground. Luckily all common linux systems have
+ * sync_file_range(). This is preferrable over FADV_DONTNEED because
+ * it doesn't flush out clean data.
+ *
+ * We map the file (mmap()), tell the kernel to sync back the contents
+ * (msync()), and then remove the mapping again (munmap()).
+ */
+
+ p = mmap(NULL, context->nbytes,
+ PROT_READ | PROT_WRITE, MAP_SHARED,
+ context->fd, context->offset);
+ if (p == MAP_FAILED)
+ {
+ ereport(WARNING,
+ (errcode_for_file_access(),
+ errmsg("could not mmap while flushing dirty data in file \"%s\": %m",
+ context->filename ? context->filename : "")));
+ goto out;
+ }
+
+ rc = msync(p, context->nbytes, MS_ASYNC);
+ if (rc != 0)
+ {
+ ereport(WARNING,
+ (errcode_for_file_access(),
+ errmsg("could not flush dirty data in file \"%s\": %m",
+ context->filename ? context->filename : "")));
+ /* NB: need to fall through to munmap()! */
+ }
+
+ rc = munmap(p, context->nbytes);
+ if (rc != 0)
+ {
+ /* FATAL error because mapping would remain */
+ ereport(FATAL,
+ (errcode_for_file_access(),
+ errmsg("could not munmap while flushing blocks in file \"%s\": %m",
+ context->filename ? context->filename : "")));
+ }
+ }
#elif defined(USE_POSIX_FADVISE) && defined(POSIX_FADV_DONTNEED)
- return posix_fadvise(fd, offset, amount, POSIX_FADV_DONTNEED);
+ {
+ int rc = 0;
+
+ /*
+ * Signal the kernel that the passed in range should not be cached
+ * anymore. This has the, desired, side effect of writing out dirty
+ * data, and the, undesired, side effect of likely discarding useful
+ * clean cached blocks. For the latter reason this is the least
+ * preferrable method.
+ */
+
+ rc = posix_fadvise(context->fd, context->offset, context->nbytes,
+ POSIX_FADV_DONTNEED);
+
+ /* don't error out, this is just a performance optimization */
+ if (rc != 0)
+ {
+ ereport(WARNING,
+ (errcode_for_file_access(),
+ errmsg("could not flush dirty data in file \"%s\": %m",
+ context->filename ? context->filename : "")));
+ goto out;
+ }
+ }
#else
#error PG_FLUSH_DATA_WORKS should not have been defined
#endif
- }
-#endif
- return 0;
+
+#endif /* PG_FLUSH_DATA_WORKS */
}
@@ -1345,7 +1449,8 @@ retry:
}
int
-FileWrite(File file, char *buffer, int amount)
+FileWrite(File file, char *buffer, int amount,
+ FileFlushContext *flush_context)
{
int returnCode;
@@ -1408,6 +1513,11 @@ retry:
VfdCache[file].fileSize = newPos;
}
}
+
+ /* update bulk flush state */
+ if (flush_context != NULL)
+ FlushContextSchedule(flush_context, file,
+ VfdCache[file].seekPos, amount);
}
else
{
@@ -1579,6 +1689,103 @@ FilePathName(File file)
/*
+ * Initialize a FileFlushContext, discarding potential previous state in
+ * context.
+ *
+ * max_coalesce is the maximum number of flush requests that will be coalesced
+ * into a bigger one. 0 meaning there is no limit.
+ */
+void
+FlushContextInit(FileFlushContext *context, int max_coalesce)
+{
+ context->max_coalesce = max_coalesce;
+ context->file = -1;
+ context->ncalls = 0;
+ context->offset = 0;
+ context->nbytes = 0;
+}
+
+/*
+ * Schedule writeout of a range of bytes in a file.
+ *
+ * filename is just used for error reporting, and may be NULL.
+ */
+void
+FlushContextSchedule(FileFlushContext *context,
+ File file, off_t offset, off_t nbytes)
+{
+ /*
+ * If the new range of blocks is in the same file as a previous request
+ * try to coalesce with previous requests. That increases the chance that
+ * these writeouts can be coalesced in the OSs IO layer and decreases the
+ * number of syscalls. If there are a lot of outstanding flush requests,
+ * immediately trigger writeout of previously blocks to avoid overflowing
+ * request queues and thelike, thereby causing latency spikes.
+ */
+ if (context->file == file && context->ncalls != 0)
+ {
+ int64 startoff;
+ int64 endoff;
+
+ /* merge current flush with previous ones */
+ startoff = Min(context->offset, offset);
+ endoff = Max(context->offset + context->nbytes, offset + nbytes);
+
+ context->offset = startoff;
+ context->nbytes = endoff - startoff;
+ context->ncalls++;
+
+ /*
+ * Accumulated enough dirty ranges - flush now. XXX: It might be
+ * worthwhile to count actual bytes that we've been asked to flush,
+ * and to have additional limits; but that's for another day.
+ */
+ if (context->max_coalesce > 0 &&
+ context->ncalls >= context->max_coalesce)
+ FlushContextIssuePending(context);
+ }
+ else
+ {
+ /* flush previous file & reset flush accumulator */
+ FlushContextIssuePending(context);
+
+ context->file = file;
+ context->ncalls = 1;
+ context->offset = offset;
+ context->nbytes = nbytes;
+ }
+}
+
+/*
+ * Issue all pending flush requests previously scheduled with
+ * FlushContextSchedule to the OS.
+ *
+ * Because this is, currently, only used to improve the OSs IO scheduling we
+ * try hard to never error out - it's just a hint.
+ */
+void
+FlushContextIssuePending(FileFlushContext *context)
+{
+ int rc;
+
+ if (context->ncalls == 0)
+ return;
+
+ rc = FileAccess(context->file);
+ if (rc < 0)
+ return;
+
+ pg_flush_data(VfdCache[context->file].fd,
+ context->offset, context->nbytes);
+
+ context->file = -1;
+ context->ncalls = 0;
+ context->offset = 0;
+ context->nbytes = 0;
+}
+
+
+/*
* Make room for another allocatedDescs[] array entry if needed and possible.
* Returns true if an array element is available.
*/
@@ -2655,9 +2862,10 @@ pre_sync_fname(const char *fname, bool isdir, int elevel)
}
/*
- * We ignore errors from pg_flush_data() because this is only a hint.
+ * pg_flush_data() ignores errors, which is ok because this is only a
+ * hint.
*/
- (void) pg_flush_data(fd, 0, 0);
+ pg_flush_data(fd, 0, 0);
(void) CloseTransientFile(fd);
}
diff --git a/src/backend/storage/smgr/md.c b/src/backend/storage/smgr/md.c
index 42a43bb..eeaac07 100644
--- a/src/backend/storage/smgr/md.c
+++ b/src/backend/storage/smgr/md.c
@@ -531,7 +531,7 @@ mdextend(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
errmsg("could not seek to block %u in file \"%s\": %m",
blocknum, FilePathName(v->mdfd_vfd))));
- if ((nbytes = FileWrite(v->mdfd_vfd, buffer, BLCKSZ)) != BLCKSZ)
+ if ((nbytes = FileWrite(v->mdfd_vfd, buffer, BLCKSZ, NULL)) != BLCKSZ)
{
if (nbytes < 0)
ereport(ERROR,
@@ -738,7 +738,7 @@ mdread(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
*/
void
mdwrite(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
- char *buffer, bool skipFsync)
+ char *buffer, bool skipFsync, FileFlushContext *flush_context)
{
off_t seekpos;
int nbytes;
@@ -767,7 +767,7 @@ mdwrite(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
errmsg("could not seek to block %u in file \"%s\": %m",
blocknum, FilePathName(v->mdfd_vfd))));
- nbytes = FileWrite(v->mdfd_vfd, buffer, BLCKSZ);
+ nbytes = FileWrite(v->mdfd_vfd, buffer, BLCKSZ, flush_context);
TRACE_POSTGRESQL_SMGR_MD_WRITE_DONE(forknum, blocknum,
reln->smgr_rnode.node.spcNode,
diff --git a/src/backend/storage/smgr/smgr.c b/src/backend/storage/smgr/smgr.c
index 244b4ea..31c15a6 100644
--- a/src/backend/storage/smgr/smgr.c
+++ b/src/backend/storage/smgr/smgr.c
@@ -52,7 +52,8 @@ typedef struct f_smgr
void (*smgr_read) (SMgrRelation reln, ForkNumber forknum,
BlockNumber blocknum, char *buffer);
void (*smgr_write) (SMgrRelation reln, ForkNumber forknum,
- BlockNumber blocknum, char *buffer, bool skipFsync);
+ BlockNumber blocknum, char *buffer, bool skipFsync,
+ FileFlushContext *flush_context);
BlockNumber (*smgr_nblocks) (SMgrRelation reln, ForkNumber forknum);
void (*smgr_truncate) (SMgrRelation reln, ForkNumber forknum,
BlockNumber nblocks);
@@ -643,10 +644,11 @@ smgrread(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
*/
void
smgrwrite(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
- char *buffer, bool skipFsync)
+ char *buffer, bool skipFsync, FileFlushContext *flush_context)
{
(*(smgrsw[reln->smgr_which].smgr_write)) (reln, forknum, blocknum,
- buffer, skipFsync);
+ buffer, skipFsync,
+ flush_context);
}
/*
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index fda0fb9..b72f782 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -1004,6 +1004,18 @@ static struct config_bool ConfigureNamesBool[] =
false,
NULL, NULL, NULL
},
+
+ {
+ {"checkpoint_flush_to_disk", PGC_SIGHUP, WAL_CHECKPOINTS,
+ gettext_noop("Hint that checkpoint's writes are high priority."),
+ NULL
+ },
+ &checkpoint_flush_to_disk,
+ /* see bufmgr.h: true on Linux, false otherwise */
+ DEFAULT_CHECKPOINT_FLUSH_TO_DISK,
+ NULL, NULL, NULL
+ },
+
{
{"log_connections", PGC_SU_BACKEND, LOGGING_WHAT,
gettext_noop("Logs each successful connection."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index dcf929f..20726dc 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -202,6 +202,8 @@
#max_wal_size = 1GB
#min_wal_size = 80MB
#checkpoint_completion_target = 0.5 # checkpoint target duration, 0.0 - 1.0
+#checkpoint_flush_to_disk = ? # send buffers to disk on checkpoint
+ # default is on if Linux, off otherwise
#checkpoint_warning = 30s # 0 disables
# - Archiving -
diff --git a/src/include/access/xlog.h b/src/include/access/xlog.h
index 790ca66..11815a8 100644
--- a/src/include/access/xlog.h
+++ b/src/include/access/xlog.h
@@ -186,6 +186,8 @@ extern bool XLOG_DEBUG;
typedef struct CheckpointStatsData
{
TimestampTz ckpt_start_t; /* start of checkpoint */
+ TimestampTz ckpt_sort_t; /* start buffer sorting */
+ TimestampTz ckpt_sort_end_t; /* end of sorting */
TimestampTz ckpt_write_t; /* start of flushing buffers */
TimestampTz ckpt_sync_t; /* start of fsyncs */
TimestampTz ckpt_sync_end_t; /* end of fsyncs */
diff --git a/src/include/storage/buf_internals.h b/src/include/storage/buf_internals.h
index 521ee1c..1628154 100644
--- a/src/include/storage/buf_internals.h
+++ b/src/include/storage/buf_internals.h
@@ -210,6 +210,24 @@ extern PGDLLIMPORT BufferDescPadded *BufferDescriptors;
/* in localbuf.c */
extern BufferDesc *LocalBufferDescriptors;
+/* in bufmgr.c */
+
+/*
+ * Structure to sort buffers per file on checkpoints.
+ *
+ * This structure is allocated per buffer in shared memory, so it should be
+ * kept as small as possible.
+ */
+typedef struct CkptSortItem
+{
+ Oid tsId;
+ Oid relNode;
+ ForkNumber forkNum;
+ BlockNumber blockNum;
+ int buf_id;
+} CkptSortItem;
+
+extern CkptSortItem *CkptBufferIds;
/*
* Internal routines: only called by bufmgr
diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.h
index 0f59201..28a3deb 100644
--- a/src/include/storage/bufmgr.h
+++ b/src/include/storage/bufmgr.h
@@ -55,6 +55,14 @@ extern double bgwriter_lru_multiplier;
extern bool track_io_timing;
extern int target_prefetch_pages;
+#ifdef HAVE_SYNC_FILE_RANGE
+#define DEFAULT_CHECKPOINT_FLUSH_TO_DISK true
+#else
+#define DEFAULT_CHECKPOINT_FLUSH_TO_DISK false
+#endif /* HAVE_SYNC_FILE_RANGE */
+
+extern bool checkpoint_flush_to_disk;
+
/* in buf_init.c */
extern PGDLLIMPORT char *BufferBlocks;
diff --git a/src/include/storage/fd.h b/src/include/storage/fd.h
index 7eabe09..a05500a 100644
--- a/src/include/storage/fd.h
+++ b/src/include/storage/fd.h
@@ -59,6 +59,34 @@ extern int max_files_per_process;
*/
extern int max_safe_fds;
+/*
+ * FlushContext structure - This is used to accumulate several flush requests
+ * made by one callsite into a larger flush request.
+ */
+typedef struct FileFlushContext
+{
+ /* max number of flush requests to coalesce */
+ int max_coalesce;
+ /* VFD of the last file processed or -1 */
+ File file;
+ /* number of flush requests merged together */
+ int ncalls;
+ /* offset to start flushing (minimum of all offsets) */
+ int64 offset;
+
+ /*
+ * Size (minimum extent to cover all flushed data). If 0 byt ncalls > 0,
+ * the whole file should be flushed.
+ */
+ int64 nbytes;
+} FileFlushContext;
+
+/*
+ * By default coalesce up to 32 flush requests to the same file. As flush
+ * requests usually are BLCKSZ large, that amounts to about the size of common
+ * IO request queues.
+ */
+#define FLUSH_CONTEXT_DEFAULT_MAX_COALESCE 64
/*
* prototypes for functions in fd.c
@@ -70,11 +98,16 @@ extern File OpenTemporaryFile(bool interXact);
extern void FileClose(File file);
extern int FilePrefetch(File file, off_t offset, int amount);
extern int FileRead(File file, char *buffer, int amount);
-extern int FileWrite(File file, char *buffer, int amount);
+extern int FileWrite(File file, char *buffer, int amount,
+ FileFlushContext * flush_context);
extern int FileSync(File file);
extern off_t FileSeek(File file, off_t offset, int whence);
extern int FileTruncate(File file, off_t offset);
extern char *FilePathName(File file);
+extern void FlushContextInit(FileFlushContext *context, int max_coalesce);
+extern void FlushContextIssuePending(FileFlushContext *context);
+extern void FlushContextSchedule(FileFlushContext *context, File file,
+ off_t offset, off_t nbytes);
/* Operations that allow use of regular stdio --- USE WITH CAUTION */
extern FILE *AllocateFile(const char *name, const char *mode);
@@ -112,7 +145,7 @@ extern int pg_fsync(int fd);
extern int pg_fsync_no_writethrough(int fd);
extern int pg_fsync_writethrough(int fd);
extern int pg_fdatasync(int fd);
-extern int pg_flush_data(int fd, off_t offset, off_t amount);
+extern void pg_flush_data(int fd, off_t offset, off_t amount);
extern void fsync_fname(char *fname, bool isdir);
extern void SyncDataDirectory(void);
diff --git a/src/include/storage/smgr.h b/src/include/storage/smgr.h
index 69a624f..e95b859 100644
--- a/src/include/storage/smgr.h
+++ b/src/include/storage/smgr.h
@@ -16,6 +16,7 @@
#include "fmgr.h"
#include "storage/block.h"
+#include "storage/fd.h"
#include "storage/relfilenode.h"
@@ -95,7 +96,8 @@ extern void smgrprefetch(SMgrRelation reln, ForkNumber forknum,
extern void smgrread(SMgrRelation reln, ForkNumber forknum,
BlockNumber blocknum, char *buffer);
extern void smgrwrite(SMgrRelation reln, ForkNumber forknum,
- BlockNumber blocknum, char *buffer, bool skipFsync);
+ BlockNumber blocknum, char *buffer, bool skipFsync,
+ FileFlushContext *flush_context);
extern BlockNumber smgrnblocks(SMgrRelation reln, ForkNumber forknum);
extern void smgrtruncate(SMgrRelation reln, ForkNumber forknum,
BlockNumber nblocks);
@@ -121,7 +123,8 @@ extern void mdprefetch(SMgrRelation reln, ForkNumber forknum,
extern void mdread(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
char *buffer);
extern void mdwrite(SMgrRelation reln, ForkNumber forknum,
- BlockNumber blocknum, char *buffer, bool skipFsync);
+ BlockNumber blocknum, char *buffer, bool skipFsync,
+ FileFlushContext *flush_context);
extern BlockNumber mdnblocks(SMgrRelation reln, ForkNumber forknum);
extern void mdtruncate(SMgrRelation reln, ForkNumber forknum,
BlockNumber nblocks);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 03e1d2c..2f00050 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -576,6 +576,7 @@ FileNameMap
FindSplitData
FixedParallelState
FixedParamState
+FileFlushContext
FmgrBuiltin
FmgrHookEventType
FmgrInfo
--
2.6.0.rc3
Hello Andres,
And here's v14. It's not something entirely ready.
I'm going to have a careful look at it.
A lot of details have changed, I unfortunately don't remember them all.
But there are more important things than the details of the patch.I've played *a lot* with this patch. I found a bunch of issues:
1) The FileFlushContext context infrastructure isn't actually
correct. There's two problems: First, using the actual 'fd' number to
reference a to-be-flushed file isn't meaningful. If there are lots
of files open, fds get reused within fd.c.
Hmm.
My assumption is that a file being used (i.e. with modifie pages, being
used for writes...) would not be closed before everything is cleared...
After some poking in the code, I think that this issue may indeed be
there, although the probability of hitting it is close to 0, but alas not
0:-)
To fix it, ITSM that it is enough to hold a "do not close lock" on the
file while a flush is in progress (a short time) that would prevent
mdclose to do its stuff.
That part is enough fixed by referencing File instead the fd. The bigger
problem is that the infrastructure doesn't deal with files being closed.
There can, which isn't that hard to trigger, be smgr invalidations
causing smgr handle and thus the file to be closed.I think this means that the entire flushing infrastructure actually
needs to be hoisted up, onto the smgr/md level.
Hmmm. I'm not sure that it is necessary, see above my suggestion.
2) I noticed that sync_file_range() blocked far more often than I'd
expected. Reading the kernel code that turned out to be caused by a
pessimization in the kernel introduced years ago - in many situation
SFR_WRITE waited for the writes. A fix for this will be in the 4.4
kernel.
Alas, Pg cannot help issues in the kernel.
3) I found that latency wasn't improved much for workloads that are
significantly bigger than shared buffers. The problem here is that
neither bgwriter nor the backends have, so far, done
sync_file_range() calls. That meant that the old problem of having
gigabytes of dirty data that periodically get flushed out, still
exists. Having these do flushes mostly attacks that problem.
I'm concious that the patch only addresses *checkpointer* writes, not
those from bgwrither or backends writes. I agree that these should need to
be addressed at some point as well, but given the time to get a patch
through, the more complex the slower (sort propositions are 10 years old),
I think this should be postponed for later.
Benchmarking revealed that for workloads where the hot data set mostly
fits into shared buffers flushing and sorting is anywhere from a small
to a massive improvement, both in throughput and latency. Even without
the patch from 2), although fixing that improves things furhter.
This is consistent with my experiments: sorting improves things, and
flushing on top of sorting improves things further.
What I did not expect, and what confounded me for a long while, is that
for workloads where the hot data set does *NOT* fit into shared buffers,
sorting often led to be a noticeable reduction in throughput. Up to
30%.
I did not see such behavior in the many tests I ran. Could you share more
precise details so that I can try to reproduce this performance
regression? (available memory, shared buffers, db size, ...).
The performance was still much more regular than before, i.e. no
more multi-second periods without any transactions happening.By now I think I know what's going on: Before the sorting portion of the
patch the write-loop in BufferSync() starts at the current clock hand,
by using StrategySyncStart(). But after the sorting that obviously
doesn't happen anymore - buffers are accessed in their sort order. By
starting at the current clock hand and moving on from there the
checkpointer basically makes it more less likely that victim buffers
need to be written either by the backends themselves or by
bgwriter. That means that the sorted checkpoint writes can, indirectly,
increase the number of unsorted writes by other processes :(
I'm quite surprised at such a large effect on throughput, though.
This explanation seems to suggest that if bgwriter/workders write are
sorted and/or coordinated with the checkpointer somehow then all would be
well?
ISTM that this explanation could be checked by looking whether
bgwriter/workers writes are especially large compared to checkpointer
writes in those cases with reduced throughput? The data is in the log.
My benchmarking suggest that that effect is the larger, the shorter the
checkpoint timeout is.
Hmmm. The shorter the timeout, the more likely the sorting NOT to be
effective, and the more likely to go back to random I/Os, and maybe to
seem some effect of the sync strategy stuff.
That seems to intuitively make sense, give the above explanation
attempt. If the checkpoint takes longer the clock hand will almost
certainly soon overtake checkpoints 'implicit' hand.I'm not sure if we can really do anything about this problem. While I'm
pretty jet lagged, I still spent a fair amount of time thinking about
it. Seems to suggest that we need to bring back the setting to
enable/disable sorting :(What I think needs to happen next with the patch is:
1) Hoist up the FileFlushContext stuff into the smgr layer. Carefully
handling the issue of smgr invalidations.
Not sure that much is necessary, see above.
2) Replace the boolean checkpoint_flush_to_disk GUC with a list guc that
later can contain multiple elements like checkpoint, bgwriter,
backends, ddl, bulk-writes. That seems better than adding GUCs for
these separately. Then make the flush locations in the patch
configurable using that.
My 0,02ᅵ on this point: I have not seen much of this style of guc
elsewhere. The only one I found while scanning the postgres file are
*_path and *_libraries. It seems to me that this would depart
significantly from the usual style, so one guc per case, or one shared guc
but with only on/off, would blend in more cleanly with the usual style.
3) I think we should remove the sort timing from the checkpoint logging
before commit. It'll always be pretty short.
I added it to show that it was really short, in response to concerns that
my approach of just sorting through indexes to reduce the memory needed
instead of copying the data to be sorted did not induce significant
performance issues. I prooved my point, but peer pressure made me switch
to larger memory anyway.
I think it should be kept while the features are under testing. I do not
think that it harms in anyway.
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Hi,
On 2015-11-12 15:31:41 +0100, Fabien COELHO wrote:
A lot of details have changed, I unfortunately don't remember them all.
But there are more important things than the details of the patch.I've played *a lot* with this patch. I found a bunch of issues:
1) The FileFlushContext context infrastructure isn't actually
correct. There's two problems: First, using the actual 'fd' number to
reference a to-be-flushed file isn't meaningful. If there are lots
of files open, fds get reused within fd.c.Hmm.
My assumption is that a file being used (i.e. with modifie pages, being used
for writes...) would not be closed before everything is cleared...
That's likely, but far from guaranteed.
After some poking in the code, I think that this issue may indeed be there,
although the probability of hitting it is close to 0, but alas not 0:-)
I did hit it...
To fix it, ITSM that it is enough to hold a "do not close lock" on the file
while a flush is in progress (a short time) that would prevent mdclose to do
its stuff.
Could you expand a bit more on this? You're suggesting something like a
boolean in the vfd struct? If that, how would you deal with FileClose()
being called?
3) I found that latency wasn't improved much for workloads that are
significantly bigger than shared buffers. The problem here is that
neither bgwriter nor the backends have, so far, done
sync_file_range() calls. That meant that the old problem of having
gigabytes of dirty data that periodically get flushed out, still
exists. Having these do flushes mostly attacks that problem.I'm concious that the patch only addresses *checkpointer* writes, not those
from bgwrither or backends writes. I agree that these should need to be
addressed at some point as well, but given the time to get a patch through,
the more complex the slower (sort propositions are 10 years old), I think
this should be postponed for later.
I think we need to have at least a PoC of all of the relevant
changes. We're doing these to fix significant latency and throughput
issues, and if the approach turns out not to be suitable for
e.g. bgwriter or backends, that might have influence over checkpointer's
design as well.
What I did not expect, and what confounded me for a long while, is that
for workloads where the hot data set does *NOT* fit into shared buffers,
sorting often led to be a noticeable reduction in throughput. Up to
30%.I did not see such behavior in the many tests I ran. Could you share more
precise details so that I can try to reproduce this performance regression?
(available memory, shared buffers, db size, ...).
I generally found that I needed to disable autovacuum's analyze to get
anything even close to stable numbers. The issue in described in
http://archives.postgresql.org/message-id/20151031145303.GC6064%40alap3.anarazel.de
otherwise badly kicks in. I basically just set
autovacuum_analyze_threshold to INT_MAX/2147483647 to prevent that from occuring.
I'll show actual numbers at some point yes. I tried three different systems:
* my laptop, 16 GB Ram, 840 EVO 1TB as storage. With 2GB
shared_buffers. Tried checkpoint timeouts from 60 to 300s. I could
see issues in workloads ranging from scale 300 to 5000. Throughput
regressions are visible for both sync_commit on/off workloads. Here
the largest regressions were visible.
* my workstation: 24GB Ram, 2x E5520, a) Raid 10 of of 4 4TB, 7.2krpm
devices b) Raid 1 of 2 m4 512GB SSDs. One of the latter was killed
during the test. Both showed regressions, but smaller.
* EC2 d2.8xlarge, 244 GB RAM, 24 x 2000 HDD, 64GB shared_buffers. I
tried scale 3000,8000,15000. Here sorting, without flushing, didn't
lead much to regressions.
I think generally the regressions were visible with a) noticeable shared
buffers, b) workload not fitting into shared buffers, c) significant
throughput, leading to high cache replacement ratios.
Another thing that's worthwhile to mention, while not surprising, is
that the benefits of this patch are massively smaller when WAL and data
are separated onto different disks. For workloads fitting into
shared_buffers I saw no performance difference - not particularly
surprising. I guess if you'd construct a case where the data, not WAL,
is the bottleneck that'd be different. Also worthwhile to mention that
the separate disks setups was noticeably faster.
The performance was still much more regular than before, i.e. no
more multi-second periods without any transactions happening.By now I think I know what's going on: Before the sorting portion of the
patch the write-loop in BufferSync() starts at the current clock hand,
by using StrategySyncStart(). But after the sorting that obviously
doesn't happen anymore - buffers are accessed in their sort order. By
starting at the current clock hand and moving on from there the
checkpointer basically makes it more less likely that victim buffers
need to be written either by the backends themselves or by
bgwriter. That means that the sorted checkpoint writes can, indirectly,
increase the number of unsorted writes by other processes :(I'm quite surprised at such a large effect on throughput, though.
Me too.
This explanation seems to suggest that if bgwriter/workders write are sorted
and/or coordinated with the checkpointer somehow then all would be well?
Well, you can't easily sort bgwriter/backend writes stemming from cache
replacement. Unless your access patterns are entirely sequential the
data in shared buffers will be laid out in a nearly entirely random
order. We could try sorting the data, but with any reasonable window,
for many workloads the likelihood of actually achieving much with that
seems low.
ISTM that this explanation could be checked by looking whether
bgwriter/workers writes are especially large compared to checkpointer writes
in those cases with reduced throughput? The data is in the log.
What do you mean with "large"? Numerous?
My benchmarking suggest that that effect is the larger, the shorter the
checkpoint timeout is.Hmmm. The shorter the timeout, the more likely the sorting NOT to be
effective
You mean, as evidenced by the results, or is that what you'd actually
expect?
2) Replace the boolean checkpoint_flush_to_disk GUC with a list guc that
later can contain multiple elements like checkpoint, bgwriter,
backends, ddl, bulk-writes. That seems better than adding GUCs for
these separately. Then make the flush locations in the patch
configurable using that.My 0,02€ on this point: I have not seen much of this style of guc elsewhere.
The only one I found while scanning the postgres file are *_path and
*_libraries. It seems to me that this would depart significantly from the
usual style, so one guc per case, or one shared guc but with only on/off,
would blend in more cleanly with the usual style.
Such a guc would allow one 'on' and 'off' setting, and either would
hopefully be the norm. That seems advantageous to me.
3) I think we should remove the sort timing from the checkpoint logging
before commit. It'll always be pretty short.I added it to show that it was really short, in response to concerns that my
approach of just sorting through indexes to reduce the memory needed instead
of copying the data to be sorted did not induce significant performance
issues. I prooved my point, but peer pressure made me switch to larger
memory anyway.
Grumble. I'm getting a bit tired about this topic. This wasn't even
remotely primarily about sorting speed, and you damn well know it.
I think it should be kept while the features are under testing. I do not
think that it harms in anyway.
That's why I said we should remove it *before commit*.
Greetings,
Andres Freund
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
To fix it, ITSM that it is enough to hold a "do not close lock" on the file
while a flush is in progress (a short time) that would prevent mdclose to do
its stuff.Could you expand a bit more on this? You're suggesting something like a
boolean in the vfd struct?
Basically yes, I'm suggesting a mutex in the vdf struct.
If that, how would you deal with FileClose() being called?
Just wait for the mutex, which would be held while flushes are accumulated
into the flush context and released after the flush is performed and the
fd is not necessary anymore for this purpose, which is expected to be
short (at worst between the wake & sleep of the checkpointer, and just one
file at a time).
I'm concious that the patch only addresses *checkpointer* writes, not those
from bgwrither or backends writes. I agree that these should need to be
addressed at some point as well, but given the time to get a patch through,
the more complex the slower (sort propositions are 10 years old), I think
this should be postponed for later.I think we need to have at least a PoC of all of the relevant
changes. We're doing these to fix significant latency and throughput
issues, and if the approach turns out not to be suitable for
e.g. bgwriter or backends, that might have influence over checkpointer's
design as well.
Hmmm. See below.
What I did not expect, and what confounded me for a long while, is that
for workloads where the hot data set does *NOT* fit into shared buffers,
sorting often led to be a noticeable reduction in throughput. Up to
30%.I did not see such behavior in the many tests I ran. Could you share more
precise details so that I can try to reproduce this performance regression?
(available memory, shared buffers, db size, ...).I generally found that I needed to disable autovacuum's analyze to get
anything even close to stable numbers. The issue in described in
http://archives.postgresql.org/message-id/20151031145303.GC6064%40alap3.anarazel.de
otherwise badly kicks in. I basically just set
autovacuum_analyze_threshold to INT_MAX/2147483647 to prevent that from occuring.I'll show actual numbers at some point yes. I tried three different systems:
* my laptop, 16 GB Ram, 840 EVO 1TB as storage. With 2GB
shared_buffers. Tried checkpoint timeouts from 60 to 300s.
Hmmm. This is quite short. I tend to do tests with much larger timeouts. I
would advise against a short timeout esp. in a high throughput system, the
whole point of the checkpointer is to accumulate as much changes as
possible.
I'll look into that.
This explanation seems to suggest that if bgwriter/workders write are sorted
and/or coordinated with the checkpointer somehow then all would be well?Well, you can't easily sort bgwriter/backend writes stemming from cache
replacement. Unless your access patterns are entirely sequential the
data in shared buffers will be laid out in a nearly entirely random
order. We could try sorting the data, but with any reasonable window,
for many workloads the likelihood of actually achieving much with that
seems low.
Maybe the sorting could be shared with others so that everybody uses the
same order?
That would suggest to have one global sorting of buffers, maybe maintained
by the checkpointer, which could be used by all processes that need to
scan the buffers (in file order), instead of scanning them in memory
order.
For this purpose, I think that the initial index-based sorting would
suffice. Could be resorted periodically with some delay maintained in a
guc, or when significant buffer changes have occured (read & writes).
ISTM that this explanation could be checked by looking whether
bgwriter/workers writes are especially large compared to checkpointer writes
in those cases with reduced throughput? The data is in the log.What do you mean with "large"? Numerous?
I mean the amount of buffers written by bgwriter/worker is greater than
what is written by the checkpointer. If all fits in shared buffers,
bgwriter/worker mostly do not need to write anything and the checkpointer
does all the writes.
The larger the memory needed, the more likely workers/bgwriter will have
to quick in and generate random I/Os because nothing sensible is currently
done, so this is consistent with your findings, although I'm surprised
that it would have a large effect on throughput, as already said.
Hmmm. The shorter the timeout, the more likely the sorting NOT to be
effectiveYou mean, as evidenced by the results, or is that what you'd actually
expect?
What I would expect...
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 2015-11-12 17:44:40 +0100, Fabien COELHO wrote:
To fix it, ITSM that it is enough to hold a "do not close lock" on the file
while a flush is in progress (a short time) that would prevent mdclose to do
its stuff.Could you expand a bit more on this? You're suggesting something like a
boolean in the vfd struct?Basically yes, I'm suggesting a mutex in the vdf struct.
I can't see that being ok. I mean what would that thing even do? VFD
isn't shared between processes, and if we get a smgr flush we have to
apply it, or risk breaking other things.
* my laptop, 16 GB Ram, 840 EVO 1TB as storage. With 2GB
shared_buffers. Tried checkpoint timeouts from 60 to 300s.Hmmm. This is quite short.
Indeed. I'd never do that in a production scenario myself. But
nonetheless it showcases a problem.
Well, you can't easily sort bgwriter/backend writes stemming from cache
replacement. Unless your access patterns are entirely sequential the
data in shared buffers will be laid out in a nearly entirely random
order. We could try sorting the data, but with any reasonable window,
for many workloads the likelihood of actually achieving much with that
seems low.Maybe the sorting could be shared with others so that everybody uses the
same order?That would suggest to have one global sorting of buffers, maybe maintained
by the checkpointer, which could be used by all processes that need to scan
the buffers (in file order), instead of scanning them in memory order.
Uh. Cache replacement is based on an approximated LRU, you can't just
remove that without serious regressions.
Hmmm. The shorter the timeout, the more likely the sorting NOT to be
effectiveYou mean, as evidenced by the results, or is that what you'd actually
expect?What I would expect...
I don't see why then? If you very quickly writes lots of data the OS
will continously flush dirty data to the disk, in which case sorting is
rather important?
Greetings,
Andres Freund
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Hello,
Basically yes, I'm suggesting a mutex in the vdf struct.
I can't see that being ok. I mean what would that thing even do? VFD
isn't shared between processes, and if we get a smgr flush we have to
apply it, or risk breaking other things.
Probably something is eluding my comprehension:-)
My basic assumption is that the fopen & fd is per process, so we just have
to deal with the one in the checkpointer process, so it is enough that the
checkpointer does not close the file while it is flushing things to it?
* my laptop, 16 GB Ram, 840 EVO 1TB as storage. With 2GB
shared_buffers. Tried checkpoint timeouts from 60 to 300s.Hmmm. This is quite short.
Indeed. I'd never do that in a production scenario myself. But
nonetheless it showcases a problem.
I would say that it would render sorting ineffective because all the
rewriting is done by bgwriter or workers, which does not totally explain
why the throughput would be worst than before, I would expect it to be as
bad as before...
Well, you can't easily sort bgwriter/backend writes stemming from cache
replacement. Unless your access patterns are entirely sequential the
data in shared buffers will be laid out in a nearly entirely random
order. We could try sorting the data, but with any reasonable window,
for many workloads the likelihood of actually achieving much with that
seems low.Maybe the sorting could be shared with others so that everybody uses the
same order?That would suggest to have one global sorting of buffers, maybe maintained
by the checkpointer, which could be used by all processes that need to scan
the buffers (in file order), instead of scanning them in memory order.Uh. Cache replacement is based on an approximated LRU, you can't just
remove that without serious regressions.
I understand that, but there is a balance to find. Generating random I/Os
is very bad for performance, so the decision process must combine LRU/LFU
heuristics with considering things in some order as well.
Hmmm. The shorter the timeout, the more likely the sorting NOT to be
effectiveYou mean, as evidenced by the results, or is that what you'd actually
expect?What I would expect...
I don't see why then? If you very quickly writes lots of data the OS
will continously flush dirty data to the disk, in which case sorting is
rather important?
What I have in mind is: the shorter the timeout the less neighboring
buffers will be touched, so the less nice sequential writes will be found
by sorting them, so the worst the positive impact on performance...
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Basically yes, I'm suggesting a mutex in the vdf struct.
I can't see that being ok. I mean what would that thing even do? VFD
isn't shared between processes, and if we get a smgr flush we have to
apply it, or risk breaking other things.Probably something is eluding my comprehension:-)
My basic assumption is that the fopen & fd is per process, so we just have to
deal with the one in the checkpointer process, so it is enough that the
checkpointer does not close the file while it is flushing things to it?
Hmmm...
Maybe I'm a little bit too optimistic here, because it seems that I'm
suggesting to create a dead lock if the checkpointer has both buffers to
flush in waiting and wishes to close the very same file that holds them.
So on wanting to close the file the checkpointer should rather flushes the
outstanding flushes in wait and then close the fd, which suggest some
global variable to hold flush context so that this can be done.
Hmmm.
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Wed, Nov 11, 2015 at 1:08 PM, Andres Freund <andres@anarazel.de> wrote:
On 2015-09-10 17:15:26 +0200, Fabien COELHO wrote:
Here is a v13, which is just a rebase after 1aba62ec.
3) I found that latency wasn't improved much for workloads that are
significantly bigger than shared buffers. The problem here is that
neither bgwriter nor the backends have, so far, done
sync_file_range() calls. That meant that the old problem of having
gigabytes of dirty data that periodically get flushed out, still
exists. Having these do flushes mostly attacks that problem.Benchmarking revealed that for workloads where the hot data set mostly
fits into shared buffers flushing and sorting is anywhere from a small
to a massive improvement, both in throughput and latency. Even without
the patch from 2), although fixing that improves things furhter.What I did not expect, and what confounded me for a long while, is that
for workloads where the hot data set does *NOT* fit into shared buffers,
sorting often led to be a noticeable reduction in throughput. Up to
30%. The performance was still much more regular than before, i.e. no
more multi-second periods without any transactions happening.By now I think I know what's going on: Before the sorting portion of the
patch the write-loop in BufferSync() starts at the current clock hand,
by using StrategySyncStart(). But after the sorting that obviously
doesn't happen anymore - buffers are accessed in their sort order. By
starting at the current clock hand and moving on from there the
checkpointer basically makes it more less likely that victim buffers
need to be written either by the backends themselves or by
bgwriter. That means that the sorted checkpoint writes can, indirectly,
increase the number of unsorted writes by other processes :(
That sounds to be a tricky problem. I think the way to improve the current
situation is to change buffer allocation algorithm such that instead of
backend issuing the write for dirty buffer, it will just continue to find
next
free buffer when it finds that selected buffer is dirty and if it could not
find the non-dirty buffer for certain number of attempts, it will signal
bgwriter
to write-out some buffers. Now the writing algorithm of bgwriter has to
be such that it picks the buffers in chunks from checkpoint-list, sort them
and then write them. Checkpoint also uses the same checkpoint-list to flush
the dirty buffers. This will ensure that the writes will always be
sorted-writes
irrespective of which process does the writes. There could be multiple ways
to form this checkpoint-list and one of the way could be MarkBufferDirty()
adds it to such a list. I think following such a mechanism could solve the
problem of unsorted writes in the system, but it arises a question, what
kind
of latency such a mechanism could introduce for a backend which
signals bgwriter after not finding a non-dirty buffer for certain number of
attempts, I think if we sense this could be a problematic case, then we
can make both bgwriter and checkpoint to always start from next victim
buffer and then traverse the checkpoint-list.
My benchmarking suggest that that effect is the larger, the shorter the
checkpoint timeout is. That seems to intuitively make sense, give the
above explanation attempt. If the checkpoint takes longer the clock hand
will almost certainly soon overtake checkpoints 'implicit' hand.I'm not sure if we can really do anything about this problem. While I'm
pretty jet lagged, I still spent a fair amount of time thinking about
it. Seems to suggest that we need to bring back the setting to
enable/disable sorting :(What I think needs to happen next with the patch is:
1) Hoist up the FileFlushContext stuff into the smgr layer. Carefully
handling the issue of smgr invalidations.
2) Replace the boolean checkpoint_flush_to_disk GUC with a list guc that
later can contain multiple elements like checkpoint, bgwriter,
backends, ddl, bulk-writes. That seems better than adding GUCs for
these separately. Then make the flush locations in the patch
configurable using that.
3) I think we should remove the sort timing from the checkpoint logging
before commit. It'll always be pretty short.
It seems for now you have left out the windows specific implementation
in pg_flush_data().
With Regards,
Amit Kapila.
EnterpriseDB: http://www.enterprisedb.com
Hmmm...
Maybe I'm a little bit too optimistic here, because it seems that I'm
suggesting to create a dead lock if the checkpointer has both buffers to
flush in waiting and wishes to close the very same file that holds them.So on wanting to close the file the checkpointer should rather flushes the
outstanding flushes in wait and then close the fd, which suggest some global
variable to hold flush context so that this can be done.Hmmm.
On third (fourth, fifth:-) thoughts:
The vfd (virtual file descriptor?) structure in the checkpointer could
keep a pointer to the current flush if it concerns this fd, so that if it
decides to close if while there is a write in progress (I'm still baffled
at why and when the checkpointer process would take such a decision, maybe
while responding to some signals, because it seems that there is no such
event in the checkpointer loop itself...) then on close the process could
flush before close, or just close which probably would induce flushing,
but at least cleanup the structure so that the the closed fd would not be
flushed after being closed and result in an error.
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Hi,
I'm planning to do some thorough benchmarking of the patches proposed in
this thread, on various types of hardware (10k SAS drives and SSDs). But
is that actually needed? I see Andres did some testing, as he posted
summary of the results on 11/12, but I don't see any actual results or
even info about what benchmarks were done (pgbench?).
If yes, do we only want to compare 0001-ckpt-14-andres.patch against
master, or do we need to test one of the previous Fabien's patches?
regards
--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Hello Tomas,
I'm planning to do some thorough benchmarking of the patches proposed in this
thread, on various types of hardware (10k SAS drives and SSDs). But is that
actually needed? I see Andres did some testing, as he posted summary of the
results on 11/12, but I don't see any actual results or even info about what
benchmarks were done (pgbench?).If yes, do we only want to compare 0001-ckpt-14-andres.patch against master,
or do we need to test one of the previous Fabien's patches?
My 0.02�,
Although I disagree with some aspects of Andres patch, I'm not a committer
and I'm tired of arguing. I'm just planing to do minor changes to Andres
version to fix a potential issue if the file is closed which flushing is
in progress, but that will not change the overall shape of it.
So testing on Andres version seems relevant to me.
For SSD the performance impact should be limited. For disk it should be
significant if there is no big cache in front of it. There were some
concerns raised for some loads in the thread (shared memory smaller than
needed I think?), if you can include such cases that would be great. My
guess is that it should be not very beneficial in this case because the
writing is mostly done by bgwriter & worker in this case, and these are
still random.
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Thu, Dec 17, 2015 at 4:27 AM, Fabien COELHO <coelho@cri.ensmp.fr> wrote:
Hello Tomas,
I'm planning to do some thorough benchmarking of the patches proposed in
this thread, on various types of hardware (10k SAS drives and SSDs). But is
that actually needed? I see Andres did some testing, as he posted summary of
the results on 11/12, but I don't see any actual results or even info about
what benchmarks were done (pgbench?).If yes, do we only want to compare 0001-ckpt-14-andres.patch against
master, or do we need to test one of the previous Fabien's patches?My 0.02€,
Although I disagree with some aspects of Andres patch, I'm not a committer
and I'm tired of arguing. I'm just planing to do minor changes to Andres
version to fix a potential issue if the file is closed which flushing is in
progress, but that will not change the overall shape of it.So testing on Andres version seems relevant to me.
For SSD the performance impact should be limited. For disk it should be
significant if there is no big cache in front of it. There were some
concerns raised for some loads in the thread (shared memory smaller than
needed I think?), if you can include such cases that would be great. My
guess is that it should be not very beneficial in this case because the
writing is mostly done by bgwriter & worker in this case, and these are
still random.
As there are still plans to move on regarding tests (and because this
patch makes a difference), this is moved to next CF.
--
Michael
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Hi,
On 12/16/2015 08:27 PM, Fabien COELHO wrote:
Hello Tomas,
I'm planning to do some thorough benchmarking of the patches proposed
in this thread, on various types of hardware (10k SAS drives and
SSDs). But is that actually needed? I see Andres did some testing, as
he posted summary of the results on 11/12, but I don't see any actual
results or even info about what benchmarks were done (pgbench?).If yes, do we only want to compare 0001-ckpt-14-andres.patch against
master, or do we need to test one of the previous Fabien's patches?My 0.02�,
Although I disagree with some aspects of Andres patch, I'm not a
committer and I'm tired of arguing. I'm just planing to do minor changes
to Andres version to fix a potential issue if the file is closed which
flushing is in progress, but that will not change the overall shape of it.So testing on Andres version seems relevant to me.
The patch no longer applies to master. Can someone rebase it?
regards
--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 2016-01-06 21:01:47 +0100, Tomas Vondra wrote:
Although I disagree with some aspects of Andres patch, I'm not a
committer and I'm tired of arguing. I'm just planing to do minor changes
to Andres version to fix a potential issue if the file is closed which
flushing is in progress, but that will not change the overall shape of it.
Are you working on that aspect?
So testing on Andres version seems relevant to me.
The patch no longer applies to master. Can someone rebase it?
I'm working on an updated version, trying to mitigate the performance
regressions I observed.
Andres
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
<Ooops, wrong from address, resent, sorry for the noise>
Hello Andres,
Although I disagree with some aspects of Andres patch, I'm not a
committer and I'm tired of arguing. I'm just planing to do minor changes
to Andres version to fix a potential issue if the file is closed which
flushing is in progress, but that will not change the overall shape of
it.Are you working on that aspect?
I read your patch and I know what I want to try to have a small and simple
fix. I must admit that I have not really understood in which condition the
checkpointer would decide to close a file, but that does not mean that the
potential issue should not be addressed.
Also, I gave some thoughts about what should be done for bgwriter random IOs.
The idea is to implement some per-file sorting there and then do some LRU/LFU
combing. It would not interact much with the checkpointer, so for me the two
issues should be kept separate and this should not preclude changing the
checkpointer, esp. given the significant performance benefit of the patch.
However, all this is still in my stack of things to do, and I had not much
time in the Fall for that. I may have more time in the coming weeks. I'm fine
if things are updated and performance figures are collected in between, I'll
take it from where it is when I have time, if something remains to be done.
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Import Notes
Reply to msg id not found: alpine.DEB.2.10.1601071101490.5278@sto
On 2016-01-07 11:27:13 +0100, Fabien COELHO wrote:
I read your patch and I know what I want to try to have a small and simple
fix. I must admit that I have not really understood in which condition the
checkpointer would decide to close a file, but that does not mean that the
potential issue should not be addressed.
There's a trivial example: Consider three tablespaces and
max_files_per_process = 2. The balancing can easily cause three files
being flushed at the same time.
But more importantly: You designed the API to be generic because you
wanted it to be usable for other purposes as well. And for that it
certainly needs to deal with that.
Also, I gave some thoughts about what should be done for bgwriter random
IOs. The idea is to implement some per-file sorting there and then do some
LRU/LFU combing. It would not interact much with the checkpointer, so for me
the two issues should be kept separate and this should not preclude changing
the checkpointer, esp. given the significant performance benefit of the
patch.
Well, the problem is that the patch significantly regresses some cases
right now. So keeping them separate isn't particularly feasible.
Greetings,
Andres Freund
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Hello,
I read your patch and I know what I want to try to have a small and simple
fix. I must admit that I have not really understood in which condition the
checkpointer would decide to close a file, but that does not mean that the
potential issue should not be addressed.There's a trivial example: Consider three tablespaces and
max_files_per_process = 2. The balancing can easily cause three files
being flushed at the same time.
Indeed. Thanks for this explanation!
But more importantly: You designed the API to be generic because you
wanted it to be usable for other purposes as well. And for that it
certainly needs to deal with that.
Yes, I'm planning to try to do the minimum possible damage to the current
API to fix the issue.
Also, I gave some thoughts about what should be done for bgwriter random
IOs. The idea is to implement some per-file sorting there and then do some
LRU/LFU combing. It would not interact much with the checkpointer, so for me
the two issues should be kept separate and this should not preclude changing
the checkpointer, esp. given the significant performance benefit of the
patch.Well, the problem is that the patch significantly regresses some cases
right now. So keeping them separate isn't particularly feasible.
I have not seen significant regressions on my many test runs. In
particular, I would not consider that having a tps deep in cases where
postgresql is doing 0 tps most of the time anyway (ie pg is offline)
because of random IO issues should be blocker.
As I understood it, the regressions occur when the checkpointer is less
used, i.e. bgwriter is doing most of the writes, but this does not change
much whether the checkpointer sorts buffers or not, and the overall
behavior of pg is very bad anyway in these cases.
Also I think that coupling the two issues is a recipee for never having
anything done in the end and keep the current awful behavior:-(
The solution on the bgwriter front is somehow similar to the checkpointer,
but from a code point of view there is minimum interaction, so I would
really separate them, esp. as the bgwriter part will require extensive
testing and discussions as well.
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 2016-01-07 12:50:07 +0100, Fabien COELHO wrote:
But more importantly: You designed the API to be generic because you
wanted it to be usable for other purposes as well. And for that it
certainly needs to deal with that.Yes, I'm planning to try to do the minimum possible damage to the current
API to fix the issue.
What's your thought there? Afaics it's infeasible to do the flushing tat
the fd.c level.
Andres
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Yes, I'm planning to try to do the minimum possible damage to the current
API to fix the issue.What's your thought there? Afaics it's infeasible to do the flushing tat
the fd.c level.
I thought of adding a pointer to the current flush structure at the vfd
level, so that on closing a file with a flush in progress the flush can be
done and the structure properly cleaned up, hence later the checkpointer
would see a clean thing and be able to skip it instead of generating
flushes on a closed file or on a different file...
Maybe I'm missing something, but that is the plan I had in mind.
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 2016-01-07 13:07:33 +0100, Fabien COELHO wrote:
Yes, I'm planning to try to do the minimum possible damage to the current
API to fix the issue.What's your thought there? Afaics it's infeasible to do the flushing tat
the fd.c level.I thought of adding a pointer to the current flush structure at the vfd
level, so that on closing a file with a flush in progress the flush can be
done and the structure properly cleaned up, hence later the checkpointer
would see a clean thing and be able to skip it instead of generating flushes
on a closed file or on a different file...Maybe I'm missing something, but that is the plan I had in mind.
That might work, although it'd not be pretty (not fatally so
though). But I'm inclined to go a different way: I think it's a mistake
to do flusing based on a single file. It seems better to track a fixed
number of outstanding 'block flushes', independent of the file. Whenever
the number of outstanding blocks is exceeded, sort that list, and flush
all outstanding flush requests after merging neighbouring flushes. Imo
that means that we'd better track writes on a relfilenode + block number
level.
Andres
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Hello Andres,
I thought of adding a pointer to the current flush structure at the vfd
level, so that on closing a file with a flush in progress the flush can be
done and the structure properly cleaned up, hence later the checkpointer
would see a clean thing and be able to skip it instead of generating flushes
on a closed file or on a different file...Maybe I'm missing something, but that is the plan I had in mind.
That might work, although it'd not be pretty (not fatally so
though).
Alas, any solution has to communicate somehow between the API levels, so
it cannot be "pretty", although we should avoid the worse.
But I'm inclined to go a different way: I think it's a mistake to do
flusing based on a single file. It seems better to track a fixed number
of outstanding 'block flushes', independent of the file. Whenever the
number of outstanding blocks is exceeded, sort that list, and flush all
outstanding flush requests after merging neighbouring flushes.
Hmmm. I'm not sure I understand your strategy.
I do not think that flushing without a prior sorting would be effective,
because there is no clear reason why buffers written together would then
be next to the other and thus give sequential write benefits, we would
just get flushed random IO, I tested that and it worked badly.
One of the point of aggregating flushes is that the range flush call cost
is significant, as shown by preliminary tests I did, probably up in the
thread, so it makes sense to limit this cost, hence the aggregation. These
removed some performation regression I had in some cases.
Also, the granularity of the buffer flush call is a file + offset + size,
so necessarily it should be done this way (i.e. per file).
Once buffers are sorted per file and offset within file, then written
buffers are as close as possible one after the other, the merging is very
easy to compute (it is done on the fly, no need to keep the list of
buffers for instance), it is optimally effective, and when the
checkpointed file changes then we will never go back to it before the next
checkpoint, so there is no reason not to flush right then.
So basically I do not see a clear positive advantage to your suggestion,
especially when taking into consideration the scheduling process of the
scheduler:
In effect the checkpointer already works with little bursts of activity
between sleep phases, so that it writes buffers a few at a time, so it may
already work more or less as you expect, but not for the same reason.
The closest stategy that I experimented which is maybe close to your
suggestion was to manage a minimum number of buffers to write when awaken
and to change the sleep delay in between, but I had no clear way to choose
values and the experiments I did did not show significant performance
impact by varying these parameters, so I kept that out. If you find a
magic number of buffer which results in consistant better performance,
fine with me, but this is independent with aggregating before or after.
Imo that means that we'd better track writes on a relfilenode + block
number level.
I do not think that it is a better option. Moreover, the current approach
has been proven to be very effective on hundreds of runs, so redoing it
differently for the sake of it does not look like good resource
allocation.
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 2016-01-07 16:05:32 +0100, Fabien COELHO wrote:
But I'm inclined to go a different way: I think it's a mistake to do
flusing based on a single file. It seems better to track a fixed number of
outstanding 'block flushes', independent of the file. Whenever the number
of outstanding blocks is exceeded, sort that list, and flush all
outstanding flush requests after merging neighbouring flushes.Hmmm. I'm not sure I understand your strategy.
I do not think that flushing without a prior sorting would be effective,
because there is no clear reason why buffers written together would then be
next to the other and thus give sequential write benefits, we would just get
flushed random IO, I tested that and it worked badly.
Oh, I was thinking of sorting & merging these outstanding flushes. Sorry
for not making that clear.
One of the point of aggregating flushes is that the range flush call cost
is significant, as shown by preliminary tests I did, probably up in the
thread, so it makes sense to limit this cost, hence the aggregation. These
removed some performation regression I had in some cases.
FWIW, my tests show that flushing for clean ranges is pretty cheap.
Also, the granularity of the buffer flush call is a file + offset + size, so
necessarily it should be done this way (i.e. per file).
What syscalls we issue, and at what level we track outstanding flushes,
doesn't have to be the same.
Once buffers are sorted per file and offset within file, then written
buffers are as close as possible one after the other, the merging is very
easy to compute (it is done on the fly, no need to keep the list of buffers
for instance), it is optimally effective, and when the checkpointed file
changes then we will never go back to it before the next checkpoint, so
there is no reason not to flush right then.
Well, that's true if there's only one tablespace, but e.g. not the case
with two tablespaces of about the same number of dirty buffers.
So basically I do not see a clear positive advantage to your suggestion,
especially when taking into consideration the scheduling process of the
scheduler:
I don't think it makes a big difference for the checkpointer alone, but
it makes the interface much more suitable for other processes, e.g. the
bgwriter, and normal backends.
Imo that means that we'd better track writes on a relfilenode + block
number level.I do not think that it is a better option. Moreover, the current approach
has been proven to be very effective on hundreds of runs, so redoing it
differently for the sake of it does not look like good resource allocation.
For a subset of workloads, yes.
Greetings,
Andres Freund
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Hello Andres,
One of the point of aggregating flushes is that the range flush call cost
is significant, as shown by preliminary tests I did, probably up in the
thread, so it makes sense to limit this cost, hence the aggregation. These
removed some performation regression I had in some cases.FWIW, my tests show that flushing for clean ranges is pretty cheap.
Yes, I agree that it is quite cheap, but I had a few % tps regressions
in some cases without aggregating, and aggregating was enough to avoid
these small regressions.
Also, the granularity of the buffer flush call is a file + offset + size, so
necessarily it should be done this way (i.e. per file).What syscalls we issue, and at what level we track outstanding flushes,
doesn't have to be the same.
Sure. But the current version is simple, efficient and proven by many
runs, so there should be a very strong argument to justify a significant
benefit to change the approach, and I see no such thing in your arguments.
For me the current approach is optimal for the checkpointer, because it
takes advantage of all available information to perform a better job.
Once buffers are sorted per file and offset within file, then written
buffers are as close as possible one after the other, the merging is very
easy to compute (it is done on the fly, no need to keep the list of buffers
for instance), it is optimally effective, and when the checkpointed file
changes then we will never go back to it before the next checkpoint, so
there is no reason not to flush right then.Well, that's true if there's only one tablespace, but e.g. not the case
with two tablespaces of about the same number of dirty buffers.
ISTM that in the version of the patch I sent there was one flushing
structure per tablespace each doing its own flushing on its files, so it
should work the same, only the writing intensity is devided by the number
of tablespace? Or am I missing something?
So basically I do not see a clear positive advantage to your suggestion,
especially when taking into consideration the scheduling process of the
scheduler:I don't think it makes a big difference for the checkpointer alone, but
it makes the interface much more suitable for other processes, e.g. the
bgwriter, and normal backends.
Hmmm.
ISTM that the requirement are not exactly the same for the bgwriter and
backends vs the checkpointer. The checkpointer has the advantage of being
able to plan its IOs on the long term (volume & time is known...) and the
implementation takes the full benefit of this planing by sorting and
scheduling and flushing buffers so as to generate as much sequential
writes as possible.
The bgwriter and backends have a much shorter vision (a few seconds, or
juste one query being process), so the solution will be less efficient and
probably more messy on the coding side. This is life. I do not see why not
to take the benefit of a full planing in the checkpointer just because
other processes cannot do the same, especially as under plenty of loads
the checkpointer does most of the writing so is the limiting factor.
So I do not buy your suggestion for the checkpointer. Maybe it will be the
way to go for bgwriter and backends, then fine for them.
Imo that means that we'd better track writes on a relfilenode + block
number level.I do not think that it is a better option. Moreover, the current approach
has been proven to be very effective on hundreds of runs, so redoing it
differently for the sake of it does not look like good resource allocation.For a subset of workloads, yes.
Hmmm. What I understood is that the workloads that have some performance
regressions (regressions that I have *not* seen in the many tests I ran)
are not due to checkpointer IOs, but rather in settings where most of the
writes is done by backends or bgwriter.
I do not see the point of rewriting the checkpointer for them, although
obviously I agree that something has to be done also for the other
processes.
Maybe if all the writes (bgwriter and checkpointer) where performed by the
same process then some dynamic mixing and sorting and aggregating would
make sense, but this is currently not the case, and would probably have
quite limited effect.
Basically I do not understand how changing the flushing organisation as
you suggest would improve the checkpointer performance significantly, for
me it should only degrade the performance compared to the current version,
as far as the checkpointer is concerned.
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 2016-01-07 21:08:10 +0100, Fabien COELHO wrote:
Hmmm. What I understood is that the workloads that have some performance
regressions (regressions that I have *not* seen in the many tests I ran) are
not due to checkpointer IOs, but rather in settings where most of the writes
is done by backends or bgwriter.
As far as I can see you've not run many tests where the hot/warm data
set is larger than memory (the full machine's memory, not
shared_buffers). That quite drastically alters the performance
characteristics here, because you suddenly have lots of synchronous read
IO thrown into the mix.
Whether it's bgwriter or not I've not fully been able to establish, but
it's a working theory.
I do not see the point of rewriting the checkpointer for them, although
obviously I agree that something has to be done also for the other
processes.
Rewriting the checkpointer and fixing the flush interface in a more
generic way aren't the same thing at all.
Greetings,
Andres Freund
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Hello Andres,
Hmmm. What I understood is that the workloads that have some performance
regressions (regressions that I have *not* seen in the many tests I ran) are
not due to checkpointer IOs, but rather in settings where most of the writes
is done by backends or bgwriter.As far as I can see you've not run many tests where the hot/warm data
set is larger than memory (the full machine's memory, not
shared_buffers).
Indeed, I think I ran some, but not many with such characteristics.
That quite drastically alters the performance characteristics here,
because you suddenly have lots of synchronous read IO thrown into the
mix.
If I understand this point correctly...
I would expect the overall performance to be abysmal in such a situation
because you get only intermixed *random* read and writes: As you point
out, synchroneous *random* reads (very slow), but on the write side the
IOs are mostly random as well on the checkpointer side because there is
not much to aggregate to get sequential writes.
Now why would that degrade performance significantly? For me it should
render the sorting/flushing less and less effective, and it would go back
to the previous performance levels...
Or maybe it only the flushing itself which degrades performance, as you
point out, because then you have some synchronous (synced) writes as well
as read, as opposed to just the reads before without the patch.
If this is indeed the issue, then the solution to avoid the regression is
*not* to flush so that the OS IO scheduler is less constrained in its job,
and can be slightly more effective (well, we talking of abysmal random IO
disk performance here, so effective would be between slightly more or less
very very very bad).
Maybe a trick could be not to aggregate and flush when buffers in the same
file are too much apart anyway, for instance, based on some threshold?
This can be implemented locally when deciding to merge buffer flushes or
not, and whether to flush or not, so it would fit the current code quite
simply.
Now my understanding of the sync_file_range call is that it is an advice
to flush the stuff, but it is still asynchronous in nature, so whether it
would impact performance that badly depends on the OS IO scheduler. Also,
I would like to check whether, under the "regressed performance" (in tps
term that you observed), pg is more or less responsive. It could be that
the average performance is better but pg is offline longer on fsync. In
which case, I would consider it better to have lower tps in such cases
*if* pg responsiveness is significantly improved.
Would you have these measures for the regression runs you observed?
Whether it's bgwriter or not I've not fully been able to establish, but
it's a working theory.
Ok, that is something to check for confirmation or infirmation.
Given the above discussion, I think my suggestion may be wrong: as the tps
is low because of random read/write accesses then not many buffers are
modified (so the bgwriter/backends won't need to make space), the
checkpointer does not have much to write (good), *but* all of it is random
(bad).
I do not see the point of rewriting the checkpointer for them, although
obviously I agree that something has to be done also for the other
processes.Rewriting the checkpointer and fixing the flush interface in a more
generic way aren't the same thing at all.
Hmmm, probably I misunderstood something in the discussion. It started
with an implementation strategy, but it derived to discussing a
performance regression. I aggree that these are two different subjects.
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Thu, Jan 7, 2016 at 4:21 PM, Andres Freund <andres@anarazel.de> wrote:
On 2016-01-07 11:27:13 +0100, Fabien COELHO wrote:
I read your patch and I know what I want to try to have a small and
simple
fix. I must admit that I have not really understood in which condition
the
checkpointer would decide to close a file, but that does not mean that
the
potential issue should not be addressed.
There's a trivial example: Consider three tablespaces and
max_files_per_process = 2. The balancing can easily cause three files
being flushed at the same time.
Won't the same thing can occur without patch in mdsync() and can't
we handle it in same way? In particular, I am referring to below code:
mdsync()
{
..
/*
* It is possible that the relation has been dropped or
* truncated since the fsync request was entered.
* Therefore, allow ENOENT, but only if we didn't fail
* already on this file. This applies both for
* _mdfd_getseg() and for FileSync, since fd.c might have
* closed the file behind our back.
*
* XXX is there any point in allowing more than one retry?
* Don't see one at the moment, but easy to change the
* test here if so.
*/
if (!FILE_POSSIBLY_DELETED(errno) ||
failures > 0)
ereport(ERROR,
(errcode_for_file_access(),
errmsg("could not fsync file \"%s\": %m",
path)));
else
ereport(DEBUG1,
(errcode_for_file_access(),
errmsg("could not fsync file \"%s\" but retrying: %m",
path)));
}
With Regards,
Amit Kapila.
EnterpriseDB: http://www.enterprisedb.com
On 2016-01-09 18:04:39 +0530, Amit Kapila wrote:
On Thu, Jan 7, 2016 at 4:21 PM, Andres Freund <andres@anarazel.de> wrote:
On 2016-01-07 11:27:13 +0100, Fabien COELHO wrote:
I read your patch and I know what I want to try to have a small and
simple
fix. I must admit that I have not really understood in which condition
the
checkpointer would decide to close a file, but that does not mean that
the
potential issue should not be addressed.
There's a trivial example: Consider three tablespaces and
max_files_per_process = 2. The balancing can easily cause three files
being flushed at the same time.Won't the same thing can occur without patch in mdsync() and can't
we handle it in same way? In particular, I am referring to below code:
I don't see how that's corresponding - the problem is that current
proposed infrastructure keeps a kernel level (or fd.c in my versio) fd
open in it's 'pending flushes' struct. But since that isn't associated
with fd.c opening/closing files that fd isn't very meaningful.
mdsync()
That seems to address different issues.
Greetings,
Andres Freund
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Sat, Jan 9, 2016 at 6:08 PM, Andres Freund <andres@anarazel.de> wrote:
On 2016-01-09 18:04:39 +0530, Amit Kapila wrote:
On Thu, Jan 7, 2016 at 4:21 PM, Andres Freund <andres@anarazel.de>
wrote:
On 2016-01-07 11:27:13 +0100, Fabien COELHO wrote:
I read your patch and I know what I want to try to have a small and
simple
fix. I must admit that I have not really understood in which
condition
the
checkpointer would decide to close a file, but that does not mean
that
the
potential issue should not be addressed.
There's a trivial example: Consider three tablespaces and
max_files_per_process = 2. The balancing can easily cause three files
being flushed at the same time.Won't the same thing can occur without patch in mdsync() and can't
we handle it in same way? In particular, I am referring to below code:I don't see how that's corresponding - the problem is that current
proposed infrastructure keeps a kernel level (or fd.c in my versio) fd
open in it's 'pending flushes' struct. But since that isn't associated
with fd.c opening/closing files that fd isn't very meaningful.
Okay, but I think that is the reason why you are worried that it is possible
to issue sync_file_range() on a closed file, is that right or am I missing
something?
With Regards,
Amit Kapila.
EnterpriseDB: http://www.enterprisedb.com
On 2016-01-09 18:24:01 +0530, Amit Kapila wrote:
Okay, but I think that is the reason why you are worried that it is possible
to issue sync_file_range() on a closed file, is that right or am I missing
something?
That's one potential issue. You can also fsync a different file, try to
print an error message containing an unallocated filename (that's how I
noticed the issue in the first place)...
I don't think it's going to be acceptable to issue operations on more or
less random fds, even if that operation is hopefully harmless.
Greetings,
Andres Freund
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Sat, Jan 9, 2016 at 6:26 PM, Andres Freund <andres@anarazel.de> wrote:
On 2016-01-09 18:24:01 +0530, Amit Kapila wrote:
Okay, but I think that is the reason why you are worried that it is
possible
to issue sync_file_range() on a closed file, is that right or am I
missing
something?
That's one potential issue. You can also fsync a different file, try to
print an error message containing an unallocated filename (that's how I
noticed the issue in the first place)...I don't think it's going to be acceptable to issue operations on more or
less random fds, even if that operation is hopefully harmless.
Right that won't be acceptable, however I think with your latest
proposal [1]"It seems better to track a fixed number of outstanding 'block flushes', independent of the file. Whenever the number of outstanding blocks is exceeded, sort that list, and flush all outstanding flush requests after merging neighbouring flushes.", we might not need to solve this problem or do we still
need to address it. I think that idea will help to mitigate the problem of
backend and bgwriter writes as well. In that, can't we do it with the
help of existing infrastructure of *pendingOpsTable* and
*CheckpointerShmem->requests[]*, as already the flush requests are
remembered in those structures, we can use those to apply your idea
to issue flush requests.
[1]: "It seems better to track a fixed number of outstanding 'block flushes', independent of the file. Whenever the number of outstanding blocks is exceeded, sort that list, and flush all outstanding flush requests after merging neighbouring flushes."
"It seems better to track a fixed
number of outstanding 'block flushes', independent of the file. Whenever
the number of outstanding blocks is exceeded, sort that list, and flush
all outstanding flush requests after merging neighbouring flushes."
With Regards,
Amit Kapila.
EnterpriseDB: http://www.enterprisedb.com
On 2016-01-09 19:05:54 +0530, Amit Kapila wrote:
Right that won't be acceptable, however I think with your latest
proposal [1]
Sure, that'd address that problem.
[...] think that idea will help to mitigate the problem of backend and
bgwriter writes as well. In that, can't we do it with the help of
existing infrastructure of *pendingOpsTable* and
*CheckpointerShmem->requests[]*, as already the flush requests are
remembered in those structures, we can use those to apply your idea to
issue flush requests.
Hm, that might be possible. But that might have some bigger implications
- we currently can issue thousands of flush requests a second, without
much chance of merging. I'm not sure it's a good idea to overlay that
into the lower frequency pendingOpsTable. Backends having to issue
fsyncs because the pending fsync queue is full is darn expensive. In
contrast to that a 'flush hint' request getting lost doesn't cost that
much.
Greetings,
Andres Freund
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 2016-01-07 21:17:32 +0100, Andres Freund wrote:
On 2016-01-07 21:08:10 +0100, Fabien COELHO wrote:
Hmmm. What I understood is that the workloads that have some performance
regressions (regressions that I have *not* seen in the many tests I ran) are
not due to checkpointer IOs, but rather in settings where most of the writes
is done by backends or bgwriter.As far as I can see you've not run many tests where the hot/warm data
set is larger than memory (the full machine's memory, not
shared_buffers). That quite drastically alters the performance
characteristics here, because you suddenly have lots of synchronous read
IO thrown into the mix.Whether it's bgwriter or not I've not fully been able to establish, but
it's a working theory.
Hm. New theory: The current flush interface does the flushing inside
FlushBuffer()->smgrwrite()->mdwrite()->FileWrite()->FlushContextSchedule(). The
problem with that is that at that point we (need to) hold a content lock
on the buffer!
Especially on a system that's bottlenecked on IO that means we'll
frequently hold content locks for a noticeable amount of time, while
flushing blocks, without any need to.
Even if that's not the reason for the slowdowns I observed, I think this
fact gives further credence to the current "pending flushes" tracking
residing on the wrong level.
Andres
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Hello Andres,
Hm. New theory: The current flush interface does the flushing inside
FlushBuffer()->smgrwrite()->mdwrite()->FileWrite()->FlushContextSchedule(). The
problem with that is that at that point we (need to) hold a content lock
on the buffer!
You are worrying that FlushBuffer is holding a lock on a buffer and the
"sync_file_range" call occurs is issued at that moment.
Although I agree that it is not that good, I would be surprise if that was
the explanation for a performance regression, because the sync_file_range
with the chosen parameters is an async call, it "advises" the OS to send
the file, but it does not wait for it to be completed.
Moreover, for this issue to have a significant impact, it would require
that another backend just happen to need this very buffer, but ISTM that
the performance regression you are arguing about is on random IO bound
performance, that is a few 100 tps in the best case, for very large bases,
so a lot of buffers, so the probability of such a collision is very small,
so it would not explain a significant regression.
Especially on a system that's bottlenecked on IO that means we'll
frequently hold content locks for a noticeable amount of time, while
flushing blocks, without any need to.
I'm not that sure it is really noticeable, because sync_file_range does
not wait for completion.
Even if that's not the reason for the slowdowns I observed, I think this
fact gives further credence to the current "pending flushes" tracking
residing on the wrong level.
ISTM that I put the tracking at the level where is the information is
available without having to recompute it several times, as the flush needs
to know the fd and offset. Doing it differently would mean more code and
translating buffer to file/offset several times, I think.
Also, maybe you could answer a question I had about the performance
regression you observed, I could not find the post where you gave the
detailed information about it, so that I could try reproducing it: what
are the exact settings and conditions (shared_buffers, pgbench scaling,
host memory, ...), what is the observed regression (tps? other?), and what
is the responsiveness of the database under the regression (eg % of
seconds with 0 tps for instance, or something like that).
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Sat, Jan 9, 2016 at 7:10 PM, Andres Freund <andres@anarazel.de> wrote:
On 2016-01-09 19:05:54 +0530, Amit Kapila wrote:
Right that won't be acceptable, however I think with your latest
proposal [1]Sure, that'd address that problem.
[...] think that idea will help to mitigate the problem of backend and
bgwriter writes as well. In that, can't we do it with the help of
existing infrastructure of *pendingOpsTable* and
*CheckpointerShmem->requests[]*, as already the flush requests are
remembered in those structures, we can use those to apply your idea to
issue flush requests.Hm, that might be possible. But that might have some bigger implications
- we currently can issue thousands of flush requests a second, without
much chance of merging. I'm not sure it's a good idea to overlay that
into the lower frequency pendingOpsTable.
In that case, we can have unified structure to remember flush requests
rather than backend and bgwriter noting that information in
CheckpointerShmem and checkpointer in pendingOpsTable. I understand
there are some benefits of having pendingOpsTable, but having a
common structure seems to be more beneficial and in particular
because it can be used for the purpose of flush hints.
Now, I am sure we can invent a new way of tracking the flush
requests for flush hints, but I think we might want to consider why
can't we have one unified way of tracking the flush requests which
can be used both for *flush* and *flush hints*.
Backends having to issue
fsyncs because the pending fsync queue is full is darn expensive. In
contrast to that a 'flush hint' request getting lost doesn't cost that
much.
In general, I think the cases where backends have to do flush should
be less as the size of fsync queue is NBuffers and we take care of
handling duplicate fsync requests for the same buffer.
With Regards,
Amit Kapila.
EnterpriseDB: http://www.enterprisedb.com
On 2016-01-09 16:49:56 +0100, Fabien COELHO wrote:
Hello Andres,
Hm. New theory: The current flush interface does the flushing inside
FlushBuffer()->smgrwrite()->mdwrite()->FileWrite()->FlushContextSchedule(). The
problem with that is that at that point we (need to) hold a content lock
on the buffer!You are worrying that FlushBuffer is holding a lock on a buffer and the
"sync_file_range" call occurs is issued at that moment.Although I agree that it is not that good, I would be surprise if that was
the explanation for a performance regression, because the sync_file_range
with the chosen parameters is an async call, it "advises" the OS to send the
file, but it does not wait for it to be completed.
I frequently see sync_file_range blocking - it waits till it could
submit the writes into the io queues. On a system bottlenecked on IO
that's not always possible immediately.
Also, maybe you could answer a question I had about the performance
regression you observed, I could not find the post where you gave the
detailed information about it, so that I could try reproducing it: what are
the exact settings and conditions (shared_buffers, pgbench scaling, host
memory, ...), what is the observed regression (tps? other?), and what is the
responsiveness of the database under the regression (eg % of seconds with 0
tps for instance, or something like that).
I measured it in a different number of cases, both on SSDs and spinning
rust. I just reproduced it with:
postgres-ckpt14 \
-D /srv/temp/pgdev-dev-800/ \
-c maintenance_work_mem=2GB \
-c fsync=on \
-c synchronous_commit=off \
-c shared_buffers=2GB \
-c wal_level=hot_standby \
-c max_wal_senders=10 \
-c max_wal_size=100GB \
-c checkpoint_timeout=30s
Using a fresh cluster each time (copied from a "template" to save time)
and using
pgbench -M prepared -c 16 -j16 -T 300 -P 1
I get
My laptop 1 EVO 840, 1 i7-4800MQ, 16GB ram:
master:
scaling factor: 800
query mode: prepared
number of clients: 16
number of threads: 16
duration: 300 s
number of transactions actually processed: 1155733
latency average: 4.151 ms
latency stddev: 8.712 ms
tps = 3851.242965 (including connections establishing)
tps = 3851.725856 (excluding connections establishing)
ckpt-14 (flushing by backends disabled):
scaling factor: 800
query mode: prepared
number of clients: 16
number of threads: 16
duration: 300 s
number of transactions actually processed: 855156
latency average: 5.612 ms
latency stddev: 7.896 ms
tps = 2849.876327 (including connections establishing)
tps = 2849.912015 (excluding connections establishing)
My laptop 1 850 PRO, 1 i7-4800MQ, 16GB ram:
master:
transaction type: TPC-B (sort of)
scaling factor: 800
query mode: prepared
number of clients: 16
number of threads: 16
duration: 300 s
number of transactions actually processed: 2104781
latency average: 2.280 ms
latency stddev: 9.868 ms
tps = 7010.397938 (including connections establishing)
tps = 7010.475848 (excluding connections establishing)
ckpt-14 (flushing by backends disabled):
scaling factor: 800
query mode: prepared
number of clients: 16
number of threads: 16
duration: 300 s
number of transactions actually processed: 1930716
latency average: 2.484 ms
latency stddev: 7.303 ms
tps = 6434.785605 (including connections establishing)
tps = 6435.177773 (excluding connections establishing)
In neither case there are periods of 0 tps, but both have times of <
1000 tps with noticeably increased latency.
The endresults are similar with a sane checkpoint timeout - the tests
just take much longer to give meaningful results. Constantly running
long tests on prosumer level SSDs isn't nice - I've now killed 5 SSDs
with postgres testing...
As you can see there's roughly a 30% performance regression on the
slower SSD and a ~9% on the faster one. HDD results are similar (but I
can't repeat on the laptop right now since the 2nd hdd is now an SSD).
My working copy of checkpoint sorting & flushing currently results in:
My laptop 1 EVO 840, 1 i7-4800MQ, 16GB ram:
transaction type: TPC-B (sort of)
scaling factor: 800
query mode: prepared
number of clients: 16
number of threads: 16
duration: 300 s
number of transactions actually processed: 1136260
latency average: 4.223 ms
latency stddev: 8.298 ms
tps = 3786.696499 (including connections establishing)
tps = 3786.778875 (excluding connections establishing)
My laptop 1 850 PRO, 1 i7-4800MQ, 16GB ram:
transaction type: TPC-B (sort of)
scaling factor: 800
query mode: prepared
number of clients: 16
number of threads: 16
duration: 300 s
number of transactions actually processed: 2050661
latency average: 2.339 ms
latency stddev: 7.708 ms
tps = 6833.593170 (including connections establishing)
tps = 6833.680391 (excluding connections establishing)
My version of the patch currently addresses various points, which need
to be separated and benchmarked separate:
* Different approach to background writer, trying to make backends write
less. While that proves to be beneficial in isolation, on its own that
doesn't address the performance regression.
* Different flushing API, done outside the lock
So this partially addresses the performance problems, but not yet
completely.
Greetings,
Andres Freund
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 2016-01-11 14:45:16 +0100, Andres Freund wrote:
On 2016-01-09 16:49:56 +0100, Fabien COELHO wrote:
Hm. New theory: The current flush interface does the flushing inside
FlushBuffer()->smgrwrite()->mdwrite()->FileWrite()->FlushContextSchedule(). The
problem with that is that at that point we (need to) hold a content lock
on the buffer!You are worrying that FlushBuffer is holding a lock on a buffer and the
"sync_file_range" call occurs is issued at that moment.Although I agree that it is not that good, I would be surprise if that was
the explanation for a performance regression, because the sync_file_range
with the chosen parameters is an async call, it "advises" the OS to send the
file, but it does not wait for it to be completed.I frequently see sync_file_range blocking - it waits till it could
submit the writes into the io queues. On a system bottlenecked on IO
that's not always possible immediately.Also, maybe you could answer a question I had about the performance
regression you observed, I could not find the post where you gave the
detailed information about it, so that I could try reproducing it: what are
the exact settings and conditions (shared_buffers, pgbench scaling, host
memory, ...), what is the observed regression (tps? other?), and what is the
responsiveness of the database under the regression (eg % of seconds with 0
tps for instance, or something like that).I measured it in a different number of cases, both on SSDs and spinning
rust. I just reproduced it with:postgres-ckpt14 \
-D /srv/temp/pgdev-dev-800/ \
-c maintenance_work_mem=2GB \
-c fsync=on \
-c synchronous_commit=off \
-c shared_buffers=2GB \
-c wal_level=hot_standby \
-c max_wal_senders=10 \
-c max_wal_size=100GB \
-c checkpoint_timeout=30sUsing a fresh cluster each time (copied from a "template" to save time)
and using
pgbench -M prepared -c 16 -j16 -T 300 -P 1
I getMy laptop 1 EVO 840, 1 i7-4800MQ, 16GB ram:
master:
scaling factor: 800
query mode: prepared
number of clients: 16
number of threads: 16
duration: 300 s
number of transactions actually processed: 1155733
latency average: 4.151 ms
latency stddev: 8.712 ms
tps = 3851.242965 (including connections establishing)
tps = 3851.725856 (excluding connections establishing)ckpt-14 (flushing by backends disabled):
scaling factor: 800
query mode: prepared
number of clients: 16
number of threads: 16
duration: 300 s
number of transactions actually processed: 855156
latency average: 5.612 ms
latency stddev: 7.896 ms
tps = 2849.876327 (including connections establishing)
tps = 2849.912015 (excluding connections establishing)
Hm. I think I have an entirely different theory that might explain some
of this theory. I instrumented lwlocks to check for additional blocking
and found some. Admittedly not exactly where I thought it might
be. Check out what you can observe when adding/enabling an elog in
FlushBuffer() (and the progress printing from BufferSync()):
(sorry, a bit long, but it's necessary to understand)
[2016-01-11 20:15:02 CET][14957] CONTEXT: writing block 0 of relation base/13000/16387
to_scan: 131141, scanned: 6, %processed: 0.00, %writeouts: 100.00
[2016-01-11 20:15:02 CET][14957] LOG: xlog flush request 1F/D2FD7E0; write 1F/D296000; flush 1F/D296000; insert: 1F/D33B418
[2016-01-11 20:15:02 CET][14957] CONTEXT: writing block 2 of relation base/13000/16387
to_scan: 131141, scanned: 7, %processed: 0.01, %writeouts: 100.00
[2016-01-11 20:15:02 CET][14957] LOG: xlog flush request 1F/D3B2E30; write 1F/D33C000; flush 1F/D33C000; insert: 1F/D403198
[2016-01-11 20:15:02 CET][14957] CONTEXT: writing block 3 of relation base/13000/16387
to_scan: 131141, scanned: 9, %processed: 0.01, %writeouts: 100.00
[2016-01-11 20:15:02 CET][14957] LOG: xlog flush request 1F/D469990; write 1F/D402000; flush 1F/D402000; insert: 1F/D4FDD00
[2016-01-11 20:15:02 CET][14957] CONTEXT: writing block 5 of relation base/13000/16387
to_scan: 131141, scanned: 11, %processed: 0.01, %writeouts: 100.00
[2016-01-11 20:15:02 CET][14957] LOG: xlog flush request 1F/D5663E8; write 1F/D4FC000; flush 1F/D4FC000; insert: 1F/D5D1390
[2016-01-11 20:15:02 CET][14957] CONTEXT: writing block 7 of relation base/13000/16387
to_scan: 131141, scanned: 14, %processed: 0.01, %writeouts: 100.00
[2016-01-11 20:15:02 CET][14957] LOG: xlog flush request 1F/D673700; write 1F/D5D0000; flush 1F/D5D0000; insert: 1F/D687E58
[2016-01-11 20:15:02 CET][14957] CONTEXT: writing block 10 of relation base/13000/16387
to_scan: 131141, scanned: 15, %processed: 0.01, %writeouts: 100.00
[2016-01-11 20:15:02 CET][14957] LOG: xlog flush request 1F/D76BEC8; write 1F/D686000; flush 1F/D686000; insert: 1F/D7A83A0
[2016-01-11 20:15:02 CET][14957] CONTEXT: writing block 11 of relation base/13000/16387
to_scan: 131141, scanned: 16, %processed: 0.01, %writeouts: 100.00
[2016-01-11 20:15:03 CET][14957] LOG: xlog flush request 1F/D7AE5C0; write 1F/D7A83E8; flush 1F/D7A83E8; insert: 1F/D8B9A88
[2016-01-11 20:15:03 CET][14957] CONTEXT: writing block 12 of relation base/13000/16387
to_scan: 131141, scanned: 17, %processed: 0.01, %writeouts: 100.00
[2016-01-11 20:15:03 CET][14957] LOG: xlog flush request 1F/DA08370; write 1F/D963A38; flush 1F/D963A38; insert: 1F/DA0A7D0
[2016-01-11 20:15:03 CET][14957] CONTEXT: writing block 13 of relation base/13000/16387
to_scan: 131141, scanned: 18, %processed: 0.01, %writeouts: 100.00
[2016-01-11 20:15:03 CET][14957] LOG: xlog flush request 1F/DAC09A0; write 1F/DA92250; flush 1F/DA92250; insert: 1F/DB9AAC8
[2016-01-11 20:15:03 CET][14957] CONTEXT: writing block 14 of relation base/13000/16387
to_scan: 131141, scanned: 21, %processed: 0.02, %writeouts: 100.00
[2016-01-11 20:15:03 CET][14957] LOG: xlog flush request 1F/DCEFF18; write 1F/DC2AD30; flush 1F/DC2AD30; insert: 1F/DCF25B0
[2016-01-11 20:15:03 CET][14957] CONTEXT: writing block 17 of relation base/13000/16387
to_scan: 131141, scanned: 23, %processed: 0.02, %writeouts: 100.00
[2016-01-11 20:15:03 CET][14957] LOG: xlog flush request 1F/DD0E9E0; write 1F/DCF25F8; flush 1F/DCF25F8; insert: 1F/DDD6198
[2016-01-11 20:15:03 CET][14957] CONTEXT: writing block 19 of relation base/13000/16387
to_scan: 131141, scanned: 24, %processed: 0.02, %writeouts: 100.00
[2016-01-11 20:15:03 CET][14957] LOG: xlog flush request 1F/DED6A20; write 1F/DEC0358; flush 1F/DEC0358; insert: 1F/DFB64C8
[2016-01-11 20:15:03 CET][14957] CONTEXT: writing block 20 of relation base/13000/16387
to_scan: 131141, scanned: 25, %processed: 0.02, %writeouts: 100.00
[2016-01-11 20:15:03 CET][14957] LOG: xlog flush request 1F/DFDEE90; write 1F/DFB6560; flush 1F/DFB6560; insert: 1F/E073468
[2016-01-11 20:15:03 CET][14957] CONTEXT: writing block 21 of relation base/13000/16387
to_scan: 131141, scanned: 26, %processed: 0.02, %writeouts: 100.00
[2016-01-11 20:15:03 CET][14957] LOG: xlog flush request 1F/E295638; write 1F/E10B9F8; flush 1F/E10B9F8; insert: 1F/E2B40E0
[2016-01-11 20:15:03 CET][14957] CONTEXT: writing block 22 of relation base/13000/16387
to_scan: 131141, scanned: 27, %processed: 0.02, %writeouts: 100.00
[2016-01-11 20:15:03 CET][14957] LOG: xlog flush request 1F/E381688; write 1F/E354BC0; flush 1F/E354BC0; insert: 1F/E459598
[2016-01-11 20:15:03 CET][14957] CONTEXT: writing block 23 of relation base/13000/16387
to_scan: 131141, scanned: 28, %processed: 0.02, %writeouts: 100.00
[2016-01-11 20:15:03 CET][14957] LOG: xlog flush request 1F/E56EF70; write 1F/E4C0C98; flush 1F/E4C0C98; insert: 1F/E56F200
[2016-01-11 20:15:03 CET][14957] CONTEXT: writing block 24 of relation base/13000/16387
to_scan: 131141, scanned: 29, %processed: 0.02, %writeouts: 100.00
[2016-01-11 20:15:03 CET][14957] LOG: xlog flush request 1F/E67E538; write 1F/E5DC440; flush 1F/E5DC440; insert: 1F/E6F7FF8
[2016-01-11 20:15:03 CET][14957] CONTEXT: writing block 25 of relation base/13000/16387
to_scan: 131141, scanned: 31, %processed: 0.02, %writeouts: 100.00
[2016-01-11 20:15:03 CET][14957] LOG: xlog flush request 1F/E873DD8; write 1F/E7D81F0; flush 1F/E7D81F0; insert: 1F/E8A1710
[2016-01-11 20:15:03 CET][14957] CONTEXT: writing block 27 of relation base/13000/16387
to_scan: 131141, scanned: 33, %processed: 0.03, %writeouts: 100.00
[2016-01-11 20:15:03 CET][14957] LOG: xlog flush request 1F/E9E3948; write 1F/E979610; flush 1F/E979610; insert: 1F/EA27AC0
[2016-01-11 20:15:03 CET][14957] CONTEXT: writing block 29 of relation base/13000/16387
to_scan: 131141, scanned: 35, %processed: 0.03, %writeouts: 100.00
[2016-01-11 20:15:03 CET][14957] LOG: xlog flush request 1F/EABDDC8; write 1F/EA6DFE0; flush 1F/EA6DFE0; insert: 1F/EB10728
[2016-01-11 20:15:03 CET][14957] CONTEXT: writing block 31 of relation base/13000/16387
to_scan: 131141, scanned: 37, %processed: 0.03, %writeouts: 100.00
[2016-01-11 20:15:03 CET][14957] LOG: xlog flush request 1F/EC07328; write 1F/EBAABE0; flush 1F/EBAABE0; insert: 1F/EC9B8A8
[2016-01-11 20:15:03 CET][14957] CONTEXT: writing block 33 of relation base/13000/16387
to_scan: 131141, scanned: 40, %processed: 0.03, %writeouts: 100.00
[2016-01-11 20:15:03 CET][14957] LOG: xlog flush request 1F/ED18FF8; write 1F/EC9B8A8; flush 1F/EC9B8A8; insert: 1F/ED8C2F8
[2016-01-11 20:15:03 CET][14957] CONTEXT: writing block 36 of relation base/13000/16387
to_scan: 131141, scanned: 41, %processed: 0.03, %writeouts: 100.00
[2016-01-11 20:15:03 CET][14957] LOG: xlog flush request 1F/EEED640; write 1F/EE0BAD8; flush 1F/EE0BAD8; insert: 1F/EF35EA8
[2016-01-11 20:15:03 CET][14957] CONTEXT: writing block 37 of relation base/13000/16387
to_scan: 131141, scanned: 42, %processed: 0.03, %writeouts: 100.00
[2016-01-11 20:15:03 CET][14957] LOG: xlog flush request 1F/EFF20B8; write 1F/EFAAE20; flush 1F/EFAAE20; insert: 1F/F06FAC0
[2016-01-11 20:15:03 CET][14957] CONTEXT: writing block 38 of relation base/13000/16387
to_scan: 131141, scanned: 43, %processed: 0.03, %writeouts: 100.00
[2016-01-11 20:15:03 CET][14957] LOG: xlog flush request 1F/F1430B0; write 1F/F0DEAB8; flush 1F/F0DEAB8; insert: 1F/F265020
[2016-01-11 20:15:03 CET][14957] CONTEXT: writing block 39 of relation base/13000/16387
to_scan: 131141, scanned: 45, %processed: 0.03, %writeouts: 100.00
[2016-01-11 20:15:03 CET][14957] LOG: xlog flush request 1F/F3556C0; write 1F/F268F68; flush 1F/F268F68; insert: 1F/F3682B8
[2016-01-11 20:15:03 CET][14957] CONTEXT: writing block 41 of relation base/13000/16387
to_scan: 131141, scanned: 46, %processed: 0.04, %writeouts: 100.00
[2016-01-11 20:15:03 CET][14957] LOG: xlog flush request 1F/F5005F8; write 1F/F4376F8; flush 1F/F4376F8; insert: 1F/F523838
[2016-01-11 20:15:03 CET][14957] CONTEXT: writing block 42 of relation base/13000/16387
to_scan: 131141, scanned: 47, %processed: 0.04, %writeouts: 100.00
[2016-01-11 20:15:03 CET][14957] LOG: xlog flush request 1F/F6261C0; write 1F/F5A07A0; flush 1F/F5A07A0; insert: 1F/F691288
[2016-01-11 20:15:03 CET][14957] CONTEXT: writing block 43 of relation base/13000/16387
to_scan: 131141, scanned: 48, %processed: 0.04, %writeouts: 100.00
[2016-01-11 20:15:03 CET][14957] LOG: xlog flush request 1F/F7CBCD0; write 1F/F719020; flush 1F/F719020; insert: 1F/F80DBB0
[2016-01-11 20:15:03 CET][14957] CONTEXT: writing block 44 of relation base/13000/16387
to_scan: 131141, scanned: 49, %processed: 0.04, %writeouts: 100.00
[2016-01-11 20:15:03 CET][14957] LOG: xlog flush request 1F/F9359C8; write 1F/F874CB8; flush 1F/F874CB8; insert: 1F/F95AD58
[2016-01-11 20:15:03 CET][14957] CONTEXT: writing block 45 of relation base/13000/16387
to_scan: 131141, scanned: 50, %processed: 0.04, %writeouts: 100.00
[2016-01-11 20:15:03 CET][14957] LOG: xlog flush request 1F/FA33F38; write 1F/FA03490; flush 1F/FA03490; insert: 1F/FAD4DF8
[2016-01-11 20:15:03 CET][14957] CONTEXT: writing block 46 of relation base/13000/16387
to_scan: 131141, scanned: 51, %processed: 0.04, %writeouts: 100.00
[2016-01-11 20:15:03 CET][14957] LOG: xlog flush request 1F/FBDBCD8; write 1F/FB52238; flush 1F/FB52238; insert: 1F/FC54E68
[2016-01-11 20:15:03 CET][14957] CONTEXT: writing block 47 of relation base/13000/16387
to_scan: 131141, scanned: 52, %processed: 0.04, %writeouts: 100.00
[2016-01-11 20:15:03 CET][14957] LOG: xlog flush request 1F/FD74B60; write 1F/FD10360; flush 1F/FD10360; insert: 1F/FDB6A88
[2016-01-11 20:15:03 CET][14957] CONTEXT: writing block 48 of relation base/13000/16387
to_scan: 131141, scanned: 53, %processed: 0.04, %writeouts: 100.00
[2016-01-11 20:15:03 CET][14957] LOG: xlog flush request 1F/FE4FF60; write 1F/FDB6AD0; flush 1F/FDB6AD0; insert: 1F/FE90028
[2016-01-11 20:15:03 CET][14957] CONTEXT: writing block 49 of relation base/13000/16387
to_scan: 131141, scanned: 54, %processed: 0.04, %writeouts: 100.00
[2016-01-11 20:15:03 CET][14957] LOG: xlog flush request 1F/FFD6A78; write 1F/FF223F0; flush 1F/FF223F0; insert: 1F/10022F70
[2016-01-11 20:15:03 CET][14957] CONTEXT: writing block 50 of relation base/13000/16387
to_scan: 131141, scanned: 55, %processed: 0.04, %writeouts: 100.00
[2016-01-11 20:15:04 CET][14957] LOG: xlog flush request 1F/10144C98; write 1F/10023000; flush 1F/10023000; insert: 1F/10157730
[2016-01-11 20:15:04 CET][14957] CONTEXT: writing block 51 of relation base/13000/16387
to_scan: 131141, scanned: 58, %processed: 0.04, %writeouts: 100.00
[2016-01-11 20:15:04 CET][14957] LOG: xlog flush request 1F/102AA468; write 1F/1020C600; flush 1F/1020C600; insert: 1F/102C73F0
[2016-01-11 20:15:04 CET][14957] CONTEXT: writing block 54 of relation base/13000/16387
to_scan: 131141, scanned: 60, %processed: 0.05, %writeouts: 100.00
[2016-01-11 20:15:04 CET][14957] LOG: xlog flush request 1F/10313470; write 1F/102C7460; flush 1F/102C7460; insert: 1F/103D4F38
[2016-01-11 20:15:04 CET][14957] CONTEXT: writing block 56 of relation base/13000/16387
to_scan: 131141, scanned: 61, %processed: 0.05, %writeouts: 100.00
[2016-01-11 20:15:04 CET][14957] LOG: xlog flush request 1F/10510CE8; write 1F/104562F0; flush 1F/104562F0; insert: 1F/105171E8
[2016-01-11 20:15:04 CET][14957] CONTEXT: writing block 57 of relation base/13000/16387
to_scan: 131141, scanned: 62, %processed: 0.05, %writeouts: 100.00
[2016-01-11 20:15:04 CET][14957] LOG: xlog flush request 1F/10596B18; write 1F/105191B0; flush 1F/105191B0; insert: 1F/106076F8
[2016-01-11 20:15:04 CET][14957] CONTEXT: writing block 58 of relation base/13000/16387
to_scan: 131141, scanned: 63, %processed: 0.05, %writeouts: 100.00
[2016-01-11 20:15:04 CET][14957] LOG: xlog flush request 1F/1073FB28; write 1F/10693638; flush 1F/10693638; insert: 1F/10787D40
[2016-01-11 20:15:04 CET][14957] CONTEXT: writing block 59 of relation base/13000/16387
to_scan: 131141, scanned: 64, %processed: 0.05, %writeouts: 100.00
[2016-01-11 20:15:04 CET][14957] LOG: xlog flush request 1F/1088D058; write 1F/107F7068; flush 1F/107F7068; insert: 1F/10920EA0
[2016-01-11 20:15:04 CET][14957] CONTEXT: writing block 60 of relation base/13000/16387
to_scan: 131141, scanned: 67, %processed: 0.05, %writeouts: 100.00
[2016-01-11 20:15:04 CET][14957] LOG: xlog flush request 1F/109D9158; write 1F/109A8458; flush 1F/109A8458; insert: 1F/10A8A240
[2016-01-11 20:15:04 CET][14957] CONTEXT: writing block 63 of relation base/13000/16387
to_scan: 131141, scanned: 68, %processed: 0.05, %writeouts: 100.00
[2016-01-11 20:15:04 CET][14957] LOG: xlog flush request 1F/10BDAA38; write 1F/10B2AD48; flush 1F/10B2AD48; insert: 1F/10C16768
[2016-01-11 20:15:04 CET][14957] CONTEXT: writing block 64 of relation base/13000/16387
to_scan: 131141, scanned: 69, %processed: 0.05, %writeouts: 100.00
[2016-01-11 20:15:04 CET][14957] LOG: xlog flush request 1F/10D824D0; write 1F/10C859A0; flush 1F/10C859A0; insert: 1F/10DCC860
[2016-01-11 20:15:04 CET][14957] CONTEXT: writing block 65 of relation base/13000/16387
to_scan: 131141, scanned: 70, %processed: 0.05, %writeouts: 100.00
[2016-01-11 20:15:04 CET][14957] LOG: xlog flush request 1F/10E24CD8; write 1F/10DCC8A8; flush 1F/10DCC8A8; insert: 1F/10EA8588
[2016-01-11 20:15:04 CET][14957] CONTEXT: writing block 66 of relation base/13000/16387
to_scan: 131141, scanned: 71, %processed: 0.05, %writeouts: 100.00
[2016-01-11 20:15:04 CET][14957] LOG: xlog flush request 1F/10FD3E90; write 1F/10F57530; flush 1F/10F57530; insert: 1F/11043A58
[2016-01-11 20:15:04 CET][14957] CONTEXT: writing block 67 of relation base/13000/16387
to_scan: 131141, scanned: 72, %processed: 0.05, %writeouts: 100.00
[2016-01-11 20:15:04 CET][14957] LOG: xlog flush request 1F/111CE4A0; write 1F/11043AC8; flush 1F/11043AC8; insert: 1F/111ED470
[2016-01-11 20:15:04 CET][14957] CONTEXT: writing block 68 of relation base/13000/16387
to_scan: 131141, scanned: 73, %processed: 0.06, %writeouts: 100.00
[2016-01-11 20:15:04 CET][14957] LOG: xlog flush request 1F/11338080; write 1F/112917C8; flush 1F/112917C8; insert: 1F/1135CF80
[2016-01-11 20:15:04 CET][14957] CONTEXT: writing block 69 of relation base/13000/16387
to_scan: 131141, scanned: 76, %processed: 0.06, %writeouts: 100.00
[2016-01-11 20:15:04 CET][14957] LOG: xlog flush request 1F/11369068; write 1F/1135CF80; flush 1F/1135CF80; insert: 1F/1140BE88
[2016-01-11 20:15:04 CET][14957] CONTEXT: writing block 72 of relation base/13000/16387
to_scan: 131141, scanned: 77, %processed: 0.06, %writeouts: 100.00
[2016-01-11 20:15:04 CET][14957] LOG: xlog flush request 1F/1146A420; write 1F/1136E000; flush 1F/1136E000; insert: 1F/11483530
[2016-01-11 20:15:04 CET][14957] CONTEXT: writing block 73 of relation base/13000/16387
to_scan: 131141, scanned: 78, %processed: 0.06, %writeouts: 100.00
[2016-01-11 20:15:04 CET][14957] LOG: xlog flush request 1F/1157B800; write 1F/11483530; flush 1F/11483530; insert: 1F/11583E20
[2016-01-11 20:15:04 CET][14957] CONTEXT: writing block 74 of relation base/13000/16387
to_scan: 131141, scanned: 79, %processed: 0.06, %writeouts: 100.00
[2016-01-11 20:15:04 CET][14957] LOG: xlog flush request 1F/116368C0; write 1F/11583E20; flush 1F/11583E20; insert: 1F/116661A8
[2016-01-11 20:15:04 CET][14957] CONTEXT: writing block 75 of relation base/13000/16387
to_scan: 131141, scanned: 81, %processed: 0.06, %writeouts: 100.00
[2016-01-11 20:15:04 CET][14957] LOG: xlog flush request 1F/116FC598; write 1F/11668178; flush 1F/11668178; insert: 1F/11716758
[2016-01-11 20:15:04 CET][14957] CONTEXT: writing block 0 of relation base/13000/16393
to_scan: 131141, scanned: 82, %processed: 0.06, %writeouts: 100.00
[2016-01-11 20:15:04 CET][14957] LOG: xlog flush request 1F/117DA658; write 1F/117631F0; flush 1F/117631F0; insert: 1F/118206F0
[2016-01-11 20:15:04 CET][14957] CONTEXT: writing block 1 of relation base/13000/16393
to_scan: 131141, scanned: 83, %processed: 0.06, %writeouts: 100.00
[2016-01-11 20:15:04 CET][14957] LOG: xlog flush request 1F/11956320; write 1F/118E96B8; flush 1F/118E96B8; insert: 1F/1196F000
[2016-01-11 20:15:04 CET][14957] CONTEXT: writing block 2 of relation base/13000/16393
to_scan: 131141, scanned: 84, %processed: 0.06, %writeouts: 100.00
[2016-01-11 20:15:04 CET][14957] LOG: xlog flush request 1F/11A09B00; write 1F/1196F090; flush 1F/1196F090; insert: 1F/11A23D38
[2016-01-11 20:15:04 CET][14957] CONTEXT: writing block 3 of relation base/13000/16393
to_scan: 131141, scanned: 85, %processed: 0.06, %writeouts: 100.00
[2016-01-11 20:15:04 CET][14957] LOG: xlog flush request 1F/11B43C80; write 1F/11AB2148; flush 1F/11AB2148; insert: 1F/11B502D8
[2016-01-11 20:15:04 CET][14957] CONTEXT: writing block 4 of relation base/13000/16393
to_scan: 131141, scanned: 86, %processed: 0.07, %writeouts: 100.00
[2016-01-11 20:15:04 CET][14957] LOG: xlog flush request 1F/11BE2610; write 1F/11B503B8; flush 1F/11B503B8; insert: 1F/11BF9068
[2016-01-11 20:15:04 CET][14957] CONTEXT: writing block 5 of relation base/13000/16393
to_scan: 131141, scanned: 87, %processed: 0.07, %writeouts: 100.00
[2016-01-11 20:15:04 CET][14957] LOG: xlog flush request 1F/11CB9FD8; write 1F/11BF9168; flush 1F/11BF9168; insert: 1F/11CBE1F8
[2016-01-11 20:15:04 CET][14957] CONTEXT: writing block 6 of relation base/13000/16393
to_scan: 131141, scanned: 88, %processed: 0.07, %writeouts: 100.00
[2016-01-11 20:15:04 CET][14957] LOG: xlog flush request 1F/11D24E10; write 1F/11CBE268; flush 1F/11CBE268; insert: 1F/11D8BC18
[2016-01-11 20:15:04 CET][14957] CONTEXT: writing block 7 of relation base/13000/16393
to_scan: 131141, scanned: 89, %processed: 0.07, %writeouts: 100.00
[2016-01-11 20:15:04 CET][14957] LOG: xlog flush request 1F/11E9B070; write 1F/11DEC840; flush 1F/11DEC840; insert: 1F/11EB7EC0
[2016-01-11 20:15:04 CET][14957] CONTEXT: writing block 8 of relation base/13000/16393
to_scan: 131141, scanned: 90, %processed: 0.07, %writeouts: 100.00
[2016-01-11 20:15:04 CET][14957] LOG: xlog flush request 1F/11F5C3F0; write 1F/11F3FBD0; flush 1F/11F3FBD0; insert: 1F/11FE1A08
[2016-01-11 20:15:04 CET][14957] CONTEXT: writing block 9 of relation base/13000/16393
to_scan: 131141, scanned: 91, %processed: 0.07, %writeouts: 100.00
[2016-01-11 20:15:04 CET][14957] LOG: xlog flush request 1F/121EDC00; write 1F/1208E838; flush 1F/1208E838; insert: 1F/121F1EF8
[2016-01-11 20:15:04 CET][14957] CONTEXT: writing block 10 of relation base/13000/16393
to_scan: 131141, scanned: 92, %processed: 0.07, %writeouts: 100.00
[2016-01-11 20:15:04 CET][14957] LOG: xlog flush request 1F/122E0A70; write 1F/121F1F90; flush 1F/121F1F90; insert: 1F/122E9198
[2016-01-11 20:15:04 CET][14957] CONTEXT: writing block 11 of relation base/13000/16393
to_scan: 131141, scanned: 93, %processed: 0.07, %writeouts: 100.00
[2016-01-11 20:15:04 CET][14957] LOG: xlog flush request 1F/1243B698; write 1F/123A7EC8; flush 1F/123A7EC8; insert: 1F/1245E620
[2016-01-11 20:15:04 CET][14957] CONTEXT: writing block 12 of relation base/13000/16393
to_scan: 131141, scanned: 94, %processed: 0.07, %writeouts: 100.00
[2016-01-11 20:15:04 CET][14957] LOG: xlog flush request 1F/1258E7B0; write 1F/124BF6B8; flush 1F/124BF6B8; insert: 1F/1259F198
[2016-01-11 20:15:04 CET][14957] CONTEXT: writing block 13 of relation base/13000/16393
to_scan: 131141, scanned: 95, %processed: 0.07, %writeouts: 100.00
[2016-01-11 20:15:04 CET][14957] LOG: xlog flush request 1F/126C8E38; write 1F/12662BA0; flush 1F/12662BA0; insert: 1F/126FE690
[2016-01-11 20:15:04 CET][14957] CONTEXT: writing block 14 of relation base/13000/16393
to_scan: 131141, scanned: 96, %processed: 0.07, %writeouts: 100.00
[2016-01-11 20:15:04 CET][14957] LOG: xlog flush request 1F/127DE810; write 1F/126FE6D8; flush 1F/126FE6D8; insert: 1F/128081B0
[2016-01-11 20:15:04 CET][14957] CONTEXT: writing block 15 of relation base/13000/16393
to_scan: 131141, scanned: 97, %processed: 0.07, %writeouts: 100.00
[2016-01-11 20:15:04 CET][14957] LOG: xlog flush request 1F/12980108; write 1F/128A6000; flush 1F/128A6000; insert: 1F/129A8E00
[2016-01-11 20:15:04 CET][14957] CONTEXT: writing block 16 of relation base/13000/16393
to_scan: 131141, scanned: 98, %processed: 0.07, %writeouts: 100.00
[2016-01-11 20:15:05 CET][14957] LOG: xlog flush request 1F/12A55978; write 1F/129ACDB8; flush 1F/129ACDB8; insert: 1F/12A6A408
[2016-01-11 20:15:05 CET][14957] CONTEXT: writing block 17 of relation base/13000/16393
to_scan: 131141, scanned: 99, %processed: 0.08, %writeouts: 100.00
[2016-01-11 20:15:05 CET][14957] LOG: xlog flush request 1F/12BC1148; write 1F/12B12F40; flush 1F/12B12F40; insert: 1F/12BC15F8
[2016-01-11 20:15:05 CET][14957] CONTEXT: writing block 18 of relation base/13000/16393
to_scan: 131141, scanned: 100, %processed: 0.08, %writeouts: 100.00
[2016-01-11 20:15:05 CET][14957] LOG: xlog flush request 1F/12D36E20; write 1F/12C70120; flush 1F/12C70120; insert: 1F/12D4DC08
[2016-01-11 20:15:05 CET][14957] CONTEXT: writing block 19 of relation base/13000/16393
to_scan: 131141, scanned: 9892, %processed: 7.54, %writeouts: 100.00
[2016-01-11 20:15:05 CET][14957] LOG: xlog flush request 1F/13128AF8; write 1F/12DEE670; flush 1F/12DEE670; insert: 1F/1313B7D0
[2016-01-11 20:15:05 CET][14957] CONTEXT: writing block 101960 of relation base/13000/16396
to_scan: 131141, scanned: 18221, %processed: 13.89, %writeouts: 100.00
[2016-01-11 20:15:05 CET][14957] LOG: xlog flush request 1F/13276328; write 1F/1313A000; flush 1F/1313A000; insert: 1F/134E93A8
[2016-01-11 20:15:05 CET][14957] CONTEXT: writing block 188242 of relation base/13000/16396
to_scan: 131141, scanned: 25857, %processed: 19.72, %writeouts: 100.00
[2016-01-11 20:15:06 CET][14957] LOG: xlog flush request 1F/13497370; write 1F/1346E000; flush 1F/1346E000; insert: 1F/136C00F8
[2016-01-11 20:15:06 CET][14957] CONTEXT: writing block 267003 of relation base/13000/16396
to_scan: 131141, scanned: 26859, %processed: 20.48, %writeouts: 100.00
[2016-01-11 20:15:06 CET][14957] LOG: xlog flush request 1F/136B5BB0; write 1F/135D6000; flush 1F/135D6000; insert: 1F/136C00F8
[2016-01-11 20:15:06 CET][14957] CONTEXT: writing block 277621 of relation base/13000/16396
to_scan: 131141, scanned: 27582, %processed: 21.03, %writeouts: 100.00
[2016-01-11 20:15:06 CET][14957] LOG: xlog flush request 1F/138C6C38; write 1F/1375E900; flush 1F/1375E900; insert: 1F/138D5518
[2016-01-11 20:15:06 CET][14957] CONTEXT: writing block 285176 of relation base/13000/16396
to_scan: 131141, scanned: 28943, %processed: 22.07, %writeouts: 100.00
[2016-01-11 20:15:06 CET][14957] LOG: xlog flush request 1F/13A5B768; write 1F/138C8000; flush 1F/138C8000; insert: 1F/13AB61D0
[2016-01-11 20:15:06 CET][14957] CONTEXT: writing block 300007 of relation base/13000/16396
to_scan: 131141, scanned: 36181, %processed: 27.59, %writeouts: 100.00
[2016-01-11 20:15:06 CET][14957] LOG: xlog flush request 1F/13C320C8; write 1F/13A8A000; flush 1F/13A8A000; insert: 1F/13DAAB40
[2016-01-11 20:15:06 CET][14957] CONTEXT: writing block 375983 of relation base/13000/16396
to_scan: 131141, scanned: 40044, %processed: 30.54, %writeouts: 100.00
[2016-01-11 20:15:07 CET][14957] LOG: xlog flush request 1F/13E196C8; write 1F/13CBA000; flush 1F/13CBA000; insert: 1F/13F9E6D8
[2016-01-11 20:15:07 CET][14957] CONTEXT: writing block 416439 of relation base/13000/16396
to_scan: 131141, scanned: 48250, %processed: 36.79, %writeouts: 100.00
[2016-01-11 20:15:07 CET][14957] LOG: xlog flush request 1F/143F6160; write 1F/13EE8000; flush 1F/13EE8000; insert: 1F/1461BB08
You can see that initially every buffer triggers a WAL flush. That
causes a slowdown because a) we're doing significantly more WAL flushes
in that time period, both causing slowdown of concurrent IO and
concurrent WAL insertions b) due to the many slow flushes we get behind
on the checkpoint schedule, triggering a rapid fire period of writes
afterwards.
My theory is that this happens due to the sorting: pgbench is an update
heavy workload, the first few pages are always going to be used if
there's free space as freespacemap.c essentially prefers those. Due to
the sorting all a relation's early pages are going to be in "in a row".
Indeed, the behaviour is not visible in a significant manner when using
pgbench -N, where there are far fewer updated pages.
I'm not entirely sure how we can deal with that.
Greetings,
Andres Freund
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Tue, Jan 12, 2016 at 12:57 AM, Andres Freund <andres@anarazel.de> wrote:>
My theory is that this happens due to the sorting: pgbench is an update
heavy workload, the first few pages are always going to be used if
there's free space as freespacemap.c essentially prefers those. Due to
the sorting all a relation's early pages are going to be in "in a row".
Not sure, what is best way to tackle this problem, but I think one way could
be to perform sorting at flush requests level rather than before writing
to OS buffers.
With Regards,
Amit Kapila.
EnterpriseDB: http://www.enterprisedb.com
On 2016-01-12 17:50:36 +0530, Amit Kapila wrote:
On Tue, Jan 12, 2016 at 12:57 AM, Andres Freund <andres@anarazel.de> wrote:>
My theory is that this happens due to the sorting: pgbench is an update
heavy workload, the first few pages are always going to be used if
there's free space as freespacemap.c essentially prefers those. Due to
the sorting all a relation's early pages are going to be in "in a row".Not sure, what is best way to tackle this problem, but I think one way could
be to perform sorting at flush requests level rather than before writing
to OS buffers.
I'm not following. If you just sort a couple hundred more or less random
buffers - which is what you get if you look in buf_id order through
shared_buffers - the likelihood of actually finding neighbouring writes
is pretty low.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Hello Andres,
Thanks for the details. Many comments and some questions below.
Also, maybe you could answer a question I had about the performance
regression you observed, I could not find the post where you gave the
detailed information about it, so that I could try reproducing it: what are
the exact settings and conditions (shared_buffers, pgbench scaling, host
memory, ...), what is the observed regression (tps? other?), and what is the
responsiveness of the database under the regression (eg % of seconds with 0
tps for instance, or something like that).I measured it in a different number of cases, both on SSDs
and spinning rust.
Argh! This is a key point: the sort/flush is designed to help HDDs, and
would have limited effect on SSDs, and it seems that you are showing that
the effect is in fact negative on SSDs, too bad:-(
The bad news is that I do not have a host with a SSD available for
reproducing such results.
On SSDs, the linux IO scheduler works quite well, so this is a place where
I would consider simply disactivating flushing and/or sorting.
ISTM that I would rather update the documentation to "do not activate on
SSD" than try to find a miraculous solution which may or may not exist.
Basically I would use your results to give better advises in the
documentation, not as a motivation to rewrite the patch from scratch.
postgres-ckpt14 \
-D /srv/temp/pgdev-dev-800/ \
-c maintenance_work_mem=2GB \
-c fsync=on \
-c synchronous_commit=off \
I'm not sure I like this one. I guess the intention is to focus on
checkpointer writes and reduce the impact of WAL writes. Why not.
-c shared_buffers=2GB \
-c wal_level=hot_standby \
-c max_wal_senders=10 \
-c max_wal_size=100GB \
-c checkpoint_timeout=30s
That is a very short one, but the point is to exercise the checkpoint, so
why not.
My laptop 1 EVO 840, 1 i7-4800MQ, 16GB ram:
master:
scaling factor: 800
The DB is probably about 12GB, so it fits in memory in the end, meaning
that there should be only write activity after some time? So this is not
really the case where it does not fit in memory, but it is large enough to
get mostly random IOs both in read & write, so why not.
query mode: prepared
number of clients: 16
number of threads: 16
duration: 300 s
number of transactions actually processed: 1155733
Assuming one buffer accessed per transaction on average, and considering a
uniform random distribution, this means about 50% of pages actually loaded
in memory at the end of the run (1 - e(-1155766/800*2048)) (with 2048
pages per scale unit).
latency average: 4.151 ms
latency stddev: 8.712 ms
tps = 3851.242965 (including connections establishing)
tps = 3851.725856 (excluding connections establishing)
ckpt-14 (flushing by backends disabled):
Is this comment refering to "synchronous_commit = off"?
I guess this is the same on master above, even if not written?
[...] In neither case there are periods of 0 tps, but both have times of
1000 tps with noticeably increased latency.
Ok, but we are talking SSDs, things are not too bad, even if there are ups
and downs.
The endresults are similar with a sane checkpoint timeout - the tests
just take much longer to give meaningful results. Constantly running
long tests on prosumer level SSDs isn't nice - I've now killed 5 SSDs
with postgres testing...
Indeed. It wears out and costs, too bad:-(
As you can see there's roughly a 30% performance regression on the
slower SSD and a ~9% on the faster one. HDD results are similar (but I
can't repeat on the laptop right now since the 2nd hdd is now an SSD).
Ok, that is what I would have expected, the larger the database, the
smaller the impact of sorting & flushin on SSDs. Now I would have hoped
that flushing would help get a more constant load even in this case, at
least this is what I measured in my tests. The closest to your setting
test I ran is scale=660, and the sort/flush got 400 tps vs 100 tps
without, with 30 minutes checkpoints, but HDDs do not compare to SSDs...
My overall comments about this SSD regression is that the patch is really
designed to make a difference for HDDs, so to advise not activate on SSDs
if there is a regression in such a case.
Now this is a little disappointing as on paper sorted writes should also
be slightly better on SSDs, but if the bench says the contrary, I have to
believe the bench:-)
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Tue, Jan 12, 2016 at 5:52 PM, Andres Freund <andres@anarazel.de> wrote:
On 2016-01-12 17:50:36 +0530, Amit Kapila wrote:
On Tue, Jan 12, 2016 at 12:57 AM, Andres Freund <andres@anarazel.de>
wrote:>
My theory is that this happens due to the sorting: pgbench is an
update
heavy workload, the first few pages are always going to be used if
there's free space as freespacemap.c essentially prefers those. Due to
the sorting all a relation's early pages are going to be in "in a
row".
Not sure, what is best way to tackle this problem, but I think one way
could
be to perform sorting at flush requests level rather than before writing
to OS buffers.I'm not following. If you just sort a couple hundred more or less random
buffers - which is what you get if you look in buf_id order through
shared_buffers - the likelihood of actually finding neighbouring writes
is pretty low.
Why can't we do it at larger intervals (relative to total amount of writes)?
To explain, what I have in mind, let us assume that checkpoint interval
is longer (10 mins) and in the mean time all the writes are being done
by bgwriter which it registers in shared memory so that later checkpoint
can perform corresponding fsync's, now when the request queue
becomes threshhold size (let us say 1/3rd) full, then we can perform
sorting and merging and issue flush hints. Checkpointer task can
also follow somewhat similar technique which means that once it
has written 1/3rd or so of buffers (which we need to track), it can
perform flush hints after sort+merge. Now, I think we can also
do it in checkpointer alone rather than in bgwriter and checkpointer.
Basically, I think this can lead to lesser merging of neighbouring
writes, but might not hurt if sync_file_range() API is cheap.
With Regards,
Amit Kapila.
EnterpriseDB: http://www.enterprisedb.com
On 2016-01-12 13:54:21 +0100, Fabien COELHO wrote:
I measured it in a different number of cases, both on SSDs
and spinning rust.Argh! This is a key point: the sort/flush is designed to help HDDs, and
would have limited effect on SSDs, and it seems that you are showing that
the effect is in fact negative on SSDs, too bad:-(
As you quoted, I could reproduce the slowdown both with SSDs *and* with
rotating disks.
On SSDs, the linux IO scheduler works quite well, so this is a place where I
would consider simply disactivating flushing and/or sorting.
Not my experience. In different scenarios, primarily with a large
shared_buffers fitting the whole hot working set, the patch
significantly improves performance.
postgres-ckpt14 \
-D /srv/temp/pgdev-dev-800/ \
-c maintenance_work_mem=2GB \
-c fsync=on \
-c synchronous_commit=off \I'm not sure I like this one. I guess the intention is to focus on
checkpointer writes and reduce the impact of WAL writes. Why not.
Now sure what you mean? s_c = off is *very* frequent in the field.
My laptop 1 EVO 840, 1 i7-4800MQ, 16GB ram:
master:
scaling factor: 800The DB is probably about 12GB, so it fits in memory in the end, meaning that
there should be only write activity after some time? So this is not really
the case where it does not fit in memory, but it is large enough to get
mostly random IOs both in read & write, so why not.
Doesn't really fit into ram - shared buffers uses some space (which will
be double buffered) and the xlog will use some more.
ckpt-14 (flushing by backends disabled):
Is this comment refering to "synchronous_commit = off"?
I guess this is the same on master above, even if not written?
No, what I mean by that is that I didn't active flushing writes in
backends - something I found hugely effective in reducing jitter in a
number of workloads, but doesn't help throughput.
As you can see there's roughly a 30% performance regression on the
slower SSD and a ~9% on the faster one. HDD results are similar (but I
can't repeat on the laptop right now since the 2nd hdd is now an SSD).Ok, that is what I would have expected, the larger the database, the smaller
the impact of sorting & flushin on SSDs.
Again: "HDD results are similar". I primarily tested on a 4 disk raid10
of 4 disks, and a raid0 of 20 disks.
Greetings,
Andres Freund
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 2016-01-12 19:17:49 +0530, Amit Kapila wrote:
Why can't we do it at larger intervals (relative to total amount of writes)?
To explain, what I have in mind, let us assume that checkpoint interval
is longer (10 mins) and in the mean time all the writes are being done
by bgwriter
But that's not the scenario with the regression here, so I'm not sure
why you're bringing it up?
And if we're flushing significant portion of the writes, how does that
avoid the performance problem pointed out two messages upthread? Where
sorting leads to flushing highly contended buffers together, leading to
excessive wal flushing?
But more importantly, unless you also want to delay the writes
themselves, leaving that many dirty buffers in the kernel page cache
will bring back exactly the type of stalls (where the kernel flushes all
the pending dirty data in a short amount of time) we're trying to avoid
with the forced flushing. So doing flushes in a large patches is
something we really fundamentally do *not* want!
which it registers in shared memory so that later checkpoint
can perform corresponding fsync's, now when the request queue
becomes threshhold size (let us say 1/3rd) full, then we can perform
sorting and merging and issue flush hints.
Which means that a significant portion of the writes won't be able to be
collapsed, since only a random 1/3 of the buffers is sorted together.
Basically, I think this can lead to lesser merging of neighbouring
writes, but might not hurt if sync_file_range() API is cheap.
The cost of writing out data doess correspond heavily with the number of
random writes - which is what you get if you reduce the number of
neighbouring writes.
Greetings,
Andres Freund
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Tue, Jan 12, 2016 at 7:24 PM, Andres Freund <andres@anarazel.de> wrote:
On 2016-01-12 19:17:49 +0530, Amit Kapila wrote:
Why can't we do it at larger intervals (relative to total amount of
writes)?
To explain, what I have in mind, let us assume that checkpoint interval
is longer (10 mins) and in the mean time all the writes are being done
by bgwriterBut that's not the scenario with the regression here, so I'm not sure
why you're bringing it up?And if we're flushing significant portion of the writes, how does that
avoid the performance problem pointed out two messages upthread? Where
sorting leads to flushing highly contended buffers together, leading to
excessive wal flushing?
I think it will avoid that problem, because what I am telling is not-to-sort
the buffers before writing, rather sort the flush requests. If I remember
correctly, the initial patch of Fabien doesn't have sorting at the buffer
level, but still he is able to see the benefits in many cases.
But more importantly, unless you also want to delay the writes
themselves, leaving that many dirty buffers in the kernel page cache
will bring back exactly the type of stalls (where the kernel flushes all
the pending dirty data in a short amount of time) we're trying to avoid
with the forced flushing. So doing flushes in a large patches is
something we really fundamentally do *not* want!
Could it be because random I/O?
which it registers in shared memory so that later checkpoint
can perform corresponding fsync's, now when the request queue
becomes threshhold size (let us say 1/3rd) full, then we can perform
sorting and merging and issue flush hints.Which means that a significant portion of the writes won't be able to be
collapsed, since only a random 1/3 of the buffers is sorted together.Basically, I think this can lead to lesser merging of neighbouring
writes, but might not hurt if sync_file_range() API is cheap.The cost of writing out data doess correspond heavily with the number of
random writes - which is what you get if you reduce the number of
neighbouring writes.
Yeah, thats right, but I am not sure how much difference it would
create if sorting everything at one short versus if we do that in
batches. In anycase, I am just trying to think out loud to see if we
can find some solution to the regression you have seen above
without disabling sorting altogether for certain cases.
With Regards,
Amit Kapila.
EnterpriseDB: http://www.enterprisedb.com
Hello Andres,
Argh! This is a key point: the sort/flush is designed to help HDDs, and
would have limited effect on SSDs, and it seems that you are showing that
the effect is in fact negative on SSDs, too bad:-(As you quoted, I could reproduce the slowdown both with SSDs *and* with
rotating disks.
Ok, once again I misunderstood. So you have a regression on HDD with the
settings you pointed out, I can try that.
On SSDs, the linux IO scheduler works quite well, so this is a place where I
would consider simply disactivating flushing and/or sorting.Not my experience. In different scenarios, primarily with a large
shared_buffers fitting the whole hot working set, the patch
significantly improves performance.
Good! That would be what I expected, but I have no way to test that.
postgres-ckpt14 \
-D /srv/temp/pgdev-dev-800/ \
-c maintenance_work_mem=2GB \
-c fsync=on \
-c synchronous_commit=off \I'm not sure I like this one. I guess the intention is to focus on
checkpointer writes and reduce the impact of WAL writes. Why not.Now sure what you mean? s_c = off is *very* frequent in the field.
Too bad, because for me it is really disactivating the D of ACID...
I think that this setting would not issue the "sync" calls on the WAL
file, which means that the impact of WAL writing is somehow reduced and
random writes (more or less for each transaction) is switched to
sequential writes by the IO scheduler.
My laptop 1 EVO 840, 1 i7-4800MQ, 16GB ram:
master:
scaling factor: 800The DB is probably about 12GB, so it fits in memory in the end, meaning that
there should be only write activity after some time? So this is not really
the case where it does not fit in memory, but it is large enough to get
mostly random IOs both in read & write, so why not.Doesn't really fit into ram - shared buffers uses some space (which will
be double buffered) and the xlog will use some more.
Hmmm. My understanding is that you are really using about 6GB of shared
buffer data in a run, plus some write only stuff...
xlog is flush/synced constantly and never read again, I would be surprise
that it has a significant memory impact.
ckpt-14 (flushing by backends disabled):
Is this comment refering to "synchronous_commit = off"?
I guess this is the same on master above, even if not written?No, what I mean by that is that I didn't active flushing writes in
backends -
I'm not sure that I understand. What is the actual corresponding directive
in the configuration file?
As you can see there's roughly a 30% performance regression on the
slower SSD and a ~9% on the faster one. HDD results are similar (but I
can't repeat on the laptop right now since the 2nd hdd is now an SSD).Ok, that is what I would have expected, the larger the database, the smaller
the impact of sorting & flushin on SSDs.Again: "HDD results are similar". I primarily tested on a 4 disk raid10
of 4 disks, and a raid0 of 20 disks.
I guess similar but with a much lower tps. Anyway I can try that.
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Hi Fabien,
On 2016-01-11 14:45:16 +0100, Andres Freund wrote:
I measured it in a different number of cases, both on SSDs and spinning
rust. I just reproduced it with:postgres-ckpt14 \
-D /srv/temp/pgdev-dev-800/ \
-c maintenance_work_mem=2GB \
-c fsync=on \
-c synchronous_commit=off \
-c shared_buffers=2GB \
-c wal_level=hot_standby \
-c max_wal_senders=10 \
-c max_wal_size=100GB \
-c checkpoint_timeout=30s
What kernel, filesystem and filesystem option did you measure with?
I was/am using ext4, and it turns out that, when abling flushing, the
results are hugely dependant on barriers=on/off, with the latter making
flushing rather advantageous. Additionally data=ordered/writeback makes
measureable difference too.
Reading kernel sources trying to understand some more of the performance
impact.
Greetings,
Andres Freund
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Hi Fabien,
Hello Tomas.
On 2016-01-11 14:45:16 +0100, Andres Freund wrote:
I measured it in a different number of cases, both on SSDs and spinning
rust. I just reproduced it with:postgres-ckpt14 \
-D /srv/temp/pgdev-dev-800/ \
-c maintenance_work_mem=2GB \
-c fsync=on \
-c synchronous_commit=off \
-c shared_buffers=2GB \
-c wal_level=hot_standby \
-c max_wal_senders=10 \
-c max_wal_size=100GB \
-c checkpoint_timeout=30sWhat kernel, filesystem and filesystem option did you measure with?
Andres did these measures, not me, so I do not know.
I was/am using ext4, and it turns out that, when abling flushing, the
results are hugely dependant on barriers=on/off, with the latter making
flushing rather advantageous. Additionally data=ordered/writeback makes
measureable difference too.
These are very interesting tests, I'm looking forward to have a look at
the results.
The fact that these options change performance is expected. Personnaly the
test I submitted on the thread used ext4 with default mount options plus
"relatime".
If I had a choice, I would tend to take the safest options, because the
point of a database is to keep data safe. That's why I'm not found of the
"synchronous_commit=off" chosen above.
Reading kernel sources trying to understand some more of the performance
impact.
Wow!
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Hello Andres,
Hello Tomas.
Ooops, sorry Andres, I mixed up the thread in my head so was not clear who
was asking the questions to whom.
I was/am using ext4, and it turns out that, when abling flushing, the
results are hugely dependant on barriers=on/off, with the latter making
flushing rather advantageous. Additionally data=ordered/writeback makes
measureable difference too.These are very interesting tests, I'm looking forward to have a look at the
results.The fact that these options change performance is expected. Personnaly the
test I submitted on the thread used ext4 with default mount options plus
"relatime".
I confirm that: nothing special but "relatime" on ext4 on my test host.
If I had a choice, I would tend to take the safest options, because the point
of a database is to keep data safe. That's why I'm not found of the
"synchronous_commit=off" chosen above.
"found" -> "fond". I confirm this opinion. If you have BBU on you
disk/raid system probably playing with some of these options is safe,
though. Not the case with my basic hardware.
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Hello Andres,
I measured it in a different number of cases, both on SSDs and spinning
rust. I just reproduced it with:postgres-ckpt14 \
-D /srv/temp/pgdev-dev-800/ \
-c maintenance_work_mem=2GB \
-c fsync=on \
-c synchronous_commit=off \
-c shared_buffers=2GB \
-c wal_level=hot_standby \
-c max_wal_senders=10 \
-c max_wal_size=100GB \
-c checkpoint_timeout=30sUsing a fresh cluster each time (copied from a "template" to save time)
and using
pgbench -M prepared -c 16 -j 16 -T 300 -P 1
I'm running some tests similar to those above...
Do you do some warmup when testing? I guess the answer is "no".
I understand that you have 8 cores/16 threads on your host?
Loading scale 800 data for 300 seconds tests takes much more than 300
seconds (init takes ~360 seconds, vacuum & index are slow). With 30
seconds checkpoint cycles and without any warmup, I feel that these tests
are really on the very short (too short) side, so I'm not sure how much I
can trust such results as significant. The data I reported were with more
real life like parameters.
Anyway, I'll have some results to show with a setting more or less similar
to yours.
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 2016-01-16 10:01:25 +0100, Fabien COELHO wrote:
Hello Andres,
I measured it in a different number of cases, both on SSDs and spinning
rust. I just reproduced it with:postgres-ckpt14 \
-D /srv/temp/pgdev-dev-800/ \
-c maintenance_work_mem=2GB \
-c fsync=on \
-c synchronous_commit=off \
-c shared_buffers=2GB \
-c wal_level=hot_standby \
-c max_wal_senders=10 \
-c max_wal_size=100GB \
-c checkpoint_timeout=30sUsing a fresh cluster each time (copied from a "template" to save time)
and using
pgbench -M prepared -c 16 -j 16 -T 300 -P 1
So, I've analyzed the problem further, and I think I found something
rater interesting. I'd profiled the kernel looking where it blocks in
the IO request queues, and found that the wal writer was involved
surprisingly often.
So, in a workload where everything (checkpoint, bgwriter, backend
writes) is flushed: 2995 tps
After I kill the wal writer with -STOP: 10887 tps
Stracing the wal writer shows:
17:29:02.001517 --- SIGUSR1 {si_signo=SIGUSR1, si_code=SI_USER, si_pid=17857, si_uid=1000} ---
17:29:02.001538 rt_sigreturn({mask=[]}) = 0
17:29:02.001582 read(8, 0x7ffea6b6b200, 16) = -1 EAGAIN (Resource temporarily unavailable)
17:29:02.001615 write(3, "\210\320\5\0\1\0\0\0\0@\330_/\0\0\0w\f\0\0\0\0\0\0\0\4\0\2\t\30\0\372"..., 49152) = 49152
17:29:02.001671 fdatasync(3) = 0
17:29:02.005022 --- SIGUSR1 {si_signo=SIGUSR1, si_code=SI_USER, si_pid=17825, si_uid=1000} ---
17:29:02.005043 rt_sigreturn({mask=[]}) = 0
17:29:02.005081 read(8, 0x7ffea6b6b200, 16) = -1 EAGAIN (Resource temporarily unavailable)
17:29:02.005111 write(3, "\210\320\5\0\1\0\0\0\0\0\331_/\0\0\0\7\26\0\0\0\0\0\0T\251\0\0\0\0\0\0"..., 8192) = 8192
17:29:02.005147 fdatasync(3) = 0
17:29:02.008688 --- SIGUSR1 {si_signo=SIGUSR1, si_code=SI_USER, si_pid=17866, si_uid=1000} ---
17:29:02.008705 rt_sigreturn({mask=[]}) = 0
17:29:02.008730 read(8, 0x7ffea6b6b200, 16) = -1 EAGAIN (Resource temporarily unavailable)
17:29:02.008757 write(3, "\210\320\5\0\1\0\0\0\0 \331_/\0\0\0\267\30\0\0\0\0\0\0 "..., 98304) = 98304
17:29:02.008822 fdatasync(3) = 0
17:29:02.016125 --- SIGUSR1 {si_signo=SIGUSR1, si_code=SI_USER, si_pid=17865, si_uid=1000} ---
17:29:02.016141 rt_sigreturn({mask=[]}) = 0
17:29:02.016174 read(8, 0x7ffea6b6b200, 16) = -1 EAGAIN (Resource temporarily unavailable)
17:29:02.016204 write(3, "\210\320\5\0\1\0\0\0\0\240\332_/\0\0\0s\5\0\0\0\0\0\0\t\30\0\2|8\2u"..., 57344) = 57344
17:29:02.016281 fdatasync(3) = 0
17:29:02.019181 --- SIGUSR1 {si_signo=SIGUSR1, si_code=SI_USER, si_pid=17865, si_uid=1000} ---
17:29:02.019199 rt_sigreturn({mask=[]}) = 0
17:29:02.019226 read(8, 0x7ffea6b6b200, 16) = -1 EAGAIN (Resource temporarily unavailable)
17:29:02.019249 write(3, "\210\320\5\0\1\0\0\0\0\200\333_/\0\0\0\307\f\0\0\0\0\0\0 "..., 73728) = 73728
17:29:02.019355 fdatasync(3) = 0
17:29:02.022680 --- SIGUSR1 {si_signo=SIGUSR1, si_code=SI_USER, si_pid=17865, si_uid=1000} ---
17:29:02.022696 rt_sigreturn({mask=[]}) = 0
I.e. we're fdatasync()ing small amount of pages. Roughly 500 times a
second. As soon as the wal writer is stopped, it's much bigger chunks,
on the order of 50-130 pages. And, not that surprisingly, that improves
performance, because there's far fewer cache flushes submitted to the
hardware.
I'm running some tests similar to those above...
Do you do some warmup when testing? I guess the answer is "no".
Doesn't make a difference here, I tried both. As long as before/after
benchmarks start from the same state...
I understand that you have 8 cores/16 threads on your host?
On one of them, 4 cores/8 threads on the laptop.
Loading scale 800 data for 300 seconds tests takes much more than 300
seconds (init takes ~360 seconds, vacuum & index are slow). With 30 seconds
checkpoint cycles and without any warmup, I feel that these tests are really
on the very short (too short) side, so I'm not sure how much I can trust
such results as significant. The data I reported were with more real life
like parameters.
I see exactly the same with 300s or 1000s checkpoint cycles, it just
takes a lot longer to repeat. They're also similar (although obviously
both before/after patch are higher) if I disable full_page_writes,
thereby eliminating a lot of other IO.
Andres
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
<Oops, wrong "From" again, resent>
I measured it in a different number of cases, both on SSDs and spinning
rust. I just reproduced it with:postgres-ckpt14 \
-D /srv/temp/pgdev-dev-800/ \
-c maintenance_work_mem=2GB \
-c fsync=on \
-c synchronous_commit=off \
-c shared_buffers=2GB \
-c wal_level=hot_standby \
-c max_wal_senders=10 \
-c max_wal_size=100GB \
-c checkpoint_timeout=30sUsing a fresh cluster each time (copied from a "template" to save time)
and using
pgbench -M prepared -c 16 -j 16 -T 300 -P 1
I must say that I have not succeeded in reproducing any significant
regression up to now on an HDD. I'm running some more tests again because
I had left out some options above that I thought were non essential.
I have deep problems with the 30-second checkpoint tests: basically the
checkpoints take much more than 30 seconds to complete, the system is not
stable, the 300 seconds runs last more than 900 seconds because the
clients are stuck a long time. The overall behavior is appaling as most of
the time is spent in IO panic at 0 tps.
Also, the performance level is around 160 tps on HDDs, which make sense to
me for a 7200 rpm HDD capable of about x00 random writes per second. It
seems to me that you reported much better performance on HDD, but I cannot
really see how this would be possible if data are indeed writen to disk.
Any idea?
Also, what is the very precise postgres version & patch used in your
tests on HDDs?
both before/after patch are higher) if I disable full_page_writes,
thereby eliminating a lot of other IO.
Maybe this is an explanation....
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 2016-01-19 10:27:31 +0100, Fabien COELHO wrote:
Also, the performance level is around 160 tps on HDDs, which make sense to
me for a 7200 rpm HDD capable of about x00 random writes per second. It
seems to me that you reported much better performance on HDD, but I cannot
really see how this would be possible if data are indeed writen to disk. Any
idea?
synchronous_commit = off does make a significant difference.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Import Notes
Reply to msg id not found: alpine.DEB.2.10.1601191004060.15654@sto
synchronous_commit = off does make a significant difference.
Sure, but I had thought about that and kept this one...
I think I found one possible culprit: I automatically wrote 300 seconds
for checkpoint_timeout, instead of 30 seconds in your settings. I'll have
to rerun the tests with this (unreasonnable) figure to check whether I
really get a regression.
Other tests I ran with "reasonnable" settings on a large (scale=800) db
did not show any significant performance regression, up to know.
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 2016-01-19 13:34:14 +0100, Fabien COELHO wrote:
synchronous_commit = off does make a significant difference.
Sure, but I had thought about that and kept this one...
But why are you then saying this is fundamentally limited to 160
xacts/sec?
I think I found one possible culprit: I automatically wrote 300 seconds for
checkpoint_timeout, instead of 30 seconds in your settings. I'll have to
rerun the tests with this (unreasonnable) figure to check whether I really
get a regression.
I've not seen meaningful changes in the size of the regression between 30/300s.
Other tests I ran with "reasonnable" settings on a large (scale=800) db did
not show any significant performance regression, up to know.
Try running it so that the data set nearly, but not entirely fit into
the OS page cache, while definitely not fitting into shared_buffers. The
scale=800 just worked for that on my hardware, no idea how it's for yours.
That seems to be the point where the effect is the worst.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Mon, Jan 18, 2016 at 11:39 AM, Andres Freund <andres@anarazel.de> wrote:
On 2016-01-16 10:01:25 +0100, Fabien COELHO wrote:
Hello Andres,
I measured it in a different number of cases, both on SSDs and spinning
rust. I just reproduced it with:postgres-ckpt14 \
-D /srv/temp/pgdev-dev-800/ \
-c maintenance_work_mem=2GB \
-c fsync=on \
-c synchronous_commit=off \
-c shared_buffers=2GB \
-c wal_level=hot_standby \
-c max_wal_senders=10 \
-c max_wal_size=100GB \
-c checkpoint_timeout=30sUsing a fresh cluster each time (copied from a "template" to save time)
and using
pgbench -M prepared -c 16 -j 16 -T 300 -P 1So, I've analyzed the problem further, and I think I found something
rater interesting. I'd profiled the kernel looking where it blocks in
the IO request queues, and found that the wal writer was involved
surprisingly often.So, in a workload where everything (checkpoint, bgwriter, backend
writes) is flushed: 2995 tps
After I kill the wal writer with -STOP: 10887 tpsStracing the wal writer shows:
17:29:02.001517 --- SIGUSR1 {si_signo=SIGUSR1, si_code=SI_USER, si_pid=17857, si_uid=1000} ---
17:29:02.001538 rt_sigreturn({mask=[]}) = 0
17:29:02.001582 read(8, 0x7ffea6b6b200, 16) = -1 EAGAIN (Resource temporarily unavailable)
17:29:02.001615 write(3, "\210\320\5\0\1\0\0\0\0@\330_/\0\0\0w\f\0\0\0\0\0\0\0\4\0\2\t\30\0\372"..., 49152) = 49152
17:29:02.001671 fdatasync(3) = 0
17:29:02.005022 --- SIGUSR1 {si_signo=SIGUSR1, si_code=SI_USER, si_pid=17825, si_uid=1000} ---
17:29:02.005043 rt_sigreturn({mask=[]}) = 0
17:29:02.005081 read(8, 0x7ffea6b6b200, 16) = -1 EAGAIN (Resource temporarily unavailable)
17:29:02.005111 write(3, "\210\320\5\0\1\0\0\0\0\0\331_/\0\0\0\7\26\0\0\0\0\0\0T\251\0\0\0\0\0\0"..., 8192) = 8192
17:29:02.005147 fdatasync(3) = 0
17:29:02.008688 --- SIGUSR1 {si_signo=SIGUSR1, si_code=SI_USER, si_pid=17866, si_uid=1000} ---
17:29:02.008705 rt_sigreturn({mask=[]}) = 0
17:29:02.008730 read(8, 0x7ffea6b6b200, 16) = -1 EAGAIN (Resource temporarily unavailable)
17:29:02.008757 write(3, "\210\320\5\0\1\0\0\0\0 \331_/\0\0\0\267\30\0\0\0\0\0\0 "..., 98304) = 98304
17:29:02.008822 fdatasync(3) = 0
17:29:02.016125 --- SIGUSR1 {si_signo=SIGUSR1, si_code=SI_USER, si_pid=17865, si_uid=1000} ---
17:29:02.016141 rt_sigreturn({mask=[]}) = 0
17:29:02.016174 read(8, 0x7ffea6b6b200, 16) = -1 EAGAIN (Resource temporarily unavailable)
17:29:02.016204 write(3, "\210\320\5\0\1\0\0\0\0\240\332_/\0\0\0s\5\0\0\0\0\0\0\t\30\0\2|8\2u"..., 57344) = 57344
17:29:02.016281 fdatasync(3) = 0
17:29:02.019181 --- SIGUSR1 {si_signo=SIGUSR1, si_code=SI_USER, si_pid=17865, si_uid=1000} ---
17:29:02.019199 rt_sigreturn({mask=[]}) = 0
17:29:02.019226 read(8, 0x7ffea6b6b200, 16) = -1 EAGAIN (Resource temporarily unavailable)
17:29:02.019249 write(3, "\210\320\5\0\1\0\0\0\0\200\333_/\0\0\0\307\f\0\0\0\0\0\0 "..., 73728) = 73728
17:29:02.019355 fdatasync(3) = 0
17:29:02.022680 --- SIGUSR1 {si_signo=SIGUSR1, si_code=SI_USER, si_pid=17865, si_uid=1000} ---
17:29:02.022696 rt_sigreturn({mask=[]}) = 0I.e. we're fdatasync()ing small amount of pages. Roughly 500 times a
second. As soon as the wal writer is stopped, it's much bigger chunks,
on the order of 50-130 pages. And, not that surprisingly, that improves
performance, because there's far fewer cache flushes submitted to the
hardware.
This seems like a problem with the WAL writer quite independent of
anything else. It seems likely to be inadvertent fallout from this
patch:
Author: Simon Riggs <simon@2ndQuadrant.com>
Branch: master Release: REL9_2_BR [4de82f7d7] 2011-11-13 09:00:57 +0000
Wakeup WALWriter as needed for asynchronous commit performance.
Previously we waited for wal_writer_delay before flushing WAL. Now
we also wake WALWriter as soon as a WAL buffer page has filled.
Significant effect observed on performance of asynchronous commits
by Robert Haas, attributed to the ability to set hint bits on tuples
earlier and so reducing contention caused by clog lookups.
If I understand correctly, prior to that commit, WAL writer woke up 5
times per second and flushed just that often (unless you changed the
default settings). But as the commit message explained, that turned
out to suck - you could make performance go up very significantly by
radically decreasing wal_writer_delay. This commit basically lets it
flush at maximum velocity - as fast as we finish one flush, we can
start the next. That must have seemed like a win at the time from the
way the commit message was written, but you seem to now be seeing the
opposite effect, where performance is suffering because flushes are
too frequent rather than too infrequent. I wonder if there's an ideal
flush rate and what it is, and how much it depends on what hardware
you have got.
--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
synchronous_commit = off does make a significant difference.
Sure, but I had thought about that and kept this one...
But why are you then saying this is fundamentally limited to 160
xacts/sec?
I'm just saying that the tested load generates mostly random IOs (probably
on average over 1 page per transaction), random IOs are very slow on a
HDD, so I do not expect great tps.
I think I found one possible culprit: I automatically wrote 300 seconds for
checkpoint_timeout, instead of 30 seconds in your settings. I'll have to
rerun the tests with this (unreasonnable) figure to check whether I really
get a regression.I've not seen meaningful changes in the size of the regression between 30/300s.
At 300 seconds (5 minutes) the checkpoints of the accumulated takes 15-25
minutes, during which the database is mostly offline, and there is no
clear difference with/without sort+flush.
Other tests I ran with "reasonnable" settings on a large (scale=800) db did
not show any significant performance regression, up to now.Try running it so that the data set nearly, but not entirely fit into
the OS page cache, while definitely not fitting into shared_buffers. The
scale=800 just worked for that on my hardware, no idea how it's for yours.
That seems to be the point where the effect is the worst.
I have 16GB memory on the tested host, same as your hardware I think, so I
use scale 800 => 12GB at the beginning of the run. Not sure it fits the
bill as I think it fits in memory, so the load is mostly write and no/very
few reads. I'll also try with scale 1000.
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 2016-01-19 12:58:38 -0500, Robert Haas wrote:
This seems like a problem with the WAL writer quite independent of
anything else. It seems likely to be inadvertent fallout from this
patch:Author: Simon Riggs <simon@2ndQuadrant.com>
Branch: master Release: REL9_2_BR [4de82f7d7] 2011-11-13 09:00:57 +0000Wakeup WALWriter as needed for asynchronous commit performance.
Previously we waited for wal_writer_delay before flushing WAL. Now
we also wake WALWriter as soon as a WAL buffer page has filled.
Significant effect observed on performance of asynchronous commits
by Robert Haas, attributed to the ability to set hint bits on tuples
earlier and so reducing contention caused by clog lookups.
In addition to that the "powersaving" effort also plays a role - without
the latch we'd not wake up at any meaningful rate at all atm.
If I understand correctly, prior to that commit, WAL writer woke up 5
times per second and flushed just that often (unless you changed the
default settings). But as the commit message explained, that turned
out to suck - you could make performance go up very significantly by
radically decreasing wal_writer_delay. This commit basically lets it
flush at maximum velocity - as fast as we finish one flush, we can
start the next. That must have seemed like a win at the time from the
way the commit message was written, but you seem to now be seeing the
opposite effect, where performance is suffering because flushes are
too frequent rather than too infrequent. I wonder if there's an ideal
flush rate and what it is, and how much it depends on what hardware
you have got.
I think the problem isn't really that it's flushing too much WAL in
total, it's that it's flushing WAL in a too granular fashion. I suspect
we want something where we attempt a minimum number of flushes per
second (presumably tied to wal_writer_delay) and, once exceeded, a
minimum number of pages per flush. I think we even could continue to
write() the data at the same rate as today, we just would need to reduce
the number of fdatasync()s we issue. And possibly could make the
eventual fdatasync()s cheaper by hinting the kernel to write them out
earlier.
Now the question what the minimum number of pages we want to flush for
(setting wal_writer_delay triggered ones aside) isn't easy to answer. A
simple model would be to statically tie it to the size of wal_buffers;
say, don't flush unless at least 10% of XLogBuffers have been written
since the last flush. More complex approaches would be to measure the
continuous WAL writeout rate.
By tying it to both a minimum rate under activity (ensuring things go to
disk fast) and a minimum number of pages to sync (ensuring a reasonable
number of cache flush operations) we should be able to mostly accomodate
the different types of workloads. I think.
Andres
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 2016-01-19 22:43:21 +0100, Andres Freund wrote:
On 2016-01-19 12:58:38 -0500, Robert Haas wrote:
This seems like a problem with the WAL writer quite independent of
anything else. It seems likely to be inadvertent fallout from this
patch:Author: Simon Riggs <simon@2ndQuadrant.com>
Branch: master Release: REL9_2_BR [4de82f7d7] 2011-11-13 09:00:57 +0000Wakeup WALWriter as needed for asynchronous commit performance.
Previously we waited for wal_writer_delay before flushing WAL. Now
we also wake WALWriter as soon as a WAL buffer page has filled.
Significant effect observed on performance of asynchronous commits
by Robert Haas, attributed to the ability to set hint bits on tuples
earlier and so reducing contention caused by clog lookups.In addition to that the "powersaving" effort also plays a role - without
the latch we'd not wake up at any meaningful rate at all atm.
The relevant thread is at
http://archives.postgresql.org/message-id/CA%2BTgmoaCr3kDPafK5ygYDA9mF9zhObGp_13q0XwkEWsScw6h%3Dw%40mail.gmail.com
what I didn't remember is that I voiced concern back then about exactly this:
http://archives.postgresql.org/message-id/201112011518.29964.andres%40anarazel.de
;)
Simon: CCed you, as the author of the above commit. Quick summary:
The frequent wakeups of wal writer can lead to significant performance
regressions in workloads that are bigger than shared_buffers, because
the super-frequent fdatasync()s by the wal writer slow down concurrent
writes (bgwriter, checkpointer, individual backend writes)
dramatically. To the point that SIGSTOPing the wal writer gets a pgbench
workload from 2995 to 10887 tps. The reasons fdatasyncs cause a slow
down is that it prevents real use of queuing to the storage devices.
On 2016-01-19 22:43:21 +0100, Andres Freund wrote:
On 2016-01-19 12:58:38 -0500, Robert Haas wrote:
If I understand correctly, prior to that commit, WAL writer woke up 5
times per second and flushed just that often (unless you changed the
default settings). But as the commit message explained, that turned
out to suck - you could make performance go up very significantly by
radically decreasing wal_writer_delay. This commit basically lets it
flush at maximum velocity - as fast as we finish one flush, we can
start the next. That must have seemed like a win at the time from the
way the commit message was written, but you seem to now be seeing the
opposite effect, where performance is suffering because flushes are
too frequent rather than too infrequent. I wonder if there's an ideal
flush rate and what it is, and how much it depends on what hardware
you have got.I think the problem isn't really that it's flushing too much WAL in
total, it's that it's flushing WAL in a too granular fashion. I suspect
we want something where we attempt a minimum number of flushes per
second (presumably tied to wal_writer_delay) and, once exceeded, a
minimum number of pages per flush. I think we even could continue to
write() the data at the same rate as today, we just would need to reduce
the number of fdatasync()s we issue. And possibly could make the
eventual fdatasync()s cheaper by hinting the kernel to write them out
earlier.Now the question what the minimum number of pages we want to flush for
(setting wal_writer_delay triggered ones aside) isn't easy to answer. A
simple model would be to statically tie it to the size of wal_buffers;
say, don't flush unless at least 10% of XLogBuffers have been written
since the last flush. More complex approaches would be to measure the
continuous WAL writeout rate.By tying it to both a minimum rate under activity (ensuring things go to
disk fast) and a minimum number of pages to sync (ensuring a reasonable
number of cache flush operations) we should be able to mostly accomodate
the different types of workloads. I think.
This unfortunately leaves out part of the reasoning for the above
commit: We want WAL to be flushed fast, so we immediately can set hint
bits.
One, relatively extreme, approach would be to continue *writing* WAL in
the background writer as today, but use rules like suggested above
guiding the actual flushing. Additionally using operations like
sync_file_range() (and equivalents on other OSs). Then, to address the
regression of SetHintBits() having to bail out more often, actually
trigger a WAL flush whenever WAL is already written, but not flushed.
has the potential to be bad in a number of other cases tho :(
Andres
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 2016-01-20 11:13:26 +0100, Andres Freund wrote:
On 2016-01-19 22:43:21 +0100, Andres Freund wrote:
On 2016-01-19 12:58:38 -0500, Robert Haas wrote:
I think the problem isn't really that it's flushing too much WAL in
total, it's that it's flushing WAL in a too granular fashion. I suspect
we want something where we attempt a minimum number of flushes per
second (presumably tied to wal_writer_delay) and, once exceeded, a
minimum number of pages per flush. I think we even could continue to
write() the data at the same rate as today, we just would need to reduce
the number of fdatasync()s we issue. And possibly could make the
eventual fdatasync()s cheaper by hinting the kernel to write them out
earlier.Now the question what the minimum number of pages we want to flush for
(setting wal_writer_delay triggered ones aside) isn't easy to answer. A
simple model would be to statically tie it to the size of wal_buffers;
say, don't flush unless at least 10% of XLogBuffers have been written
since the last flush. More complex approaches would be to measure the
continuous WAL writeout rate.By tying it to both a minimum rate under activity (ensuring things go to
disk fast) and a minimum number of pages to sync (ensuring a reasonable
number of cache flush operations) we should be able to mostly accomodate
the different types of workloads. I think.This unfortunately leaves out part of the reasoning for the above
commit: We want WAL to be flushed fast, so we immediately can set hint
bits.One, relatively extreme, approach would be to continue *writing* WAL in
the background writer as today, but use rules like suggested above
guiding the actual flushing. Additionally using operations like
sync_file_range() (and equivalents on other OSs). Then, to address the
regression of SetHintBits() having to bail out more often, actually
trigger a WAL flush whenever WAL is already written, but not flushed.
has the potential to be bad in a number of other cases tho :(
Chatting on IM with Heikki, I noticed that we're pretty pessimistic in
SetHintBits(). Namely we don't set the bit if XLogNeedsFlush(commitLSN),
because we can't easily set the LSN. But, it's actually fairly common
that the pages LSN is already newer than the commitLSN - in which case
we, afaics, just can go ahead and set the hint bit, no?
So, instead of
if (XLogNeedsFlush(commitLSN) && BufferIsPermanent(buffer)
return; /* not flushed yet, so don't set hint */
we do
if (BufferIsPermanent(buffer) && XLogNeedsFlush(commitLSN)
&& BufferGetLSNAtomic(buffer) < commitLSN)
return; /* not flushed yet, so don't set hint */
In my tests with pgbench -s 100, 2GB of shared buffers, that's recovers
a large portion of the hint writes that we currently skip.
Right now, on my laptop, I get (-M prepared -c 32 -j 32):
current wal-writer 12827 tps, 95 % IO util, 93 % CPU
no flushing in wal writer * 13185 tps, 46 % IO util, 93 % CPU
no flushing in wal writer & above change 16366 tps, 41 % IO util, 95 % CPU
flushing in wal writer & above change: 14812 tps, 94 % IO util, 95 % CPU
* sometimes the results initially were much lower, with lots of lock
contention. Can't figure out why that's only sometimes the case. In
those cases the results were more like 8967 tps.
these aren't meant as thorough benchmarks, just to provide some
orientation.
Now that solution won't improve every situation, e.g. for a workload
that inserts a lot of rows in one transaction, and only does inserts, it
probably won't do all that much. But it still seems like a pretty good
mitigation strategy. I hope that with a smarter write strategy (getting
that 50% reduction in IO util) and the above we should be ok.
Andres
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Andres Freund wrote:
The relevant thread is at
http://archives.postgresql.org/message-id/CA%2BTgmoaCr3kDPafK5ygYDA9mF9zhObGp_13q0XwkEWsScw6h%3Dw%40mail.gmail.com
what I didn't remember is that I voiced concern back then about exactly this:
http://archives.postgresql.org/message-id/201112011518.29964.andres%40anarazel.de
;)
Interesting. If we consider for a minute that part of the cause for the
slowdown is slowness in pg_clog, maybe we should reconsider the initial
decision to flush as quickly as possible (i.e. adopt a strategy where
walwriter sleeps a bit between two flushes) in light of the group-update
feature for CLOG being proposed by Amit Kapila in another thread -- it
seems that these things might go hand-in-hand.
--
�lvaro Herrera http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 2016-01-20 12:16:24 -0300, Alvaro Herrera wrote:
Andres Freund wrote:
The relevant thread is at
http://archives.postgresql.org/message-id/CA%2BTgmoaCr3kDPafK5ygYDA9mF9zhObGp_13q0XwkEWsScw6h%3Dw%40mail.gmail.com
what I didn't remember is that I voiced concern back then about exactly this:
http://archives.postgresql.org/message-id/201112011518.29964.andres%40anarazel.de
;)Interesting. If we consider for a minute that part of the cause for the
slowdown is slowness in pg_clog, maybe we should reconsider the initial
decision to flush as quickly as possible (i.e. adopt a strategy where
walwriter sleeps a bit between two flushes) in light of the group-update
feature for CLOG being proposed by Amit Kapila in another thread -- it
seems that these things might go hand-in-hand.
I don't think it's strongly related - the contention here is on read
access to the clog, not on write access. While Amit's patch will reduce
the impact of that a bit, I don't see it making a fundamental
difference.
Andres
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Wed, Jan 20, 2016 at 9:07 PM, Andres Freund <andres@anarazel.de> wrote:
On 2016-01-20 12:16:24 -0300, Alvaro Herrera wrote:
Andres Freund wrote:
The relevant thread is at
what I didn't remember is that I voiced concern back then about
exactly this:
http://archives.postgresql.org/message-id/201112011518.29964.andres%40anarazel.de
;)
Interesting. If we consider for a minute that part of the cause for the
slowdown is slowness in pg_clog, maybe we should reconsider the initial
decision to flush as quickly as possible (i.e. adopt a strategy where
walwriter sleeps a bit between two flushes) in light of the group-update
feature for CLOG being proposed by Amit Kapila in another thread -- it
seems that these things might go hand-in-hand.I don't think it's strongly related - the contention here is on read
access to the clog, not on write access.
Aren't reads on clog contended with parallel writes to clog?
With Regards,
Amit Kapila.
EnterpriseDB: http://www.enterprisedb.com
On 2016-01-21 11:33:15 +0530, Amit Kapila wrote:
On Wed, Jan 20, 2016 at 9:07 PM, Andres Freund <andres@anarazel.de> wrote:
I don't think it's strongly related - the contention here is on read
access to the clog, not on write access.Aren't reads on clog contended with parallel writes to clog?
Sure. But you're not going to beat "no access to the clog" due to hint
bits, by making parallel writes a bit better citizens.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Wed, Jan 20, 2016 at 9:02 AM, Andres Freund <andres@anarazel.de> wrote:
Chatting on IM with Heikki, I noticed that we're pretty pessimistic in
SetHintBits(). Namely we don't set the bit if XLogNeedsFlush(commitLSN),
because we can't easily set the LSN. But, it's actually fairly common
that the pages LSN is already newer than the commitLSN - in which case
we, afaics, just can go ahead and set the hint bit, no?So, instead of
if (XLogNeedsFlush(commitLSN) && BufferIsPermanent(buffer)
return; /* not flushed yet, so don't set hint */
we do
if (BufferIsPermanent(buffer) && XLogNeedsFlush(commitLSN)
&& BufferGetLSNAtomic(buffer) < commitLSN)
return; /* not flushed yet, so don't set hint */In my tests with pgbench -s 100, 2GB of shared buffers, that's recovers
a large portion of the hint writes that we currently skip.
Dang. That's a really good idea. Although I think you'd probably
better revise the comment, since it will otherwise be false.
--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
This patch got its fair share of reviewer attention this commitfest.
Moving to the next one. Andres, if you want to commit ahead of time
you're of course encouraged to do so.
--
�lvaro Herrera http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Hi,
Fabien asked me to post a new version of the checkpoint flushing patch
series. While this isn't entirely ready for commit, I think we're
getting closer.
I don't want to post a full series right now, but my working state is
available on
http://git.postgresql.org/gitweb/?p=users/andresfreund/postgres.git;a=shortlog;h=refs/heads/checkpoint-flush
git://git.postgresql.org/git/users/andresfreund/postgres.git checkpoint-flush
The main changes are that:
1) the significant performance regressions I saw are addressed by
changing the wal writer flushing logic
2) The flushing API moved up a couple layers, and now deals with buffer
tags, rather than the physical files
3) Writes from checkpoints, bgwriter and files are flushed, configurable
by individual GUCs. Without that I still saw the spiked in a lot of circumstances.
There's also a more experimental reimplementation of bgwriter, but I'm
not sure it's realistic to polish that up within the constraints of 9.6.
Regards,
Andres
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Hi Fabien,
On 2016-02-04 16:54:58 +0100, Andres Freund wrote:
I don't want to post a full series right now, but my working state is
available on
http://git.postgresql.org/gitweb/?p=users/andresfreund/postgres.git;a=shortlog;h=refs/heads/checkpoint-flush
git://git.postgresql.org/git/users/andresfreund/postgres.git checkpoint-flushThe main changes are that:
1) the significant performance regressions I saw are addressed by
changing the wal writer flushing logic
2) The flushing API moved up a couple layers, and now deals with buffer
tags, rather than the physical files
3) Writes from checkpoints, bgwriter and files are flushed, configurable
by individual GUCs. Without that I still saw the spiked in a lot of circumstances.There's also a more experimental reimplementation of bgwriter, but I'm
not sure it's realistic to polish that up within the constraints of 9.6.
Any comments before I spend more time polishing this? I'm currently
updating docs and comments to actually describe the current state...
Andres
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Hello Andres,
Any comments before I spend more time polishing this?
I'm running tests on various settings, I'll send a report when it is done.
Up to now the performance seems as good as with the previous version.
I'm currently updating docs and comments to actually describe the
current state...
I did notice the mismatched documentation.
I think I would appreciate comments to understand why/how the ringbuffer
is used, and more comments in general, so it is fine if you improve this
part.
Minor details:
"typedefs.list" should be updated to WritebackContext.
"WritebackContext" is a typedef, "struct" is not needed.
I'll look at the code more deeply probably over next weekend.
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 2016-02-08 19:52:30 +0100, Fabien COELHO wrote:
I think I would appreciate comments to understand why/how the ringbuffer is
used, and more comments in general, so it is fine if you improve this part.
I'd suggest to leave out the ringbuffer/new bgwriter parts. I think
they'd be committed separately, and probably not in 9.6.
Thanks,
Andres
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
I think I would appreciate comments to understand why/how the
ringbuffer is used, and more comments in general, so it is fine if you
improve this part.I'd suggest to leave out the ringbuffer/new bgwriter parts.
Ok, so the patch would only onclude the checkpointer stuff.
I'll look at this part in detail.
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On February 9, 2016 10:46:34 AM GMT+01:00, Fabien COELHO <coelho@cri.ensmp.fr> wrote:
I think I would appreciate comments to understand why/how the
ringbuffer is used, and more comments in general, so it is fine ifyou
improve this part.
I'd suggest to leave out the ringbuffer/new bgwriter parts.
Ok, so the patch would only onclude the checkpointer stuff.
I'll look at this part in detail.
Yes, that's the more pressing part. I've seen pretty good results with the new bgwriter, but it's not really worthwhile until sorting and flushing is in...
Andres
---
Please excuse brevity and formatting - I am writing this on my mobile phone.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 2016-02-04 16:54:58 +0100, Andres Freund wrote:
Fabien asked me to post a new version of the checkpoint flushing patch
series. While this isn't entirely ready for commit, I think we're
getting closer.I don't want to post a full series right now, but my working state is
available on
http://git.postgresql.org/gitweb/?p=users/andresfreund/postgres.git;a=shortlog;h=refs/heads/checkpoint-flush
git://git.postgresql.org/git/users/andresfreund/postgres.git checkpoint-flush
The first two commits of the series are pretty close to being ready. I'd
welcome review of those, and I plan to commit them independently of the
rest as they're beneficial independently. The most important bits are
the comments and docs of 0002 - they weren't particularly good
beforehand, so I had to rewrite a fair bit.
0001: Make SetHintBit() a bit more aggressive, afaics that fixes all the
potential regressions of 0002
0002: Fix the overaggressive flushing by the wal writer, by only
flushing every wal_writer_delay ms or wal_writer_flush_after
bytes.
Greetings,
Andres Freund
Attachments:
0001-Allow-SetHintBits-to-succeed-if-the-buffer-s-LSN-is-.patchtext/x-patch; charset=us-asciiDownload
>From f3bc3a7c40c21277331689595814b359c55682dc Mon Sep 17 00:00:00 2001
From: Andres Freund <andres@anarazel.de>
Date: Thu, 11 Feb 2016 19:34:29 +0100
Subject: [PATCH 1/6] Allow SetHintBits() to succeed if the buffer's LSN is new
enough.
Previously we only allowed SetHintBits() to succeed if the commit LSN of
the last transaction touching the page has already been flushed to
disk. We can't generally change the LSN of the page, because we don't
necessarily have the required locks on the page. But the required LSN
interlock does not require the commit record to be flushed, it just
requires that the commit record will be flushed before the page is
written out. Therefore if the buffer LSN is newer than the commit LSN,
the hint bit can be safely set.
In a number of scenarios (e.g. pgbench) this noticeably increases the
number of hint bits are set. But more importantly it also keeps the
success rate up when flushing WAL less frequently. That was the original
reason for commit 4de82f7d7, which has negative performance consequences
in a number of scenarios. This will allow a follup commit to reduce the
flush rate.
Discussion: 20160118163908.GW10941@awork2.anarazel.de
---
src/backend/utils/time/tqual.c | 21 +++++++++++++--------
1 file changed, 13 insertions(+), 8 deletions(-)
diff --git a/src/backend/utils/time/tqual.c b/src/backend/utils/time/tqual.c
index 465933d..503bd1d 100644
--- a/src/backend/utils/time/tqual.c
+++ b/src/backend/utils/time/tqual.c
@@ -89,12 +89,13 @@ static bool XidInMVCCSnapshot(TransactionId xid, Snapshot snapshot);
* Set commit/abort hint bits on a tuple, if appropriate at this time.
*
* It is only safe to set a transaction-committed hint bit if we know the
- * transaction's commit record has been flushed to disk, or if the table is
- * temporary or unlogged and will be obliterated by a crash anyway. We
- * cannot change the LSN of the page here because we may hold only a share
- * lock on the buffer, so we can't use the LSN to interlock this; we have to
- * just refrain from setting the hint bit until some future re-examination
- * of the tuple.
+ * transaction's commit record is guaranteed to be flushed to disk before the
+ * buffer, or if the table is temporary or unlogged and will be obliterated by
+ * a crash anyway. We cannot change the LSN of the page here because we may
+ * hold only a share lock on the buffer, so we can only use the LSN to
+ * interlock this if the buffer's LSN already is newer than the commit LSN;
+ * otherwise we have to just refrain from setting the hint bit until some
+ * future re-examination of the tuple.
*
* We can always set hint bits when marking a transaction aborted. (Some
* code in heapam.c relies on that!)
@@ -122,8 +123,12 @@ SetHintBits(HeapTupleHeader tuple, Buffer buffer,
/* NB: xid must be known committed here! */
XLogRecPtr commitLSN = TransactionIdGetCommitLSN(xid);
- if (XLogNeedsFlush(commitLSN) && BufferIsPermanent(buffer))
- return; /* not flushed yet, so don't set hint */
+ if (BufferIsPermanent(buffer) && XLogNeedsFlush(commitLSN) &&
+ BufferGetLSNAtomic(buffer) < commitLSN)
+ {
+ /* not flushed and no LSN interlock, so don't set hint */
+ return;
+ }
}
tuple->t_infomask |= infomask;
--
2.7.0.229.g701fa7f
0002-Allow-the-WAL-writer-to-flush-WAL-at-a-reduced-rate.patchtext/x-patch; charset=us-asciiDownload
>From e4facce2cf8b982408ff1de174cffc202852adfd Mon Sep 17 00:00:00 2001
From: Andres Freund <andres@anarazel.de>
Date: Thu, 11 Feb 2016 19:34:29 +0100
Subject: [PATCH 2/6] Allow the WAL writer to flush WAL at a reduced rate.
Commit 4de82f7d7 increased the WAL flush rate, mainly to increase the
likelihood that hint bits can be set quickly. More quickly set hint bits
can reduce contention around the clog et al. But unfortunately the
increased flush rate can have a significant negative performance impact,
I have measured up to a factor of ~4. The reason for this slowdown is
that if there are independent writes to the underlying devices, for
example because shared buffers is a lot smaller than the hot data set,
or because a checkpoint is ongoing, the fdatasync() calls force barriers
to be emitted to the storage.
This is achieved by flushing WAL only if the last flush was longer than
wal_writer_delay ago, or if more than wal_writer_flush_after (new GUC)
unflushed blocks are pending.
Discussion: 20160118163908.GW10941@awork2.anarazel.de
---
doc/src/sgml/config.sgml | 41 +++++++---
src/backend/access/transam/README | 32 ++++----
src/backend/access/transam/xlog.c | 104 +++++++++++++++++++-------
src/backend/postmaster/walwriter.c | 1 +
src/backend/utils/misc/guc.c | 13 +++-
src/backend/utils/misc/postgresql.conf.sample | 1 +
src/include/postmaster/walwriter.h | 1 +
7 files changed, 141 insertions(+), 52 deletions(-)
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index de84b77..ee8d63d 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -2344,15 +2344,38 @@ include_dir 'conf.d'
</indexterm>
</term>
<listitem>
- <para>
- Specifies the delay between activity rounds for the WAL writer.
- In each round the writer will flush WAL to disk. It then sleeps for
- <varname>wal_writer_delay</> milliseconds, and repeats. The default
- value is 200 milliseconds (<literal>200ms</>). Note that on many
- systems, the effective resolution of sleep delays is 10 milliseconds;
- setting <varname>wal_writer_delay</> to a value that is not a multiple
- of 10 might have the same results as setting it to the next higher
- multiple of 10. This parameter can only be set in the
+ <para>
+ Specifies how often the WAL writer flushes WAL. After flushing WAL it
+ sleeps for <varname>wal_writer_delay</> milliseconds, unless woken up
+ by an asynchronously committing transaction. In case the last flush
+ happened less than <varname>wal_writer_delay</> milliseconds ago and
+ less than <varname>wal_writer_flush_after</> bytes of of WAL been
+ produced since, WAL is only written to the OS, not flushed to disk.
+ The default value is 200 milliseconds (<literal>200ms</>). Note that
+ on many systems, the effective resolution of sleep delays is 10
+ milliseconds; setting <varname>wal_writer_delay</> to a value that is
+ not a multiple of 10 might have the same results as setting it to the
+ next higher multiple of 10. This parameter can only be set in the
+ <filename>postgresql.conf</> file or on the server command line.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry id="guc-wal-writer-flush-after" xreflabel="wal_writer_flush_after">
+ <term><varname>wal_writer_flush_after</varname> (<type>integer</type>)
+ <indexterm>
+ <primary><varname>wal_writer_flush_after</> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ Specifies how often the WAL writer flushes WAL. In case the last flush
+ happened less than <varname>wal_writer_delay</> milliseconds ago and
+ less than <varname>wal_writer_flush_after</> bytes of WAL have been
+ produced since, WAL is only written to the OS, not flushed to disk.
+ If <varname>wal_writer_flush_after</> is set to <literal>0</> WAL is
+ flushed everytime the WAL writer has written WAL. The default is
+ <literal>1MB</literal>. This parameter can only be set in the
<filename>postgresql.conf</> file or on the server command line.
</para>
</listitem>
diff --git a/src/backend/access/transam/README b/src/backend/access/transam/README
index f6db580..2de0489 100644
--- a/src/backend/access/transam/README
+++ b/src/backend/access/transam/README
@@ -736,20 +736,24 @@ non-roll-backable side effects (such as filesystem changes) force sync
commit to minimize the window in which the filesystem change has been made
but the transaction isn't guaranteed committed.
-Every wal_writer_delay milliseconds, the walwriter process performs an
-XLogBackgroundFlush(). This checks the location of the last completely
-filled WAL page. If that has moved forwards, then we write all the changed
-buffers up to that point, so that under full load we write only whole
-buffers. If there has been a break in activity and the current WAL page is
-the same as before, then we find out the LSN of the most recent
-asynchronous commit, and flush up to that point, if required (i.e.,
-if it's in the current WAL page). This arrangement in itself would
-guarantee that an async commit record reaches disk during at worst the
-second walwriter cycle after the transaction completes. However, we also
-allow XLogFlush to flush full buffers "flexibly" (ie, not wrapping around
-at the end of the circular WAL buffer area), so as to minimize the number
-of writes issued under high load when multiple WAL pages are filled per
-walwriter cycle. This makes the worst-case delay three walwriter cycles.
+The walwriter regularly wakes up (via wal_writer_delay) or is woken up
+(via its latch, which is set by backends committing asynchronously) and
+performs an XLogBackgroundFlush(). This checks the location of the last
+completely filled WAL page. If that has moved forwards, then we write all
+the changed buffers up to that point, so that under full load we write
+only whole buffers. If there has been a break in activity and the current
+WAL page is the same as before, then we find out the LSN of the most
+recent asynchronous commit, and write up to that point, if required (i.e.
+if it's in the current WAL page). If more than wal_writer_delay has
+passed, or more than wal_writer_flush_after blocks have been written, since
+the last flush, WAL is also flushed up to the current location. This
+arrangement in itself would guarantee that an async commit record reaches
+disk after at most two times wal_writer_delay after the transaction
+completes. However, we also allow XLogFlush to write/flush full buffers
+"flexibly" (ie, not wrapping around at the end of the circular WAL buffer
+area), so as to minimize the number of writes issued under high load when
+multiple WAL pages are filled per walwriter cycle. This makes the worst-case
+delay three wal_writer_delay cycles.
There are some other subtle points to consider with asynchronous commits.
First, for each page of CLOG we must remember the LSN of the latest commit
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index a2846c4..32e7ef2 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -42,6 +42,7 @@
#include "miscadmin.h"
#include "pgstat.h"
#include "postmaster/bgwriter.h"
+#include "postmaster/walwriter.h"
#include "postmaster/startup.h"
#include "replication/basebackup.h"
#include "replication/logical.h"
@@ -2729,28 +2730,37 @@ XLogFlush(XLogRecPtr record)
}
/*
- * Flush xlog, but without specifying exactly where to flush to.
+ * Write & flush xlog, but without specifying exactly where to.
*
- * We normally flush only completed blocks; but if there is nothing to do on
- * that basis, we check for unflushed async commits in the current incomplete
- * block, and flush through the latest one of those. Thus, if async commits
- * are not being used, we will flush complete blocks only. We can guarantee
- * that async commits reach disk after at most three cycles; normally only
- * one or two. (When flushing complete blocks, we allow XLogWrite to write
- * "flexibly", meaning it can stop at the end of the buffer ring; this makes a
- * difference only with very high load or long wal_writer_delay, but imposes
- * one extra cycle for the worst case for async commits.)
+ * We normally write only completed blocks; but if there is nothing to do on
+ * that basis, we check for unwritten async commits in the current incomplete
+ * block, and write through the latest one of those. Thus, if async commits
+ * are not being used, we will write complete blocks only.
+ *
+ * If, based on the above, there's anything to write we do so immediately. But
+ * to avoid calling fsync, fdatasync et. al. at a rate that'd impact
+ * concurrent IO, we only flush WAL every wal_writer_delay ms, or if there's
+ * more than wal_writer_flush_after unflushed blocks.
+ *
+ * We can guarantee that async commits reach disk after at most three
+ * wal_writer_delay cycles. (When flushing complete blocks, we allow XLogWrite
+ * to write "flexibly", meaning it can stop at the end of the buffer ring;
+ * this makes a difference only with very high load or long wal_writer_delay,
+ * but imposes one extra cycle for the worst case for async commits.)
*
* This routine is invoked periodically by the background walwriter process.
*
- * Returns TRUE if we flushed anything.
+ * Returns TRUE if there was any work to do, even if we skipped flushing due
+ * to wal_writer_delay/wal_flush_after.
*/
bool
XLogBackgroundFlush(void)
{
- XLogRecPtr WriteRqstPtr;
+ XLogwrtRqst WriteRqst;
bool flexible = true;
- bool wrote_something = false;
+ static TimestampTz lastflush;
+ TimestampTz now;
+ int flushbytes;
/* XLOG doesn't need flushing during recovery */
if (RecoveryInProgress())
@@ -2759,17 +2769,17 @@ XLogBackgroundFlush(void)
/* read LogwrtResult and update local state */
SpinLockAcquire(&XLogCtl->info_lck);
LogwrtResult = XLogCtl->LogwrtResult;
- WriteRqstPtr = XLogCtl->LogwrtRqst.Write;
+ WriteRqst = XLogCtl->LogwrtRqst;
SpinLockRelease(&XLogCtl->info_lck);
/* back off to last completed page boundary */
- WriteRqstPtr -= WriteRqstPtr % XLOG_BLCKSZ;
+ WriteRqst.Write -= WriteRqst.Write % XLOG_BLCKSZ;
/* if we have already flushed that far, consider async commit records */
- if (WriteRqstPtr <= LogwrtResult.Flush)
+ if (WriteRqst.Write <= LogwrtResult.Flush)
{
SpinLockAcquire(&XLogCtl->info_lck);
- WriteRqstPtr = XLogCtl->asyncXactLSN;
+ WriteRqst.Write = XLogCtl->asyncXactLSN;
SpinLockRelease(&XLogCtl->info_lck);
flexible = false; /* ensure it all gets written */
}
@@ -2779,7 +2789,7 @@ XLogBackgroundFlush(void)
* holding an open file handle to a logfile that's no longer in use,
* preventing the file from being deleted.
*/
- if (WriteRqstPtr <= LogwrtResult.Flush)
+ if (WriteRqst.Write <= LogwrtResult.Flush)
{
if (openLogFile >= 0)
{
@@ -2791,10 +2801,47 @@ XLogBackgroundFlush(void)
return false;
}
+ /*
+ * Determine how far to flush WAL, based on the wal_writer_delay and
+ * wal_writer_flush_after GUCs.
+ */
+ now = GetCurrentTimestamp();
+ flushbytes =
+ WriteRqst.Write / XLOG_BLCKSZ - LogwrtResult.Flush / XLOG_BLCKSZ;
+
+ if (WalWriterFlushAfter == 0 || lastflush == 0)
+ {
+ /* first call, or block based limits disabled */
+ WriteRqst.Flush = WriteRqst.Write;
+ lastflush = now;
+ }
+ else if (TimestampDifferenceExceeds(lastflush, now, WalWriterDelay))
+ {
+ /*
+ * Flush the writes at least every WalWriteDelay ms. This is important
+ * to bound the amount of time it takes for an asynchronous commit to
+ * hit disk.
+ */
+ WriteRqst.Flush = WriteRqst.Write;
+ lastflush = now;
+ }
+ else if (flushbytes >= WalWriterFlushAfter)
+ {
+ /* exceeded wal_writer_flush_after blocks, flush */
+ WriteRqst.Flush = WriteRqst.Write;
+ lastflush = now;
+ }
+ else
+ {
+ /* no flushing, this time round */
+ WriteRqst.Flush = 0;
+ }
+
#ifdef WAL_DEBUG
if (XLOG_DEBUG)
- elog(LOG, "xlog bg flush request %X/%X; write %X/%X; flush %X/%X",
- (uint32) (WriteRqstPtr >> 32), (uint32) WriteRqstPtr,
+ elog(LOG, "xlog bg flush request write %X/%X; flush: %X/%X, current is write %X/%X; flush %X/%X",
+ (uint32) (WriteRqst.Write >> 32), (uint32) WriteRqst.Write,
+ (uint32) (WriteRqst.Flush >> 32), (uint32) WriteRqst.Flush,
(uint32) (LogwrtResult.Write >> 32), (uint32) LogwrtResult.Write,
(uint32) (LogwrtResult.Flush >> 32), (uint32) LogwrtResult.Flush);
#endif
@@ -2802,17 +2849,13 @@ XLogBackgroundFlush(void)
START_CRIT_SECTION();
/* now wait for any in-progress insertions to finish and get write lock */
- WaitXLogInsertionsToFinish(WriteRqstPtr);
+ WaitXLogInsertionsToFinish(WriteRqst.Write);
LWLockAcquire(WALWriteLock, LW_EXCLUSIVE);
LogwrtResult = XLogCtl->LogwrtResult;
- if (WriteRqstPtr > LogwrtResult.Flush)
+ if (WriteRqst.Write > LogwrtResult.Write ||
+ WriteRqst.Flush > LogwrtResult.Flush)
{
- XLogwrtRqst WriteRqst;
-
- WriteRqst.Write = WriteRqstPtr;
- WriteRqst.Flush = WriteRqstPtr;
XLogWrite(WriteRqst, flexible);
- wrote_something = true;
}
LWLockRelease(WALWriteLock);
@@ -2827,7 +2870,12 @@ XLogBackgroundFlush(void)
*/
AdvanceXLInsertBuffer(InvalidXLogRecPtr, true);
- return wrote_something;
+ /*
+ * If we determined that we need to write data, but somebody else
+ * wrote/flushed already, it should be considered as being active, to
+ * avoid hibernating too early.
+ */
+ return true;
}
/*
diff --git a/src/backend/postmaster/walwriter.c b/src/backend/postmaster/walwriter.c
index 243adb6..9852fed 100644
--- a/src/backend/postmaster/walwriter.c
+++ b/src/backend/postmaster/walwriter.c
@@ -64,6 +64,7 @@
* GUC parameters
*/
int WalWriterDelay = 200;
+int WalWriterFlushAfter = 128;
/*
* Number of do-nothing loops before lengthening the delay time, and the
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index 31a69ca..ea5a09a 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -2235,7 +2235,7 @@ static struct config_int ConfigureNamesInt[] =
{
{"wal_writer_delay", PGC_SIGHUP, WAL_SETTINGS,
- gettext_noop("WAL writer sleep time between WAL flushes."),
+ gettext_noop("Time between WAL flushes performed in the WAL writer."),
NULL,
GUC_UNIT_MS
},
@@ -2245,6 +2245,17 @@ static struct config_int ConfigureNamesInt[] =
},
{
+ {"wal_writer_flush_after", PGC_SIGHUP, WAL_SETTINGS,
+ gettext_noop("Amount of WAL written out by WAL writer triggering a flush."),
+ NULL,
+ GUC_UNIT_XBLOCKS
+ },
+ &WalWriterFlushAfter,
+ 128, 0, INT_MAX,
+ NULL, NULL, NULL
+ },
+
+ {
/* see max_connections */
{"max_wal_senders", PGC_POSTMASTER, REPLICATION_SENDING,
gettext_noop("Sets the maximum number of simultaneously running WAL sender processes."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 09b2003..ee3d378 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -192,6 +192,7 @@
#wal_buffers = -1 # min 32kB, -1 sets based on shared_buffers
# (change requires restart)
#wal_writer_delay = 200ms # 1-10000 milliseconds
+#wal_writer_flush_after = 1MB # 0 disables
#commit_delay = 0 # range 0-100000, in microseconds
#commit_siblings = 5 # range 1-1000
diff --git a/src/include/postmaster/walwriter.h b/src/include/postmaster/walwriter.h
index d94cb97..49c5c1d 100644
--- a/src/include/postmaster/walwriter.h
+++ b/src/include/postmaster/walwriter.h
@@ -14,6 +14,7 @@
/* GUC options */
extern int WalWriterDelay;
+extern int WalWriterFlushAfter;
extern void WalWriterMain(void) pg_attribute_noreturn();
--
2.7.0.229.g701fa7f
On Thu, Feb 11, 2016 at 1:44 PM, Andres Freund <andres@anarazel.de> wrote:
On 2016-02-04 16:54:58 +0100, Andres Freund wrote:
Fabien asked me to post a new version of the checkpoint flushing patch
series. While this isn't entirely ready for commit, I think we're
getting closer.I don't want to post a full series right now, but my working state is
available on
http://git.postgresql.org/gitweb/?p=users/andresfreund/postgres.git;a=shortlog;h=refs/heads/checkpoint-flush
git://git.postgresql.org/git/users/andresfreund/postgres.git checkpoint-flushThe first two commits of the series are pretty close to being ready. I'd
welcome review of those, and I plan to commit them independently of the
rest as they're beneficial independently. The most important bits are
the comments and docs of 0002 - they weren't particularly good
beforehand, so I had to rewrite a fair bit.0001: Make SetHintBit() a bit more aggressive, afaics that fixes all the
potential regressions of 0002
0002: Fix the overaggressive flushing by the wal writer, by only
flushing every wal_writer_delay ms or wal_writer_flush_after
bytes.
I previously reviewed 0001 and I think it's fine. I haven't reviewed
0002 in detail, but I like the concept.
--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Hello Andres,
0001: Make SetHintBit() a bit more aggressive, afaics that fixes all the
potential regressions of 0002
0002: Fix the overaggressive flushing by the wal writer, by only
flushing every wal_writer_delay ms or wal_writer_flush_after
bytes.
I've looked at these patches, especially the whole bench of explanations
and comments which is a good source for understanding what is going on in
the WAL writer, a part of pg I'm not familiar with.
When reading the patch 0002 explanations, I had the following comments:
AFAICS, there are several levels of actions when writing things in pg:
0: the thing is written in some internal buffer
1: the buffer is advised to be passed to the OS (hint bits?)
2: the buffer is actually passed to the OS (write, flush)
3: the OS is advised to send the written data to the io subsystem
(sync_file_range with SYNC_FILE_RANGE_WRITE)
4: the OS is required to send the written data to the disk
(fsync, sync_file_range with SYNC_FILE_RANGE_WAIT_AFTER)
It is not clear when reading the text which level is discussed. In
particular, I'm not sure that "flush" refers to level 2, which is
misleading. When reading the description, I'm rather under the impression
that it is about level 4, but then if actual fsync are performed every 200
ms then the tps would be very low...
After more considerations, my final understanding is that this behavior
only occurs with "asynchronous commit", aka a situation when COMMIT does
not wait for data to be really fsynced, but the fsync is to occur within
some delay so it will not be too far away, some kind of compromise for
performance where commits can be lost.
Now all this is somehow alien to me because the whole point of committing
is having the data to disk, and I would not consider a database to be safe
if commit does not imply fsync, but I understand that people may have to
compromise for performance.
Is my understanding right?
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 2016-02-11 19:44:25 +0100, Andres Freund wrote:
The first two commits of the series are pretty close to being ready. I'd
welcome review of those, and I plan to commit them independently of the
rest as they're beneficial independently. The most important bits are
the comments and docs of 0002 - they weren't particularly good
beforehand, so I had to rewrite a fair bit.0001: Make SetHintBit() a bit more aggressive, afaics that fixes all the
potential regressions of 0002
0002: Fix the overaggressive flushing by the wal writer, by only
flushing every wal_writer_delay ms or wal_writer_flush_after
bytes.
I've pushed these after some more polishing, now working on the next
two.
Greetings,
Andres Freund
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 2016-02-18 09:51:20 +0100, Fabien COELHO wrote:
I've looked at these patches, especially the whole bench of explanations and
comments which is a good source for understanding what is going on in the
WAL writer, a part of pg I'm not familiar with.When reading the patch 0002 explanations, I had the following comments:
AFAICS, there are several levels of actions when writing things in pg:
0: the thing is written in some internal buffer
1: the buffer is advised to be passed to the OS (hint bits?)
Hint bits aren't related to OS writes. They're about information like
'this transaction committed' or 'all tuples on this page are visible'.
2: the buffer is actually passed to the OS (write, flush)
3: the OS is advised to send the written data to the io subsystem
(sync_file_range with SYNC_FILE_RANGE_WRITE)4: the OS is required to send the written data to the disk
(fsync, sync_file_range with SYNC_FILE_RANGE_WAIT_AFTER)
We can't easily rely on sync_file_range(SYNC_FILE_RANGE_WAIT_AFTER) -
the guarantees it gives aren't well defined, and actually changed across
releases.
0002 is about something different, it's about the WAL writer. Which
writes WAL to disk, so individual backends don't have to. It does so in
the background every wal_writer_delay or whenever a tranasaction
asynchronously commits. The reason this interacts with checkpoint
flushing is that, when we flush writes on a regular pace, the writes by
the checkpointer happen inbetween the very frequent writes/fdatasync()
by the WAL writer. That means the disk's caches are flushed every
fdatasync() - which causes considerable slowdowns. On a decent SSD the
WAL writer, before this patch, often did 500-1000 fdatasync()s a second;
the regular sync_file_range calls slowed down things too much.
That's what caused the large regression when using checkpoint
sorting/flushing with synchronous_commit=off. With that fixed - often a
performance improvement on its own - I don't see that regression anymore.
After more considerations, my final understanding is that this behavior only
occurs with "asynchronous commit", aka a situation when COMMIT does not wait
for data to be really fsynced, but the fsync is to occur within some delay
so it will not be too far away, some kind of compromise for performance
where commits can be lost.
Right.
Now all this is somehow alien to me because the whole point of committing is
having the data to disk, and I would not consider a database to be safe if
commit does not imply fsync, but I understand that people may have to
compromise for performance.
It's obviously not applicable for every scenario, but in a *lot* of
real-world scenario a sub-second loss window doesn't have any actual
negative implications.
Andres
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Hello Andres,
I don't want to post a full series right now, but my working state is
available on
http://git.postgresql.org/gitweb/?p=users/andresfreund/postgres.git;a=shortlog;h=refs/heads/checkpoint-flush
git://git.postgresql.org/git/users/andresfreund/postgres.git checkpoint-flush
Below the results of a lot of tests with pgbench to exercise checkpoints
on the above version when fetched.
Overall comments:
- sorting & flushing is basically always a winner
- benchmarking with short runs on large databases is a bad idea
the results are very different if a longer run is used
(see andres00b vs andres00c)
# HOST/SOFT
16 GB 2 cpu 8 cores
200 GB RAID1 HDD, ext4 FS
Ubuntu 12.04 LTS (precise)
# ABOUT THE REPORTED STATISTICS
tps: is the "excluding connection" time tps, the higher the better
1-sec tps: average of measured per-second tps
note - it should be the same as the previous one, but due to various
hazards in the trace, especially when things go badly and pg get
stuck, it may be different. Such hazard also explain why there
may be some non-integer tps reported for some seconds.
stddev: standard deviation, the lower the better
the five figures in bracket give a feel of the distribution:
- min: minimal per-second tps seen in the trace
- q1: first quarter per-second tps seen in the trace
- med: median per-second tps seen in the trace
- q3: third quarter per-second tps seen in the trace
- max: maximal per-second tps seen in the trace
the last percentage dubbed "<=10.0" is percent of seconds where performance
is below 10 tps: this measures of how unresponsive pg was during the run
###### TINY2
pgbench -M prepared -N -P 1 -T 4000 -j 2 -c 4
with scale = 10 (~ 200 MB)
postgresql.conf:
shared_buffers = 1GB
max_wal_size = 1GB
checkpoint_timeout = 300s
checkpoint_completion_target = 0.8
checkpoint_flush_after = { none, 0, 32, 64 }
opts # | tps / 1-sec tps ᅵ stddev [ min q1 med q2 max ] <=10.0
head 0 | 2574.1 / 2574.3 ᅵ 367.4 [229.0, 2570.1, 2721.9, 2746.1, 2857.2] 0.0%
1 | 2575.0 / 2575.1 ᅵ 359.3 [ 1.0, 2595.9, 2712.0, 2732.0, 2847.0] 0.1%
2 | 2602.6 / 2602.7 ᅵ 359.5 [ 54.0, 2607.1, 2735.1, 2768.1, 2908.0] 0.0%
0 0 | 2583.2 / 2583.7 ᅵ 296.4 [164.0, 2580.0, 2690.0, 2717.1, 2833.8] 0.0%
1 | 2596.6 / 2596.9 ᅵ 307.4 [296.0, 2590.5, 2707.9, 2738.0, 2847.8] 0.0%
2 | 2604.8 / 2605.0 ᅵ 300.5 [110.9, 2619.1, 2712.4, 2738.1, 2849.1] 0.0%
32 0 | 2625.5 / 2625.5 ᅵ 250.5 [ 1.0, 2645.9, 2692.0, 2719.9, 2839.0] 0.1%
1 | 2630.2 / 2630.2 ᅵ 243.1 [301.8, 2654.9, 2697.2, 2726.0, 2837.4] 0.0%
2 | 2648.3 / 2648.4 ᅵ 236.7 [570.1, 2664.4, 2708.9, 2739.0, 2844.9] 0.0%
64 0 | 2587.8 / 2587.9 ᅵ 306.1 [ 83.0, 2610.1, 2680.0, 2731.0, 2857.1] 0.0%
1 | 2591.1 / 2591.1 ᅵ 305.2 [455.9, 2608.9, 2680.2, 2734.1, 2859.0] 0.0%
2 | 2047.8 / 2046.4 ᅵ 925.8 [ 0.0, 1486.2, 2592.6, 2691.1, 3001.0] 0.2% ?
Pretty small setup, all data fit in buffers. Good tps performance all around
(best for 32 flushes), and flushing shows a noticable (360 -> 240) reduction
in tps stddev.
###### SMALL
pgbench -M prepared -N -P 1 -T 4000 -j 2 -c 4
with scale = 120 (~ 2 GB)
postgresql.conf:
shared_buffers = 2GB
checkpoint_timeout = 300s
checkpoint_completion_target = 0.8
checkpoint_flush_after = { none, 0, 32, 64 }
opts # | tps / 1-sec tps ᅵ stddev [ min q1 med q2 max ] <=10.0
head 0 | 209.2 / 204.2 ᅵ 516.5 [0.0, 0.0, 4.0, 5.0, 2251.0] 82.3%
1 | 207.4 / 204.2 ᅵ 518.7 [0.0, 0.0, 4.0, 5.0, 2245.1] 82.3%
2 | 217.5 / 211.0 ᅵ 530.3 [0.0, 0.0, 3.0, 5.0, 2255.0] 82.0%
3 | 217.8 / 213.2 ᅵ 531.7 [0.0, 0.0, 4.0, 6.0, 2261.9] 81.7%
4 | 230.7 / 223.9 ᅵ 542.7 [0.0, 0.0, 4.0, 7.0, 2282.0] 80.7%
0 0 | 734.8 / 735.5 ᅵ 879.9 [0.0, 1.0, 16.5, 1748.3, 2281.1] 47.0%
1 | 694.9 / 693.0 ᅵ 849.0 [0.0, 1.0, 29.5, 1545.7, 2428.0] 46.4%
2 | 735.3 / 735.5 ᅵ 888.4 [0.0, 0.0, 12.0, 1781.2, 2312.1] 47.9%
3 | 736.0 / 737.5 ᅵ 887.1 [0.0, 1.0, 16.0, 1794.3, 2317.0] 47.5%
4 | 734.9 / 735.1 ᅵ 885.1 [0.0, 1.0, 15.5, 1781.0, 2297.1] 47.2%
32 0 | 738.1 / 737.9 ᅵ 415.8 [0.0, 553.0, 679.0, 753.0, 2312.1] 0.2%
1 | 730.5 / 730.7 ᅵ 413.2 [0.0, 546.5, 671.0, 744.0, 2319.0] 0.1%
2 | 741.9 / 741.9 ᅵ 416.5 [0.0, 556.0, 682.0, 756.0, 2331.0] 0.2%
3 | 744.1 / 744.1 ᅵ 414.4 [0.0, 555.5, 685.2, 758.0, 2285.1] 0.1%
4 | 746.9 / 746.9 ᅵ 416.6 [0.0, 566.6, 685.0, 759.0, 2308.1] 0.1%
64 0 | 743.0 / 743.1 ᅵ 416.5 [1.0, 555.0, 683.0, 759.0, 2353.0] 0.1%
1 | 742.5 / 742.5 ᅵ 415.6 [0.0, 558.2, 680.0, 758.2, 2296.0] 0.1%
2 | 742.5 / 742.5 ᅵ 415.9 [0.0, 559.0, 681.1, 757.0, 2310.0] 0.1%
3 | 529.0 / 526.6 ᅵ 410.9 [0.0, 245.0, 444.0, 701.0, 2380.9] 1.5% ??
4 | 734.8 / 735.0 ᅵ 414.1 [0.0, 550.0, 673.0, 754.0, 2298.0] 0.1%
Sorting brings * 3.3 tps, flushing significantly reduces tps stddev.
Pg comes from 80% unresponsive to nearly always responsive.
###### MEDIUM
pgbench: -M prepared -N -P 1 -T 4000 -j 2 -c 4
with scale = 250 (~ 3.8 GB)
postgresql.conf:
shared_buffers = 4GB
max_wal_size = 4GB
checkpoint_timeout = 15min
checkpoint_completion_target = 0.8
checkpoint_flush_after = { none, 0, 32, 64 }
opts # | tps / 1-sec tps ᅵ stddev [ min q1 med q2 max ] <=10.0
head 0 | 214.8 / 211.8 ᅵ 513.7 [0.0, 1.0, 4.0, 5.0, 2344.0] 82.4%
1 | 219.2 / 215.0 ᅵ 524.1 [0.0, 0.0, 4.0, 5.0, 2316.0] 82.2%
2 | 240.9 / 234.6 ᅵ 550.8 [0.0, 0.0, 4.0, 6.0, 2320.2] 81.0%
0 0 | 1064.7 / 1065.3 ᅵ 888.2 [0.0, 11.0, 1089.0, 2017.7, 2461.9] 24.7%
1 | 1060.2 / 1061.2 ᅵ 889.9 [0.0, 10.0, 1056.7, 2022.0, 2444.9] 25.1%
2 | 1060.2 / 1061.4 ᅵ 889.1 [0.0, 9.0, 1085.8, 2002.8, 2473.0] 25.6%
32 0 | 1059.4 / 1059.4 ᅵ 476.3 [3.0, 804.9, 980.0, 1123.0, 2448.1] 0.1%
1 | 1062.5 / 1062.6 ᅵ 475.6 [0.0, 807.0, 988.0, 1132.0, 2441.0] 0.1%
2 | 1063.7 / 1063.7 ᅵ 475.4 [0.0, 814.0, 987.0, 1131.2, 2432.1] 0.1%
64 0 | 1052.6 / 1052.6 ᅵ 475.3 [0.0, 793.0, 974.0, 1118.1, 2445.1] 0.1%
1 | 1059.8 / 1059.8 ᅵ 475.1 [0.0, 799.0, 987.5, 1131.0, 2457.1] 0.1%
2 | 1058.5 / 1058.5 ᅵ 472.8 [0.0, 807.0, 985.0, 1127.7, 2442.0] 0.1%
Sorting brings * 4.8 tps, flushing significantly reduces tps stddev.
Pg comes from +80% unresponsive to nearly always responsive.
Performance is significantly better than "small" above, probably thanks to
the longer checkpoint timeout.
###### LARGE
pgbench -M prepared -N -P 1 -T 7500 -j 2 -c 4
with scale = 1000 (~ 15 GB)
postgresql.conf:
shared_buffers = 4GB
max_wal_size = 2GB
checkpoint_timeout = 40min
checkpoint_completion_target = 0.8
checkpoint_flush_after = { none, 0, 32, 64}
opts # | tps / 1-sec tps ᅵ stddev [ min q1 med q2 max ] <=10.0
head 0 | 68.7 / 65.3 ᅵ 78.6 [0.0, 3.0, 6.0, 136.0, 291.0] 53.1%
1 | 70.6 / 70.3 ᅵ 80.1 [0.0, 4.0, 10.0, 151.0, 282.0] 50.1%
2 | 74.3 / 75.8 ᅵ 84.9 [0.0, 4.0, 9.0, 162.0, 311.2] 50.3%
0 0 | 117.2 / 116.9 ᅵ 83.8 [0.0, 14.0, 139.0, 193.0, 372.4] 24.0%
1 | 117.3 / 117.8 ᅵ 83.8 [0.0, 16.0, 140.0, 193.0, 279.0] 23.9%
2 | 117.6 / 118.2 ᅵ 84.1 [0.0, 16.0, 141.0, 194.0, 297.8] 23.7%
32 0 | 114.2 / 114.2 ᅵ 45.7 [0.0, 84.0, 100.0, 131.0, 613.6] 0.4%
1 | 112.5 / 112.6 ᅵ 44.0 [0.0, 83.0, 98.0, 130.0, 293.0] 0.2%
2 | 108.0 / 108.0 ᅵ 44.7 [0.0, 79.0, 94.0, 124.0, 303.6] 0.3%
64 0 | 113.0 / 113.0 ᅵ 45.5 [0.0, 83.0, 99.0, 131.0, 289.0] 0.4%
1 | 80.0 / 80.3 ᅵ 39.1 [0.0, 56.0, 72.0, 95.0, 281.0] 0.8% ??
2 | 112.2 / 112.3 ᅵ 44.5 [0.0, 82.0, 99.0, 129.0, 282.0] 0.3%
Data do not fit in the available memory, so plenty of read accesses.
Sorting still has some impact on tps performance (* 1.6), flushing
greatly improves responsiveness.
###### ANDRES00
pgbench -M prepared -N -P 1 -T 300 -c 16 -j 16
with scale = 800 (~ 13 GB)
postgresql.conf:
shared_buffers = 2GB
max_wal_size = 100GB
wal_level = hot_standby
maintenance_work_mem = 2GB
checkpoint_timeout = 30s
checkpoint_completion_target = 0.8
synchronous_commit = off
checkpoint_flush_after = { none, 0, 32, 64 }
opts # | tps / 1-sec tps ᅵ stddev [ min q1 med q2 max ] <=10.0
head 0 | 328.7 / 329.9 ᅵ 716.9 [0.0, 0.0, 0.0, 0.0, 3221.2] 77.7%
1 | 338.2 / 338.7 ᅵ 728.6 [0.0, 0.0, 0.0, 17.0, 3296.3] 75.0%
2 | 304.5 / 304.3 ᅵ 705.5 [0.0, 0.0, 0.0, 0.0, 3463.4] 79.3%
0 0 | 425.6 / 464.0 ᅵ 724.0 [0.0, 0.0, 0.0, 1000.6, 3363.7] 61.0%
1 | 461.5 / 463.1 ᅵ 735.8 [0.0, 0.0, 0.0, 1011.2, 3490.9] 58.7%
2 | 452.4 / 452.6 ᅵ 744.3 [0.0, 0.0, 0.0, 1078.9, 3631.9] 63.3%
32 0 | 514.4 / 515.8 ᅵ 651.8 [0.0, 0.0, 337.4, 808.3, 2876.0] 40.7%
1 | 512.0 / 514.6 ᅵ 661.6 [0.0, 0.0, 317.6, 690.8, 3315.8] 35.0%
2 | 529.5 / 530.3 ᅵ 673.0 [0.0, 0.0, 321.1, 906.4, 3360.8] 40.3%
64 0 | 529.6 / 530.9 ᅵ 668.2 [0.0, 0.0, 322.1, 786.1, 3538.0] 33.3%
1 | 496.4 / 498.0 ᅵ 606.6 [0.0, 0.0, 321.4, 746.0, 2629.6] 36.3%
2 | 521.0 / 521.7 ᅵ 657.0 [0.0, 0.0, 328.4, 737.9, 3262.9] 34.3%
Data just hold in memory, maybe. Run is very short, settings are low, this
is not representative of an sane installation, this is for testing a lot of
checkpoints in a difficult situation. Sorting and flushing do bring
significant benefits.
###### ANDRES00b (same as ANDRES00 but scale 800->1000)
pgbench -M prepared -N -P 1 -T 300 -c 16 -j 16
with scale = 1000 (~ 15 GB)
postgresql.conf:
shared_buffers = 2GB
max_wal_size = 100GB
wal_level = hot_standby
maintenance_work_mem = 2GB
checkpoint_timeout = 30s
checkpoint_completion_target = 0.8
synchronous_commit = off
checkpoint_flush_after = { none, 0, 32, 64 }
opts # | tps / 1-sec tps ᅵ stddev [ min q1 med q2 max ] <=10.0
head 0 | 150.2 / 150.3 ᅵ 401.6 [0.0, 0.0, 0.0, 0.0, 2199.4] 75.1%
1 | 139.2 / 139.2 ᅵ 372.2 [0.0, 0.0, 0.0, 0.0, 2111.4] 78.3% ***
2 | 127.3 / 127.1 ᅵ 341.2 [0.0, 0.0, 0.0, 53.0, 2144.3] 74.7% ***
0 0 | 199.0 / 209.2 ᅵ 400.4 [0.0, 0.0, 0.0, 243.6, 1846.0] 65.7%
1 | 220.4 / 226.7 ᅵ 423.2 [0.0, 0.0, 0.0, 264.0, 1777.0] 63.5% *
2 | 195.5 / 205.3 ᅵ 337.9 [0.0, 0.0, 123.0, 212.0, 1721.9] 43.2%
32 0 | 362.3 / 359.0 ᅵ 308.4 [0.0, 200.0, 265.0, 416.4, 1816.6] 5.0%
1 | 323.6 / 321.2 ᅵ 327.1 [0.0, 142.9, 210.0, 353.4, 1907.0] 4.0%
2 | 309.0 / 310.7 ᅵ 381.3 [0.0, 122.0, 175.5, 298.0, 2090.4] 5.0%
64 0 | 342.7 / 343.6 ᅵ 331.1 [0.0, 143.0, 239.5, 409.9, 1623.6] 5.3%
1 | 333.8 / 328.2 ᅵ 356.3 [0.0, 132.9, 211.5, 358.1, 1629.1] 10.7% ??
2 | 352.0 / 352.0 ᅵ 332.3 [0.0, 163.5, 239.9, 400.1, 1643.4] 5.3%
A little bit larger than previous so that it does not really fit in memory.
The performance inpact is significant compared to previous. Sorting and
flushing brings * 2 tps, unresponsiveness comes from 75% to reach a better 5%.
###### ANDRES00c (same as ANDRES00b but time 300 -> 4000)
opts # | tps / 1-sec tps ᅵ stddev [ min q1 med q2 max ] <=10.0
head 0 | 115.2 / 114.3 ᅵ 256.4 [0.0, 0.0, 75.0, 131.1, 3389.0] 46.5%
1 | 118.4 / 117.9 ᅵ 248.3 [0.0, 0.0, 87.0, 151.0, 3603.6] 46.7%
2 | 120.1 / 119.2 ᅵ 254.4 [0.0, 0.0, 91.0, 143.0, 3307.8] 43.8%
0 0 | 217.4 / 211.0 ᅵ 237.1 [0.0, 139.0, 193.0, 239.0, 3115.4] 16.8%
1 | 216.2 / 209.6 ᅵ 244.9 [0.0, 138.9, 188.0, 231.0, 3331.3] 16.3%
2 | 218.6 / 213.8 ᅵ 246.7 [0.0, 137.0, 187.0, 232.0, 3229.6] 16.2%
32 0 | 146.6 / 142.5 ᅵ 234.5 [0.0, 59.0, 93.0, 151.1, 3294.7] 17.5%
1 | 148.0 / 142.6 ᅵ 239.2 [0.0, 64.0, 95.9, 144.0, 3361.8] 16.0%
2 | 147.6 / 140.4 ᅵ 233.2 [0.0, 59.4, 94.0, 148.0, 3108.4] 18.0%
64 0 | 145.3 / 140.5 ᅵ 233.6 [0.0, 61.0, 93.0, 147.7, 3212.6] 16.5%
1 | 145.6 / 140.3 ᅵ 233.3 [0.0, 58.0, 93.0, 146.0, 3351.8] 17.3%
2 | 147.7 / 142.2 ᅵ 233.2 [0.0, 61.0, 97.0, 148.4, 3616.3] 17.0%
The only difference between ANDRES00B and ANDRES00C is the duration, from
5 minutes to 66 minutes. This show that short runs can be widelely misleading:
In particular the longer runs shows less that half tps for some settings, and
the relative comparison of head vs sort vs sort+flush is different.
###### ANDRES00d (same as ANDRES00b but wal_level hot_standby->minimal)
opts # | tps / 1-sec tps ᅵ stddev [ min q1 med q2 max ] <=10.0
head 0 | 191.6 / 195.1 ᅵ 439.3 [0.0, 0.0, 0.0, 0.0, 2540.2] 76.3%
1 | 211.3 / 213.6 ᅵ 461.9 [0.0, 0.0, 0.0, 13.0, 3203.7] 75.0%
2 | 152.4 / 154.9 ᅵ 217.6 [0.0, 0.0, 58.0, 235.6, 995.9] 39.3% ???
0 0 | 247.2 / 251.7 ᅵ 454.0 [0.0, 0.0, 0.0, 375.3, 2592.4] 67.7%
1 | 215.4 / 232.7 ᅵ 446.5 [0.0, 0.0, 0.0, 103.0, 3046.7] 72.3%
2 | 160.6 / 160.8 ᅵ 222.1 [0.0, 0.0, 80.0, 209.6, 885.3] 42.0% ???
32 0 | 399.9 / 397.0 ᅵ 356.6 [0.0, 67.0, 348.0, 572.8, 2604.2] 21.0%
1 | 391.8 / 392.5 ᅵ 371.7 [0.0, 85.5, 314.4, 549.3, 2590.3] 20.7%
2 | 406.1 / 404.8 ᅵ 380.6 [0.0, 95.0, 348.5, 569.0, 3383.7] 21.3%
64 0 | 395.9 / 396.1 ᅵ 352.4 [0.0, 89.5, 342.5, 556.0, 2366.9] 17.7%
1 | 355.1 / 351.9 ᅵ 296.7 [0.0, 172.5, 306.1, 468.1, 1663.5] 16.0%
2 | 403.6 / 401.8 ᅵ 390.5 [0.0, 0.0, 337.0, 636.1, 2591.3] 26.7% ???
###### ANDRES00e (same as ANDRES00b but maintenance_work_mem=2GB->64MB)
opts # | tps / 1-sec tps ᅵ stddev [ min q1 med q2 max ] <=10.0
head 0 | 153.5 / 161.3 ᅵ 401.3 [0.0, 0.0, 0.0, 0.0, 2546.0] 82.0%
1 | 170.7 / 175.9 ᅵ 399.9 [0.0, 0.0, 0.0, 14.0, 2537.4] 74.7%
2 | 184.7 / 190.4 ᅵ 389.2 [0.0, 0.0, 0.0, 158.5, 2544.6] 69.3%
0 0 | 211.2 / 227.8 ᅵ 418.8 [0.0, 0.0, 0.0, 334.6, 2589.3] 65.7%
1 | 221.7 / 226.0 ᅵ 415.7 [0.0, 0.0, 0.0, 276.8, 2588.2] 68.4%
2 | 232.5 / 233.2 ᅵ 403.5 [0.0, 0.0, 0.0, 377.0, 2260.2] 62.0%
32 0 | 373.2 / 374.4 ᅵ 309.2 [0.0, 180.6, 321.8, 475.2, 2596.5] 11.3%
1 | 348.7 / 348.1 ᅵ 328.4 [0.0, 127.0, 284.1, 451.9, 2595.1] 17.3%
2 | 376.3 / 375.3 ᅵ 315.5 [0.0, 186.5, 329.6, 487.1, 2365.4] 15.3%
64 0 | 388.9 / 387.8 ᅵ 348.7 [0.0, 164.0, 305.9, 546.5, 2587.2] 15.0%
1 | 380.3 / 378.7 ᅵ 338.8 [0.0, 171.1, 317.4, 524.8, 2592.4] 16.7%
2 | 369.8 / 367.4 ᅵ 340.5 [0.0, 77.4, 320.6, 525.5, 2484.7] 20.7%
Hmmm, interesting: maintenance_work_mem seems to have some influence on
performance, although it is not too consistent between settings, probably
because as the memory is used to its limit the performance is quite
sensitive to the available memory.
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Hi,
On 2016-02-19 10:16:41 +0100, Fabien COELHO wrote:
Below the results of a lot of tests with pgbench to exercise checkpoints on
the above version when fetched.
Wow, that's a great test series.
Overall comments:
- sorting & flushing is basically always a winner
- benchmarking with short runs on large databases is a bad idea
the results are very different if a longer run is used
(see andres00b vs andres00c)
Based on these results I think 32 will be a good default for
checkpoint_flush_after? There's a few cases where 64 showed to be
beneficial, and some where 32 is better. I've seen 64 perform a bit
better in some cases here, but the differences were not too big.
I gather that you didn't play with
backend_flush_after/bgwriter_flush_after, i.e. you left them at their
default values? Especially backend_flush_after can have a significant
positive and negative performance impact.
16 GB 2 cpu 8 cores
200 GB RAID1 HDD, ext4 FS
Ubuntu 12.04 LTS (precise)
That's with 12.04's standard kernel?
postgresql.conf:
shared_buffers = 1GB
max_wal_size = 1GB
checkpoint_timeout = 300s
checkpoint_completion_target = 0.8
checkpoint_flush_after = { none, 0, 32, 64 }
Did you re-initdb between the runs?
I've seen massively varying performance differences due to autovacuum
triggered analyzes. It's not completely deterministic when those run,
and on bigger scale clusters analyze can take ages, while holding a
snapshot.
Hmmm, interesting: maintenance_work_mem seems to have some influence on
performance, although it is not too consistent between settings, probably
because as the memory is used to its limit the performance is quite
sensitive to the available memory.
That's probably because of differing behaviour of autovacuum/vacuum,
which sometime will have to do several scans of the tables if there are
too many dead tuples.
Regards,
Andres
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Hello.
Based on these results I think 32 will be a good default for
checkpoint_flush_after? There's a few cases where 64 showed to be
beneficial, and some where 32 is better. I've seen 64 perform a bit
better in some cases here, but the differences were not too big.
Yes, these many runs show that 32 is basically as good or better than 64.
I'll do some runs with 16/48 to have some more data.
I gather that you didn't play with
backend_flush_after/bgwriter_flush_after, i.e. you left them at their
default values? Especially backend_flush_after can have a significant
positive and negative performance impact.
Indeed, non reported configuration options have their default values.
There were also minor changes in the default options for logging (prefix,
checkpoint, ...), but nothing significant, and always the same for all
runs.
[...] Ubuntu 12.04 LTS (precise)
That's with 12.04's standard kernel?
Yes.
checkpoint_flush_after = { none, 0, 32, 64 }
Did you re-initdb between the runs?
Yes, all runs are from scratch (initdb, pgbench -i, some warmup...).
I've seen massively varying performance differences due to autovacuum
triggered analyzes. It's not completely deterministic when those run,
and on bigger scale clusters analyze can take ages, while holding a
snapshot.
Yes, I agree that probably the performance changes on long vs short runs
(andres00c vs andres00b) is due to autovacuum.
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1
Hi Fabien,
Fabien COELHO schrieb am 19.02.2016 um 16:04:
[...] Ubuntu 12.04 LTS (precise)
That's with 12.04's standard kernel?
Yes.
Kernel 3.2 is extremely bad for Postgresql, as the vm seems to amplify IO somehow. The difference
to 3.13 (the latest LTS kernel for 12.04) is huge.
You might consider upgrading your kernel to 3.13 LTS. It's quite easy normally:
https://wiki.ubuntu.com/Kernel/LTSEnablementStack
/Patric
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v2.0.22 (GNU/Linux)
Comment: GnuPT 2.5.2
iEYEARECAAYFAlbHW4AACgkQfGgGu8y7ypC1EACgy8mW6AoaWjKycbuAnCZ3CEPW
Al8AmwfF0smqmDvNsaPkq0dAtop7jP5M
=TxT+
-----END PGP SIGNATURE-----
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Hallo Patric,
Kernel 3.2 is extremely bad for Postgresql, as the vm seems to amplify
IO somehow. The difference to 3.13 (the latest LTS kernel for 12.04) is
huge.
Interesting! To summarize it, 25% performance degradation from best kernel
(2.6.32) to worst (3.2.0), that is indeed significant.
You might consider upgrading your kernel to 3.13 LTS. It's quite easy
[...]
There are other stuff running on the hardware that I do not wish to touch,
so upgrading the particular host is currently not an option, otherwise I
would have switched to trusty.
Thanks for the pointer.
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 2016-02-04 16:54:58 +0100, Andres Freund wrote:
Hi,
Fabien asked me to post a new version of the checkpoint flushing patch
series. While this isn't entirely ready for commit, I think we're
getting closer.I don't want to post a full series right now, but my working state is
available on
http://git.postgresql.org/gitweb/?p=users/andresfreund/postgres.git;a=shortlog;h=refs/heads/checkpoint-flush
git://git.postgresql.org/git/users/andresfreund/postgres.git checkpoint-flush
I've updated the git tree.
Here's the next two (the most important) patches of the series:
0001: Allow to trigger kernel writeback after a configurable number of writes.
0002: Checkpoint sorting and balancing.
For 0001 I've recently changed:
* Don't schedule writeback after smgrextend() - that defeats linux
delayed allocation mechanism, increasing fragmentation noticeably.
* Add docs for the new GUC variables
* comment polishing
* BackendWritebackContext now isn't dynamically allocated anymore
I think this patch primarily needs:
* review of the docs, not sure if they're easy enough to
understand. Some language polishing might also be needed.
* review of the writeback API, combined with the smgr/md.c changes.
* Currently *_flush_after can be set to a nonzero value, even if there's
no support for flushing on that platform. Imo that's ok, but perhaps
other people's opinion differ.
For 0002 I've recently changed:
* Removed the sort timing information, we've proven sufficiently that
it doesn't take a lot of time.
* Minor comment polishing.
I think this patch primarily needs:
* Benchmarking on FreeBSD/OSX to see whether we should enable the
mmap()/msync(MS_ASYNC) method by default. Unless somebody does so, I'm
inclined to leave it off till then.
Regards,
Andres
Attachments:
0001-Allow-to-trigger-kernel-writeback-after-a-configurab.patchtext/x-patch; charset=us-asciiDownload
From 58aee659417372f3dda4420d8f2a4f4d41c56d31 Mon Sep 17 00:00:00 2001
From: Andres Freund <andres@anarazel.de>
Date: Fri, 19 Feb 2016 12:13:05 -0800
Subject: [PATCH 1/4] Allow to trigger kernel writeback after a configurable
number of writes.
Currently writes to the main data files of postgres all go through the
OS page cache. This means that currently several operating systems can
end up collecting a large number of dirty buffers in their respective
page caches. When these dirty buffers are flushed to storage rapidly,
be it because of fsync(), timeouts, or dirty ratios, latency for other
writes can increase massively. This is the primary reason for regular
massive stalls observed in real world scenarios and artificial
benchmarks; on rotating disks stalls on the order of hundreds of seconds
have been observed.
On linux it is possible to control this by reducing the global dirty
limits significantly, reducing the above problem. But global
configuration is rather problematic because it'll affect other
applications; also PostgreSQL itself doesn't always generally want this
behavior, e.g. for temporary files it's undesirable.
Several operating systems allow some control over the kernel page
cache. Linux has sync_file_range(2), several posix systems have msync(2)
and posix_fadvise(2). sync_file_range(2) is preferable because it
requires no special setup, whereas msync() requires the to-be-flushed
range to be mmap'ed. For the purpose of flushing dirty data
posix_fadvise(2) is the worst alternative, as flushing dirty data is
just a side-effect of POSIX_FADV_DONTNEED, which also removes the pages
from the page cache. Thus the feature is enabled by default only on
linux, but can be enabled on all systems that have any of the above
APIs.
With the infrastructure added, writes made via checkpointer, bgwriter
and normal user backends can be flushed after a configurable number of
writes. Each of these sources of writes controlled by a separate GUC,
checkpointer_flush_after, bgwriter_flush_after and backend_flush_after
respectively; they're separate because the number of flushes that are
good are separate, and because the performance considerations of
controlled flushing for each of these are different.
A later patch will add checkpoint sorting - after that flushes from the
ckeckpoint will almost always be desirable. Bgwriter flushes are most of
the time going to be random, which are slow on lots of storage hardware.
Flushing in backends works well if the storage and bgwriter can keep up,
but if not it can have negative consequences. This patch is likely to
have negative performance consequences without checkpoint sorting, but
unfortunately so has sorting without flush control.
TODO:
* verify msync codepath
* properly detect mmap() && msync(MS_ASYNC) support, use it by default
if available and sync_file_range is *not* available
Discussion: alpine.DEB.2.10.1506011320000.28433@sto
Author: Fabien Coelho and Andres Freund
---
doc/src/sgml/config.sgml | 81 +++++++++++++++
doc/src/sgml/wal.sgml | 13 +++
src/backend/postmaster/bgwriter.c | 8 +-
src/backend/storage/buffer/buf_init.c | 5 +
src/backend/storage/buffer/bufmgr.c | 185 +++++++++++++++++++++++++++++++++-
src/backend/storage/file/copydir.c | 4 +-
src/backend/storage/file/fd.c | 153 +++++++++++++++++++++++++---
src/backend/storage/smgr/md.c | 49 +++++++++
src/backend/storage/smgr/smgr.c | 19 +++-
src/backend/utils/misc/guc.c | 36 +++++++
src/include/storage/buf_internals.h | 31 +++++-
src/include/storage/bufmgr.h | 22 +++-
src/include/storage/fd.h | 3 +-
src/include/storage/smgr.h | 4 +
src/tools/pgindent/typedefs.list | 2 +
15 files changed, 586 insertions(+), 29 deletions(-)
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index a09ceb2..3dc6719 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -1843,6 +1843,32 @@ include_dir 'conf.d'
</para>
</listitem>
</varlistentry>
+
+ <varlistentry id="guc-bgwriter-flush-after" xreflabel="bgwriter_flush_after">
+ <term><varname>bgwriter_flush_after</varname> (<type>int</type>)
+ <indexterm>
+ <primary><varname>bgwriter_flush_after</> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ Whenever more than <varname>bgwriter_flush_after</varname> bytes have
+ been written by the bgwriter, hint to OS to flush these writes to the
+ underlying storage. Doing so will limit the amount of dirty data in
+ the kernel's page cache, reducing the likelihood of stalls when fsync
+ is issued at the end of a checkpoint, or when the OS writes out data
+ in larger batches in the background. Often that will result in
+ greatly reduced transaction latency, but there also are some cases,
+ especially with workloads that are bigger than <xref
+ linkend="guc-shared-buffers">, but smaller than the OS's page cache,
+ where performance might degrade. This setting may have no effect on
+ some platforms. <literal>0</literal> disables controlled flushing.
+ The default is <literal>256Kb</> on Linux, <literal>0</> otherwise.
+ This parameter can only be set in the <filename>postgresql.conf</>
+ file or on the server command line.
+ </para>
+ </listitem>
+ </varlistentry>
</variablelist>
<para>
@@ -1944,6 +1970,35 @@ include_dir 'conf.d'
</para>
</listitem>
</varlistentry>
+
+ <varlistentry id="guc-backend-flush-after" xreflabel="backend_flush_after">
+ <term><varname>backend_flush_after</varname> (<type>int</type>)
+ <indexterm>
+ <primary><varname>backend_flush_after</> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ Whenever more than <varname>backend_flush_after</varname> bytes have
+ been written by a single backend, hint to OS to flush these writes to
+ the underlying storage. Doing so will limit the amount of dirty data
+ in the kernel's page cache, reducing the likelihood of stalls when
+ fsync is issued at the end of a checkpoint, or when the OS writes out
+ data in larger batches in the background. Often that will result in
+ greatly reduced transaction latency, but there also are some cases,
+ especially with workloads that are bigger than <xref
+ linkend="guc-shared-buffers">, but smaller than the OS's page cache,
+ where performance might degrade. Note that because
+ <varname>backend_flush_after</varname> is per-backend, the total
+ amount of dirty data in the kerne's page cache can be considerably
+ bigger than this setting. This setting may have no effect on some
+ platforms. <literal>0</literal> disables controlled flushing. The
+ default is <literal>256Kb</> on Linux, <literal>0</> otherwise. This
+ parameter can only be set in the <filename>postgresql.conf</> file or
+ on the server command line.
+ </para>
+ </listitem>
+ </varlistentry>
</variablelist>
</sect2>
</sect1>
@@ -2475,6 +2530,32 @@ include_dir 'conf.d'
</listitem>
</varlistentry>
+ <varlistentry id="guc-checkpoint-flush-after" xreflabel="checkpoint_flush_after">
+ <term><varname>checkpoint_flush_after</varname> (<type>int</type>)
+ <indexterm>
+ <primary><varname>checkpoint_flush_after</> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ Whenever more than <varname>checkpoint_flush_after</varname> bytes
+ have been written while performing a checkpoint, hint to OS to flush
+ these writes to the underlying storage. Doing so will limit the
+ amount of dirty data in the kernel's page cache, reducing the
+ likelihood of stalls when fsync is issued at the end of a checkpoint,
+ or when the OS writes out data in larger batches in the background.
+ Often that will result in greatly reduced transaction latency, but
+ there also are some cases, especially with workloads that are bigger
+ than <xref linkend="guc-shared-buffers">, but smaller than the OS's
+ page cache, where performance might degrade. This setting may have no
+ effect on some platforms. <literal>0</literal> disables controlled
+ flushing. The default is <literal>256Kb</> on Linux, <literal>0</>
+ otherwise. This parameter can only be set in the
+ <filename>postgresql.conf</> file or on the server command line.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="guc-checkpoint-warning" xreflabel="checkpoint_warning">
<term><varname>checkpoint_warning</varname> (<type>integer</type>)
<indexterm>
diff --git a/doc/src/sgml/wal.sgml b/doc/src/sgml/wal.sgml
index e3941c9..96496b0 100644
--- a/doc/src/sgml/wal.sgml
+++ b/doc/src/sgml/wal.sgml
@@ -546,6 +546,19 @@
</para>
<para>
+ On Linux and POSIX platforms <xref linkend="guc-checkpoint-flush-after">
+ allows to guide the OS that pages written by the checkpoint should be
+ flushed to disk. Otherwise, these pages may be kept in the OS's page
+ cache, inducing a stall when <literal>fsync</> is called later. This
+ setting helps to reduce transaction latency, but it also can an adverse
+ effect on performance; particularly for workloads that are bigger than
+ <xref linkend="guc-shared-buffers">, but smaller than the OS's page cache.
+ It should be beneficial for high write loads on HDD. This feature probably
+ brings no benefit on SSD, as the I/O write latency is small on such
+ hardware, thus it may be disabled.
+ </para>
+
+ <para>
The number of WAL segment files in <filename>pg_xlog</> directory depends on
<varname>min_wal_size</>, <varname>max_wal_size</> and
the amount of WAL generated in previous checkpoint cycles. When old log
diff --git a/src/backend/postmaster/bgwriter.c b/src/backend/postmaster/bgwriter.c
index 4ff4caf..7d0371d 100644
--- a/src/backend/postmaster/bgwriter.c
+++ b/src/backend/postmaster/bgwriter.c
@@ -111,6 +111,7 @@ BackgroundWriterMain(void)
sigjmp_buf local_sigjmp_buf;
MemoryContext bgwriter_context;
bool prev_hibernate;
+ WritebackContext wb_context;
/*
* Properly accept or ignore signals the postmaster might send us.
@@ -164,6 +165,8 @@ BackgroundWriterMain(void)
ALLOCSET_DEFAULT_MAXSIZE);
MemoryContextSwitchTo(bgwriter_context);
+ WritebackContextInit(&wb_context, &bgwriter_flush_after);
+
/*
* If an exception is encountered, processing resumes here.
*
@@ -208,6 +211,9 @@ BackgroundWriterMain(void)
/* Flush any leaked data in the top-level context */
MemoryContextResetAndDeleteChildren(bgwriter_context);
+ /* re-initilialize to avoid repeated errors causing problems */
+ WritebackContextInit(&wb_context, &bgwriter_flush_after);
+
/* Now we can allow interrupts again */
RESUME_INTERRUPTS();
@@ -269,7 +275,7 @@ BackgroundWriterMain(void)
/*
* Do one cycle of dirty-buffer writing.
*/
- can_hibernate = BgBufferSync();
+ can_hibernate = BgBufferSync(&wb_context);
/*
* Send off activity statistics to the stats collector
diff --git a/src/backend/storage/buffer/buf_init.c b/src/backend/storage/buffer/buf_init.c
index f013a4d..e10071d 100644
--- a/src/backend/storage/buffer/buf_init.c
+++ b/src/backend/storage/buffer/buf_init.c
@@ -23,6 +23,7 @@ char *BufferBlocks;
LWLockMinimallyPadded *BufferIOLWLockArray = NULL;
LWLockTranche BufferIOLWLockTranche;
LWLockTranche BufferContentLWLockTranche;
+WritebackContext BackendWritebackContext;
/*
@@ -149,6 +150,10 @@ InitBufferPool(void)
/* Init other shared buffer-management stuff */
StrategyInitialize(!foundDescs);
+
+ /* Initialize per-backend file flush context */
+ WritebackContextInit(&BackendWritebackContext,
+ &backend_flush_after);
}
/*
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index 7141eb8..cdbda0f 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -83,6 +83,14 @@ bool track_io_timing = false;
int effective_io_concurrency = 0;
/*
+ * GUC variables about triggering kernel writeback for buffers written; OS
+ * dependant defaults are set via the GUC mechanism.
+ */
+int checkpoint_flush_after = 0;
+int bgwriter_flush_after = 0;
+int backend_flush_after = 0;
+
+/*
* How many buffers PrefetchBuffer callers should try to stay ahead of their
* ReadBuffer calls by. This is maintained by the assign hook for
* effective_io_concurrency. Zero means "never prefetch". This value is
@@ -399,7 +407,7 @@ static bool PinBuffer(BufferDesc *buf, BufferAccessStrategy strategy);
static void PinBuffer_Locked(BufferDesc *buf);
static void UnpinBuffer(BufferDesc *buf, bool fixOwner);
static void BufferSync(int flags);
-static int SyncOneBuffer(int buf_id, bool skip_recently_used);
+static int SyncOneBuffer(int buf_id, bool skip_recently_used, WritebackContext *flush_context);
static void WaitIO(BufferDesc *buf);
static bool StartBufferIO(BufferDesc *buf, bool forInput);
static void TerminateBufferIO(BufferDesc *buf, bool clear_dirty,
@@ -416,6 +424,7 @@ static void FlushBuffer(BufferDesc *buf, SMgrRelation reln);
static void AtProcExit_Buffers(int code, Datum arg);
static void CheckForBufferLeaks(void);
static int rnode_comparator(const void *p1, const void *p2);
+static int buffertag_comparator(const void *p1, const void *p2);
/*
@@ -818,6 +827,12 @@ ReadBuffer_common(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
MemSet((char *) bufBlock, 0, BLCKSZ);
/* don't set checksum for all-zero page */
smgrextend(smgr, forkNum, blockNum, (char *) bufBlock, false);
+
+ /*
+ * XXX: Note that we're *not* doing a ScheduleBufferTagForWriteback
+ * here. At least on linux doing so defeats 'delayed allocation',
+ * leading to more fragmented files.
+ */
}
else
{
@@ -1084,6 +1099,9 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
FlushBuffer(buf, NULL);
LWLockRelease(BufferDescriptorGetContentLock(buf));
+ ScheduleBufferTagForWriteback(&BackendWritebackContext,
+ &buf->tag);
+
TRACE_POSTGRESQL_BUFFER_WRITE_DIRTY_DONE(forkNum, blockNum,
smgr->smgr_rnode.node.spcNode,
smgr->smgr_rnode.node.dbNode,
@@ -1642,6 +1660,7 @@ BufferSync(int flags)
int num_to_write;
int num_written;
int mask = BM_DIRTY;
+ WritebackContext wb_context;
/* Make sure we can handle the pin inside SyncOneBuffer */
ResourceOwnerEnlargeBuffers(CurrentResourceOwner);
@@ -1694,6 +1713,9 @@ BufferSync(int flags)
if (num_to_write == 0)
return; /* nothing to do */
+
+ WritebackContextInit(&wb_context, &checkpoint_flush_after);
+
TRACE_POSTGRESQL_BUFFER_SYNC_START(NBuffers, num_to_write);
/*
@@ -1725,7 +1747,7 @@ BufferSync(int flags)
*/
if (bufHdr->flags & BM_CHECKPOINT_NEEDED)
{
- if (SyncOneBuffer(buf_id, false) & BUF_WRITTEN)
+ if (SyncOneBuffer(buf_id, false, &wb_context) & BUF_WRITTEN)
{
TRACE_POSTGRESQL_BUFFER_SYNC_WRITTEN(buf_id);
BgWriterStats.m_buf_written_checkpoints++;
@@ -1777,7 +1799,7 @@ BufferSync(int flags)
* bgwriter_lru_maxpages to 0.)
*/
bool
-BgBufferSync(void)
+BgBufferSync(WritebackContext *wb_context)
{
/* info obtained from freelist.c */
int strategy_buf_id;
@@ -2002,7 +2024,8 @@ BgBufferSync(void)
/* Execute the LRU scan */
while (num_to_scan > 0 && reusable_buffers < upcoming_alloc_est)
{
- int buffer_state = SyncOneBuffer(next_to_clean, true);
+ int buffer_state = SyncOneBuffer(next_to_clean, true,
+ wb_context);
if (++next_to_clean >= NBuffers)
{
@@ -2079,10 +2102,11 @@ BgBufferSync(void)
* Note: caller must have done ResourceOwnerEnlargeBuffers.
*/
static int
-SyncOneBuffer(int buf_id, bool skip_recently_used)
+SyncOneBuffer(int buf_id, bool skip_recently_used, WritebackContext *wb_context)
{
BufferDesc *bufHdr = GetBufferDescriptor(buf_id);
int result = 0;
+ BufferTag tag;
ReservePrivateRefCountEntry();
@@ -2123,8 +2147,13 @@ SyncOneBuffer(int buf_id, bool skip_recently_used)
FlushBuffer(bufHdr, NULL);
LWLockRelease(BufferDescriptorGetContentLock(bufHdr));
+
+ tag = bufHdr->tag;
+
UnpinBuffer(bufHdr, true);
+ ScheduleBufferTagForWriteback(wb_context, &tag);
+
return result | BUF_WRITTEN;
}
@@ -3724,3 +3753,149 @@ rnode_comparator(const void *p1, const void *p2)
else
return 0;
}
+
+
+/*
+ * BufferTag comparator.
+ */
+static int
+buffertag_comparator(const void *a, const void *b)
+{
+ const BufferTag *ba = (const BufferTag *) a;
+ const BufferTag *bb = (const BufferTag *) b;
+ int ret;
+
+ ret = rnode_comparator(&ba->rnode, &bb->rnode);
+
+ if (ret != 0)
+ return ret;
+
+ if (ba->forkNum < bb->forkNum)
+ return -1;
+ if (ba->forkNum > bb->forkNum)
+ return 1;
+
+ if (ba->blockNum < bb->blockNum)
+ return -1;
+ if (ba->blockNum > bb->blockNum)
+ return 1;
+
+ return 0;
+}
+
+
+/*
+ * Initialize a writeback context, discarding potential previous state.
+ *
+ * *max_coalesce is a pointer to a variable containing the current maximum
+ * number of writeback requests that will be coalesced into a bigger one. A
+ * value <= 0 means that no writeback control will be performed. max_pending
+ * is a pointer instead of an immediate value, so the coalesce limits can
+ * easily changed by the GUC mechanism, and so calling code does not have to
+ * check the current config variables.
+ */
+void
+WritebackContextInit(WritebackContext *context, int *max_pending)
+{
+ Assert(*max_pending <= WRITEBACK_MAX_PENDING_FLUSHES);
+
+ context->max_pending = max_pending;
+ context->nr_pending = 0;
+}
+
+
+/*
+ * Add buffer to list of pending writeback requests.
+ */
+void
+ScheduleBufferTagForWriteback(WritebackContext *context, BufferTag *tag)
+{
+ PendingWriteback *pending;
+
+ /* nothing to do if flushing is disabled */
+ if (*context->max_pending <= 0 && context->nr_pending <= 0)
+ return;
+
+ Assert(*context->max_pending <= WRITEBACK_MAX_PENDING_FLUSHES);
+
+ pending = &context->pending_writebacks[context->nr_pending++];
+
+ pending->tag = *tag;
+
+ if (context->nr_pending >= *context->max_pending)
+ IssuePendingWritebacks(context);
+}
+
+/*
+ * Issue all pending writeback requests, previously scheduled with
+ * ScheduleBufferTagForWriteback, to the OS.
+ *
+ * Because this is only used to improve the OSs IO scheduling we try to never
+ * error out - it's just a hint.
+ */
+void
+IssuePendingWritebacks(WritebackContext *context)
+{
+ int i;
+
+ if (context->nr_pending == 0)
+ return;
+
+ /*
+ * Executing the writes in-order can make them a lot faster, and allows to
+ * merge writeback requests to consecutive blocks into larger writebacks.
+ */
+ qsort(&context->pending_writebacks, context->nr_pending,
+ sizeof(PendingWriteback), buffertag_comparator);
+
+ /*
+ * Coalesce neighbouring writes, but nothing else. For that we iterate
+ * through the, now sorted, array of pending flushes, and look forward to
+ * find all neighbouring (or identical) writes.
+ */
+ for (i = 0; i < context->nr_pending; i++)
+ {
+ PendingWriteback *cur;
+ PendingWriteback *next;
+ SMgrRelation reln;
+ int ahead;
+ BufferTag tag;
+ Size nblocks = 1;
+
+ cur = &context->pending_writebacks[i];
+ tag = cur->tag;
+
+ /*
+ * Peek ahead, into following writeback requests, to see if they can
+ * be combined with the current one.
+ */
+ for (ahead = 0; i + ahead + 1 < context->nr_pending; ahead++)
+ {
+ next = &context->pending_writebacks[i + ahead + 1];
+
+ /* different file, skip */
+ if (!RelFileNodeEquals(cur->tag.rnode, next->tag.rnode) ||
+ cur->tag.forkNum != cur->tag.forkNum)
+ break;
+
+ /* ok, block flushed twice, skip */
+ if (cur->tag.blockNum == next->tag.blockNum)
+ continue;
+
+ /* only merge consecutive writes */
+ if (cur->tag.blockNum + 1 != next->tag.blockNum)
+ break;
+
+ nblocks++;
+ cur = next;
+ }
+
+ i += ahead;
+
+ /* and finally tell the kernel to write the data to storage */
+ reln = smgropen(tag.rnode, InvalidBackendId);
+ smgrwriteback(reln, tag.forkNum, tag.blockNum, nblocks);
+ }
+
+ context->nr_pending = 0;
+}
diff --git a/src/backend/storage/file/copydir.c b/src/backend/storage/file/copydir.c
index 522f420..a51ee81 100644
--- a/src/backend/storage/file/copydir.c
+++ b/src/backend/storage/file/copydir.c
@@ -190,9 +190,9 @@ copy_file(char *fromfile, char *tofile)
/*
* We fsync the files later but first flush them to avoid spamming the
* cache and hopefully get the kernel to start writing them out before
- * the fsync comes. Ignore any error, since it's only a hint.
+ * the fsync comes.
*/
- (void) pg_flush_data(dstfd, offset, nbytes);
+ pg_flush_data(dstfd, offset, nbytes);
}
if (CloseTransientFile(dstfd))
diff --git a/src/backend/storage/file/fd.c b/src/backend/storage/file/fd.c
index 1b30100..5b8a765 100644
--- a/src/backend/storage/file/fd.c
+++ b/src/backend/storage/file/fd.c
@@ -61,6 +61,9 @@
#include <sys/file.h>
#include <sys/param.h>
#include <sys/stat.h>
+#ifndef WIN32
+#include <sys/mman.h>
+#endif
#include <unistd.h>
#include <fcntl.h>
#ifdef HAVE_SYS_RESOURCE_H
@@ -82,6 +85,8 @@
/* Define PG_FLUSH_DATA_WORKS if we have an implementation for pg_flush_data */
#if defined(HAVE_SYNC_FILE_RANGE)
#define PG_FLUSH_DATA_WORKS 1
+#elif !defined(WIN32) && defined(MS_ASYNC)
+#define PG_FLUSH_DATA_WORKS 1
#elif defined(USE_POSIX_FADVISE) && defined(POSIX_FADV_DONTNEED)
#define PG_FLUSH_DATA_WORKS 1
#endif
@@ -380,29 +385,126 @@ pg_fdatasync(int fd)
}
/*
- * pg_flush_data --- advise OS that the data described won't be needed soon
+ * pg_flush_data --- advise OS that the described dirty data should be flushed
*
- * Not all platforms have sync_file_range or posix_fadvise; treat as no-op
- * if not available. Also, treat as no-op if enableFsync is off; this is
- * because the call isn't free, and some platforms such as Linux will actually
- * block the requestor until the write is scheduled.
+ * An offset of 0 with an amount of 0 means that the entire file should be
+ * flushed.
*/
-int
-pg_flush_data(int fd, off_t offset, off_t amount)
+void
+pg_flush_data(int fd, off_t offset, off_t nbytes)
{
#ifdef PG_FLUSH_DATA_WORKS
- if (enableFsync)
- {
+
+ /*
+ * Right now file flushing is primarily used to avoid making later
+ * fsync()/fdatasync() calls have a less impact. Thus don't trigger
+ * flushes if fsyncs are disabled - that's a decision we might want to
+ * make configurable at some point.
+ */
+ if (!enableFsync)
+ return;
+
#if defined(HAVE_SYNC_FILE_RANGE)
- return sync_file_range(fd, offset, amount, SYNC_FILE_RANGE_WRITE);
+ {
+ int rc = 0;
+
+ /*
+ * sync_file_range(SYNC_FILE_RANGE_WRITE), currently linux specific,
+ * tells the OS that writeback for the passed in blocks should be
+ * started, but that we don't want to wait for completion. Note that
+ * this call might block if too much dirty data exists in the range.
+ * This is the preferrable method on OSs supporting it, as it works
+ * reliably when available (contrast to msync()) and doesn't flush out
+ * clean data (like FADV_DONTNEED).
+ */
+ rc = sync_file_range(fd, offset, nbytes,
+ SYNC_FILE_RANGE_WRITE);
+
+ /* don't error out, this is just a performance optimization */
+ if (rc != 0)
+ {
+ ereport(WARNING,
+ (errcode_for_file_access(),
+ errmsg("could not flush dirty data: %m")));
+ }
+ }
+#elif !defined(WIN32) && defined(MS_ASYNC)
+ {
+ int rc = 0;
+ void *p;
+
+ /*
+ * On many OSs msync() on a mmap'ed file triggers writeback. On linux
+ * it only does so when MS_SYNC is specified, but then it does the
+ * writeback synchronously. Luckily all common linux systems have
+ * sync_file_range(). This is preferrable over FADV_DONTNEED because
+ * it doesn't flush out clean data.
+ *
+ * We map the file (mmap()), tell the kernel to sync back the contents
+ * (msync()), and then remove the mapping again (munmap()).
+ */
+ p = mmap(NULL, context->nbytes,
+ PROT_READ | PROT_WRITE, MAP_SHARED,
+ context->fd, context->offset);
+ if (p == MAP_FAILED)
+ {
+ ereport(WARNING,
+ (errcode_for_file_access(),
+ errmsg("could not mmap while flushing dirty data in file \"%s\": %m",
+ context->filename ? context->filename : "")));
+ goto out;
+ }
+
+ rc = msync(p, context->nbytes, MS_ASYNC);
+ if (rc != 0)
+ {
+ ereport(WARNING,
+ (errcode_for_file_access(),
+ errmsg("could not flush dirty data in file \"%s\": %m",
+ context->filename ? context->filename : "")));
+ /* NB: need to fall through to munmap()! */
+ }
+
+ rc = munmap(p, context->nbytes);
+ if (rc != 0)
+ {
+ /* FATAL error because mapping would remain */
+ ereport(FATAL,
+ (errcode_for_file_access(),
+ errmsg("could not munmap while flushing blocks in file \"%s\": %m",
+ context->filename ? context->filename : "")));
+ }
+ }
#elif defined(USE_POSIX_FADVISE) && defined(POSIX_FADV_DONTNEED)
- return posix_fadvise(fd, offset, amount, POSIX_FADV_DONTNEED);
+ {
+ int rc = 0;
+
+ /*
+ * Signal the kernel that the passed in range should not be cached
+ * anymore. This has the, desired, side effect of writing out dirty
+ * data, and the, undesired, side effect of likely discarding useful
+ * clean cached blocks. For the latter reason this is the least
+ * preferrable method.
+ */
+
+ rc = posix_fadvise(context->fd, context->offset, context->nbytes,
+ POSIX_FADV_DONTNEED);
+
+ /* don't error out, this is just a performance optimization */
+ if (rc != 0)
+ {
+ ereport(WARNING,
+ (errcode_for_file_access(),
+ errmsg("could not flush dirty data in file \"%s\": %m",
+ context->filename ? context->filename : "")));
+ goto out;
+ }
+ }
#else
#error PG_FLUSH_DATA_WORKS should not have been defined
#endif
- }
-#endif
- return 0;
+
+#endif /* PG_FLUSH_DATA_WORKS */
}
@@ -1289,6 +1391,24 @@ FilePrefetch(File file, off_t offset, int amount)
#endif
}
+void
+FileWriteback(File file, off_t offset, int amount)
+{
+ int returnCode;
+
+ Assert(FileIsValid(file));
+
+ DO_DB(elog(LOG, "FileWriteback: %d (%s) " INT64_FORMAT " %d",
+ file, VfdCache[file].fileName,
+ (int64) offset, amount));
+
+ returnCode = FileAccess(file);
+ if (returnCode < 0)
+ return;
+
+ pg_flush_data(VfdCache[file].fd, offset, amount);
+}
+
int
FileRead(File file, char *buffer, int amount)
{
@@ -2655,9 +2775,10 @@ pre_sync_fname(const char *fname, bool isdir, int elevel)
}
/*
- * We ignore errors from pg_flush_data() because this is only a hint.
+ * pg_flush_data() ignores errors, which is ok because this is only a
+ * hint.
*/
- (void) pg_flush_data(fd, 0, 0);
+ pg_flush_data(fd, 0, 0);
(void) CloseTransientFile(fd);
}
diff --git a/src/backend/storage/smgr/md.c b/src/backend/storage/smgr/md.c
index f6b79a9..bb2b465 100644
--- a/src/backend/storage/smgr/md.c
+++ b/src/backend/storage/smgr/md.c
@@ -662,6 +662,55 @@ mdprefetch(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum)
#endif /* USE_PREFETCH */
}
+/*
+ * mdwriteback() -- Tell the kernel to write pages back to storage.
+ *
+ * This accepts a rnage of blocks because flushing several pages at once is
+ * considerably more efficient than doing so individually.
+ */
+void
+mdwriteback(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum, int nblocks)
+{
+ off_t seekpos;
+ MdfdVec *v;
+
+ /*
+ * Issue flush requests in as few requests as possible; have to split at
+ * segment boundaries though, since those are actually separate files.
+ */
+ while (nblocks != 0)
+ {
+ int nflush = nblocks;
+ int segnum_start, segnum_end;
+
+ v = _mdfd_getseg(reln, forknum, blocknum, false, EXTENSION_RETURN_NULL);
+
+ /*
+ * We might be flushing buffers of already removed relations, that's
+ * ok, just ignore that case.
+ */
+ if (!v)
+ return;
+
+ /* compute offset inside the current segment */
+ segnum_start = blocknum / RELSEG_SIZE;
+
+ /* compute number of desired writes within the current segment */
+ segnum_end = (blocknum + nblocks - 1) / RELSEG_SIZE;
+ if (segnum_start != segnum_end)
+ nflush = RELSEG_SIZE - (blocknum % ((BlockNumber) RELSEG_SIZE) );
+
+ Assert(nflush >= 1);
+ Assert(nflush <= nblocks);
+
+ seekpos = (off_t) BLCKSZ * (blocknum % ((BlockNumber) RELSEG_SIZE));
+
+ FileWriteback(v->mdfd_vfd, seekpos, BLCKSZ * nflush);
+
+ nblocks -= nflush;
+ blocknum += nflush;
+ }
+}
/*
* mdread() -- Read the specified block from a relation.
diff --git a/src/backend/storage/smgr/smgr.c b/src/backend/storage/smgr/smgr.c
index 87ff358..2cae5aa 100644
--- a/src/backend/storage/smgr/smgr.c
+++ b/src/backend/storage/smgr/smgr.c
@@ -53,6 +53,8 @@ typedef struct f_smgr
BlockNumber blocknum, char *buffer);
void (*smgr_write) (SMgrRelation reln, ForkNumber forknum,
BlockNumber blocknum, char *buffer, bool skipFsync);
+ void (*smgr_writeback) (SMgrRelation reln, ForkNumber forknum,
+ BlockNumber blocknum, int nblocks);
BlockNumber (*smgr_nblocks) (SMgrRelation reln, ForkNumber forknum);
void (*smgr_truncate) (SMgrRelation reln, ForkNumber forknum,
BlockNumber nblocks);
@@ -66,8 +68,8 @@ typedef struct f_smgr
static const f_smgr smgrsw[] = {
/* magnetic disk */
{mdinit, NULL, mdclose, mdcreate, mdexists, mdunlink, mdextend,
- mdprefetch, mdread, mdwrite, mdnblocks, mdtruncate, mdimmedsync,
- mdpreckpt, mdsync, mdpostckpt
+ mdprefetch, mdread, mdwrite, mdwriteback, mdnblocks, mdtruncate,
+ mdimmedsync, mdpreckpt, mdsync, mdpostckpt
}
};
@@ -649,6 +651,19 @@ smgrwrite(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
buffer, skipFsync);
}
+
+/*
+ * smgrwriteback() -- Trigger kernel writeback for the supplied range of
+ * blocks.
+ */
+void
+smgrwriteback(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
+ int nblocks)
+{
+ (*(smgrsw[reln->smgr_which].smgr_writeback)) (reln, forknum, blocknum,
+ nblocks);
+}
+
/*
* smgrnblocks() -- Calculate the number of blocks in the
* supplied relation.
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index ea5a09a..789efbc 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -2385,6 +2385,42 @@ static struct config_int ConfigureNamesInt[] =
},
{
+ {"checkpoint_flush_after", PGC_SIGHUP, RESOURCES_ASYNCHRONOUS,
+ gettext_noop("Number of pages after which previously performed writes are flushed to disk."),
+ NULL,
+ GUC_UNIT_BLOCKS
+ },
+ &checkpoint_flush_after,
+ /* see bufmgr.h: OS dependant default */
+ DEFAULT_CHECKPOINT_FLUSH_AFTER, 0, WRITEBACK_MAX_PENDING_FLUSHES,
+ NULL, NULL, NULL
+ },
+
+ {
+ {"backend_flush_after", PGC_USERSET, WAL_CHECKPOINTS,
+ gettext_noop("Number of pages after which previously performed writes are flushed to disk."),
+ NULL,
+ GUC_UNIT_BLOCKS
+ },
+ &backend_flush_after,
+ /* see bufmgr.h: OS dependant default */
+ DEFAULT_BACKEND_FLUSH_AFTER, 0, WRITEBACK_MAX_PENDING_FLUSHES,
+ NULL, NULL, NULL
+ },
+
+ {
+ {"bgwriter_flush_after", PGC_SIGHUP, WAL_CHECKPOINTS,
+ gettext_noop("Number of pages after which previously performed writes are flushed to disk."),
+ NULL,
+ GUC_UNIT_BLOCKS
+ },
+ &bgwriter_flush_after,
+ /* see bufmgr.h: 16 on Linux, 0 otherwise */
+ DEFAULT_BGWRITER_FLUSH_AFTER, 0, WRITEBACK_MAX_PENDING_FLUSHES,
+ NULL, NULL, NULL
+ },
+
+ {
{"max_worker_processes",
PGC_POSTMASTER,
RESOURCES_ASYNCHRONOUS,
diff --git a/src/include/storage/buf_internals.h b/src/include/storage/buf_internals.h
index cbc4843..fe8b423 100644
--- a/src/include/storage/buf_internals.h
+++ b/src/include/storage/buf_internals.h
@@ -16,6 +16,7 @@
#define BUFMGR_INTERNALS_H
#include "storage/buf.h"
+#include "storage/bufmgr.h"
#include "storage/latch.h"
#include "storage/lwlock.h"
#include "storage/shmem.h"
@@ -208,16 +209,44 @@ extern PGDLLIMPORT LWLockMinimallyPadded *BufferIOLWLockArray;
#define UnlockBufHdr(bufHdr) SpinLockRelease(&(bufHdr)->buf_hdr_lock)
+/*
+ * The PendingWriteback & WritebackContext structure are used to keep
+ * information about pending flush requests to be issued to the OS.
+ */
+typedef struct PendingWriteback
+{
+ /* could store different types of pending flushes here */
+ BufferTag tag;
+} PendingWriteback;
+
+/* typedef forward declared in bufmgr.h */
+typedef struct WritebackContext
+{
+ /* max number of writeback requests to coalesce */
+ int *max_pending;
+
+ /* current number of pending writeback requests */
+ int nr_pending;
+
+ /* pending requests */
+ PendingWriteback pending_writebacks[WRITEBACK_MAX_PENDING_FLUSHES];
+} WritebackContext;
+
/* in buf_init.c */
extern PGDLLIMPORT BufferDescPadded *BufferDescriptors;
+extern PGDLLIMPORT WritebackContext BackendWritebackContext;
/* in localbuf.c */
extern BufferDesc *LocalBufferDescriptors;
/*
- * Internal routines: only called by bufmgr
+ * Internal buffer management routines
*/
+/* bufmgr.c */
+extern void WritebackContextInit(WritebackContext *context, int *max_coalesce);
+extern void IssuePendingWritebacks(WritebackContext *context);
+extern void ScheduleBufferTagForWriteback(WritebackContext *context, BufferTag *tag);
/* freelist.c */
extern BufferDesc *StrategyGetBuffer(BufferAccessStrategy strategy);
diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.h
index 92c4bc5..a4b1b37 100644
--- a/src/include/storage/bufmgr.h
+++ b/src/include/storage/bufmgr.h
@@ -45,16 +45,36 @@ typedef enum
* replay; otherwise same as RBM_NORMAL */
} ReadBufferMode;
+/* forward declared, to avoid having to expose buf_internals.h here */
+struct WritebackContext;
+
/* in globals.c ... this duplicates miscadmin.h */
extern PGDLLIMPORT int NBuffers;
/* in bufmgr.c */
+#define WRITEBACK_MAX_PENDING_FLUSHES 128
+
+/* FIXME: Also default to on for mmap && msync(MS_ASYNC)? */
+#ifdef HAVE_SYNC_FILE_RANGE
+#define DEFAULT_CHECKPOINT_FLUSH_AFTER 32
+#define DEFAULT_BACKEND_FLUSH_AFTER 16
+#define DEFAULT_BGWRITER_FLUSH_AFTER 64
+#else
+#define DEFAULT_CHECKPOINT_FLUSH_AFTER 0
+#define DEFAULT_BACKEND_FLUSH_AFTER 0
+#define DEFAULT_BGWRITER_FLUSH_AFTER 0
+#endif /* HAVE_SYNC_FILE_RANGE */
+
extern bool zero_damaged_pages;
extern int bgwriter_lru_maxpages;
extern double bgwriter_lru_multiplier;
extern bool track_io_timing;
extern int target_prefetch_pages;
+extern int checkpoint_flush_after;
+extern int backend_flush_after;
+extern int bgwriter_flush_after;
+
/* in buf_init.c */
extern PGDLLIMPORT char *BufferBlocks;
@@ -209,7 +229,7 @@ extern bool HoldingBufferPinThatDelaysRecovery(void);
extern void AbortBufferIO(void);
extern void BufmgrCommit(void);
-extern bool BgBufferSync(void);
+extern bool BgBufferSync(struct WritebackContext *wb_context);
extern void AtProcExit_LocalBuffers(void);
diff --git a/src/include/storage/fd.h b/src/include/storage/fd.h
index 4a3fccb..0f67760 100644
--- a/src/include/storage/fd.h
+++ b/src/include/storage/fd.h
@@ -75,6 +75,7 @@ extern int FileSync(File file);
extern off_t FileSeek(File file, off_t offset, int whence);
extern int FileTruncate(File file, off_t offset);
extern char *FilePathName(File file);
+extern void FileWriteback(File file, off_t offset, int amount);
/* Operations that allow use of regular stdio --- USE WITH CAUTION */
extern FILE *AllocateFile(const char *name, const char *mode);
@@ -112,7 +113,7 @@ extern int pg_fsync(int fd);
extern int pg_fsync_no_writethrough(int fd);
extern int pg_fsync_writethrough(int fd);
extern int pg_fdatasync(int fd);
-extern int pg_flush_data(int fd, off_t offset, off_t amount);
+extern void pg_flush_data(int fd, off_t offset, off_t amount);
extern void fsync_fname(char *fname, bool isdir);
extern void SyncDataDirectory(void);
diff --git a/src/include/storage/smgr.h b/src/include/storage/smgr.h
index a7267ea..0483fa3 100644
--- a/src/include/storage/smgr.h
+++ b/src/include/storage/smgr.h
@@ -96,6 +96,8 @@ extern void smgrread(SMgrRelation reln, ForkNumber forknum,
BlockNumber blocknum, char *buffer);
extern void smgrwrite(SMgrRelation reln, ForkNumber forknum,
BlockNumber blocknum, char *buffer, bool skipFsync);
+extern void smgrwriteback(SMgrRelation reln, ForkNumber forknum,
+ BlockNumber blocknum, int nblocks);
extern BlockNumber smgrnblocks(SMgrRelation reln, ForkNumber forknum);
extern void smgrtruncate(SMgrRelation reln, ForkNumber forknum,
BlockNumber nblocks);
@@ -122,6 +124,8 @@ extern void mdread(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum,
char *buffer);
extern void mdwrite(SMgrRelation reln, ForkNumber forknum,
BlockNumber blocknum, char *buffer, bool skipFsync);
+extern void mdwriteback(SMgrRelation reln, ForkNumber forknum,
+ BlockNumber blocknum, int nblocks);
extern BlockNumber mdnblocks(SMgrRelation reln, ForkNumber forknum);
extern void mdtruncate(SMgrRelation reln, ForkNumber forknum,
BlockNumber nblocks);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index d96896b..f501f55 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1411,6 +1411,7 @@ Pattern_Type
PendingOperationEntry
PendingRelDelete
PendingUnlinkEntry
+PendingWriteback
PerlInterpreter
Perl_ppaddr_t
Permutation
@@ -2142,6 +2143,7 @@ WriteBytePtr
WriteDataPtr
WriteExtraTocPtr
WriteFunc
+WritebackContext
X509
X509_NAME
X509_NAME_ENTRY
--
2.7.0.229.g701fa7f
0002-Checkpoint-sorting-and-balancing.patchtext/x-patch; charset=us-asciiDownload
From 73e9eb9fa487aef370c0ffac710e71d0ee431b8d Mon Sep 17 00:00:00 2001
From: Andres Freund <andres@anarazel.de>
Date: Fri, 19 Feb 2016 12:17:51 -0800
Subject: [PATCH 2/4] Checkpoint sorting and balancing.
Up to now checkpoints were written in the order they're in the
BufferDescriptors. That's nearly random in a lot of cases, which
performs badly on rotating media, but even on SSDs it causes slowdowns.
To avoid that, sort checkpoints before writing them out. We currently
sort by tablespace, relfilenode, fork and block number.
Previously that wasn't done out of fear of imbalance between
tablespaces, so additionally balance writes between tablespaces.
Another concern was that the relatively large allocation to sort the
buffers in might fail, preventing checkpoints from happening. Thus
pre-allocate the required memory in shared memory, at server startup.
This particularly makes it more efficient to have checkpoint flushing
enabled, because that'll often result in a lot of writes that can be
coalesced into one flush.
TODO:
* remove debugging output
Discussion: alpine.DEB.2.10.1506011320000.28433@sto
Author: Fabien Coelho and Andres Freund
---
src/backend/storage/buffer/README | 5 -
src/backend/storage/buffer/buf_init.c | 22 ++-
src/backend/storage/buffer/bufmgr.c | 289 +++++++++++++++++++++++++++++-----
src/include/storage/buf_internals.h | 18 +++
src/tools/pgindent/typedefs.list | 2 +
5 files changed, 291 insertions(+), 45 deletions(-)
diff --git a/src/backend/storage/buffer/README b/src/backend/storage/buffer/README
index c4a7668..dc12c8c 100644
--- a/src/backend/storage/buffer/README
+++ b/src/backend/storage/buffer/README
@@ -267,11 +267,6 @@ only needs to take the lock long enough to read the variable value, not
while scanning the buffers. (This is a very substantial improvement in
the contention cost of the writer compared to PG 8.0.)
-During a checkpoint, the writer's strategy must be to write every dirty
-buffer (pinned or not!). We may as well make it start this scan from
-nextVictimBuffer, however, so that the first-to-be-written pages are the
-ones that backends might otherwise have to write for themselves soon.
-
The background writer takes shared content lock on a buffer while writing it
out (and anyone else who flushes buffer contents to disk must do so too).
This ensures that the page image transferred to disk is reasonably consistent.
diff --git a/src/backend/storage/buffer/buf_init.c b/src/backend/storage/buffer/buf_init.c
index e10071d..bfa37f1 100644
--- a/src/backend/storage/buffer/buf_init.c
+++ b/src/backend/storage/buffer/buf_init.c
@@ -24,6 +24,7 @@ LWLockMinimallyPadded *BufferIOLWLockArray = NULL;
LWLockTranche BufferIOLWLockTranche;
LWLockTranche BufferContentLWLockTranche;
WritebackContext BackendWritebackContext;
+CkptSortItem *CkptBufferIds;
/*
@@ -70,7 +71,8 @@ InitBufferPool(void)
{
bool foundBufs,
foundDescs,
- foundIOLocks;
+ foundIOLocks,
+ foundBufCkpt;
/* Align descriptors to a cacheline boundary. */
BufferDescriptors = (BufferDescPadded *)
@@ -104,10 +106,21 @@ InitBufferPool(void)
LWLockRegisterTranche(LWTRANCHE_BUFFER_CONTENT,
&BufferContentLWLockTranche);
- if (foundDescs || foundBufs || foundIOLocks)
+ /*
+ * The array used to sort to-be-checkpointed buffer ids is located in
+ * shared memory, to avoid having to allocate significant amounts of
+ * memory at runtime. As that'd be in the middle of a checkpoint, or when
+ * the checkpointer is restarted, memory allocation failures would be
+ * painful.
+ */
+ CkptBufferIds = (CkptSortItem *)
+ ShmemInitStruct("Checkpoint BufferIds",
+ NBuffers * sizeof(CkptSortItem), &foundBufCkpt);
+
+ if (foundDescs || foundBufs || foundIOLocks || foundBufCkpt)
{
/* should find all of these, or none of them */
- Assert(foundDescs && foundBufs && foundIOLocks);
+ Assert(foundDescs && foundBufs && foundIOLocks && foundBufCkpt);
/* note: this path is only taken in EXEC_BACKEND case */
}
else
@@ -190,5 +203,8 @@ BufferShmemSize(void)
/* to allow aligning the above */
size = add_size(size, PG_CACHE_LINE_SIZE);
+ /* size of checkpoint sort array in bufmgr.c */
+ size = add_size(size, mul_size(NBuffers, sizeof(CkptSortItem)));
+
return size;
}
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index cdbda0f..7a13997 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -37,6 +37,7 @@
#include "catalog/catalog.h"
#include "catalog/storage.h"
#include "executor/instrument.h"
+#include "lib/binaryheap.h"
#include "miscadmin.h"
#include "pg_trace.h"
#include "pgstat.h"
@@ -75,6 +76,34 @@ typedef struct PrivateRefCountEntry
/* 64 bytes, about the size of a cache line on common systems */
#define REFCOUNT_ARRAY_ENTRIES 8
+/*
+ * Status of buffers to checkpoint for a particular tablespace, used
+ * internally in BufferSync.
+ */
+typedef struct CkptTsStatus
+{
+ /* oid of the tablespace */
+ Oid tsId;
+
+ /*
+ * Checkpoint progress for this tablespace. To make progress comparable
+ * between tablespaces the progress is, for each tablespace, measured as a
+ * number between 0 and the total number of to-be-checkpointed pages. Each
+ * page checkpointed in this tablespace increments this space's progress
+ * by progress_slice.
+ */
+ float8 progress;
+ float8 progress_slice;
+
+ /* number of to-be checkpointed pages in this tablespace */
+ int num_to_scan;
+ /* already processed pages in this tablespace */
+ int num_scanned;
+
+ /* current offset in CkptBufferIds for this tablespace */
+ int index;
+} CkptTsStatus;
+
/* GUC variables */
bool zero_damaged_pages = false;
int bgwriter_lru_maxpages = 100;
@@ -425,6 +454,8 @@ static void AtProcExit_Buffers(int code, Datum arg);
static void CheckForBufferLeaks(void);
static int rnode_comparator(const void *p1, const void *p2);
static int buffertag_comparator(const void *p1, const void *p2);
+static int ckpt_buforder_comparator(const void *pa, const void *pb);
+static int ts_ckpt_progress_comparator(Datum a, Datum b, void *arg);
/*
@@ -1657,8 +1688,13 @@ BufferSync(int flags)
{
int buf_id;
int num_to_scan;
- int num_to_write;
+ int num_spaces;
+ int num_processed;
int num_written;
+ CkptTsStatus *per_ts_stat = NULL;
+ Oid last_tsid;
+ binaryheap *ts_heap;
+ int i;
int mask = BM_DIRTY;
WritebackContext wb_context;
@@ -1676,7 +1712,7 @@ BufferSync(int flags)
/*
* Loop over all buffers, and mark the ones that need to be written with
- * BM_CHECKPOINT_NEEDED. Count them as we go (num_to_write), so that we
+ * BM_CHECKPOINT_NEEDED. Count them as we go (num_to_scan), so that we
* can estimate how much work needs to be done.
*
* This allows us to write only those pages that were dirty when the
@@ -1690,7 +1726,7 @@ BufferSync(int flags)
* BM_CHECKPOINT_NEEDED still set. This is OK since any such buffer would
* certainly need to be written for the next checkpoint attempt, too.
*/
- num_to_write = 0;
+ num_to_scan = 0;
for (buf_id = 0; buf_id < NBuffers; buf_id++)
{
BufferDesc *bufHdr = GetBufferDescriptor(buf_id);
@@ -1703,35 +1739,140 @@ BufferSync(int flags)
if ((bufHdr->flags & mask) == mask)
{
+ CkptSortItem *item;
+
bufHdr->flags |= BM_CHECKPOINT_NEEDED;
- num_to_write++;
+
+ item = &CkptBufferIds[num_to_scan++];
+ item->buf_id = buf_id;
+ item->tsId = bufHdr->tag.rnode.spcNode;
+ item->relNode = bufHdr->tag.rnode.relNode;
+ item->forkNum = bufHdr->tag.forkNum;
+ item->blockNum = bufHdr->tag.blockNum;
}
UnlockBufHdr(bufHdr);
}
- if (num_to_write == 0)
+ if (num_to_scan == 0)
return; /* nothing to do */
-
WritebackContextInit(&wb_context, &checkpoint_flush_after);
- TRACE_POSTGRESQL_BUFFER_SYNC_START(NBuffers, num_to_write);
+ TRACE_POSTGRESQL_BUFFER_SYNC_START(NBuffers, num_to_scan);
/*
- * Loop over all buffers again, and write the ones (still) marked with
- * BM_CHECKPOINT_NEEDED. In this loop, we start at the clock sweep point
- * since we might as well dump soon-to-be-recycled buffers first.
- *
- * Note that we don't read the buffer alloc count here --- that should be
- * left untouched till the next BgBufferSync() call.
+ * Sort buffers that need to be written to reduce the likelihood of random
+ * IO. The sorting is also important for the implementation of balancing
+ * writes between tablespaces. Without balancing writes we'd potentially
+ * end up writing to the tablespaces one-by-one; possibly overloading the
+ * underlying system.
*/
- buf_id = StrategySyncStart(NULL, NULL);
- num_to_scan = NBuffers;
+ qsort(CkptBufferIds, num_to_scan, sizeof(CkptSortItem),
+ ckpt_buforder_comparator);
+
+ num_spaces = 0;
+
+ /*
+ * Allocate progress status for each tablespace with buffers that need to
+ * be flushed. This requires the to-be-flushed array to be sorted.
+ */
+ last_tsid = InvalidOid;
+ for (i = 0; i < num_to_scan; i++)
+ {
+ CkptTsStatus *s;
+ Oid cur_tsid;
+
+ cur_tsid = CkptBufferIds[i].tsId;
+
+ /*
+ * Grow array of per-tablespace status structs, everytime a new
+ * tablespace is found.
+ */
+ if (last_tsid == InvalidOid || last_tsid != cur_tsid)
+ {
+ Size sz;
+
+ num_spaces++;
+
+ /*
+ * Not worth adding grow-by-power-of-2 logic here - even with a
+ * few hundred tablespaces this will be fine.
+ */
+ sz = sizeof(CkptTsStatus) * num_spaces;
+
+ if (per_ts_stat == NULL)
+ per_ts_stat = (CkptTsStatus *) palloc(sz);
+ else
+ per_ts_stat = (CkptTsStatus *) repalloc(per_ts_stat, sz);
+
+ s = &per_ts_stat[num_spaces - 1];
+ memset(s, 0, sizeof(*s));
+ s->tsId = cur_tsid;
+
+ /*
+ * The first buffer in this tablespace. As CkptBufferIds is sorted
+ * by tablespace all (s->num_to_scan) buffers in this tablespace
+ * will follow afterwards.
+ */
+ s->index = i;
+
+ /*
+ * progress_slice will be determined once we know how many buffers
+ * are in each tablespace, i.e. after this loop.
+ */
+
+ last_tsid = cur_tsid;
+ }
+ else
+ {
+ s = &per_ts_stat[num_spaces - 1];
+ }
+
+ s->num_to_scan++;
+ }
+
+ Assert(num_spaces > 0);
+
+ /*
+ * Build a min-heap over the write-progress in the individual tablespaces,
+ * and compute how large a portion of the total progress a single
+ * processed buffer is.
+ */
+ ts_heap = binaryheap_allocate(num_spaces,
+ ts_ckpt_progress_comparator,
+ NULL);
+
+ for (i = 0; i < num_spaces; i++)
+ {
+ CkptTsStatus *ts_stat = &per_ts_stat[i];
+
+ ts_stat->progress_slice = (float8) num_to_scan / ts_stat->num_to_scan;
+
+ binaryheap_add_unordered(ts_heap, PointerGetDatum(ts_stat));
+ }
+
+ binaryheap_build(ts_heap);
+
+ /*
+ * Iterate through to-be-checkpointed buffers and write the ones (still)
+ * marked with BM_CHECKPOINT_NEEDED. The writes are balanced between
+ * tablespaces.
+ */
+ num_processed = 0;
num_written = 0;
- while (num_to_scan-- > 0)
+ while (!binaryheap_empty(ts_heap))
{
- BufferDesc *bufHdr = GetBufferDescriptor(buf_id);
+ BufferDesc *bufHdr = NULL;
+ CkptTsStatus *ts_stat = (CkptTsStatus *)
+ DatumGetPointer(binaryheap_first(ts_heap));
+
+ buf_id = CkptBufferIds[ts_stat->index].buf_id;
+ Assert(buf_id != -1);
+
+ bufHdr = GetBufferDescriptor(buf_id);
+
+ num_processed++;
/*
* We don't need to acquire the lock here, because we're only looking
@@ -1752,31 +1893,52 @@ BufferSync(int flags)
TRACE_POSTGRESQL_BUFFER_SYNC_WRITTEN(buf_id);
BgWriterStats.m_buf_written_checkpoints++;
num_written++;
+ }
+ }
- /*
- * We know there are at most num_to_write buffers with
- * BM_CHECKPOINT_NEEDED set; so we can stop scanning if
- * num_written reaches num_to_write.
- *
- * Note that num_written doesn't include buffers written by
- * other backends, or by the bgwriter cleaning scan. That
- * means that the estimate of how much progress we've made is
- * conservative, and also that this test will often fail to
- * trigger. But it seems worth making anyway.
- */
- if (num_written >= num_to_write)
- break;
+ /*
+ * Measure progress independent of actualy having to flush the buffer
+ * - otherwise writing become unbalanced.
+ */
+ ts_stat->progress += ts_stat->progress_slice;
+ ts_stat->num_scanned++;
+ ts_stat->index++;
- /*
- * Sleep to throttle our I/O rate.
- */
- CheckpointWriteDelay(flags, (double) num_written / num_to_write);
- }
+ /* Have all the buffers from the tablespace been processed? */
+ if (ts_stat->num_scanned == ts_stat->num_to_scan)
+ {
+ binaryheap_remove_first(ts_heap);
+ }
+ else
+ {
+ /* update heap with the new progress */
+ binaryheap_replace_first(ts_heap, PointerGetDatum(ts_stat));
}
- if (++buf_id >= NBuffers)
- buf_id = 0;
+ /*
+ * Sleep to throttle our I/O rate.
+ */
+ CheckpointWriteDelay(flags, (double) num_processed / num_to_scan);
+
+/* #define CHECKPOINT_PROGRESS */
+#ifdef CHECKPOINT_PROGRESS
+ /* FIXME: remove before commit */
+ /* delete current content of the line, print progress */
+ fprintf(stderr, "\33[2K\rto_scan: %d, scanned: %d, %%processed: %.2f, %%writeouts: %.2f",
+ num_to_scan, num_processed,
+ (((double) num_processed) / num_to_scan) * 100,
+ ((double) num_written / num_processed) * 100);
+#endif
}
+#ifdef CHECKPOINT_PROGRESS
+ fprintf(stderr, "\n");
+#endif
+
+ /* issue all pending flushes */
+ IssuePendingWritebacks(&wb_context);
+
+ pfree(per_ts_stat);
+ per_ts_stat = NULL;
/*
* Update checkpoint statistics. As noted above, this doesn't include
@@ -3754,6 +3916,59 @@ rnode_comparator(const void *p1, const void *p2)
return 0;
}
+/*
+ * Comparator determining the writeout order in a checkpoint.
+ *
+ * It is important that tablespaces are compared first, the logic balancing
+ * writes between tablespaces relies on it.
+ */
+static int
+ckpt_buforder_comparator(const void *pa, const void *pb)
+{
+ const CkptSortItem *a = (CkptSortItem *) pa;
+ const CkptSortItem *b = (CkptSortItem *) pb;
+
+ /* compare tablespace */
+ if (a->tsId < b->tsId)
+ return -1;
+ else if (a->tsId > b->tsId)
+ return 1;
+ /* compare relation */
+ if (a->relNode < b->relNode)
+ return -1;
+ else if (a->relNode > b->relNode)
+ return 1;
+ /* compare fork */
+ else if (a->forkNum < b->forkNum)
+ return -1;
+ else if (a->forkNum > b->forkNum)
+ return 1;
+ /* compare block number */
+ else if (a->blockNum < b->blockNum)
+ return -1;
+ else /* should not be the same block anyway... */
+ return 1;
+}
+
+/*
+ * Comparator for a Min-Heap over the, per-tablespace, checkpoint completion
+ * progress.
+ */
+static int
+ts_ckpt_progress_comparator(Datum a, Datum b, void *arg)
+{
+ CkptTsStatus *sa = (CkptTsStatus *) a;
+ CkptTsStatus *sb = (CkptTsStatus *) b;
+
+ /* we want a min-heap, so return 1 for the a < b */
+ if (sa->progress < sb->progress)
+ return 1;
+ else if (sa->progress == sb->progress)
+ return 0;
+ else
+ return -1;
+}
+
/*
* BufferTag comparator.
diff --git a/src/include/storage/buf_internals.h b/src/include/storage/buf_internals.h
index fe8b423..de84bc4 100644
--- a/src/include/storage/buf_internals.h
+++ b/src/include/storage/buf_internals.h
@@ -239,6 +239,24 @@ extern PGDLLIMPORT WritebackContext BackendWritebackContext;
/* in localbuf.c */
extern BufferDesc *LocalBufferDescriptors;
+/* in bufmgr.c */
+
+/*
+ * Structure to sort buffers per file on checkpoints.
+ *
+ * This structure is allocated per buffer in shared memory, so it should be
+ * kept as small as possible.
+ */
+typedef struct CkptSortItem
+{
+ Oid tsId;
+ Oid relNode;
+ ForkNumber forkNum;
+ BlockNumber blockNum;
+ int buf_id;
+} CkptSortItem;
+
+extern CkptSortItem *CkptBufferIds;
/*
* Internal buffer management routines
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index f501f55..b850db0 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -283,6 +283,8 @@ CheckpointerRequest
CheckpointerShmemStruct
Chromosome
City
+CkptSortItem
+CkptTsStatus
ClientAuthentication_hook_type
ClientData
ClonePtr
--
2.7.0.229.g701fa7f
Hello Andres,
Here's the next two (the most important) patches of the series:
0001: Allow to trigger kernel writeback after a configurable number of writes.
0002: Checkpoint sorting and balancing.
I will look into these two in depth.
Note that I would have ordered them in reverse because sorting is nearly
always very beneficial, and "writeback" (formely called flushing) is then
nearly always very beneficial on sorted buffers.
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 2016-02-19 22:46:44 +0100, Fabien COELHO wrote:
Hello Andres,
Here's the next two (the most important) patches of the series:
0001: Allow to trigger kernel writeback after a configurable number of writes.
0002: Checkpoint sorting and balancing.I will look into these two in depth.
Note that I would have ordered them in reverse because sorting is nearly
always very beneficial, and "writeback" (formely called flushing) is then
nearly always very beneficial on sorted buffers.
I had it that way earlier. I actually saw pretty large regressions from
sorting alone in some cases as well, apparently because the kernel
submits much larger IOs to disk; although that probably only shows on
SSDs. This way the modifications imo look a trifle better ;). I'm
intending to commit both at the same time, keep them separate only
because they're easier to ynderstand separately.
Andres
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Sat, Feb 20, 2016 at 5:08 AM, Fabien COELHO <coelho@cri.ensmp.fr> wrote:
Kernel 3.2 is extremely bad for Postgresql, as the vm seems to amplify IO
somehow. The difference to 3.13 (the latest LTS kernel for 12.04) is huge.Interesting! To summarize it, 25% performance degradation from best kernel
(2.6.32) to worst (3.2.0), that is indeed significant.
As far as I recall, the OS cache eviction is very aggressive in 3.2,
so it would be possible that data from the FS cache that was just read
could be evicted even if it was not used yet. Thie represents a large
difference when the database does not fit in RAM.
--
Michael
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Hello Andres,
For 0001 I've recently changed:
* Don't schedule writeback after smgrextend() - that defeats linux
delayed allocation mechanism, increasing fragmentation noticeably.
* Add docs for the new GUC variables
* comment polishing
* BackendWritebackContext now isn't dynamically allocated anymoreI think this patch primarily needs:
* review of the docs, not sure if they're easy enough to
understand. Some language polishing might also be needed.
Yep, see below.
* review of the writeback API, combined with the smgr/md.c changes.
See various comments below.
* Currently *_flush_after can be set to a nonzero value, even if there's
no support for flushing on that platform. Imo that's ok, but perhaps
other people's opinion differ.
In some previous version I think a warning was shown of the feature was
requested but not available.
Here are some quick comments on the patch:
Patch applies cleanly on head. Compiled and checked on Linux. Compilation
issues on other systems, see below.
When pages are written by a process (checkpointer, bgwriter, backend worker),
the list of recently written pages is kept and every so often an advisory
fsync (sync_file_range, other options for other systems) is issued so that
the data is sent to the io system without relying on more or less
(un)controllable os policy.
The documentation seems to use "flush" but the code talks about "writeback"
or "flush", depending. I think one vocabulary, whichever it is, should be
chosen and everything should stick to it, otherwise everything look kind of
fuzzy and raises doubt for the reader (is it the same thing? is it something
else?). I initially used "flush", but it seems a bad idea because it has
nothing to do with the flush function, so I'm fine with writeback or anything
else, I just think that *one* word should be chosen and used everywhere.
The sgml documentation about "*_flush_after" configuration parameter talks
about bytes, but the actual unit should be buffers. I think that keeping
a number of buffers should be fine, because that is what the internal stuff
will manage, not bytes. Also, the maximum value (128 ?) should appear in
the text. In the discussion in the wal section, I'm not sure about the effect
of setting writebacks on SSD, but I think that you have made some tests so
maybe you have an answer and the corresponding section could be written with
some more definitive text than "probably brings no benefit on SSD".
A good point of the whole approach is that it is available to all kind
of pg processes. However it does not address the point that bgwriter and
backends basically issue random writes, so I would not expect much positive
effect before these writes are somehow sorted, which means doing some
compromise in the LRU/LFU logic... well, all this is best kept for later,
and I'm fine to have the logic flushing logic there. I'm wondering why you
choose 16 & 64 as default for backends & bgwriter, though.
IssuePendingWritebacks: you merge only strictly neightboring writes.
Maybe the merging strategy could be more aggressive than just strict
neighbors?
mdwriteback: all variables could be declared within the while, I do not
understand why some are in and some are out. ISTM that putting writeback
management at the relation level does not help a lot, because you have to
translate again from relation to files. The good news is that it should work
as well, and that it does avoid the issue that the file may have been closed
in between, so why not.
The PendingWriteback struct looks useless. I think it should be removed,
and maybe put back if one day if it is needed, which I rather doubt it.
struct WritebackContext: keeping a pointer to guc variables is a kind of
trick, I think it deserves a comment.
ScheduleBufferTagForWriteback: the "pending" variable is not very useful.
Maybe consider shortening the "pending_writebacks" field name to "writebacks"?
IssuePendingWritebacks: I understand that qsort is needed "again"
because when balancing writes over tablespaces they may be intermixed.
AFAICR I used a "flush context" for each table space in some version
I submitted, because I do think that this whole writeback logic really
does make sense *per table space*, which suggest that there should be as
many write backs contexts as table spaces, otherwise the positive effect
may going to be totally lost of tables spaces are used. Any thoughts?
Assert(*context->max_pending <= WRITEBACK_MAX_PENDING_FLUSHES); is always
true, I think, it is already checked in the initialization and when setting
gucs.
SyncOneBuffer: I'm wonder why you copy the tag after releasing the lock.
I guess it is okay because it is still pinned.
pg_flush_data: in the first #elif, "context" is undeclared line 446.
Label "out" is not defined line 455. In the second #elif, "context" is
undeclared line 490 and label "out" line 500 is not defined either.
For the checkpointer, a key aspect is that the scheduling process goes
to sleep from time to time, and this sleep time looked like a great
opportunity to do this kind of flushing. You choose not to take advantage
of the behavior, why?
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Hi,
On 2016-02-20 20:56:31 +0100, Fabien COELHO wrote:
* Currently *_flush_after can be set to a nonzero value, even if there's
no support for flushing on that platform. Imo that's ok, but perhaps
other people's opinion differ.In some previous version I think a warning was shown of the feature was
requested but not available.
I think we should either silently ignore it, or error out. Warnings
somewhere in the background aren't particularly meaningful.
Here are some quick comments on the patch:
Patch applies cleanly on head. Compiled and checked on Linux. Compilation
issues on other systems, see below.
For those I've already pushed a small fixup commit to git... Stupid
mistake.
The documentation seems to use "flush" but the code talks about "writeback"
or "flush", depending. I think one vocabulary, whichever it is, should be
chosen and everything should stick to it, otherwise everything look kind of
fuzzy and raises doubt for the reader (is it the same thing? is it something
else?). I initially used "flush", but it seems a bad idea because it has
nothing to do with the flush function, so I'm fine with writeback or anything
else, I just think that *one* word should be chosen and used everywhere.
Hm.
The sgml documentation about "*_flush_after" configuration parameter talks
about bytes, but the actual unit should be buffers.
The unit actually is buffers, but you can configure it using
bytes. We've done the same for other GUCs (shared_buffers, wal_buffers,
...). Refering to bytes is easier because you don't have to explain that
it depends on compilation settings how many data it actually is and
such.
Also, the maximum value (128 ?) should appear in the text. \
Right.
In the discussion in the wal section, I'm not sure about the effect of
setting writebacks on SSD, but I think that you have made some tests
so maybe you have an answer and the corresponding section could be
written with some more definitive text than "probably brings no
benefit on SSD".
Yea, that paragraph needs some editing. I think we should basically
remove that last sentence.
A good point of the whole approach is that it is available to all kind
of pg processes.
Exactly.
However it does not address the point that bgwriter and
backends basically issue random writes, so I would not expect much positive
effect before these writes are somehow sorted, which means doing some
compromise in the LRU/LFU logic...
The benefit is primarily that you don't collect large amounts of dirty
buffers in the kernel page cache. In most cases the kernel will not be
able to coalesce these writes either... I've measured *massive*
performance latency differences for workloads that are bigger than
shared buffers - because suddenly bgwriter / backends do the majority of
the writes. Flushing in the checkpoint quite possibly makes nearly no
difference in such cases.
well, all this is best kept for later, and I'm fine to have the logic
flushing logic there. I'm wondering why you choose 16 & 64 as default
for backends & bgwriter, though.
I chose a small value for backends because there often are a large
number of backends, and thus the amount of dirty data of each adds up. I
used a larger value for bgwriter because I saw that ending up using
bigger IOs.
IssuePendingWritebacks: you merge only strictly neightboring writes.
Maybe the merging strategy could be more aggressive than just strict
neighbors?
I don't think so. If you flush more than neighbouring writes you'll
often end up flushing buffers dirtied by another backend, causing
additional stalls. And if the writes aren't actually neighbouring
there's not much gained from issuing them in one sync_file_range call.
mdwriteback: all variables could be declared within the while, I do not
understand why some are in and some are out.
Right.
ISTM that putting writeback management at the relation level does not
help a lot, because you have to translate again from relation to
files.
Sure, but what's the problem with that? That's how normal read/write IO
works as well?
struct WritebackContext: keeping a pointer to guc variables is a kind of
trick, I think it deserves a comment.
It has, it's just in WritebackContextInit(). Can duplicateit.
ScheduleBufferTagForWriteback: the "pending" variable is not very
useful.
Shortens line length a good bit, at no cost.
IssuePendingWritebacks: I understand that qsort is needed "again"
because when balancing writes over tablespaces they may be intermixed.
Also because the infrastructure is used for more than checkpoint
writes. There's absolutely no ordering guarantees there.
AFAICR I used a "flush context" for each table space in some version
I submitted, because I do think that this whole writeback logic really
does make sense *per table space*, which suggest that there should be as
many write backs contexts as table spaces, otherwise the positive effect
may going to be totally lost of tables spaces are used. Any thoughts?
Leads to less regular IO, because if your tablespaces are evenly sized
(somewhat common) you'll sometimes end up issuing sync_file_range's
shortly after each other. For latency outside checkpoints it's
important to control the total amount of dirty buffers, and that's
obviously independent of tablespaces.
SyncOneBuffer: I'm wonder why you copy the tag after releasing the lock.
I guess it is okay because it is still pinned.
Don't do things while holding a lock that don't require said lock. A pin
prevents a buffer changing its identity.
For the checkpointer, a key aspect is that the scheduling process goes
to sleep from time to time, and this sleep time looked like a great
opportunity to do this kind of flushing. You choose not to take advantage
of the behavior, why?
Several reasons: Most importantly there's absolutely no guarantee that
you'll ever end up sleeping, it's quite common to happen only
seldomly. If you're bottlenecked on IO, you can end up being behind all
the time. But even then you don't want to cause massive latency spikes
due to gigabytes of dirty data - a slower checkpoint is a much better
choice. It'd make the writeback infrastructure less generic. I also
don't really believe it helps that much, although that's a complex
argument to make.
Thanks for the review!
Andres
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Sun, Feb 21, 2016 at 3:37 AM, Andres Freund <andres@anarazel.de> wrote:
The documentation seems to use "flush" but the code talks about "writeback"
or "flush", depending. I think one vocabulary, whichever it is, should be
chosen and everything should stick to it, otherwise everything look kind of
fuzzy and raises doubt for the reader (is it the same thing? is it something
else?). I initially used "flush", but it seems a bad idea because it has
nothing to do with the flush function, so I'm fine with writeback or anything
else, I just think that *one* word should be chosen and used everywhere.Hm.
I think there might be a semantic distinction between these two terms.
Doesn't writeback mean writing pages to disk, and flushing mean making
sure that they are durably on disk? So for example when the Linux
kernel thinks there is too much dirty data, it initiates writeback,
not a flush; on the other hand, at transaction commit, we initiate a
flush, not writeback.
--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Hallo Andres,
In some previous version I think a warning was shown if the feature was
requested but not available.I think we should either silently ignore it, or error out. Warnings
somewhere in the background aren't particularly meaningful.
I like "ignoring with a warning" in the log file, because when things do
not behave as expected that is where I'll be looking. I do not think that
it should error out.
The sgml documentation about "*_flush_after" configuration parameter
talks about bytes, but the actual unit should be buffers.The unit actually is buffers, but you can configure it using
bytes. We've done the same for other GUCs (shared_buffers, wal_buffers,
...). Refering to bytes is easier because you don't have to explain that
it depends on compilation settings how many data it actually is and
such.
So I understand that it works with kb as well. Now I do not think that it
would need a lot if explanations if you say that it is a number of pages,
and I think that a number of pages is significant because it is a number
of IO requests to be coalesced, eventually.
In the discussion in the wal section, I'm not sure about the effect of
setting writebacks on SSD, [...]Yea, that paragraph needs some editing. I think we should basically
remove that last sentence.
Ok, fine with me. Does that mean that flushing as a significant positive
impact on SSD in your tests?
However it does not address the point that bgwriter and backends
basically issue random writes, [...]The benefit is primarily that you don't collect large amounts of dirty
buffers in the kernel page cache. In most cases the kernel will not be
able to coalesce these writes either... I've measured *massive*
performance latency differences for workloads that are bigger than
shared buffers - because suddenly bgwriter / backends do the majority of
the writes. Flushing in the checkpoint quite possibly makes nearly no
difference in such cases.
So I understand that there is a positive impact under some load. Good!
Maybe the merging strategy could be more aggressive than just strict
neighbors?I don't think so. If you flush more than neighbouring writes you'll
often end up flushing buffers dirtied by another backend, causing
additional stalls.
Ok. Maybe the neightbor definition could be relaxed just a little bit so
that small holes are overtake, but not large holes? If there is only a few
pages in between, even if written by another process, then writing them
together should be better? Well, this can wait for a clear case, because
hopefully the OS will recoalesce them behind anyway.
struct WritebackContext: keeping a pointer to guc variables is a kind of
trick, I think it deserves a comment.It has, it's just in WritebackContextInit(). Can duplicateit.
I missed it, I expected something in the struct definition. Do not
duplicate, but cross reference it?
IssuePendingWritebacks: I understand that qsort is needed "again"
because when balancing writes over tablespaces they may be intermixed.Also because the infrastructure is used for more than checkpoint
writes. There's absolutely no ordering guarantees there.
Yep, but not much benefit to expect from a few dozens random pages either.
[...] I do think that this whole writeback logic really does make sense
*per table space*,Leads to less regular IO, because if your tablespaces are evenly sized
(somewhat common) you'll sometimes end up issuing sync_file_range's
shortly after each other. For latency outside checkpoints it's
important to control the total amount of dirty buffers, and that's
obviously independent of tablespaces.
I do not understand/buy this argument.
The underlying IO queue is per device, and table spaces should be per
device as well (otherwise what the point?), so you should want to coalesce
and "writeback" pages per device as wel. Calling sync_file_range on
distinct devices should probably be issued more or less randomly, and
should not interfere one with the other.
If you use just one context, the more table spaces the less performance
gains, because there is less and less aggregation thus sequential writes
per device.
So for me there should really be one context per tablespace. That would
suggest a hashtable or some other structure to keep and retrieve them,
which would not be that bad, and I think that it is what is needed.
For the checkpointer, a key aspect is that the scheduling process goes
to sleep from time to time, and this sleep time looked like a great
opportunity to do this kind of flushing. You choose not to take advantage
of the behavior, why?Several reasons: Most importantly there's absolutely no guarantee that
you'll ever end up sleeping, it's quite common to happen only seldomly.
Well, that would be under a situation when pg is completely unresponsive.
More so, this behavior *makes* pg unresponsive.
If you're bottlenecked on IO, you can end up being behind all the time.
Hopefully sorting & flushing should improve this situation a lot.
But even then you don't want to cause massive latency spikes
due to gigabytes of dirty data - a slower checkpoint is a much better
choice. It'd make the writeback infrastructure less generic.
Sure. It would be sufficient to have a call to ask for writebacks
independently of the number of writebacks accumulated in the queue, it
does not need to change the infrastructure.
Also, I think that such a call would make sense at the end of the
checkpoint.
I also don't really believe it helps that much, although that's a
complex argument to make.
Yep. My thinking is that doing things in the sleeping interval does not
interfere with the checkpointer scheduling, so it is less likely to go
wrong and falling behind.
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Hallo Andres,
[...] I do think that this whole writeback logic really does make sense
*per table space*,Leads to less regular IO, because if your tablespaces are evenly sized
(somewhat common) you'll sometimes end up issuing sync_file_range's
shortly after each other. For latency outside checkpoints it's
important to control the total amount of dirty buffers, and that's
obviously independent of tablespaces.I do not understand/buy this argument.
The underlying IO queue is per device, and table spaces should be per device
as well (otherwise what the point?), so you should want to coalesce and
"writeback" pages per device as wel. Calling sync_file_range on distinct
devices should probably be issued more or less randomly, and should not
interfere one with the other.If you use just one context, the more table spaces the less performance
gains, because there is less and less aggregation thus sequential writes per
device.So for me there should really be one context per tablespace. That would
suggest a hashtable or some other structure to keep and retrieve them, which
would not be that bad, and I think that it is what is needed.
Note: I think that an easy way to do that in the "checkpoint sort" patch
is simply to keep a WritebackContext in CkptTsStatus structure which is
per table space in the checkpointer.
For bgwriter & backends it can wait, there is few "writeback" coalescing
because IO should be pretty random, so it does not matter much.
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Hallo Andres,
Here is a review for the second patch.
For 0002 I've recently changed:
* Removed the sort timing information, we've proven sufficiently that
it doesn't take a lot of time.
I put it there initialy to demonstrate that there was no cache performance
issue when sorting on just buffer indexes. As it is always small, I agree
that it is not needed. Well, it could be still be in seconds on a very
large shared buffers setting with a very large checkpoint, but then the
checkpoint would be tremendously huge...
* Minor comment polishing.
Patch applies and checks on Linux.
* CpktSortItem:
I think that allocating 20 bytes per buffer in shared memory is a little
on the heavy side. Some compression can be achieved: sizeof(ForlNum) is 4
bytes to hold 4 values, could be one byte or even 2 bits somewhere. Also,
there are very few tablespaces, they could be given a small number and
this number could be used instead of the Oid, so the space requirement
could be reduced to say 16 bytes per buffer by combining space & fork in 2
shorts and keeping 4 bytes alignement and also getting 8 byte
alignement... If this is too much, I have shown that it can work with only
4 bytes per buffer, as the sorting is really just a performance
optimisation and is not broken if some stuff changes between sorting &
writeback, but you did not like the idea. If the amount of shared memory
required is a significant concern, it could be resurrected, though.
* CkptTsStatus:
As I suggested in the other mail, I think that this structure should also keep
a per tablespace WritebackContext so that coalescing is done per tablespace.
ISTM that "progress" and "progress_slice" only depend on num_scanned and
per-tablespace num_to_scan and total num_to_scan, so they are somehow
redundant and the progress could be recomputed from the initial figures
when needed.
If these fields are kept, I think that a comment should justify why float8
precision is okay for the purpose. I think it is quite certainly fine in
the worst case with 32 bits buffer_ids, but it would not be if this size
is changed someday.
* BufferSync
After a first sweep to collect buffers to write, they are sorted, and then
there those buffers are swept again to compute some per tablespace data
and organise a heap.
ISTM that nearly all of the collected data on the second sweep could be
collected on the first sweep, so that this second sweep could be avoided
altogether. The only missing data is the index of the first buffer in the
array, which can be computed by considering tablespaces only, sweeping
over buffers is not needed. That would suggest creating the heap or using
a hash in the initial buffer sweep to keep this information. This would
also provide a point where to number tablespaces for compressing the
CkptSortItem struct.
I'm wondering about calling CheckpointWriteDelay on each round, maybe
a minimum amount of write would make sense. This remark is independent of
this patch. Probably it works fine because after a sleep the checkpointer
is behind enough so that it will write a bunch of buffers before sleeping
again.
I see a binary_heap_allocate but no corresponding deallocation, this
looks like a memory leak... or is there some magic involved?
There are some debug stuff to remove in #ifdefs.
I think that the buffer/README should be updated with explanations about
sorting in the checkpointer.
I think this patch primarily needs:
* Benchmarking on FreeBSD/OSX to see whether we should enable the
mmap()/msync(MS_ASYNC) method by default. Unless somebody does so, I'm
inclined to leave it off till then.
I do not have that. As "msync" seems available on Linux, it is possible to
force using it with a "ifdef 0" to skip sync_file_range and check whether
it does some good there. Idem for the "posix_fadvise" stuff. I can try to
do that, but it takes time to do so, if someone can test on other OS it
would be much better. I think that if it works it should be kept in, so it
is just a matter of testing it.
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 2016-02-21 08:26:28 +0100, Fabien COELHO wrote:
In the discussion in the wal section, I'm not sure about the effect of
setting writebacks on SSD, [...]Yea, that paragraph needs some editing. I think we should basically
remove that last sentence.Ok, fine with me. Does that mean that flushing as a significant positive
impact on SSD in your tests?
Yes. The reason we need flushing is that the kernel amasses dirty pages,
and then flushes them at once. That hurts for both SSDs and rotational
media. Sorting is the the bigger question, but I've seen it have clearly
beneficial performance impacts. I guess if you look at devices with a
internal block size bigger than 8k, you'd even see larger differences.
Maybe the merging strategy could be more aggressive than just strict
neighbors?I don't think so. If you flush more than neighbouring writes you'll
often end up flushing buffers dirtied by another backend, causing
additional stalls.Ok. Maybe the neightbor definition could be relaxed just a little bit so
that small holes are overtake, but not large holes? If there is only a few
pages in between, even if written by another process, then writing them
together should be better? Well, this can wait for a clear case, because
hopefully the OS will recoalesce them behind anyway.
I'm against doing so without clear measurements of a benefit.
Also because the infrastructure is used for more than checkpoint
writes. There's absolutely no ordering guarantees there.Yep, but not much benefit to expect from a few dozens random pages either.
Actually, there's kinda frequently a benefit observable. Even if few
requests can be merged, doing IO requests in an order more likely doable
within a few rotations is beneficial. Also, the cost is marginal, so why
worry?
[...] I do think that this whole writeback logic really does make
sense *per table space*,Leads to less regular IO, because if your tablespaces are evenly sized
(somewhat common) you'll sometimes end up issuing sync_file_range's
shortly after each other. For latency outside checkpoints it's
important to control the total amount of dirty buffers, and that's
obviously independent of tablespaces.I do not understand/buy this argument.
The underlying IO queue is per device, and table spaces should be per device
as well (otherwise what the point?), so you should want to coalesce and
"writeback" pages per device as wel. Calling sync_file_range on distinct
devices should probably be issued more or less randomly, and should not
interfere one with the other.
The kernel's dirty buffer accounting is global, not per block device.
It's also actually rather common to have multiple tablespaces on a
single block device. Especially if SANs and such are involved; where you
don't even know which partitions are on which disks.
If you use just one context, the more table spaces the less performance
gains, because there is less and less aggregation thus sequential writes per
device.So for me there should really be one context per tablespace. That would
suggest a hashtable or some other structure to keep and retrieve them, which
would not be that bad, and I think that it is what is needed.
That'd be much easier to do by just keeping the context in the
per-tablespace struct. But anyway, I'm really doubtful about going for
that; I had it that way earlier, and observing IO showed it not being
beneficial.
For the checkpointer, a key aspect is that the scheduling process goes
to sleep from time to time, and this sleep time looked like a great
opportunity to do this kind of flushing. You choose not to take advantage
of the behavior, why?Several reasons: Most importantly there's absolutely no guarantee that
you'll ever end up sleeping, it's quite common to happen only seldomly.Well, that would be under a situation when pg is completely unresponsive.
More so, this behavior *makes* pg unresponsive.
No. The checkpointer being bottlenecked on actual IO performance doesn't
impact production that badly. It'll just sometimes block in
sync_file_range(), but the IO queues will have enough space to
frequently give way to other backends, particularly to synchronous reads
(most pg reads) and synchronous writes (fdatasync()). So a single
checkpoint will take a bit longer, but otherwise the system will mostly
keep up the work in a regular manner. Without the sync_file_range()
calls the kernel will amass dirty buffers until global dirty limits are
reached, which then will bring the whole system to a standstill.
It's pretty common that checkpoint_timeout is too short to be able to
write all shared_buffers out, in that case it's much better to slow down
the whole checkpoint, instead of being incredibly slow at the end.
I also don't really believe it helps that much, although that's a complex
argument to make.Yep. My thinking is that doing things in the sleeping interval does not
interfere with the checkpointer scheduling, so it is less likely to go wrong
and falling behind.
I don't really see why that's the case. Triggering writeback every N
writes doesn't really influence the scheduling in a bad way - the
flushing is done *before* computing the sleep time. Triggering the
writeback *after* computing the sleep time, and then sleep for that
long, in addition of the time for sync_file_range, skews things more.
Greetings,
Andres Freund
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Hi,
On 2016-02-21 10:52:45 +0100, Fabien COELHO wrote:
* CpktSortItem:
I think that allocating 20 bytes per buffer in shared memory is a little on
the heavy side. Some compression can be achieved: sizeof(ForlNum) is 4 bytes
to hold 4 values, could be one byte or even 2 bits somewhere. Also, there
are very few tablespaces, they could be given a small number and this number
could be used instead of the Oid, so the space requirement could be reduced
to say 16 bytes per buffer by combining space & fork in 2 shorts and keeping
4 bytes alignement and also getting 8 byte alignement... If this is too
much, I have shown that it can work with only 4 bytes per buffer, as the
sorting is really just a performance optimisation and is not broken if some
stuff changes between sorting & writeback, but you did not like the idea. If
the amount of shared memory required is a significant concern, it could be
resurrected, though.
This is less than 0.2 % of memory related to shared buffers. We have the
same amount of memory allocated in CheckpointerShmemSize(), and nobody
has complained so far. And sorry, going back to the previous approach
isn't going to fly, and I've no desire to discuss that *again*.
ISTM that "progress" and "progress_slice" only depend on num_scanned and
per-tablespace num_to_scan and total num_to_scan, so they are somehow
redundant and the progress could be recomputed from the initial figures
when needed.
They don't cause much space usage, and we access the values
frequently. So why not store them?
If these fields are kept, I think that a comment should justify why float8
precision is okay for the purpose. I think it is quite certainly fine in the
worst case with 32 bits buffer_ids, but it would not be if this size is
changed someday.
That seems pretty much unrelated to having the fields - the question of
accuracy plays a role regardless, no? Given realistic amounts of memory
the max potential "skew" seems fairly small with float8. If we ever
flush one buffer "too much" for a tablespace it's pretty much harmless.
ISTM that nearly all of the collected data on the second sweep could be
collected on the first sweep, so that this second sweep could be avoided
altogether. The only missing data is the index of the first buffer in the
array, which can be computed by considering tablespaces only, sweeping over
buffers is not needed. That would suggest creating the heap or using a hash
in the initial buffer sweep to keep this information. This would also
provide a point where to number tablespaces for compressing the CkptSortItem
struct.
Doesn't seem worth the complexity to me.
I'm wondering about calling CheckpointWriteDelay on each round, maybe
a minimum amount of write would make sense.
Why? There's not really much benefit of doing more work than needed. I
think we should sleep far shorter in many cases, but that's indeed a
separate issue.
I see a binary_heap_allocate but no corresponding deallocation, this
looks like a memory leak... or is there some magic involved?
Hm. I think we really should use a memory context for all of this - we
could after all error out somewhere in the middle...
I think this patch primarily needs:
* Benchmarking on FreeBSD/OSX to see whether we should enable the
mmap()/msync(MS_ASYNC) method by default. Unless somebody does so, I'm
inclined to leave it off till then.I do not have that. As "msync" seems available on Linux, it is possible to
force using it with a "ifdef 0" to skip sync_file_range and check whether it
does some good there.
Unfortunately it doesn't work well on linux:
* On many OSs msync() on a mmap'ed file triggers writeback. On linux
* it only does so when MS_SYNC is specified, but then it does the
* writeback synchronously. Luckily all common linux systems have
* sync_file_range(). This is preferrable over FADV_DONTNEED because
* it doesn't flush out clean data.
I've verified beforehand, with a simple demo program, that
msync(MS_ASYNC) does something reasonable of freebsd...
Idem for the "posix_fadvise" stuff. I can try to do
that, but it takes time to do so, if someone can test on other OS it would
be much better. I think that if it works it should be kept in, so it is just
a matter of testing it.
I'm not arguing for ripping it out, what I mean is that we don't set a
nondefault value for the GUCs on platforms with just posix_fadivise
available...
Greetings,
Andres Freund
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
[...] I do think that this whole writeback logic really does make
sense *per table space*,Leads to less regular IO, because if your tablespaces are evenly sized
(somewhat common) you'll sometimes end up issuing sync_file_range's
shortly after each other. For latency outside checkpoints it's
important to control the total amount of dirty buffers, and that's
obviously independent of tablespaces.I do not understand/buy this argument.
The underlying IO queue is per device, and table spaces should be per device
as well (otherwise what the point?), so you should want to coalesce and
"writeback" pages per device as wel. Calling sync_file_range on distinct
devices should probably be issued more or less randomly, and should not
interfere one with the other.The kernel's dirty buffer accounting is global, not per block device.
Sure, but this is not my point. My point is that "sync_file_range" moves
buffers to the device io queues, which are per device. If there is one
queue in pg and many queues on many devices, the whole point of coalescing
to get sequential writes is somehow lost.
It's also actually rather common to have multiple tablespaces on a
single block device. Especially if SANs and such are involved; where you
don't even know which partitions are on which disks.
Ok, some people would not benefit if the use many tablespaces on one
device, too bad but that does not look like a useful very setting anyway,
and I do not think it would harm much in this case.
If you use just one context, the more table spaces the less performance
gains, because there is less and less aggregation thus sequential writes per
device.So for me there should really be one context per tablespace. That would
suggest a hashtable or some other structure to keep and retrieve them, which
would not be that bad, and I think that it is what is needed.That'd be much easier to do by just keeping the context in the
per-tablespace struct. But anyway, I'm really doubtful about going for
that; I had it that way earlier, and observing IO showed it not being
beneficial.
ISTM that you would need a significant number of tablespaces to see the
benefit. If you do not do that, the more table spaces the more random the
IOs, which is disappointing. Also, "the cost is marginal", so I do not see
any good argument not to do it.
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
ISTM that "progress" and "progress_slice" only depend on num_scanned and
per-tablespace num_to_scan and total num_to_scan, so they are somehow
redundant and the progress could be recomputed from the initial figures
when needed.They don't cause much space usage, and we access the values frequently.
So why not store them?
The same question would work the other way around: these values are one
division away, why not compute them when needed? No big deal.
[...] Given realistic amounts of memory the max potential "skew" seems
fairly small with float8. If we ever flush one buffer "too much" for a
tablespace it's pretty much harmless.
I do agree. I'm suggesting that a comment should be added to justify why
float8 accuracy is okay.
I see a binary_heap_allocate but no corresponding deallocation, this
looks like a memory leak... or is there some magic involved?Hm. I think we really should use a memory context for all of this - we
could after all error out somewhere in the middle...
I'm not sure that a memory context is justified here, there are only two
mallocs and the checkpointer works for very long times. I think that it is
simpler to just get the malloc/free right.
[...] I'm not arguing for ripping it out, what I mean is that we don't
set a nondefault value for the GUCs on platforms with just
posix_fadivise available...
Ok with that.
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Hallo Andres,
AFAICR I used a "flush context" for each table space in some version
I submitted, because I do think that this whole writeback logic really
does make sense *per table space*, which suggest that there should be as
many write backs contexts as table spaces, otherwise the positive effect
may going to be totally lost of tables spaces are used. Any thoughts?Leads to less regular IO, because if your tablespaces are evenly sized
(somewhat common) you'll sometimes end up issuing sync_file_range's
shortly after each other. For latency outside checkpoints it's
important to control the total amount of dirty buffers, and that's
obviously independent of tablespaces.
I did a quick & small test with random updates on 16 tables with
checkpoint_flush_after=16 checkpoint_timeout=30
(1) with 16 tablespaces (1 per table, but same disk) :
tps = 1100, 27% time under 100 tps
(2) with 1 tablespace :
tps = 1200, 3% time under 100 tps
This result is logical: with one writeback context shared between
tablespaces the sync_file_range is issued on a few buffers per file at a
time on the 16 files, no coalescing occurs there, so this result in random
IOs, while with one table space all writes are aggregated per file.
ISTM that this quick test shows that a writeback context are relevant per
tablespace, as I expected.
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
I did a quick & small test with random updates on 16 tables with
checkpoint_flush_after=16 checkpoint_timeout=30
Another run with more "normal" settings and over 1000 seconds, so less
"quick & small" that the previous one.
checkpoint_flush_after = 16
checkpoint_timeout = 5min # default
shared_buffers = 2GB # 1/8 of available memory
Random updates on 16 tables which total to 1.1GB of data, so this is in
buffer, no significant "read" traffic.
(1) with 16 tablespaces (1 per table) on 1 disk : 680.0 tps
per second avg, stddev [ min q1 median d3 max ] <=300tps
679.6 ᅵ 750.4 [0.0, 317.0, 371.0, 438.5, 2724.0] 19.5%
(2) with 1 tablespace on 1 disk : 956.0 tps
per second avg, stddev [ min q1 median d3 max ] <=300tps
956.2 ᅵ 796.5 [3.0, 488.0, 583.0, 742.0, 2774.0] 2.1%
--
Fabien.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 2016-02-22 14:11:05 +0100, Fabien COELHO wrote:
I did a quick & small test with random updates on 16 tables with
checkpoint_flush_after=16 checkpoint_timeout=30Another run with more "normal" settings and over 1000 seconds, so less
"quick & small" that the previous one.checkpoint_flush_after = 16
checkpoint_timeout = 5min # default
shared_buffers = 2GB # 1/8 of available memoryRandom updates on 16 tables which total to 1.1GB of data, so this is in
buffer, no significant "read" traffic.(1) with 16 tablespaces (1 per table) on 1 disk : 680.0 tps
per second avg, stddev [ min q1 median d3 max ] <=300tps
679.6 � 750.4 [0.0, 317.0, 371.0, 438.5, 2724.0] 19.5%(2) with 1 tablespace on 1 disk : 956.0 tps
per second avg, stddev [ min q1 median d3 max ] <=300tps
956.2 � 796.5 [3.0, 488.0, 583.0, 742.0, 2774.0] 2.1%
Interesting. That doesn't reflect my own tests, even on rotating media,
at all. I wonder if it's related to:
https://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/commit/?id=23d0127096cb91cb6d354bdc71bd88a7bae3a1d5
If you use your 12.04 kernel, that'd not be fixed. Which might be a
reason to do it as you suggest.
Could you share the exact details of that workload?
Greetings,
Andres Freund
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Andres Freund <andres@anarazel.de> writes:
Interesting. That doesn't reflect my own tests, even on rotating media,
at all. I wonder if it's related to:
https://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/commit/?id=23d0127096cb91cb6d354bdc71bd88a7bae3a1d5
If you use your 12.04 kernel, that'd not be fixed. Which might be a
reason to do it as you suggest.
Hmm ... that kernel commit is less than 4 months old. Would it be
reflected in *any* production kernels yet?
regards, tom lane
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 2016-02-22 11:05:20 -0500, Tom Lane wrote:
Andres Freund <andres@anarazel.de> writes:
Interesting. That doesn't reflect my own tests, even on rotating media,
at all. I wonder if it's related to:
https://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/commit/?id=23d0127096cb91cb6d354bdc71bd88a7bae3a1d5If you use your 12.04 kernel, that'd not be fixed. Which might be a
reason to do it as you suggest.Hmm ... that kernel commit is less than 4 months old. Would it be
reflected in *any* production kernels yet?
Probably not - so far I though it mainly has some performance benefits
on relatively extreme workloads; where without the patch, flushing still
is better performancewise than not flushing. But in the scenario Fabien
has brought up it seems quite possible that sync_file_range emitting
"storage cache flush" instructions, could explain the rather large
performance difference between his and my experiments.
Regards,
Andres
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Random updates on 16 tables which total to 1.1GB of data, so this is in
buffer, no significant "read" traffic.(1) with 16 tablespaces (1 per table) on 1 disk : 680.0 tps
per second avg, stddev [ min q1 median d3 max ] <=300tps
679.6 � 750.4 [0.0, 317.0, 371.0, 438.5, 2724.0] 19.5%(2) with 1 tablespace on 1 disk : 956.0 tps
per second avg, stddev [ min q1 median d3 max ] <=300tps
956.2 � 796.5 [3.0, 488.0, 583.0, 742.0, 2774.0] 2.1%Interesting. That doesn't reflect my own tests, even on rotating media,
at all. I wonder if it's related to:
https://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/commit/?id=23d0127096cb91cb6d354bdc71bd88a7bae3a1d5If you use your 12.04 kernel, that'd not be fixed. Which might be a
reason to do it as you suggest.Could you share the exact details of that workload?
See attached scripts (sh to create the 16 tables in the default or 16
table spaces, small sql bench script, stat computation script).
The per-second stats were computed with:
grep progress: pgbench.out | cut -d' ' -f4 | avg.py --length=1000 --limit=300
Host is 8 cpu 16 GB, 2 HDD in RAID 1.
--
Fabien.
Hi,
On 02/18/2016 11:31 AM, Andres Freund wrote:
On 2016-02-11 19:44:25 +0100, Andres Freund wrote:
The first two commits of the series are pretty close to being ready. I'd
welcome review of those, and I plan to commit them independently of the
rest as they're beneficial independently. The most important bits are
the comments and docs of 0002 - they weren't particularly good
beforehand, so I had to rewrite a fair bit.0001: Make SetHintBit() a bit more aggressive, afaics that fixes all the
potential regressions of 0002
0002: Fix the overaggressive flushing by the wal writer, by only
flushing every wal_writer_delay ms or wal_writer_flush_after
bytes.I've pushed these after some more polishing, now working on the next
two.
I've finally had time to do some benchmarks on those two (already
committed) pieces. I've promised to do more testing while discussing the
patches with Andres some time ago, so here we go.
I do have two machines I use for this kind of benchmarks
1) HP DL380 G5 (old rack server)
- 2x Xeon E5450, 16GB RAM (8 cores)
- 4x 10k SAS drives in RAID-10 on H400 controller (with BBWC)
- RedHat 6
- shared_buffers = 4GB
- min_wal_size = 2GB
- max_wal_size = 6GB
2) workstation with i5 CPU
- 1x i5-2500k, 8GB RAM
- 6x Intel S3700 100GB (in RAID0 for this benchmark)
- Gentoo
- shared_buffers = 2GB
- min_wal_size = 1GB
- max_wal_size = 8GB
Both machines were using the same kernel version 4.4.2 and default io
scheduler (cfq). The
The test procedure was quite simple - pgbench with three different
scales, for each scale three runs, 1h per run (and 30 minutes of warmup
before each run).
Due to the difference in amount of RAM, each machine used different
scales - the goal is to have small, ~50% RAM, >200% RAM sizes:
1) Xeon: 100, 400, 6000
2) i5: 50, 200, 3000
The commits actually tested are
cfafd8be (right before the first patch)
7975c5e0 Allow the WAL writer to flush WAL at a reduced rate.
db76b1ef Allow SetHintBits() to succeed if the buffer's LSN ...
For the Xeon, the total tps for each run looks like this:
scale commit 1 2 3
----------------------------------------------------
100 cfafd8be 5136 5132 5144
7975c5e0 5172 5148 5164
db76b1ef 5131 5139 5131
400 cfafd8be 3049 3042 2880
7975c5e0 3038 3026 3027
db76b1ef 2946 2940 2933
6000 cfafd8be 394 389 391
7975c5e0 391 479 467
db76b1ef 443 416 481
So I'd say not much difference, except for the largest data set where
the improvement is visible (although it's a bit too noisy and additional
runs would be useful).
On the i5 workstation with SSDs, the results look like this:
scale commit 1 2 3
------------------------------------------------
50 cfafd8be 5478 5486 5485
7975c5e0 5473 5468 5436
db76b1ef 5484 5453 5452
200 cfafd8be 5169 5176 5167
7975c5e0 5144 5151 5148
db76b1ef 5162 5131 5131
3000 cfafd8be 2392 2367 2359
7975c5e0 2301 2340 2347
db76b1ef 2277 2348 2342
So pretty much no difference, or perhaps maybe a slight slowdown.
One of the goals of this thread (as I understand it) was to make the
overall behavior smoother - eliminate sudden drops in transaction rate
due to bursts of random I/O etc.
One way to look at this is in terms of how much the tps fluctuates, so
let's see some charts. I've collected per-second tps measurements (using
the aggregation built into pgbench) but looking at that directly is
pretty pointless because it's very difficult to compare two noisy lines
jumping up and down.
So instead let's see CDF of the per-second tps measurements. I.e. we
have 3600 tps measurements, and given a tps value the question is what
percentage of the measurements is below this value.
y = Probability(tps <= x)
We prefer higher values, and the ideal behavior would be that we get
exactly the same tps every second. Thus an ideal CDF line would be a
step line. Of course, that's rarely the case in practice. But comparing
two CDF curves is easy - the line more to the right is better, at least
for tps measurements, where we prefer higher values.
1) tps-xeon.png
The original behavior (red lines) is quite consistent. The two patches
generally seem to improve the performance, although sadly it seems that
the variability of the performance actually increased quite a bit, as
the CDFs are much wider (but generally to the right of the old ones).
I'm not sure what exactly causes the volatility.
2) maxlat-xeon.png
Another view at the per-second data, this time using "max latency" from
the pgbench aggregated log. Of course, this time "lower is better" so
we'd like to move the CDF to the left (to get lower max latencies).
Sadly, it changes is mostly the other direction, i.e. the max latency
slightly increases (but the differences are not as significant as for
the tps rate, discussed in the previous paragraph). But apparently the
average latency actually improves (which gives us better tps).
Note: In this chart, x-axis is logarithmic.
3) tps-i5.png
Same chart with CDF of tps, but for the i5 workstation. This actually
shows the consistent slowdown due to the two patches, the tps
consistently shifts to the lower end (~2000tps).
I do have some more data, but those are the most interesting charts. The
rest usually shows about the same thing (or nothing).
Overall, I'm not quite sure the patches actually achieve the intended
goals. On the 10k SAS drives I got better performance, but apparently
much more variable behavior. On SSDs, I get a bit worse results.
Also, I really wonder what will happen with non-default io schedulers. I
believe all the testing so far was done with cfq, so what happens on
machines that use e.g. "deadline" (as many DB machines actually do)?
regards
--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services
Attachments:
maxlat-xeon.pngimage/png; name=maxlat-xeon.pngDownload
�PNG
IHDR � �G�c pHYs =� =����t tIME�
/%tL�� IDATx���{\L���_�i��EI5c��]��2�����7��m���.�,�n��I���]JS���.�tS"�uYR�%���n
����������SB����������v��y����!`0��`�� ��`0��b0��`��`0� ,��`0X��`0L`1��`0��`0��`��`0� ,��`0&���`0L`1��`0��`0����`0X��8��g���)�3QGv�&��c���~�����Xv��k�n��=�J
��}���W|���8�������]�b���so�������������f����q����]���?{����]���g
Ettt]]��*��=55u������Md��W/�����7x�`�v>p������;Z�~�����(�3�%�&`0Ms������� �������������^(�����rkk�M�6M�6���j��9 N�>moo?`� B�P�P������.]�����T�Fs�������[��W�\9}�t��}}}���-Z��}��/B222F�ikk��/��h4�����pvv>z����]DDDhh(m"=f�����OIJJ�D���s���V�]]]���
�"-��_����d2;;;�X1���x�h��&,
�6}� �{9?�%� --���D"QVV�#����d2WWW]���h4�{{{�\�[�������g��[�n-x� !r�������yyy�-HJJ�s���F777www���=**J(R���(�J��T*�����[��B
����2B���?��Xq�����|���&����������J�H���'O��p����We2��BA/���]�|9=
}��� !�������`������*�*!!�������W��
Jdd�{����X���|��R)��uuu577�H$, �(
�'33��:..�����}||��mYY���5���B���g=x����@����j�<<W�^
>v�X�*77@III�VRR�B������z���*���tV���'"===����s����{���%^VR�t/���k@@ ��P(�����(&&F*��Zz����w���b ���|#h4'''Zf�L�����f�q���w� ,���0t�P�Z}���^x@UUU}}��o�)��!K�.
�UUU�:�C��������VTT ��
r��J�\����2y�d:C��q#�'t���/��\������G���ppp������K�.[�n%�dff8x� _���<�c�6mf��E�p����cs'N��+B�p�������_������INNn��Q �W������+M�'N4L���z�=999 >L�8q"-C�U�WC���V����
�������R��j�iii�u����]�)m�T�k)--m��- =�N�:M�<Y�VWTT���SYb``�[�S����?����k����h��������7����s��o�'N���������*���#�H��]��������P5*��{a��d��u555�8m�4�HD��b1u�D��^^^tr�n��+6--Mw �v����b``@��cbbj)�?��j���X���7L�{��z=a�n�����{U������^���P(ttt��dr�|�����3**����aS��n``�+�m�@���;88 �����������f!�H�^.>�F�8=vuu�?��s���`<�y���F�����Z-�8::n��m������UUU �����9s�,X������;n"��.O��k�������iii���UUU���z>���qqq��-��������Q���#=�t��X,���G���W�5j���+!������F(�f�o�>sssGG��V988x��YM�455�����32D*�r'����?��3KK�F�p��j��x��������������d�n�jX`[[�F�/��bYY��~BCCKJJ�����D�N9r�}��b��� ��m+++377�o���GVV��jx���E���`<�0��`<}���W�Z?p��1c������J��N�:UYY)��b!KJJ���G�u�������t����kjj��C��q����1c�_�^PP��������a� dgg���(o�����KXtF���������I�w���M�����%$$���*��F/?{�l�n��h������e��=�6�:ujdd������t,�Q��� �P�}�uyy��y�6l��������R "��S�N?��#m=�@`dd��{��fG�U�X�i�r��m�����;zb����R���o�R��vn=(|g����(���y���?}�����������g�^�w�}�o���&��c���*�Jebb`�����_��k��Q��Z���#]]]v� �B�L0t�P ����4i���k���S��d2��&HLL���_���/��}�����z�-�������YYY5���q��n���������G�1�qK�.6l4���Tim��������
�
��v�i4�3g�xxx�F"��*��}����:��a��L6|�pgg��={�����8�����DW����,]����MOO���aO=� ,����J�s���u����aPP���"OO�3f��b�BCC������if��1h���G�fgg[XX<t!{��}�����W�o����#111��u;s��j�{�nGG���XWWW�H����egg�.��c���������?~���G�������)�B�7j��~���]����[ZZn��%::Z.���� ,Z�(>>�WTT����_�������������~���3g��?�V���S�&M���K�Uh������J������������GBBBYYYpp������O@@���QUU�B�(((���Q{pp�B����Z�������uvv>r����C9��|�2������C��d���{��y��7��J�b��6����'�|`���2�L,�rj�Z�� :��G��3��44�q�/U������ !;w��.�T*www�H����WUUM�0���L���9s��(T*�_}�����_N����H7prr��&L��?��}��U���L+++kk�Y�f]�x�S�N4���h�HD�
h����t��^��������[YY��V���ehh�o���7n��r�H$��RiAA*22R~����k���(�����o_��h��b����c�Uh��t��V���w"�H&������XYY��X�v�D�����*99�����k��v���o�������:u���$BIOO�J�z���/))a�;� a��������.��H-^<�G�Q__����l����6�}���9rWZZ*�H���3,,������sX�����gdd,]�4//�^��-��������7o����������cMM���1ky�s���`0Zb�������'�#���z���'����'�?�oA�`<���b0��hYX0��`0��`0����`0X��`0&���`0��b0��xNyj�`���������Q����z���a���n�s����[����O�8������m�`0���S���Xpqq ���)���G����;w�\hh��9s!#G�����T*5
���`0�u���r�D"9t���1::����������r����s�yzz��������77�����{�`0� ,}�j��I���m�k\�z���/=�J�B�p���999 ����}�� ��mc���`0F+�)�-X� ����u�/^������������� �B!5ZZZj4�F�||����`�{h)E�Z�3����D"=cEE���is.�8� ,��`0�.�Z$��"�^|����2=�����[�(����=�X(.Z�@PP�����v���k�����J�G�����o������T�&2`��<�JQ��~��S����[��!*��y����Ommm��h���x���A� �R�S��V����3�~���{|�� \�u1�1��p��1Ss�j.�~#C� ��N���37@ 8����%�����~.�U)�����%P�m|�"�K�+��R�|��.Z0QU��Zk����� �E`��G�r�2\� �1��G$~�[��0�k���U6�{:�\�W� ����8N}�6��L
4�R�s����������SA,�����J$�y��i�Z����CHHHrr�n 322�(����c��������d-��������W
�\.����-U��}����R���[){{���N��z�G����}'4���S��V������n�Ji���I�%����H���S�{/�K9\B�$�L3+b�}���C��N�
M~�/"�ByF�(%X�e�4��������8�s�W��E���_H�OCl��X���_o������m�\�8b�+a_������������g�����G�r"�y��R���N'�?�/����Ok�.�����l���u[�sC��V�R�s�������h-=X
�b��Y 4��3g<<<�R)���t����M�\]]Y�%��`0Z9{��8�W��s|� �n�V�z
4�0�fg��6Q��_��Ex ��3��j�����Q\�9�����#���F��5� f����n�_�Cz���`��p�
j:�� Xw�=pu��m?��V/X�@3Xc0�������n��ko� o��[ `��g�z ��~�r�����s�;� >���y�u/�q]�N+h��8<s~Z��
V(��������U�����^�>>>FFFUUU
�����_�������u}}= �V�j�EK�X+�@Y4�RO��>hS?��j����#>�|=D�����)O�T��| ���ld���/���x�l�B����u�y���U�yT ��3U���������D��������o_�VUI^w����7o���[�$zj��F���f�u��h�@����`mm�pv��}����V/ �2�'��=�;��OJ+|?5��;�jkk[VV6t����hgg���|j���
�0a������{����u��=�������<h��O9))��}�Dz����+5v��f���J�H��-
�������>������qT*))���~�����'�9�i��?C\�����J������H�� '!�v���^p����8[2��/�����X�E� X��_F��J����6 >x�7�����T�W/\������+�W5��h�N/��tQ�Q�����z��c�0����!���E��{�=�;��OJs*���R��N��qor�$=�U���%==}��r�|��9�5�"aaat���Q�=d�7�T���#c�������#����u8�U6H�}���n����W�����p�F^+�e��~A��}S�i}�����u��F�����p�[��3���;=O�xb��F�
�g����`� ����m�H/TjU����b/�V
�q[x��?��N��#�0`���
�wvU�q.��h�y����N��:�������w�jX��I�.�i#
��������<<Xc��`q����c'2bX1�O��*�J]b�k�k�]|%��=���g�&[�+��-?Ts:���M���K��xc�5
�n����6 .B\3nW?,���""X3��b0Z"��}���)�(���777o��# ����x��������v$X�)����$v(�
��_�,��7��:Si����A��6;�<�
�
��]:���uc�6���F���g���������p��o���y��5��g�KKKjl8���v��-}��C���k%%%={�|\�6B8�c���������Ww��f��6��*pY��Z�6�G���@Hu `[���X\����[�/�VU��� u��@8��&grzLr#17���k�zW��{�I"��8����s�\��NHH�P(\�x�C��D���;;�eff��BR����L�����<b�����������ha0����-����Q����O�O:�L~ktG��?>�����O9}aB b�+r��*M2�.|�v�N���ZVH�!�'��U��ry``�C� ���^^^
����|��m�D�R���G!77��_~yLW*�K�,���3�`0)��}��v�\^��� ���� ^�R�-��T�����t�[���E�o��Fx�p����
��[&m�{���#�������z|���geeYYYM�8���8??�m��G�-..�#}K�.���������������������>}:���b1����s���7n��@tttuu5��nI ���v���aQ��;�p;��k��;v��e=z��g�Q����>��C������6m�$$$ ��������>}�O?���^{�1�C����|Jp���|��U8���n���=�n�8B�qd�hn������GX�F�$l�|�ZS���~�-�E��X� ����s;s���k���������������MLL��?���[QQQQQQYY ���y���B�������-55 �SQQq��a�D�u��+W������g !��
������M�lll��B�p���4���;�V�X���v�����"�D��
E���������>}��E�
��]�v�Z������;�[�p�����������v�ZRR��Ndd$!d�����M��cE�AB~� �a�!��\w�jb��7�kA�4Yn�1l�q(:y;q~�W�
���}M����T���c.�z��%�����U��/���0���
��0yyyT��8�NNNAAA4�$u�������:L�6����EI}���&O�L�TH��zyy�D�6m�,Y���:g��@w��h4J����y{���u��h{�����6u���n�M�\nll�G,500�WH���4�X��y��$���X�����;��9s������� D*%����Y �].� ��� ���tRR�����`=cdee988XYY����7o����r�JFFF�5���c����VVV����������g;;;'$$�9������ ,,l��
/^ttt�p�Bzz:�����������M�T�#�z<�����gtX��K{H8�)w��Y��m�`0ZSG 8�O����XD�Y���h\�!��-$%����?��[����/
V�hR9�K��i�����wX{2�L`�<���H����������b'''[[[���s��) �2�&�q������sCBB �d277���ww�F�QYYy��%ooo~��X�'��l�2u����e��F
����*��X3�Q<