当前位置: 首页 > news >正文

API-Server的监听器Controller的List分页失效

前言

最近做项目,还是K8S的插件监听器(理论上插件都是通过API-server通信),官方的不同写法居然都能出现争议,争议点就是对API-Server的请求的耗时,说是会影响API-Server。实际上通过源码分析两着有差别,但是差别不大,对API-Server的影响几乎一样。

老式写法

package mainimport ("controller/control"v1 "k8s.io/api/core/v1""k8s.io/apimachinery/pkg/fields""k8s.io/client-go/kubernetes""k8s.io/client-go/tools/cache""k8s.io/client-go/tools/clientcmd""k8s.io/client-go/util/workqueue""k8s.io/klog/v2"
)func main() {// 读取构建 configconfig, err := clientcmd.BuildConfigFromFlags("", "xxx/config")if err != nil {klog.Fatal(err)}// 创建 k8s clientclientSet, err := kubernetes.NewForConfig(config)if err != nil {klog.Fatal(err)}// 指定 ListWatcher 在所有namespace下监听 pod 资源podListWatcher := cache.NewListWatchFromClient(clientSet.CoreV1().RESTClient(), "pods", v1.NamespaceAll, fields.Everything())// 创建 workqueuequeue := workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter())// 创建 indexer 和 informerindexer, informer := cache.NewIndexerInformer(podListWatcher, &v1.Pod{}, 0, cache.ResourceEventHandlerFuncs{// 当有 pod 创建时,根据 Delta queue 弹出的 object 生成对应的Key,并加入到 workqueue中。此处可以根据Object的一些属性,进行过滤AddFunc: func(obj interface{}) {key, err := cache.MetaNamespaceKeyFunc(obj)if err == nil {queue.Add(key)}},UpdateFunc: func(obj, newObj interface{}) {key, err := cache.MetaNamespaceKeyFunc(newObj)if err == nil {queue.Add(key)}},// pod 删除操作DeleteFunc: func(obj interface{}) {// DeletionHandlingMetaNamespaceKeyFunc 会在生成key 之前检查。因为资源删除后有可能会进行重建等操作,监听时错过了删除信息,从而导致该条记录是陈旧的。key, err := cache.DeletionHandlingMetaNamespaceKeyFunc(obj)if err == nil {queue.Add(key)}},}, cache.Indexers{})controller := control.NewController(queue, indexer, informer)stop := make(chan struct{})defer close(stop)// 启动 controlgo controller.Run(1, stop)select {}
}

然后写个Controller代码

package controlimport ("fmt"v1 "k8s.io/api/core/v1""k8s.io/apimachinery/pkg/util/runtime""k8s.io/apimachinery/pkg/util/wait""k8s.io/client-go/tools/cache""k8s.io/client-go/util/workqueue""k8s.io/klog/v2""time"
)type Controller struct {indexer  cache.Indexer                   // Indexer 的引用queue    workqueue.RateLimitingInterface //workqueue 的引用informer cache.Controller                // Informer 的引用
}func NewController(queue workqueue.RateLimitingInterface, indexer cache.Indexer, informer cache.Controller) *Controller {return &Controller{indexer:  indexer,queue:    queue,informer: informer,}
}func (c *Controller) Run(threadiness int, stopCh chan struct{}) {defer runtime.HandleCrash()defer c.queue.ShutDown()klog.Info("Starting pod control")go c.informer.Run(stopCh) // 启动 informerif !cache.WaitForCacheSync(stopCh, c.informer.HasSynced) {runtime.HandleError(fmt.Errorf("time out waitng for caches to sync"))return}// 启动多个 worker 处理 workqueue 中的对象for i := 0; i < threadiness; i++ {go wait.Until(c.runWorker, time.Second, stopCh)}<-stopChklog.Info("Stopping Pod control")
}func (c *Controller) runWorker() {// 启动无限循环,接收并处理消息for c.processNextItem() {}
}// 从 workqueue 中获取对象,并打印信息。
func (c *Controller) processNextItem() bool {key, shutdown := c.queue.Get()// 退出if shutdown {return false}// 标记此key已经处理defer c.queue.Done(key)// 将key对应的 object 的信息进行打印err := c.syncToStdout(key.(string))c.handleError(err, key)return true
}// 获取 key 对应的 object,并打印相关信息
func (c *Controller) syncToStdout(key string) error {obj, exists, err := c.indexer.GetByKey(key)if err != nil {klog.Errorf("Fetching object with key %s from store failed with %v", key, err)return err}if !exists {fmt.Printf("Pod %s does not exist\n", obj.(*v1.Pod).GetName())} else {fmt.Printf("Sync/Add/Update for Pod %s\n", obj.(*v1.Pod).GetName())}return nil
}func (c *Controller) handleError(err error, key interface{}) {}

