【kubernetes/k8s源码分析】k8s 性能测试 perf-tests 解读源码

本文深入探讨了Kubernetes性能测试框架pert-tests,重点介绍了clusterloader2的使用方法及测试过程,解析了APIResponsiveness、SaturationPodStartupLatency等关键测量指标的实现原理与测试结果。
摘要由CSDN通过智能技术生成

    github: https://github.com/kubernetes/perf-tests/tree/master/clusterloader2 

    pert-tests 是性能测试框架,需要用户自己通过配置文件定义性能测试策略。以 kubernetes density 测试为例,clusterloader2 的测试过程

 

./clusterload2 --kubeconfig=/root/.kube/config \

    --kubemark-root-kubeconfig=/root/.kube/config \

    --enable-prometheus-server=true \

    --tear-down-prometheus-server=false \

    --provider=kubemark \

     --masterip=${master_ip}  \

     --testconfig=config.yaml --report-dir=./reports \

     --alsologtostderr 2>&1 | tee ./tmp.log

 

      Measurement 都是用于采集数据的观测程序。包括从metrics,或者event,或者从 apiserver 获得的资源数据,density 测试包括 APIResponsiveness、SaturationPodStartupLatency、WaitForRunningSaturationRCs、SchedulingThroughput、PodStartupLatency(第 2 章节)。后续文章会分析这些 measurement 的实现方式。

 

   第 1 章节根据配置如下 step所示

steps:
- name: Starting measurements
  measurements:
  - Identifier: APIResponsiveness
    Method: APIResponsiveness
    Params:
      action: reset
  - Identifier: APIResponsivenessPrometheus
    Method: APIResponsivenessPrometheus
    Params:
      action: start
  # TODO(oxddr): figure out how many probers to run in function of cluster
  - Identifier: InClusterNetworkLatency
    Method: InClusterNetworkLatency
    Params:
      action: start
      replicasPerProbe: {{AddInt 2 (DivideInt .Nodes 100)}}
  - Identifier: DnsLookupLatency
    Method: DnsLookupLatency
    Params:
      action: start
      replicasPerProbe: {{AddInt 2 (DivideInt .Nodes 100)}}
  - Identifier: TestMetrics
    Method: TestMetrics
    Params:
      action: start
      nodeMode: {{$NODE_MODE}}
      resourceConstraints: {{$DENSITY_RESOURCE_CONSTRAINTS_FILE}}
      systemPodMetricsEnabled: {{$ENABLE_SYSTEM_POD_METRICS}}

 

1. TESTMtrics

    TestMetrics 包括如下

  • EtcdMetrics
  • SchedulingMetrics
  • MetricsForE2E
  • ResourceUsageSummay
  • CPUProfile 包括 etcd apiserver scheduler controllermanager 等
  • MemoryProfile
  • SystemPodMetrics
type testMetrics struct {
	etcdMetrics                    measurement.Measurement
	schedulingMetrics              measurement.Measurement
	metricsForE2E                  measurement.Measurement
	resourceUsageSummary           measurement.Measurement
	etcdCPUProfile                 measurement.Measurement
	etcdMemoryProfile              measurement.Measurement
	etcdMutexProfile               measurement.Measurement
	apiserverCPUProfile            measurement.Measurement
	apiserverMemoryProfile         measurement.Measurement
	schedulerCPUProfile            measurement.Measurement
	schedulerMemoryProfile         measurement.Measurement
	controllerManagerCPUProfile    measurement.Measurement
	controllerManagerMemoryProfile measurement.Measurement
	systemPodMetrics               measurement.Measurement
}

    1.1 ETCDMetric

      通过 curl http://localhost:2379/metrics 或者 curl http://localhost:2382/metrics (新版本) 获取数据

etcd_disk_backend_commit_duration_seconds_bucket{le="0.001"} 0

etcd_disk_backend_commit_duration_seconds_bucket{le="0.002"} 0

etcd_disk_backend_commit_duration_seconds_bucket{le="0.004"} 0

etcd_disk_backend_commit_duration_seconds_bucket{le="8.192"} 141478

