mirror of
https://github.com/neondatabase/neon.git
synced 2026-10-28 22:46:46 +00:00
Measure cross-AZ traffic in safekeepers (#3806)
Create `safekeeper_pg_io_bytes_total` metric to track total amount of bytes written/read in a postgres connections to safekeepers. This metric has the following labels: - `client_az` – availability zone of the connection initiator, or `"unknown"` - `sk_az` – availability zone of the safekeeper, or `"unknown"` - `app_name` – `application_name` of the postgres client - `dir` – data direction, either `"read"` or `"write"` - `same_az` – `"true"`, `"false"` or `"unknown"`. Can be derived from `client_az` and `sk_az`, exists purely for convenience. This is implemented by passing availability zone in the connection string, like this: `-c tenant_id=AAA timeline_id=BBB availability-zone=AZ-1`. Update ansible deployment scripts to add availability_zone argument to safekeeper and pageserver in systemd service files.
This commit is contained in:
@@ -71,6 +71,9 @@ struct Args {
|
||||
/// Listen http endpoint for management and metrics in the form host:port.
|
||||
#[arg(long, default_value = DEFAULT_HTTP_LISTEN_ADDR)]
|
||||
listen_http: String,
|
||||
/// Availability zone of the safekeeper.
|
||||
#[arg(long)]
|
||||
availability_zone: Option<String>,
|
||||
/// Do not wait for changes to be written safely to disk. Unsafe.
|
||||
#[arg(short, long)]
|
||||
no_sync: bool,
|
||||
@@ -166,6 +169,7 @@ fn main() -> anyhow::Result<()> {
|
||||
my_id: id,
|
||||
listen_pg_addr: args.listen_pg,
|
||||
listen_http_addr: args.listen_http,
|
||||
availability_zone: args.availability_zone,
|
||||
no_sync: args.no_sync,
|
||||
broker_endpoint: args.broker_endpoint,
|
||||
broker_keepalive_interval: args.broker_keepalive_interval,
|
||||
|
||||
@@ -9,6 +9,7 @@ use tracing::{info, info_span, Instrument};
|
||||
use crate::auth::check_permission;
|
||||
use crate::json_ctrl::{handle_json_ctrl, AppendLogicalMessage};
|
||||
|
||||
use crate::metrics::TrafficMetrics;
|
||||
use crate::wal_service::ConnectionId;
|
||||
use crate::{GlobalTimelines, SafeKeeperConf};
|
||||
use postgres_backend::QueryError;
|
||||
@@ -33,6 +34,7 @@ pub struct SafekeeperPostgresHandler {
|
||||
/// Unique connection id is logged in spans for observability.
|
||||
pub conn_id: ConnectionId,
|
||||
claims: Option<Claims>,
|
||||
io_metrics: Option<TrafficMetrics>,
|
||||
}
|
||||
|
||||
/// Parsed Postgres command.
|
||||
@@ -94,6 +96,11 @@ impl<IO: AsyncRead + AsyncWrite + Unpin + Send> postgres_backend::Handler<IO>
|
||||
format!("Failed to parse {value} as timeline id")
|
||||
})?);
|
||||
}
|
||||
Some(("availability_zone", client_az)) => {
|
||||
if let Some(metrics) = self.io_metrics.as_ref() {
|
||||
metrics.set_client_az(client_az)
|
||||
}
|
||||
}
|
||||
_ => continue,
|
||||
}
|
||||
}
|
||||
@@ -101,6 +108,9 @@ impl<IO: AsyncRead + AsyncWrite + Unpin + Send> postgres_backend::Handler<IO>
|
||||
|
||||
if let Some(app_name) = params.get("application_name") {
|
||||
self.appname = Some(app_name.to_owned());
|
||||
if let Some(metrics) = self.io_metrics.as_ref() {
|
||||
metrics.set_app_name(app_name)
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -187,7 +197,7 @@ impl<IO: AsyncRead + AsyncWrite + Unpin + Send> postgres_backend::Handler<IO>
|
||||
}
|
||||
|
||||
impl SafekeeperPostgresHandler {
|
||||
pub fn new(conf: SafeKeeperConf, conn_id: u32) -> Self {
|
||||
pub fn new(conf: SafeKeeperConf, conn_id: u32, io_metrics: Option<TrafficMetrics>) -> Self {
|
||||
SafekeeperPostgresHandler {
|
||||
conf,
|
||||
appname: None,
|
||||
@@ -196,6 +206,7 @@ impl SafekeeperPostgresHandler {
|
||||
ttid: TenantTimelineId::empty(),
|
||||
conn_id,
|
||||
claims: None,
|
||||
io_metrics,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -52,6 +52,7 @@ pub struct SafeKeeperConf {
|
||||
pub my_id: NodeId,
|
||||
pub listen_pg_addr: String,
|
||||
pub listen_http_addr: String,
|
||||
pub availability_zone: Option<String>,
|
||||
pub no_sync: bool,
|
||||
pub broker_endpoint: Uri,
|
||||
pub broker_keepalive_interval: Duration,
|
||||
@@ -82,6 +83,7 @@ impl SafeKeeperConf {
|
||||
no_sync: false,
|
||||
listen_pg_addr: defaults::DEFAULT_PG_LISTEN_ADDR.to_string(),
|
||||
listen_http_addr: defaults::DEFAULT_HTTP_LISTEN_ADDR.to_string(),
|
||||
availability_zone: None,
|
||||
remote_storage: None,
|
||||
my_id: NodeId(0),
|
||||
broker_endpoint: storage_broker::DEFAULT_ENDPOINT
|
||||
|
||||
+127
-4
@@ -1,15 +1,19 @@
|
||||
//! Global safekeeper mertics and per-timeline safekeeper metrics.
|
||||
|
||||
use std::time::{Instant, SystemTime};
|
||||
use std::{
|
||||
sync::{Arc, RwLock},
|
||||
time::{Instant, SystemTime},
|
||||
};
|
||||
|
||||
use ::metrics::{register_histogram, GaugeVec, Histogram, IntGauge, DISK_WRITE_SECONDS_BUCKETS};
|
||||
use anyhow::Result;
|
||||
use metrics::{
|
||||
core::{AtomicU64, Collector, Desc, GenericGaugeVec, Opts},
|
||||
core::{AtomicU64, Collector, Desc, GenericCounter, GenericGaugeVec, Opts},
|
||||
proto::MetricFamily,
|
||||
register_int_counter_vec, Gauge, IntCounterVec, IntGaugeVec,
|
||||
};
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
use postgres_ffi::XLogSegNo;
|
||||
use utils::{id::TenantTimelineId, lsn::Lsn};
|
||||
|
||||
@@ -63,13 +67,132 @@ pub static PERSIST_CONTROL_FILE_SECONDS: Lazy<Histogram> = Lazy::new(|| {
|
||||
});
|
||||
pub static PG_IO_BYTES: Lazy<IntCounterVec> = Lazy::new(|| {
|
||||
register_int_counter_vec!(
|
||||
"safekeeper_pg_io_bytes",
|
||||
"safekeeper_pg_io_bytes_total",
|
||||
"Bytes read from or written to any PostgreSQL connection",
|
||||
&["direction"]
|
||||
&["client_az", "sk_az", "app_name", "dir", "same_az"]
|
||||
)
|
||||
.expect("Failed to register safekeeper_pg_io_bytes gauge")
|
||||
});
|
||||
|
||||
pub const LABEL_UNKNOWN: &str = "unknown";
|
||||
|
||||
/// Labels for traffic metrics.
|
||||
#[derive(Clone)]
|
||||
struct ConnectionLabels {
|
||||
/// Availability zone of the connection origin.
|
||||
client_az: String,
|
||||
/// Availability zone of the current safekeeper.
|
||||
sk_az: String,
|
||||
/// Client application name.
|
||||
app_name: String,
|
||||
}
|
||||
|
||||
impl ConnectionLabels {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
client_az: LABEL_UNKNOWN.to_string(),
|
||||
sk_az: LABEL_UNKNOWN.to_string(),
|
||||
app_name: LABEL_UNKNOWN.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn build_metrics(
|
||||
&self,
|
||||
) -> (
|
||||
GenericCounter<metrics::core::AtomicU64>,
|
||||
GenericCounter<metrics::core::AtomicU64>,
|
||||
) {
|
||||
let same_az = match (self.client_az.as_str(), self.sk_az.as_str()) {
|
||||
(LABEL_UNKNOWN, _) | (_, LABEL_UNKNOWN) => LABEL_UNKNOWN,
|
||||
(client_az, sk_az) => {
|
||||
if client_az == sk_az {
|
||||
"true"
|
||||
} else {
|
||||
"false"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let read = PG_IO_BYTES.with_label_values(&[
|
||||
&self.client_az,
|
||||
&self.sk_az,
|
||||
&self.app_name,
|
||||
"read",
|
||||
same_az,
|
||||
]);
|
||||
let write = PG_IO_BYTES.with_label_values(&[
|
||||
&self.client_az,
|
||||
&self.sk_az,
|
||||
&self.app_name,
|
||||
"write",
|
||||
same_az,
|
||||
]);
|
||||
(read, write)
|
||||
}
|
||||
}
|
||||
|
||||
struct TrafficMetricsState {
|
||||
/// Labels for traffic metrics.
|
||||
labels: ConnectionLabels,
|
||||
/// Total bytes read from this connection.
|
||||
read: GenericCounter<metrics::core::AtomicU64>,
|
||||
/// Total bytes written to this connection.
|
||||
write: GenericCounter<metrics::core::AtomicU64>,
|
||||
}
|
||||
|
||||
/// Metrics for measuring traffic (r/w bytes) in a single PostgreSQL connection.
|
||||
#[derive(Clone)]
|
||||
pub struct TrafficMetrics {
|
||||
state: Arc<RwLock<TrafficMetricsState>>,
|
||||
}
|
||||
|
||||
impl Default for TrafficMetrics {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl TrafficMetrics {
|
||||
pub fn new() -> Self {
|
||||
let labels = ConnectionLabels::new();
|
||||
let (read, write) = labels.build_metrics();
|
||||
let state = TrafficMetricsState {
|
||||
labels,
|
||||
read,
|
||||
write,
|
||||
};
|
||||
Self {
|
||||
state: Arc::new(RwLock::new(state)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_client_az(&self, value: &str) {
|
||||
let mut state = self.state.write().unwrap();
|
||||
state.labels.client_az = value.to_string();
|
||||
(state.read, state.write) = state.labels.build_metrics();
|
||||
}
|
||||
|
||||
pub fn set_sk_az(&self, value: &str) {
|
||||
let mut state = self.state.write().unwrap();
|
||||
state.labels.sk_az = value.to_string();
|
||||
(state.read, state.write) = state.labels.build_metrics();
|
||||
}
|
||||
|
||||
pub fn set_app_name(&self, value: &str) {
|
||||
let mut state = self.state.write().unwrap();
|
||||
state.labels.app_name = value.to_string();
|
||||
(state.read, state.write) = state.labels.build_metrics();
|
||||
}
|
||||
|
||||
pub fn observe_read(&self, cnt: usize) {
|
||||
self.state.read().unwrap().read.inc_by(cnt as u64)
|
||||
}
|
||||
|
||||
pub fn observe_write(&self, cnt: usize) {
|
||||
self.state.read().unwrap().write.inc_by(cnt as u64)
|
||||
}
|
||||
}
|
||||
|
||||
/// Metrics for WalStorage in a single timeline.
|
||||
#[derive(Clone, Default)]
|
||||
pub struct WalStorageMetrics {
|
||||
|
||||
@@ -9,8 +9,9 @@ use tokio::net::TcpStream;
|
||||
use tracing::*;
|
||||
use utils::measured_stream::MeasuredStream;
|
||||
|
||||
use crate::handler::SafekeeperPostgresHandler;
|
||||
use crate::metrics::TrafficMetrics;
|
||||
use crate::SafeKeeperConf;
|
||||
use crate::{handler::SafekeeperPostgresHandler, metrics::PG_IO_BYTES};
|
||||
use postgres_backend::{AuthType, PostgresBackend};
|
||||
|
||||
/// Accept incoming TCP connections and spawn them into a background thread.
|
||||
@@ -68,20 +69,21 @@ fn handle_socket(
|
||||
.build()?;
|
||||
let local = tokio::task::LocalSet::new();
|
||||
|
||||
let read_metrics = PG_IO_BYTES.with_label_values(&["read"]);
|
||||
let write_metrics = PG_IO_BYTES.with_label_values(&["write"]);
|
||||
|
||||
socket.set_nodelay(true)?;
|
||||
let peer_addr = socket.peer_addr()?;
|
||||
|
||||
// TODO: measure cross-az traffic
|
||||
let traffic_metrics = TrafficMetrics::new();
|
||||
if let Some(current_az) = conf.availability_zone.as_deref() {
|
||||
traffic_metrics.set_sk_az(current_az);
|
||||
}
|
||||
|
||||
let socket = MeasuredStream::new(
|
||||
socket,
|
||||
|cnt| {
|
||||
read_metrics.inc_by(cnt as u64);
|
||||
traffic_metrics.observe_read(cnt);
|
||||
},
|
||||
|cnt| {
|
||||
write_metrics.inc_by(cnt as u64);
|
||||
traffic_metrics.observe_write(cnt);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -89,7 +91,8 @@ fn handle_socket(
|
||||
None => AuthType::Trust,
|
||||
Some(_) => AuthType::NeonJWT,
|
||||
};
|
||||
let mut conn_handler = SafekeeperPostgresHandler::new(conf, conn_id);
|
||||
let mut conn_handler =
|
||||
SafekeeperPostgresHandler::new(conf, conn_id, Some(traffic_metrics.clone()));
|
||||
let pgbackend = PostgresBackend::new_from_io(socket, peer_addr, auth_type, None)?;
|
||||
// libpq protocol between safekeeper and walproposer / pageserver
|
||||
// We don't use shutdown.
|
||||
|
||||
Reference in New Issue
Block a user