kubelet_perf.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292
  1. /*
  2. Copyright 2015 The Kubernetes Authors.
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package node
  14. import (
  15. "fmt"
  16. "strings"
  17. "time"
  18. "k8s.io/apimachinery/pkg/util/sets"
  19. "k8s.io/apimachinery/pkg/util/uuid"
  20. clientset "k8s.io/client-go/kubernetes"
  21. kubeletstatsv1alpha1 "k8s.io/kubernetes/pkg/kubelet/apis/stats/v1alpha1"
  22. "k8s.io/kubernetes/test/e2e/framework"
  23. e2ekubelet "k8s.io/kubernetes/test/e2e/framework/kubelet"
  24. e2enode "k8s.io/kubernetes/test/e2e/framework/node"
  25. e2eperf "k8s.io/kubernetes/test/e2e/framework/perf"
  26. e2erc "k8s.io/kubernetes/test/e2e/framework/rc"
  27. "k8s.io/kubernetes/test/e2e/perftype"
  28. testutils "k8s.io/kubernetes/test/utils"
  29. imageutils "k8s.io/kubernetes/test/utils/image"
  30. "github.com/onsi/ginkgo"
  31. )
  32. const (
  33. // Interval to poll /stats/container on a node
  34. containerStatsPollingPeriod = 10 * time.Second
  35. // The monitoring time for one test.
  36. monitoringTime = 20 * time.Minute
  37. // The periodic reporting period.
  38. reportingPeriod = 5 * time.Minute
  39. )
  40. type resourceTest struct {
  41. podsPerNode int
  42. cpuLimits e2ekubelet.ContainersCPUSummary
  43. memLimits e2ekubelet.ResourceUsagePerContainer
  44. }
  45. func logPodsOnNodes(c clientset.Interface, nodeNames []string) {
  46. for _, n := range nodeNames {
  47. podList, err := e2ekubelet.GetKubeletRunningPods(c, n)
  48. if err != nil {
  49. framework.Logf("Unable to retrieve kubelet pods for node %v", n)
  50. continue
  51. }
  52. framework.Logf("%d pods are running on node %v", len(podList.Items), n)
  53. }
  54. }
  55. func runResourceTrackingTest(f *framework.Framework, podsPerNode int, nodeNames sets.String, rm *e2ekubelet.ResourceMonitor,
  56. expectedCPU map[string]map[float64]float64, expectedMemory e2ekubelet.ResourceUsagePerContainer) {
  57. numNodes := nodeNames.Len()
  58. totalPods := podsPerNode * numNodes
  59. ginkgo.By(fmt.Sprintf("Creating a RC of %d pods and wait until all pods of this RC are running", totalPods))
  60. rcName := fmt.Sprintf("resource%d-%s", totalPods, string(uuid.NewUUID()))
  61. // TODO: Use a more realistic workload
  62. err := e2erc.RunRC(testutils.RCConfig{
  63. Client: f.ClientSet,
  64. Name: rcName,
  65. Namespace: f.Namespace.Name,
  66. Image: imageutils.GetPauseImageName(),
  67. Replicas: totalPods,
  68. })
  69. framework.ExpectNoError(err)
  70. // Log once and flush the stats.
  71. rm.LogLatest()
  72. rm.Reset()
  73. ginkgo.By("Start monitoring resource usage")
  74. // Periodically dump the cpu summary until the deadline is met.
  75. // Note that without calling e2ekubelet.ResourceMonitor.Reset(), the stats
  76. // would occupy increasingly more memory. This should be fine
  77. // for the current test duration, but we should reclaim the
  78. // entries if we plan to monitor longer (e.g., 8 hours).
  79. deadline := time.Now().Add(monitoringTime)
  80. for time.Now().Before(deadline) {
  81. timeLeft := deadline.Sub(time.Now())
  82. framework.Logf("Still running...%v left", timeLeft)
  83. if timeLeft < reportingPeriod {
  84. time.Sleep(timeLeft)
  85. } else {
  86. time.Sleep(reportingPeriod)
  87. }
  88. logPodsOnNodes(f.ClientSet, nodeNames.List())
  89. }
  90. ginkgo.By("Reporting overall resource usage")
  91. logPodsOnNodes(f.ClientSet, nodeNames.List())
  92. usageSummary, err := rm.GetLatest()
  93. framework.ExpectNoError(err)
  94. // TODO(random-liu): Remove the original log when we migrate to new perfdash
  95. framework.Logf("%s", rm.FormatResourceUsage(usageSummary))
  96. // Log perf result
  97. printPerfData(e2eperf.ResourceUsageToPerfData(rm.GetMasterNodeLatest(usageSummary)))
  98. verifyMemoryLimits(f.ClientSet, expectedMemory, usageSummary)
  99. cpuSummary := rm.GetCPUSummary()
  100. framework.Logf("%s", rm.FormatCPUSummary(cpuSummary))
  101. // Log perf result
  102. printPerfData(e2eperf.CPUUsageToPerfData(rm.GetMasterNodeCPUSummary(cpuSummary)))
  103. verifyCPULimits(expectedCPU, cpuSummary)
  104. ginkgo.By("Deleting the RC")
  105. e2erc.DeleteRCAndWaitForGC(f.ClientSet, f.Namespace.Name, rcName)
  106. }
  107. func verifyMemoryLimits(c clientset.Interface, expected e2ekubelet.ResourceUsagePerContainer, actual e2ekubelet.ResourceUsagePerNode) {
  108. if expected == nil {
  109. return
  110. }
  111. var errList []string
  112. for nodeName, nodeSummary := range actual {
  113. var nodeErrs []string
  114. for cName, expectedResult := range expected {
  115. container, ok := nodeSummary[cName]
  116. if !ok {
  117. nodeErrs = append(nodeErrs, fmt.Sprintf("container %q: missing", cName))
  118. continue
  119. }
  120. expectedValue := expectedResult.MemoryRSSInBytes
  121. actualValue := container.MemoryRSSInBytes
  122. if expectedValue != 0 && actualValue > expectedValue {
  123. nodeErrs = append(nodeErrs, fmt.Sprintf("container %q: expected RSS memory (MB) < %d; got %d",
  124. cName, expectedValue, actualValue))
  125. }
  126. }
  127. if len(nodeErrs) > 0 {
  128. errList = append(errList, fmt.Sprintf("node %v:\n %s", nodeName, strings.Join(nodeErrs, ", ")))
  129. heapStats, err := e2ekubelet.GetKubeletHeapStats(c, nodeName)
  130. if err != nil {
  131. framework.Logf("Unable to get heap stats from %q", nodeName)
  132. } else {
  133. framework.Logf("Heap stats on %q\n:%v", nodeName, heapStats)
  134. }
  135. }
  136. }
  137. if len(errList) > 0 {
  138. framework.Failf("Memory usage exceeding limits:\n %s", strings.Join(errList, "\n"))
  139. }
  140. }
  141. func verifyCPULimits(expected e2ekubelet.ContainersCPUSummary, actual e2ekubelet.NodesCPUSummary) {
  142. if expected == nil {
  143. return
  144. }
  145. var errList []string
  146. for nodeName, perNodeSummary := range actual {
  147. var nodeErrs []string
  148. for cName, expectedResult := range expected {
  149. perContainerSummary, ok := perNodeSummary[cName]
  150. if !ok {
  151. nodeErrs = append(nodeErrs, fmt.Sprintf("container %q: missing", cName))
  152. continue
  153. }
  154. for p, expectedValue := range expectedResult {
  155. actualValue, ok := perContainerSummary[p]
  156. if !ok {
  157. nodeErrs = append(nodeErrs, fmt.Sprintf("container %q: missing percentile %v", cName, p))
  158. continue
  159. }
  160. if actualValue > expectedValue {
  161. nodeErrs = append(nodeErrs, fmt.Sprintf("container %q: expected %.0fth%% usage < %.3f; got %.3f",
  162. cName, p*100, expectedValue, actualValue))
  163. }
  164. }
  165. }
  166. if len(nodeErrs) > 0 {
  167. errList = append(errList, fmt.Sprintf("node %v:\n %s", nodeName, strings.Join(nodeErrs, ", ")))
  168. }
  169. }
  170. if len(errList) > 0 {
  171. framework.Failf("CPU usage exceeding limits:\n %s", strings.Join(errList, "\n"))
  172. }
  173. }
  174. // Slow by design (1 hour)
  175. var _ = SIGDescribe("Kubelet [Serial] [Slow]", func() {
  176. var nodeNames sets.String
  177. f := framework.NewDefaultFramework("kubelet-perf")
  178. var om *e2ekubelet.RuntimeOperationMonitor
  179. var rm *e2ekubelet.ResourceMonitor
  180. ginkgo.BeforeEach(func() {
  181. nodes, err := e2enode.GetReadySchedulableNodes(f.ClientSet)
  182. framework.ExpectNoError(err)
  183. nodeNames = sets.NewString()
  184. for _, node := range nodes.Items {
  185. nodeNames.Insert(node.Name)
  186. }
  187. om = e2ekubelet.NewRuntimeOperationMonitor(f.ClientSet)
  188. rm = e2ekubelet.NewResourceMonitor(f.ClientSet, e2ekubelet.TargetContainers(), containerStatsPollingPeriod)
  189. rm.Start()
  190. })
  191. ginkgo.AfterEach(func() {
  192. rm.Stop()
  193. result := om.GetLatestRuntimeOperationErrorRate()
  194. framework.Logf("runtime operation error metrics:\n%s", e2ekubelet.FormatRuntimeOperationErrorRate(result))
  195. })
  196. SIGDescribe("regular resource usage tracking [Feature:RegularResourceUsageTracking]", func() {
  197. // We assume that the scheduler will make reasonable scheduling choices
  198. // and assign ~N pods on the node.
  199. // Although we want to track N pods per node, there are N + add-on pods
  200. // in the cluster. The cluster add-on pods can be distributed unevenly
  201. // among the nodes because they are created during the cluster
  202. // initialization. This *noise* is obvious when N is small. We
  203. // deliberately set higher resource usage limits to account for the
  204. // noise.
  205. //
  206. // We set all resource limits generously because this test is mainly
  207. // used to catch resource leaks in the soak cluster. For tracking
  208. // kubelet/runtime resource usage, please see the node e2e benchmark
  209. // dashboard. http://node-perf-dash.k8s.io/
  210. //
  211. // TODO(#36621): Deprecate this test once we have a node e2e soak
  212. // cluster.
  213. rTests := []resourceTest{
  214. {
  215. podsPerNode: 0,
  216. cpuLimits: e2ekubelet.ContainersCPUSummary{
  217. kubeletstatsv1alpha1.SystemContainerKubelet: {0.50: 0.10, 0.95: 0.20},
  218. kubeletstatsv1alpha1.SystemContainerRuntime: {0.50: 0.10, 0.95: 0.20},
  219. },
  220. memLimits: e2ekubelet.ResourceUsagePerContainer{
  221. kubeletstatsv1alpha1.SystemContainerKubelet: &e2ekubelet.ContainerResourceUsage{MemoryRSSInBytes: 200 * 1024 * 1024},
  222. // The detail can be found at https://github.com/kubernetes/kubernetes/issues/28384#issuecomment-244158892
  223. kubeletstatsv1alpha1.SystemContainerRuntime: &e2ekubelet.ContainerResourceUsage{MemoryRSSInBytes: 125 * 1024 * 1024},
  224. },
  225. },
  226. {
  227. cpuLimits: e2ekubelet.ContainersCPUSummary{
  228. kubeletstatsv1alpha1.SystemContainerKubelet: {0.50: 0.35, 0.95: 0.50},
  229. kubeletstatsv1alpha1.SystemContainerRuntime: {0.50: 0.10, 0.95: 0.50},
  230. },
  231. podsPerNode: 100,
  232. memLimits: e2ekubelet.ResourceUsagePerContainer{
  233. kubeletstatsv1alpha1.SystemContainerKubelet: &e2ekubelet.ContainerResourceUsage{MemoryRSSInBytes: 300 * 1024 * 1024},
  234. kubeletstatsv1alpha1.SystemContainerRuntime: &e2ekubelet.ContainerResourceUsage{MemoryRSSInBytes: 350 * 1024 * 1024},
  235. },
  236. },
  237. }
  238. for _, testArg := range rTests {
  239. itArg := testArg
  240. podsPerNode := itArg.podsPerNode
  241. name := fmt.Sprintf(
  242. "resource tracking for %d pods per node", podsPerNode)
  243. ginkgo.It(name, func() {
  244. runResourceTrackingTest(f, podsPerNode, nodeNames, rm, itArg.cpuLimits, itArg.memLimits)
  245. })
  246. }
  247. })
  248. SIGDescribe("experimental resource usage tracking [Feature:ExperimentalResourceUsageTracking]", func() {
  249. density := []int{100}
  250. for i := range density {
  251. podsPerNode := density[i]
  252. name := fmt.Sprintf(
  253. "resource tracking for %d pods per node", podsPerNode)
  254. ginkgo.It(name, func() {
  255. runResourceTrackingTest(f, podsPerNode, nodeNames, rm, nil, nil)
  256. })
  257. }
  258. })
  259. })
  260. // printPerfData prints the perfdata in json format with PerfResultTag prefix.
  261. // If an error occurs, nothing will be printed.
  262. func printPerfData(p *perftype.PerfData) {
  263. // Notice that we must make sure the perftype.PerfResultEnd is in a new line.
  264. if str := framework.PrettyPrintJSON(p); str != "" {
  265. framework.Logf("%s %s\n%s", perftype.PerfResultTag, str, perftype.PerfResultEnd)
  266. }
  267. }