K8s集群稳定性:LIST请求源码分析、性能评估与大规模基础服务部署调优

原创 Linux阅码场 2022-10-21 08:00

作者简介

赵亚楠,携程资深架构师,负责携程云平台网络虚拟化、云原生安全、内核等基础设施研发工作。

简介

对于非结构化的数据存储系统来说,LIST 操作通常都是非常重量级的,不仅占用大量的 磁盘 IO、网络带宽和 CPU,而且会影响同时间段的其他请求(尤其是响应延迟要求极高的 选主请求),是集群稳定性的一大杀手。

例如,对于 Ceph 对象存储来说,每个 LIST bucket 请求都需要去多个磁盘中捞出这个 bucket 的全部数据;不仅自身很慢,还影响了同一时间段内的其他普通读写请求,因为 IO 是共享的,导致响应延迟上升乃至超时。如果 bucket 内的对象非常多(例如用作 harbor/docker-registry 的存储后端),LIST 操作甚至都无法在常规时间内完成( 因而依赖 LIST bucket 操作的 registry GC 也就跑不起来)。

又如 KV 存储 etcd。相比于 Ceph,一个实际 etcd 集群存储的数据量可能很小(几个 ~ 几十个 GB),甚至足够缓存到内存中。但与 Ceph 不同的是,它的并发请求数量可能会高 几个量级,比如它是一个 ~4000 nodes 的 k8s 集群的 etcd。单个 LIST 请求可能只需要 返回几十 MB 到上 GB 的流量,但并发请求一多,etcd 显然也扛不住,所以最好在前面有 一层缓存,这就是 apiserver 的功能(之一)。K8s 的 LIST 请求大部分都应该被 apiserver 挡住,从它的本地缓存提供服务,但如果使用不当,就会跳过缓存直接到达 etcd,有很大的稳定性风险。

本文深入研究 k8s apiserver/etcd 的 LIST 操作处理逻辑和性能瓶颈,并提供一些基础服务的 LIST 压力测试、 部署和调优建议,提升大规模 K8s 集群的稳定性。

kube-apiserver LIST 请求处理逻辑:


代码基于 v1.24.0,不过 1.19~1.24 的基本逻辑和代码路径是一样的,有需要可对照参考。

1 引言

1.1 K8s 架构:环形层次视图

从架构层次和组件依赖角度,可以将一个 K8s 集群和一台 Linux 主机做如下类比:


Fig 1. Anology: a Linux host and a Kubernetes cluster

对于 K8s 集群,从内到外的几个组件和功能:

  1. etcd:持久化 KV 存储,集群资源(pods/services/networkpolicies/…)的唯一的权威数据(状态)源;

  2. apiserver:从 etcd 读取(ListWatch)全量数据,并缓存在内存中;无状态服务,可水平扩展;

  3. 各种基础服务(e.g. kubelet、*-agent、*-operator):连接apiserver,获取(List/ListWatch)各自需要的数据;

  4. 集群内的 workloads:在 1 和 2 正常的情况下由 3 来创建、管理和 reconcile,例如 kubelet 创建 pod、cilium 配置网络和安全策略。

1.2 apiserver/etcd 角色

以上可以看到,系统路径中存在两级 List/ListWatch(但数据是同一份):

  1. apiserver List/ListWatch etcd

  2. 基础服务 List/ListWatch apiserver

因此,从最简形式上来说,apiserver 就是挡在 etcd 前面的一个代理(proxy),

           +--------+              +---------------+                 +------------+
| Client | -----------> | Proxy (cache) | --------------> | Data store |
+--------+ +---------------+ +------------+

infra services apiserver etcd
  1. 绝大部分情况下,apiserver 直接从本地缓存提供服务(因为它缓存了集群全量数据);

  2. 某些特殊情况,例如,

    apiserver 就只能将请求转发给 etcd —— 这里就要特别注意了 —— 客户端 LIST 参数设置不当也可能会走到这个逻辑。

    a.客户端明确要求从 etcd 读数据(追求最高的数据准确性)

    b.apiserver 本地缓存还没建好

apiserver 就只能将请求转发给 etcd —— 这里就要特别注意了 —— 客户端 LIST 参数设置不当也可能会走到这个逻辑。

1.3 apiserver/etcd List 开销

1.3.1 请求举例

考虑下面几个 LIST 操作:

1.LIST apis/cilium.io/v2/ciliumendpoints?limit=500&resourceVersion = 0

这里同时传了两个参数,但 resourceVersion=0 会导致 apiserver 忽略 limit=500, 所以客户端拿到的是全量 ciliumendpoints 数据。

一种资源的全量数据可能是比较大的,需要考虑清楚是否真的需要全量数据。后文会介绍定量测量与分析方法。

2.LIST api/v1/pods?filedSelector=spec.nodeName%3Dnode1

这个请求是获取 node1 上的所有 pods(%3D 是 = 的转义)。

根据 nodename 做过滤,给人的感觉可能是数据量不太大,但其实背后要比看上去复杂:

