Received: from malur.postgresql.org ([217.196.149.56]) by arkaria.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1x64sX-0000jP-0G for pgsql-hackers@arkaria.postgresql.org; Mon, 14 Sep 2026 11:29:41 +0000 Received: from localhost ([127.0.0.1] helo=malur.postgresql.org) by malur.postgresql.org with esmtp (Exim 4.96) (envelope-from ) id 1x64sW-00CUPO-1G for pgsql-hackers@arkaria.postgresql.org; Mon, 14 Sep 2026 11:29:40 +0000 Received: from makus.postgresql.org ([2001:4800:3e1:1::229]) by malur.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1x64sV-00CUPG-2z for pgsql-hackers@lists.postgresql.org; Mon, 14 Sep 2026 11:29:40 +0000 Received: from mail-wr2-x0a.google.com ([2a00:1450:4864:30::a]) by makus.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (Exim 4.98.2) (envelope-from ) id 1x64sT-000000000RN-3a7d for pgsql-hackers@lists.postgresql.org; Mon, 14 Sep 2026 11:29:39 +0000 Received: by mail-wr2-x0a.google.com with SMTP id ffacd0b85a97d-484373a2e82so1350725f8f.0 for ; Mon, 14 Sep 2026 04:29:37 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=planetscale.com; s=google; t=1789385376; x=1789990176; darn=lists.postgresql.org; h=content-transfer-encoding:content-type:in-reply-to:cc:from :content-language:references:to:subject:user-agent:mime-version:date :message-id:from:to:cc:subject:date:message-id:reply-to:content-type; bh=Pd+D3yXrTPTCgWTgKH9nA6QbVuqcQ+y6kVWOZdzjps0=; b=WbmCkfyC+GhdBKjeKgV2BZe37X+FWOHUnJRqA8uhBFpghWfmAZEEn8PFtjBvbPJ/Zk +dJlHdz8aBroI4rU5OmGVL1SwAzb3hPaLsa6Abctyj4q8dtjivnLBeDkuR9cDHmbvnNp DKBb4K1uZ3nrR8x4bKUKHwm7TnkQzF0SomhkgHM1wQ3Ggc74tI9zmHLThvioaOkYzp0Q ZkGUTEhOEFK+JbU3QM+uq5VlH61zBEij9qhb8zVE1Kz3oYPxG/H2N9MhK6ruTxZLMSB9 b/30F4PCY9ylLL9rVkpVJRkRQIh4bJ5RggJfvny440Uhw+a6elqJ0DuA+cw2SRp5I7Yo VZVg== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1789385376; x=1789990176; h=content-transfer-encoding:content-type:in-reply-to:cc:from :content-language:references:to:subject:user-agent:mime-version:date :message-id:x-gm-gg:x-gm-message-state:from:to:cc:subject:date :message-id:reply-to:content-type; bh=Pd+D3yXrTPTCgWTgKH9nA6QbVuqcQ+y6kVWOZdzjps0=; b=O+eRpl90xdMvJiTUrzNds1tS3A9IgWok/lZZ8ED8MJ7VKPPVOlwy+8kgZ8eaqNODxj A6NAGqRVXKGKHU1t/Ab5+unH79eFgEMFBQToGQXspLwkH6d5mhHHFbNVWteemS8MWg10 dfqObCYb3wguYEXatSyNv4Q42mS8Bqd4eWWoXfMP/+emoVhqxd+os5AO3cXyBJkhP9eA ernBQviOqfMizXr1Ye9ToM/LHi6479AVnjZL/goW9+s3GKmdZdQ7sLcYVaC9irOX7Zrd PRdwmtzX9p7g/dgq+shB1mTgFKQYE2O5BWi1YK4yjC6qgCfZgC7d78ggLE/ljHkFt/dh D5eA== X-Gm-Message-State: AFuF++lXcvx03E9b/T/Q0qdwTtt7oztcn2VoMZ6YbQJtPipMC5hkSILd 1s3RthUfKJyR19gDqVibpvDX5Z7yh+55xxTZHj14kSvhX2WbLad6HZhqTExEbu2W448kXtSYe9w UGLCw6KvWhUhf X-Gm-Gg: AYBFou340Zd2L61yzWnqr7XPSxb/iuYFdBBnKyDU2/93nBAXnHqMvJEQGEZzMVVgT29 dFyjYVnqbAOj46aeeu4GE4rP0pNoW1aBSRbEVqamKKq0vbsrNOxgP1k7h9ybYm2LGhVLYSfwIg5 DN8gUB9k74mmbl831fYGKtBnw+jWCSGUVJVaT8nDPX2ei95b2cqi58AYAhsJwLLxk5CmIggFl0i a+/sa0GrjT9Oh4QkP9VkOD5PrWEkoiz5bzuNrRip8fF+llGBlS5GYOLLJDlDHbpCEkrU/eOX78A nwmuz0Ye8yajBCcFblEAclbyfm/csz70W2YNP7jUm4F9t6F10oMmqsH6tOwVFT8CLioZEx/nrHO bCKZclmcmvDf3sO6KdD8Dk2JWf20QYyqkPbvqbochQ8T7Toyhteu2h1coeYmpj/ZaPGO6Uss7xp EXpHVJ/GJKuJnxa65QOvxQ58mTb50RAiEJRkF+Qvf2X59Y97Lqq/PowiCSRxp7a5XYq02V5TRyB smFmfgk9xhIWVqLltW3 X-Received: by 2002:adf:e18d:0:b0:486:e5f7:89c5 with SMTP id ffacd0b85a97d-48702aaccccmr5143972f8f.1.1789385375726; Mon, 14 Sep 2026 04:29:35 -0700 (PDT) Received: from [192.168.1.146] ([139.91.207.69]) by smtp.gmail.com with ESMTPSA id ffacd0b85a97d-486eb33ea4fsm27589966f8f.21.2026.09.14.04.29.35 (version=TLS1_3 cipher=TLS_AES_128_GCM_SHA256 bits=128/128); Mon, 14 Sep 2026 04:29:35 -0700 (PDT) Message-ID: <1c9fe4ba-1f49-4116-9c44-add3ed0310e5@planetscale.com> Date: Mon, 14 Sep 2026 14:29:34 +0300 MIME-Version: 1.0 User-Agent: Mozilla Thunderbird Subject: Re: Recovery does not honor io_combine_limit, causing IOPS saturation To: Mats Kindahl References: <30e5bab4-8f61-4f03-9cec-e486540a78fb@gmail.com> Content-Language: en-US From: Markos Fountoulakis Cc: PostgreSQL Hackers In-Reply-To: <30e5bab4-8f61-4f03-9cec-e486540a78fb@gmail.com> Content-Type: text/plain; charset=UTF-8; format=flowed Content-Transfer-Encoding: 8bit List-Id: List-Help: List-Subscribe: List-Post: List-Owner: List-Archive: Archived-At: Precedence: bulk On 2026-07-26 08:47 +0300, Mats Kindahl wrote: > Hi all, > > I have been running recovery of Postgres clusters and small AWS instances and > with a long history to replay, this quickly eats up the I/O budget for the > device. For example, with a gp3 with an IOPS limit of 3000 you quickly eat it up > and get long stalls to recover the cluster. This is because the WAL replay reads > the WAL in 8 KiB block rather than honoring io_combine_limit, which typically > could allow reading 256 KiB in one go. > > Since Postgres already have the option io_combine_limit to combine multiple > block reads into a single read, it is straightforward to honor it during > recovery using a simple cache (see attached patch). Since WAL replay is mostly > from beginning to end, it is a simple way to reduce IOPS usage. > > With this patch, and the default io_combine_limit of 16, I managed to reduce the > number of I/O reads significantly: > > |            | reads  | read_bytes  | avg bytes/read | > |------------|--------|-------------|----------------| > | WAL before | 31,283 | 256,270,336 | 8,192          | > | WAL after  | 1,955  | 256,131,072 | 131,013        | > > Some back-of-the-envelope calculations give that with this patch we should not be > able to saturate the device interface with the maximum read bandwidth for gp3 is > approx 125 MiB/s, but the maximum read speed if you want to not hit the limit > when you read 8 KiB blocks is 23-24 MiB/s. > > I thought I'd share the patch and the results and hear if people think this is a > useful improvement. There is already some work on pre-fetching, but this is such > a simple patch that I think it could be worth to add to the current version. > > (I also have some Perl scripts to set up crash recovery and some bpftrace > scripts that checks the reads, if anybody is interested, but I need to clean > them up a little.) > > Best wishes, > Mats Kindahl, Multigres Engineer, Supabase Dear Mats, I tested this patch on an Ubuntu 26.04 VM over BTRFS filesystem and a throttled block device to see the actual behaviour of the system and how the block device affects the redo performance of the server recovery process (redo done elapsed time). I generated the dataset with pgbench as seen below: ===================== pgbench -s 20 -i test pgbench --progress=1 -T 1000 -j 64 -c 64 --skip-some-updates test & sleep 5 ; killall -9 /usr/local/pgsql/bin/postgres ===================== This kills the server ungracefully during the load generation. By backing up the PGDATA directory at this point we get a dataset that reproduces the same recovery every time. pg_waldump shows ~50% of the records have type "Heap" and ~80% of those records' size is FPI (this will be useful later). I throttled the device Read IOPS side, which is the condition under which this patch improves performance. Writes go at full speed, and are also more asynchronous in nature for filesystems. I used 100 IOPS as the limit, which is quite low, to exaggerate the impact of the patch. I used cgroups v2 to throttle the block device as seen below: ===================== echo "+io" > /sys/fs/cgroup/cgroup.subtree_control echo "254:16 riops=100" | tee /sys/fs/cgroup/*/io.max /sys/fs/cgroup/.*/io.max ===================== Before every server restart, that triggers the redo process, I simulated a crashed host by cleaning the linux page cache so as to issue new Read I/O requests to the block device: ===================== sync ; echo 3 > /proc/sys/vm/drop_caches ===================== Comparing the redo done elapsed time of postgres with and without the patch there was no difference, both at ~18 seconds which very slow. By tracing the pread64 system call however the patch did what it was supposed to, 128KB pread64 calls instead of 8KB ones: ===================== strace -f --syscall-times=us --trace=pread64 postgres -D /usr/local/pgsql/data ===================== However, the real time block device statistics (iostat) in both cases had an average Read request size very close to 8KB which does not explain things yet. I traced the root cause of this bad I/O access pattern to fadvise calls: ===================== pread64(6, "..."..., 131072, 12320768) = 131072 <0.000119> fadvise64(13, 966656, 8192, POSIX_FADV_WILLNEED) = 0 <0.000041> fadvise64(13, 966656, 8192, POSIX_FADV_WILLNEED) = 0 <0.000010> fadvise64(13, 966656, 8192, POSIX_FADV_WILLNEED) = 0 <0.000031> fadvise64(13, 983040, 8192, POSIX_FADV_WILLNEED) = 0 <0.000025> fadvise64(13, 999424, 8192, POSIX_FADV_WILLNEED) = 0 <0.000031> fadvise64(13, 1015808, 8192, POSIX_FADV_WILLNEED) = 0 <0.000050> fadvise64(13, 1032192, 8192, POSIX_FADV_WILLNEED) = 0 <0.000055> fadvise64(13, 1007616, 8192, POSIX_FADV_WILLNEED) = 0 <0.000035> fadvise64(13, 1015808, 8192, POSIX_FADV_WILLNEED) = 0 <0.000024> fadvise64(13, 1032192, 8192, POSIX_FADV_WILLNEED) = 0 <0.000120> fadvise64(13, 1040384, 8192, POSIX_FADV_WILLNEED) = 0 <0.000068> pread64(6, "..."..., 131072, 12451840) = 131072 <0.000054> ... ===================== Those fadvise calls are probably issued for Full Page Write reasons (remember FPI size of WAL above) and dominate the Read I/O pattern as they are so many of them. They are small 8KB reads, and are asynchronous. However, being asynchronous does not help much because we have a hard IOPS limit and there is no I/O combining taking place in the linux kernel. Since there is no runtime flag to disable fadvise I did a quick hack to kill the hints: ===================== diff --git a/src/backend/storage/file/fd.c b/src/backend/storage/file/fd.c index 190c9974494..113b7d4d5a9 100644 --- a/src/backend/storage/file/fd.c +++ b/src/backend/storage/file/fd.c @@ -2072,7 +2072,7 @@ FilePrefetch(File file, pgoff_t offset, pgoff_t amount, uint32 wait_event_info) file, VfdCache[file].fileName, (int64) offset, (int64) amount)); -#if defined(USE_POSIX_FADVISE) && defined(POSIX_FADV_WILLNEED) +#if 0//defined(USE_POSIX_FADVISE) && defined(POSIX_FADV_WILLNEED) { int returnCode; ===================== After disabling fadvise calls altogether we get a x30 speedup with a redo elapsed time of ~0.6 seconds versus ~18 seconds. However, the patch still does show any improvement over the upstream code. Examining the real block device statistics again, one can see that Read I/O request sizes are similar even though the pread64 syscalls are very different. The reason for that is filesystem readahead behaviour. I disabled BTRFS readahead as such: ===================== echo 8 > /sys/fs/btrfs/a1bc685b-8e1e-4a53-be23-c26036fb5dba/bdi/read_ahead_kb ===================== By doing that we can finally see the different in I/O request sizes in the block device and the patch delivers x15 performance, for x16 larger pread64 calls. This means that the usefulness of this patch is affected by the OS kernel and the filesystem readahead logic. If OS-level readahead is in effect then this patch at worst case may only reduce the pread64 count and only offer CPU-level overhead reduction. Also, it would be good if the fadvise problem is addressed. Maybe it will be fixed by: https://www.postgresql.org/message-id/9bdb428a-fc8e-4801-8fff-1ad0bc6fa76b@postgrespro.ru -- Markos Fountoulakis PlanetScale Postgres Core Team