etcd_disk_backend_commit_duration_seconds_bucket{le="+Inf"} 141478

  • etcd_disk_backend_commit_duration_seconds_bucket:The latency distributions of commit called by backend
  • etcd_debugging_snap_save_total_duration_seconds_bucket:The total latency distributions of save called by snapshot.
  • etcd_disk_wal_fsync_duration_seconds_bucket
  • etcd_network_peer_round_trip_time_seconds_bucket

     etcd 集群磁盘latency性能问题,通过etcd metrics接口dump backend_commit_duration_seconds 和 wal_fsync_duration_seconds,latency区间在128ms

     getEtcdMetrics 通过 curl http://localhost:2379/metrics 或者 curl http://localhost:2382/metrics (新版本) 获取数据

     根据上述 metrics label 获得数据

func (e *etcdMetricsMeasurement) stopAndSummarize(host, provider string) error {
	defer e.Dispose()
	// Do some one-off collection of metrics.
	samples, err := e.getEtcdMetrics(host, provider)
	if err != nil {
		return err
	}
	for _, sample := range samples {
		switch sample.Metric[model.MetricNameLabel] {
		case "etcd_disk_backend_commit_duration_seconds_bucket":
			measurementutil.ConvertSampleToBucket(sample, &e.metrics.BackendCommitDuration)
		case "etcd_debugging_snap_save_total_duration_seconds_bucket":
			measurementutil.ConvertSampleToBucket(sample, &e.metrics.SnapshotSaveTotalDuration)
		case "etcd_disk_wal_fsync_duration_seconds_bucket":
			measurementutil.ConvertSampleToBucket(sample, &e.metrics.WalFsyncDuration)
		case "etcd_network_peer_round_trip_time_seconds_bucket":
			measurementutil.ConvertSampleToBucket(sample, &e.metrics.PeerRoundTripTime)
		}
	}
	return nil
}

    1.1.1 Etcd Disk

     backend_commit_duration_seconds (p99 duration should be less than 25ms),当 etcd 将其最新更改提交到磁盘的增量快照提交时,将调用 backend_commit。

     wal_fsync_duration_seconds (p99 duration should be less than 10ms) ,当 etcd 在应用日志之前将其日志条目持久保存到磁盘时,将调用wal_fsync。

     磁盘操作延迟(wal_fsync_duration_seconds或backend_commit_duration_seconds)通常指示磁盘问题。 这可能会导致请求延迟或使群集不稳定。     

 

    1.1.2 Etcd 调优 time  参数

      通过修改 etcd 的 --heartbeat-interval 与 --election-timeout 启动参数来适当提高高吞吐网络, 可以根据到对端 RTT 值进行设置, 默认为 100ms

# Command line arguments:
$ etcd --heartbeat-interval=100 --election-timeout=500

# Environment variables:
$ ETCD_HEARTBEAT_INTERVAL=100 ETCD_ELECTION_TIMEOUT=500 etcd

    1.1.3 Etcd 调优 snapshot  参数

      快照提供了一种通过保存系统的当前状态并移除旧日志文件的方式来压缩日志文件如果 Etcd 的内存使用和磁盘使用过高,那么应该尝试调低快照触发的阈值,默认值为 10000

# Command line arguments:
$ etcd --snapshot-count=5000

# Environment variables:
$ ETCD_SNAPSHOT_COUNT=5000 etcd

    1.1.4 Etcd 调优 Disk 优先级

      etcd群集对磁盘延迟非常敏感。 由于etcd必须将建议持久保存到其日志中,因此来自其他进程的磁盘活动可能会导致较长的fsync延迟。 结果是etcd可能会错过心跳,从而导致请求超时和临时领导者丢失。 当给予较高的磁盘优先级时,etcd服务器有时可以与这些进程一起稳定运行。Linux 中 etcd 的磁盘优先级可以使用 ionice 配置

ionice -c2 -n0 -p `pgrep etcd`

    1.1.5 Etcd 调优 Network 优先级

      如果 etcd 领导者处理大量并发的客户端请求,由于网络拥塞,可能会延迟处理跟随者 peer 请求。 这表现为跟随者节点上的发送缓冲区错误消息

dropped MsgProp to 247ae21ff9436b2d since streamMsg's sending buffer is full
dropped MsgAppResp to 247ae21ff9436b2d since streamMsg's sending buffer is full

      通过将 etcd 的 peer 流量优先于其客户端流量,可以解决这些错误。 在Linux上,可以使用流量控制机制来确定 peer 流量的优先级:   

