目录

TiDB Operator 自动扩缩容:TidbClusterAutoScaler 使用与实现

警告
本文最后更新于 2023-08-31,文中内容可能已过时。

在 Kubernetes 上运行 TiDB 时,最常见的运维动作之一就是扩缩容。TiDB 计算节点 TiDB 是无状态的,扩缩容相对简单;TiKV 存储节点涉及数据重分布,需要谨慎处理。

TiDB Operator 里提供了一个叫 TidbClusterAutoScaler 的 CRD,试图让 TiDB 和 TiKV 的副本数根据负载自动调整。它支持两种决策方式:一种是把决策交给 PD,由 PD 根据集群负载给出扩缩容计划;另一种是调用外部服务,由用户自定义推荐副本数。

不过,这个特性长期处于实验状态,官方文档也比较少。本文基于 TiDB Operator v1.6.0-alpha.7 的代码,从使用到源码做一次完整梳理。

  • 一个已经运行的 TidbCluster
  • 一套 TiDB Monitor(PD 依赖它采集负载指标)
  • TiDB Operator 版本支持 TidbClusterAutoScaler,本文使用 v1.6.0-alpha.7

以下 YAML 会让 TiKV 在 CPU 使用率超过 80% 时自动扩容,低于 10% 时自动缩容:

apiVersion: pingcap.com/v1alpha1
kind: TidbClusterAutoScaler
metadata:
  name: basic
spec:
  cluster:
    name: basic
  tikv:
    rules:
      cpu:
        max_threshold: 0.8
        min_threshold: 0.1
        resource_types:
        - default_tikv
    resources:
      default_tikv:
        cpu: "1000m"
        memory: "1Gi"
        storage: "10Gi"

执行:

kubectl apply -f tidbcluster-autoscaler.yaml

查看状态:

kubectl get ta basic -o yaml

注意:这个 CRD 不会直接修改原 TidbClusterreplicas。如果 PD 判断需要扩容,Operator 会创建一个新的 TidbCluster 资源来承载新的 TiKV group。

spec:
  tidb:
    rules:
      cpu:
        max_threshold: 0.8
        min_threshold: 0.1
        resource_types:
        - default_tidb
    resources:
      default_tidb:
        cpu: "1000m"
        memory: "1Gi"

TiDB 计算节点无状态,扩缩容时只需要改副本数,不需要像 TiKV 那样考虑数据迁移。

默认缩容间隔是 500 秒,扩容间隔是 300 秒。可以通过以下字段调整:

spec:
  tikv:
    scaleInIntervalSeconds: 600
    scaleOutIntervalSeconds: 120

这个冷静期机制很重要,避免负载抖动导致反复扩缩容。

TidbClusterAutoScaler 的定义在 pkg/apis/pingcap/v1alpha1/tidbclusterautoscaler_types.go

顶层结构很简单:

type TidbClusterAutoScaler struct {
    metav1.TypeMeta
    metav1.ObjectMeta
    Spec   TidbClusterAutoScalerSpec
    Status TidbClusterAutoScalerStatus
}

type TidbClusterAutoScalerSpec struct {
    Cluster TidbClusterRef        // 目标 TidbCluster
    TiKV    *TikvAutoScalerSpec    // TiKV 自动扩缩容配置
    TiDB    *TidbAutoScalerSpec    // TiDB 自动扩缩容配置
}

TikvAutoScalerSpecTidbAutoScalerSpec 都内嵌了 BasicAutoScalerSpec

type BasicAutoScalerSpec struct {
    Rules                   map[corev1.ResourceName]AutoRule
    ScaleInIntervalSeconds  *int32
    ScaleOutIntervalSeconds *int32
    External                *ExternalConfig
    Resources               map[string]AutoResource
}

Rules 是触发扩缩容的判断条件。以 CPU 规则为例:

type AutoRule struct {
    MaxThreshold  float64   // 扩容阈值
    MinThreshold  *float64  // 缩容阈值
    ResourceTypes []string  // 命中时使用的资源模板
}

当 CPU 使用率超过 MaxThreshold 时,PD 会建议扩容;低于 MinThreshold 时,会建议缩容。ResourceTypes 对应 Resources 中定义的 key。

type AutoResource struct {
    CPU     resource.Quantity
    Memory  resource.Quantity
    Storage resource.Quantity
    Count   *int32
}

这是扩缩容时使用的资源模板。PD 会从这个模板列表里选择合适的规格来生成扩缩容计划。

如果不想用 PD 做决策,也可以接入外部推荐服务:

type ExternalConfig struct {
    Endpoint    ExternalEndpoint
    MaxReplicas int32
}

type ExternalEndpoint struct {
    Host      string
    Port      int32
    Path      string
    TLSSecret *SecretRef
}

外部服务返回推荐副本数,Operator 会直接根据这个值创建或更新独立的 TidbCluster