这种行为是要避免的,除非对数据准确性有极高要求,特意要绕过 apiserver 缓存。

    • 首先,这里没有指定 resourceVersion=0,导致 apiserver 跳过缓存,直接去 etcd 读数据

    • 其次,etcd 只是 KV 存储,没有按 label/field 过滤功能(只处理 limit/continue),

    • 所以,apiserver 是从 etcd 拉全量数据,然后在内存做过滤,开销也是很大的,后文有代码分析。

    3.LISTapi/v1/pods?filedSelector=spec.nodeName%3Dnode1&resourceVersion = 0

    跟 2 的区别是加上了 resourceVersion=0,因此 apiserver 会从缓存读数据,性能会有量级的提升

    但要注意,虽然实际上返回给客户端的可能只有几百 KB 到上百 MB(取决于 node 上 pod 的数量、pod 上 label 的多少等因素), 但 apiserver 需要处理的数据量可能是几个 GB。后面会有定量分析。

    以上可以看到,不同的 LIST 操作产生的影响是不一样的,而客户端看到数据还有可能只 是 apiserver/etcd 处理数据的很小一部分。如果基础服务大规模启动或重启, 就极有可能把控制平面打爆。

    1.3.2 处理开销

    List 请求可以分为两种:

    1. List 全量数据:开销主要花在数据传输;

    2. 指定用 label 或字段(field)过滤,只需要匹配的数据。

    这里需要特别说明的是第二种情况,也就是 list 请求带了过滤条件。

    • 大部分情况下,apiserver 会用自己的缓存做过滤,这个很快,因此耗时主要花在数据传输

    • 需要将请求转给 etcd 的情况,

      前面已经提到,etcd 只是 KV 存储,并不理解 label/field 信息,因此它无法处理过滤请求。实际的过程是:apiserver 从 etcd 拉全量数据,然后在内存做过滤,再返回给客户端。

      因此除了数据传输开销(网络带宽),这种情况下还会占用大量apiserver CPU 和内存

    1.4 大规模部署时潜在的问题

    再来看个例子,下面这行代码用 k8s client-go 根据 nodename 过滤 pod,

        podList, err := Client().CoreV1().Pods("").List(ctx(), ListOptions{FieldSelector: "spec.nodeName=node1"})


    看起来非常简单的操作,我们来实际看一下它背后的数据量。以一个 4000 node,10w pod 的集群为例,全量 pod 数据量

    1. etcd 中:紧凑的非结构化 KV 存储,在 1GB 量级

    2. apiserver 缓存中:已经是结构化的 golang objects,在 2GB 量级( TODO:需进一步确认);

    3. apiserver 返回:client 一般选择默认的 json 格式接收, 也已经是结构化数据。全量 pod 的 json 也在 2GB 量级

    可以看到,某些请求看起来很简单,只是客户端一行代码的事情,但背后的数据量是惊人的。指定按 nodeName 过滤 pod 可能只返回了 500KB 数据,但 apiserver 却需要过滤 2GB 数据 —— 最坏的情况,etcd 也要跟着处理 1GB 数据 (以上参数配置确实命中了最坏情况,见下文代码分析)。

    集群规模比较小的时候,这个问题可能看不出来(etcd 在 LIST 响应延迟超过某个阈值 后才开始打印 warning 日志);规模大了之后,如果这样的请求比较多,apiserver/etcd 肯定是扛不住的。

    1.5 本文目的

    通过深入代码查看 k8s 的 List/ListWatch 实现,加深对性能问题的理解,对大规模 K8s 集群的稳定性优化提供一些参考。

    2 apiserver List() 操作源码分析

    有了以上理论预热,接下来可以看代码实现了。

    2.1 调用栈和流程图

    store.List
    |-store.ListPredicate
    |-if opt == nil
    | opt = ListOptions{ResourceVersion: ""}
    |-Init SelectionPredicate.Limit/Continue fileld
    |-list := e.NewListFunc() // objects will be stored in this list
    |-storageOpts := storage.ListOptions{opt.ResourceVersion, opt.ResourceVersionMatch, Predicate: p}
    |
    |-if MatchesSingle ok // 1. when "metadata.name" is specified, get single obj
    | // Get single obj from cache or etcd
    |
    |-return e.Storage.List(KeyRootFunc(ctx), storageOpts) // 2. get all objs and perform filtering
    |-cacher.List()
    | // case 1: list all from etcd and filter in apiserver
    |-if shouldDelegateList(opts) // true if resourceVersion == ""
    | return c.storage.List // list from etcd
    | |- fromRV *int64 = nil
    | |- if len(storageOpts.ResourceVersion) > 0
    | | rv = ParseResourceVersion
    | | fromRV = &rv
    | |
    | |- for hasMore {
    | | objs := etcdclient.KV.Get()
    | | filter(objs) // filter by labels or filelds
    | | }
    |
    | // case 2: list & filter from apiserver local cache (memory)
    |-if cache.notready()
    | return c.storage.List // get from etcd
    |
    | // case 3: list & filter from apiserver local cache (memory)
    |-obj := watchCache.WaitUntilFreshAndGet
    |-for elem in obj.(*storeElement)
    | listVal.Set() // append results to listOjb
    |-return // results stored in listObj

    对应的流程图:

    Fig 2-1. List operation processing in apiserver

    2.2 请求处理入口:List()

    // https://github.com/kubernetes/kubernetes/blob/v1.24.0/staging/src/k8s.io/apiserver/pkg/registry/generic/registry/store.go#L361

    // 根据 PredicateFunc 中指定的 LabelSelector 和 FieldSelector 过滤,返回一个对象列表
    func (e *Store) List(ctx, options *metainternalversion.ListOptions) (runtime.Object, error) {
    label := labels.Everything()
    if options != nil && options.LabelSelector != nil
    label = options.LabelSelector // Label 过滤器,例如 app=nginx

    field := fields.Everything()
    if options != nil && options.FieldSelector != nil
    field = options.FieldSelector // 字段过滤器,例如 spec.nodeName=node1

    out := e.ListPredicate(ctx, e.PredicateFunc(label, field), options) // 拉取(List)数据并过滤(Predicate)
    if e.Decorator != nil
    e.Decorator(out)

    return out, nil
    }

    2.3 ListPredicate()

    // https://github.com/kubernetes/kubernetes/blob/v1.24.0/staging/src/k8s.io/apiserver/pkg/registry/generic/registry/store.go#L411

    func (e *Store) ListPredicate(ctx , p storage.SelectionPredicate, options *metainternalversion.ListOptions) (runtime.Object, error) {
    // Step 1: 初始化
    if options == nil
    options = &metainternalversion.ListOptions{ResourceVersion: ""}

    p.Limit = options.Limit
    p.Continue = options.Continue
    list := e.NewListFunc() // 返回结果将存储在这里面
    storageOpts := storage.ListOptions{ // 将 API 侧的 ListOption 转成底层存储侧的 ListOption,字段区别见下文
    ResourceVersion: options.ResourceVersion,
    ResourceVersionMatch: options.ResourceVersionMatch,
    Predicate: p,
    Recursive: true,
    }

    // Step 2:如果请求指定了 metadata.name,则应获取单个 object,无需对全量数据做过滤
    if name, ok := p.MatchesSingle(); ok { // 检查是否设置了 metadata.name 字段
    if key := e.KeyFunc(ctx, name); err == nil { // 获取这个 object 在 etcd 中的 key(唯一或不存在)
    storageOpts.Recursive = false
    e.Storage.GetList(ctx, key, storageOpts, list)
    return list
    }
    // else 逻辑:如果执行到这里,说明没有从 context 中拿到过滤用的 key,则 fallback 到下面拿全量数据再过滤
    }

    // Step 3: 对全量数据做过滤
    e.Storage.GetList(ctx, e.KeyRootFunc(), storageOpts, list) // KeyRootFunc() 用来获取这种资源在 etcd 里面的 root key(即 prefix,不带最后的 /)
    return list
    }


    1.24.0 中 case 1 & 2 都是 调用 e.Storage.GetList(),之前的版本有点不同:

    • Case 1 中的 e.Storage.GetToList

    • Case 1 中的 e.Storage.List

    不过基本流程是一样的。

    1. 果客户端没传 ListOption,则初始化一个默认值,其中的 ResourceVersion设置为空字符串, 这将使 apiserver 从 etcd 拉取数据来返回给客户端,而不使用本地缓存(除非本地缓存还没有建好);

    举例,客户端设置 ListOption{Limit: 5000, ResourceVersion: 0} list ciliumendpoints 时,发送的请求将为 /apis/cilium.io/v2/ciliumendpoints?limit=500&resourceVersion=0

    ResourceVersion 为空字符串的行为,后面会看到对它的解析。

    2.用 listoptions 中的字段分别初始化过滤器(SelectionPredicate)的 limit/continue 字段;.

    3.初始化返回结果,list := e.NewListFunc()

    4.将 API 侧的 ListOption 转成底层存储的 ListOption,字段区别见下文

    metainternalversion.ListOptions 是 API 侧的结构体,包含了

     // staging/src/k8s.io/apimachinery/pkg/apis/meta/internalversion/types.go

    // ListOptions is the query options to a standard REST list call.
    type ListOptions struct {
    metav1.TypeMeta

    LabelSelector labels.Selector // 标签过滤器,例如 app=nginx
    FieldSelector fields.Selector // 字段过滤器,例如 spec.nodeName=node1

    Watch bool
    AllowWatchBookmarks bool
    ResourceVersion string
    ResourceVersionMatch metav1.ResourceVersionMatch

    TimeoutSeconds *int64 // Timeout for the list/watch call.
    Limit int64
    Continue string // a token returned by the server. return a 410 error if the token has expired.
    }

    storage.ListOptions 是传给底层存储的结构体,字段有一些区别:

     // staging/src/k8s.io/apiserver/pkg/storage/interfaces.go

    // ListOptions provides the options that may be provided for storage list operations.
    type ListOptions struct {
    ResourceVersion string
    ResourceVersionMatch metav1.ResourceVersionMatch
    Predicate SelectionPredicate // Predicate provides the selection rules for the list operation.
    Recursive bool // true: 根据 key 获取单个对象;false:根据 key prefix 获取全量数据
    ProgressNotify bool // storage-originated bookmark, ignored for non-watch requests.
    }


    2.4请求指定了资源名(resource name):获取单个对象

    接下来根据请求中是否指定了 meta.Name 分为两种情况:

    1. 如果指定了,说明是查询单个对象,因为 Name 是唯一的,接下来转入查询单个 object 的逻辑;

    2. 如果未指定,则需要获取全量数据,然后在 apiserver 内存中根据 SelectionPredicate 中的过滤条件进行过滤,将最终结果返回给客户端;

    代码如下:

        // case 1:根据 metadata.name 获取单个 object,无需对全量数据做过滤
    if name, ok := p.MatchesSingle(); ok { // 检查是否设置了 metadata.name 字段
    if key := e.KeyFunc(ctx, name); err == nil {
    e.Storage.GetList(ctx, key, storageOpts, list)
    return list
    }
    // else 逻辑:如果执行到这里,说明没有从 context 中拿到过滤用的 key,则 fallback 到下面拿全量数据再过滤
    }

    e.Storage 是一个 Interface,

    // staging/src/k8s.io/apiserver/pkg/storage/interfaces.go

    // Interface offers a common interface for object marshaling/unmarshaling operations and
    // hides all the storage-related operations behind it.
    type Interface interface {
    Create(ctx , key string, obj, out runtime.Object, ttl uint64) error
    Delete(ctx , key string, out runtime.Object, preconditions *Preconditions,...)
    Watch(ctx , key string, opts ListOptions) (watch.Interface, error)
    Get(ctx , key string, opts GetOptions, objPtr runtime.Object) error

    // unmarshall objects found at key into a *List api object (an object that satisfies runtime.IsList definition).
    // If 'opts.Recursive' is false, 'key' is used as an exact match; if is true, 'key' is used as a prefix.
    // The returned contents may be delayed, but it is guaranteed that they will
    // match 'opts.ResourceVersion' according 'opts.ResourceVersionMatch'.
    GetList(ctx , key string, opts ListOptions, listObj runtime.Object) error


    e.Storage.GetList() 会执行到 cacher 代码。

    不管是获取单个 object,还是获取全量数据,都经历类似的过程:

    1. 优先从 apiserver 本地缓存获取(决定因素包括 ResourceVersion 等),

    2. 不得已才到 etcd 去获取;

    获取单个对象的逻辑相对比较简单,这里就不看了。接下来看 List 全量数据再做过滤的逻辑。

    2.5 请求未指定资源名,获取全量数据做过滤

    2.5.1 apiserver 缓存层:GetList() 处理逻辑

    // https://github.com/kubernetes/kubernetes/blob/v1.24.0/staging/src/k8s.io/apiserver/pkg/storage/cacher/cacher.go#L622

    // GetList implements storage.Interface
    func (c *Cacher) GetList(ctx , key string, opts storage.ListOptions, listObj runtime.Object) error {
    recursive := opts.Recursive
    resourceVersion := opts.ResourceVersion
    pred := opts.Predicate

    // 情况一:ListOption 要求必须从 etcd 读
    if shouldDelegateList(opts)
    return c.storage.GetList(ctx, key, opts, listObj) // c.storage 指向 etcd

    // If resourceVersion is specified, serve it from cache.
    listRV := c.versioner.ParseResourceVersion(resourceVersion)

    // 情况二:apiserver 缓存未建好,只能从 etcd 读
    if listRV == 0 && !c.ready.check()
    return c.storage.GetList(ctx, key, opts, listObj)

    // 情况三:apiserver 缓存正常,从缓存读:保证返回的 objects 版本不低于 `listRV`
    listPtr := meta.GetItemsPtr(listObj)
    listVal := conversion.EnforcePtr(listPtr)
    filter := filterWithAttrsFunction(key, pred) // 最终的过滤器

    objs, readResourceVersion, indexUsed := c.listItems(listRV, key, pred, ...) // 根据 index 预筛,性能优化
    for _, obj := range objs {
    elem := obj.(*storeElement)
    if filter(elem.Key, elem.Labels, elem.Fields) // 真正的过滤
    listVal.Set(reflect.Append(listVal, reflect.ValueOf(elem))
    }

    // 更新最后一次读到的 ResourceVersion
    if c.versioner != nil
    c.versioner.UpdateList(listObj, readResourceVersion, "", nil)
    return nil
    }

    2.5.2 判断是否必须从 etcd 读数据:shouldDelegateList()

    // https://github.com/kubernetes/kubernetes/blob/v1.24.0/staging/src/k8s.io/apiserver/pkg/storage/cacher/cacher.go#L591

    func shouldDelegateList(opts storage.ListOptions) bool {
    resourceVersion := opts.ResourceVersion
    pred := opts.Predicate
    pagingEnabled := DefaultFeatureGate.Enabled(features.APIListChunking) // 默认是启用的
    hasContinuation := pagingEnabled && len(pred.Continue) > 0 // Continue 是个 token
    hasLimit := pagingEnabled && pred.Limit > 0 && resourceVersion != "0" // 只有在 resourceVersion != "0" 的情况下,hasLimit 才有可能为 true

    // 1. 如果未指定 resourceVersion,从底层存储(etcd)拉去数据;
    // 2. 如果有 continuation,也从底层存储拉数据;
    // 3. 只有 resourceVersion != "0" 时,才会将 limit 传给底层存储(etcd),因为 watch cache 不支持 continuation
    return resourceVersion == "" || hasContinuation || hasLimit || opts.ResourceVersionMatch == metav1.ResourceVersionMatchExact
    }


    这里非常重要:

    1. 问:客户端未设置 ListOption{} 中的 ResourceVersion 字段,是否对应到这里的 resourceVersion == ""?

      答:是的,所以第一节的 例子 会导致从 etcd 拉全量数据。

    2. 问:客户端设置了 limit=500&resourceVersion=0 是否会导致下次 hasContinuation==true?

      答:不会,resourceVersion=0 将导致 limit 被忽略(hasLimit 那一行代码),也就是说, 虽然指定了 limit=500,但这个请求会返回全量数据

    3. 问:ResourceVersionMatch 是什么用途?

      答:用来告诉 apiserver,该如何解读 ResourceVersion。官方有个很复杂的 表格 ,有兴趣可以看看。

    接下来再返回到 cacher 的 GetList() 逻辑,来看下具体有哪几种处理情况。

    2.5.3 情况一:ListOption 要求从 etcd 读数据

    这种情况下,apiserver 会直接从 etcd 读取所有 objects 并过滤,然后返回给客户端, 适用于数据一致性要求极其高的场景。当然,也容易误入这种场景造成 etcd 压力过大,例如第一节的例子。

    // https://github.com/kubernetes/kubernetes/blob/v1.24.0/staging/src/k8s.io/apiserver/pkg/storage/etcd3/store.go#L563

    // GetList implements storage.Interface.
    func (s *store) GetList(ctx , key string, opts storage.ListOptions, listObj runtime.Object) error {
    listPtr := meta.GetItemsPtr(listObj)
    v := conversion.EnforcePtr(listPtr)
    key = path.Join(s.pathPrefix, key)
    keyPrefix := key // append '/' if needed

    newItemFunc := getNewItemFunc(listObj, v)

    var fromRV *uint64
    if len(resourceVersion) > 0 { // 如果 RV 非空(客户端不传时,默认是空字符串)
    parsedRV := s.versioner.ParseResourceVersion(resourceVersion)
    fromRV = &parsedRV
    }

    // ResourceVersion, ResourceVersionMatch 等处理逻辑
    switch {
    case recursive && s.pagingEnabled && len(pred.Continue) > 0: ...
    case recursive && s.pagingEnabled && pred.Limit > 0 : ...
    default : ...
    }

    // loop until we have filled the requested limit from etcd or there are no more results
    for {
    getResp = s.client.KV.Get(ctx, key, options...) // 从 etcd 拉数据
    numFetched += len(getResp.Kvs)
    hasMore = getResp.More

    for i, kv := range getResp.Kvs {
    if limitOption != nil && int64(v.Len()) >= pred.Limit {
    hasMore = true
    break
    }

    lastKey = kv.Key
    data := s.transformer.TransformFromStorage(ctx, kv.Value, kv.Key)
    appendListItem(v, data, kv.ModRevision, pred, s.codec, s.versioner, newItemFunc) // 这里面会做过滤
    numEvald++
    }

    key = string(lastKey) + "\x00"
    }

    // instruct the client to begin querying from immediately after the last key we returned
    if hasMore {
    // we want to start immediately after the last key
    next := encodeContinue(string(lastKey)+"\x00", keyPrefix, returnedRV)
    return s.versioner.UpdateList(listObj, uint64(returnedRV), next, remainingItemCount)
    }

    // no continuation
    return s.versioner.UpdateList(listObj, uint64(returnedRV), "", nil)
    }

    client.KV.Get() 就进入 etcd client 库了,感兴趣可以继续往下挖。appendListItem() 会对拿到的数据进行过滤,这就是我们第一节提到的apiserver 内存过滤操作。

    2.5.4 情况二:本地缓存还没建好,只能从 etcd 读数据

    具体执行过程与情况一相同。

    2.5.5 情况三:使用本地缓存

    // https://github.com/kubernetes/kubernetes/blob/v1.24.0/staging/src/k8s.io/apiserver/pkg/storage/cacher/cacher.go#L622

    // GetList implements storage.Interface
    func (c *Cacher) GetList(ctx , key string, opts storage.ListOptions, listObj runtime.Object) error {
    // 情况一:ListOption 要求必须从 etcd 读
    ...
    // 情况二:apiserver 缓存未建好,只能从 etcd 读
    ...
    // 情况三:apiserver 缓存正常,从缓存读:保证返回的 objects 版本不低于 `listRV`
    listPtr := meta.GetItemsPtr(listObj) // List elements with at least 'listRV' from cache.
    listVal := conversion.EnforcePtr(listPtr)
    filter := filterWithAttrsFunction(key, pred) // 最终的过滤器

    objs, readResourceVersion, indexUsed := c.listItems(listRV, key, pred, ...) // 根据 index 预筛,性能优化
    for _, obj := range objs {
    elem := obj.(*storeElement)
    if filter(elem.Key, elem.Labels, elem.Fields) // 真正的过滤
    listVal.Set(reflect.Append(listVal, reflect.ValueOf(elem))
    }

    if c.versioner != nil
    c.versioner.UpdateList(listObj, readResourceVersion, "", nil)
    return nil
    }

    3 LIST 测试

    为了避免客户端库(例如 client-go)自动帮我们设置一些参数,我们直接用 curl 来测试,指定证书就行了:

    $ cat curl-k8s-apiserver.sh
    curl -s --cert /etc/kubernetes/pki/admin.crt --key /etc/kubernetes/pki/admin.key --cacert /etc/kubernetes/pki/ca.crt $@

    使用方式:

    $ ./curl-k8s-apiserver.sh "https://localhost:6443/api/v1/pods?limit=2"
    {
    "kind": "PodList",
    "metadata": {
    "resourceVersion": "2127852936",
    "continue": "eyJ2IjoibWV0YS5rOHMuaW8vdjEiLCJ...",
    },
    "items": [ {pod1 data }, {pod2 data}]
    }

    3.1 指定 limit=2:response 将返回分页信息(continue

    3.1.1 curl 测试

    $ ./curl-k8s-apiserver.sh "https://localhost:6443/api/v1/pods?limit=2"
    {
    "kind": "PodList",
    "metadata": {
    "resourceVersion": "2127852936",
    "continue": "eyJ2IjoibWV0YS5rOHMuaW8vdjEiLCJ...",
    },
    "items": [ {pod1 data }, {pod2 data}]
    }


    可以看到,

    • 确实返回了两个 pod 信息,在 items[] 字段中;

    • 另外在 metadata 中返回了一个 continue 字段,客户端下次带上这个参数,apiserver 将继续返回剩下的内容,直到 apiserver 不再返回 continue。

    3.1.2 kubectl 测试

    调大 kubectl 的日志级别,也可以看到它背后用了 continue 来获取全量 pods:

    $ kubectl get pods --all-namespaces --v=10
    # 以下都是 log 输出,做了适当调整
    # curl -k -v -XGET -H "User-Agent: kubectl/v1.xx" -H "Accept: application/json;as=Table;v=v1;g=meta.k8s.io,application/json;as=Table;v=v1beta1;g=meta.k8s.io,application/json"
    # 'http://localhost:8080/api/v1/pods?limit=500'
    # GET http://localhost:8080/api/v1/pods?limit=500 200 OK in 202 milliseconds
    # Response Body: {"kind":"Table","metadata":{"continue":"eyJ2Ijoib...","remainingItemCount":54},"columnDefinitions":[...],"rows":[...]}
    #
    # curl -k -v -XGET -H "Accept: application/json;as=Table;v=v1;g=meta.k8s.io,application/json;as=Table;v=v1beta1;g=meta.k8s.io,application/json" -H "User-Agent: kubectl/v1.xx"
    # 'http://localhost:8080/api/v1/pods?continue=eyJ2Ijoib&limit=500'
    # GET http://localhost:8080/api/v1/pods?continue=eyJ2Ijoib&limit=500 200 OK in 44 milliseconds
    # Response Body: {"kind":"Table","metadata":{"resourceVersion":"2122644698"},"columnDefinitions":[],"rows":[...]}

    第一次请求拿到了 500 个 pods,第二次请求把返回的 continue 带上了GET http://localhost:8080/api/v1/pods?continue=eyJ2Ijoib&limit=500,continue 是个 token, 有点长,为了更好的展示这里把它截断了。

    3.2 指定 limit=2&resourceVersion=0limit=2 将被忽略,返回全量数据

    $ ./curl-k8s-apiserver.sh "https://localhost:6443/api/v1/pods?limit=2&resourceVersion=0"
    {
    "kind": "PodList",
    "metadata": {
    "resourceVersion": "2127852936",
    "continue": "eyJ2IjoibWV0YS5rOHMuaW8vdjEiLCJ...",
    },
    "items": [ {pod1 data }, {pod2 data}, ...]
    }

    items[] 里面是全量 pod 信息。

    3.3 指定 spec.nodeName=node1&resourceVersion=0 vs. spec.nodeName=node1"

    结果相同

    $ ./curl-k8s-apiserver.sh "https://localhost:6443/api/v1/namespaces/default/pods?fieldSelector=spec.nodeName%3Dnode1" | jq '.items[].spec.nodeName'
    "node1"
    "node1"
    "node1"
    ...

    $ ./curl-k8s-apiserver.sh "https://localhost:6443/api/v1/namespaces/default/pods?fieldSelector=spec.nodeName%3Dnode1&resourceVersion=0" | jq '.items[].spec.nodeName'
    "node1"
    "node1"
    "node1"
    ...

    结果是一样的,除非是 apiserver 缓存和 etcd 数据出现不一致,这个概率极小,我们这里不讨论。

    速度差异很大

    用 time 测量以上两种情况下的耗时,会发现对于大一些的集群,这两种请求的响应时间就会有明显差异。

    $ time ./curl-k8s-apiserver.sh  > result


    对于 4K nodes, 100K pods 规模的集群,以下数据供参考:

    • 不带 resourceVersion=0(读 etcd 并在 apiserver 过滤): 耗时 10s

    • 带 resourceVersion=0(读 apiserver 缓存): 耗时 0.05s

    差了 200 倍。

    全量 pod 的总大小按 2GB 计算,平均每个 20KB。

    4 LIST 请求对控制平面压力:量化分析

    本节以 cilium-agent 为例,介绍定量测量它启动时对控制平面压力。

    4.1 收集 LIST 请求

    首先获取 agent 启动时,都 LIST k8s 哪些资源。有几种收集方式:

    1. 在 k8s access log,按 ServiceAccount、verb、request_uri 等过滤;

    2. 通过 agent 日志;

    3. 通过进一步代码分析等等。

    假设我们收集到如下 LIST 请求:

    1. api/v1/namespaces?resourceVersion=0

    2. api/v1/pods?filedSelector=spec.nodeName%3Dnode1&resourceVersion=0

    3. api/v1/nodes?fieldSelector=metadata.name%3Dnode1&resourceVersion=0

    4. api/v1/services?labelSelector=%21service.kubernetes.io%2Fheadless%2C%21service.kubernetes.io%2Fservice-proxy-name

    5. apis/discovery.k8s.io/v1beta1/endpointslices?resourceVersion=0

    6. apis/networking.k8s.io/networkpolicies?resourceVersion=0

    7. apis/cilium.io/v2/ciliumnodes?resourceVersion=0

    8. apis/cilium.io/v2/ciliumnetworkpolicies?resourceVersion=0

    9. apis/cilium.io/v2/ciliumclusterwidenetworkpolicies?resourceVersion=0

    2.2 测试 LIST 请求数据量和耗时

    有了 LIST 请求列表,接下来就可以手动执行这些请求,拿到如下数据:

    1. 请求耗时

    2. 请求处理的数据量,这里分为两种:

      1. apiserver 处理的数据量(全量数据),评估对 apiserver/etcd 的性能影响应该以这个为主

      2. agent 最终拿到的数据量(按 selector 做了过滤)

    用下面这个脚本(放到真实环境 k8s master 上)来就可以执行一遍测试

    $ cat benchmark-list-overheads.sh
    apiserver_url="https://localhost:6443"

    # List k8s core resources (e.g. pods, services)
    # API: GET/LIST /api/v1/?&resourceVersion=0
    function benchmark_list_core_resource() {
    resource=$1
    selectors=$2

    echo "----------------------------------------------------"
    echo "Benchmarking list $2"
    listed_file="listed-$resource"
    url="$apiserver_url/api/v1/$resource?resourceVersion=0"

    # first perform a request without selectors, this is the size apiserver really handles
    echo "curl $url"
    time ./curl-k8s-apiserver.sh "$url" > $listed_file

    # perform another request if selectors are provided, this is the size client receives
    listed_file2="$listed_file-filtered"
    if [ ! -z "$selectors" ]; then
    url="$url&$selectors"
    echo "curl $url"
    time ./curl-k8s-apiserver.sh "$url" > $listed_file2
    fi

    ls -ahl $listed_file $listed_file2 2>/dev/null

    echo "----------------------------------------------------"
    echo ""
    }

    # List k8s apiextension resources (e.g. pods, services)
    # API: GET/LIST /apis//?&resourceVersion=0
    function benchmark_list_apiexternsion_resource() {
    api_group=$1
    resource=$2
    selectors=$3

    echo "----------------------------------------------------"
    echo "Benchmarking list $api_group/$resource"
    api_group_flatten_name=$(echo $api_group | sed 's/\//-/g')
    listed_file="listed-$api_group_flatten_name-$resource"
    url="$apiserver_url/apis/$api_group/$resource?resourceVersion=0"
    if [ ! -z "$selectors" ]; then
    url="$url&$selectors"
    fi

    echo "curl $url"
    time ./curl-k8s-apiserver.sh "$url" > $listed_file
    ls -ahl $listed_file
    echo "----------------------------------------------------"
    echo ""
    }

    benchmark_list_core_resource "namespaces" ""
    benchmark_list_core_resource "pods" "filedSelector=spec.nodeName%3Dnode1"
    benchmark_list_core_resource "nodes" "fieldSelector=metadata.name%3Dnode1"
    benchmark_list_core_resource "services" "labelSelector=%21service.kubernetes.io%2Fheadless%2C%21service.kubernetes.io%2Fservice-proxy-name"

    benchmark_list_apiexternsion_resource "discovery.k8s.io/v1beta1" "endpointslices" ""
    benchmark_list_apiexternsion_resource "apiextensions.k8s.io/v1" "customresourcedefinitions" ""
    benchmark_list_apiexternsion_resource "networking.k8s.io" "networkpolicies" ""
    benchmark_list_apiexternsion_resource "cilium.io/v2" "ciliumnodes" ""
    benchmark_list_apiexternsion_resource "cilium.io/v2" "ciliumendpoints" ""
    benchmark_list_apiexternsion_resource "cilium.io/v2" "ciliumnetworkpolicies" ""
    benchmark_list_apiexternsion_resource "cilium.io/v2" "ciliumclusterwidenetworkpolicies" ""

    执行效果如下:

    $ benchmark-list-overheads.sh
    ----------------------------------------------------
    Benchmarking list
    curl https://localhost:6443/api/v1/namespaces?resourceVersion=0

    real 0m0.090s
    user 0m0.038s
    sys 0m0.044s
    -rw-r--r-- 1 root root 69K listed-namespaces
    ----------------------------------------------------

    Benchmarking list fieldSelector=spec.nodeName%3Dnode1
    curl https://localhost:6443/api/v1/pods?resourceVersion=0

    real 0m18.332s
    user 0m1.355s
    sys 0m1.822s
    curl https://localhost:6443/api/v1/pods?resourceVersion=0&fieldSelector=spec.nodeName%3Dnode1

    real 0m0.242s
    user 0m0.044s
    sys 0m0.188s
    -rw-r--r-- 1 root root 2.0G listed-pods
    -rw-r--r-- 1 root root 526K listed-pods-filtered
    ----------------------------------------------------

    ...

    说明:凡是带了 selector 的 LIST,例如 LIST pods?spec.nodeName=node1,这个脚本会先执行一遍不带 selector 的请求,目的是测量 apiserver 需要处理的数据量,例如上面的 list pods:

    1. agent 真正执行的是 pods?resourceVersion=0&fieldSelector=spec.nodeName%3Dnode1,所以请求耗时应该以这个为准

    2.额外执行了 pods?resourceVersion=0,这样是为了测试 1 的请求到底需要 apiserver 处理多少数据量

    注意:list all pods 这样的操作会产生 2GB 的文件,因此谨慎使用这个 benchmark 工具,首先理解你写的脚本在测什么,尤其不要自动化或并发跑,可能会把 apiserver/etcd 打爆。

    4.3 测试结果分析

    以上输出有如下关键信息:

    1. LIST 的资源类型,例如 pods/endpoints/services

    2. LIST 操作耗时

    3. LIST 操作涉及的数据量

      1. apiserver 需要处理的数据量(json 格式):以上面 list pods 为例,对应的是 listed-pods 文件,共 2GB;

      2. agent 收到的数据量(因为 agent 可能指定了 label/field 过滤器):以上面 list pods 为例,对应 listed-pods-filtered 文件,共计 526K

    按以上方式将所有 LIST 请求都收集起来并排序,就知道了 agent 一次启动操作,对 apiserver/etcd 的压力。

    $ ls -ahl listed-*
    -rw-r--r-- 1 root root 222 listed-apiextensions.k8s.io-v1-customeresourcedefinitions
    -rw-r--r-- 1 root root 5.8M listed-apiextensions.k8s.io-v1-customresourcedefinitions
    -rw-r--r-- 1 root root 2.0M listed-cilium.io-v2-ciliumclusterwidenetworkpolicies
    -rw-r--r-- 1 root root 193M listed-cilium.io-v2-ciliumendpoints
    -rw-r--r-- 1 root root 185 listed-cilium.io-v2-ciliumnetworkpolicies
    -rw-r--r-- 1 root root 6.6M listed-cilium.io-v2-ciliumnodes
    -rw-r--r-- 1 root root 42M listed-discovery.k8s.io-v1beta1-endpointslices
    -rw-r--r-- 1 root root 69K listed-namespaces
    -rw-r--r-- 1 root root 222 listed-networking.k8s.io-networkpolicies
    -rw-r--r-- 1 root root 70M listed-nodes # 仅用于评估 apiserver 需要处理的数据量
    -rw-r--r-- 1 root root 25K listed-nodes-filtered
    -rw-r--r-- 1 root root 2.0G listed-pods # 仅用于评估 apiserver 需要处理的数据量
    -rw-r--r-- 1 root root 526K listed-pods-filtered
    -rw-r--r-- 1 root root 23M listed-services # 仅用于评估 apiserver 需要处理的数据量
    -rw-r--r-- 1 root root 23M listed-services-filtered

    还是以 cilium 为例,有大致这样一个排序(apiserver 处理的数据量,json 格式):

    List 资源类型apiserver 处理的数据量(json)耗时
    CiliumEndpoints (全量)193MB11s
    CiliumNodes (全量)70MB0.5s

    5 大规模基础服务:部署和调优建议

    5.1 List 请求默认设置 ResourceVersion=0

    前面已经介绍,不设置这个参数将导致 apiserver 从 etcd 拉全量数据再过滤,导致

    1. 很慢

    2. 规模大了 etcd 扛不住

    因此,除非对数据准确性要求极高,必须从 etcd 拉数据,否则应该在 LIST 请求时设置 ResourceVersion=0 参数, 让 apiserver 用缓存提供服务。

    如果你使用的是 client-go 的 ListWatch/informer 接口, 那它默认已经设置了 ResourceVersion=0

    5.2 优先使用 namespaced API

    如果要 LIST 的资源在单个或少数几个 namespace,考虑使用 namespaced API:

    • Namespaced API: /api/v1/namespaces//pods?query=xxx

    • Un-namespaced API: /api/v1/pods?query=xxx

    5.3 Restart backoff

    对于 per-node 部署的基础服务,例如 kubelet、cilium-agent、daemonsets,需要 通过有效的 restart backoff 降低大面积重启时对控制平面的压力。

    例如,同时挂掉后,每分钟重启的 agent 数量不超过集群规模的 10%(可配置,或可自动计算)。

    5.4 优先通过 label/field selector 在服务端做过滤

    如果需要缓存某些资源并监听变动,那需要使用 ListWatch 机制,将数据拉到本地,业务逻辑根据需要自己从 local cache 过滤。这是 client-go 的 ListWatch/informer 机制。

    但如果只是一次性的 LIST 操作,并且有筛选条件,例如前面提到的根据 nodename 过滤 pod 的例子, 那显然应该通过设置 label 或字段过滤器,让 apiserver 帮我们把数据过滤出来。LIST 10w pods 需要几十秒(大部分时间花在数据传输上,同时也占用 apiserver 大量 CPU/BW/IO), 而如果只需要本机上的 pod,那设置 nodeName=node1 之后,LIST 可能只需要 0.05s 就能返回结果。另外非常重要的一点时,不要忘记在请求中同时带上 resourceVersion=0。

    5.4.1 Label selector

    在 apiserver 内存过滤。

    5.4.2 Field selector

    在 apiserver 内存过滤。

    5.4.3 Namespace selector

    etcd 中 namespace 是前缀的一部分,因此能指定 namespace 过滤资源,速度比不是前缀的 selector 快很多。

    5.5 配套基础设施(监控、告警等)

    以上分析可以看成,client 的单个请求可能只返回几百 KB 的数据,但 apiserver(更糟糕的情况,etcd)需要处理上 GB 的数据。因此,应该极力避免基础服务的大规模重启,为此需要在监控、告警上做的尽量完善。

    5.5.1 使用独立 ServiceAccount

    每个基础服务(例如 kubelet、cilium-agent 等),以及对 apiserver 有大量 LIST 操作的各种 operator, 都使用各自独立的 SA, 这样便于 apiserver 区分请求来源,对监控、排障和服务端限流都非常有用。

    5.5.2 Liveness 监控告警

    基础服务必须覆盖到 liveness 监控。

    必须有 P1 级别的 liveness 告警,能第一时间发现大规模挂掉的场景。然后通过 restart backoff

    降低对控制平面的压力。

    5.5.3 监控和调优 etcd

    需要针对性能相关的关键指标做好监控和告警:

    1. 内存

    2. 带宽

    3. 大 LIST 请求数量及响应耗时

      比如下面这个 LIST all pods 日志:


    4.  {
      "level":"warn",
      "msg":"apply request took too long",
      "took":"5357.87304ms",
      "expected-duration":"100ms",
      "prefix":"read-only range ",
      "request":"key:\"/registry/pods/\" range_end:\"/registry/pods0\" ",
      "response":"range_response_count:60077 size:602251227"
      }

      部署和配置调优:

      1. K8s events 拆到单独的 etcd 集群

      2. 其他。

      6 其他

      6.1 Get 请求:GetOptions{}

      基本原理与 ListOption{} 一样,不设置 ResourceVersion=0 会导致 apiserver 去 etcd 拿数据,应该尽量避免。

      参考资料

      1. Kubernetes API Concepts, kubernetes doc

      2. (译) [论文] Raft 共识算法(及 etcd/raft 源码解析)(USENIX, 2014)

      Linux阅码场 专业的Linux技术社区和Linux操作系统学习平台,内容涉及Linux内核,Linux内存管理,Linux进程管理,Linux文件系统和IO,Linux性能调优,Linux设备驱动以及Linux虚拟化和云计算等各方各面.
      评论
      • 在测试XTS时会遇到修改产品属性、SElinux权限、等一些内容,修改源码再编译很费时。今天为大家介绍一个便捷的方法,让OpenHarmony通过挂载镜像来修改镜像内容!触觉智能Purple Pi OH鸿蒙开发板演示。搭载了瑞芯微RK3566四核处理器,树莓派卡片电脑设计,支持开源鸿蒙OpenHarmony3.2-5.0系统,适合鸿蒙开发入门学习。挂载镜像首先,将要修改内容的镜像传入虚拟机当中,并创建一个要挂载镜像的文件夹,如下图:之后通过挂载命令将system.img镜像挂载到sys
        Industio_触觉智能 2025-01-03 11:39 112浏览
      • 前言近年来,随着汽车工业的快速发展,尤其是新能源汽车与智能汽车领域的崛起,汽车安全标准和认证要求日益严格,应用范围愈加广泛。ISO 26262和ISO 21448作为两个重要的汽车安全标准,它们在“系统安全”中扮演的角色各自不同,但又有一定交集。在智能网联汽车的高级辅助驾驶系统(ADAS)应用中,理解这两个标准的区别及其相互关系,对于保障车辆的安全性至关重要。ISO 26262:汽车功能安全的基石如图2.1所示,ISO 26262对“功能安全”的定义解释为:不存在由于电子/电气系统失效引起的危害
        广电计量 2025-01-02 17:18 214浏览
      • Matter加持:新世代串流装置如何改变智能家居体验?随着现在智能家庭快速成长,串流装置(Streaming Device,以下简称Streaming Device)除了提供更卓越的影音体验,越来越多厂商开始推出支持Matter标准的串流产品,使其能作为智能家庭中枢,连结多种智能家电。消费者可以透过Matter的功能执行多样化功能,例如:开关灯、控制窗帘、对讲机开门,以及操作所有支持Matter的智能家电。此外,再搭配语音遥控器与语音助理,打造出一个更加智能、便捷的居家生活。支持Matter协议
        百佳泰测试实验室 2025-01-03 10:29 136浏览
      • 车身域是指负责管理和控制汽车车身相关功能的一个功能域,在汽车域控系统中起着至关重要的作用。它涵盖了车门、车窗、车灯、雨刮器等各种与车身相关的功能模块。与汽车电子电气架构升级相一致,车身域发展亦可以划分为三个阶段,功能集成愈加丰富:第一阶段为分布式架构:对应BCM车身控制模块,包含灯光、雨刮、门窗等传统车身控制功能。第二阶段为域集中架构:对应BDC/CEM域控制器,在BCM基础上集成网关、PEPS等。第三阶段为SOA理念下的中央集中架构:VIU/ZCU区域控制器,在BDC/CEM基础上集成VCU、
        北汇信息 2025-01-03 16:01 166浏览
      • 在科技飞速发展的今天,机器人已经逐渐深入到我们生活和工作的各个领域。从工业生产线上不知疲倦的机械臂,到探索未知环境的智能探测机器人,再到贴心陪伴的家用服务机器人,它们的身影无处不在。而在这些机器人的背后,C 语言作为一种强大且高效的编程语言,发挥着至关重要的作用。C 语言为何适合机器人编程C 语言诞生于 20 世纪 70 年代,凭借其简洁高效、可移植性强以及对硬件的直接操控能力,成为机器人编程领域的宠儿。机器人的运行环境往往对资源有着严格的限制,需要程序占用较少的内存和运行空间。C 语言具有出色
        Jeffreyzhang123 2025-01-02 16:26 153浏览
      • 【工程师故事】+半年的经历依然忧伤,带着焦虑和绝望  对于一个企业来说,赚钱才是第一位的,对于一个人来说,赚钱也是第一位的。因为企业要活下去,因为个人也要活下去。企业打不了倒闭。个人还是要吃饭的。企业倒闭了,打不了从头再来。个人失业了,面对的不仅是房贷车贷和教育,还有找工作的焦虑。企业说,一个公司倒闭了,说明不了什么,这是正常的一个现象。个人说,一个中年男人失业了,面对的压力太大了,焦虑会摧毁你的一切。企业说,是个公司倒闭了,也不是什么大的问题,只不过是这些公司经营有问题吧。
        curton 2025-01-02 23:08 284浏览
      • 物联网(IoT)的快速发展彻底改变了从智能家居到工业自动化等各个行业。由于物联网系统需要高效、可靠且紧凑的组件来处理众多传感器、执行器和通信设备,国产固态继电器(SSR)已成为满足中国这些需求的关键解决方案。本文探讨了国产SSR如何满足物联网应用的需求,重点介绍了它们的优势、技术能力以及在现实场景中的应用。了解物联网中的固态继电器固态继电器是一种电子开关设备,它使用半导体而不是机械触点来控制负载。与传统的机械继电器不同,固态继电器具有以下优势:快速切换:确保精确快速的响应,这对于实时物联网系统至
        克里雅半导体科技 2025-01-03 16:11 160浏览
      • 在快速发展的能源领域,发电厂是发电的支柱,效率和安全性至关重要。在这种背景下,国产数字隔离器已成为现代化和优化发电厂运营的重要组成部分。本文探讨了这些设备在提高性能方面的重要性,同时展示了中国在生产可靠且具有成本效益的数字隔离器方面的进步。什么是数字隔离器?数字隔离器充当屏障,在电气上将系统的不同部分隔离开来,同时允许无缝数据传输。在发电厂中,它们保护敏感的控制电路免受高压尖峰的影响,确保准确的信号处理,并在恶劣条件下保持系统完整性。中国国产数字隔离器经历了重大创新,在许多方面达到甚至超过了全球
        克里雅半导体科技 2025-01-03 16:10 117浏览
      • 光耦合器,也称为光隔离器,是一种利用光在两个隔离电路之间传输电信号的组件。在医疗领域,确保患者安全和设备可靠性至关重要。在众多有助于医疗设备安全性和效率的组件中,光耦合器起着至关重要的作用。这些紧凑型设备经常被忽视,但对于隔离高压和防止敏感医疗设备中的电气危害却是必不可少的。本文深入探讨了光耦合器的功能、其在医疗应用中的重要性以及其实际使用示例。什么是光耦合器?它通常由以下部分组成:LED(发光二极管):将电信号转换为光。光电探测器(例如光电晶体管):检测光并将其转换回电信号。这种布置确保输入和
        腾恩科技-彭工 2025-01-03 16:27 155浏览
      • 国际标准IPC 标准:IPC-A-600:规定了印刷电路板制造过程中的质量要求和验收标准,涵盖材料、外观、尺寸、焊接、表面处理等方面。IPC-2221/2222:IPC-2221 提供了用于设计印刷电路板的一般原则和要求,IPC-2222 则针对高可靠性电子产品的设计提供了进一步的指导。IPC-6012:详细定义了刚性基板和柔性基板的要求,包括材料、工艺、尺寸、层次结构、特征等。IPC-4101:定义了印刷电路板的基板材料的物理和电气特性。IPC-7351:提供了元件封装的设计规范,包括封装尺寸
        Jeffreyzhang123 2025-01-02 16:50 198浏览
      • 本文继续介绍Linux系统查看硬件配置及常用调试命令,方便开发者快速了解开发板硬件信息及进行相关调试。触觉智能RK3562开发板演示,搭载4核A53处理器,主频高达2.0GHz;内置独立1Tops算力NPU,可应用于物联网网关、平板电脑、智能家居、教育电子、工业显示与控制等行业。查看系统版本信息查看操作系统版本信息root@ido:/# cat /etc/*releaseDISTRIB_ID=UbuntuDISTRIB_RELEASE=20.04DISTRIB_CODENAME=focalDIS
        Industio_触觉智能 2025-01-03 11:37 137浏览
      • 自动化已成为现代制造业的基石,而驱动隔离器作为关键组件,在提升效率、精度和可靠性方面起到了不可或缺的作用。随着工业技术不断革新,驱动隔离器正助力自动化生产设备适应新兴趋势,并推动行业未来的发展。本文将探讨自动化的核心趋势及驱动隔离器在其中的重要角色。自动化领域的新兴趋势智能工厂的崛起智能工厂已成为自动化生产的新标杆。通过结合物联网(IoT)、人工智能(AI)和机器学习(ML),智能工厂实现了实时监控和动态决策。驱动隔离器在其中至关重要,它确保了传感器、执行器和控制单元之间的信号完整性,同时提供高
        腾恩科技-彭工 2025-01-03 16:28 157浏览
      • 从无到有:智能手机的早期探索无线电话装置的诞生:1902 年,美国人内森・斯塔布菲尔德在肯塔基州制成了第一个无线电话装置,这是人类对 “手机” 技术最早的探索。第一部移动手机问世:1938 年,美国贝尔实验室为美国军方制成了世界上第一部 “移动” 手机。民用手机的出现:1973 年 4 月 3 日,摩托罗拉工程师马丁・库珀在纽约曼哈顿街头手持世界上第一台民用手机摩托罗拉 DynaTAC 8000X 的原型机,给竞争对手 AT&T 公司的朋友打了一个电话。这款手机重 2 磅,通话时间仅能支持半小时
        Jeffreyzhang123 2025-01-02 16:41 167浏览
      • 影像质量应用于多个不同领域,无论是在娱乐、医疗或工业应用中,高质量的影像都是决策的关键基础。清晰的影像不仅能提升观看体验,还能保证关键细节的准确传达,例如:在医学影像中,它对诊断结果有着直接的影响!不仅如此,影像质量还影响了:▶ 压缩技术▶ 存储需求▶ 传输效率随着技术进步,影像质量的标准不断提高,对于研究与开发领域,理解并提升影像质量已成为不可忽视的重要课题。在图像处理的过程中,硬件与软件除了各自扮演着不可或缺的基础角色,有效地协作能够确保图像处理过程既高效又具有优异的质量。软硬件各扮演了什么
        百佳泰测试实验室 2025-01-03 10:39 132浏览
      我要评论
      0
      点击右上角,分享到朋友圈 我知道啦
      请使用浏览器分享功能 我知道啦