Skip to content

Commit 5cfca2c

Browse files
committed
topology-aware: respect unlimitedBurstable for unlimited memory.
A burstable container with a memory request but no memory limit always ended up in the root pool. The memory burstability check ran before the CPU burstability check and, for a zero limit, preferred the pool with the largest memory capacity. That is always the root, so the unlimitedBurstable setting (and the per-container annotation) never got a say. A pod requesting 1 CPU and 1G of memory with no limits was pinned to the shared cpuset of the whole machine and had its memory spread over every NUMA node. Keep the capacity check for containers that have a memory limit. For unlimited memory, decide after the CPU burstability check: a pool at the configured unlimitedBurstable topology level wins, and only for a tie do we prefer the pool with more memory capacity. Add a unit test for the placement. Change test14-burstable accordingly, since pod3 (memory limit 0) now gets one socket instead of all four NUMA nodes by default.
1 parent 80db7b2 commit 5cfca2c

4 files changed

Lines changed: 101 additions & 7 deletions

File tree

‎cmd/plugins/topology-aware/policy/e2eish-gen-n4c16_test.go‎

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎cmd/plugins/topology-aware/policy/pools.go‎

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1055,13 +1055,14 @@ func (p *policy) compareScores(request Request, pools []Node, scores map[int]Sco
10551055
}
10561056
log.Debugf(" - offer memory types are a tie (%s vs %s)", t1, t2)
10571057

