kubelet_perf.go 9.7 KB

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