前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Kubelet从入门到放弃:识透CPU管理

Kubelet从入门到放弃:识透CPU管理

作者头像
CNCF
发布2021-02-23 15:39:15
4490
发布2021-02-23 15:39:15
举报
文章被收录于专栏:CNCFCNCF

摘要

《Kubelet从入门到放弃系列》将对Kubelet组件由Linux基础知识到源码进行深入梳理。在这篇文章中zouyee会介绍CPU的相关概念以及Kubelet组件CPU Manager的源码。关于《Kubernetes调度框架系列》剩余的配置及源码部分,将陆续放出。

一、背景介绍

1.1 需求背景

默认情况下,kubelet 基于CFS 调度算法来执行Pod的CPU分配。但是当节点上运行了CPU 密集型 Pod 时,应用可能会因抢占等情况导致CPU切换,而上述的切换导致的延时与中断对于业务敏感性Pod是无法接受的。

为了解决上述问题。Kubelet 提供了可选的 CPU 管理策略,以满足不同的业务场景。

a. 以API形式向用户提供配置方案

b. 同时支持多种CPU使用场景共存

c. CPU基于亲和性分配时,考虑设备拓扑

1.2 CPU架构

a. SMT

同时多线程Simultaneous multithreading,简称SMT,SMT可通过复制处理器上的结构状态,让同一个处理器上的多个线程同步执行并共享处理器的执行资源,可最大限度地实现宽发射、乱序的超标量处理,提高处理器运算部件的利用率,缓和由于数据相关或Cache未命中带来的访问内存延时。当没有多个线程可用时,SMT处理器几乎和传统的宽发射超标量处理器一样。多线程技术则可以为高速的运算核心准备更多的待处理数据,减少运算核心的闲置时间。Intel的hyper-threading其实就是 two-thread SMT.

b. CMP

片上多处理器(Chip multiprocessors,简称CMP,其思想是将大规模并行处理器中的SMP(对称多处理器)集成到同一芯片内,各个处理器并行执行不同的进程。由于CMP结构已经被划分成多个处理器核来设计,每个核都比较简单,有利于优化设计。多核处理器可以在处理器内部共享缓存,提高缓存利用率,同时简化多处理器系统设计的复杂度。

c. SMP

对称多处理器(Symmetric Multi-Processors,简称SMP),其是指在一个计算机上汇集了一组处理器(多CPU),各CPU之间共享内存子系统以及总线结构。共享存储型多处理机有三种模型:均匀存储器存取(Uniform-Memory-Access,简称UMA)模型、非均匀存储器存取(Non-uniform Memory Access,简称NUMA)模型和只用高速缓存的存储器结构(Cache-Only Memory Architecture,简称COMA)模型,这些模型的区别在于存储器和外围资源如何共享或分布。

1) S2MP全称为可扩展共享存储多处理(Scalable Shared-Memory Multiprocessing)技术。S2MP系统将大量高性能微处理器连接起来,共享一个统一的地址空间,较好地解决其他并行处理系统无法解决的问题。

2) MMP也被称为海量并行处理架构。MPP提供了另外一种进行系统扩展的方式,它由多个SMP服务器通过一定的节点互联网络进行连接,协同工作,完成相同的任务,从用户的角度来看是一个服务器系统。

1.3 相关技术

在CPU管理中,涉及NUMA、HT及cpuset技术,以下为简要介绍。

NUMA

NUMA,以内存访问的不一致性为代价,减轻对总线和memory的带宽需求。这种结构对进程调度算法的要求较高,尽量减少跨Node的内存访问次数,以提升系统性能。Core之间会共享总线、内存等资源。如果Core的数量较少,则没什么问题,但随着Core的增多,对总线以及内存带宽的需求就会显著增大,最终总线和内存会成为系统性能的瓶颈。