这总写法的好处是自己处理各个环节,Informer和indexer,那个queue仅仅是队列,从cache缓存取数据用的,实际看看创建过程

创建lw的过程

cache.NewListWatchFromClient
// NewListWatchFromClient creates a new ListWatch from the specified client, resource, namespace and field selector.
func NewListWatchFromClient(c Getter, resource string, namespace string, fieldSelector fields.Selector) *ListWatch {optionsModifier := func(options *metav1.ListOptions) {options.FieldSelector = fieldSelector.String()}return NewFilteredListWatchFromClient(c, resource, namespace, optionsModifier)
}// NewFilteredListWatchFromClient creates a new ListWatch from the specified client, resource, namespace, and option modifier.
// Option modifier is a function takes a ListOptions and modifies the consumed ListOptions. Provide customized modifier function
// to apply modification to ListOptions with a field selector, a label selector, or any other desired options.
func NewFilteredListWatchFromClient(c Getter, resource string, namespace string, optionsModifier func(options *metav1.ListOptions)) *ListWatch {listFunc := func(options metav1.ListOptions) (runtime.Object, error) {optionsModifier(&options)return c.Get().Namespace(namespace).Resource(resource).VersionedParams(&options, metav1.ParameterCodec).Do(context.TODO()).Get()}watchFunc := func(options metav1.ListOptions) (watch.Interface, error) {options.Watch = trueoptionsModifier(&options)return c.Get().Namespace(namespace).Resource(resource).VersionedParams(&options, metav1.ParameterCodec).Watch(context.TODO())}return &ListWatch{ListFunc: listFunc, WatchFunc: watchFunc}
}

ListAndWatch方法,函数指针,关键是List和Watch的函数,跟新的写法有些许区别

创建Informer

此处默认使用DeletionHandlingMetaNamespaceKeyFunc函数创建key

func NewIndexerInformer(lw ListerWatcher,objType runtime.Object,resyncPeriod time.Duration,h ResourceEventHandler,indexers Indexers,
) (Indexer, Controller) {// This will hold the client state, as we know it.clientState := NewIndexer(DeletionHandlingMetaNamespaceKeyFunc, indexers)return clientState, newInformer(lw, objType, resyncPeriod, h, clientState, nil)
}func newInformer(lw ListerWatcher,objType runtime.Object,resyncPeriod time.Duration,h ResourceEventHandler,clientState Store,transformer TransformFunc,
) Controller {// This will hold incoming changes. Note how we pass clientState in as a// KeyLister, that way resync operations will result in the correct set// of update/delete deltas.fifo := NewDeltaFIFOWithOptions(DeltaFIFOOptions{KnownObjects:          clientState,EmitDeltaTypeReplaced: true,})cfg := &Config{Queue:            fifo,ListerWatcher:    lw,ObjectType:       objType,FullResyncPeriod: resyncPeriod,RetryOnError:     false,Process: func(obj interface{}) error {if deltas, ok := obj.(Deltas); ok {return processDeltas(h, clientState, transformer, deltas)}return errors.New("object given as Process argument is not Deltas")},}return New(cfg)
}func New(c *Config) Controller {ctlr := &controller{config: *c,clock:  &clock.RealClock{},}return ctlr
}

这里注意,消费delta队列的过程 ,这里是没有加锁的,即Process函数指针

另外实际上还是创建controller内置结构体,也是client-go创建的。

 

新式写法

    config, err := clientcmd.BuildConfigFromFlags("", "~/.kube/config")//注意路径if err != nil {log.Fatal(err)}//这2行是抓包的时候使用,日常是不需要的config.TLSClientConfig.CAData = nilconfig.TLSClientConfig.Insecure = trueclientSet, err := kubernetes.NewForConfig(config)if err != nil {log.Fatal(err)}//这里可以调一些参数,defaultResync很关键factory := informers.NewSharedInformerFactoryWithOptions(clientSet, 0, informers.WithNamespace("default"))informer := factory.Core().V1().Pods().Informer()//获取pod的informer,实际上使用client-go的api很多informer都创建了,直接拿过来用,避免使用的时候重复创建informer.AddEventHandler(xxx) //事件处理,是一个回调hookstopper := make(chan struct{}, 1)go informer.Run(stopper)log.Println("----- list and watch pod starting...")sigs := make(chan os.Signal, 1)signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)<-sigsclose(stopper)log.Println("main stopped...")

实际上就是很多过程封装了,比如创建Controller的过程

lw的创建过程