type TidbClusterAutoScalerStatus struct {
    TiKV map[string]TikvAutoScalerStatus
    TiDB map[string]TidbAutoScalerStatus
}

type BasicAutoScalerStatus struct {
    LastAutoScalingTimestamp *metav1.Time
}

Status 只记录每个 group 最后一次扩缩容的时间戳,用于控制冷静期。

TidbClusterAutoScaler 的核心逻辑并不直接修改原 TidbCluster 的副本数,而是基于 PD 或外部服务的建议,创建、更新、删除额外的 TidbCluster 资源。

PD 计划模式

这是默认模式。Operator 把 TAC 中配置的 rules 和 resources 转换成 pdapi.Strategy,调用 PD 的 /pd/api/v1/autoscaling 接口获取扩缩容计划。PD 根据集群负载返回一组 Plan,每个 Plan 包含:

  • Component:tidb 或 tikv
  • Count:建议副本数
  • ResourceType:使用哪个资源模板
  • Labels:group 标签

Operator 拿到这些 Plan 后,去创建、更新或删除对应的 autoscaling TC。

外部服务模式

如果 spec.tikv.externalspec.tidb.external 有配置,Operator 会跳过 PD,直接调用外部 HTTP 服务。外部服务返回一个推荐副本数,Operator 会创建或更新名为 <原TC名>-<component>-external 的独立 TC。

                +---------------------+
                | TidbClusterAutoScaler |
                +----------+----------+
                           |
                           v
              +------------------------+
              |  tidb-controller-manager |
              +----------+-------------+
                         |
         +---------------+---------------+
         |                               |
         v                               v
+--------+-------+              +-------+--------+
|       PD        |              | External Service |
| GetAutoscalingPlans |          | /recommend       |
+--------+-------+              +-------+--------+
         |                               |
         v                               v
+--------+--------------------------------+--------+
|              创建 / 更新 / 删除                     |
|         独立的 TidbCluster(autoscaling)          |
+--------------------------------------------------+

原 TC 是用户显式声明的期望状态。如果 AutoScaler 直接修改原 TC 的 replicas,相当于和用户配置打架。生成独立 TC 的好处是:

  • 原 TC 保持稳定,用户仍然清楚自己的集群长什么样
  • 不同 group 可以配置不同的资源规格
  • AutoScaler 生成的 TC 可以设置 ownerReference,跟随 TAC 一起清理

但这也带来复杂度:集群被拆成了多个 TC,资源管理、监控、命名空间视角下都更混乱。

控制器在 pkg/controller/autoscaler/tidbcluster_autoscaler_controller.go

func NewController(deps *controller.Dependencies) *Controller {
    t := &Controller{
        deps:    deps,
        control: NewDefaultAutoScalerControl(autoscaler.NewAutoScalerManager(deps)),
        queue: workqueue.NewNamedRateLimitingQueue(...),
    }
    tidbAutoScalerInformer := deps.InformerFactory.Pingcap().V1alpha1().TidbClusterAutoScalers()
    controller.WatchForObject(tidbAutoScalerInformer.Informer(), t.queue)
    return t
}

这是一个标准的 Kubernetes 控制器:监听 TidbClusterAutoScaler 变化,事件进入队列,worker 逐个处理。

processNextWorkItem 调用 sync

func (c *Controller) sync(key string) (err error) {
    ns, name, err := cache.SplitMetaNamespaceKey(key)
    ta, err := c.deps.TiDBClusterAutoScalerLister.TidbClusterAutoScalers(ns).Get(name)
    return c.control.ReconcileAutoScaler(ta)
}

核心逻辑在 pkg/autoscaler/autoscaler/autoscaler_manager.goSync 方法:

func (am *autoScalerManager) Sync(tac *v1alpha1.TidbClusterAutoScaler) error {
    // 1. 获取目标 TidbCluster
    tc, err := am.deps.TiDBClusterLister.TidbClusters(...).Get(tcName)

    // 2. 填充默认值
    defaultTAC(tac, tc)

    // 3. 校验
    if err := validateTAC(tac); err != nil {
        return nil
    }

    // 4. 执行扩缩容
    updatedTac := tac.DeepCopy()
    if err := am.syncAutoScaling(tc, updatedTac); err != nil {
        return err
    }

    // 5. 更新 TAC status
    return am.updateTidbClusterAutoScaler(updatedTac)
}

流程很清晰:找目标 TC → 默认值 → 校验 → 执行 → 更新状态。

syncAutoScaling 根据组件选择 PD 模式或外部模式:

func (am *autoScalerManager) syncAutoScaling(tc, tac) error {
    if tac.Spec.TiDB != nil {
        if tac.Spec.TiDB.External != nil {
            am.syncExternal(...)
        } else {
            am.syncPD(...)
        }
    }
    if tac.Spec.TiKV != nil { ... }
}

syncPDpkg/autoscaler/autoscaler/pdplan_autoscaler.go 中:

