Skip to content

Commit 05e09fd

Browse files
committed
feat: pmem: add rate-limiter for virtio-pmem device
Add rate-limiter support to the virtio-pmem device to allow users to configure limits of the I/O bandwidth generated by the `msync` call in the device which could be triggered by the guest FLUSH requests. Signed-off-by: Egor Lazarchuk <yegorlz@amazon.co.uk>
1 parent 71cb1f9 commit 05e09fd

16 files changed

Lines changed: 398 additions & 7 deletions

File tree

src/firecracker/src/api_server/parsed_request.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ use super::request::machine_configuration::{
2424
use super::request::metrics::parse_put_metrics;
2525
use super::request::mmds::{parse_get_mmds, parse_patch_mmds, parse_put_mmds};
2626
use super::request::net::{parse_patch_net, parse_put_net};
27-
use super::request::pmem::parse_put_pmem;
27+
use super::request::pmem::{parse_patch_pmem, parse_put_pmem};
2828
use super::request::snapshot::{parse_patch_vm_state, parse_put_snapshot};
2929
use super::request::version::parse_get_version;
3030
use super::request::vsock::parse_put_vsock;
@@ -120,6 +120,7 @@ impl TryFrom<&Request> for ParsedRequest {
120120
(Method::Patch, "network-interfaces", Some(body)) => {
121121
parse_patch_net(body, path_tokens.next())
122122
}
123+
(Method::Patch, "pmem", Some(body)) => parse_patch_pmem(body, path_tokens.next()),
123124
(Method::Patch, "vm", Some(body)) => parse_patch_vm_state(body),
124125
(Method::Patch, "hotplug", Some(body)) if path_tokens.next() == Some("memory") => {
125126
parse_patch_memory_hotplug(body)

src/firecracker/src/api_server/request/pmem.rs

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
use vmm::logger::{IncMetric, METRICS};
55
use vmm::rpc_interface::VmmAction;
6-
use vmm::vmm_config::pmem::PmemConfig;
6+
use vmm::vmm_config::pmem::{PmemConfig, PmemDeviceUpdateConfig};
77

88
use super::super::parsed_request::{ParsedRequest, RequestError, checked_id};
99
use super::{Body, StatusCode};
@@ -37,6 +37,36 @@ pub(crate) fn parse_put_pmem(
3737
}
3838
}
3939

40+
pub(crate) fn parse_patch_pmem(
41+
body: &Body,
42+
id_from_path: Option<&str>,
43+
) -> Result<ParsedRequest, RequestError> {
44+
METRICS.patch_api_requests.pmem_count.inc();
45+
let id = if let Some(id) = id_from_path {
46+
checked_id(id)?
47+
} else {
48+
METRICS.patch_api_requests.pmem_fails.inc();
49+
return Err(RequestError::EmptyID);
50+
};
51+
52+
let update_cfg =
53+
serde_json::from_slice::<PmemDeviceUpdateConfig>(body.raw()).inspect_err(|_| {
54+
METRICS.patch_api_requests.pmem_fails.inc();
55+
})?;
56+
57+
if id == update_cfg.id {
58+
Ok(ParsedRequest::new_sync(VmmAction::UpdatePmemDevice(
59+
update_cfg,
60+
)))
61+
} else {
62+
METRICS.patch_api_requests.pmem_fails.inc();
63+
Err(RequestError::Generic(
64+
StatusCode::BadRequest,
65+
"The id from the path does not match the id from the body!".to_string(),
66+
))
67+
}
68+
}
69+
4070
#[cfg(test)]
4171
mod tests {
4272
use super::*;
@@ -60,7 +90,8 @@ mod tests {
6090
"id": "1000",
6191
"path_on_host": "dummy",
6292
"root_device": true,
63-
"read_only": true
93+
"read_only": true,
94+
"rate_limiter": {}
6495
}"#;
6596
let r = vmm_action_from_request(parse_put_pmem(&Body::new(body), Some("1000")).unwrap());
6697

@@ -69,7 +100,35 @@ mod tests {
69100
path_on_host: "dummy".to_string(),
70101
root_device: true,
71102
read_only: true,
103+
rate_limiter: Some(Default::default()),
72104
};
73105
assert_eq!(r, VmmAction::InsertPmemDevice(expected_config));
74106
}
107+
108+
#[test]
109+
fn test_parse_patch_pmem_request() {
110+
parse_put_pmem(&Body::new("invalid_payload"), None).unwrap_err();
111+
parse_put_pmem(&Body::new("invalid_payload"), Some("id")).unwrap_err();
112+
113+
let body = r#"{
114+
"id": "bar",
115+
}"#;
116+
parse_put_pmem(&Body::new(body), Some("1")).unwrap_err();
117+
let body = r#"{
118+
"foo": "1",
119+
}"#;
120+
parse_put_pmem(&Body::new(body), Some("1")).unwrap_err();
121+
122+
let body = r#"{
123+
"id": "1000",
124+
"rate_limiter": {}
125+
}"#;
126+
let r = vmm_action_from_request(parse_patch_pmem(&Body::new(body), Some("1000")).unwrap());
127+
128+
let expected_config = PmemDeviceUpdateConfig {
129+
id: "1000".to_string(),
130+
rate_limiter: Some(Default::default()),
131+
};
132+
assert_eq!(r, VmmAction::UpdatePmemDevice(expected_config));
133+
}
75134
}