如下图所示,一个NUMA Node包括一个或者多个Socket,以及与之相连的local memory。一个多核的Socket有多个Core。如果CPU支持HT,OS还会把这个Core看成 2个Logical Processor。

  • Socket是一个物理上的概念,指的是主板上的cpu插槽
  • Node是一个逻辑上的概念,上图中没有提及。由于SMP体系中各个CPU访问内存只能通过单一的通道,导致内存访问成为瓶颈,cpu再多也无用。后来引入了NUMA,通过划分node,每个node有本地RAM,这样node内访问RAM速度会非常快。但跨Node的RAM访问代价会相对高一点,我们用Node之间的距离(Distance,抽象的概念)来定义各个Node之间互访资源的开销。
  • Core就是一个物理cpu,一个独立的硬件执行单元,比如寄存器,计算单元等
  • Thread就是超线程(HyperThreading)的概念,是一个逻辑cpu,共享core上的执行单元
HT

Hyperthreading 使操作系统认为处理器的核心数是实际核心数的2倍,超线程(hyper-threading)本质上就是CPU支持的同时多线程(simultaneous multi-threading)技术,简单理解就是对CPU的虚拟化,一颗物理CPU可以被操作系统当做多颗CPU来使用。Hyper-threading只是一种“欺骗”手段。

cpuset

cpuset作为cgroup的子系统,主要用于numa架构,用于设置cpu的亲和性,为 cgroup 中的 task 分配独立的 CPU和内存等。

cpuset使用sysfs提供用户态接口,可以通过普通文件读写,工作流程为:cpuset调用sched_setaffinity来设置进程的cpu、内存的亲和性,调用mbind和set_mempolicy来设置内存的亲和性。

1.4 数据说明

Numa Node

numactl是设定进程NUMA策略的命令行工具,也可以用来查看当前的Nuwa node:

代码语言:javascript
复制
[root@xxx ~]# numactl -H
available: 1 nodes (0)
node 0 cpus: 0 1 2 3 4 5 6 7
node 0 size: 16047 MB
node 0 free: 3693 MB
node distances:
node   0
  0:  10

从上面可以看出本机有一个Numa node(操作系统配置numa=on效果一样...),如果要进一步知道一个Node包含哪几个CPU,该怎么办?

一种方法是通过查看ls /sys/devices/system/node/目录下的信息,例如:

代码语言:javascript
复制
[root@xxx ~]# ls /sys/devices/system/node/
has_cpu  has_normal_memory  node0  online  possible  power  uevent
[root@xxx ~]# ls /sys/devices/system/node/node0
compact    cpu0       cpu1       cpu2       cpu3       cpu4       cpu5       
cpu6       cpu7       cpulist    cpumap     distance   hugepages  
...

可见, node0包含0/1/2/3/4/5/6/7八个Processor。

查看Socket

一个Socket对应主板上的一个插槽,在本文中是指一个CPU封装。在/proc/cpuinfo中的physical id就是Socket的ID,可以从中找到本机到底有多少个Socket,并且每个Socket有那几个Processor。

1) 查看Socket数量

代码语言:javascript
复制
$ grep 'physical id' /proc/cpuinfo | awk -F: '{print $2 | "sort -un"}'
 0
 1
$ grep 'physical id' /proc/cpuinfo | awk -F: '{print $2 | "sort -un"}' | wc -l
2

2)查看每个Socket有几个Processor

代码语言:javascript
复制
$ grep 'physical id' /proc/cpuinfo | awk -F: '{print $2}' | sort | uniq -c
      4  0
      4  1

3) 查看Socket对应哪几个Processor

代码语言:javascript
复制
$ awk -F: '{ 
    if ($1 ~ /processor/) {
        gsub(/ /,"",$2);
        p_id=$2;
    } else if ($1 ~ /physical id/){
        gsub(/ /,"",$2);
        s_id=$2;
        arr[s_id]=arr[s_id] " " p_id
    }
} 

END{
    for (i in arr) 
        print arr[i];
}' /proc/cpuinfo | cut -c2-
0 1 2 3
4 5 6 7

Core

/proc/cpuinfo文件中的cpu cores表明一个socket中有几个cores,例如:

代码语言:javascript
复制
cat /proc/cpuinfo | grep 'core'  | sort -u
core id    : 0
core id    : 1
core id    : 2
core id    : 3
cpu cores  : 4

Logical Processor

查看Processors的个数就比较简单了,从上面的统计结果中我们已经可以知道有8个Logical processor,不过也可以直接从/proc/cpuinfo文件中获取:

