blob: f5dde8735e6959e7785cb64cabee3be50bf724f6 [file] [log] [blame]
// Copyright Istio Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package xds
import (
"bytes"
"fmt"
"os"
"path"
"strings"
"testing"
"text/template"
"time"
)
import (
"github.com/Masterminds/sprig/v3"
cluster "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
discovery "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
any "google.golang.org/protobuf/types/known/anypb"
meshconfig "istio.io/api/mesh/v1alpha1"
networking "istio.io/api/networking/v1alpha3"
"istio.io/pkg/env"
istiolog "istio.io/pkg/log"
)
import (
"github.com/apache/dubbo-go-pixiu/pilot/pkg/config/kube/crd"
"github.com/apache/dubbo-go-pixiu/pilot/pkg/features"
"github.com/apache/dubbo-go-pixiu/pilot/pkg/model"
"github.com/apache/dubbo-go-pixiu/pilot/pkg/networking/util"
v3 "github.com/apache/dubbo-go-pixiu/pilot/pkg/xds/v3"
"github.com/apache/dubbo-go-pixiu/pilot/test/xdstest"
"github.com/apache/dubbo-go-pixiu/pkg/config"
"github.com/apache/dubbo-go-pixiu/pkg/config/mesh"
"github.com/apache/dubbo-go-pixiu/pkg/config/schema/collections"
"github.com/apache/dubbo-go-pixiu/pkg/config/schema/gvk"
"github.com/apache/dubbo-go-pixiu/pkg/spiffe"
"github.com/apache/dubbo-go-pixiu/pkg/test"
"github.com/apache/dubbo-go-pixiu/pkg/test/util/yml"
"github.com/apache/dubbo-go-pixiu/pkg/util/protomarshal"
)
// ConfigInput defines inputs passed to the test config templates
// This allows tests to do things like create a virtual service for each service, for example
type ConfigInput struct {
// Name of the test
Name string
// Name of the test config file to use. If not set, <Name> is used
ConfigName string
// Number of services to make
Services int
// Number of instances to make
Instances int
// Type of proxy to generate configs for
ProxyType model.NodeType
}
var testCases = []ConfigInput{
{
// Gateways provides an example config for a large Ingress deployment. This will create N
// virtual services and gateways, where routing is determined by hostname, meaning we generate N routes for HTTPS.
Name: "gateways",
Services: 1000,
ProxyType: model.Router,
},
{
// Gateways-shared provides an example config for a large Ingress deployment. This will create N
// virtual services and gateways, where routing is determined by path. This means there will be a single large route.
Name: "gateways-shared",
Services: 1000,
ProxyType: model.Router,
},
{
Name: "empty",
Services: 100,
ProxyType: model.SidecarProxy,
},
{
Name: "tls",
Services: 100,
},
{
Name: "telemetry",
Services: 100,
},
{
Name: "telemetry-api",
Services: 100,
},
{
Name: "virtualservice",
Services: 100,
},
{
Name: "authorizationpolicy",
Services: 100,
},
{
Name: "peerauthentication",
Services: 100,
},
{
Name: "knative-gateway",
Services: 100,
ProxyType: model.Router,
},
{
Name: "serviceentry-workloadentry",
Services: 100,
Instances: 1000,
ProxyType: model.SidecarProxy,
},
}
var sidecarTestCases = func() (res []ConfigInput) {
for _, c := range testCases {
if c.ProxyType == model.Router {
continue
}
res = append(res, c)
}
return res
}()
func configureBenchmark(t test.Failer) {
for _, s := range istiolog.Scopes() {
if s.Name() == benchmarkScope.Name() {
continue
}
s.SetOutputLevel(istiolog.NoneLevel)
}
test.SetBoolForTest(t, &features.EnableXDSCaching, false)
}
func BenchmarkInitPushContext(b *testing.B) {
configureBenchmark(b)
for _, tt := range testCases {
b.Run(tt.Name, func(b *testing.B) {
s, proxy := setupTest(b, tt)
b.ResetTimer()
for n := 0; n < b.N; n++ {
s.Env().PushContext.InitDone.Store(false)
initPushContext(s.Env(), proxy)
}
})
}
}
// Do a quick sanity tests to make sure telemetry v2 filters are applying. This ensures as they
// update our benchmark doesn't become useless.
func TestValidateTelemetry(t *testing.T) {
s, proxy := setupAndInitializeTest(t, ConfigInput{Name: "telemetry", Services: 1})
c, _, _ := s.Discovery.Generators[v3.ClusterType].Generate(proxy, nil, &model.PushRequest{Full: true, Push: s.PushContext()})
if len(c) == 0 {
t.Fatal("Got no clusters!")
}
for _, r := range c {
cls := &cluster.Cluster{}
if err := r.GetResource().UnmarshalTo(cls); err != nil {
t.Fatal(err)
}
for _, ff := range cls.Filters {
if ff.Name == "istio.metadata_exchange" {
return
}
}
}
t.Fatalf("telemetry v2 filters not found")
}
func BenchmarkRouteGeneration(b *testing.B) {
runBenchmark(b, v3.RouteType, testCases)
}
func TestRouteGeneration(t *testing.T) {
testBenchmark(t, v3.RouteType, testCases)
}
func BenchmarkClusterGeneration(b *testing.B) {
runBenchmark(b, v3.ClusterType, testCases)
}
func TestClusterGeneration(t *testing.T) {
testBenchmark(t, v3.ClusterType, testCases)
}
func BenchmarkListenerGeneration(b *testing.B) {
runBenchmark(b, v3.ListenerType, testCases)
}
func TestListenerGeneration(t *testing.T) {
testBenchmark(t, v3.ListenerType, testCases)
}
func BenchmarkNameTableGeneration(b *testing.B) {
runBenchmark(b, v3.NameTableType, sidecarTestCases)
}
func TestNameTableGeneration(t *testing.T) {
testBenchmark(t, v3.NameTableType, sidecarTestCases)
}
var secretCases = []ConfigInput{
{
Name: "secrets",
Services: 10,
ProxyType: model.Router,
},
{
Name: "secrets",
Services: 1000,
ProxyType: model.Router,
},
}
func TestSecretGeneration(t *testing.T) {
testBenchmark(t, v3.SecretType, secretCases)
}
func BenchmarkSecretGeneration(b *testing.B) {
runBenchmark(b, v3.SecretType, secretCases)
}
func createGateways(n int) map[string]*meshconfig.Network {
out := make(map[string]*meshconfig.Network, n)
for i := 0; i < n; i++ {
out[fmt.Sprintf("network-%d", i)] = &meshconfig.Network{
Gateways: []*meshconfig.Network_IstioNetworkGateway{{
Gw: &meshconfig.Network_IstioNetworkGateway_Address{Address: fmt.Sprintf("35.0.0.%d", i)},
Port: 15443,
}},
}
}
return out
}
// BenchmarkEDS measures performance of EDS config generation
// TODO Add more variables, such as different services
func BenchmarkEndpointGeneration(b *testing.B) {
configureBenchmark(b)
const numNetworks = 4
tests := []struct {
endpoints int
services int
}{
{1, 100},
{10, 10},
{100, 10},
{1000, 1},
}
var response *discovery.DiscoveryResponse
for _, tt := range tests {
b.Run(fmt.Sprintf("%d/%d", tt.endpoints, tt.services), func(b *testing.B) {
s := NewFakeDiscoveryServer(b, FakeOptions{
Configs: createEndpoints(tt.endpoints, tt.services, numNetworks),
NetworksWatcher: mesh.NewFixedNetworksWatcher(&meshconfig.MeshNetworks{
Networks: createGateways(numNetworks),
}),
})
proxy := &model.Proxy{
Type: model.SidecarProxy,
IPAddresses: []string{"10.3.3.3"},
ID: "random",
ConfigNamespace: "default",
Metadata: &model.NodeMetadata{},
}
push := s.PushContext()
proxy.SetSidecarScope(push)
b.ResetTimer()
for n := 0; n < b.N; n++ {
loadAssignments := make([]*any.Any, 0)
for svc := 0; svc < tt.services; svc++ {
l := s.Discovery.generateEndpoints(NewEndpointBuilder(fmt.Sprintf("outbound|80||foo-%d.com", svc), proxy, push))
loadAssignments = append(loadAssignments, util.MessageToAny(l))
}
response = endpointDiscoveryResponse(loadAssignments, version, push.LedgerVersion)
}
logDebug(b, model.AnyToUnnamedResources(response.GetResources()))
})
}
}
func runBenchmark(b *testing.B, tpe string, testCases []ConfigInput) {
configureBenchmark(b)
for _, tt := range testCases {
b.Run(tt.Name, func(b *testing.B) {
s, proxy := setupAndInitializeTest(b, tt)
wr := getWatchedResources(tpe, tt, s, proxy)
b.ResetTimer()
var c model.Resources
for n := 0; n < b.N; n++ {
c, _, _ = s.Discovery.Generators[tpe].Generate(proxy, wr, &model.PushRequest{Full: true, Push: s.PushContext()})
if len(c) == 0 {
b.Fatalf("Got no %v's!", tpe)
}
}
logDebug(b, c)
})
}
}
func testBenchmark(t *testing.T, tpe string, testCases []ConfigInput) {
for _, tt := range testCases {
t.Run(tt.Name, func(t *testing.T) {
// No need for large test here
tt.Services = 1
tt.Instances = 1
s, proxy := setupAndInitializeTest(t, tt)
wr := getWatchedResources(tpe, tt, s, proxy)
c, _, _ := s.Discovery.Generators[tpe].Generate(proxy, wr, &model.PushRequest{Full: true, Push: s.PushContext()})
if len(c) == 0 {
t.Fatalf("Got no %v's!", tpe)
}
})
}
}
func getWatchedResources(tpe string, tt ConfigInput, s *FakeDiscoveryServer, proxy *model.Proxy) *model.WatchedResource {
switch tpe {
case v3.SecretType:
watchedResources := []string{}
for i := 0; i < tt.Services; i++ {
watchedResources = append(watchedResources, fmt.Sprintf("kubernetes://default/sds-credential-%d", i))
}
return &model.WatchedResource{ResourceNames: watchedResources}
case v3.RouteType:
l := s.Discovery.ConfigGenerator.BuildListeners(proxy, s.PushContext())
routeNames := xdstest.ExtractRoutesFromListeners(l)
return &model.WatchedResource{ResourceNames: routeNames}
}
return nil
}
// Setup test builds a mock test environment. Note: push context is not initialized, to be able to benchmark separately
// most should just call setupAndInitializeTest
func setupTest(t testing.TB, config ConfigInput) (*FakeDiscoveryServer, *model.Proxy) {
proxyType := config.ProxyType
if proxyType == "" {
proxyType = model.SidecarProxy
}
proxy := &model.Proxy{
Type: proxyType,
IPAddresses: []string{"1.1.1.1"},
ID: "v0.default",
DNSDomain: "default.example.org",
Metadata: &model.NodeMetadata{
Namespace: "default",
Labels: map[string]string{
"istio.io/benchmark": "true",
},
ClusterID: "Kubernetes",
IstioVersion: "1.14.0",
},
ConfigNamespace: "default",
VerifiedIdentity: &spiffe.Identity{Namespace: "default"},
}
proxy.IstioVersion = model.ParseIstioVersion(proxy.Metadata.IstioVersion)
configs, k8sConfig := getConfigsWithCache(t, config)
s := NewFakeDiscoveryServer(t, FakeOptions{
Configs: configs,
KubernetesObjectString: k8sConfig,
// Allow debounce to avoid overwhelming with writes
DebounceTime: time.Millisecond * 10,
DisableSecretAuthorization: true,
})
return s, proxy
}
var (
configCache = map[ConfigInput][]config.Config{}
k8sConfigCache = map[ConfigInput]string{}
)
func getConfigsWithCache(t testing.TB, input ConfigInput) ([]config.Config, string) {
// Config setup is slow for large tests. Cache this and return from Cache.
// This improves even running a single test, as go will run the full test (including setup) at least twice.
if cached, f := configCache[input]; f {
return cached, k8sConfigCache[input]
}
configName := input.ConfigName
if configName == "" {
configName = input.Name
}
tmpl := template.Must(template.New("").Funcs(sprig.TxtFuncMap()).ParseFiles(path.Join("testdata", "benchmarks", configName+".yaml")))
var buf bytes.Buffer
if err := tmpl.ExecuteTemplate(&buf, configName+".yaml", input); err != nil {
t.Fatalf("failed to execute template: %v", err)
}
extra := path.Join("testdata", "benchmarks", configName+".extra.yaml")
inputYAML := buf.String()
if _, err := os.Stat(extra); err == nil {
bdata, err := os.ReadFile(extra)
if err != nil {
t.Fatal(err)
}
inputYAML += "\n---\n" + yml.SplitYamlByKind(string(bdata))[gvk.EnvoyFilter.Kind]
}
configs, badKinds, err := crd.ParseInputs(inputYAML)
if err != nil {
t.Fatalf("failed to read config: %v", err)
}
scrt, count := parseSecrets(inputYAML)
if len(badKinds) != count {
t.Fatalf("Got unknown resources: %v", badKinds)
}
// setup default namespace if not defined
for i, c := range configs {
if c.Namespace == "" {
c.Namespace = "default"
}
configs[i] = c
}
configCache[input] = configs
k8sConfigCache[input] = scrt
return configs, scrt
}
func parseSecrets(inputs string) (string, int) {
matches := 0
sb := strings.Builder{}
for _, text := range strings.Split(inputs, "\n---") {
if strings.Contains(text, "kind: Secret") {
sb.WriteString(text + "\n---\n")
matches++
}
}
return sb.String(), matches
}
func setupAndInitializeTest(t testing.TB, config ConfigInput) (*FakeDiscoveryServer, *model.Proxy) {
s, proxy := setupTest(t, config)
initPushContext(s.Env(), proxy)
return s, proxy
}
func initPushContext(env *model.Environment, proxy *model.Proxy) {
env.PushContext.InitContext(env, nil, nil)
proxy.SetSidecarScope(env.PushContext)
proxy.SetGatewaysForProxy(env.PushContext)
proxy.SetServiceInstances(env.ServiceDiscovery)
}
var debugGeneration = env.RegisterBoolVar("DEBUG_CONFIG_DUMP", false, "if enabled, print a full config dump of the generated config")
var benchmarkScope = istiolog.RegisterScope("benchmark", "", 0)
// Add additional debug info for a test
func logDebug(b *testing.B, m model.Resources) {
b.Helper()
b.StopTimer()
if debugGeneration.Get() {
for i, r := range m {
s, err := protomarshal.MarshalIndent(r, " ")
if err != nil {
b.Fatal(err)
}
// Cannot use b.Logf, it truncates
benchmarkScope.Infof("Generated: %d %s", i, s)
}
}
bytes := 0
for _, r := range m {
bytes += len(r.GetResource().Value)
}
b.ReportMetric(float64(bytes)/1000, "kb/msg")
b.ReportMetric(float64(len(m)), "resources/msg")
b.StartTimer()
}
func createEndpoints(numEndpoints, numServices, numNetworks int) []config.Config {
result := make([]config.Config, 0, numServices)
for s := 0; s < numServices; s++ {
endpoints := make([]*networking.WorkloadEntry, 0, numEndpoints)
for e := 0; e < numEndpoints; e++ {
endpoints = append(endpoints, &networking.WorkloadEntry{
Address: fmt.Sprintf("111.%d.%d.%d", e/(256*256), (e/256)%256, e%256),
Network: fmt.Sprintf("network-%d", e%numNetworks),
})
}
result = append(result, config.Config{
Meta: config.Meta{
GroupVersionKind: collections.IstioNetworkingV1Alpha3Serviceentries.Resource().GroupVersionKind(),
Name: fmt.Sprintf("foo-%d", s),
Namespace: "default",
CreationTimestamp: time.Now(),
},
Spec: &networking.ServiceEntry{
Hosts: []string{fmt.Sprintf("foo-%d.com", s)},
Ports: []*networking.Port{
{Number: 80, Name: "http-port", Protocol: "http"},
},
Endpoints: endpoints,
Resolution: networking.ServiceEntry_STATIC,
},
})
}
return result
}
func BenchmarkPushRequest(b *testing.B) {
// allTriggers contains all triggers, so we can pick one at random.
// It is not a big issue if it falls out of sync, as we are just trying to generate test data
allTriggers := []model.TriggerReason{
model.EndpointUpdate,
model.ConfigUpdate,
model.ServiceUpdate,
model.ProxyUpdate,
model.GlobalUpdate,
model.UnknownTrigger,
model.DebugTrigger,
model.SecretTrigger,
model.NetworksTrigger,
model.ProxyRequest,
model.NamespaceUpdate,
}
// Number of (simulated) proxies
proxies := 500
// Number of (simulated) pushes merged
pushesMerged := 10
// Number of configs per push
configs := 1
for n := 0; n < b.N; n++ {
var req *model.PushRequest
for i := 0; i < pushesMerged; i++ {
trigger := allTriggers[i%len(allTriggers)]
nreq := &model.PushRequest{
ConfigsUpdated: map[model.ConfigKey]struct{}{},
Reason: []model.TriggerReason{trigger},
}
for c := 0; c < configs; c++ {
nreq.ConfigsUpdated[model.ConfigKey{Kind: gvk.ServiceEntry, Name: fmt.Sprintf("%d", c), Namespace: "default"}] = struct{}{}
}
req = req.Merge(nreq)
}
for p := 0; p < proxies; p++ {
recordPushTriggers(req.Reason...)
}
}
}