func NewFilteredPodInformer(client kubernetes.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer {return cache.NewSharedIndexInformer(&cache.ListWatch{ListFunc: func(options metav1.ListOptions) (runtime.Object, error) {if tweakListOptions != nil {tweakListOptions(&options)}return client.CoreV1().Pods(namespace).List(context.TODO(), options)},WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) {if tweakListOptions != nil {tweakListOptions(&options)}return client.CoreV1().Pods(namespace).Watch(context.TODO(), options)},},&corev1.Pod{},resyncPeriod,indexers,)
}

实际上实现是有pod实现的,List最后取结果略有区别

// List takes label and field selectors, and returns the list of Pods that match those selectors.
func (c *pods) List(ctx context.Context, opts metav1.ListOptions) (result *v1.PodList, err error) {var timeout time.Durationif opts.TimeoutSeconds != nil {timeout = time.Duration(*opts.TimeoutSeconds) * time.Second}result = &v1.PodList{}err = c.client.Get().Namespace(c.ns).Resource("pods").VersionedParams(&opts, scheme.ParameterCodec).Timeout(timeout).Do(ctx).Into(result)return
}// Watch returns a watch.Interface that watches the requested pods.
func (c *pods) Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) {var timeout time.Durationif opts.TimeoutSeconds != nil {timeout = time.Duration(*opts.TimeoutSeconds) * time.Second}opts.Watch = truereturn c.client.Get().Namespace(c.ns).Resource("pods").VersionedParams(&opts, scheme.ParameterCodec).Timeout(timeout).Watch(ctx)
}

最关键的一点,超时,老式写法是没有超时设置的,超时的重要性不言而喻,推荐使用新写法

indexer的创建

默认使用MetaNamespaceIndexFunc函数创建key

func (f *podInformer) defaultInformer(client kubernetes.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer {return NewFilteredPodInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions)
}

创建Informer的同时创建indexer

func NewSharedIndexInformer(lw ListerWatcher, exampleObject runtime.Object, defaultEventHandlerResyncPeriod time.Duration, indexers Indexers) SharedIndexInformer {realClock := &clock.RealClock{}sharedIndexInformer := &sharedIndexInformer{processor:                       &sharedProcessor{clock: realClock},indexer:                         NewIndexer(DeletionHandlingMetaNamespaceKeyFunc, indexers),listerWatcher:                   lw,objectType:                      exampleObject,resyncCheckPeriod:               defaultEventHandlerResyncPeriod,defaultEventHandlerResyncPeriod: defaultEventHandlerResyncPeriod,cacheMutationDetector:           NewCacheMutationDetector(fmt.Sprintf("%T", exampleObject)),clock:                           realClock,}return sharedIndexInformer
}// NewIndexer returns an Indexer implemented simply with a map and a lock.
func NewIndexer(keyFunc KeyFunc, indexers Indexers) Indexer {return &cache{cacheStorage: NewThreadSafeStore(indexers, Indices{}),keyFunc:      keyFunc,}
}

除了创建key的函数不同,其他一模一样 ,但是解析delta队列确加了锁

func (s *sharedIndexInformer) HandleDeltas(obj interface{}) error {s.blockDeltas.Lock()defer s.blockDeltas.Unlock()if deltas, ok := obj.(Deltas); ok {return processDeltas(s, s.indexer, s.transform, deltas)}return errors.New("object given as Process argument is not Deltas")
}

实际上http请求而言,http response关闭后http的访问就结束了,本地加锁仅仅会影响本地的执行效率,api-server无影响

 

根源

从代码分析,两种写法没有区别,对API-Server造成的影响仅仅是Http response的解析,老式写法解析后直接返回,新式写法的意思是创建结构体,然后结构体去处理值,并带上了超时时间。

那么为什么API-Server觉得一次请求时间很长呢,比如List的过程(Watch是长轮询,不涉及请求时长),根源在于API-Server在低版本(测试版本1.20.x)分页参数会失效。笔者自己尝试的1.25.4分页是有效的。估计是中间某次提交修复了,笔者在github看到很多关于List的提交优化

还有

 

 

1.25.4的API-Server的List过程