代码语言:javascript
复制
$ grep 'processor' /proc/cpuinfo | wc -l
8

其实,每个socket中能有几个processor也可以从siblings字段中获取:

代码语言:javascript
复制
$ grep 'siblings' /proc/cpuinfo | sort -u
siblings  : 4
1.5 结构体

需要注意的是,Kubelet内部启动cadvisor Manager,封装cadvisor接口为cadvisor.Interface,其对外暴露MachineInfo() (*cadvisorapi.MachineInfo, error)方法,CPU manager通过cadvisor的MachineInfo结构体信息生产CPU 拓扑信息, 具体实现为调用GetNodesInfo

https://github.com/google/cadvisor/blob/master/utils/sysinfo/sysinfo.go#L193:6

三个关键的结构体定义如下:

https://github.com/google/cadvisor/blob/master/info/v1/machine.go#L174

代码语言:javascript
复制
type MachineInfo struct {
...
  // Machine Topology
  // Describes cpu/memory layout and hierarchy.
  Topology []Node `json:"topology"`
...
}

type Node struct {
  Id int `json:"node_id"`
  // Per-node memory
  Memory    uint64          `json:"memory"`
  HugePages []HugePagesInfo `json:"hugepages"`
  Cores     []Core          `json:"cores"`
  Caches    []Cache         `json:"caches"`
}

type Core struct {
  Id       int     `json:"core_id"`
  Threads  []int   `json:"thread_ids"`
  Caches   []Cache `json:"caches"`
  SocketID int     `json:"socket_id"`
}

说明如下:

1. Node对应NUMA节点,其中ID由/sys/devices/system/node/nodei中的i获得

2. Core对应 Core,其中ID由/sys/devices/system/node/nodei/cpuj/topology/core_id获得

3. Threads对应Logical Processor,其中ID由/sys/devices/system/node/nodei/cpuj中的

4. SocketID对应Socket,其中ID由/sys/devices/system/node/nodei/cpuj/topology/physical_package_id获得

二、功能介绍

⚠️:其中涉及到的拓扑管理、设备管理等内容,后续有针对性文章进行介绍,此处带过。

Kubernetes版本

API版本

v1.8

alpha

v1.12

beta

CPU 管理器(CPU Manager)作为 alpha 特性引入 Kubernetes 1.8 版本,Kubernetes 1.12进入beta版本后,默认开启。CPU 管理策略通过 kubelet 参数 --cpu-manager-policy 来指定。支持两种策略:

a. none: 默认策略,保持现有的调度行为。

b.static: 节点上满足某些资源特征的 Pod 根据 CPU 亲和性和独占性进行分配。

CPU 管理器(即:通过goroutine方式执行reconcileState方法)定期通过 CRI(即containerRuntime) 写入资源更新,以保证内存中 CPU 分配与 cgroupfs 一致(可参考第三节)。同步频率通过新增的 Kubelet 配置参数 --cpu-manager-reconcile-period 来设置。如果不指定,默认与 --node-status-update-frequency 的周期相同。关于CPU管理器需要注意以下几点:

a. 像容器运行时和 kubelet 此类的系统服务可以继续在这些独占 CPU 上运行。独占性仅针对一般 Pod。

b. CPU 管理器不支持offline/online CPUs热更新。此外,如果节点上的 CPUs 发生变化, 则必须驱逐 Pod,并通过删除 kubelet 配置的 cpu_manager_state 文件以重置 CPU 管理器。

c. 当启用 static 策略时, kube-reserved 加上 system-reserved 或 reserved-cpus 设置的 CPU 值大于零。

当前支持以下两种策略:

1. none

none 策略显式地启用现有的默认 CPU 亲和方案。通过 CFS 配额来实现 Guaranteed pods的 CPU 使用限制。

2. static

static 策略针对具有整数型 CPU请求 的 Guaranteed Pod (后续文章介绍),它允许该类 Pod 中的容器独占 CPU 资源。其基于 cpuset cgroup 控制器 实现的

从1.17版本开始,CPU保留列表可以通过 kublet 的 --reserved-cpus参数设置, 并且--reserved-cpus 指定的CPU列表优先级高于--kube-reserved 和 --system-reserved 参数指定的保留CPU,若同时指定,将进行覆盖。

