mirror of
https://github.com/neondatabase/neon.git
synced 2026-07-08 06:30:37 +00:00
## Problem The vectored read path proposed in https://github.com/neondatabase/neon/pull/6576 seems to be functionally correct, but in my testing (see below) it is about 10-20% slower than the naive sequential vectored implementation. ## Summary of changes There's three parts to this PR: 1. Supporting vectored blob reads. This is actually trickier than it sounds because on disk blobs are prefixed with a variable length size header. Since the blobs are not necessarily fixed size, we need to juggle the offsets such that the callers can retrieve the blobs from the resulting buffer. 2. Merge disk read requests issued by the vectored read path up to a maximum size. Again, the merging is complicated by the fact that blobs are not fixed size. We keep track of the begin and end offset of each blob and pass them into the vectored blob reader. In turn, the reader will return a buffer and the offsets at which the blobs begin and end. 3. A benchmark for basebackup requests against tenant with large SLRU block counts is added. This required a small change to pagebench and a new config variable for the pageserver which toggles the vectored get validation. We can probably optimise things further by adding a little bit of concurrency for our IO. In principle, it's as simple as spawning a task which deals with issuing IO and doing the serialisation and handling on the parent task which receives input via a channel.
264 lines
7.6 KiB
Rust
264 lines
7.6 KiB
Rust
//!
|
|
//! Low-level Block-oriented I/O functions
|
|
//!
|
|
|
|
use super::ephemeral_file::EphemeralFile;
|
|
use super::storage_layer::delta_layer::{Adapter, DeltaLayerInner};
|
|
use crate::context::RequestContext;
|
|
use crate::page_cache::{self, FileId, PageReadGuard, PageWriteGuard, ReadBufResult, PAGE_SZ};
|
|
use crate::virtual_file::VirtualFile;
|
|
use bytes::Bytes;
|
|
use std::ops::Deref;
|
|
|
|
/// This is implemented by anything that can read 8 kB (PAGE_SZ)
|
|
/// blocks, using the page cache
|
|
///
|
|
/// There are currently two implementations: EphemeralFile, and FileBlockReader
|
|
/// below.
|
|
pub trait BlockReader {
|
|
///
|
|
/// Create a new "cursor" for reading from this reader.
|
|
///
|
|
/// A cursor caches the last accessed page, allowing for faster
|
|
/// access if the same block is accessed repeatedly.
|
|
fn block_cursor(&self) -> BlockCursor<'_>;
|
|
}
|
|
|
|
impl<B> BlockReader for &B
|
|
where
|
|
B: BlockReader,
|
|
{
|
|
fn block_cursor(&self) -> BlockCursor<'_> {
|
|
(*self).block_cursor()
|
|
}
|
|
}
|
|
|
|
/// Reference to an in-memory copy of an immutable on-disk block.
|
|
pub enum BlockLease<'a> {
|
|
PageReadGuard(PageReadGuard<'static>),
|
|
EphemeralFileMutableTail(&'a [u8; PAGE_SZ]),
|
|
#[cfg(test)]
|
|
Arc(std::sync::Arc<[u8; PAGE_SZ]>),
|
|
#[cfg(test)]
|
|
Vec(Vec<u8>),
|
|
}
|
|
|
|
impl From<PageReadGuard<'static>> for BlockLease<'static> {
|
|
fn from(value: PageReadGuard<'static>) -> BlockLease<'static> {
|
|
BlockLease::PageReadGuard(value)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
impl<'a> From<std::sync::Arc<[u8; PAGE_SZ]>> for BlockLease<'a> {
|
|
fn from(value: std::sync::Arc<[u8; PAGE_SZ]>) -> Self {
|
|
BlockLease::Arc(value)
|
|
}
|
|
}
|
|
|
|
impl<'a> Deref for BlockLease<'a> {
|
|
type Target = [u8; PAGE_SZ];
|
|
|
|
fn deref(&self) -> &Self::Target {
|
|
match self {
|
|
BlockLease::PageReadGuard(v) => v.deref(),
|
|
BlockLease::EphemeralFileMutableTail(v) => v,
|
|
#[cfg(test)]
|
|
BlockLease::Arc(v) => v.deref(),
|
|
#[cfg(test)]
|
|
BlockLease::Vec(v) => {
|
|
TryFrom::try_from(&v[..]).expect("caller must ensure that v has PAGE_SZ")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Provides the ability to read blocks from different sources,
|
|
/// similar to using traits for this purpose.
|
|
///
|
|
/// Unlike traits, we also support the read function to be async though.
|
|
pub(crate) enum BlockReaderRef<'a> {
|
|
FileBlockReader(&'a FileBlockReader<'a>),
|
|
EphemeralFile(&'a EphemeralFile),
|
|
Adapter(Adapter<&'a DeltaLayerInner>),
|
|
#[cfg(test)]
|
|
TestDisk(&'a super::disk_btree::tests::TestDisk),
|
|
#[cfg(test)]
|
|
VirtualFile(&'a VirtualFile),
|
|
}
|
|
|
|
impl<'a> BlockReaderRef<'a> {
|
|
#[inline(always)]
|
|
async fn read_blk(
|
|
&self,
|
|
blknum: u32,
|
|
ctx: &RequestContext,
|
|
) -> Result<BlockLease, std::io::Error> {
|
|
use BlockReaderRef::*;
|
|
match self {
|
|
FileBlockReader(r) => r.read_blk(blknum, ctx).await,
|
|
EphemeralFile(r) => r.read_blk(blknum, ctx).await,
|
|
Adapter(r) => r.read_blk(blknum, ctx).await,
|
|
#[cfg(test)]
|
|
TestDisk(r) => r.read_blk(blknum),
|
|
#[cfg(test)]
|
|
VirtualFile(r) => r.read_blk(blknum).await,
|
|
}
|
|
}
|
|
}
|
|
|
|
///
|
|
/// A "cursor" for efficiently reading multiple pages from a BlockReader
|
|
///
|
|
/// You can access the last page with `*cursor`. 'read_blk' returns 'self', so
|
|
/// that in many cases you can use a BlockCursor as a drop-in replacement for
|
|
/// the underlying BlockReader. For example:
|
|
///
|
|
/// ```no_run
|
|
/// # use pageserver::tenant::block_io::{BlockReader, FileBlockReader};
|
|
/// # use pageserver::context::RequestContext;
|
|
/// # let reader: FileBlockReader = unimplemented!("stub");
|
|
/// # let ctx: RequestContext = unimplemented!("stub");
|
|
/// let cursor = reader.block_cursor();
|
|
/// let buf = cursor.read_blk(1, &ctx);
|
|
/// // do stuff with 'buf'
|
|
/// let buf = cursor.read_blk(2, &ctx);
|
|
/// // do stuff with 'buf'
|
|
/// ```
|
|
///
|
|
pub struct BlockCursor<'a> {
|
|
reader: BlockReaderRef<'a>,
|
|
}
|
|
|
|
impl<'a> BlockCursor<'a> {
|
|
pub(crate) fn new(reader: BlockReaderRef<'a>) -> Self {
|
|
BlockCursor { reader }
|
|
}
|
|
// Needed by cli
|
|
pub fn new_fileblockreader(reader: &'a FileBlockReader) -> Self {
|
|
BlockCursor {
|
|
reader: BlockReaderRef::FileBlockReader(reader),
|
|
}
|
|
}
|
|
|
|
/// Read a block.
|
|
///
|
|
/// Returns a "lease" object that can be used to
|
|
/// access to the contents of the page. (For the page cache, the
|
|
/// lease object represents a lock on the buffer.)
|
|
#[inline(always)]
|
|
pub async fn read_blk(
|
|
&self,
|
|
blknum: u32,
|
|
ctx: &RequestContext,
|
|
) -> Result<BlockLease, std::io::Error> {
|
|
self.reader.read_blk(blknum, ctx).await
|
|
}
|
|
}
|
|
|
|
/// An adapter for reading a (virtual) file using the page cache.
|
|
///
|
|
/// The file is assumed to be immutable. This doesn't provide any functions
|
|
/// for modifying the file, nor for invalidating the cache if it is modified.
|
|
pub struct FileBlockReader<'a> {
|
|
pub file: &'a VirtualFile,
|
|
|
|
/// Unique ID of this file, used as key in the page cache.
|
|
file_id: page_cache::FileId,
|
|
}
|
|
|
|
impl<'a> FileBlockReader<'a> {
|
|
pub fn new(file: &'a VirtualFile, file_id: FileId) -> Self {
|
|
FileBlockReader { file_id, file }
|
|
}
|
|
|
|
/// Read a page from the underlying file into given buffer.
|
|
async fn fill_buffer(
|
|
&self,
|
|
buf: PageWriteGuard<'static>,
|
|
blkno: u32,
|
|
) -> Result<PageWriteGuard<'static>, std::io::Error> {
|
|
assert!(buf.len() == PAGE_SZ);
|
|
self.file
|
|
.read_exact_at_page(buf, blkno as u64 * PAGE_SZ as u64)
|
|
.await
|
|
}
|
|
/// Read a block.
|
|
///
|
|
/// Returns a "lease" object that can be used to
|
|
/// access to the contents of the page. (For the page cache, the
|
|
/// lease object represents a lock on the buffer.)
|
|
pub async fn read_blk<'b>(
|
|
&self,
|
|
blknum: u32,
|
|
ctx: &RequestContext,
|
|
) -> Result<BlockLease<'b>, std::io::Error> {
|
|
let cache = page_cache::get();
|
|
match cache
|
|
.read_immutable_buf(self.file_id, blknum, ctx)
|
|
.await
|
|
.map_err(|e| {
|
|
std::io::Error::new(
|
|
std::io::ErrorKind::Other,
|
|
format!("Failed to read immutable buf: {e:#}"),
|
|
)
|
|
})? {
|
|
ReadBufResult::Found(guard) => Ok(guard.into()),
|
|
ReadBufResult::NotFound(write_guard) => {
|
|
// Read the page from disk into the buffer
|
|
let write_guard = self.fill_buffer(write_guard, blknum).await?;
|
|
Ok(write_guard.mark_valid().into())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl BlockReader for FileBlockReader<'_> {
|
|
fn block_cursor(&self) -> BlockCursor<'_> {
|
|
BlockCursor::new(BlockReaderRef::FileBlockReader(self))
|
|
}
|
|
}
|
|
|
|
///
|
|
/// Trait for block-oriented output
|
|
///
|
|
pub trait BlockWriter {
|
|
///
|
|
/// Write a page to the underlying storage.
|
|
///
|
|
/// 'buf' must be of size PAGE_SZ. Returns the block number the page was
|
|
/// written to.
|
|
///
|
|
fn write_blk(&mut self, buf: Bytes) -> Result<u32, std::io::Error>;
|
|
}
|
|
|
|
///
|
|
/// A simple in-memory buffer of blocks.
|
|
///
|
|
pub struct BlockBuf {
|
|
pub blocks: Vec<Bytes>,
|
|
}
|
|
impl BlockWriter for BlockBuf {
|
|
fn write_blk(&mut self, buf: Bytes) -> Result<u32, std::io::Error> {
|
|
assert!(buf.len() == PAGE_SZ);
|
|
let blknum = self.blocks.len();
|
|
self.blocks.push(buf);
|
|
Ok(blknum as u32)
|
|
}
|
|
}
|
|
|
|
impl BlockBuf {
|
|
pub fn new() -> Self {
|
|
BlockBuf { blocks: Vec::new() }
|
|
}
|
|
|
|
pub fn size(&self) -> u64 {
|
|
(self.blocks.len() * PAGE_SZ) as u64
|
|
}
|
|
}
|
|
impl Default for BlockBuf {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|