Skip to content

Commit e1b7f6a

Browse files
kailun-qinAkihiroSudaZheaoli
committed
libct/cg: add CFS bandwidth burst for CPU
Burstable CFS controller is introduced in Linux 5.14. This helps with parallel workloads that might be bursty. They can get throttled even when their average utilization is under quota. And they may be latency sensitive at the same time so that throttling them is undesired. This feature borrows time now against the future underrun, at the cost of increased interference against the other system users, by introducing cfs_burst_us into CFS bandwidth control to enact the cap on unused bandwidth accumulation, which will then used additionally for burst. The patch adds the support/control for CFS bandwidth burst. runtime-spec: opencontainers/runtime-spec#1120 Co-authored-by: Akihiro Suda <[email protected]> Co-authored-by: Nadeshiko Manju <[email protected]> Signed-off-by: Kailun Qin <[email protected]>
1 parent 0a5cd69 commit e1b7f6a

File tree

12 files changed

+105
-4
lines changed

12 files changed

+105
-4
lines changed

contrib/completions/bash/runc

+1
Original file line numberDiff line numberDiff line change
@@ -735,6 +735,7 @@ _runc_update() {
735735
--blkio-weight
736736
--cpu-period
737737
--cpu-quota
738+
--cpu-burst
738739
--cpu-rt-period
739740
--cpu-rt-runtime
740741
--cpu-share

docs/spec-conformance.md

-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ Spec version | Feature | PR
1010
v1.0.0 | `SCMP_ARCH_PARISC` | Unplanned, due to lack of users
1111
v1.0.0 | `SCMP_ARCH_PARISC64` | Unplanned, due to lack of users
1212
v1.0.2 | `.linux.personality` | [#3126](https://github.com/opencontainers/runc/pull/3126)
13-
v1.1.0 | `.linux.resources.cpu.burst` | [#3749](https://github.com/opencontainers/runc/pull/3749)
1413
v1.1.0 | `SECCOMP_FILTER_FLAG_WAIT_KILLABLE_RECV` | [#3862](https://github.com/opencontainers/runc/pull/3862)
1514
v1.1.0 | time namespaces | [#3876](https://github.com/opencontainers/runc/pull/3876)
1615
v1.1.0 | rsvd hugetlb cgroup | TODO ([#3859](https://github.com/opencontainers/runc/issues/3859))

libcontainer/cgroups/fs/cpu.go

+29
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,28 @@ func (s *CpuGroup) Set(path string, r *configs.Resources) error {
8484
period = ""
8585
}
8686
}
87+
88+
var burst string
89+
if r.CpuBurst != 0 {
90+
burst = strconv.FormatUint(r.CpuBurst, 10)
91+
if err := cgroups.WriteFile(path, "cpu.cfs_burst_us", burst); err != nil {
92+
// this is a special trick for burst feature, the current systemd and low version of kernel will not support it.
93+
// So, an `no such file or directory` error would be raised, and we can ignore it .
94+
if !errors.Is(err, unix.ENOENT) {
95+
// Sometimes when the burst to be set is larger
96+
// than the current one, it is rejected by the kernel
97+
// (EINVAL) as old_quota/new_burst exceeds the parent
98+
// cgroup quota limit. If this happens and the quota is
99+
// going to be set, ignore the error for now and retry
100+
// after setting the quota.
101+
if !errors.Is(err, unix.EINVAL) || r.CpuQuota == 0 {
102+
return err
103+
}
104+
}
105+
} else {
106+
burst = ""
107+
}
108+
}
87109
if r.CpuQuota != 0 {
88110
if err := cgroups.WriteFile(path, "cpu.cfs_quota_us", strconv.FormatInt(r.CpuQuota, 10)); err != nil {
89111
return err
@@ -93,6 +115,13 @@ func (s *CpuGroup) Set(path string, r *configs.Resources) error {
93115
return err
94116
}
95117
}
118+
if burst != "" {
119+
if err := cgroups.WriteFile(path, "cpu.cfs_burst_us", burst); err != nil {
120+
if !errors.Is(err, unix.ENOENT) {
121+
return err
122+
}
123+
}
124+
}
96125
}
97126

98127
if r.CPUIdle != nil {

libcontainer/cgroups/fs/cpu_test.go

+12
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ func TestCpuSetBandWidth(t *testing.T) {
4545
const (
4646
quotaBefore = 8000
4747
quotaAfter = 5000
48+
burstBefore = 2000
49+
burstAfter = 1000
4850
periodBefore = 10000
4951
periodAfter = 7000
5052
rtRuntimeBefore = 8000
@@ -55,13 +57,15 @@ func TestCpuSetBandWidth(t *testing.T) {
5557

5658
writeFileContents(t, path, map[string]string{
5759
"cpu.cfs_quota_us": strconv.Itoa(quotaBefore),
60+
"cpu.cfs_burst_us": strconv.Itoa(burstBefore),
5861
"cpu.cfs_period_us": strconv.Itoa(periodBefore),
5962
"cpu.rt_runtime_us": strconv.Itoa(rtRuntimeBefore),
6063
"cpu.rt_period_us": strconv.Itoa(rtPeriodBefore),
6164
})
6265

6366
r := &configs.Resources{
6467
CpuQuota: quotaAfter,
68+
CpuBurst: burstAfter,
6569
CpuPeriod: periodAfter,
6670
CpuRtRuntime: rtRuntimeAfter,
6771
CpuRtPeriod: rtPeriodAfter,
@@ -79,6 +83,14 @@ func TestCpuSetBandWidth(t *testing.T) {
7983
t.Fatal("Got the wrong value, set cpu.cfs_quota_us failed.")
8084
}
8185

86+
burst, err := fscommon.GetCgroupParamUint(path, "cpu.cfs_burst_us")
87+
if err != nil {
88+
t.Fatal(err)
89+
}
90+
if burst != burstAfter {
91+
t.Fatal("Got the wrong value, set cpu.cfs_burst_us failed.")
92+
}
93+
8294
period, err := fscommon.GetCgroupParamUint(path, "cpu.cfs_period_us")
8395
if err != nil {
8496
t.Fatal(err)

libcontainer/cgroups/fs/fs.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ func (m *Manager) Set(r *configs.Resources) error {
191191
if path == "" {
192192
// We never created a path for this cgroup, so we cannot set
193193
// limits for it (though we have already tried at this point).
194-
return fmt.Errorf("cannot set %s limit: container could not join or create cgroup", sys.Name())
194+
return fmt.Errorf("cannot set %s limit: container could not join or create cgroup, and the error is %w", sys.Name(), err)
195195
}
196196
return err
197197
}

libcontainer/cgroups/fs2/cpu.go

+26-1
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,19 @@ package fs2
22

33
import (
44
"bufio"
5+
"errors"
56
"os"
67
"strconv"
78

9+
"golang.org/x/sys/unix"
10+
811
"github.com/opencontainers/runc/libcontainer/cgroups"
912
"github.com/opencontainers/runc/libcontainer/cgroups/fscommon"
1013
"github.com/opencontainers/runc/libcontainer/configs"
1114
)
1215

1316
func isCpuSet(r *configs.Resources) bool {
14-
return r.CpuWeight != 0 || r.CpuQuota != 0 || r.CpuPeriod != 0 || r.CPUIdle != nil
17+
return r.CpuWeight != 0 || r.CpuQuota != 0 || r.CpuPeriod != 0 || r.CPUIdle != nil || r.CpuBurst != 0
1518
}
1619

1720
func setCpu(dirPath string, r *configs.Resources) error {
@@ -32,6 +35,23 @@ func setCpu(dirPath string, r *configs.Resources) error {
3235
}
3336
}
3437

38+
var burst string
39+
if r.CpuBurst != 0 {
40+
burst = strconv.FormatUint(r.CpuBurst, 10)
41+
if err := cgroups.WriteFile(dirPath, "cpu.max.burst", burst); err != nil {
42+
// Sometimes when the burst to be set is larger
43+
// than the current one, it is rejected by the kernel
44+
// (EINVAL) as old_quota/new_burst exceeds the parent
45+
// cgroup quota limit. If this happens and the quota is
46+
// going to be set, ignore the error for now and retry
47+
// after setting the quota.
48+
if !errors.Is(err, unix.EINVAL) || r.CpuQuota == 0 {
49+
return err
50+
}
51+
} else {
52+
burst = ""
53+
}
54+
}
3555
if r.CpuQuota != 0 || r.CpuPeriod != 0 {
3656
str := "max"
3757
if r.CpuQuota > 0 {
@@ -47,6 +67,11 @@ func setCpu(dirPath string, r *configs.Resources) error {
4767
if err := cgroups.WriteFile(dirPath, "cpu.max", str); err != nil {
4868
return err
4969
}
70+
if burst != "" {
71+
if err := cgroups.WriteFile(dirPath, "cpu.max.burst", burst); err != nil {
72+
return err
73+
}
74+
}
5075
}
5176

5277
return nil

libcontainer/configs/cgroup_linux.go

+3
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@ type Resources struct {
6969
// CPU hardcap limit (in usecs). Allowed cpu time in a given period.
7070
CpuQuota int64 `json:"cpu_quota"`
7171

72+
// CPU hardcap burst limit (in usecs). Allowed accumulated cpu time additionally for burst in a given period.
73+
CpuBurst uint64 `json:"cpu_burst"` //nolint:revive
74+
7275
// CPU period to be used for hardcapping (in usecs). 0 to use system default.
7376
CpuPeriod uint64 `json:"cpu_period"`
7477

libcontainer/specconv/spec_linux.go

+3
Original file line numberDiff line numberDiff line change
@@ -755,6 +755,9 @@ func CreateCgroupConfig(opts *CreateOpts, defaultDevs []*devices.Device) (*confi
755755
if r.CPU.Quota != nil {
756756
c.Resources.CpuQuota = *r.CPU.Quota
757757
}
758+
if r.CPU.Burst != nil {
759+
c.Resources.CpuBurst = *r.CPU.Burst
760+
}
758761
if r.CPU.Period != nil {
759762
c.Resources.CpuPeriod = *r.CPU.Period
760763
}

man/runc-update.8.md

+4
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ In case **-r** is used, the JSON format is like this:
2828
"cpu": {
2929
"shares": 0,
3030
"quota": 0,
31+
"burst": 0,
3132
"period": 0,
3233
"realtimeRuntime": 0,
3334
"realtimePeriod": 0,
@@ -53,6 +54,9 @@ stdin. If this option is used, all other options are ignored.
5354
**--cpu-quota** _num_
5455
: Set CPU usage limit within a given period (in microseconds).
5556

57+
**--cpu-burst** _num_
58+
: Set CPU burst limit within a given period (in microseconds).
59+
5660
**--cpu-rt-period** _num_
5761
: Set CPU realtime period to be used for hardcapping (in microseconds).
5862

tests/integration/helpers.bash

+15-1
Original file line numberDiff line numberDiff line change
@@ -260,11 +260,16 @@ function get_cgroup_value() {
260260
# Helper to check a if value in a cgroup file matches the expected one.
261261
function check_cgroup_value() {
262262
local current
263+
local cgroup
264+
cgroup="$(get_cgroup_path "$1")"
265+
if [ ! -f "$cgroup/$1" ]; then
266+
skip "$cgroup/$1 does not exist"
267+
fi
263268
current="$(get_cgroup_value "$1")"
264269
local expected=$2
265270

266271
echo "current $current !? $expected"
267-
[ "$current" = "$expected" ]
272+
[ "$current" = "$expected" ] || [ "$current" = "$((expected / 1000))" ]
268273
}
269274

270275
# Helper to check a value in systemd.
@@ -310,6 +315,15 @@ function check_cpu_quota() {
310315
check_systemd_value "CPUQuotaPeriodUSec" $sd_period $sd_infinity
311316
}
312317

318+
function check_cpu_burst() {
319+
local burst=$1
320+
if [ -v CGROUP_V2 ]; then
321+
check_cgroup_value "cpu.max.burst" "$burst"
322+
else
323+
check_cgroup_value "cpu.cfs_burst_us" "$burst"
324+
fi
325+
}
326+
313327
# Works for cgroup v1 and v2, accepts v1 shares as an argument.
314328
function check_cpu_shares() {
315329
local shares=$1

tests/integration/update.bats

+3
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,9 @@ EOF
288288
runc update test_update --cpu-share 200
289289
[ "$status" -eq 0 ]
290290
check_cpu_shares 200
291+
runc update test_update --cpu-period 900000 --cpu-burst 500000
292+
[ "$status" -eq 0 ]
293+
check_cpu_burst 500000
291294

292295
# Revert to the test initial value via json on stding
293296
runc update -r - test_update <<EOF

update.go

+8
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ The accepted format is as follow (unchanged values can be omitted):
4444
"cpu": {
4545
"shares": 0,
4646
"quota": 0,
47+
"burst": 0,
4748
"period": 0,
4849
"realtimeRuntime": 0,
4950
"realtimePeriod": 0,
@@ -73,6 +74,10 @@ other options are ignored.
7374
Name: "cpu-quota",
7475
Usage: "CPU CFS hardcap limit (in usecs). Allowed cpu time in a given period",
7576
},
77+
cli.StringFlag{
78+
Name: "cpu-burst",
79+
Usage: "CPU CFS hardcap burst limit (in usecs). Allowed accumulated cpu time additionally for burst a given period",
80+
},
7681
cli.StringFlag{
7782
Name: "cpu-share",
7883
Usage: "CPU shares (relative weight vs. other containers)",
@@ -153,6 +158,7 @@ other options are ignored.
153158
CPU: &specs.LinuxCPU{
154159
Shares: u64Ptr(0),
155160
Quota: i64Ptr(0),
161+
Burst: u64Ptr(0),
156162
Period: u64Ptr(0),
157163
RealtimeRuntime: i64Ptr(0),
158164
RealtimePeriod: u64Ptr(0),
@@ -209,6 +215,7 @@ other options are ignored.
209215
opt string
210216
dest *uint64
211217
}{
218+
{"cpu-burst", r.CPU.Burst},
212219
{"cpu-period", r.CPU.Period},
213220
{"cpu-rt-period", r.CPU.RealtimePeriod},
214221
{"cpu-share", r.CPU.Shares},
@@ -298,6 +305,7 @@ other options are ignored.
298305
}
299306
}
300307

308+
config.Cgroups.Resources.CpuBurst = *r.CPU.Burst
301309
config.Cgroups.Resources.CpuShares = *r.CPU.Shares
302310
// CpuWeight is used for cgroupv2 and should be converted
303311
config.Cgroups.Resources.CpuWeight = cgroups.ConvertCPUSharesToCgroupV2Value(*r.CPU.Shares)

0 commit comments

Comments
 (0)