a. static策略管理一个CPU共享资源池,起初,该资源池包含节点上所有的 CPU 资源。可用且独占的CPU 数量等于节点的 CPU总量减去通过 reserved-cpus或--kube-reserved 或 --system-reserved 命令行保留的 CPU(其实还有eviction资源,但当前不支持CPU类型,因此省略)。

b. 通过这些参数预留的 CPU 是以整数方式,按物理内核 ID 升序从初始共享池获取的。共享池是 BestEffort 和 Burstable pod 运行的CPU 集合。Guaranteed Pod 中的容器,如果声明了非整数值的 CPU requests ,也将运行在共享池的 CPU 上。只有 指定了正整数型的 CPU requests 的Guaranteed Pod ,才能独占 CPU 资源

c. 当 Guaranteed Pod 调度到节点上时,如果其容器符合独占要求, 相应的CPU会从共享池中移除,并放置到容器的cpuset 中。容器cgroup目录的 cpuset文件 中的 CPU 数量与 Pod 中指定的 CPU limit 相等。这种分配增强了CPU亲和性,减少了CPU上下文切换。

因为其 requests 值与 limits相等,下述Pod属于Guaranteed QoS类型, 且容器对 CPU 资源的限制值是正整数值。符合独占要求,因此该 nginx 容器独占2个CPU。

代码语言:javascript
复制
spec:
  containers:
  - name: nginx
    image: nginx
    resources:
      limits:
        memory: "200Mi"
        cpu: "2"
      requests:
        memory: "200Mi"
        cpu: "2"

三、源码分析

在介绍代码之前,zouyee先带各位看一看CPU manager的启动图(CPU manager属于Container Manager模块的子系统)

对于上图的内容,zouyee总结流程如下:

1、在命令行启动部分,Kubelet中调用NewContainerManager构建ContainerManager

2、NewContainerManager函数调用topologymanager.NewManager构建拓扑管理器

3、NewContainerManager函数调用cpumanager.NewManager构建CPU管理器

4、拓扑管理器使用AddHintPriovider方法将CPU管理器加入管理

5、回到命令行启动部分,调用NewMainKubelet(),构建Kubelet结构体

6、构建Kubelet结构体时,将CPU管理器跟拓扑管理器封装为InternalContainerLifecycle接口,其实现Pod相关的生命周期资源管理操作,涉及CPU相关的是PreStart方法

7、构建Kubelet结构体时,调用AddPodmitHandler将GetAllocateResourcesPodAdmitHandler方法加入到Pod准入插件中,在Pod创建时,资源预分配检查

8、构建Kubelet结构体后,调用ContainerManager的Start方法,ContainerManager在Start方法中调用CPU管理器的Start方法,其做一些处理工作并孵化一个goroutine,执行reconcileState()

下面依次进行讲解。

STEP 1

Kubelet调用NewContainerManager构建ContainerManager, 涉及代码为cmd/kubelet/app/server.go

在run函数中完成ContainerManager初始化工作

代码语言:javascript
复制
func run(ctx context.Context, 参数太长,不写全了){
  ....
  if kubeDeps.ContainerManager == nil {
  ...
  kubeDeps.ContainerManager, err = cm.NewContainerManager(
  ...
  )
  ...
  }
}

STEP 2-4

NewContainerManager函数调用topologymanager.NewManager构建拓扑管理器,涉及代码pkg/kubelet/cm/container_manager_linux.go

代码语言:javascript
复制
func NewContainerManager(参数太长,不写全了) {
  if utilfeature.DefaultFeatureGate.Enabled(kubefeatures.TopologyManager){
    // 判断特性是否开启,构建拓扑管理
    cm.topologyManager, err = topologymanager.NewManager(
      machineInfo.Topology,
      nodeConfig.ExperimentalTopologyManagerPolicy,
      nodeConfig.ExperimentalTopologyManagerScope,
    )
  }
  // 判断特性是否开启,构建CPU管理
  if utilfeature.DefaultFeatureGate.Enabled(kubefeatures.CPUManager) {
    cm.cpuManager, err = cpumanager.NewManager(
      nodeConfig.ExperimentalCPUManagerPolicy,
      nodeConfig.ExperimentalCPUManagerReconcilePeriod,
      machineInfo,
      nodeConfig.NodeAllocatableConfig.ReservedSystemCPUs,
      cm.GetNodeAllocatableReservation(),
      nodeConfig.KubeletRootDir,
      cm.topologyManager,
    )
    if err != nil {
      klog.Errorf("failed to initialize cpu manager: %v", err)
      return nil, err
    }
    // 拓扑管理器使用AddHintPriovider方法将CPU管理器加入管理 
    cm.topologyManager.AddHintProvider(cm.cpuManager)
  }
} 

