-
Notifications
You must be signed in to change notification settings - Fork 267
feat(sandbox): add Firecracker disk I/O rate limiting (bandwidth + IOPS) #48
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
ef01c5c
ab2ab1f
16f1722
d394ede
26994b8
3535c8b
de45f98
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -252,6 +252,32 @@ pub struct MachineConfig { | |||||||||||||||||||||||||||||||||||||||||||||||||
| pub vcpu_count: u32, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| #[config(default = 1024u32)] | ||||||||||||||||||||||||||||||||||||||||||||||||||
| pub mem_size_mib: u32, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| #[config(nested)] | ||||||||||||||||||||||||||||||||||||||||||||||||||
| pub disk_rate_limit: DiskRateLimitConfig, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| #[derive(Debug, Config, Clone, Default, serde::Serialize, serde::Deserialize)] | ||||||||||||||||||||||||||||||||||||||||||||||||||
| pub struct DiskRateLimitConfig { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Enable per-sandbox disk I/O rate limiting via Firecracker's virtio-blk rate limiter. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| #[config(default = false)] | ||||||||||||||||||||||||||||||||||||||||||||||||||
| pub enabled: bool, | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+261
to
+263
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [bug · medium] Suggestion:
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Sustained disk bandwidth limit in bytes per second (0 = unlimited). | ||||||||||||||||||||||||||||||||||||||||||||||||||
| #[config(default = 0u64)] | ||||||||||||||||||||||||||||||||||||||||||||||||||
| pub bandwidth_bytes_per_sec: u64, | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+264
to
+266
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This value is described as a per-second rate, but the Firecracker integration passes it directly as the token-bucket |
||||||||||||||||||||||||||||||||||||||||||||||||||
| /// One-time bandwidth burst in bytes, granted once when the VM starts (maps | ||||||||||||||||||||||||||||||||||||||||||||||||||
| /// to Firecracker's `one_time_burst`). It is a separate allowance consumed | ||||||||||||||||||||||||||||||||||||||||||||||||||
| /// before the sustained bucket and is not replenished after use, so it only | ||||||||||||||||||||||||||||||||||||||||||||||||||
| /// absorbs the initial I/O spike; it does not raise the steady-state rate. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| #[config(default = 0u64)] | ||||||||||||||||||||||||||||||||||||||||||||||||||
| pub bandwidth_burst_bytes: u64, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Sustained IOPS limit (0 = unlimited). | ||||||||||||||||||||||||||||||||||||||||||||||||||
| #[config(default = 0u64)] | ||||||||||||||||||||||||||||||||||||||||||||||||||
| pub iops: u64, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| /// One-time IOPS burst, granted once when the VM starts (maps to | ||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Firecracker's `one_time_burst`). Consumed before the sustained bucket and | ||||||||||||||||||||||||||||||||||||||||||||||||||
| /// not replenished, so it only absorbs the initial spike, not steady state. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| #[config(default = 0u64)] | ||||||||||||||||||||||||||||||||||||||||||||||||||
| pub iops_burst: u64, | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+265
to
+280
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [bug · medium]
Comment on lines
+271
to
+280
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [bug · medium] |
||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| #[derive(Debug, Config, Clone)] | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -778,6 +804,52 @@ impl AppConfig { | |||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| self.validate_memory_snapshot_background_download()?; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| self.validate_overlaybd_global_config_paths()?; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| self.validate_disk_rate_limit()?; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| Ok(()) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Reject internally inconsistent or out-of-range disk rate limit configs so | ||||||||||||||||||||||||||||||||||||||||||||||||||
| /// operator mistakes fail at load time. Disabled sections are skipped: both | ||||||||||||||||||||||||||||||||||||||||||||||||||
| /// the fresh-boot and snapshot-resume paths ignore all configured values when | ||||||||||||||||||||||||||||||||||||||||||||||||||
| /// disabled, so dormant/pre-staged values must not block startup. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| /// | ||||||||||||||||||||||||||||||||||||||||||||||||||
| /// When enabled: a one-time burst is meaningless without a nonzero sustained | ||||||||||||||||||||||||||||||||||||||||||||||||||
| /// limit (`build_disk_rate_limiter` only creates a bucket when the sustained | ||||||||||||||||||||||||||||||||||||||||||||||||||
| /// value is > 0, so a burst paired with a zero sustained limit is silently | ||||||||||||||||||||||||||||||||||||||||||||||||||
| /// ignored), and every value must fit Firecracker's signed `i64` token-bucket | ||||||||||||||||||||||||||||||||||||||||||||||||||
| /// fields (the consumer converts with `i64::try_from`, so an out-of-range | ||||||||||||||||||||||||||||||||||||||||||||||||||
| /// value would otherwise only fail later at sandbox start). | ||||||||||||||||||||||||||||||||||||||||||||||||||
| fn validate_disk_rate_limit(&self) -> Result<()> { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let cfg = &self.machine.disk_rate_limit; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if !cfg.enabled { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return Ok(()); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if cfg.bandwidth_burst_bytes > 0 && cfg.bandwidth_bytes_per_sec == 0 { | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+823
to
+827
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [bug · medium] Suggestion:
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| bail!( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| "machine.disk_rate_limit: bandwidth_burst_bytes is set but \ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| bandwidth_bytes_per_sec is 0; a burst requires a nonzero sustained limit" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if cfg.iops_burst > 0 && cfg.iops == 0 { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| bail!( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| "machine.disk_rate_limit: iops_burst is set but iops is 0; \ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| a burst requires a nonzero sustained limit" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| for (name, value) in [ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ("bandwidth_bytes_per_sec", cfg.bandwidth_bytes_per_sec), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ("bandwidth_burst_bytes", cfg.bandwidth_burst_bytes), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ("iops", cfg.iops), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ("iops_burst", cfg.iops_burst), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ] { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if value > i64::MAX as u64 { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| bail!( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| "machine.disk_rate_limit.{name} ({value}) exceeds the maximum \ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| supported value {}", | ||||||||||||||||||||||||||||||||||||||||||||||||||
| i64::MAX | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| Ok(()) | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+833
to
853
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [bug · medium] Suggestion:
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -1110,6 +1182,69 @@ mod tests { | |||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| #[test] | ||||||||||||||||||||||||||||||||||||||||||||||||||
| fn validate_rejects_disk_burst_without_sustained() { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let mut config = AppConfig::default(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| config.machine.disk_rate_limit.enabled = true; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| config.machine.disk_rate_limit.bandwidth_bytes_per_sec = 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| config.machine.disk_rate_limit.bandwidth_burst_bytes = 1024; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let err = config.validate().unwrap_err(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| assert!( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| err.to_string().contains("bandwidth_burst_bytes is set but"), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| "unexpected error: {err}" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| let mut config = AppConfig::default(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| config.machine.disk_rate_limit.enabled = true; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| config.machine.disk_rate_limit.iops = 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| config.machine.disk_rate_limit.iops_burst = 500; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let err = config.validate().unwrap_err(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| assert!( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| err.to_string().contains("iops_burst is set but"), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| "unexpected error: {err}" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| #[test] | ||||||||||||||||||||||||||||||||||||||||||||||||||
| fn validate_skips_disabled_disk_rate_limit() { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // A disabled section is ignored at runtime, so even internally | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // inconsistent or out-of-range values must not block startup. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let mut config = AppConfig::default(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| config.machine.disk_rate_limit.enabled = false; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| config.machine.disk_rate_limit.bandwidth_bytes_per_sec = 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| config.machine.disk_rate_limit.bandwidth_burst_bytes = 1024; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| config.machine.disk_rate_limit.iops = u64::MAX; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| config | ||||||||||||||||||||||||||||||||||||||||||||||||||
| .validate() | ||||||||||||||||||||||||||||||||||||||||||||||||||
| .expect("disabled disk rate limit config is not validated"); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| #[test] | ||||||||||||||||||||||||||||||||||||||||||||||||||
| fn validate_rejects_disk_rate_limit_above_i64_max() { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let mut config = AppConfig::default(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| config.machine.disk_rate_limit.enabled = true; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| config.machine.disk_rate_limit.bandwidth_bytes_per_sec = i64::MAX as u64 + 1; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let err = config.validate().unwrap_err(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| assert!( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| err.to_string() | ||||||||||||||||||||||||||||||||||||||||||||||||||
| .contains("machine.disk_rate_limit.bandwidth_bytes_per_sec"), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| "unexpected error: {err}" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| #[test] | ||||||||||||||||||||||||||||||||||||||||||||||||||
| fn validate_accepts_consistent_disk_rate_limit() { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let mut config = AppConfig::default(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| config.machine.disk_rate_limit.enabled = true; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| config.machine.disk_rate_limit.bandwidth_bytes_per_sec = 104_857_600; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| config.machine.disk_rate_limit.bandwidth_burst_bytes = 10_485_760; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| config.machine.disk_rate_limit.iops = 3000; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| config.machine.disk_rate_limit.iops_burst = 500; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| config | ||||||||||||||||||||||||||||||||||||||||||||||||||
| .validate() | ||||||||||||||||||||||||||||||||||||||||||||||||||
| .expect("consistent disk rate limit config passes"); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| #[test] | ||||||||||||||||||||||||||||||||||||||||||||||||||
| fn validate_rejects_shared_overlaybd_global_config_path() { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let mut config = AppConfig::default(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[documentation · low]
“At VM start” is misleading for snapshot restores. The resume path PATCHes a newly constructed rate limiter before every
resume(), includingone_time_burst, so Firecracker can grant this allowance again on each restore/resume rather than only at the original VM boot. Please document that the burst is one-time per limiter application (fresh boot or snapshot restore), or avoid sending it during resume if the intended contract is truly once per VM lifecycle.Suggestion: