Skip to content

Commit 4d1b830

Browse files
authored
Merge pull request kubernetes#74933 from yujuhong/fix-cpu-nano-cores
Fix computing of cpu nano core usage
2 parents d314276 + 191666d commit 4d1b830

12 files changed

+172
-37
lines changed

pkg/kubelet/metrics/collectors/volume_stats_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ func TestVolumeStatsCollector(t *testing.T) {
131131

132132
mockStatsProvider := new(statstest.StatsProvider)
133133
mockStatsProvider.On("ListPodStats").Return(podStats, nil)
134+
mockStatsProvider.On("ListPodStatsAndUpdateCPUNanoCoreUsage").Return(podStats, nil)
134135
if err := testutil.CollectAndCompare(&volumeStatsCollector{statsProvider: mockStatsProvider}, strings.NewReader(want), metrics...); err != nil {
135136
t.Errorf("unexpected collecting result:\n%s", err)
136137
}

pkg/kubelet/server/server_test.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -257,8 +257,11 @@ func (fk *fakeKubelet) ListVolumesForPod(podUID types.UID) (map[string]volume.Vo
257257
return map[string]volume.Volume{}, true
258258
}
259259

260-
func (*fakeKubelet) RootFsStats() (*statsapi.FsStats, error) { return nil, nil }
261-
func (*fakeKubelet) ListPodStats() ([]statsapi.PodStats, error) { return nil, nil }
260+
func (*fakeKubelet) RootFsStats() (*statsapi.FsStats, error) { return nil, nil }
261+
func (*fakeKubelet) ListPodStats() ([]statsapi.PodStats, error) { return nil, nil }
262+
func (*fakeKubelet) ListPodStatsAndUpdateCPUNanoCoreUsage() ([]statsapi.PodStats, error) {
263+
return nil, nil
264+
}
262265
func (*fakeKubelet) ListPodCPUAndMemoryStats() ([]statsapi.PodStats, error) { return nil, nil }
263266
func (*fakeKubelet) ImageFsStats() (*statsapi.FsStats, error) { return nil, nil }
264267
func (*fakeKubelet) RlimitStats() (*statsapi.RlimitStats, error) { return nil, nil }

pkg/kubelet/server/stats/handler.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,15 @@ type Provider interface {
4343
//
4444
// ListPodStats returns the stats of all the containers managed by pods.
4545
ListPodStats() ([]statsapi.PodStats, error)
46-
// ListPodCPUAndMemoryStats returns the CPU and memory stats of all the containers managed by pods.
46+
// ListPodStatsAndUpdateCPUNanoCoreUsage updates the cpu nano core usage for
47+
// the containers and returns the stats for all the pod-managed containers.
4748
ListPodCPUAndMemoryStats() ([]statsapi.PodStats, error)
49+
// ListPodStatsAndUpdateCPUNanoCoreUsage returns the stats of all the
50+
// containers managed by pods and force update the cpu usageNanoCores.
51+
// This is a workaround for CRI runtimes that do not integrate with
52+
// cadvisor. See https://github.com/kubernetes/kubernetes/issues/72788
53+
// for more details.
54+
ListPodStatsAndUpdateCPUNanoCoreUsage() ([]statsapi.PodStats, error)
4855
// ImageFsStats returns the stats of the image filesystem.
4956
ImageFsStats() (*statsapi.FsStats, error)
5057

pkg/kubelet/server/stats/summary.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,10 +84,16 @@ func (sp *summaryProviderImpl) Get(updateStats bool) (*statsapi.Summary, error)
8484
if err != nil {
8585
return nil, fmt.Errorf("failed to get imageFs stats: %v", err)
8686
}
87-
podStats, err := sp.provider.ListPodStats()
87+
var podStats []statsapi.PodStats
88+
if updateStats {
89+
podStats, err = sp.provider.ListPodStatsAndUpdateCPUNanoCoreUsage()
90+
} else {
91+
podStats, err = sp.provider.ListPodStats()
92+
}
8893
if err != nil {
8994
return nil, fmt.Errorf("failed to list pod stats: %v", err)
9095
}
96+
9197
rlimit, err := sp.provider.RlimitStats()
9298
if err != nil {
9399
return nil, fmt.Errorf("failed to get rlimit stats: %v", err)

pkg/kubelet/server/stats/summary_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ func TestSummaryProviderGetStats(t *testing.T) {
7474
On("GetNodeConfig").Return(nodeConfig).
7575
On("GetPodCgroupRoot").Return(cgroupRoot).
7676
On("ListPodStats").Return(podStats, nil).
77+
On("ListPodStatsAndUpdateCPUNanoCoreUsage").Return(podStats, nil).
7778
On("ImageFsStats").Return(imageFsStats, nil).
7879
On("RootFsStats").Return(rootFsStats, nil).
7980
On("RlimitStats").Return(rlimitStats, nil).

pkg/kubelet/server/stats/summary_windows_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ func TestSummaryProvider(t *testing.T) {
5757
On("GetNodeConfig").Return(nodeConfig).
5858
On("GetPodCgroupRoot").Return(cgroupRoot).
5959
On("ListPodStats").Return(podStats, nil).
60+
On("ListPodStatsAndUpdateCPUNanoCoreUsage").Return(podStats, nil).
6061
On("ImageFsStats").Return(imageFsStats, nil).
6162
On("RootFsStats").Return(rootFsStats, nil).
6263
On("RlimitStats").Return(nil, nil).

pkg/kubelet/server/stats/testing/mock_stats_provider.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,29 @@ func (_m *StatsProvider) ListPodStats() ([]v1alpha1.PodStats, error) {
275275
return r0, r1
276276
}
277277

278+
// ListPodStatsAndUpdateCPUNanoCoreUsage provides a mock function with given fields:
279+
func (_m *StatsProvider) ListPodStatsAndUpdateCPUNanoCoreUsage() ([]v1alpha1.PodStats, error) {
280+
ret := _m.Called()
281+
282+
var r0 []v1alpha1.PodStats
283+
if rf, ok := ret.Get(0).(func() []v1alpha1.PodStats); ok {
284+
r0 = rf()
285+
} else {
286+
if ret.Get(0) != nil {
287+
r0 = ret.Get(0).([]v1alpha1.PodStats)
288+
}
289+
}
290+
291+
var r1 error
292+
if rf, ok := ret.Get(1).(func() error); ok {
293+
r1 = rf()
294+
} else {
295+
r1 = ret.Error(1)
296+
}
297+
298+
return r0, r1
299+
}
300+
278301
// ListPodCPUAndMemoryStats provides a mock function with given fields:
279302
func (_m *StatsProvider) ListPodCPUAndMemoryStats() ([]v1alpha1.PodStats, error) {
280303
ret := _m.Called()

pkg/kubelet/stats/cadvisor_stats_provider.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,14 @@ func (p *cadvisorStatsProvider) ListPodStats() ([]statsapi.PodStats, error) {
155155
return result, nil
156156
}
157157

158+
// ListPodStatsAndUpdateCPUNanoCoreUsage updates the cpu nano core usage for
159+
// the containers and returns the stats for all the pod-managed containers.
160+
// For cadvisor, cpu nano core usages are pre-computed and cached, so this
161+
// function simply calls ListPodStats.
162+
func (p *cadvisorStatsProvider) ListPodStatsAndUpdateCPUNanoCoreUsage() ([]statsapi.PodStats, error) {
163+
return p.ListPodStats()
164+
}
165+
158166
// ListPodCPUAndMemoryStats returns the cpu and memory stats of all the pod-managed containers.
159167
func (p *cadvisorStatsProvider) ListPodCPUAndMemoryStats() ([]statsapi.PodStats, error) {
160168
infos, err := getCadvisorContainerInfo(p.cadvisor)

pkg/kubelet/stats/cri_stats_provider.go

Lines changed: 86 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,12 @@ var (
4545
defaultCachePeriod = 10 * time.Minute
4646
)
4747

48+
// cpuUsageRecord holds the cpu usage stats and the calculated usageNanoCores.
49+
type cpuUsageRecord struct {
50+
stats *runtimeapi.CpuUsage
51+
usageNanoCores *uint64
52+
}
53+
4854
// criStatsProvider implements the containerStatsProvider interface by getting
4955
// the container stats from CRI.
5056
type criStatsProvider struct {
@@ -63,8 +69,8 @@ type criStatsProvider struct {
6369
logMetricsService LogMetricsService
6470

6571
// cpuUsageCache caches the cpu usage for containers.
66-
cpuUsageCache map[string]*runtimeapi.CpuUsage
67-
mutex sync.Mutex
72+
cpuUsageCache map[string]*cpuUsageRecord
73+
mutex sync.RWMutex
6874
}
6975

7076
// newCRIStatsProvider returns a containerStatsProvider implementation that
@@ -82,12 +88,32 @@ func newCRIStatsProvider(
8288
runtimeService: runtimeService,
8389
imageService: imageService,
8490
logMetricsService: logMetricsService,
85-
cpuUsageCache: make(map[string]*runtimeapi.CpuUsage),
91+
cpuUsageCache: make(map[string]*cpuUsageRecord),
8692
}
8793
}
8894

8995
// ListPodStats returns the stats of all the pod-managed containers.
9096
func (p *criStatsProvider) ListPodStats() ([]statsapi.PodStats, error) {
97+
// Don't update CPU nano core usage.
98+
return p.listPodStats(false)
99+
}
100+
101+
// ListPodStatsAndUpdateCPUNanoCoreUsage updates the cpu nano core usage for
102+
// the containers and returns the stats for all the pod-managed containers.
103+
// This is a workaround because CRI runtimes do not supply nano core usages,
104+
// so this function calculate the difference between the current and the last
105+
// (cached) cpu stats to calculate this metrics. The implementation assumes a
106+
// single caller to periodically invoke this function to update the metrics. If
107+
// there exist multiple callers, the period used to compute the cpu usage may
108+
// vary and the usage could be incoherent (e.g., spiky). If no caller calls
109+
// this function, the cpu usage will stay nil. Right now, eviction manager is
110+
// the only caller, and it calls this function every 10s.
111+
func (p *criStatsProvider) ListPodStatsAndUpdateCPUNanoCoreUsage() ([]statsapi.PodStats, error) {
112+
// Update CPU nano core usage.
113+
return p.listPodStats(true)
114+
}
115+
116+
func (p *criStatsProvider) listPodStats(updateCPUNanoCoreUsage bool) ([]statsapi.PodStats, error) {
91117
// Gets node root filesystem information, which will be used to populate
92118
// the available and capacity bytes/inodes in container stats.
93119
rootFsInfo, err := p.cadvisor.RootFsInfo()
@@ -157,7 +183,7 @@ func (p *criStatsProvider) ListPodStats() ([]statsapi.PodStats, error) {
157183
}
158184

159185
// Fill available stats for full set of required pod stats
160-
cs := p.makeContainerStats(stats, container, &rootFsInfo, fsIDtoInfo, podSandbox.GetMetadata().GetUid())
186+
cs := p.makeContainerStats(stats, container, &rootFsInfo, fsIDtoInfo, podSandbox.GetMetadata().GetUid(), updateCPUNanoCoreUsage)
161187
p.addPodNetworkStats(ps, podSandboxID, caInfos, cs)
162188
p.addPodCPUMemoryStats(ps, types.UID(podSandbox.Metadata.Uid), allInfos, cs)
163189

@@ -435,6 +461,7 @@ func (p *criStatsProvider) makeContainerStats(
435461
rootFsInfo *cadvisorapiv2.FsInfo,
436462
fsIDtoInfo map[runtimeapi.FilesystemIdentifier]*cadvisorapiv2.FsInfo,
437463
uid string,
464+
updateCPUNanoCoreUsage bool,
438465
) *statsapi.ContainerStats {
439466
result := &statsapi.ContainerStats{
440467
Name: stats.Attributes.Metadata.Name,
@@ -450,8 +477,12 @@ func (p *criStatsProvider) makeContainerStats(
450477
if stats.Cpu.UsageCoreNanoSeconds != nil {
451478
result.CPU.UsageCoreNanoSeconds = &stats.Cpu.UsageCoreNanoSeconds.Value
452479
}
453-
454-
usageNanoCores := p.getContainerUsageNanoCores(stats)
480+
var usageNanoCores *uint64
481+
if updateCPUNanoCoreUsage {
482+
usageNanoCores = p.getAndUpdateContainerUsageNanoCores(stats)
483+
} else {
484+
usageNanoCores = p.getContainerUsageNanoCores(stats)
485+
}
455486
if usageNanoCores != nil {
456487
result.CPU.UsageNanoCores = usageNanoCores
457488
}
@@ -541,27 +572,63 @@ func (p *criStatsProvider) makeContainerCPUAndMemoryStats(
541572
return result
542573
}
543574

544-
// getContainerUsageNanoCores gets usageNanoCores based on cached usageCoreNanoSeconds.
575+
// getContainerUsageNanoCores gets the cached usageNanoCores.
545576
func (p *criStatsProvider) getContainerUsageNanoCores(stats *runtimeapi.ContainerStats) *uint64 {
546-
if stats == nil || stats.Cpu == nil || stats.Cpu.UsageCoreNanoSeconds == nil {
577+
if stats == nil || stats.Attributes == nil {
547578
return nil
548579
}
549580

550-
p.mutex.Lock()
551-
defer func() {
552-
// Update cache with new value.
553-
p.cpuUsageCache[stats.Attributes.Id] = stats.Cpu
554-
p.mutex.Unlock()
555-
}()
581+
p.mutex.RLock()
582+
defer p.mutex.RUnlock()
556583

557584
cached, ok := p.cpuUsageCache[stats.Attributes.Id]
558-
if !ok || cached.UsageCoreNanoSeconds == nil {
585+
if !ok || cached.usageNanoCores == nil {
559586
return nil
560587
}
588+
// return a copy of the usage
589+
latestUsage := *cached.usageNanoCores
590+
return &latestUsage
591+
}
561592

562-
nanoSeconds := stats.Cpu.Timestamp - cached.Timestamp
563-
usageNanoCores := (stats.Cpu.UsageCoreNanoSeconds.Value - cached.UsageCoreNanoSeconds.Value) * uint64(time.Second/time.Nanosecond) / uint64(nanoSeconds)
564-
return &usageNanoCores
593+
// getContainerUsageNanoCores computes usageNanoCores based on the given and
594+
// the cached usageCoreNanoSeconds, updates the cache with the computed
595+
// usageNanoCores, and returns the usageNanoCores.
596+
func (p *criStatsProvider) getAndUpdateContainerUsageNanoCores(stats *runtimeapi.ContainerStats) *uint64 {
597+
if stats == nil || stats.Attributes == nil || stats.Cpu == nil || stats.Cpu.UsageCoreNanoSeconds == nil {
598+
return nil
599+
}
600+
id := stats.Attributes.Id
601+
usage, err := func() (*uint64, error) {
602+
p.mutex.Lock()
603+
defer p.mutex.Unlock()
604+
605+
cached, ok := p.cpuUsageCache[id]
606+
if !ok || cached.stats.UsageCoreNanoSeconds == nil {
607+
// Cannot compute the usage now, but update the cached stats anyway
608+
p.cpuUsageCache[id] = &cpuUsageRecord{stats: stats.Cpu, usageNanoCores: nil}
609+
return nil, nil
610+
}
611+
612+
newStats := stats.Cpu
613+
cachedStats := cached.stats
614+
nanoSeconds := newStats.Timestamp - cachedStats.Timestamp
615+
if nanoSeconds <= 0 {
616+
return nil, fmt.Errorf("zero or negative interval (%v - %v)", newStats.Timestamp, cachedStats.Timestamp)
617+
}
618+
usageNanoCores := (newStats.UsageCoreNanoSeconds.Value - cachedStats.UsageCoreNanoSeconds.Value) * uint64(time.Second/time.Nanosecond) / uint64(nanoSeconds)
619+
620+
// Update cache with new value.
621+
usageToUpdate := usageNanoCores
622+
p.cpuUsageCache[id] = &cpuUsageRecord{stats: newStats, usageNanoCores: &usageToUpdate}
623+
624+
return &usageNanoCores, nil
625+
}()
626+
627+
if err != nil {
628+
// This should not happen. Log now to raise visiblity
629+
klog.Errorf("failed updating cpu usage nano core: %v", err)
630+
}
631+
return usage
565632
}
566633

567634
func (p *criStatsProvider) cleanupOutdatedCaches() {
@@ -573,7 +640,7 @@ func (p *criStatsProvider) cleanupOutdatedCaches() {
573640
delete(p.cpuUsageCache, k)
574641
}
575642

576-
if time.Since(time.Unix(0, v.Timestamp)) > defaultCachePeriod {
643+
if time.Since(time.Unix(0, v.stats.Timestamp)) > defaultCachePeriod {
577644
delete(p.cpuUsageCache, k)
578645
}
579646
}

pkg/kubelet/stats/cri_stats_provider_test.go

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -692,17 +692,17 @@ func TestGetContainerUsageNanoCores(t *testing.T) {
692692

693693
tests := []struct {
694694
desc string
695-
cpuUsageCache map[string]*runtimeapi.CpuUsage
695+
cpuUsageCache map[string]*cpuUsageRecord
696696
stats *runtimeapi.ContainerStats
697697
expected *uint64
698698
}{
699699
{
700700
desc: "should return nil if stats is nil",
701-
cpuUsageCache: map[string]*runtimeapi.CpuUsage{},
701+
cpuUsageCache: map[string]*cpuUsageRecord{},
702702
},
703703
{
704704
desc: "should return nil if cpu stats is nil",
705-
cpuUsageCache: map[string]*runtimeapi.CpuUsage{},
705+
cpuUsageCache: map[string]*cpuUsageRecord{},
706706
stats: &runtimeapi.ContainerStats{
707707
Attributes: &runtimeapi.ContainerAttributes{
708708
Id: "1",
@@ -712,7 +712,7 @@ func TestGetContainerUsageNanoCores(t *testing.T) {
712712
},
713713
{
714714
desc: "should return nil if usageCoreNanoSeconds is nil",
715-
cpuUsageCache: map[string]*runtimeapi.CpuUsage{},
715+
cpuUsageCache: map[string]*cpuUsageRecord{},
716716
stats: &runtimeapi.ContainerStats{
717717
Attributes: &runtimeapi.ContainerAttributes{
718718
Id: "1",
@@ -725,7 +725,7 @@ func TestGetContainerUsageNanoCores(t *testing.T) {
725725
},
726726
{
727727
desc: "should return nil if cpu stats is not cached yet",
728-
cpuUsageCache: map[string]*runtimeapi.CpuUsage{},
728+
cpuUsageCache: map[string]*cpuUsageRecord{},
729729
stats: &runtimeapi.ContainerStats{
730730
Attributes: &runtimeapi.ContainerAttributes{
731731
Id: "1",
@@ -751,11 +751,13 @@ func TestGetContainerUsageNanoCores(t *testing.T) {
751751
},
752752
},
753753
},
754-
cpuUsageCache: map[string]*runtimeapi.CpuUsage{
754+
cpuUsageCache: map[string]*cpuUsageRecord{
755755
"1": {
756-
Timestamp: 0,
757-
UsageCoreNanoSeconds: &runtimeapi.UInt64Value{
758-
Value: 10000000000,
756+
stats: &runtimeapi.CpuUsage{
757+
Timestamp: 0,
758+
UsageCoreNanoSeconds: &runtimeapi.UInt64Value{
759+
Value: 10000000000,
760+
},
759761
},
760762
},
761763
},
@@ -774,11 +776,13 @@ func TestGetContainerUsageNanoCores(t *testing.T) {
774776
},
775777
},
776778
},
777-
cpuUsageCache: map[string]*runtimeapi.CpuUsage{
779+
cpuUsageCache: map[string]*cpuUsageRecord{
778780
"1": {
779-
Timestamp: 0,
780-
UsageCoreNanoSeconds: &runtimeapi.UInt64Value{
781-
Value: 10000000000,
781+
stats: &runtimeapi.CpuUsage{
782+
Timestamp: 0,
783+
UsageCoreNanoSeconds: &runtimeapi.UInt64Value{
784+
Value: 10000000000,
785+
},
782786
},
783787
},
784788
},
@@ -788,7 +792,16 @@ func TestGetContainerUsageNanoCores(t *testing.T) {
788792

789793
for _, test := range tests {
790794
provider := &criStatsProvider{cpuUsageCache: test.cpuUsageCache}
791-
real := provider.getContainerUsageNanoCores(test.stats)
795+
// Before the update, the cached value should be nil
796+
cached := provider.getContainerUsageNanoCores(test.stats)
797+
assert.Nil(t, cached)
798+
799+
// Update the cache and get the latest value.
800+
real := provider.getAndUpdateContainerUsageNanoCores(test.stats)
792801
assert.Equal(t, test.expected, real, test.desc)
802+
803+
// After the update, the cached value should be up-to-date
804+
cached = provider.getContainerUsageNanoCores(test.stats)
805+
assert.Equal(t, test.expected, cached, test.desc)
793806
}
794807
}

0 commit comments

Comments
 (0)