Kubernetesスケジューラのソースコード分析:起動前の初期化処理

概要

Kubernetesのスケジューラは、未スケジュールのPodを監視し、適切なノードを見つけてバインディング情報をAPIサーバーに書き込むコンポーネントです。この記事では、Kubernetes 1.13バージョンのスケジューラが実際のスケジューリング処理を開始する前に行われる初期化処理について、ソースコードレベルで分析します。

Cobraフレームワークとmain関数

Cobraとは

Cobraは、強力で現代的なCLIアプリケーションを作成するためのGoライブラリです。Kubernetes、Docker、Hugoなど、多くの有名なGoプロジェクトで採用されています。Cobraは以下の特徴を持っています:

  • サブコマンドの簡単な作成
  • フラグの位置に依存しないサポート
  • 自動生成されるヘルプとマニュアル
  • 高度なカスタマイズが可能

Cobraの使用方法

まず、Cobraをインストールしてプロジェクトを作成してみましょう:

# Cobraのインストール
go get -u github.com/spf13/cobra/cobra

# 新しいプロジェクトの作成
cobra init k8s-scheduler-example

生成されたプロジェクト構造は以下のようになります:

k8s-scheduler-example/
├── cmd/
│   └── root.go
├── LICENSE
└── main.go

main.goの内容は非常にシンプルです:

package main

import "k8s-scheduler-example/cmd"

func main() {
    cmd.Execute()
}

次に、サブコマンドを追加してみましょう:

cobra add version

これにより、cmdディレクトリにversion.goが作成されます。このファイルでは、rootCmdにversionCmdが追加されます:

func init() {
    rootCmd.AddCommand(versionCmd)
}

さらに、多階層のサブコマンドも作成可能です:

cobra add create -a 'server'

これにより、serverコマンドの下にcreateサブコマンドが追加され、kubectl get podsのような構造が実現できます。

Kubernetesスケジューラのmain関数

Kubernetesスケジューラのmain関数は、cmd/kube-scheduler/scheduler.goにあります。その内容は以下のようになっています:

func main() {
    command := app.NewSchedulerCommand()
    if err := command.Execute(); err != nil {
        fmt.Fprintf(os.Stderr, "%v\n", err)
        os.Exit(1)
    }
}

ここで呼び出されているNewSchedulerCommand()関数を見てみましょう。この関数は*cobra.Command型のオブジェクトを返します:

// NewSchedulerCommandはデフォルトパラメータで*cobra.Commandオブジェクトを作成します
func NewSchedulerCommand() *cobra.Command {
    cmd := &cobra.Command{
        Use: "kube-scheduler",
        Long: `Kubernetesスケジューラは、ポリシーが豊富でトポロジーを認識し、
ワークロードに特化した機能であり、可用性、パフォーマンス、キャパシティに
大きな影響を与えます。スケジューラは個々および集合的なリソース要件、
サービス品質要件、ハードウェア/ソフトウェア/ポリシーの制約、
アフィニティとアンチアフィニティの仕様、データの局所性、
ワークロード間の干渉、締め切りなどを考慮する必要があります。`,
        Run: func(cmd *cobra.Command, args []string) {
            if err := runCommand(cmd, args, opts); err != nil {
                fmt.Fprintf(os.Stderr, "%v\n", err)
                os.Exit(1)
            }
        },
    }
    return cmd
}

スケジューラの実行時に呼び出されるrunCommand関数は以下のようになっています:

// runCommandはスケジューラを実行します
func runCommand(cmd *cobra.Command, args []string, opts *options.Options) error {
    config, err := opts.Config()
    stopCh := make(chan struct{})
    // 完了した設定を取得
    completedConfig := config.Complete()
    return Run(completedConfig, stopCh)
}

最後に、実際のスケジューラの実行を担当するRun関数を見てみましょう:

// Runは指定された設定に基づいてスケジューラを実行します
// エラーが発生した場合またはstopChが閉じられた場合にのみ戻ります
func Run(completedConfig schedulerserverconfig.CompletedConfig, stopCh <-chan struct{}) error {
    // スケジューラを作成
    sched, err := scheduler.New(completedConfig.Client,
        completedConfig.InformerFactory.Core().V1().Nodes(),
        stopCh,
        scheduler.WithName(completedConfig.ComponentConfig.SchedulerName))

    // 再利用可能なrun関数を準備
    run := func(ctx context.Context) {
        sched.Run()
        <-ctx.Done()
    }

    ctx, cancel := context.WithCancel(context.TODO()) 
    defer cancel()

    go func() {
        select {
        case <-stopCh:
            cancel()
        case <-ctx.Done():
        }
    }()

    // リーダー選択が無効なため、完了するまでインラインで実行
    run(ctx)
    return fmt.Errorf("リーダー選択なしで終了しました")
}

このRun関数内で呼び出されているsched.Run()が、実際のスケジューリング処理を開始するエントリーポイントです。このメソッドはpkg/scheduler/scheduler.goに定義されています。

最後に、スケジューラの主要な構造体であるSchedulerを見てみましょう:

// Schedulerは新しくスケジュールされていないPodを監視します
// Podが適合するノードを見つけようとし、バインディングをAPIサーバーに書き戻します
type Scheduler struct {
    config *factory.Config
}

このScheduler構造体が持つConfigオブジェクトは、スケジューリング処理に必要な様々なコンポーネントを保持しています:

// Configはスケジューラの設定された入力データの実装です
type Config struct {
    // SchedulerCacheへの変更はNodeListerとAlgorithmによって
    // 観察されることが期待されます
    SchedulerCache schedulerinternalcache.Cache
    // EcacheはPodのバインドに成功した後、影響を受ける
    // キャッシュ項目を楽観的に無効化するために使用されます
    Ecache     *equivalence.Cache
    NodeLister algorithm.NodeLister
    Algorithm  algorithm.ScheduleAlgorithm
    GetBinder  func(pod *v1.Pod) Binder
    // PodConditionUpdaterはスケジューリングエラーの場合にのみ使用されます
    // スケジューリングに成功した場合、PodScheduled条件は/bindハンドラで
    // APIサーバーで更新されるため、バインディングとPodConditionの設定はアトミックです
    PodConditionUpdater PodConditionUpdater
    // PodPreemptorはPodを退去させ、Podアノテーションを更新するために使用されます
    PodPreemptor PodPreemptor

    // NextPodは次のPodが利用可能になるまでブロックする関数であるべきです
    // チャネルを使用しないのは、Podのスケジューリングに時間がかかる可能性があり、
    // Podがチャネルにある間に古くなってしまうのを避けたいからです
    NextPod func() *v1.Pod

    // SchedulingQueueはスケジュールされるPodを保持します
    SchedulingQueue internalqueue.SchedulingQueue
}

これらのコンポーネントが連携して、Kubernetesクラスタ内でのPodのスケジューリングを実現しています。次回は、このスケジューラの実際の動作プロセスについて詳しく分析していきます。

タグ: Kubernetes スケジューラ ソースコード分析 Cobra Go言語

8月4日 09:52 投稿