func (am *autoScalerManager) syncPD(tc, tac, component) error {
    strategy := autoscalerToStrategy(tac, component)
    plans, err := controller.GetPDClient(...).GetAutoscalingPlans(*strategy)
    if err != nil { return err }
    return am.syncPlans(tc, tac, plans, component)
}

autoscalerToStrategy 把 TAC 的 resources 和 rules 转成 PD 能理解的结构:

type Strategy struct {
    Rules     []*Rule
    Resources []*Resource
}

拿到 PD 返回的 Plan 后,syncPlans 做三件事:

  1. 找出当前已经存在的 autoscaling groups
  2. 对比 PD 计划与当前 groups
  3. 创建新增的、更新变更的、删除多余的
toDelete := existedGroups.Difference(planGroups)
toUpdate := planGroups.Intersection(existedGroups)
toCreate := planGroups.Difference(existedGroups)

创建新 TC 时,代码会克隆原 TC 的 spec,但只保留一个组件:

autoTc := newAutoScalingCluster(tc, tac, autoTcName, component)
autoTc.Spec.TiCDC = nil
autoTc.Spec.TiFlash = nil
autoTc.Spec.PD = nil
autoTc.Spec.Pump = nil

pkg/autoscaler/autoscaler/external_autoscaler.go 更简单。Operator 调用外部服务拿到 targetReplicas,然后创建或更新名为 <tcname>-<component>-external 的 TC:

externalTcName := fmt.Sprintf(externalTcNamePattern, tc.Name, component.String())

如果 targetReplicas <= 0,则优雅地删除这个 external TC。

pkg/autoscaler/autoscaler/util.go 里有几个关键函数:

  • defaultTAC:填充默认资源模板、默认间隔、默认阈值
  • validateTAC:校验 rules、resources、thresholds 是否合法
  • autoscalerToStrategy:把 CRD 转成 PD Strategy
  • newAutoScalingCluster:克隆原 TC 生成 autoscaling TC
  • checkAutoScaling:根据 LastAutoScalingTimestamp 判断是否在冷静期内

checkAutoScaling 逻辑:

func checkAutoScaling(tac, memberType, group, beforeReplicas, afterReplicas) bool {
    if beforeReplicas > afterReplicas {
        // 缩容,检查 scaleInIntervalSeconds
    } else if beforeReplicas < afterReplicas {
        // 扩容,检查 scaleOutIntervalSeconds
    }
    return true
}

冷静期机制是这段代码里比较务实的部分,避免负载抖动导致反复扩缩容。

  1. 与 PD 联动:TiKV 数据分布的负载信息只有 PD 最清楚,把扩缩容决策交给 PD 是合理的。
  2. 资源模板化:通过 resources 定义多种规格,PD 可以按需选择不同资源类型的 group。
  3. 独立 TC 不污染原集群:用户配置和自动扩缩容产物分离,职责清晰。
  4. 支持外部推荐:如果内部规则不够,可以接入自己的决策服务。
  1. 实验性,未 GA:长期处于非稳定状态,官方文档和维护投入有限。
  2. 生成多个 TC 管理复杂:命名空间下会出现多个 TidbCluster,监控、运维、排查都会变复杂。
  3. TiKV 扩缩容仍然重:创建新 TiKV group 涉及数据重分布,不是瞬间完成的,自动扩缩容的及时性有限。
  4. 依赖 PD autoscaling API:PD 侧的自动扩缩容计划本身成熟度有限,Operator 这边能做的只是执行者。
  5. 缺少 min/max replicas 约束:CRD 里没有直接限制总副本数上下限,完全依赖 PD 或外部服务返回合理值。

TidbClusterAutoScaler 的设计思路是对的:把决策和 execution 分离,Operator 只做 Kubernetes 侧的资源编排,PD 或外部服务负责决策。但问题在于这个特性在 TiDB 生态里一直处于比较尴尬的位置——PD 的 autoscaling 计划不够成熟,用户侧真正用起来的场景也不多,导致代码长期缺乏维护。

对于 TiDB 计算层 TiDB,直接用 HPA 可能更自然;对于 TiKV 存储层,由于数据迁移成本,真正意义上的"自动弹性"在分布式数据库里本身就是一个难题。这个 CRD 更像是一个探索性实现,而不是一个成熟的生产特性。

  • TidbClusterAutoScaler 是 TiDB Operator 提供的自动扩缩容 CRD,支持 PD 决策和外部推荐两种模式
  • 它不会直接修改原 TidbCluster,而是创建独立的 autoscaling TC
  • 源码实现清晰,分为控制器、Sync 主流程、PD 计划执行、外部模式执行四部分
  • 该特性长期处于实验状态,生产使用需谨慎

后续如果想继续深入,可以看看 PD 侧的 autoscaling API 实现,以及 tipocket 里相关的 autoscaling 测试 case。