最近,我花了很多时间使用go operator-sdk编写Kubernetes操作员,该操作员是在Kubebuilder框架之上构建的。这是我过去几个月使用这些框架的一些技巧和技巧的列表。
日志格式
Kubebuilder与K8S生态系统的许多大多数一样,都利用zap进行记录。开箱即用,KubeBuilder ZAP配置为每个日志输出时间戳,该日志使用科学符号进行了格式化。这使我很难通过瞥了一眼活动的时间来阅读活动的时间。就我个人而言,我更喜欢ISO 8601,所以让我们更改它!
在脚手架的main.go
中,您可以通过修改zap.Options
struct并调用ctrl.SetLogger
来配置当前的记录器格式。
opts := zap.Options{
Development: true,
TimeEncoder: zapcore.ISO8601TimeEncoder,
}
ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
在这种情况下,我添加了zapcore.ISO8601TimeEncoder
,该zapcore.ISO8601TimeEncoder
将时间戳编码为可读的ISO 8601构造字符串。挖掘了一些挖掘,并从库伯尼特(Kubernetes Slack Org)那里得到了一些帮助,才弄清楚了这一点。但是,在调试复杂的和解循环时,尤其是在多线程环境中,这是一个巨大的生活质量改善。
maxConcurrentReconciles
说到多线程环境,默认情况下,操作员只能运行单个和解循环。但是,在实践中,尤其是在运行全球范围的控制器时,运行多个并发和解循环以同时处理许多资源更改很有用。幸运的是,操作员SDK使用MaxConcurrentReconciles
设置使它变得非常容易。我们可以在新的控制器的SetupWithManager
func中进行设置:
func (r *CustomReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
WithOptions(controller.Options{MaxConcurrentReconciles: 10}).
...
Complete(r)
}
我已经在我的main.go
文件中创建了一个命令行arg,允许用户将此值设置为任何整数值,因为这可能会随着时间的推移而进行调整,具体取决于控制器在生产群集中的执行方式。 P>
亲子关系
控制器的基本功能之一是充当Kubernetes资源的父母。这允许控制器“拥有”
这些对象使其被删除后,所有子对象都是由kubernetes运行时自动收集的。
我喜欢这个小功能,可以要求任何client.Object
添加父级引用
你在写。
func (r *CustomReconciler) ownObject(ctx context.Context, cr *myapiv1alpha1.CustomResource, obj client.Object) error {
err := ctrl.SetControllerReference(cr, obj, r.Scheme)
if err != nil {
return err
}
return r.Update(ctx, obj)
}
然后,您可以在SetupWithManager
func中添加这些资源的Owns
手表。这些将指示您的控制器聆听指定类型的儿童资源的更改,从而在每个更改上触发和解循环。
func (r *CustomReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
Owns(&v1apps.Deployment{}).
Owns(&v1core.ConfigMap{}).
Owns(&v1core.Service{}).
Complete(r)
}
手表
您的控制器还可以观察其没有自己的资源。这对于您需要注意诸如PersistentVolumes或Nodes等全球资源的变化时很有用。这是您如何在SetupWithManager
func中注册此手表的示例。
func (r *CustomReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
Watches(
&source.Kind{Type: &v1core.Node{}},
handler.EnqueueRequestsFromMapFunc(myNodeFilterFunc),
builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}),
).
Complete(r)
}
在这种情况下,您需要实现myNodeFilterFunc
接受
obj client.Object
并返回[]reconcile.Request
。使用
ResourceVersionChangedPredicate
触发该资源类型上的每个更改的过滤功能,因此对
很重要
写出您的过滤功能以提高效率,因为有可能被称为很多,尤其是
如果您的控制器是全球范围的。
现场索引器
我遇到的一个陷阱发生在试图查询在特定节点上运行的POD列表时。此查询使用fieldSelector过滤器,如
在这里看到:
// Get a list of all pods on the node
err := c.List(ctx, &pods, &client.ListOptions{
Namespace: "",
FieldSelector: fields.ParseSelectorOrDie(fmt.Sprintf("spec.nodeName=%s", node.Name)),
})
此编码器导致以下错误:Index with name field:spec.nodeName does not exist
。经过一些谷歌搜索,我
找到引用的this GitHub issue
一个包含答案的Kubebuilder docs page。
使用操作员SDK和KubeBuilder创建的控制器使用内置的缓存机制来存储API请求的结果。这是为了防止
垃圾邮件K8S API,并提高和解绩效。
使用FieldSelectors执行资源查找时,首先需要将所需的搜索字段添加到索引
缓存可以用于查找。这是一个示例,它将为POD的nodeName
构建此索引:
if err := mgr.GetFieldIndexer().IndexField(context.TODO(), &v1core.Pod{}, "spec.nodeName", func(rawObj client.Object) []string {
pod := rawObj.(*v1core.Pod)
return []string{pod.Spec.NodeName}
}); err != nil {
return err
}
现在,我们可以使用fieldSelector从上方运行List
函数,没有任何问题。
在冲突中进行检验
如果您曾经编写过控制器,那么您可能非常熟悉错误Operation cannot be fulfilled on ...: the object has been modified; please apply your changes to the latest version and try again
这发生在您当前在控制器中的资源的版本与K8S群集状态的最新版本中的最新版本之外。如果您要在任何错误上重试和解循环,则您的控制器最终将调和资源,但这确实可以污染您的日志并难以发现更重要的错误。
阅读了K8S来源后,我找到了解决方案:RetryOnConflict
。这是client-go
软件包中的utility function,它运行一个函数并自动在冲突上重试,直到某个点。
现在,您可以将逻辑包装在此函数参数中,而不必再担心这个问题!额外的好处是,您只能到达return err
而不是return ctrl.Result{}, err
,这使您的代码更容易阅读。
有用的Kubebuilder标记
这是我在开发操作员时发现的一些有用的代码标记。
要在自定义资源的描述中添加自定义列(运行kubectl get
),您可以将注释添加到API对象中:
//+kubebuilder:printcolumn:name="NodeReady",type="boolean",JSONPath=".status.nodeReady"
//+kubebuilder:printcolumn:name="NodeIp",type="string",JSONPath=".status.nodeIp"
要在您的自定义资源中添加短名称(例如pvc
,例如PersistentVolumeClaim
),您可以添加此注释:
//+kubebuilder:resource:shortName=mycr;mycrs
可以在此处找到有关KubeBuilder标记的更多文档:
https://book.kubebuilder.io/reference/markers/crd.html
最初发表在我的博客上:https://sklar.rocks/kubebuilder-tips/