其中关于CPU管理器的初始化:

代码语言:javascript
复制
type CPUTopology struct {
    NumCPUs    int
    NumCores   int
    NumSockets int
    CPUDetails CPUDetails
}

type CPUDetails map[int]CPUInfo

type CPUInfo struct {
    NUMANodeID int
    SocketID   int
    CoreID     int
}

func NewManager(参数太多,省略了) (Manager, error) {
  // 根据cpuPolicyName,决定初始化policy,当前支持none和static
  switch policyName(cpuPolicyName) {
    ...
    case PolicyStatic:
      // 1. 根据cadvisor的数据,生产topology结构体
      topo, err = topology.Discover(machineInfo)
      // 2. 检查reserved的CPU是否为0,需要kube+system reserved的CPU > 0
      // 3. 初始化policy
      policy, err = NewStaticPolicy(topo, numReservedCPUs, specificCPUs, affinity)
    ...
  }
}

STEP 5-7

在run函数中完成ContainerManager初始化工作后,调用RunKubelet函数构建Kubelet结构体,其最终调用NewMainKubelet(),完成Kubelet结构体构建。涉及代码pkg/kubelet/kubelet.go

代码语言:javascript
复制
func NewMainKubelet(参数太长,不写全了)(*Kubelet, error) {
  ...
  klet := &Kubelet{
  ...
  containerManager: kubeDeps.ContainerManager,
  ...
  }
  ...
  runtime, err := kuberuntime.NewKubeGenericRuntimeManager(
  ...
  // 构建Kubelet结构体时,将CPU管理器跟拓扑管理器封装为InternalContainerLifecycle接口
  kubeDeps.ContainerManager.InternalContainerLifecycle(),
  ...
  )
  ...
  // 调用AddPodmitHandler将GetAllocateResourcesPodAdmitHandler方法加入到Pod准入插件中,在Pod创建时,资源预分配检查
klet.admitHandlers.AddPodAdmitHandler(klet.containerManager.GetAllocateResourcesPodAdmitHandler())
  ...
}

其中在InternalContainerLifecycle接口,涉及CPU部分在PreStartContainer方法,涉及代码pkg/kubelet/cm/internal_container_lifecycle.go

代码语言:javascript
复制
func (i *internalContainerLifecycleImpl) PreStartContainer(参数太长,不写全了) error {
   if i.cpuManager != nil {
      i.cpuManager.AddContainer(pod, container, containerID)
   }

   if utilfeature.DefaultFeatureGate.Enabled(kubefeatures.TopologyManager) {
      err := i.topologyManager.AddContainer(pod, containerID)
      if err != nil {
         return err
      }
   }
   return nil
}

那么何时调用呢?

上面我们提到了kuberuntime.NewKubeGenericRuntimeManager,该函数实例化KubeGenericRuntimeManager结构体(后续详细介绍),而该结构体在startContainer方法中,进行调用,涉及代码pkg/kubelet/kuberuntime/kuberuntime_container.go

代码语言:javascript
复制
// 用于启动容器,该结构体实现了Runtime接口
func (m *kubeGenericRuntimeManager) startContainer(参数太多,不写了) (string, error) {
  ...
  err = m.internalLifecycle.PreStartContainer(pod, container, containerID)
  ...
}

另外GetAllocateResourcesPodAdmitHandler需要实现返回的结构体需要实现Admit接口.