func ListResource(r rest.Lister, rw rest.Watcher, scope *RequestScope, forceWatch bool, minRequestTimeout time.Duration) http.HandlerFunc {return func(w http.ResponseWriter, req *http.Request) {// For performance tracking purposes. 创建埋点trace := utiltrace.New("List", traceFields(req)...)namespace, err := scope.Namer.Namespace(req)if err != nil {scope.err(err, w, req)return}// Watches for single objects are routed to this function.// Treat a name parameter the same as a field selector entry.hasName := true_, name, err := scope.Namer.Name(req)if err != nil {hasName = false}ctx := req.Context()ctx = request.WithNamespace(ctx, namespace)outputMediaType, _, err := negotiation.NegotiateOutputMediaType(req, scope.Serializer, scope)if err != nil {scope.err(err, w, req)return}opts := metainternalversion.ListOptions{}if err := metainternalversionscheme.ParameterCodec.DecodeParameters(req.URL.Query(), scope.MetaGroupVersion, &opts); err != nil {err = errors.NewBadRequest(err.Error())scope.err(err, w, req)return}if errs := metainternalversionvalidation.ValidateListOptions(&opts); len(errs) > 0 {err := errors.NewInvalid(schema.GroupKind{Group: metav1.GroupName, Kind: "ListOptions"}, "", errs)scope.err(err, w, req)return}// transform fields// TODO: DecodeParametersInto should do this.if opts.FieldSelector != nil {fn := func(label, value string) (newLabel, newValue string, err error) {return scope.Convertor.ConvertFieldLabel(scope.Kind, label, value)}if opts.FieldSelector, err = opts.FieldSelector.Transform(fn); err != nil {// TODO: allow bad request to set field causes based on query parameterserr = errors.NewBadRequest(err.Error())scope.err(err, w, req)return}}if hasName {// metadata.name is the canonical internal name.// SelectionPredicate will notice that this is a request for// a single object and optimize the storage query accordingly.nameSelector := fields.OneTermEqualSelector("metadata.name", name)// Note that fieldSelector setting explicitly the "metadata.name"// will result in reaching this branch (as the value of that field// is propagated to requestInfo as the name parameter.// That said, the allowed field selectors in this branch are:// nil, fields.Everything and field selector matching metadata.name// for our name.if opts.FieldSelector != nil && !opts.FieldSelector.Empty() {selectedName, ok := opts.FieldSelector.RequiresExactMatch("metadata.name")if !ok || name != selectedName {scope.err(errors.NewBadRequest("fieldSelector metadata.name doesn't match requested name"), w, req)return}} else {opts.FieldSelector = nameSelector}}if opts.Watch || forceWatch {if rw == nil {scope.err(errors.NewMethodNotSupported(scope.Resource.GroupResource(), "watch"), w, req)return}// TODO: Currently we explicitly ignore ?timeout= and use only ?timeoutSeconds=.timeout := time.Duration(0)if opts.TimeoutSeconds != nil {timeout = time.Duration(*opts.TimeoutSeconds) * time.Second}if timeout == 0 && minRequestTimeout > 0 {timeout = time.Duration(float64(minRequestTimeout) * (rand.Float64() + 1.0))}klog.V(3).InfoS("Starting watch", "path", req.URL.Path, "resourceVersion", opts.ResourceVersion, "labels", opts.LabelSelector, "fields", opts.FieldSelector, "timeout", timeout)ctx, cancel := context.WithTimeout(ctx, timeout)defer cancel()watcher, err := rw.Watch(ctx, &opts)if err != nil {scope.err(err, w, req)return}requestInfo, _ := request.RequestInfoFrom(ctx)metrics.RecordLongRunning(req, requestInfo, metrics.APIServerComponent, func() {serveWatch(watcher, scope, outputMediaType, req, w, timeout)})return}// Log only long List requests (ignore Watch).defer trace.LogIfLong(500 * time.Millisecond) //超过500ms就埋点打印日志,这个埋点非常好用,建议使用trace.Step("About to List from storage")result, err := r.List(ctx, &opts) //API-Server实际上也是去ETCD取数据if err != nil {scope.err(err, w, req)return}trace.Step("Listing from storage done")defer trace.Step("Writing http response done", utiltrace.Field{"count", meta.LenList(result)})transformResponseObject(ctx, scope, trace, req, w, http.StatusOK, outputMediaType, result)}

可以看出超过500毫秒就会打印数据,笔者测试差不多500个pod的List就是差不多500毫秒少一点,Client-Go设计默认分页参数就是500条,😅精确设计。

// GetList implements storage.Interface.
func (s *store) GetList(ctx context.Context, key string, opts storage.ListOptions, listObj runtime.Object) error {preparedKey, err := s.prepareKey(key)if err != nil {return err}recursive := opts.RecursiveresourceVersion := opts.ResourceVersionmatch := opts.ResourceVersionMatchpred := opts.Predicatetrace := utiltrace.New(fmt.Sprintf("List(recursive=%v) etcd3", recursive),utiltrace.Field{"audit-id", endpointsrequest.GetAuditIDTruncated(ctx)},utiltrace.Field{"key", key},utiltrace.Field{"resourceVersion", resourceVersion},utiltrace.Field{"resourceVersionMatch", match},utiltrace.Field{"limit", pred.Limit},utiltrace.Field{"continue", pred.Continue})defer trace.LogIfLong(500 * time.Millisecond)listPtr, err := meta.GetItemsPtr(listObj)if err != nil {return err}v, err := conversion.EnforcePtr(listPtr)if err != nil || v.Kind() != reflect.Slice {return fmt.Errorf("need ptr to slice: %v", err)}

去读取ETCD3的数据,可以试试把k8s的低版本安装上debug试试。分析limit失效的原因,笔者是高版本的K8S,是已经修复版本。自定义的埋点List的代码

package mainimport ("context""fmt"v1 "k8s.io/api/core/v1""k8s.io/apimachinery/pkg/api/meta"metav1 "k8s.io/apimachinery/pkg/apis/meta/v1""k8s.io/apimachinery/pkg/fields""k8s.io/apimachinery/pkg/runtime""k8s.io/apimachinery/pkg/watch""k8s.io/client-go/kubernetes""k8s.io/client-go/tools/cache""k8s.io/client-go/tools/pager""k8s.io/utils/trace""time"
)func TimeNewFilteredPodInformer(client *kubernetes.Clientset) error {options := metav1.ListOptions{ResourceVersion: "0"}initTrace := trace.New("Reflector ListAndWatch", trace.Field{Key: "name", Value: r.name})defer initTrace.LogIfLong(1 * time.Millisecond)var list runtime.Objectvar paginatedResult boolvar err errorlistCh := make(chan struct{}, 1)panicCh := make(chan interface{}, 1)go func() {defer func() {if r := recover(); r != nil {panicCh <- r}}()// Attempt to gather list in chunks, if supported by listerWatcher, if not, the first// list request will return the full response.pager := pager.New(pager.SimplePageFunc(func(opts metav1.ListOptions) (runtime.Object, error) {lw := &cache.ListWatch{ListFunc: func(options metav1.ListOptions) (runtime.Object, error) {return client.CoreV1().Pods(v1.NamespaceAll).List(context.TODO(), options)},WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) {return client.CoreV1().Pods(v1.NamespaceAll).Watch(context.TODO(), options)},}return lw.List(opts)}))list, paginatedResult, err = pager.List(context.Background(), options)initTrace.Step("Objects listed: ")fmt.Println("list END, is pager ", paginatedResult)if err != nil {fmt.Println("error is : ", err.Error())}close(listCh)}()select {case r := <-panicCh:panic(r)case <-listCh:}initTrace.Step("Resource version extracted")items, err := meta.ExtractList(list)fmt.Println("list items size is : ", len(items))if err != nil {return fmt.Errorf("unable to understand list result %#v (%v)", list, err)}initTrace.Step("Objects extracted")return nil
}func TimeNewIndexerInformer(client *kubernetes.Clientset) error {options := metav1.ListOptions{ResourceVersion: "0"}initTrace := trace.New("Reflector ListAndWatch", trace.Field{Key: "name", Value: r.name})defer initTrace.LogIfLong(1 * time.Millisecond)var list runtime.Objectvar paginatedResult boolvar err errorlistCh := make(chan struct{}, 1)panicCh := make(chan interface{}, 1)go func() {defer func() {if r := recover(); r != nil {panicCh <- r}}()// Attempt to gather list in chunks, if supported by listerWatcher, if not, the first// list request will return the full response.pager := pager.New(pager.SimplePageFunc(func(opts metav1.ListOptions) (runtime.Object, error) {lw := cache.NewListWatchFromClient(client.CoreV1().RESTClient(), "pods", v1.NamespaceAll, fields.Everything())return lw.List(opts)}))list, paginatedResult, err = pager.List(context.Background(), options)initTrace.Step("Objects listed: ")fmt.Println("list END, is pager ", paginatedResult)if err != nil {fmt.Println("error is : ", err.Error())}close(listCh)}()select {case r := <-panicCh:panic(r)case <-listCh:}initTrace.Step("Resource version extracted")items, err := meta.ExtractList(list)fmt.Println("list items size is : ", len(items))if err != nil {return fmt.Errorf("unable to understand list result %#v (%v)", list, err)}initTrace.Step("Objects extracted")return nil
}

trace的包好用,这里使用的k8s的包,实际上sdk基础包也有相似的功能。

func (t *Trace) durationIsWithinThreshold() bool {if t.endTime == nil { // we don't assume incomplete traces meet the thresholdreturn false}return t.threshold == nil || *t.threshold == 0 || t.endTime.Sub(t.startTime) >= *t.threshold
}

总结

知其然知其所以然,要想知道为什么分页不生效,需要自定义API-Server debug才行,看代码很难看出原因,因为K8S实际上估计设计的时候也考虑过这个。

相关文章:

API-Server的监听器Controller的List分页失效

前言 最近做项目&#xff0c;还是K8S的插件监听器&#xff08;理论上插件都是通过API-server通信&#xff09;&#xff0c;官方的不同写法居然都能出现争议&#xff0c;争议点就是对API-Server的请求的耗时&#xff0c;说是会影响API-Server。实际上通过源码分析两着有差别&am…...

jupyter notebook 进阶使用:nbextensions,终极避坑

jupyter notebook 进阶使用&#xff1a;nbextensions&#xff0c;终极避坑吐槽安装 jupyter_contrib_nbextensions1. Install the python package&#xff08;安装python包&#xff09;方法一&#xff0c;PIP&#xff1a;方法二&#xff0c;Conda&#xff08;推荐&#xff09;&…...

C 语言编程 — Doxygen + Graphviz 静态项目分析

目录 文章目录目录安装配置解析Project related configuration optionsBuild related configuration optionsConfiguration options related to warning and progress messagesConfiguration options related to the input filesConfiguration options related to source brows…...

Mybatis报BindingException:Invalid bound statement (not found)异常

一、前言 本文的mybatis是与springboot整合时出现的异常&#xff0c;若使用的不是基于springboot&#xff0c;解决思路也大体一样的。 二、从整合mybatis的三个步骤排查问题 但在这之前&#xff0c;我们先要知道整合mybatis的三个重要的工作&#xff0c;如此才能排查&#x…...

HttpRunner3.x(1)-框架介绍

HttpRunner 是一款面向 HTTP(S) 协议的通用测试框架&#xff0c;只需编写维护一份 YAML/JSON 脚本&#xff0c;即可实现自动化测试、性能测试、线上监控、持续集成等多种测试需求。主要特征继承的所有强大功能requests &#xff0c;只需以人工方式获得乐趣即可处理HTTP&#xf…...

pytest学习和使用20-pytes如何进行分布式测试?(pytest-xdist)

20-pytes如何进行分布式测试&#xff1f;&#xff08;pytest-xdist&#xff09;1 什么是分布式测试&#xff1f;2 为什么要进行分布式测试&#xff1f;2.1 场景1&#xff1a;自动化测试场景2.2 场景2&#xff1a;性能测试场景3 分布式测试有什么特点&#xff1f;4 分布式测试关…...

三、Python 操作 MongoDB ----非 ODM

文章目录一、连接器的安装和配置二、新增文档三、查询文档四、更新文档五、删除文档一、连接器的安装和配置 pymongo&#xff1a; MongoDB 官方提供的 Python 工具包。官方文档&#xff1a; https://pymongo.readthedocs.io/en/stable/ pip安装&#xff0c;命令如下&#xff1…...

求最大公约数和最小公倍数---辗转相除法(欧几里得算法)

目录 一.GCD和LCM 1.最大公约数 2.最小公倍数 二.暴力求解 1.最大公约数 2.最小公倍数 三.辗转相除法 1.最大公约数 2.最小公倍数 一.GCD和LCM 1.最大公约数 最大公约数&#xff08;Greatest Common Divisor&#xff0c;简称GCD&#xff09;指的是两个或多个整数共有…...

音视频开发_获取媒体文件的详细信息

一、前言 做音视频开发过程中,经常需要获取媒体文件的详细信息。 比如:获取视频文件的总时间、帧率、尺寸、码率等等信息。 获取音频文件的的总时间、帧率、码率,声道等信息。 这篇文章贴出2个我封装好的函数,直接调用就能获取媒体信息返回,copy过去就能使用,非常方便。…...

Springboot集成Swagger

一、Swagger简介注意点&#xff01; 在正式发布的时候要关闭swagger&#xff08;出于安全考虑&#xff0c;而且节省内存空间&#xff09;之前开发的时候&#xff0c;前端只用管理静态页面&#xff0c; http请求到后端&#xff0c; 模板引擎JSP&#xff0c;故后端是主力如今是前…...

Vue全新一代状态管理库 Pinia【一篇通】

文章目录前言1. Pinia 是什么&#xff1f;1.1 为什么取名叫 Pinia?1.2. 为什么要使用 Pinia ?2. 安装 Pinia2.1.创建 Store2.1.1. Option 类型 Store2.1.2 Setup 函数类型 Store2.1.3 模板中使用3. State 的使用事项&#xff08;Option Store &#xff09;3.1 读取 State3.2 …...

STM32 -4 关于STM32的RAM、ROM

一 stm32 的flash是什么、有什么用、注意事项、如何查看 一 、说明 它主要用于存储代码&#xff0c;FLASH 存储器的内容在掉电后不会丢失&#xff0c;STM32 芯片在运行的时候&#xff0c;也能对自身的内部 FLASH 进行读写&#xff0c;因此&#xff0c;若内部 FLASH 存储了应用…...

第一个 Qt 程序

第一个 Qt 程序 “hello world ”的起源要追溯到 1972 年&#xff0c;贝尔实验室著名研究员 Brian Kernighan 在撰写 “B 语言教程与指导(Tutorial Introduction to the Language B)”时初次使用&#xff08;程序&#xff09;&#xff0c;这是目前已 知最早的在计算机著作中将…...

Spring注解驱动开发--AOP底层原理

Spring注解驱动开发–AOP底层原理 21. AOP-AOP功能测试 AOP&#xff1a;【动态代理】 指在程序运行期间动态的将某段代码切入到指定方法指定位置进行运行的编程方式&#xff1b; 1、导入aop模块&#xff1a;Spring AOP&#xff0c;(Spring-aspects) 2、定义一个业务逻辑类(Ma…...

对象的动态创建和销毁以及对象的复制,赋值

&#x1f436;博主主页&#xff1a;ᰔᩚ. 一怀明月ꦿ ❤️‍&#x1f525;专栏系列&#xff1a;线性代数&#xff0c;C初学者入门训练&#xff0c;题解C&#xff0c;C的使用文章,「初学」C​​​​​​​ &#x1f525;座右铭&#xff1a;“不要等到什么都没有了&#xff0c;才…...

JVM调优,调的是什么?目的是什么?

文章目录前言一、jvm是如何运行代码的&#xff1f;二、jvm的内存模型1 整体内存模型结构图2 堆中的年代区域划分3 对象在内存模型中是如何流转的?4 什么是FULL GC,STW? 为什么会发生FULL GC?5 要调优,首先要知道有哪些垃圾收集器及哪些算法6 调优不是盲目的,要有依据,几款内…...

docker部署zabbix监控

docker部署zabbix监控 1、环境说明 公有云ubuntu22.04 系统->部署docker环境zabbix-server 6.4 2、准备docker环境 更新apt以及安装一些必要的系统工具 sudo apt-get update sudo apt-get -y install apt-transport-https ca-certificates curl software-properties-co…...

C语言刷题(6)(猜名次)——“C”

各位CSDN的uu们你们好呀&#xff0c;今天&#xff0c;小雅兰还是在复习噢&#xff0c;今天来给大家介绍一个有意思的题目 题目名称&#xff1a; 猜名次 题目内容&#xff1a; 5位运动员参加了10米台跳水比赛&#xff0c;有人让他们预测比赛结果&#xff1a; A选…...

两年外包生涯,感觉自己废了一半....

先说一下自己的情况。大专生&#xff0c;17年通过校招进入湖南某软件公司&#xff0c;干了接近2年的点点点&#xff0c;今年年上旬&#xff0c;感觉自己不能够在这样下去了&#xff0c;长时间呆在一个舒适的环境会让一个人堕落&#xff01;而我已经在一个企业干了五年的功能测试…...

【python】喜欢XJJ?这不得来一波大采集?

前言 大家早好、午好、晚好吖 ❤ ~欢迎光临本文章 俗话说的好&#xff1a;技能学了~就要用在自己喜欢得东西上&#xff01;&#xff01; 这我不得听个话~我喜欢小姐姐&#xff0c;跳舞的小姐姐 这不得用python把小姐姐舞采集下来~嘿嘿嘿 完整源码、素材皆可点击文章下方名片…...

公司测试员用例写得乱七八糟,测试总监制定了这份《测试用例编写规范》

统一测试用例编写的规范&#xff0c;为测试设计人员提供测试用例编写的指导&#xff0c;提高编写的测试用例的可读性&#xff0c;可执行性、合理性。为测试执行人员更好执行测试&#xff0c;提高测试效率&#xff0c;最终提高公司整个产品的质量。 一、范围 适用于集成测试用…...

LeetCode 热题 HOT 100【题型归类汇总,助力刷题】

介绍 对于算法题&#xff0c;按题型类别刷题才会更有成效&#xff0c;因此我这里在网上搜索并参考了下 “&#x1f525; LeetCode 热题 HOT 100” 的题型归类&#xff0c;并在其基础上做了一定的完善&#xff0c;希望能够记录自己的刷题历程&#xff0c;有所收获&#xff01;具…...

【Java进阶篇】—— File类与IO流

一、File类的使用 1.1 概述 File 类以及本章中的各种流都定义在 java.io 包下 一个File对象代表硬盘或网络中可能存在的一个文件或文件夹&#xff08;文件目录&#xff09; File 能新建、删除、重命名 文件和目录&#xff0c;但 File不能访问文件内容本身。如果我们想要访问…...

Mysql 竟然还有这么多不为人知的查询优化技巧,还不看看?

前言 Mysql 我随手造200W条数据&#xff0c;给你们讲讲分页优化 MySql 索引失效、回表解析 今天再聊聊一些我想分享的查询优化相关点。 正文 准备模拟数据。 首先是一张 test_orde 表&#xff1a; CREATE TABLE test_order (id INT(11) NOT NULL AUTO_INCREMENT,p_sn VARCHA…...

MATLAB算法实战应用案例精讲-【智能优化算法】海洋捕食者算法(MPA) (附MATLAB和python代码实现)

目录 前言 知识储备 Lvy 飞行 布朗运动 算法原理 算法思想 数学模型...

Spring @Profile

1. Overview In this tutorial, we’ll focus on introducing Profiles in Spring. Profiles are a core feature of the framework — allowing us to map our beans to different profiles — for example, dev, test, and prod. We can then activate different profiles…...

Vue3电商项目实战-个人中心模块4【09-订单管理-列表渲染、10-订单管理-条件查询】

文章目录09-订单管理-列表渲染10-订单管理-条件查询09-订单管理-列表渲染 目的&#xff1a;完成订单列表默认渲染。 大致步骤&#xff1a; 定义API接口函数抽取单条订单组件获取数据进行渲染 落的代码&#xff1a; 1.获取订单列表API借口 /*** 查询订单列表* param {Number…...

【十二天学java】day01-Java基础语法

day01 - Java基础语法 1. 人机交互 1.1 什么是cmd&#xff1f; 就是在windows操作系统中&#xff0c;利用命令行的方式去操作计算机。 我们可以利用cmd命令去操作计算机&#xff0c;比如&#xff1a;打开文件&#xff0c;打开文件夹&#xff0c;创建文件夹等。 1.2 如何打…...

【面试题】闭包是什么?this 到底指向谁?

一通百通&#xff0c;其实函数执行上下文、作用域链、闭包、this、箭头函数是相互关联的&#xff0c;他们的特性并不是孤立的&#xff0c;而是相通的。因为内部函数可以访问外层函数的变量&#xff0c;所以才有了闭包的现象。箭头函数内没有 this 和 arguments&#xff0c;所以…...

汽车4S店业务管理软件

一、产品简介  它主要提供给汽车4S商店&#xff0c;用于管理各种业务&#xff0c;如汽车销售、售后服务、配件、精品和保险。整个系统以客户为中心&#xff0c;以财务为基础&#xff0c;覆盖4S商店的每一个业务环节&#xff0c;不仅可以提高服务效率和客户满意度&#xff0c;…...

个人档案网站该怎么做/外贸推广优化公司

题目描述&#xff1a; 题目一&#xff1a;寻找数组重复的数字 在一个长度为n的数组里面所有的数字都在0——n-1的范围内。数组中某些数字是重复的&#xff0c;但是不知道有几个数字重复了&#xff0c;也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。例如&…...

怎样做投资与理财网站/腾讯域名注册官网

运行内部类的main方法定义两个线程&#xff1a;一个线程的名字“thread1”,线程功能输出1~10的阶乘。另一个线程的名字“thread2”,线程功能输出线程的名字10次。public class T1 extends Thread {public T1(String s) {super(s);}public void run() {int sum1;for(int i1;i<…...

黑彩网站怎么建设/怎么开网站平台挣钱

版权所有&#xff0c;转载请说明转自 http://my.csdn.net/weiqing1981127 二&#xff0e;内核代码 2.1输入子系统设备驱动层 我们先从设备驱动层进行讲解 首先设备驱动层调用input_allocate_device进行申请input_dev结构体&#xff0c;接着对该结构体进行赋值&#xff0c;然…...

网站建设分为那几个模块/seozou是什么意思

如果你在周末、有WIFI的房间里不知道做什么&#xff0c;不如学下Python吧。有了它&#xff0c;你可以什么都不需要&#xff01; 基础需求篇&#xff1a;温饱与空虚 躺着赚钱 一位匿名知乎网友爆料用Python写了自动化交易程序&#xff0c;2年躺着赚了200万&#xff01;相当于普通…...

wap网站开发视频教程/数据分析软件

最近开发的C项目中需要调用外部C#写的DLL驱动&#xff0c;网上有许多教程&#xff0c;可参考我前面转载的一篇blog。 程序调用成功后需要进行数据的转换&#xff0c;通过查找MSDN文档找到可调用Marshal类来实现。其中System::String转换成standed string可用StringToHGlobalAn…...

建设部企业资质查询/北京seo培训机构

任务 1.通过连接数据库完成用户登录模块。 2.登录成功后查询出一张数据库表中的内容&#xff1b;登录不成功返回登录页面。 3.页面面端要有空值和非法字符验证。 4.登录成功后对一张表中数据进行增加、删除、修改和查询操作。 代码 数据库相关代码 创建数据库 名字为jdbcH…...