LCOV - code coverage report
Current view: top level - libs/pageserver_api/src/models - utilization.rs (source / functions) Coverage Total Hit
Test: 2a9d99866121f170b43760bd62e1e2431e597707.info Lines: 82.1 % 84 69
Test Date: 2024-09-02 14:10:37 Functions: 30.8 % 26 8

            Line data    Source code
       1              : use std::time::SystemTime;
       2              : use utils::{serde_percent::Percent, serde_system_time};
       3              : 
       4              : /// Pageserver current utilization and scoring for how good candidate the pageserver would be for
       5              : /// the next tenant.
       6              : ///
       7              : /// See and maintain pageserver openapi spec for `/v1/utilization_score` as the truth.
       8              : ///
       9              : /// `format: int64` fields must use `ser_saturating_u63` because openapi generated clients might
      10              : /// not handle full u64 values properly.
      11            3 : #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
      12              : pub struct PageserverUtilization {
      13              :     /// Used disk space (physical, ground truth from statfs())
      14              :     #[serde(serialize_with = "ser_saturating_u63")]
      15              :     pub disk_usage_bytes: u64,
      16              :     /// Free disk space
      17              :     #[serde(serialize_with = "ser_saturating_u63")]
      18              :     pub free_space_bytes: u64,
      19              : 
      20              :     /// Wanted disk space, based on the tenant shards currently present on this pageserver: this
      21              :     /// is like disk_usage_bytes, but it is stable and does not change with the cache state of
      22              :     /// tenants, whereas disk_usage_bytes may reach the disk eviction `max_usage_pct` and stay
      23              :     /// there, or may be unrealistically low if the pageserver has attached tenants which haven't
      24              :     /// downloaded layers yet.
      25              :     #[serde(serialize_with = "ser_saturating_u63", default)]
      26              :     pub disk_wanted_bytes: u64,
      27              : 
      28              :     // What proportion of total disk space will this pageserver use before it starts evicting data?
      29              :     #[serde(default = "unity_percent")]
      30              :     pub disk_usable_pct: Percent,
      31              : 
      32              :     // How many shards are currently on this node?
      33              :     #[serde(default)]
      34              :     pub shard_count: u32,
      35              : 
      36              :     // How many shards should this node be able to handle at most?
      37              :     #[serde(default)]
      38              :     pub max_shard_count: u32,
      39              : 
      40              :     /// Cached result of [`Self::score`]
      41              :     pub utilization_score: Option<u64>,
      42              : 
      43              :     /// When was this snapshot captured, pageserver local time.
      44              :     ///
      45              :     /// Use millis to give confidence that the value is regenerated often enough.
      46              :     pub captured_at: serde_system_time::SystemTime,
      47              : }
      48              : 
      49            0 : fn unity_percent() -> Percent {
      50            0 :     Percent::new(0).unwrap()
      51            0 : }
      52              : 
      53              : pub type RawScore = u64;
      54              : 
      55              : impl PageserverUtilization {
      56              :     const UTILIZATION_FULL: u64 = 1000000;
      57              : 
      58              :     /// Calculate a utilization score.  The result is to be inrepreted as a fraction of
      59              :     /// Self::UTILIZATION_FULL.
      60              :     ///
      61              :     /// Lower values are more affine to scheduling more work on this node.
      62              :     /// - UTILIZATION_FULL represents an ideal node which is fully utilized but should not receive any more work.
      63              :     /// - 0.0 represents an empty node.
      64              :     /// - Negative values are forbidden
      65              :     /// - Values over UTILIZATION_FULL indicate an overloaded node, which may show degraded performance due to
      66              :     ///   layer eviction.
      67           41 :     pub fn score(&self) -> RawScore {
      68           41 :         let disk_usable_capacity = ((self.disk_usage_bytes + self.free_space_bytes)
      69           41 :             * self.disk_usable_pct.get() as u64)
      70           41 :             / 100;
      71           41 :         let disk_utilization_score =
      72           41 :             self.disk_wanted_bytes * Self::UTILIZATION_FULL / disk_usable_capacity;
      73           41 : 
      74           41 :         let shard_utilization_score =
      75           41 :             self.shard_count as u64 * Self::UTILIZATION_FULL / self.max_shard_count as u64;
      76           41 :         std::cmp::max(disk_utilization_score, shard_utilization_score)
      77           41 :     }
      78              : 
      79           84 :     pub fn cached_score(&mut self) -> RawScore {
      80           84 :         match self.utilization_score {
      81              :             None => {
      82           41 :                 let s = self.score();
      83           41 :                 self.utilization_score = Some(s);
      84           41 :                 s
      85              :             }
      86           43 :             Some(s) => s,
      87              :         }
      88           84 :     }
      89              : 
      90              :     /// If a node is currently hosting more work than it can comfortably handle.  This does not indicate that
      91              :     /// it will fail, but it is a strong signal that more work should not be added unless there is no alternative.
      92           84 :     pub fn is_overloaded(score: RawScore) -> bool {
      93           84 :         score >= Self::UTILIZATION_FULL
      94           84 :     }
      95              : 
      96           43 :     pub fn adjust_shard_count_max(&mut self, shard_count: u32) {
      97           43 :         if self.shard_count < shard_count {
      98           39 :             self.shard_count = shard_count;
      99           39 : 
     100           39 :             // Dirty cache: this will be calculated next time someone retrives the score
     101           39 :             self.utilization_score = None;
     102           39 :         }
     103           43 :     }
     104              : 
     105              :     /// A utilization structure that has a full utilization score: use this as a placeholder when
     106              :     /// you need a utilization but don't have real values yet.
     107            0 :     pub fn full() -> Self {
     108            0 :         Self {
     109            0 :             disk_usage_bytes: 1,
     110            0 :             free_space_bytes: 0,
     111            0 :             disk_wanted_bytes: 1,
     112            0 :             disk_usable_pct: Percent::new(100).unwrap(),
     113            0 :             shard_count: 1,
     114            0 :             max_shard_count: 1,
     115            0 :             utilization_score: Some(Self::UTILIZATION_FULL),
     116            0 :             captured_at: serde_system_time::SystemTime(SystemTime::now()),
     117            0 :         }
     118            0 :     }
     119              : }
     120              : 
     121              : /// Test helper
     122              : pub mod test_utilization {
     123              :     use super::PageserverUtilization;
     124              :     use std::time::SystemTime;
     125              :     use utils::{
     126              :         serde_percent::Percent,
     127              :         serde_system_time::{self},
     128              :     };
     129              : 
     130              :     // Parameters of the imaginary node used for test utilization instances
     131              :     const TEST_DISK_SIZE: u64 = 1024 * 1024 * 1024 * 1024;
     132              :     const TEST_SHARDS_MAX: u32 = 1000;
     133              : 
     134              :     /// Unit test helper.  Unconditionally compiled because cfg(test) doesn't carry across crates.  Do
     135              :     /// not abuse this function from non-test code.
     136              :     ///
     137              :     /// Emulates a node with a 1000 shard limit and a 1TB disk.
     138           27 :     pub fn simple(shard_count: u32, disk_wanted_bytes: u64) -> PageserverUtilization {
     139           27 :         PageserverUtilization {
     140           27 :             disk_usage_bytes: disk_wanted_bytes,
     141           27 :             free_space_bytes: TEST_DISK_SIZE - std::cmp::min(disk_wanted_bytes, TEST_DISK_SIZE),
     142           27 :             disk_wanted_bytes,
     143           27 :             disk_usable_pct: Percent::new(100).unwrap(),
     144           27 :             shard_count,
     145           27 :             max_shard_count: TEST_SHARDS_MAX,
     146           27 :             utilization_score: None,
     147           27 :             captured_at: serde_system_time::SystemTime(SystemTime::now()),
     148           27 :         }
     149           27 :     }
     150              : }
     151              : 
     152              : /// openapi knows only `format: int64`, so avoid outputting a non-parseable value by generated clients.
     153              : ///
     154              : /// Instead of newtype, use this because a newtype would get require handling deserializing values
     155              : /// with the highest bit set which is properly parsed by serde formats, but would create a
     156              : /// conundrum on how to handle and again serialize such values at type level. It will be a few
     157              : /// years until we can use more than `i64::MAX` bytes on a disk.
     158            3 : fn ser_saturating_u63<S: serde::Serializer>(value: &u64, serializer: S) -> Result<S::Ok, S::Error> {
     159            3 :     const MAX_FORMAT_INT64: u64 = i64::MAX as u64;
     160            3 : 
     161            3 :     let value = (*value).min(MAX_FORMAT_INT64);
     162            3 : 
     163            3 :     serializer.serialize_u64(value)
     164            3 : }
     165              : 
     166              : #[cfg(test)]
     167              : mod tests {
     168              :     use std::time::Duration;
     169              : 
     170              :     use super::*;
     171              : 
     172              :     #[test]
     173            1 :     fn u64_max_is_serialized_as_u63_max() {
     174            1 :         let doc = PageserverUtilization {
     175            1 :             disk_usage_bytes: u64::MAX,
     176            1 :             free_space_bytes: 0,
     177            1 :             disk_wanted_bytes: u64::MAX,
     178            1 :             utilization_score: Some(13),
     179            1 :             disk_usable_pct: Percent::new(90).unwrap(),
     180            1 :             shard_count: 100,
     181            1 :             max_shard_count: 200,
     182            1 :             captured_at: serde_system_time::SystemTime(
     183            1 :                 std::time::SystemTime::UNIX_EPOCH + Duration::from_secs(1708509779),
     184            1 :             ),
     185            1 :         };
     186            1 : 
     187            1 :         let s = serde_json::to_string(&doc).unwrap();
     188            1 : 
     189            1 :         let expected = "{\"disk_usage_bytes\":9223372036854775807,\"free_space_bytes\":0,\"disk_wanted_bytes\":9223372036854775807,\"disk_usable_pct\":90,\"shard_count\":100,\"max_shard_count\":200,\"utilization_score\":13,\"captured_at\":\"2024-02-21T10:02:59.000Z\"}";
     190            1 : 
     191            1 :         assert_eq!(s, expected);
     192            1 :     }
     193              : }
        

Generated by: LCOV version 2.1-beta