代码语言:javascript
复制
func (m *resourceAllocator) Admit(attrs *lifecycle.PodAdmitAttributes) lifecycle.PodAdmitResult {
   pod := attrs.Pod

   for _, container := range append(pod.Spec.InitContainers, pod.Spec.Containers...) {
      ...

      if m.cpuManager != nil {
         err = m.cpuManager.Allocate(pod, &container)
         ...
      }
   }

   return lifecycle.PodAdmitResult{Admit: true}
}

实际调用逻辑为m.cpuManager.Allocate->m.policy.Allocate->func (p *staticPolicy) Allocate (none策略无需操作),涉及代码pkg/kubelet/cm/cpumanager/policy_static.go

代码语言:javascript
复制
func (p *staticPolicy) Allocate(s state.State, pod *v1.Pod, container *v1.Container) error {
   // 1. 如介绍所说,检查是否满足分配,即QOS为Guaranteed,且分配CPU为整型
   if numCPUs := p.guaranteedCPUs(pod, container); numCPUs != 0 {
       // 2. 获取是否分配过,分配过则更新即可
      if cpuset, ok := s.GetCPUSet(string(pod.UID), container.Name); ok {
       ...
      }
      // 3. 获取亲和性拓扑
      hint := p.affinity.GetAffinity(string(pod.UID), container.Name)
      // 4. 根据numa亲和性进行分配
      cpuset, err := p.allocateCPUs(.. )
      // 5. 设置分配结果
      s.SetCPUSet(string(pod.UID), container.Name, cpuset)
      // 6. 设置reuse字段
      p.updateCPUsToReuse(pod, container, cpuset)

  }
  // container belongs in the shared pool (nothing to do; use default cpuset)
  return nil
}

STEP 8

构建完成Kubelet结构体后,在Kubelet方法initializeRuntimeDependentModules中调用ContainerManager的Start方法,涉及代码pkg/kubelet/kubelet.go

代码语言:javascript
复制
func (kl *Kubelet) initializeRuntimeDependentModules() {
  ...
  // 这里根据我们前面说明的,需要cadvisor的数据,因此需要提前启动
  if err := kl.containerManager.Start(省略); err != nil {
    ...
  }
  ...
}

ContainerManager在Start方法中调用CPU管理器的Start方法,具体步骤如下:

a. 构建Checkpoint,其中包含文件及内存的操作

b. 根据初始化的policy,运行Start, 实际只有static起到作用,主要是校验工作

c. 孵化一个goroutine,执行reconcileState()

代码语言:javascript
复制
func (cm *containerManagerImpl) Start(参数太多,省略) error {
  ...
  // 初始化CPU管理器
  if utilfeature.DefaultFeatureGate.Enabled(kubefeatures.CPUManager) {
    ...
    err = cm.cpuManager.Start(参数太多,省略)
    ...
  }
  ...
}
// 涉及代码pkg/kubelet/cm/cpumanager/cpu_manager.go
func (m *manager) Start(参数太多,省略) error {
  ...
  // 该处为Checkpoint处理,实际为文件管理工作,即分配等情况的数据保存
  
  stateImpl, err := state.NewCheckpointState(m.stateFileDirectory, cpuManagerStateFileName, m.policy.Name(), m.containerMap)
  ...
  // 孵化一个goroutine,执行reconcileState()
  // 处理当前实际CPU分配的工作,类似actual与desired
  go wait.Until(func() { m.reconcileState() }, m.reconcilePeriod, wait.NeverStop)
}

其中reconcileState 主要完成以下工作

a. 处理当前活跃Pod,更新containerMap结构体

b.通过CRI接口更新容器的CPU配置(即m.containerruntime.UpdateContainerResources)

后续zouyee将带各位看看ContainerManager各大组件:拓扑管理、设备管理、容器管理等。

四、参考资料

1. http://abcdxyzk.github.io/blog/2015/08/07/cgroup-9/

2.http://abcdxyzk.github.io/blog/2015/02/09/kernel-mm-numa/

3.https://kubernetes.io/docs/tasks/administer-cluster/cpu-management-policies/

END

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-02-02,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 CNCF 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • b. CMP
  • c. SMP
  • 1.3 相关技术
  • NUMA
  • HT
  • cpuset
  • 1.4 数据说明
  • 1.5 结构体
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档