src/firecracker/swagger/firecracker.yaml

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,35 @@ paths:
370370
description: Internal server error.
371371
schema:
372372
$ref: "#/definitions/Error"
373+
patch:
374+
summary: Updates the rate limiter of a pmem device. Post-boot only.
375+
description:
376+
Updates the rate limiter applied to the pmem device with the ID specified
377+
by the id path parameter.
378+
operationId: patchGuestPmemByID
379+
parameters:
380+
- name: id
381+
in: path
382+
description: The id of the guest pmem device
383+
required: true
384+
type: string
385+
- name: body
386+
in: body
387+
description: Pmem rate limiter properties
388+
required: true
389+
schema:
390+
$ref: "#/definitions/PartialPmem"
391+
responses:
392+
204:
393+
description: Pmem device updated
394+
400:
395+
description: Pmem device cannot be updated due to bad input
396+
schema:
397+
$ref: "#/definitions/Error"
398+
default:
399+
description: Internal server error.
400+
schema:
401+
$ref: "#/definitions/Error"
373402

374403
/logger:
375404
put:
@@ -1255,6 +1284,8 @@ definitions:
12551284
type: boolean
12561285
description:
12571286
Flag to map backing file in read-only mode.
1287+
rate_limiter:
1288+
$ref: "#/definitions/RateLimiter"
12581289

12591290
Error:
12601291
type: object
@@ -1538,6 +1569,19 @@ definitions:
15381569
tx_rate_limiter:
15391570
$ref: "#/definitions/RateLimiter"
15401571

1572+
PartialPmem:
1573+
type: object
1574+
description:
1575+
Defines a partial pmem device structure, used to update the rate limiter
1576+
for that device, after microvm start.
1577+
required:
1578+
- id
1579+
properties:
1580+
id:
1581+
type: string
1582+
rate_limiter:
1583+
$ref: "#/definitions/RateLimiter"
1584+
15411585
RateLimiter:
15421586
type: object
15431587
description:

src/vmm/src/builder.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1311,6 +1311,7 @@ pub(crate) mod tests {
13111311
path_on_host: "".into(),
13121312
root_device: true,
13131313
read_only: true,
1314+
..Default::default()
13141315
}];
13151316
let mut vmm = default_vmm();
13161317
let mut cmdline = default_kernel_cmdline();

