github.com/kubewharf/katalyst-core@v0.5.3/pkg/agent/evictionmanager/plugin/memory/numa_pressure.go (about) 1 /* 2 Copyright 2022 The Katalyst Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package memory 18 19 import ( 20 "context" 21 "fmt" 22 "strconv" 23 "time" 24 25 v1 "k8s.io/api/core/v1" 26 "k8s.io/client-go/tools/events" 27 28 pluginapi "github.com/kubewharf/katalyst-api/pkg/protocol/evictionplugin/v1alpha1" 29 "github.com/kubewharf/katalyst-core/pkg/agent/evictionmanager/plugin" 30 "github.com/kubewharf/katalyst-core/pkg/client" 31 "github.com/kubewharf/katalyst-core/pkg/config" 32 "github.com/kubewharf/katalyst-core/pkg/config/agent/dynamic" 33 "github.com/kubewharf/katalyst-core/pkg/consts" 34 "github.com/kubewharf/katalyst-core/pkg/metaserver" 35 "github.com/kubewharf/katalyst-core/pkg/metaserver/agent/metric/helper" 36 "github.com/kubewharf/katalyst-core/pkg/metrics" 37 "github.com/kubewharf/katalyst-core/pkg/util/general" 38 "github.com/kubewharf/katalyst-core/pkg/util/native" 39 "github.com/kubewharf/katalyst-core/pkg/util/process" 40 ) 41 42 const ( 43 EvictionPluginNameNumaMemoryPressure = "numa-memory-pressure-eviction-plugin" 44 EvictionScopeNumaMemory = "NumaMemory" 45 ) 46 47 // NewNumaMemoryPressureEvictionPlugin returns a new MemoryPressureEvictionPlugin 48 func NewNumaMemoryPressureEvictionPlugin(_ *client.GenericClientSet, _ events.EventRecorder, 49 metaServer *metaserver.MetaServer, emitter metrics.MetricEmitter, conf *config.Configuration, 50 ) plugin.EvictionPlugin { 51 return &NumaMemoryPressurePlugin{ 52 pluginName: EvictionPluginNameNumaMemoryPressure, 53 emitter: emitter, 54 StopControl: process.NewStopControl(time.Time{}), 55 metaServer: metaServer, 56 dynamicConfig: conf.DynamicAgentConfiguration, 57 reclaimedPodFilter: conf.CheckReclaimedQoSForPod, 58 numaActionMap: make(map[int]int), 59 numaFreeBelowWatermarkTimesMap: make(map[int]int), 60 evictionHelper: NewEvictionHelper(emitter, metaServer, conf), 61 } 62 } 63 64 // NumaMemoryPressurePlugin implements the EvictionPlugin interface 65 // It triggers pod eviction based on the numa node pressure 66 type NumaMemoryPressurePlugin struct { 67 *process.StopControl 68 69 emitter metrics.MetricEmitter 70 reclaimedPodFilter func(pod *v1.Pod) (bool, error) 71 pluginName string 72 metaServer *metaserver.MetaServer 73 evictionHelper *EvictionHelper 74 75 dynamicConfig *dynamic.DynamicAgentConfiguration 76 77 numaActionMap map[int]int 78 numaFreeBelowWatermarkTimesMap map[int]int 79 isUnderNumaPressure bool 80 } 81 82 func (n *NumaMemoryPressurePlugin) Start() { 83 return 84 } 85 86 func (n *NumaMemoryPressurePlugin) Name() string { 87 if n == nil { 88 return "" 89 } 90 91 return n.pluginName 92 } 93 94 func (n *NumaMemoryPressurePlugin) ThresholdMet(_ context.Context) (*pluginapi.ThresholdMetResponse, error) { 95 resp := &pluginapi.ThresholdMetResponse{ 96 MetType: pluginapi.ThresholdMetType_NOT_MET, 97 } 98 99 if !n.dynamicConfig.GetDynamicConfiguration().EnableNumaLevelEviction { 100 return resp, nil 101 } 102 103 n.detectNumaPressures() 104 if n.isUnderNumaPressure { 105 resp = &pluginapi.ThresholdMetResponse{ 106 MetType: pluginapi.ThresholdMetType_HARD_MET, 107 EvictionScope: EvictionScopeNumaMemory, 108 } 109 } 110 111 return resp, nil 112 } 113 114 func (n *NumaMemoryPressurePlugin) detectNumaPressures() { 115 n.isUnderNumaPressure = false 116 for _, numaID := range n.metaServer.CPUDetails.NUMANodes().ToSliceNoSortInt() { 117 n.numaActionMap[numaID] = actionNoop 118 if _, ok := n.numaFreeBelowWatermarkTimesMap[numaID]; !ok { 119 n.numaFreeBelowWatermarkTimesMap[numaID] = 0 120 } 121 122 if err := n.detectNumaWatermarkPressure(numaID); err != nil { 123 continue 124 } 125 } 126 } 127 128 func (n *NumaMemoryPressurePlugin) detectNumaWatermarkPressure(numaID int) error { 129 free, total, scaleFactor, err := helper.GetWatermarkMetrics(n.metaServer.MetricsFetcher, n.emitter, numaID) 130 if err != nil { 131 general.Errorf("failed to getWatermarkMetrics for numa %d, err: %v", numaID, err) 132 _ = n.emitter.StoreInt64(metricsNameFetchMetricError, 1, metrics.MetricTypeNameCount, 133 metrics.ConvertMapToTags(map[string]string{ 134 metricsTagKeyNumaID: strconv.Itoa(numaID), 135 })...) 136 return err 137 } 138 139 dynamicConfig := n.dynamicConfig.GetDynamicConfiguration() 140 general.Infof("numa watermark metrics of ID: %d, "+ 141 "free: %+v, total: %+v, scaleFactor: %+v, numaFreeBelowWatermarkTimes: %+v, numaFreeBelowWatermarkTimesThreshold: %+v", 142 numaID, free, total, scaleFactor, n.numaFreeBelowWatermarkTimesMap[numaID], 143 dynamicConfig.NumaFreeBelowWatermarkTimesThreshold) 144 _ = n.emitter.StoreFloat64(metricsNameNumaMetric, float64(n.numaFreeBelowWatermarkTimesMap[numaID]), metrics.MetricTypeNameRaw, 145 metrics.ConvertMapToTags(map[string]string{ 146 metricsTagKeyNumaID: strconv.Itoa(numaID), 147 metricsTagKeyMetricName: metricsTagValueNumaFreeBelowWatermarkTimes, 148 })...) 149 150 if free < total*scaleFactor/10000 { 151 n.isUnderNumaPressure = true 152 n.numaActionMap[numaID] = actionReclaimedEviction 153 n.numaFreeBelowWatermarkTimesMap[numaID]++ 154 } else { 155 n.numaFreeBelowWatermarkTimesMap[numaID] = 0 156 } 157 158 if n.numaFreeBelowWatermarkTimesMap[numaID] >= dynamicConfig.NumaFreeBelowWatermarkTimesThreshold { 159 n.numaActionMap[numaID] = actionEviction 160 } 161 162 switch n.numaActionMap[numaID] { 163 case actionReclaimedEviction: 164 _ = n.emitter.StoreInt64(metricsNameThresholdMet, 1, metrics.MetricTypeNameCount, 165 metrics.ConvertMapToTags(map[string]string{ 166 metricsTagKeyEvictionScope: EvictionScopeNumaMemory, 167 metricsTagKeyDetectionLevel: metricsTagValueDetectionLevelNuma, 168 metricsTagKeyNumaID: strconv.Itoa(numaID), 169 metricsTagKeyAction: metricsTagValueActionReclaimedEviction, 170 })...) 171 case actionEviction: 172 _ = n.emitter.StoreInt64(metricsNameThresholdMet, 1, metrics.MetricTypeNameCount, 173 metrics.ConvertMapToTags(map[string]string{ 174 metricsTagKeyEvictionScope: EvictionScopeNumaMemory, 175 metricsTagKeyDetectionLevel: metricsTagValueDetectionLevelNuma, 176 metricsTagKeyNumaID: strconv.Itoa(numaID), 177 metricsTagKeyAction: metricsTagValueActionEviction, 178 })...) 179 } 180 181 return nil 182 } 183 184 func (n *NumaMemoryPressurePlugin) GetTopEvictionPods(_ context.Context, request *pluginapi.GetTopEvictionPodsRequest) (*pluginapi.GetTopEvictionPodsResponse, error) { 185 if request == nil { 186 return nil, fmt.Errorf("GetTopEvictionPods got nil request") 187 } 188 189 if len(request.ActivePods) == 0 { 190 general.Warningf("GetTopEvictionPods got empty active pods list") 191 return &pluginapi.GetTopEvictionPodsResponse{}, nil 192 } 193 194 dynamicConfig := n.dynamicConfig.GetDynamicConfiguration() 195 targetPods := make([]*v1.Pod, 0, len(request.ActivePods)) 196 podToEvictMap := make(map[string]*v1.Pod) 197 198 general.Infof("GetTopEvictionPods condition, isUnderNumaPressure: %+v, n.numaActionMap: %+v", 199 n.isUnderNumaPressure, 200 n.numaActionMap) 201 202 if dynamicConfig.EnableNumaLevelEviction && n.isUnderNumaPressure { 203 for numaID, action := range n.numaActionMap { 204 candidates := n.getCandidates(request.ActivePods, numaID, dynamicConfig.NumaVictimMinimumUtilizationThreshold) 205 n.evictionHelper.selectTopNPodsToEvictByMetrics(candidates, request.TopN, numaID, action, 206 dynamicConfig.NumaEvictionRankingMetrics, podToEvictMap) 207 } 208 } 209 210 for uid := range podToEvictMap { 211 targetPods = append(targetPods, podToEvictMap[uid]) 212 } 213 214 _ = n.emitter.StoreInt64(metricsNameNumberOfTargetPods, int64(len(targetPods)), metrics.MetricTypeNameRaw) 215 general.Infof("[numa-memory-pressure-eviction-plugin] GetTopEvictionPods result, targetPods: %+v", native.GetNamespacedNameListFromSlice(targetPods)) 216 217 resp := &pluginapi.GetTopEvictionPodsResponse{ 218 TargetPods: targetPods, 219 } 220 if gracePeriod := dynamicConfig.MemoryPressureEvictionConfiguration.GracePeriod; gracePeriod > 0 { 221 resp.DeletionOptions = &pluginapi.DeletionOptions{ 222 GracePeriodSeconds: gracePeriod, 223 } 224 } 225 226 return resp, nil 227 } 228 229 // getCandidates returns pods which use memory more than minimumUsageThreshold. 230 func (n *NumaMemoryPressurePlugin) getCandidates(pods []*v1.Pod, numaID int, minimumUsageThreshold float64) []*v1.Pod { 231 result := make([]*v1.Pod, 0, len(pods)) 232 for i := range pods { 233 pod := pods[i] 234 totalMem, totalMemErr := helper.GetNumaMetric(n.metaServer.MetricsFetcher, n.emitter, 235 consts.MetricMemTotalNuma, numaID) 236 usedMem, usedMemErr := helper.GetPodMetric(n.metaServer.MetricsFetcher, n.emitter, pod, 237 consts.MetricsMemTotalPerNumaContainer, numaID) 238 if totalMemErr != nil || usedMemErr != nil { 239 result = append(result, pod) 240 continue 241 } 242 243 usedMemRatio := usedMem / totalMem 244 if usedMemRatio < minimumUsageThreshold { 245 general.Infof("pod %v/%v memory usage on numa %v is %v, which is lower than threshold %v, "+ 246 "ignore it", pod.Namespace, pod.Name, numaID, usedMemRatio, minimumUsageThreshold) 247 continue 248 } 249 250 result = append(result, pod) 251 } 252 253 return result 254 } 255 256 func (n *NumaMemoryPressurePlugin) GetEvictPods(_ context.Context, request *pluginapi.GetEvictPodsRequest) (*pluginapi.GetEvictPodsResponse, error) { 257 if request == nil { 258 return nil, fmt.Errorf("GetEvictPods got nil request") 259 } 260 261 return &pluginapi.GetEvictPodsResponse{}, nil 262 }