k8s.io/kubernetes@v1.29.3/test/integration/apiserver/flowcontrol/concurrency_test.go (about)

     1  /*
     2  Copyright 2019 The Kubernetes 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 flowcontrol
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"io"
    23  	"strings"
    24  	"sync"
    25  	"testing"
    26  	"time"
    27  
    28  	"github.com/prometheus/common/expfmt"
    29  	"github.com/prometheus/common/model"
    30  
    31  	flowcontrol "k8s.io/api/flowcontrol/v1"
    32  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    33  	"k8s.io/apimachinery/pkg/util/wait"
    34  	clientset "k8s.io/client-go/kubernetes"
    35  	"k8s.io/client-go/rest"
    36  	"k8s.io/kubernetes/cmd/kube-apiserver/app/options"
    37  	"k8s.io/kubernetes/test/integration/framework"
    38  	"k8s.io/kubernetes/test/utils/ktesting"
    39  	"k8s.io/utils/ptr"
    40  )
    41  
    42  const (
    43  	nominalConcurrencyMetricsName     = "apiserver_flowcontrol_nominal_limit_seats"
    44  	dispatchedRequestCountMetricsName = "apiserver_flowcontrol_dispatched_requests_total"
    45  	rejectedRequestCountMetricsName   = "apiserver_flowcontrol_rejected_requests_total"
    46  	labelPriorityLevel                = "priority_level"
    47  	timeout                           = time.Second * 10
    48  )
    49  
    50  func setup(t testing.TB, maxReadonlyRequestsInFlight, maxMutatingRequestsInFlight int) (context.Context, *rest.Config, framework.TearDownFunc) {
    51  	_, ctx := ktesting.NewTestContext(t)
    52  	ctx, cancel := context.WithCancel(ctx)
    53  
    54  	_, kubeConfig, tearDownFn := framework.StartTestServer(ctx, t, framework.TestServerSetup{
    55  		ModifyServerRunOptions: func(opts *options.ServerRunOptions) {
    56  			// Ensure all clients are allowed to send requests.
    57  			opts.Authorization.Modes = []string{"AlwaysAllow"}
    58  			opts.GenericServerRunOptions.MaxRequestsInFlight = maxReadonlyRequestsInFlight
    59  			opts.GenericServerRunOptions.MaxMutatingRequestsInFlight = maxMutatingRequestsInFlight
    60  		},
    61  	})
    62  
    63  	newTeardown := func() {
    64  		cancel()
    65  		tearDownFn()
    66  	}
    67  	return ctx, kubeConfig, newTeardown
    68  }
    69  
    70  func TestPriorityLevelIsolation(t *testing.T) {
    71  	ctx, kubeConfig, closeFn := setup(t, 1, 1)
    72  	defer closeFn()
    73  
    74  	loopbackClient := clientset.NewForConfigOrDie(kubeConfig)
    75  	noxu1Client := getClientFor(kubeConfig, "noxu1")
    76  	noxu2Client := getClientFor(kubeConfig, "noxu2")
    77  
    78  	queueLength := 50
    79  	concurrencyShares := 1
    80  
    81  	priorityLevelNoxu1, _, err := createPriorityLevelAndBindingFlowSchemaForUser(
    82  		loopbackClient, "noxu1", concurrencyShares, queueLength)
    83  	if err != nil {
    84  		t.Error(err)
    85  	}
    86  	priorityLevelNoxu2, _, err := createPriorityLevelAndBindingFlowSchemaForUser(
    87  		loopbackClient, "noxu2", concurrencyShares, queueLength)
    88  	if err != nil {
    89  		t.Error(err)
    90  	}
    91  
    92  	nominalConcurrency, err := getNominalConcurrencyOfPriorityLevel(loopbackClient)
    93  	if err != nil {
    94  		t.Error(err)
    95  	}
    96  
    97  	if 1 != nominalConcurrency[priorityLevelNoxu1.Name] {
    98  		t.Errorf("unexpected shared concurrency %v instead of %v", nominalConcurrency[priorityLevelNoxu1.Name], 1)
    99  	}
   100  	if 1 != nominalConcurrency[priorityLevelNoxu2.Name] {
   101  		t.Errorf("unexpected shared concurrency %v instead of %v", nominalConcurrency[priorityLevelNoxu2.Name], 1)
   102  	}
   103  
   104  	stopCh := make(chan struct{})
   105  	wg := sync.WaitGroup{}
   106  	defer func() {
   107  		close(stopCh)
   108  		wg.Wait()
   109  	}()
   110  
   111  	// "elephant"
   112  	wg.Add(concurrencyShares + queueLength)
   113  	streamRequests(concurrencyShares+queueLength, func() {
   114  		_, err := noxu1Client.CoreV1().Namespaces().List(ctx, metav1.ListOptions{})
   115  		if err != nil {
   116  			t.Error(err)
   117  		}
   118  	}, &wg, stopCh)
   119  	// "mouse"
   120  	wg.Add(3)
   121  	streamRequests(3, func() {
   122  		_, err := noxu2Client.CoreV1().Namespaces().List(ctx, metav1.ListOptions{})
   123  		if err != nil {
   124  			t.Error(err)
   125  		}
   126  	}, &wg, stopCh)
   127  
   128  	time.Sleep(time.Second * 10) // running in background for a while
   129  
   130  	allDispatchedReqCounts, rejectedReqCounts, err := getRequestCountOfPriorityLevel(loopbackClient)
   131  	if err != nil {
   132  		t.Error(err)
   133  	}
   134  
   135  	noxu1RequestCount := allDispatchedReqCounts[priorityLevelNoxu1.Name]
   136  	noxu2RequestCount := allDispatchedReqCounts[priorityLevelNoxu2.Name]
   137  
   138  	if rejectedReqCounts[priorityLevelNoxu1.Name] > 0 {
   139  		t.Errorf(`%v requests from the "elephant" stream were rejected unexpectedly`, rejectedReqCounts[priorityLevelNoxu2.Name])
   140  	}
   141  	if rejectedReqCounts[priorityLevelNoxu2.Name] > 0 {
   142  		t.Errorf(`%v requests from the "mouse" stream were rejected unexpectedly`, rejectedReqCounts[priorityLevelNoxu2.Name])
   143  	}
   144  
   145  	// Theoretically, the actual expected value of request counts upon the two priority-level should be
   146  	// the equal. We're deliberately lax to make flakes super rare.
   147  	if (noxu1RequestCount/2) > noxu2RequestCount || (noxu2RequestCount/2) > noxu1RequestCount {
   148  		t.Errorf("imbalanced requests made by noxu1/2: (%d:%d)", noxu1RequestCount, noxu2RequestCount)
   149  	}
   150  }
   151  
   152  func getClientFor(loopbackConfig *rest.Config, username string) clientset.Interface {
   153  	config := rest.CopyConfig(loopbackConfig)
   154  	config.Impersonate = rest.ImpersonationConfig{
   155  		UserName: username,
   156  	}
   157  	return clientset.NewForConfigOrDie(config)
   158  }
   159  
   160  func getMetrics(c clientset.Interface) (string, error) {
   161  	resp, err := c.CoreV1().
   162  		RESTClient().
   163  		Get().
   164  		RequestURI("/metrics").
   165  		DoRaw(context.Background())
   166  	if err != nil {
   167  		return "", err
   168  	}
   169  	return string(resp), err
   170  }
   171  
   172  func getNominalConcurrencyOfPriorityLevel(c clientset.Interface) (map[string]int, error) {
   173  	resp, err := getMetrics(c)
   174  	if err != nil {
   175  		return nil, err
   176  	}
   177  
   178  	dec := expfmt.NewDecoder(strings.NewReader(string(resp)), expfmt.FmtText)
   179  	decoder := expfmt.SampleDecoder{
   180  		Dec:  dec,
   181  		Opts: &expfmt.DecodeOptions{},
   182  	}
   183  
   184  	concurrency := make(map[string]int)
   185  	for {
   186  		var v model.Vector
   187  		if err := decoder.Decode(&v); err != nil {
   188  			if err == io.EOF {
   189  				// Expected loop termination condition.
   190  				return concurrency, nil
   191  			}
   192  			return nil, fmt.Errorf("failed decoding metrics: %v", err)
   193  		}
   194  		for _, metric := range v {
   195  			switch name := string(metric.Metric[model.MetricNameLabel]); name {
   196  			case nominalConcurrencyMetricsName:
   197  				concurrency[string(metric.Metric[labelPriorityLevel])] = int(metric.Value)
   198  			}
   199  		}
   200  	}
   201  }
   202  
   203  func getRequestCountOfPriorityLevel(c clientset.Interface) (map[string]int, map[string]int, error) {
   204  	resp, err := getMetrics(c)
   205  	if err != nil {
   206  		return nil, nil, err
   207  	}
   208  
   209  	dec := expfmt.NewDecoder(strings.NewReader(string(resp)), expfmt.FmtText)
   210  	decoder := expfmt.SampleDecoder{
   211  		Dec:  dec,
   212  		Opts: &expfmt.DecodeOptions{},
   213  	}
   214  
   215  	allReqCounts := make(map[string]int)
   216  	rejectReqCounts := make(map[string]int)
   217  	for {
   218  		var v model.Vector
   219  		if err := decoder.Decode(&v); err != nil {
   220  			if err == io.EOF {
   221  				// Expected loop termination condition.
   222  				return allReqCounts, rejectReqCounts, nil
   223  			}
   224  			return nil, nil, fmt.Errorf("failed decoding metrics: %v", err)
   225  		}
   226  		for _, metric := range v {
   227  			switch name := string(metric.Metric[model.MetricNameLabel]); name {
   228  			case dispatchedRequestCountMetricsName:
   229  				allReqCounts[string(metric.Metric[labelPriorityLevel])] = int(metric.Value)
   230  			case rejectedRequestCountMetricsName:
   231  				rejectReqCounts[string(metric.Metric[labelPriorityLevel])] = int(metric.Value)
   232  			}
   233  		}
   234  	}
   235  }
   236  
   237  func createPriorityLevelAndBindingFlowSchemaForUser(c clientset.Interface, username string, concurrencyShares, queuelength int) (*flowcontrol.PriorityLevelConfiguration, *flowcontrol.FlowSchema, error) {
   238  	i0 := int32(0)
   239  	pl, err := c.FlowcontrolV1().PriorityLevelConfigurations().Create(context.Background(), &flowcontrol.PriorityLevelConfiguration{
   240  		ObjectMeta: metav1.ObjectMeta{
   241  			Name: username,
   242  		},
   243  		Spec: flowcontrol.PriorityLevelConfigurationSpec{
   244  			Type: flowcontrol.PriorityLevelEnablementLimited,
   245  			Limited: &flowcontrol.LimitedPriorityLevelConfiguration{
   246  				NominalConcurrencyShares: ptr.To(int32(concurrencyShares)),
   247  				BorrowingLimitPercent:    &i0,
   248  				LimitResponse: flowcontrol.LimitResponse{
   249  					Type: flowcontrol.LimitResponseTypeQueue,
   250  					Queuing: &flowcontrol.QueuingConfiguration{
   251  						Queues:           100,
   252  						HandSize:         1,
   253  						QueueLengthLimit: int32(queuelength),
   254  					},
   255  				},
   256  			},
   257  		},
   258  	}, metav1.CreateOptions{})
   259  	if err != nil {
   260  		return nil, nil, err
   261  	}
   262  	fs, err := c.FlowcontrolV1().FlowSchemas().Create(context.TODO(), &flowcontrol.FlowSchema{
   263  		ObjectMeta: metav1.ObjectMeta{
   264  			Name: username,
   265  		},
   266  		Spec: flowcontrol.FlowSchemaSpec{
   267  			DistinguisherMethod: &flowcontrol.FlowDistinguisherMethod{
   268  				Type: flowcontrol.FlowDistinguisherMethodByUserType,
   269  			},
   270  			MatchingPrecedence: 1000,
   271  			PriorityLevelConfiguration: flowcontrol.PriorityLevelConfigurationReference{
   272  				Name: username,
   273  			},
   274  			Rules: []flowcontrol.PolicyRulesWithSubjects{
   275  				{
   276  					ResourceRules: []flowcontrol.ResourcePolicyRule{
   277  						{
   278  							Verbs:        []string{flowcontrol.VerbAll},
   279  							APIGroups:    []string{flowcontrol.APIGroupAll},
   280  							Resources:    []string{flowcontrol.ResourceAll},
   281  							Namespaces:   []string{flowcontrol.NamespaceEvery},
   282  							ClusterScope: true,
   283  						},
   284  					},
   285  					Subjects: []flowcontrol.Subject{
   286  						{
   287  							Kind: flowcontrol.SubjectKindUser,
   288  							User: &flowcontrol.UserSubject{
   289  								Name: username,
   290  							},
   291  						},
   292  					},
   293  				},
   294  			},
   295  		},
   296  	}, metav1.CreateOptions{})
   297  	if err != nil {
   298  		return nil, nil, err
   299  	}
   300  
   301  	return pl, fs, wait.Poll(time.Second, timeout, func() (bool, error) {
   302  		fs, err := c.FlowcontrolV1().FlowSchemas().Get(context.TODO(), username, metav1.GetOptions{})
   303  		if err != nil {
   304  			return false, err
   305  		}
   306  		for _, condition := range fs.Status.Conditions {
   307  			if condition.Type == flowcontrol.FlowSchemaConditionDangling {
   308  				if condition.Status == flowcontrol.ConditionFalse {
   309  					return true, nil
   310  				}
   311  			}
   312  		}
   313  		return false, nil
   314  	})
   315  }
   316  
   317  func streamRequests(parallel int, request func(), wg *sync.WaitGroup, stopCh <-chan struct{}) {
   318  	for i := 0; i < parallel; i++ {
   319  		go func() {
   320  			defer wg.Done()
   321  			for {
   322  				select {
   323  				case <-stopCh:
   324  					return
   325  				default:
   326  					request()
   327  				}
   328  			}
   329  		}()
   330  	}
   331  }