SR-IOV Network Operator 1.6.0 — main.go 超深度分析
文件路径:
main.go(305行)
模块定位: Operator 主入口,基于 controller-runtime 的 Manager 模式
一、模块定位
1.1 业务职责
main.go 是 SR-IOV Network Operator 的主控制器入口,运行在 Kubernetes 集群中作为 Deployment。它的核心职责包括:
- 初始化运行时 Scheme:注册所有需要管理的 CRD 资源类型到 runtime.Scheme
- 创建双 Manager 架构:一个命名空间级别 Manager(监听 operator 所在 namespace),一个全局 Manager(跨命名空间监听网络资源)
- 注册 6+1 个 Controller:SriovNetwork、SriovIBNetwork、OVSNetwork、SriovNetworkNodePolicy、SriovOperatorConfig、SriovNetworkPoolConfig + DrainReconcile
- 健康检查:配置 healthz/readyz 探针
- 优雅关闭:捕获终止信号,执行清理(调用
utils.Shutdown) - NIC ID 映射初始化:从 ConfigMap 加载支持的 NIC 设备 ID 列表
1.2 在系统中的位置
┌─────────────────────────────────────────────────┐
│ Kubernetes Cluster │
│ │
│ ┌──────────────────────────────────────────┐ │
│ │ SR-IOV Operator Deployment │ │
│ │ ┌────────────────────────────────────┐ │ │
│ │ │ main.go (本文件) │ │ │
│ │ │ ┌─────────┐ ┌──────────────┐ │ │ │
│ │ │ │ mgr │ │ mgrGlobal │ │ │ │
│ │ │ │(ns级) │ │ (全局级) │ │ │ │
│ │ │ └────┬─────┘ └──────┬───────┘ │ │ │
│ │ │ │ │ │ │ │
│ │ │ ┌────▼─────┐ ┌──────▼───────┐ │ │ │
│ │ │ │NodePolicy│ │SriovNetwork │ │ │ │
│ │ │ │Operator │ │SriovIBNetwork│ │ │ │
│ │ │ │Config │ │OVSNetwork │ │ │ │
│ │ │ │PoolConfig│ │ │ │ │ │
│ │ │ │Drain │ │ │ │ │ │
│ │ │ └──────────┘ └──────────────┘ │ │ │
│ │ └────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────┘ │
│ │
│ ┌──────────────┐ ┌────────────────────────┐ │
│ │ Config Daemon│ │ Webhook Server │ │
│ │ (DaemonSet) │ │ (Validating/Mutating) │ │
│ └──────────────┘ └────────────────────────┘ │
└─────────────────────────────────────────────────┘
二、模块整体结构
2.1 包级变量与 init() 函数
var (
scheme = runtime.NewScheme() // 全局 runtime.Scheme,用于序列化/反序列化 K8s 对象
setupLog = ctrl.Log.WithName("setup") // 带有 "setup" 标识的日志器
)
init() 函数 — Scheme 注册
func init() {
// 注册 Kubernetes 核心API(Pod, Node, ConfigMap 等)到 scheme
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
// 注册 SR-IOV 自定义资源(SriovNetwork, SriovNetworkNodeState 等)
utilruntime.Must(sriovnetworkv1.AddToScheme(scheme))
// 注册 NetworkAttachmentDefinition CRD(用于多网络接口)
utilruntime.Must(netattdefv1.AddToScheme(scheme))
// 注册 OpenShift MachineConfiguration CRD(用于节点配置管理)
utilruntime.Must(mcfgv1.AddToScheme(scheme))
// 注册 OpenShift Config CRD(用于集群配置信息)
utilruntime.Must(openshiftconfigv1.AddToScheme(scheme))
}
设计意图: init() 在包加载时自动执行,确保 Scheme 在 main() 之前就准备好。utilruntime.Must 会在失败时 panic,这是合理的——如果 Scheme 注册失败,Operator 根本无法工作。
2.2 import 分析
| Import 路径 | 用途 |
|---|---|
sriovnetworkv1 | SR-IOV CRD API 定义 |
netattdefv1 | NetworkAttachmentDefinition CRD (CNI 多网络) |
openshiftconfigv1 | OpenShift 集群配置 API |
mcfgv1 | OpenShift MachineConfig API (节点配置) |
clientgoscheme | K8s 核心 API scheme |
_ "k8s.io/client-go/plugin/pkg/client/auth" | 匿名导入,注册所有认证插件 |
_ "k8s.io/client-go/plugin/pkg/client/auth/gcp" | GCP 认证插件(重复注册,兼容性考虑) |
ctrl | controller-runtime 框架核心 |
cache | controller-runtime 缓存管理 |
healthz | 健康检查端点 |
featuregate | 特性门控 |
platforms | 平台检测(OpenShift vs Kubernetes) |
vars | 全局变量存储 |
utils | 工具函数 |
2.3 核心方法清单
| 方法 | 作用 | 调用时机 |
|---|---|---|
init() | 注册所有 CRD 类型到 Scheme | 包加载时 |
main() | 程序入口,创建 Manager,注册控制器,启动管理器 | 程序启动 |
initNicIDMap() | 从 ConfigMap 初始化支持的 NIC ID 映射 | main() 中调用 |
2.4 双 Manager 架构数据流
为什么需要双 Manager?
- mgrGlobal:需要跨命名空间监听
SriovNetwork、SriovIBNetwork、OVSNetwork——这些网络资源可以定义在任何 namespace 中,但其spec.networkNamespace指向目标 namespace - mgr:只需要监听 operator 所在 namespace 的
SriovNetworkNodePolicy、SriovOperatorConfig、SriovNetworkPoolConfig等,缩小缓存范围,降低内存消耗
三、核心业务逻辑深度解析
3.1 完整执行流程
3.2 main() 函数逐行解析
3.2.1 Flag 解析与日志初始化
func main() {
var metricsAddr string
var probeAddr string
// 定义命令行 flag:metrics 端口,默认 :8080
flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080",
"The address the metric endpoint binds to.")
// 定义命令行 flag:健康探针端口,默认 :8081
flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081",
"The address the probe endpoint binds to.")
// 将日志相关 flag 绑定到标准 flag 库(如 log-level 等)
snolog.BindFlags(flag.CommandLine)
flag.Parse() // 解析命令行参数
snolog.InitLog() // 初始化日志系统
设计意图: 使用标准的 flag 包而非 cobra,因为 Operator 是简单的单命令程序。snolog.BindFlags 允许用户通过 --log-level 等参数控制日志详细程度。
3.2.2 获取 K8s 配置与 RESOURCE_PREFIX 检查
// 获取 in-cluster REST 配置,失败则 panic
// 内部读取 /etc/kubernetes/serviceaccount/ 下的 token 和 ca.crt
restConfig := ctrl.GetConfigOrDie()
// 检查 RESOURCE_PREFIX 环境变量(由 vars.init() 从环境读取)
// RESOURCE_PREFIX 用于 device plugin 暴露设备时的资源名前缀
// 例如: "openshift.io" → 资源名 "openshift.io/intel_nic_kmod_..."
if vars.ResourcePrefix == "" {
setupLog.Error(nil, "RESOURCE_PREFIX environment variable can't be empty")
os.Exit(1) // 没有 prefix 就无法暴露设备资源,必须退出
}
RESOURCE_PREFIX 的重要性: SR-IOV Device Plugin 通过这个前缀向 Kubernetes 注册扩展资源。例如 intel.com/sriov_net_device 中的 intel.com 就是前缀。没有它,Pod 无法请求 SR-IOV VF 设备。
3.2.3 创建命名空间 Manager (mgr)
// 创建命名空间级别的 controller-runtime Manager
mgr, err := ctrl.NewManager(restConfig, ctrl.Options{
Scheme: scheme, // 使用全局 scheme
HealthProbeBindAddress: probeAddr, // 健康检查地址 :8081
Metrics: server.Options{BindAddress: metricsAddr}, // Metrics 端点 :8080
WebhookServer: webhook.NewServer(webhook.Options{Port: 9443}), // Webhook 端口 9443
Cache: cache.Options{
DefaultNamespaces: map[string]cache.Config{
vars.Namespace: {}, // 只缓存 operator 所在 namespace 的资源
},
},
})
if err != nil {
setupLog.Error(err, "unable to create manager")
os.Exit(1)
}
关键设计决策——命名空间缓存: DefaultNamespaces 限制 cache 只监听 vars.Namespace(operator 所在 namespace)的资源变更。这大幅减少了内存消耗和 API Server 压力,因为 NodePolicy、OperatorConfig 等资源只存在于 operator namespace。
3.2.4 健康检查注册
// 注册 liveness 探针(healthz)
// healthz.Ping 是一个简单的健康检查,只要 Manager 能响应就返回 OK
if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
setupLog.Error(err, "unable to set up health check")
os.Exit(1)
}
// 注册 readiness 探针(readyz)
// readyz.Ping 表示 controller 已准备好处理请求
if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil {
setupLog.Error(err, "unable to set up ready check")
os.Exit(1)
}
K8s 探针映射:
healthz→ livenessProbe → 失败则重启 Podreadyz→ readinessProbe → 失败则从 Service Endpoints 移除
3.2.5 创建全局 Manager (mgrGlobal)
// 创建全局 Manager(不限制 namespace)
// 用于监听跨 namespace 的网络资源
mgrGlobal, err := ctrl.NewManager(restConfig, ctrl.Options{
Scheme: scheme,
Metrics: server.Options{BindAddress: "0"}, // 禁用 metrics(避免端口冲突)
})
if err != nil {
setupLog.Error(err, "unable to start global manager")
os.Exit(1)
}
BindAddress: "0" 的含义: 设置为 “0” 表示禁用 metrics server。因为 mgr 已经在 :8080 上暴露了 metrics,mgrGlobal 不需要重复暴露。
3.2.6 Field 索引注册(3 个)
// 为 SriovNetwork 创建 field index: "spec.networkNamespace"
// 这个索引允许通过 networkNamespace 快速查找所有指向同一 namespace 的 SriovNetwork
err = mgrGlobal.GetCache().IndexField(context.Background(),
&sriovnetworkv1.SriovNetwork{}, // 目标 CRD 类型
"spec.networkNamespace", // 索引字段名
func(o client.Object) []string {
// 提取函数:返回该对象的 networkNamespace 值
return []string{o.(*sriovnetworkv1.SriovNetwork).Spec.NetworkNamespace}
})
if err != nil {
setupLog.Error(err, "unable to create index field for cache")
os.Exit(1)
}
Field Index 的作用: controller-runtime 的 cache 默认只能按 namespace 和 name 索引对象。通过 IndexField,我们创建了一个自定义索引,使得控制器可以通过 client.MatchingFields{"spec.networkNamespace": "my-ns"} 快速查询所有指向特定 namespace 的 SriovNetwork。这对网络资源到 namespace 的映射至关重要。
同样的模式重复两次,分别为 SriovIBNetwork 和 OVSNetwork 创建索引:
// SriovIBNetwork 的 networkNamespace 索引
err = mgrGlobal.GetCache().IndexField(context.Background(),
&sriovnetworkv1.SriovIBNetwork{}, "spec.networkNamespace",
func(o client.Object) []string {
return []string{o.(*sriovnetworkv1.SriovIBNetwork).Spec.NetworkNamespace}
})
// ... 错误处理 ...
// OVSNetwork 的 networkNamespace 索引
err = mgrGlobal.GetCache().IndexField(context.Background(),
&sriovnetworkv1.OVSNetwork{}, "spec.networkNamespace",
func(o client.Object) []string {
return []string{o.(*sriovnetworkv1.OVSNetwork).Spec.NetworkNamespace}
})
// ... 错误处理 ...
3.2.7 NIC ID 映射初始化
// 从 ConfigMap 加载支持的 NIC ID 列表
// 这个 ConfigMap 包含了所有经过测试的 NIC 的 PCI Vendor:Device ID
if err := initNicIDMap(); err != nil {
setupLog.Error(err, "unable to init NicIdMap")
os.Exit(1)
}
initNicIDMap 函数实现:
func initNicIDMap() error {
// 创建一个原始的 Kubernetes client(非 controller-runtime client)
// NewForConfigOrDie 在失败时 panic
kubeclient := kubernetes.NewForConfigOrDie(ctrl.GetConfigOrDie())
// 从 operator namespace 中的 ConfigMap 读取 NIC ID 数据
if err := sriovnetworkv1.InitNicIDMapFromConfigMap(kubeclient, vars.Namespace); err != nil {
return err
}
return nil
}
NIC ID Map 的作用: SR-IOV 只支持特定的网卡型号。这个 ConfigMap 包含了所有支持的 PCI Vendor:Device ID 组合(如 8086:1583 对应 Intel XXV710)。在运行时,Daemon 会检查节点上的网卡 PCI ID 是否在支持列表中。
3.2.8 全局变量赋值
// 将 REST 配置和 Scheme 存入全局变量
// 其他包(如 daemon、helper)通过 vars.Config 和 vars.Scheme 访问
vars.Config = restConfig
vars.Scheme = mgrGlobal.GetScheme()
// 创建平台检测器
// 用于判断运行在 OpenShift 还是原生 Kubernetes 上
platformsHelper, err := platforms.NewDefaultPlatformHelper()
if err != nil {
setupLog.Error(err, "couldn't create openshift context")
os.Exit(1)
}
// 创建特性门控实例
// FeatureGate 用于控制新特性的启用/禁用
featureGate := featuregate.New()
3.2.9 注册全局控制器(3 个)
// 1. SriovNetworkReconciler — 管理 SriovNetwork CR
// 负责创建/更新/删除 NetworkAttachmentDefinition
// 使用 mgrGlobal 的 client(跨 namespace)
if err = (&controllers.SriovNetworkReconciler{
Client: mgrGlobal.GetClient(),
Scheme: mgrGlobal.GetScheme(),
}).SetupWithManager(mgrGlobal); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "SriovNetwork")
os.Exit(1)
}
// 2. SriovIBNetworkReconciler — 管理 SriovIBNetwork CR
// 类似 SriovNetwork,但针对 InfiniBand 网络
if err = (&controllers.SriovIBNetworkReconciler{
Client: mgrGlobal.GetClient(),
Scheme: mgrGlobal.GetScheme(),
}).SetupWithManager(mgrGlobal); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "SriovIBNetwork")
os.Exit(1)
}
// 3. OVSNetworkReconciler — 管理 OVSNetwork CR
// 管理 OVS(Open vSwitch)网络资源
if err = (&controllers.OVSNetworkReconciler{
Client: mgrGlobal.GetClient(),
Scheme: mgrGlobal.GetScheme(),
}).SetupWithManager(mgrGlobal); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "OVSNetwork")
os.Exit(1)
}
3.2.10 注册命名空间控制器(3 个)
// 4. SriovNetworkNodePolicyReconciler — 管理 SriovNetworkNodePolicy CR
// 策略资源:定义哪些节点应该配置哪些 PF/VF
// 使用 mgr 的 client(仅 namespace 级别)
if err = (&controllers.SriovNetworkNodePolicyReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
FeatureGate: featureGate, // 注入特性门控
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "SriovNetworkNodePolicy")
os.Exit(1)
}
// 5. SriovOperatorConfigReconciler — 管理 SriovOperatorConfig CR
// Operator 自身的配置资源(日志级别、特性门控等)
// 注入 PlatformHelper 和 UncachedAPIReader
if err = (&controllers.SriovOperatorConfigReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
PlatformHelper: platformsHelper,
FeatureGate: featureGate,
UncachedAPIReader: mgr.GetAPIReader(), // 不经过缓存的直接 API reader
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "SriovOperatorConfig")
os.Exit(1)
}
// 6. SriovNetworkPoolConfigReconciler — 管理 SriovNetworkPoolConfig CR
// 节点池配置:定义一组节点的 SR-IOV 配置
if err = (&controllers.SriovNetworkPoolConfigReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
PlatformHelper: platformsHelper,
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "SriovNetworkPoolConfig")
os.Exit(1)
}
UncachedAPIReader 的设计意图: mgr.GetAPIReader() 返回一个直接访问 API Server 的 reader,不经过本地缓存。这在需要读取最新数据时很重要——SriovOperatorConfig 的变更需要立即反映,不能容忍缓存的延迟。
3.2.11 Drain 控制器创建
// 创建一个禁用缓存的 client,专门用于 Drain 控制器
// Drain 操作需要读取最新的 NodeState、Node、MachineConfigPool 状态
// 使用缓存可能导致 drain 决策基于过期数据
drainKClient, err := client.New(restConfig, client.Options{
Scheme: scheme,
Cache: &client.CacheOptions{
DisableFor: []client.Object{
&sriovnetworkv1.SriovNetworkNodeState{}, // 节点状态必须实时
&corev1.Node{}, // 节点信息必须实时
&mcfgv1.MachineConfigPool{}, // MCP 状态必须实时
},
},
})
if err != nil {
setupLog.Error(err, "unable to create drain kubernetes client")
os.Exit(1)
}
// 创建 Drain 控制器
// 负责在配置变更前 drain 节点(驱逐 Pod),确保安全变更
drainController, err := controllers.NewDrainReconcileController(
drainKClient,
mgr.GetScheme(),
mgr.GetEventRecorderFor("SR-IOV operator"), // 事件记录器
platformsHelper)
if err != nil {
setupLog.Error(err, "unable to create controller", "controller", "DrainReconcile")
os.Exit(1)
}
if err = drainController.SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to setup controller with manager", "controller", "DrainReconcile")
os.Exit(1)
}
3.2.12 双 Manager 启动
// 设置信号处理器,监听 SIGINT/SIGTERM
stopSignalCh := ctrl.SetupSignalHandler()
// 为 globalManager 创建可取消的 context
globalManagerErr := make(chan error)
globalManagerCtx, globalManagerCancel := context.WithCancel(context.Background())
go func() {
setupLog.Info("starting global manager")
// 启动全局 Manager(阻塞,直到 context 取消)
globalManagerErr <- mgrGlobal.Start(globalManagerCtx)
}()
// 为 namespacedManager 创建可取消的 context
namespacedManagerErr := make(chan error)
namespacedManagerCtx, namespacedManagerCancel := context.WithCancel(context.Background())
go func() {
setupLog.Info("starting namespaced manager")
// 启动命名空间 Manager(阻塞,直到 context 取消)
namespacedManagerErr <- mgr.Start(namespacedManagerCtx)
}()
为什么用 goroutine? mgr.Start() 是阻塞调用,会一直运行直到 context 取消或出错。要在同一进程中运行两个 Manager,必须在各自的 goroutine 中启动。
3.2.13 Shutdown Client 创建
// 创建一个专用于关闭流程的 client
// 禁用 SriovNetwork 缓存——需要读取最新的网络资源状态来清理
shutdownClient, err := client.New(restConfig, client.Options{
Scheme: vars.Scheme,
Cache: &client.CacheOptions{
DisableFor: []client.Object{
&sriovnetworkv1.SriovNetwork{},
},
},
})
if err != nil {
setupLog.Error(err, "unable to create generic client for shutdown process")
os.Exit(1)
}
3.2.14 优雅关闭 select 循环
select {
// 情况1:收到终止信号(SIGINT/SIGTERM)
case <-stopSignalCh.Done():
setupLog.Info("Stop signal received")
// 取消两个 Manager 的 context
globalManagerCancel()
namespacedManagerCancel()
// 等待两个 Manager 完全停止
<-globalManagerErr
<-namespacedManagerErr
// 执行清理(清理 webhook、daemonset 等资源)
utils.Shutdown(shutdownClient)
// 情况2:全局 Manager 出错
case err := <-globalManagerErr:
setupLog.Error(err, "Global Manager error")
// 取消命名空间 Manager
namespacedManagerCancel()
<-namespacedManagerErr
utils.Shutdown(shutdownClient)
os.Exit(1) // 出错退出码 1
// 情况3:命名空间 Manager 出错
case err := <-namespacedManagerErr:
setupLog.Error(err, "Namsepaced Manager error")
// 取消全局 Manager
globalManagerCancel()
<-globalManagerErr
utils.Shutdown(shutdownClient)
os.Exit(1)
}
3.3 控制器注册全景图
四、Mermaid 图表汇总
4.1 模块架构图
4.2 类关系图
4.3 启动序列图
4.4 错误处理流程图
4.5 资源监听范围对比图
4.6 Controller 依赖注入图
五、关键设计模式总结
5.1 双 Manager 模式
Operator 使用两个 Manager 实例分离关注点:全局网络资源 vs 命名空间级配置资源。这是 controller-runtime 中的高级模式,适用于需要跨命名空间监听部分资源但又想限制其他资源缓存范围的场景。
5.2 依赖注入模式
所有 Reconciler 都通过结构体字段接收依赖(Client、Scheme、FeatureGate 等),而非全局变量。这使得控制器可测试——在单元测试中可以注入 mock client。
5.3 优雅关闭模式
通过 context.WithCancel + chan error + select 实现优雅关闭。无论哪个 Manager 出错,都会取消另一个并执行 utils.Shutdown 清理。
5.4 分层缓存策略
- mgr: namespace 级缓存(减少内存)
- mgrGlobal: 全局缓存 + Field 索引(快速跨 ns 查询)
- drainKClient: 禁用缓存(实时数据)
- shutdownClient: 部分禁用缓存(清理需要实时数据)
5.5 错误处理策略
- 初始化阶段:
os.Exit(1)快速失败 - 运行时阶段:通过 channel 传播错误,优雅关闭
- 重复注册 GCP auth:兼容性考虑,确保不同 K8s 发行版都能正常认证

2855

被折叠的 条评论
为什么被折叠?