1058-
if req, lim := request.MemAmountToAllocate(), request.MemoryLimit(); req != lim {
1058+
// Unlimited memory (lim == 0) is handled below, after CPU burstability.
1059+
if req, lim := request.MemAmountToAllocate(), request.MemoryLimit(); req != lim && lim != 0 {
10591060
capa1, capa2 := p.poolZoneCapacity(node1, memType), p.poolZoneCapacity(node2, memType)
1060-
if (lim != 0 && capa1 >= lim && capa2 < lim) || (lim == 0 && capa1 > capa2) {
1061+
if capa1 >= lim && capa2 < lim {
10611062
log.Debugf(" - %s loses on memory offer burstability", node2.Name())
10621063
return true
10631064
}
1064-
if (lim != 0 && capa1 < lim && capa2 >= lim) || (lim == 0 && capa2 > capa1) {
1065+
if capa1 < lim && capa2 >= lim {
10651066
log.Debugf(" - %s loses on memory offer burstability", node1.Name())
10661067
return false
10671068
}
@@ -1144,6 +1145,38 @@ func (p *policy) compareScores(request Request, pools []Node, scores map[int]Sco
11441145
}
11451146
}
11461147

1148+
// for unlimited memory, prefer a pool at the unlimited burstable topology
1149+
// level, then the pool with more memory capacity
1150+
if req, lim := request.MemAmountToAllocate(), request.MemoryLimit(); lim == 0 && req != lim && o1 != nil && o2 != nil {
1151+
var (
1152+
ctr = request.GetContainer()
1153+
target = node1.Policy().unlimitedBurstablePreference(ctr)
1154+
level1 = node1.Kind().TopologyLevel()
1155+
level2 = node2.Kind().TopologyLevel()
1156+
memType = request.MemoryType()
1157+
)
1158+
1159+
switch {
1160+
case level1 == target && level2 != target:
1161+
log.Debugf(" - %s WINS on unlimited memory topology level (%s)", node1.Name(), target)
1162+
return true
1163+
case level2 == target && level1 != target:
1164+
log.Debugf(" - %s WINS on unlimited memory topology level (%s)", node2.Name(), target)
1165+
return false
1166+
}
1167+
1168+
capa1, capa2 := p.poolZoneCapacity(node1, memType), p.poolZoneCapacity(node2, memType)
1169+
if capa1 > capa2 {
1170+
log.Debugf(" - %s loses on unlimited memory capacity", node2.Name())
1171+
return true
1172+
}
1173+
if capa2 > capa1 {
1174+
log.Debugf(" - %s loses on unlimited memory capacity", node1.Name())
1175+
return false
1176+
}
1177+
log.Debugf(" - unlimited memory capacity is a TIE")
1178+
}
1179+
11471180
// tighter memory offer wins (skip if both offers are nil)
11481181
if o1 != nil && o2 != nil {
11491182
m1, m2 := o1.NodeMask(), o2.NodeMask()

‎cmd/plugins/topology-aware/policy/pools_test.go‎

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,12 @@ import (
2121
"os"
2222
"path"
2323
"path/filepath"
24+
"strings"
2425
"sync"
2526
"testing"
2627

28+
corev1 "k8s.io/api/core/v1"
29+
2730
cfgapi "github.com/containers/nri-plugins/pkg/apis/config/v1alpha1/resmgr/policy/topologyaware"
2831
policyapi "github.com/containers/nri-plugins/pkg/resmgr/policy"
2932

@@ -619,3 +622,61 @@ func TestAffinities(t *testing.T) {
619622
})
620623
}
621624
}
625+
626+
// Test that a burstable container without memory and CPU limits is placed
627+
// at the unlimitedBurstable topology level, instead of the largest pool.
628+
func TestUnlimitedBurstablePlacement(t *testing.T) {
629+
dir := testSysfs(t)
630+
631+
tcases := []struct {
632+
name string
633+
level cfgapi.CPUTopologyLevel
634+
expected string
635+
}{
636+
{name: "package level", level: cfgapi.CPUTopologyLevelPackage, expected: "socket #"},
637+
{name: "system level", level: cfgapi.CPUTopologyLevelSystem, expected: "root"},
638+
}
639+
640+
for _, tc := range tcases {
641+
t.Run(tc.name, func(t *testing.T) {
642+
sys, err := system.DiscoverSystemAt(path.Join(dir, "sysfs", "4-socket-server-nosnc", "sys"))
643+
if err != nil {
644+
panic(err)
645+
}
646+
647+
policy := New().(*policy)
648+
err = policy.Setup(&policyapi.BackendOptions{
649+
Cache: &mockCache{},
650+
System: sys,
651+
Config: &cfgapi.Config{
652+
ReservedResources: cfgapi.Constraints{cfgapi.CPU: "750m"},
653+
UnlimitedBurstable: tc.level,
654+
},
655+
})
656+
if err != nil {
657+
log.Warnf("failed to setup test policy: %v", err)
658+
}
659+
660+
req := &request{
661+
memReq: 1 << 30,
662+
memLim: 0,
663+
memType: memoryDRAM,
664+
fraction: 1000,
665+
limit: unlimitedCPU,
666+
container: &mockContainer{
667+
returnValueForQOSClass: corev1.PodQOSBurstable,
668+
},
669+
}
670+
671+
log.EnableDebug(true)
672+
_, pools := policy.sortPoolsByScore(req, map[int]int32{})
673+
log.EnableDebug(false)
674+
if len(pools) < 1 {
675+
t.Fatalf("pool scoring failed to find any pools")
676+
}
677+
if name := pools[0].Name(); !strings.HasPrefix(name, tc.expected) {
678+
t.Errorf("expected best pool %q*, got %s", tc.expected, name)
679+
}
680+
})
681+
}
682+
}

‎test/e2e/policies.test-suite/topology-aware/n4c16/test14-burstable/code.var.sh‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,14 @@ CPUREQ=1 CPULIM=2 MEMREQ=100M MEMLIM=2.5G create burstable
1515
CPUREQ=1 CPULIM=2 MEMREQ=200M MEMLIM=0 create burstable
1616

1717
# pod0 and pod2 have limits that require 2 NUMA nodes.
18-
# pod1 requires all 4 NUMA nodes. pod3 is unlimited, so
19-
# it should also get all NUMA nodes.
18+
# pod1 requires all 4 NUMA nodes. pod3 has no memory limit, so
19+
# it is placed at the unlimitedBurstable level, by default a socket.
2020
report allowed
2121
verify \
2222
'len(nodes["pod0c0"]) == 2' \
2323
'len(nodes["pod1c0"]) == 4' \
2424
'len(nodes["pod2c0"]) == 2' \
25-
'len(nodes["pod3c0"]) == 4'
25+
'len(nodes["pod3c0"]) == 2'
2626

2727
vm-command "kubectl delete pods --all --now"
2828

0 commit comments

Comments
 (0)