mirror of
https://github.com/neondatabase/neon.git
synced 2026-01-13 08:22:55 +00:00
# TLDR All changes are no-op except some metrics. ## Summary of changes I ### Pageserver Added a new global counter metric `pageserver_pagestream_handler_results_total` that categorizes pagestream request results according to their outcomes: 1. Success 2. Internal errors 3. Other errors Internal errors include: 1. Page reconstruction error: This probably indicates a pageserver bug/corruption 2. LSN timeout error: Could indicate overload or bugs with PS's ability to reach other components 3. Misrouted request error: Indicates bugs in the Storage Controller/HCC Other errors include transient errors that are expected during normal operation or errors indicating bugs with other parts of the system (e.g., malformed requests, errors due to cancelled operations during PS shutdown, etc.) ## Summary of changes II This PR adds a pageserver endpoint and its counterpart in storage controller to list visible size of all tenant shards. This will be a prerequisite of the tenant rebalance command. ## Problem III We need a way to download WAL segments/layerfiles from S3 and replay WAL records. We cannot access production S3 from our laptops directly, and we also can't transfer any user data out of production systems for GDPR compliance, so we need solutions. ## Summary of changes III This PR adds a couple of tools to support the debugging workflow in production: 1. A new `pagectl download-remote-object` command that can be used to download remote storage objects assuming the correct access is set up. ## Summary of changes IV This PR adds a command to list all visible delta and image layers from index_part. This is useful to debug compaction issues as index_part often contain a lot of covered layers due to PITR. --------- Co-authored-by: William Huang <william.huang@databricks.com> Co-authored-by: Chen Luo <chen.luo@databricks.com> Co-authored-by: Vlad Lazar <vlad@neon.tech>
199 lines
6.0 KiB
Rust
199 lines
6.0 KiB
Rust
use std::str::FromStr;
|
|
|
|
use anyhow::{Context, Ok};
|
|
use camino::Utf8PathBuf;
|
|
use pageserver::tenant::{
|
|
IndexPart,
|
|
layer_map::{LayerMap, SearchResult},
|
|
remote_timeline_client::{index::LayerFileMetadata, remote_layer_path},
|
|
storage_layer::{LayerName, LayerVisibilityHint, PersistentLayerDesc, ReadableLayerWeak},
|
|
};
|
|
use pageserver_api::key::Key;
|
|
use serde::Serialize;
|
|
use std::collections::BTreeMap;
|
|
use utils::{
|
|
id::{TenantId, TimelineId},
|
|
lsn::Lsn,
|
|
shard::TenantShardId,
|
|
};
|
|
|
|
#[derive(clap::Subcommand)]
|
|
pub(crate) enum IndexPartCmd {
|
|
Dump {
|
|
path: Utf8PathBuf,
|
|
},
|
|
/// Find all layers that need to be searched to construct the given page at the given LSN.
|
|
Search {
|
|
#[arg(long)]
|
|
tenant_id: String,
|
|
#[arg(long)]
|
|
timeline_id: String,
|
|
#[arg(long)]
|
|
path: Utf8PathBuf,
|
|
#[arg(long)]
|
|
key: String,
|
|
#[arg(long)]
|
|
lsn: String,
|
|
},
|
|
/// List all visible delta and image layers at the latest LSN.
|
|
ListVisibleLayers {
|
|
#[arg(long)]
|
|
path: Utf8PathBuf,
|
|
},
|
|
}
|
|
|
|
fn create_layer_map_from_index_part(
|
|
index_part: &IndexPart,
|
|
tenant_shard_id: TenantShardId,
|
|
timeline_id: TimelineId,
|
|
) -> LayerMap {
|
|
let mut layer_map = LayerMap::default();
|
|
{
|
|
let mut updates = layer_map.batch_update();
|
|
for (key, value) in index_part.layer_metadata.iter() {
|
|
updates.insert_historic(PersistentLayerDesc::from_filename(
|
|
tenant_shard_id,
|
|
timeline_id,
|
|
key.clone(),
|
|
value.file_size,
|
|
));
|
|
}
|
|
}
|
|
layer_map
|
|
}
|
|
|
|
async fn search_layers(
|
|
tenant_id: &str,
|
|
timeline_id: &str,
|
|
path: &Utf8PathBuf,
|
|
key: &str,
|
|
lsn: &str,
|
|
) -> anyhow::Result<()> {
|
|
let tenant_id = TenantId::from_str(tenant_id).unwrap();
|
|
let tenant_shard_id = TenantShardId::unsharded(tenant_id);
|
|
let timeline_id = TimelineId::from_str(timeline_id).unwrap();
|
|
let index_json = {
|
|
let bytes = tokio::fs::read(path).await?;
|
|
IndexPart::from_json_bytes(&bytes).unwrap()
|
|
};
|
|
let layer_map = create_layer_map_from_index_part(&index_json, tenant_shard_id, timeline_id);
|
|
let key = Key::from_hex(key)?;
|
|
|
|
let lsn = Lsn::from_str(lsn).unwrap();
|
|
let mut end_lsn = lsn;
|
|
loop {
|
|
let result = layer_map.search(key, end_lsn);
|
|
match result {
|
|
Some(SearchResult { layer, lsn_floor }) => {
|
|
let disk_layer = match layer {
|
|
ReadableLayerWeak::PersistentLayer(layer) => layer,
|
|
ReadableLayerWeak::InMemoryLayer(_) => {
|
|
anyhow::bail!("unexpected in-memory layer")
|
|
}
|
|
};
|
|
|
|
let metadata = index_json
|
|
.layer_metadata
|
|
.get(&disk_layer.layer_name())
|
|
.unwrap();
|
|
println!(
|
|
"{}",
|
|
remote_layer_path(
|
|
&tenant_id,
|
|
&timeline_id,
|
|
metadata.shard,
|
|
&disk_layer.layer_name(),
|
|
metadata.generation
|
|
)
|
|
);
|
|
end_lsn = lsn_floor;
|
|
}
|
|
None => break,
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize)]
|
|
struct VisibleLayers {
|
|
pub total_images: u64,
|
|
pub total_image_bytes: u64,
|
|
pub total_deltas: u64,
|
|
pub total_delta_bytes: u64,
|
|
pub layer_metadata: BTreeMap<LayerName, LayerFileMetadata>,
|
|
}
|
|
|
|
impl VisibleLayers {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
layer_metadata: BTreeMap::new(),
|
|
total_images: 0,
|
|
total_image_bytes: 0,
|
|
total_deltas: 0,
|
|
total_delta_bytes: 0,
|
|
}
|
|
}
|
|
|
|
pub fn add_layer(&mut self, name: LayerName, layer: LayerFileMetadata) {
|
|
match name {
|
|
LayerName::Image(_) => {
|
|
self.total_images += 1;
|
|
self.total_image_bytes += layer.file_size;
|
|
}
|
|
LayerName::Delta(_) => {
|
|
self.total_deltas += 1;
|
|
self.total_delta_bytes += layer.file_size;
|
|
}
|
|
}
|
|
self.layer_metadata.insert(name, layer);
|
|
}
|
|
}
|
|
|
|
async fn list_visible_layers(path: &Utf8PathBuf) -> anyhow::Result<()> {
|
|
let tenant_id = TenantId::generate();
|
|
let tenant_shard_id = TenantShardId::unsharded(tenant_id);
|
|
let timeline_id = TimelineId::generate();
|
|
|
|
let bytes = tokio::fs::read(path).await.context("read file")?;
|
|
let index_part = IndexPart::from_json_bytes(&bytes).context("deserialize")?;
|
|
let layer_map = create_layer_map_from_index_part(&index_part, tenant_shard_id, timeline_id);
|
|
let mut visible_layers = VisibleLayers::new();
|
|
let (layers, _key_space) = layer_map.get_visibility(Vec::new());
|
|
for (layer, visibility) in layers {
|
|
if visibility == LayerVisibilityHint::Visible {
|
|
visible_layers.add_layer(
|
|
layer.layer_name(),
|
|
index_part
|
|
.layer_metadata
|
|
.get(&layer.layer_name())
|
|
.unwrap()
|
|
.clone(),
|
|
);
|
|
}
|
|
}
|
|
let output = serde_json::to_string_pretty(&visible_layers).context("serialize output")?;
|
|
println!("{output}");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) async fn main(cmd: &IndexPartCmd) -> anyhow::Result<()> {
|
|
match cmd {
|
|
IndexPartCmd::Dump { path } => {
|
|
let bytes = tokio::fs::read(path).await.context("read file")?;
|
|
let des: IndexPart = IndexPart::from_json_bytes(&bytes).context("deserialize")?;
|
|
let output = serde_json::to_string_pretty(&des).context("serialize output")?;
|
|
println!("{output}");
|
|
Ok(())
|
|
}
|
|
IndexPartCmd::Search {
|
|
tenant_id,
|
|
timeline_id,
|
|
path,
|
|
key,
|
|
lsn,
|
|
} => search_layers(tenant_id, timeline_id, path, key, lsn).await,
|
|
IndexPartCmd::ListVisibleLayers { path } => list_visible_layers(path).await,
|
|
}
|
|
}
|