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  }