tc qdisc add dev eth0 root handle 1: prio bands 3
tc filter add dev eth0 parent 1: protocol ip prio 1 u32 match ip sport 2380 0xffff flowid 1:1
tc filter add dev eth0 parent 1: protocol ip prio 1 u32 match ip dport 2380 0xffff flowid 1:1
tc filter add dev eth0 parent 1: protocol ip prio 2 u32 match ip sport 2379 0xffff flowid 1:1
tc filter add dev eth0 parent 1: protocol ip prio 2 u32 match ip dport 2379 0xffff flowid 1:1

     参考:  https://etcd.io/docs/v3.4/tuning/

 

    1.2 SchedulerMetrics

      通过 curl -XGET http://localhost:10251/metrics 获取数据,label 为 scheduler_scheduling_latency_seconds

      统计的是数据包括 predicate_evaluation priority_evaluation preemption_evaluation bind

scheduler_scheduling_latency_seconds{operation="binding",quantile="0.5"} 0.0472671

scheduler_scheduling_latency_seconds{operation="binding",quantile="0.9"} 0.100486235

scheduler_scheduling_latency_seconds{operation="binding",quantile="0.99"} 0.113182433

scheduler_scheduling_latency_seconds_sum{operation="binding"} 14.535012662000003

scheduler_scheduling_latency_seconds_count{operation="binding"} 240

scheduler_scheduling_latency_seconds{operation="predicate_evaluation",quantile="0.5"} 0.000407027

scheduler_scheduling_latency_seconds{operation="predicate_evaluation",quantile="0.9"} 0.000732365

scheduler_scheduling_latency_seconds{operation="predicate_evaluation",quantile="0.99"} 0.001816966

scheduler_scheduling_latency_seconds_sum{operation="predicate_evaluation"} 0.134087715

scheduler_scheduling_latency_seconds_count{operation="predicate_evaluation"} 240