src/vmm/src/device_manager/pci_mngr.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -705,6 +705,7 @@ mod tests {
705705
path_on_host: "".into(),
706706
root_device: true,
707707
read_only: true,
708+
..Default::default()
708709
}];
709710
_pmem_files =
710711
insert_pmem_devices(&mut vmm, &mut cmdline, &mut event_manager, pmem_configs);
@@ -812,7 +813,8 @@ mod tests {
812813
"id": "pmem",
813814
"path_on_host": "{}",
814815
"root_device": true,
815-
"read_only": true
816+
"read_only": true,
817+
"rate_limiter": null
816818
}}
817819
],
818820
"memory-hotplug": {{

src/vmm/src/device_manager/persist.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -738,6 +738,7 @@ mod tests {
738738
path_on_host: "".into(),
739739
root_device: true,
740740
read_only: true,
741+
..Default::default()
741742
}];
742743
_pmem_files =
743744
insert_pmem_devices(&mut vmm, &mut cmdline, &mut event_manager, pmem_configs);
@@ -842,7 +843,8 @@ mod tests {
842843
"id": "pmem",
843844
"path_on_host": "{}",
844845
"root_device": true,
845-
"read_only": true
846+
"read_only": true,
847+
"rate_limiter": null
846848
}}
847849
],
848850
"memory-hotplug": {{

src/vmm/src/devices/virtio/pmem/device.rs

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ use crate::devices::virtio::pmem::metrics::{PmemMetrics, PmemMetricsPerDevice};
2222
use crate::devices::virtio::queue::{DescriptorChain, InvalidAvailIdx, Queue, QueueError};
2323
use crate::devices::virtio::transport::{VirtioInterrupt, VirtioInterruptType};
2424
use crate::logger::{IncMetric, error, info};
25+
use crate::rate_limiter::{BucketUpdate, RateLimiter, TokenType};
2526
use crate::utils::{align_up, u64_to_usize};
27+
use crate::vmm_config::RateLimiterConfig;
2628
use crate::vmm_config::pmem::PmemConfig;
2729
use crate::vstate::memory::{ByteValued, Bytes, GuestMemoryMmap, GuestMmapRegion};
2830
use crate::vstate::vm::VmError;
@@ -58,6 +60,8 @@ pub enum PmemError {
5860
Queue(#[from] QueueError),
5961
/// Error during obtaining the descriptor from the queue: {0}
6062
QueuePop(#[from] InvalidAvailIdx),
63+
/// Error creating rate limiter: {0}
64+
RateLimiter(std::io::Error),
6165
}
6266

6367
const VIRTIO_PMEM_REQ_TYPE_FLUSH: u32 = 0;
@@ -94,6 +98,7 @@ pub struct Pmem {
9498
pub file_len: u64,
9599
pub mmap_ptr: u64,
96100
pub metrics: Arc<PmemMetrics>,
101+
pub rate_limiter: RateLimiter,
97102

98103
pub config: PmemConfig,
99104
}
@@ -125,6 +130,13 @@ impl Pmem {
125130
let (file, file_len, mmap_ptr, mmap_len) =
126131
Self::mmap_backing_file(&config.path_on_host, config.read_only)?;
127132

133+
let rate_limiter = config
134+
.rate_limiter
135+
.map(RateLimiterConfig::try_into)
136+
.transpose()
137+
.map_err(PmemError::RateLimiter)?
138+
.unwrap_or_default();
139+
128140
Ok(Self {
129141
avail_features: 1u64 << VIRTIO_F_VERSION_1,
130142
acked_features: 0u64,
@@ -140,6 +152,7 @@ impl Pmem {
140152
file_len,
141153
mmap_ptr,
142154
metrics: PmemMetricsPerDevice::alloc(config.id.clone()),
155+
rate_limiter,
143156
config,
144157
})
145158
}
@@ -254,6 +267,26 @@ impl Pmem {
254267
// This is safe since we checked in the event handler that the device is activated.
255268
let active_state = self.device_state.active_state().unwrap();
256269

270+
if self.queues[0].is_empty() {
271+
return Ok(());
272+
}
273+
274+
// There is only 1 type of request pmem supports, so we can consume
275+
// rate-limiter before even looking at the queue. This is still valid
276+
// even if the queue will not have any valid requests since it indicate
277+
// broken guest driver and rate-limiting should still apply for such case.
278+
// Rate limit: consume 1 op and file_len bytes for the coalesced msync.
279+
// If the rate limiter is blocked, defer notification until the timer fires.
280+
if !self.rate_limiter.consume(1, TokenType::Ops) {
281+
self.metrics.rate_limiter_throttled_events.inc();
282+
return Ok(());
283+
}
284+
if !self.rate_limiter.consume(self.file_len, TokenType::Bytes) {
285+
self.rate_limiter.manual_replenish(1, TokenType::Ops);
286+
self.metrics.rate_limiter_throttled_events.inc();
287+
return Ok(());
288+
}
289+
257290
let mut cached_result = None;
258291
while let Some(head) = self.queues[0].pop()? {
259292
let add_result = match self.process_chain(head, &mut cached_result) {
@@ -270,8 +303,8 @@ impl Pmem {
270303
break;
271304
}
272305
}
273-
self.queues[0].advance_used_ring_idx();
274306

307+
self.queues[0].advance_used_ring_idx();
275308
if self.queues[0].prepare_kick() {
276309
active_state
277310
.interrupt
@@ -347,6 +380,11 @@ impl Pmem {
347380
Ok(())
348381
}
349382

383+
/// Updates the parameters for the rate limiter.
384+
pub fn update_rate_limiter(&mut self, bytes: BucketUpdate, ops: BucketUpdate) {
385+
self.rate_limiter.update_buckets(bytes, ops);
386+
}
387+
350388
pub fn process_queue(&mut self) {
351389
self.metrics.queue_event_count.inc();
352390
if let Err(err) = self.queue_events[0].read() {
@@ -355,6 +393,25 @@ impl Pmem {
355393
return;
356394
}
357395

396+
if self.rate_limiter.is_blocked() {
397+
self.metrics.rate_limiter_throttled_events.inc();
398+
return;
399+
}
400+
401+
self.handle_queue().unwrap_or_else(|err| {
402+
error!("pmem: {err:?}");
403+
self.metrics.event_fails.inc();
404+
});
405+
}
406+
407+
pub fn process_rate_limiter_event(&mut self) {
408+
self.metrics.rate_limiter_event_count.inc();
409+
if let Err(err) = self.rate_limiter.event_handler() {
410+
error!("pmem: Failed to get rate-limiter event: {err:?}");
411+
self.metrics.event_fails.inc();
412+
return;
413+
}
414+
358415
self.handle_queue().unwrap_or_else(|err| {
359416
error!("pmem: {err:?}");
360417
self.metrics.event_fails.inc();
@@ -458,6 +515,7 @@ mod tests {
458515
path_on_host: "not_a_path".into(),
459516
root_device: true,
460517
read_only: false,
518+
..Default::default()
461519
};
462520
assert!(matches!(
463521
Pmem::new(config).unwrap_err(),
@@ -471,6 +529,7 @@ mod tests {
471529
path_on_host: dummy_path.clone(),
472530
root_device: true,
473531
read_only: false,
532+
..Default::default()
474533
};
475534
assert!(matches!(
476535
Pmem::new(config).unwrap_err(),
@@ -483,6 +542,7 @@ mod tests {
483542
path_on_host: dummy_path,
484543
root_device: true,
485544
read_only: false,
545+
..Default::default()
486546
};
487547
Pmem::new(config).unwrap();
488548
}
@@ -497,6 +557,7 @@ mod tests {
497557
path_on_host: dummy_path,
498558
root_device: true,
499559
read_only: false,
560+
..Default::default()
500561
};
501562
let mut pmem = Pmem::new(config).unwrap();
502563

0 commit comments

Comments
 (0)