for _, sample := range samples {
	if sample.Metric[model.MetricNameLabel] != schedulingLatencyMetricName {
		continue
	}

	var metric *measurementutil.LatencyMetric
	switch sample.Metric[schedulermetric.OperationLabel] {
	case schedulermetric.PredicateEvaluation:
		metric = &result.PredicateEvaluationLatency
	case schedulermetric.PriorityEvaluation:
		metric = &result.PriorityEvaluationLatency
	case schedulermetric.PreemptionEvaluation:
		metric = &result.PreemptionEvaluationLatency
	case schedulermetric.Binding:
		metric = &result.BindingLatency
	}
	if metric == nil {
		continue
	}

    生成报告文件  SchedulingMetrics_density_, 包括预选,优选, 抢占, 绑定的延时

{
  "predicateEvaluationLatency": {
    "Perc50": 361071,
    "Perc90": 517682,
    "Perc99": 839416
  },
  "priorityEvaluationLatency": {
    "Perc50": 326542,
    "Perc90": 442761,
    "Perc99": 750440
  },
  "preemptionEvaluationLatency": {
    "Perc50": 7095583,
    "Perc90": 73038746,
    "Perc99": 213016585
  },
  "bindingLatency": {
    "Perc50": 6799968,
    "Perc90": 39049090,
    "Perc99": 1484440686
  },
  "e2eSchedulingLatency": {
    "Perc50": 0,
    "Perc90": 0,
    "Perc99": 0
  }
}

    1.3 APIResponsiveness

      通过 curl -XGET http://localhost:8080/metrics可以获得,代码中使用 client-go 一样的功能,过滤的 label 为apiserver_request_latencies_summaryapiserver_request_count

      这个资源特别多,只罗列几个,其他略

apiserver_request_latencies_summary{resource="pods",scope="namespace",subresource="",verb="LIST",quantile="0.5"} 2050
apiserver_request_latencies_summary{resource="pods",scope="namespace",subresource="",verb="LIST",quantile="0.9"} 2050
apiserver_request_latencies_summary{resource="pods",scope="namespace",subresource="",verb="LIST",quantile="0.99"} 2050
apiserver_request_latencies_summary_sum{resource="pods",scope="namespace",subresource="",verb="LIST"} 78645
apiserver_request_latencies_summary_count{resource="pods",scope="namespace",subresource="",verb="LIST"} 5
apiserver_request_latencies_summary{resource="pods",scope="namespace",subresource="",verb="POST",quantile="0.5"} 39036
apiserver_request_latencies_summary{resource="pods",scope="namespace",subresource="",verb="POST",quantile="0.9"} 120477
apiserver_request_latencies_summary{resource="pods",scope="namespace",subresource="",verb="POST",quantile="0.99"} 144922
apiserver_request_latencies_summary_sum{resource="pods",scope="namespace",subresource="",verb="POST"} 1.5654715e+07

{

"data": {

"Perc50": 0.498,

"Perc90": 0.498,

"Perc99": 0.498

},

"unit": "ms",

"labels": {

"Count": "2",

"Resource": "pods",

"Scope": "cluster",

"Subresource": "",

"Verb": "LIST"

}

}

 

    1.4 systemPodMetrics

     只搜集系统 pod 重启次数,也就是 kube-system namespace 下的 pod,统计的结果如下:

  "pods": [
    {
      "name": "calico-node-b6mq4",
      "containers": []
    },
    {
      "name": "calico-kube-controllers-8597b9886-dvkl4",
      "containers": [
        {
          "name": "calico-kube-controllers",
          "restartCount": 0
        }
      ]
    },
    {
      "name": "calico-node-kcp6w",
      "containers": []
    },
    {
      "name": "calico-node-jvj54",
      "containers": []
    },
    {
      "name": "calico-node-p75cp",
      "containers": [
        {
          "name": "calico-node",
          "restartCount": 0
        }

 

    第 2 章节根据如下配置讲解

- name: Starting saturation pod measurements
  measurements:
  - Identifier: SaturationPodStartupLatency
    Method: PodStartupLatency
    Params:
      action: start
      labelSelector: group = saturation
      threshold: {{$saturationDeploymentTimeout}}s
  - Identifier: WaitForRunningSaturationDeployments
    Method: WaitForControlledPodsRunning
    Params:
      action: start
      apiVersion: apps/v1
      kind: Deployment
      labelSelector: group = saturation
      operationTimeout: {{$saturationDeploymentHardTimeout}}s
  - Identifier: SchedulingThroughput
    Method: SchedulingThroughput
    Params:
      action: start
      labelSelector: group = saturation

 

2. PodStartupLatency

    列出该 namespace 下的 event 且 KIND 类型为 Pod,

func (p *podStartupLatencyMeasurement) gatherScheduleTimes(c clientset.Interface) error {
	selector := fields.Set{
		"involvedObject.kind": "Pod",
		"source":              corev1.DefaultSchedulerName,
	}.AsSelector().String()
	options := metav1.ListOptions{FieldSelector: selector}
	schedEvents, err := c.CoreV1().Events(p.selector.Namespace).List(options)
	if err != nil {
		return err
	}

     如下这段代码是 start 阶段启动时执行,如果 pod 是 running 状态,使用 namespace-pod 作为 key,存入 podStartupEntries ,设置 watch 时间为 time.Now,设置 create 时间为 CreationTimestamp,设置 run 时间

if pod.Status.Phase == corev1.PodRunning {
	key := createMetaNamespaceKey(pod.Namespace, pod.Name)
	if _, found := p.podStartupEntries.Get(key, createPhase); !found {
		p.podStartupEntries.Set(key, watchPhase, time.Now())
		p.podStartupEntries.Set(key, createPhase, pod.CreationTimestamp.Time)
		var startTime metav1.Time
		for _, cs := range pod.Status.ContainerStatuses {
			if cs.State.Running != nil {
				if startTime.Before(&cs.State.Running.StartedAt) {
					startTime = cs.State.Running.StartedAt
				}
			}
		}
		if startTime != metav1.NewTime(time.Time{}) {
			p.podStartupEntries.Set(key, runPhase, startTime.Time)
		} else {
			klog.Errorf("%s: pod %v (%v) is reported to be running, but none of its containers is", p, pod.Name, pod.Namespace)
		}
	}
}

   2.1 测试的目标如下

podStartupLatency := p.podStartupEntries.CalculateTransitionsLatency(map[string]measurementutil.Transition{
   "create_to_schedule": {
      From: createPhase,
      To:   schedulePhase,
   },
   "schedule_to_run": {
      From: schedulePhase,
      To:   runPhase,
   },
   "run_to_watch": {
      From: runPhase,
      To:   watchPhase,
   },
   "schedule_to_watch": {
      From: schedulePhase,
      To:   watchPhase,
   },
   "pod_startup": {
      From:      createPhase,
      To:        watchPhase,
      Threshold: p.threshold,
   },
})

    2.2 测试结果如下所示:

{
  "version": "1.0",
  "dataItems": [
    {
      "data": {
        "Perc50": 0,
        "Perc90": 0,
        "Perc99": 0
      },
      "unit": "ms",
      "labels": {
        "Metric": "create_to_schedule"
      }
    },
    {
      "data": {
        "Perc50": 0,
        "Perc90": 1000,
        "Perc99": 1000
      },
      "unit": "ms",
      "labels": {
        "Metric": "schedule_to_run"
      }
    },
    {
      "data": {
        "Perc50": 1090.348398,
        "Perc90": 1757.317935,
        "Perc99": 2299.464715
      },
      "unit": "ms",
      "labels": {
        "Metric": "run_to_watch"
      }
    },
    {
      "data": {
        "Perc50": 1578.38022,
        "Perc90": 2286.247476,
        "Perc99": 2630.67882
      },
      "unit": "ms",
      "labels": {
        "Metric": "schedule_to_watch"
      }
    },
    {
      "data": {
        "Perc50": 1580.205515,
        "Perc90": 2286.247476,
        "Perc99": 2630.67882
      },
      "unit": "ms",
      "labels": {
        "Metric": "pod_startup"
      }
    }
  ]
}

 

3. metricsForE2EMeasurement

     metricsForE2EMeasurement 测试主要是从 kube-apiserver kube-controller-manager kube-scheduler 等组件采集 metrics,可选的组件 kubelet 

    3.1 对 kube-apiserver 观测的指标有

var interestingApiServerMetricsLabels = []string{
   "apiserver_init_events_total",
   "apiserver_request_count",
   "apiserver_request_latencies_summary",
   "etcd_request_latencies_summary",
}

      apiserver_request_count,kube-apiserver 的指标数据从 kube-apiserver 的 metrics 获取,apiserver_request_count 包括各种资源的 LIST WATCH GET 等操作,如下示例为 nodes 的 LIST 操作,请求为 4 次

      {
        "metric": {
          "__name__": "apiserver_request_count",
          "client": "clusterloader/v0.0.0 (linux/amd64) kubernetes/$Format",
          "code": "200",
          "contentType": "application/vnd.kubernetes.protobuf",
          "resource": "nodes",
          "scope": "cluster",
          "subresource": "",
          "verb": "LIST"
        },
        "value": [
          0,
          "4"
        ]
      },

     apiserver_request_latencies_summary 指标包括各种资源的 LIST WATCH GET 等操作,如下所示为 service 的status 的 PUT 操作,在集群负载 0.5 0.9 0.99 时的请求延迟

      {
        "metric": {
          "__name__": "apiserver_request_latencies_summary",
          "quantile": "0.5",
          "resource": "apiservices",
          "scope": "cluster",
          "subresource": "status",
          "verb": "PUT"
        },
        "value": [
          0,
          "2308"
        ]
      },

      {
        "metric": {
          "__name__": "apiserver_request_latencies_summary",
          "quantile": "0.9",
          "resource": "apiservices",
          "scope": "cluster",
          "subresource": "status",
          "verb": "PUT"
        },
        "value": [
          0,
          "3941"
        ]
      },

      {
        "metric": {
          "__name__": "apiserver_request_latencies_summary",
          "quantile": "0.99",
          "resource": "apiservices",
          "scope": "cluster",
          "subresource": "status",
          "verb": "PUT"
        },
        "value": [
          0,
          "4137"
        ]
      },

 

 

总结:

    测试的指标大多根据 metrics 获取

    也有数据从 event 获取,比如 podStartupLatency

 

EtcdMetrics 测试方法

  • etcd_disk_backend_commit_duration_seconds_bucket
  • etcd_debugging_snap_save_total_duration_seconds_bucket
  • etcd_disk_wal_fsync_duration_seconds_bucket
  • etcd_network_peer_round_trip_time_seconds_bucket         可能未实现

 

metrics_for_e2e 测试方法

   kube-apiserver 的指标有:

  • apiserver_init_events_total                      可能未实现
  • apiserver_request_count
  • apiserver_request_latencies_summary
  • etcd_request_latencies_summary            可能未实现
google-perftools 简介 google-perftools 是一款针对 C/C++ 程序的性能分析工具,它是一个遵守 BSD 协议的开源项目。使用该工具可以对 CPU 时间片、内存等系统资源的分配和使用进行分析,本文将重点介绍如何进行 CPU 时间片的剖析。 google-perftools 对一个程序的 CPU 性能剖析包括以下几个步骤。 1. 编译目标程序,加入对 google-perftools 库的依赖。 2. 运行目标程序,并用某种方式启动 / 终止剖析函数并产生剖析结果。 3. 运行剖结果转换工具,将不可读的结果数据转化成某种格式的文档(例如 pdf,txt,gv 等)。 安装 您可以在 google-perftools 的网站 (http://code.google.com/p/google-perftools/downloads/list) 上下载最新版的安装包。为完成步骤 3 的工作,您还需要一个将剖析结果转化为程序员可读文档的工具,例如 gv(http://www.gnu.org/software/gv/)。 编译与运行 您需要在原有的编译选项中加入对 libprofiler.so 的引用,这样在目标程序运行时会加载工具的动态库。例如本例中作者的系统中,libprofiler.so 安装在"/usr/lib"目录下,所以需要在 makefile 文件中的编译选项加入“-L/usr/lib -lprofiler”。 google-perftools 需要在目标代码的开始和结尾点分别调用剖析模块的启动和终止函数,这样在目标程序运行时就可以对这段时间内程序实际占用的 CPU 时间片进行统计和分析。工具的启动和终止可以采用以下两种方式。 a. 使用调试工具 gdb 在程序中手动运行性能工具的启动 / 终止函数。 gdb 是 Linux 上广泛使用的调试工具,它提供了强大的命令行功能,使我们可以在程序运行时插入断点并在断点处执行其他函数。具体的文档请参照 http://www.gnu.org/software/gdb/,本文中将只对用到的几个基本功能进行简单介绍。使用以下几个功能就可以满足我们性能调试的基本需求,具体使用请参见下文示例。 命令 功能 ctrl+c 暂停程序的运行 c 继续程序的运行 b 添加函数断点(参数可以是源代码中的行号或者一个函数名) p 打印某个量的值或者执行一个函数调用 b. 在目标代码中直接加入性能工具函数的调用,该方法就是在程序代码中直接加入调试函数的调用。 两种方式都需要对目标程序重新编译,加入对性能工具的库依赖。对于前者,他的好处是使用比较灵活,但工具的启动和终止依赖于程序员的手动操作,常常需要一些暂停函数(比如休眠 sleep)的支持才能达到控制程序的目的,因此精度可能受到影响。对于后者,它需要对目标代码的进行修改,需要处理函数声明等问题,但得到的结果精度较高,缺点是每次重新设置启动点都需要重新编译,灵活度不高,读者可以根据自己的实际需求采用有效的方式。 示例详解 该程序是一个简单的例子,文中有两处耗时的无用操作,并且二者间有一定的调用关系。 清单 1. 示例程序 void consumeSomeCPUTime1(int input){ int i = 0; input++; while(i++ < 10000){ i--; i++; i--; i++; } }; void consumeSomeCPUTime2(int input){ input++; consumeSomeCPUTime1(input); int i = 0; while(i++ < 10000){ i--; i++; i--; i++; } }; int stupidComputing(int a, int b){ int i = 0; while( i++ < 10000){ consumeSomeCPUTime1(i); } int j = 0; while(j++ < 5000){ consumeSomeCPUTime2(j); } return a+b; }; int smartComputing(int a, int b){ return a+b; }; void main(){ int i = 0; printf("reached the start point of performance bottle neck\n"); sleep(5); //ProfilerStart("CPUProfile"); while( i++ MyProfile.pdf 转换后产生的结果文档如下图。图中的数字和框体的大小代表了的某个函数的运行时间占整个剖析时间的比例。由代码的逻辑可知,stupidComputing,stupidComputing2 都是费时操作并且它们和 consumeSomeCPUTime 存在着一定的调用关系。 图 1. 剖析结果 结束语 本文介绍了一个 Linux 平台上的性能剖析工具 google-perftools,并结合实例向读者展示了如何使用该工具配置、使用及分析性能瓶颈。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值